diff --git a/v2/auth.go b/v2/auth.go index efcd5526..30d4a613 100644 --- a/v2/auth.go +++ b/v2/auth.go @@ -46,14 +46,15 @@ type Opts struct { DisableIAT bool // disable IssuedAt claim // optional (custom) names for cookies and headers - JWTCookieName string // default "JWT" - JWTCookieDomain string // default empty - JWTHeaderKey string // default "X-JWT" - XSRFCookieName string // default "XSRF-TOKEN" - XSRFHeaderKey string // default "X-XSRF-TOKEN" - JWTQuery string // default "token" - SendJWTHeader bool // if enabled send JWT as a header instead of cookie - SameSiteCookie http.SameSite // limit cross-origin requests with SameSite cookie attribute + JWTCookieName string // default "JWT" + JWTCookieDomain string // default empty + JWTHeaderKey string // default "X-JWT" + XSRFCookieName string // default "XSRF-TOKEN" + XSRFHeaderKey string // default "X-XSRF-TOKEN" + XSRFIgnoreMethods []string // disable XSRF protection for the specified request methods (ex. []string{"GET", "POST")}, default empty + JWTQuery string // default "token" + 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/v2/token/jwt.go b/v2/token/jwt.go index c899b070..73cc1c2c 100644 --- a/v2/token/jwt.go +++ b/v2/token/jwt.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "net/http" + "slices" "strings" "time" @@ -49,6 +50,10 @@ const ( defaultTokenQuery = "token" ) +var ( + defaultXSRFIgnoreMethods = []string{} +) + // Opts holds constructor params type Opts struct { SecretReader Secret @@ -59,17 +64,18 @@ type Opts struct { DisableXSRF bool DisableIAT bool // disable IssuedAt claim // optional (custom) names for cookies and headers - JWTCookieName string - JWTCookieDomain string - JWTHeaderKey string - 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 - SameSite http.SameSite // define a cookie attribute making it impossible for the browser to send this cookie cross-site + JWTCookieName string + JWTCookieDomain string + JWTHeaderKey string + XSRFCookieName string + XSRFHeaderKey string + XSRFIgnoreMethods []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 + SameSite http.SameSite // define a cookie attribute making it impossible for the browser to send this cookie cross-site } // NewService makes JWT service @@ -90,6 +96,10 @@ func NewService(opts Opts) *Service { setDefault(&res.Issuer, defaultIssuer) setDefault(&res.JWTCookieDomain, defaultJWTCookieDomain) + if opts.XSRFIgnoreMethods == nil { + opts.XSRFIgnoreMethods = defaultXSRFIgnoreMethods + } + if opts.TokenDuration == 0 { res.TokenDuration = defaultTokenDuration } @@ -293,7 +303,7 @@ func (j *Service) Get(r *http.Request) (Claims, string, error) { return Claims{}, "", fmt.Errorf("token expired") } - if j.DisableXSRF { + if j.DisableXSRF || slices.Contains(j.XSRFIgnoreMethods, r.Method) { return claims, tokenString, nil } diff --git a/v2/token/jwt_test.go b/v2/token/jwt_test.go index e30ee137..7b111588 100644 --- a/v2/token/jwt_test.go +++ b/v2/token/jwt_test.go @@ -471,6 +471,53 @@ func TestJWT_SetAndGetWithXsrfMismatch(t *testing.T) { assert.Equal(t, claims, c) } +func TestJWT_GetWithXsrfMismatchOnIgnoredMethod(t *testing.T) { + j := NewService(Opts{SecretReader: SecretFunc(mockKeyStore), SecureCookies: false, + TokenDuration: time.Hour, CookieDuration: days31, + JWTCookieName: jwtCustomCookieName, JWTHeaderKey: jwtCustomHeaderKey, + XSRFCookieName: xsrfCustomCookieName, XSRFHeaderKey: xsrfCustomHeaderKey, + ClaimsUpd: ClaimsUpdFunc(func(claims Claims) Claims { + claims.User.SetStrAttr("stra", "stra-val") + claims.User.SetBoolAttr("boola", true) + return claims + }), + Issuer: "remark42", + DisableIAT: true, + }) + + claims := testClaims + claims.SessionOnly = true + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/valid" { + _, e := j.Set(w, claims) + require.NoError(t, e) + w.WriteHeader(200) + } + })) + defer ts.Close() + + resp, err := http.Get(ts.URL + "/valid") + require.Nil(t, err) + assert.Equal(t, 200, resp.StatusCode) + + j.XSRFIgnoreMethods = []string{"GET"} + req := httptest.NewRequest("GET", "/valid", nil) + req.AddCookie(resp.Cookies()[0]) + req.Header.Add(xsrfCustomHeaderKey, "random id wrong") + _, _, err = j.Get(req) + require.NoError(t, err, "xsrf mismatch, but ignored") + + j.DisableXSRF = true + j.XSRFIgnoreMethods = []string{} + req = httptest.NewRequest("GET", "/valid", nil) + req.AddCookie(resp.Cookies()[0]) + req.Header.Add(xsrfCustomHeaderKey, "random id wrong") + c, _, err := j.Get(req) + require.NoError(t, err, "xsrf mismatch, but ignored") + claims.User.Audience = c.Audience // set aud to user because we don't do the normal Get call + assert.Equal(t, claims, c) +} + func TestJWT_SetAndGetWithCookiesExpired(t *testing.T) { j := NewService(Opts{SecretReader: SecretFunc(mockKeyStore), SecureCookies: false, TokenDuration: time.Hour, CookieDuration: days31,