From aff79a6fa903cc5df817af67050948a604d5988d Mon Sep 17 00:00:00 2001 From: Umputun Date: Thu, 14 Jan 2021 13:06:29 -0600 Subject: [PATCH] Same site (#850) * switch to auth master for same-site * add same-site policy support #784 * add parse same site param --- README.md | 2 ++ backend/app/cmd/server.go | 29 +++++++++++++++---- backend/app/cmd/server_test.go | 23 +++++++++++++++ backend/go.mod | 2 +- backend/go.sum | 2 ++ .../vendor/github.com/go-pkgz/auth/auth.go | 3 +- .../github.com/go-pkgz/auth/token/jwt.go | 17 ++++++----- backend/vendor/modules.txt | 2 +- 8 files changed, 64 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index efeab781..1ae588c6 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,8 @@ _this is the recommended way to run remark42_ | image.resize-height | IMAGE_RESIZE_HEIGHT | `900` | height of resized image | | auth.ttl.jwt | AUTH_TTL_JWT | `5m` | jwt TTL | | auth.ttl.cookie | AUTH_TTL_COOKIE | `200h` | cookie TTL | +| auth.send-jwt-header | AUTH_SEND_JWT_HEADER | `false` | send JWT as a header instead of cookie | +| auth.same-site | AUTH_SAME_SITE | `default` | set same site policy for cookies (`default`, `none`, `lax` or `strict`)| | auth.google.cid | AUTH_GOOGLE_CID | | Google OAuth client ID | | auth.google.csec | AUTH_GOOGLE_CSEC | | Google OAuth client secret | | auth.facebook.cid | AUTH_FACEBOOK_CID | | Facebook OAuth client ID | diff --git a/backend/app/cmd/server.go b/backend/app/cmd/server.go index e15bec31..aad76e28 100644 --- a/backend/app/cmd/server.go +++ b/backend/app/cmd/server.go @@ -79,10 +79,13 @@ type ServerCommand struct { Auth struct { TTL struct { - JWT time.Duration `long:"jwt" env:"JWT" default:"5m" description:"jwt TTL"` - SendJWTHeader bool `long:"send-jwt-header" env:"SEND_JWT_HEADER" description:"send JWT as a header instead of cookie"` - Cookie time.Duration `long:"cookie" env:"COOKIE" default:"200h" description:"auth cookie TTL"` + JWT time.Duration `long:"jwt" env:"JWT" default:"5m" description:"jwt TTL"` + Cookie time.Duration `long:"cookie" env:"COOKIE" default:"200h" description:"auth cookie TTL"` } `group:"ttl" namespace:"ttl" env-namespace:"TTL"` + + SendJWTHeader bool `long:"send-jwt-header" env:"SEND_JWT_HEADER" description:"send JWT as a header instead of cookie"` + SameSite string `long:"same-site" env:"SAME_SITE" description:"set same site policy for cookies" choice:"default" choice:"none" choice:"lax" choice:"strict" default:"default"` // nolint + Google AuthGroup `group:"google" namespace:"google" env-namespace:"GOOGLE" description:"Google OAuth"` Github AuthGroup `group:"github" namespace:"github" env-namespace:"GITHUB" description:"Github OAuth"` Facebook AuthGroup `group:"facebook" namespace:"facebook" env-namespace:"FACEBOOK" description:"Facebook OAuth"` @@ -445,7 +448,7 @@ func (s *ServerCommand) newServerApp() (*serverApp, error) { SimpleView: s.SimpleView, ProxyCORS: s.ProxyCORS, AllowedAncestors: s.AllowedHosts, - SendJWTHeader: s.Auth.TTL.SendJWTHeader, + SendJWTHeader: s.Auth.SendJWTHeader, } srv.ScoreThresholds.Low, srv.ScoreThresholds.Critical = s.LowScore, s.CriticalScore @@ -899,8 +902,9 @@ func (s *ServerCommand) makeAuthenticator(ds *service.DataStore, avas avatar.Sto URL: strings.TrimSuffix(s.RemarkURL, "/"), Issuer: "remark42", TokenDuration: s.Auth.TTL.JWT, - SendJWTHeader: s.Auth.TTL.SendJWTHeader, CookieDuration: s.Auth.TTL.Cookie, + SendJWTHeader: s.Auth.SendJWTHeader, + SameSiteCookie: s.parseSameSite(s.Auth.SameSite), SecureCookies: strings.HasPrefix(s.RemarkURL, "https://"), SecretReader: token.SecretFunc(func(aud string) (string, error) { // get secret per site return admns.Key() @@ -960,6 +964,21 @@ func (s *ServerCommand) makeAuthenticator(ds *service.DataStore, avas avatar.Sto return authenticator, nil } +func (s *ServerCommand) parseSameSite(ss string) http.SameSite { + switch strings.ToLower(ss) { + case "default": + return http.SameSiteDefaultMode + case "none": + return http.SameSiteNoneMode + case "lax": + return http.SameSiteLaxMode + case "strict": + return http.SameSiteStrictMode + default: + return http.SameSiteDefaultMode + } +} + // authRefreshCache used by authenticator to minimize repeatable token refreshes type authRefreshCache struct { cache.LoadingCache diff --git a/backend/app/cmd/server_test.go b/backend/app/cmd/server_test.go index 9faaef3f..d425a251 100644 --- a/backend/app/cmd/server_test.go +++ b/backend/app/cmd/server_test.go @@ -586,6 +586,29 @@ func TestServer_loadEmailTemplate(t *testing.T) { assert.Equal(t, r, "") } +func TestServerCommand_parseSameSite(t *testing.T) { + + tbl := []struct { + inp string + res http.SameSite + }{ + {"", http.SameSiteDefaultMode}, + {"default", http.SameSiteDefaultMode}, + {"blah", http.SameSiteDefaultMode}, + {"none", http.SameSiteNoneMode}, + {"lax", http.SameSiteLaxMode}, + {"strict", http.SameSiteStrictMode}, + } + + cmd := ServerCommand{} + for i, tt := range tbl { + tt := tt + t.Run(strconv.Itoa(i), func(t *testing.T) { + assert.Equal(t, tt.res, cmd.parseSameSite(tt.inp)) + }) + } +} + func chooseRandomUnusedPort() (port int) { for i := 0; i < 10; i++ { port = 40000 + int(rand.Int31n(10000)) diff --git a/backend/go.mod b/backend/go.mod index fdbbf05a..ab4ca840 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -12,7 +12,7 @@ require ( github.com/go-chi/chi v4.1.1+incompatible github.com/go-chi/cors v1.1.1 github.com/go-chi/render v1.0.1 - github.com/go-pkgz/auth v1.13.1 + github.com/go-pkgz/auth v1.13.2-0.20210114092942-d2b22c3f3881 github.com/go-pkgz/jrpc v0.2.0 github.com/go-pkgz/lcw v0.8.1 github.com/go-pkgz/lgr v0.10.4 diff --git a/backend/go.sum b/backend/go.sum index 20942744..fd8fdedc 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -60,6 +60,8 @@ github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8= github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= github.com/go-pkgz/auth v1.13.1 h1:+N9f2fSbvczMzOEaiGSxz1018JtvIaVM017GbmF7dF4= github.com/go-pkgz/auth v1.13.1/go.mod h1:1GVd61pXZcuJ0ZnOUdCTY08V8SreO7MJtsvEd5/WEWA= +github.com/go-pkgz/auth v1.13.2-0.20210114092942-d2b22c3f3881 h1:BlRK66nY8N7xkV51kQ0XEQD3No4Txk6QGedza5uaaGI= +github.com/go-pkgz/auth v1.13.2-0.20210114092942-d2b22c3f3881/go.mod h1:1GVd61pXZcuJ0ZnOUdCTY08V8SreO7MJtsvEd5/WEWA= github.com/go-pkgz/expirable-cache v0.0.3 h1:rTh6qNPp78z0bQE6HDhXBHUwqnV9i09Vm6dksJLXQDc= github.com/go-pkgz/expirable-cache v0.0.3/go.mod h1:+IauqN00R2FqNRLCLA+X5YljQJrwB179PfiAoMPlTlQ= github.com/go-pkgz/jrpc v0.2.0 h1:CLy/eZyekjraVrxZV18N2R1mYLMJ/nWrgdfyIOGPY/E= diff --git a/backend/vendor/github.com/go-pkgz/auth/auth.go b/backend/vendor/github.com/go-pkgz/auth/auth.go index 4953f46e..6e0b16d1 100644 --- a/backend/vendor/github.com/go-pkgz/auth/auth.go +++ b/backend/vendor/github.com/go-pkgz/auth/auth.go @@ -53,7 +53,8 @@ type Opts struct { XSRFHeaderKey string // default "X-XSRF-TOKEN" JWTQuery string // default "token" - SendJWTHeader bool // if enabled send JWT as a header instead of cookie + SendJWTHeader bool // if enabled send JWT as a header instead of cookie + SameSiteCookie http.SameSite // limit cross-origin requests with SameSite cookie attribute Issuer string // optional value for iss claim, usually the application name, default "go-pkgz/auth" diff --git a/backend/vendor/github.com/go-pkgz/auth/token/jwt.go b/backend/vendor/github.com/go-pkgz/auth/token/jwt.go index 99e5acfd..8e0cd3e4 100644 --- a/backend/vendor/github.com/go-pkgz/auth/token/jwt.go +++ b/backend/vendor/github.com/go-pkgz/auth/token/jwt.go @@ -63,10 +63,11 @@ type Opts struct { XSRFCookieName string XSRFHeaderKey string JWTQuery string - AudienceReader Audience // allowed aud values - Issuer string // optional value for iss claim, usually application name - AudSecrets bool // uses different secret for differed auds. important: adds pre-parsing of unverified token - SendJWTHeader bool // if enabled send JWT as a header instead of cookie + AudienceReader Audience // allowed aud values + Issuer string // optional value for iss claim, usually application name + AudSecrets bool // uses different secret for differed auds. important: adds pre-parsing of unverified token + SendJWTHeader bool // if enabled send JWT as a header instead of cookie + SameSite http.SameSite // define a cookie attribute making it impossible for the browser to send this cookie cross-site } // NewService makes JWT service @@ -238,11 +239,11 @@ func (j *Service) Set(w http.ResponseWriter, claims Claims) (Claims, error) { } jwtCookie := http.Cookie{Name: j.JWTCookieName, Value: tokenString, HttpOnly: true, Path: "/", - MaxAge: cookieExpiration, Secure: j.SecureCookies} + MaxAge: cookieExpiration, Secure: j.SecureCookies, SameSite: j.SameSite} http.SetCookie(w, &jwtCookie) xsrfCookie := http.Cookie{Name: j.XSRFCookieName, Value: claims.Id, HttpOnly: false, Path: "/", - MaxAge: cookieExpiration, Secure: j.SecureCookies} + MaxAge: cookieExpiration, Secure: j.SecureCookies, SameSite: j.SameSite} http.SetCookie(w, &xsrfCookie) return claims, nil @@ -311,11 +312,11 @@ func (j *Service) IsExpired(claims Claims) bool { // Reset token's cookies func (j *Service) Reset(w http.ResponseWriter) { jwtCookie := http.Cookie{Name: j.JWTCookieName, Value: "", HttpOnly: false, Path: "/", - MaxAge: -1, Expires: time.Unix(0, 0), Secure: j.SecureCookies} + MaxAge: -1, Expires: time.Unix(0, 0), Secure: j.SecureCookies, SameSite: j.SameSite} http.SetCookie(w, &jwtCookie) xsrfCookie := http.Cookie{Name: j.XSRFCookieName, Value: "", HttpOnly: false, Path: "/", - MaxAge: -1, Expires: time.Unix(0, 0), Secure: j.SecureCookies} + MaxAge: -1, Expires: time.Unix(0, 0), Secure: j.SecureCookies, SameSite: j.SameSite} http.SetCookie(w, &xsrfCookie) } diff --git a/backend/vendor/modules.txt b/backend/vendor/modules.txt index 49a97417..632c4371 100644 --- a/backend/vendor/modules.txt +++ b/backend/vendor/modules.txt @@ -90,7 +90,7 @@ github.com/go-chi/cors # github.com/go-chi/render v1.0.1 ## explicit github.com/go-chi/render -# github.com/go-pkgz/auth v1.13.1 +# github.com/go-pkgz/auth v1.13.2-0.20210114092942-d2b22c3f3881 ## explicit github.com/go-pkgz/auth github.com/go-pkgz/auth/avatar