From e5dc89b6ee4e148d73599a95e91c4132d724d266 Mon Sep 17 00:00:00 2001 From: nor Date: Sun, 17 Mar 2024 22:17:13 +0800 Subject: [PATCH 1/4] feat: allow authentication using proxy request header --- internal/cmd/server.go | 6 +++ internal/config/config.go | 16 ++++---- internal/dependencies/dependencies.go | 9 +++-- internal/http/middleware/auth.go | 57 +++++++++++++++++++++++++++ internal/http/middleware/cidrs.go | 51 ++++++++++++++++++++++++ internal/http/server.go | 2 + internal/webserver/handler.go | 31 +++++++++------ 7 files changed, 148 insertions(+), 24 deletions(-) create mode 100644 internal/http/middleware/cidrs.go diff --git a/internal/cmd/server.go b/internal/cmd/server.go index f8ccc47cf..b14b3daf2 100644 --- a/internal/cmd/server.go +++ b/internal/cmd/server.go @@ -23,6 +23,8 @@ func newServerCommand() *cobra.Command { cmd.Flags().Bool("access-log", false, "Print out a non-standard access log") cmd.Flags().Bool("serve-web-ui", true, "Serve static files from the webroot path") cmd.Flags().String("secret-key", "", "Secret key used for encrypting session data") + cmd.Flags().StringSlice("trusted-proxies", []string{}, "list of trusted proxy IPs, empty means no proxy allowed") + cmd.Flags().String("reverse-proxy-auth-user", "", "http header name of proxy auth") return cmd } @@ -38,6 +40,8 @@ func newServerCommandHandler() func(cmd *cobra.Command, args []string) { accessLog, _ := cmd.Flags().GetBool("access-log") serveWebUI, _ := cmd.Flags().GetBool("serve-web-ui") secretKey, _ := cmd.Flags().GetBytesHex("secret-key") + trustedProxies, _ := cmd.Flags().GetStringSlice("trusted-proxies") + reverseProxyAuthUser, _ := cmd.Flags().GetString("reverse-proxy-auth-user") cfg, dependencies := initShiori(ctx, cmd) @@ -61,6 +65,8 @@ func newServerCommandHandler() func(cmd *cobra.Command, args []string) { cfg.Http.AccessLog = accessLog cfg.Http.ServeWebUI = serveWebUI cfg.Http.SecretKey = secretKey + cfg.Http.TrustedProxies = trustedProxies + cfg.Http.ReverseProxyAuthUser = reverseProxyAuthUser dependencies.Log.Infof("Starting Shiori v%s", model.BuildVersion) diff --git a/internal/config/config.go b/internal/config/config.go index 390b642d6..b01e20316 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -49,13 +49,15 @@ func readDotEnv(logger *logrus.Logger) map[string]string { } type HttpConfig struct { - Enabled bool `env:"HTTP_ENABLED,default=True"` - Port int `env:"HTTP_PORT,default=8080"` - Address string `env:"HTTP_ADDRESS,default=:"` - RootPath string `env:"HTTP_ROOT_PATH,default=/"` - AccessLog bool `env:"HTTP_ACCESS_LOG,default=True"` - ServeWebUI bool `env:"HTTP_SERVE_WEB_UI,default=True"` - SecretKey []byte `env:"HTTP_SECRET_KEY"` + Enabled bool `env:"HTTP_ENABLED,default=True"` + Port int `env:"HTTP_PORT,default=8080"` + Address string `env:"HTTP_ADDRESS,default=:"` + RootPath string `env:"HTTP_ROOT_PATH,default=/"` + AccessLog bool `env:"HTTP_ACCESS_LOG,default=True"` + ServeWebUI bool `env:"HTTP_SERVE_WEB_UI,default=True"` + SecretKey []byte `env:"HTTP_SECRET_KEY"` + TrustedProxies []string `env:"TRUSTED_PROXIES"` + ReverseProxyAuthUser string `env:"REVERSE_PROXY_AUTH_USER"` // Fiber Specific BodyLimit int `env:"HTTP_BODY_LIMIT,default=1024"` ReadTimeout time.Duration `env:"HTTP_READ_TIMEOUT,default=10s"` diff --git a/internal/dependencies/dependencies.go b/internal/dependencies/dependencies.go index 644607515..811b5edaf 100644 --- a/internal/dependencies/dependencies.go +++ b/internal/dependencies/dependencies.go @@ -8,10 +8,11 @@ import ( ) type Domains struct { - Archiver model.ArchiverDomain - Auth model.AccountsDomain - Bookmarks model.BookmarksDomain - Storage model.StorageDomain + Archiver model.ArchiverDomain + Auth model.AccountsDomain + Bookmarks model.BookmarksDomain + Storage model.StorageDomain + LegacyLogin model.LegacyLoginHandler } type Dependencies struct { diff --git a/internal/http/middleware/auth.go b/internal/http/middleware/auth.go index cb3da436f..10f313181 100644 --- a/internal/http/middleware/auth.go +++ b/internal/http/middleware/auth.go @@ -3,6 +3,7 @@ package middleware import ( "net/http" "strings" + "time" "github.com/gin-gonic/gin" "github.com/go-shiori/shiori/internal/dependencies" @@ -20,6 +21,9 @@ func AuthMiddleware(deps *dependencies.Dependencies) gin.HandlerFunc { if token == "" { token = getTokenFromCookie(c) } + if token == "" { + token = getTokenFromAuthHeader(c, deps) + } account, err := deps.Domains.Auth.CheckToken(c, token) if err != nil { @@ -42,6 +46,59 @@ func AuthenticationRequired() gin.HandlerFunc { } } +// getAuth user from oauth proxy, if any +func getTokenFromAuthHeader(c *gin.Context, deps *dependencies.Dependencies) string { + if deps.Config.Http.ReverseProxyAuthUser == "" { + deps.Log.Debugf("auth proxy: reverse-proxy-auth-user not set") + return "" + } + authUser := c.GetHeader(deps.Config.Http.ReverseProxyAuthUser) + if authUser == "" { + deps.Log.Debugf("auth proxy: can not get user header from proxy") + return "" + } + remoteAddr := c.ClientIP() + deps.Log.Debugf("auth proxy: got auth user (%s), client ip (%s)", authUser, remoteAddr) + cidrs, err := newCIDRs(deps.Config.Http.TrustedProxies) + if err != nil { + deps.Log.Errorf("auth proxy: trusted proxy config error (%v)", err) + return "" + } + canTrustProxy := false + if len(deps.Config.Http.TrustedProxies) == 0 || cidrs.ContainStringIP(remoteAddr) { + canTrustProxy = true + } + if canTrustProxy { + account, exit, err := deps.Database.GetAccount(c, authUser) + if err == nil && exit { + token, err := deps.Domains.Auth.CreateTokenForAccount(&account, time.Now().Add(time.Hour*24*365)) + if err != nil { + deps.Log.Errorf("auth proxy: create token error %v", err) + return "" + } + sessionId, err := deps.Domains.LegacyLogin(account, time.Hour*24*30) + if err != nil { + deps.Log.Errorf("auth proxy: create session error %v", err) + return "" + } + deps.Log.Debugf("auth proxy: write session %s token %s", sessionId, token) + sessionCookie := &http.Cookie{Name: "session-id", Value: sessionId} + http.SetCookie(c.Writer, sessionCookie) + c.Request.AddCookie(sessionCookie) + tokenCookie := &http.Cookie{Name: "token", Value: token} + http.SetCookie(c.Writer, tokenCookie) + c.Request.AddCookie(tokenCookie) + + return token + } else { + deps.Log.Warnf("auth proxy: no such user (%s) or error %v", authUser, err) + } + } else if authUser != "" { + deps.Log.Warnf("auth proxy: invalid auth request from %s", remoteAddr) + } + return "" +} + // getTokenFromHeader returns the token from the Authorization header, if any. func getTokenFromHeader(c *gin.Context) string { authorization := c.GetHeader(model.AuthorizationHeader) diff --git a/internal/http/middleware/cidrs.go b/internal/http/middleware/cidrs.go new file mode 100644 index 000000000..de38e6b10 --- /dev/null +++ b/internal/http/middleware/cidrs.go @@ -0,0 +1,51 @@ +package middleware + +import ( + "net" + "strings" +) + +type CIDRs struct { + elements []*net.IPNet +} + +func newCIDRs(addresses []string) (*CIDRs, error) { + cidr := make([]*net.IPNet, 0, len(addresses)) + for _, addr := range addresses { + if !strings.Contains(addr, "/") { + ip := net.ParseIP(addr) + if ip == nil { + return nil, &net.ParseError{Type: "IP address", Text: addr} + } + if ip.To4() == nil { + addr += "/128" + } else { + addr += "/32" + } + } + _, cidrNet, err := net.ParseCIDR(addr) + if err != nil { + return nil, err + } + cidr = append(cidr, cidrNet) + } + return &CIDRs{elements: cidr}, nil +} + +func (c *CIDRs) Len() int { + return len(c.elements) +} + +func (c *CIDRs) ContainIP(ip net.IP) bool { + for _, el := range c.elements { + if el.Contains(ip) { + return true + } + } + + return false +} + +func (c *CIDRs) ContainStringIP(ip string) bool { + return c.ContainIP(net.ParseIP(ip)) +} diff --git a/internal/http/server.go b/internal/http/server.go index b1fa63f08..a2099ece2 100644 --- a/internal/http/server.go +++ b/internal/http/server.go @@ -57,6 +57,8 @@ func (s *HttpServer) Setup(cfg *config.Config, deps *dependencies.Dependencies) // package. legacyRoutes := routes.NewLegacyAPIRoutes(s.logger, deps, cfg) legacyRoutes.Setup(s.engine) + // used for auth header save session + deps.Domains.LegacyLogin = legacyRoutes.HandleLogin s.handle("/system", routes.NewSystemRoutes(s.logger)) s.handle("/bookmark", routes.NewBookmarkRoutes(s.logger, deps)) diff --git a/internal/webserver/handler.go b/internal/webserver/handler.go index 7a625b5d9..cde8d2127 100644 --- a/internal/webserver/handler.go +++ b/internal/webserver/handler.go @@ -111,28 +111,33 @@ func (h *Handler) validateSession(r *http.Request) error { return fmt.Errorf("session has been expired") } - account, err := h.dependencies.Domains.Auth.CheckToken(r.Context(), authParts[1]) - if err != nil { - return fmt.Errorf("session has been expired") - } + // authelia maybe return an null AuthorizationHeader header, ignore exception + if authParts[1] != "null" { - if r.Method != "" && r.Method != "GET" && !account.Owner { - return fmt.Errorf("account level is not sufficient") - } + account, err := h.dependencies.Domains.Auth.CheckToken(r.Context(), authParts[1]) + if err != nil { + return fmt.Errorf("session has been expired") + } + + if r.Method != "" && r.Method != "GET" && !account.Owner { + return fmt.Errorf("account level is not sufficient") + } - h.dependencies.Log.WithFields(logrus.Fields{ - "username": account.Username, - "method": r.Method, - "path": r.URL.Path, - }).Info("allowing legacy api access using JWT token") + h.dependencies.Log.WithFields(logrus.Fields{ + "username": account.Username, + "method": r.Method, + "path": r.URL.Path, + }).Info("allowing legacy api access using JWT token") - return nil + return nil + } } sessionID := h.GetSessionID(r) if sessionID == "" { return fmt.Errorf("session is not exist") } + h.dependencies.Log.Debugf("get session id is %s", sessionID) // Make sure session is not expired yet val, found := h.SessionCache.Get(sessionID) From d5c39ad0496361b9102c65222a88dc2946ae49ca Mon Sep 17 00:00:00 2001 From: nor Date: Mon, 18 Mar 2024 16:29:15 +0800 Subject: [PATCH 2/4] update index page --- internal/cmd/server.go | 6 ------ internal/view/index.html | 25 ++++++++++++++++++++++++- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/internal/cmd/server.go b/internal/cmd/server.go index b14b3daf2..f8ccc47cf 100644 --- a/internal/cmd/server.go +++ b/internal/cmd/server.go @@ -23,8 +23,6 @@ func newServerCommand() *cobra.Command { cmd.Flags().Bool("access-log", false, "Print out a non-standard access log") cmd.Flags().Bool("serve-web-ui", true, "Serve static files from the webroot path") cmd.Flags().String("secret-key", "", "Secret key used for encrypting session data") - cmd.Flags().StringSlice("trusted-proxies", []string{}, "list of trusted proxy IPs, empty means no proxy allowed") - cmd.Flags().String("reverse-proxy-auth-user", "", "http header name of proxy auth") return cmd } @@ -40,8 +38,6 @@ func newServerCommandHandler() func(cmd *cobra.Command, args []string) { accessLog, _ := cmd.Flags().GetBool("access-log") serveWebUI, _ := cmd.Flags().GetBool("serve-web-ui") secretKey, _ := cmd.Flags().GetBytesHex("secret-key") - trustedProxies, _ := cmd.Flags().GetStringSlice("trusted-proxies") - reverseProxyAuthUser, _ := cmd.Flags().GetString("reverse-proxy-auth-user") cfg, dependencies := initShiori(ctx, cmd) @@ -65,8 +61,6 @@ func newServerCommandHandler() func(cmd *cobra.Command, args []string) { cfg.Http.AccessLog = accessLog cfg.Http.ServeWebUI = serveWebUI cfg.Http.SecretKey = secretKey - cfg.Http.TrustedProxies = trustedProxies - cfg.Http.ReverseProxyAuthUser = reverseProxyAuthUser dependencies.Log.Infof("Starting Shiori v%s", model.BuildVersion) diff --git a/internal/view/index.html b/internal/view/index.html index 0f120d794..3d0a5d827 100644 --- a/internal/view/index.html +++ b/internal/view/index.html @@ -141,9 +141,32 @@ username: username, owner: owner, }; - } + }, + loadToken() { + function parseJWT(token) { + try { + return JSON.parse(atob(token.split('.')[1])); + } catch (e) { + return null; + } + } + function getCookie(cname){ + var name = cname + "="; + var ca = document.cookie.split(';'); + for(var i=0; i Date: Mon, 18 Mar 2024 16:40:34 +0800 Subject: [PATCH 3/4] update config readme --- docs/Configuration.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/Configuration.md b/docs/Configuration.md index fdaf4e7e9..4c8927206 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -42,6 +42,8 @@ Most configuration can be set directly using environment variables or flags. The | `SHIORI_HTTP_IDLE_TIMEOUT` | 10s | No | Maximum amount of time to wait for the next request | | `SHIORI_HTTP_DISABLE_KEEP_ALIVE` | true | No | Disable HTTP keep-alive connections | | `SHIORI_HTTP_DISABLE_PARSE_MULTIPART_FORM` | true | No | Disable pre-parsing of multipart form | +| `SHIORI_TRUSTED_PROXIES` | "" | No | oidc trust proxy ip address | +| `SHIORI_REVERSE_PROXY_AUTH_USER` | "" | No | user id key of oidc auth header, ie. Remote-User | ### Storage Configuration From c66eba40d0b5af557d67088e132ad5cb392197ae Mon Sep 17 00:00:00 2001 From: nor Date: Sat, 27 Apr 2024 15:34:46 +0800 Subject: [PATCH 4/4] clean token cookie with auth header fail --- internal/webserver/handler-api.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/internal/webserver/handler-api.go b/internal/webserver/handler-api.go index 3772459bd..c0c89122b 100644 --- a/internal/webserver/handler-api.go +++ b/internal/webserver/handler-api.go @@ -63,6 +63,16 @@ func (h *Handler) ApiGetBookmarks(w http.ResponseWriter, r *http.Request, ps htt // Make sure session still valid err := h.validateSession(r) + if err != nil { + c := &http.Cookie{ + Name: "token", + Value: "", + Path: "/", + MaxAge: -1, + HttpOnly: true, + } + http.SetCookie(w, c) + } checkError(err) // Get URL queries