Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement XSRFIgnoreMethods #207

Merged
merged 4 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading