Skip to content

Commit

Permalink
Merge pull request #207 from oalexander6/master
Browse files Browse the repository at this point in the history
Implement XSRFIgnoreMethods
  • Loading branch information
umputun authored Jul 25, 2024
2 parents cbbbd34 + 88b74a4 commit db26833
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 20 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,21 @@ For more details refer to [Complete Guide of Battle.net OAuth API and Login Butt
1. Fill **App name** and **Description** and **URL** of your site
1. In the field **Callback URLs** enter the correct url of your callback handler e.g. https://example.mysite.com/{route}/twitter/callback
1. Under **Key and tokens** take note of the **Consumer API Key** and **Consumer API Secret key**. Those will be used as `cid` and `csecret`

## XSRF Protections
By default, the XSRF protections will apply to all requests which reach the `middlewares.Auth`,
`middlewares.Admin` or `middlewares.RBAC` middlewares. This will require setting a request header
with a key of `<XSRFHeaderKey>` containing the value of the cookie named `<XSRFCookieName>`.

To disable all XSRF protections, set `DisableXSRF` to `true`. This should probably only be used
during testing or debugging.

When setting a custom request header is not possible, such as when building a web application which
is not a Single-Page-Application and HTML link tags are used to navigate pages, specific HTTP methods
may be excluded using the `XSRFIgnoreMethods` option. For example, to disable GET requests, set this
option to `XSRFIgnoreMethods: []string{"GET"}`. Adding methods other than GET to this list may result
in XSRF vulnerabilities.

## Status

The library extracted from [remark42](https://github.com/umputun/remark) project. The original code in production use on multiple sites and seems to work fine.
Expand Down
17 changes: 9 additions & 8 deletions auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
34 changes: 22 additions & 12 deletions token/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"slices"
"strings"
"time"

Expand Down Expand Up @@ -49,6 +50,10 @@ const (
defaultTokenQuery = "token"
)

var (
defaultXSRFIgnoreMethods = []string{}
)

// Opts holds constructor params
type Opts struct {
SecretReader Secret
Expand All @@ -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
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}

Expand Down
47 changes: 47 additions & 0 deletions token/jwt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit db26833

Please sign in to comment.