From 99e1df689b9508e92435eb73dbcd13725cd6e06c Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Thu, 28 Apr 2022 16:39:14 -0700 Subject: [PATCH 01/11] add reuse interval to config --- conf/configuration.go | 1 + 1 file changed, 1 insertion(+) diff --git a/conf/configuration.go b/conf/configuration.go index c99e840e61..b3a817e57d 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -181,6 +181,7 @@ type CaptchaConfiguration struct { type SecurityConfiguration struct { Captcha CaptchaConfiguration `json:"captcha"` RefreshTokenRotationEnabled bool `json:"refresh_token_rotation_enabled" split_words:"true" default:"true"` + RefreshTokenReuseInterval int `json:"refresh_token_reuse_interval" split_words:"true"` UpdatePasswordRequireReauthentication bool `json:"update_password_require_reauthentication" split_words:"true"` } From e33224619718891eb88a20c14c405c3e3bb15f35 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Thu, 28 Apr 2022 16:39:30 -0700 Subject: [PATCH 02/11] add test for refresh token reuse detection --- api/token_test.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/api/token_test.go b/api/token_test.go index eaa5dcb5fa..6a6c1e66b3 100644 --- a/api/token_test.go +++ b/api/token_test.go @@ -150,6 +150,32 @@ func (ts *TokenTestSuite) TestTokenRefreshTokenGrantFailure() { assert.Equal(ts.T(), http.StatusBadRequest, w.Code) } +func (ts *TokenTestSuite) TestTokenRefreshReuseDetection() { + u, err := models.NewUser(ts.instanceID, "foo@example.com", "password", ts.Config.JWT.Aud, nil) + require.NoError(ts.T(), err, "Error creating test user model") + t := time.Now() + u.EmailConfirmedAt = &t + require.NoError(ts.T(), ts.API.db.Create(u), "Error saving foo user") + + ts.Config.Security.RefreshTokenRotationEnabled = true + ts.Config.Security.RefreshTokenReuseInterval = 0 + oldRefreshToken, err := models.GrantAuthenticatedUser(ts.API.db, u) + require.NoError(ts.T(), err) + _, err = models.GrantRefreshTokenSwap(ts.API.db, u, oldRefreshToken) + require.NoError(ts.T(), err) + + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "refresh_token": oldRefreshToken.Token, + })) + req := httptest.NewRequest(http.MethodPost, "http://localhost/token?grant_type=refresh_token", &buffer) + req.Header.Set("Content-Type", "application/json") + + w := httptest.NewRecorder() + ts.API.handler.ServeHTTP(w, req) + assert.Equal(ts.T(), http.StatusBadRequest, w.Code) +} + func (ts *TokenTestSuite) createBannedUser() *models.User { u, err := models.NewUser(ts.instanceID, "banned@example.com", "password", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error creating test user model") From 2dc4c8668a863cd1ed63845b7ce91a3d5ac4ca1f Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Mon, 2 May 2022 14:29:27 -0700 Subject: [PATCH 03/11] fix: add reuse interval to refresh token grant --- api/token.go | 56 +++++++++++++++++++++++++++++------------ models/refresh_token.go | 28 ++++++++++++++++++--- 2 files changed, 65 insertions(+), 19 deletions(-) diff --git a/api/token.go b/api/token.go index edd86b8378..55385f5e0a 100644 --- a/api/token.go +++ b/api/token.go @@ -288,26 +288,48 @@ func (a *API) RefreshTokenGrant(ctx context.Context, w http.ResponseWriter, r *h } } + var newToken *models.RefreshToken if token.Revoked { - a.clearCookieTokens(config, w) - if config.Security.RefreshTokenRotationEnabled { - // Revoke all tokens in token family - err = a.db.Transaction(func(tx *storage.Connection) error { - var terr error - if terr = models.RevokeTokenFamily(tx, token); terr != nil { - return terr + err = a.db.Transaction(func(tx *storage.Connection) error { + validToken, terr := models.GetCurrentValidToken(tx, token) + if terr != nil { + return terr + } + // check if token is the last previous revoked token + if validToken.Parent == storage.NullString(token.Token) { + refreshTokenReuseWindow := token.UpdatedAt.Add(time.Second * time.Duration(config.Security.RefreshTokenReuseInterval)) + if time.Now().Before(refreshTokenReuseWindow) { + // if token is the last revoked token and is within the reuse interval + // just return the current valid refresh token + newToken = validToken } - return nil - }) - if err != nil { - return internalServerError(err.Error()) } + return nil + }) + if err != nil { + return internalServerError(err.Error()) + } + + if newToken == nil { + a.clearCookieTokens(config, w) + if config.Security.RefreshTokenRotationEnabled { + // Revoke all tokens in token family + err = a.db.Transaction(func(tx *storage.Connection) error { + var terr error + if terr = models.RevokeTokenFamily(tx, token); terr != nil { + return terr + } + return nil + }) + if err != nil { + return internalServerError(err.Error()) + } + } + return oauthError("invalid_grant", "Invalid Refresh Token").WithInternalMessage("Possible abuse attempt: %v", r) } - return oauthError("invalid_grant", "Invalid Refresh Token").WithInternalMessage("Possible abuse attempt: %v", r) } var tokenString string - var newToken *models.RefreshToken var newTokenResponse *AccessTokenResponse err = a.db.Transaction(func(tx *storage.Connection) error { @@ -316,9 +338,11 @@ func (a *API) RefreshTokenGrant(ctx context.Context, w http.ResponseWriter, r *h return terr } - newToken, terr = models.GrantRefreshTokenSwap(tx, user, token) - if terr != nil { - return internalServerError(terr.Error()) + if newToken == nil { + newToken, terr = models.GrantRefreshTokenSwap(tx, user, token) + if terr != nil { + return internalServerError(terr.Error()) + } } tokenString, terr = generateAccessToken(user, time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret) diff --git a/models/refresh_token.go b/models/refresh_token.go index 054806e4ea..96b1fff73e 100644 --- a/models/refresh_token.go +++ b/models/refresh_token.go @@ -1,6 +1,7 @@ package models import ( + "database/sql" "time" "github.com/gobuffalo/pop/v5" @@ -57,19 +58,40 @@ func GrantRefreshTokenSwap(tx *storage.Connection, user *User, token *RefreshTok // RevokeTokenFamily revokes all refresh tokens that descended from the provided token. func RevokeTokenFamily(tx *storage.Connection, token *RefreshToken) error { + tablename := (&pop.Model{Value: RefreshToken{}}).TableName() err := tx.RawQuery(` with recursive token_family as ( - select id, user_id, token, revoked, parent from refresh_tokens where parent = ? + select id, user_id, token, revoked, parent from `+tablename+` where parent = ? union - select r.id, r.user_id, r.token, r.revoked, r.parent from `+(&pop.Model{Value: RefreshToken{}}).TableName()+` r inner join token_family t on t.token = r.parent + select r.id, r.user_id, r.token, r.revoked, r.parent from `+tablename+` r inner join token_family t on t.token = r.parent ) - update `+(&pop.Model{Value: RefreshToken{}}).TableName()+` r set revoked = true from token_family where token_family.id = r.id;`, token.Token).Exec() + update `+tablename+` r set revoked = true from token_family where token_family.id = r.id;`, token.Token).Exec() if err != nil { return err } return nil } +// GetCurrentValidToken finds the most recent unrevoked token descending from the token provided. +func GetCurrentValidToken(tx *storage.Connection, token *RefreshToken) (*RefreshToken, error) { + tablename := (&pop.Model{Value: RefreshToken{}}).TableName() + refreshToken := &RefreshToken{} + err := tx.RawQuery(`with recursive token_family as ( + select id, user_id, token, revoked, parent from `+tablename+` where parent = ? + union + select r.id, r.user_id, r.token, r.revoked, r.parent from `+tablename+` r inner join token_family t on t.token = r.parent + ) + select * from token_family where id = (select max(id) from token_family) + `, token.Token).First(refreshToken) + if err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return nil, errors.Wrap(err, "no valid refresh token found") + } + return nil, err + } + return refreshToken, nil +} + // Logout deletes all refresh tokens for a user. func Logout(tx *storage.Connection, instanceID uuid.UUID, id uuid.UUID) error { return tx.RawQuery("DELETE FROM "+(&pop.Model{Value: RefreshToken{}}).TableName()+" WHERE instance_id = ? AND user_id = ?", instanceID, id).Exec() From dbbd55fcbb13afcccb966e16ead08f4c286b722b Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Mon, 2 May 2022 14:29:44 -0700 Subject: [PATCH 04/11] add tests for reuse interval --- api/token_test.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/api/token_test.go b/api/token_test.go index 6a6c1e66b3..de6cda54d0 100644 --- a/api/token_test.go +++ b/api/token_test.go @@ -176,6 +176,32 @@ func (ts *TokenTestSuite) TestTokenRefreshReuseDetection() { assert.Equal(ts.T(), http.StatusBadRequest, w.Code) } +func (ts *TokenTestSuite) TestTokenRefreshReuseInterval() { + u, err := models.NewUser(ts.instanceID, "foo@example.com", "password", ts.Config.JWT.Aud, nil) + require.NoError(ts.T(), err, "Error creating test user model") + t := time.Now() + u.EmailConfirmedAt = &t + require.NoError(ts.T(), ts.API.db.Create(u), "Error saving foo user") + + ts.Config.Security.RefreshTokenRotationEnabled = true + ts.Config.Security.RefreshTokenReuseInterval = 30 + oldRefreshToken, err := models.GrantAuthenticatedUser(ts.API.db, u) + require.NoError(ts.T(), err) + _, err = models.GrantRefreshTokenSwap(ts.API.db, u, oldRefreshToken) + require.NoError(ts.T(), err) + + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "refresh_token": oldRefreshToken.Token, + })) + req := httptest.NewRequest(http.MethodPost, "http://localhost/token?grant_type=refresh_token", &buffer) + req.Header.Set("Content-Type", "application/json") + + w := httptest.NewRecorder() + ts.API.handler.ServeHTTP(w, req) + assert.Equal(ts.T(), http.StatusOK, w.Code) +} + func (ts *TokenTestSuite) createBannedUser() *models.User { u, err := models.NewUser(ts.instanceID, "banned@example.com", "password", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error creating test user model") From a78df053a0fa3a66957ef43ee6b616be855b9198 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Mon, 2 May 2022 15:21:05 -0700 Subject: [PATCH 05/11] refactor token test --- api/token_test.go | 93 +++++++++++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 40 deletions(-) diff --git a/api/token_test.go b/api/token_test.go index de6cda54d0..56f2ac71ee 100644 --- a/api/token_test.go +++ b/api/token_test.go @@ -150,56 +150,69 @@ func (ts *TokenTestSuite) TestTokenRefreshTokenGrantFailure() { assert.Equal(ts.T(), http.StatusBadRequest, w.Code) } -func (ts *TokenTestSuite) TestTokenRefreshReuseDetection() { +func (ts *TokenTestSuite) TestTokenRefreshTokenRotation() { u, err := models.NewUser(ts.instanceID, "foo@example.com", "password", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error creating test user model") t := time.Now() u.EmailConfirmedAt = &t require.NoError(ts.T(), ts.API.db.Create(u), "Error saving foo user") - - ts.Config.Security.RefreshTokenRotationEnabled = true - ts.Config.Security.RefreshTokenReuseInterval = 0 - oldRefreshToken, err := models.GrantAuthenticatedUser(ts.API.db, u) - require.NoError(ts.T(), err) - _, err = models.GrantRefreshTokenSwap(ts.API.db, u, oldRefreshToken) - require.NoError(ts.T(), err) - - var buffer bytes.Buffer - require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ - "refresh_token": oldRefreshToken.Token, - })) - req := httptest.NewRequest(http.MethodPost, "http://localhost/token?grant_type=refresh_token", &buffer) - req.Header.Set("Content-Type", "application/json") - - w := httptest.NewRecorder() - ts.API.handler.ServeHTTP(w, req) - assert.Equal(ts.T(), http.StatusBadRequest, w.Code) -} - -func (ts *TokenTestSuite) TestTokenRefreshReuseInterval() { - u, err := models.NewUser(ts.instanceID, "foo@example.com", "password", ts.Config.JWT.Aud, nil) - require.NoError(ts.T(), err, "Error creating test user model") - t := time.Now() - u.EmailConfirmedAt = &t - require.NoError(ts.T(), ts.API.db.Create(u), "Error saving foo user") - - ts.Config.Security.RefreshTokenRotationEnabled = true - ts.Config.Security.RefreshTokenReuseInterval = 30 oldRefreshToken, err := models.GrantAuthenticatedUser(ts.API.db, u) require.NoError(ts.T(), err) - _, err = models.GrantRefreshTokenSwap(ts.API.db, u, oldRefreshToken) + newRefreshToken, err := models.GrantRefreshTokenSwap(ts.API.db, u, oldRefreshToken) require.NoError(ts.T(), err) - var buffer bytes.Buffer - require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ - "refresh_token": oldRefreshToken.Token, - })) - req := httptest.NewRequest(http.MethodPost, "http://localhost/token?grant_type=refresh_token", &buffer) - req.Header.Set("Content-Type", "application/json") + cases := []struct { + desc string + refreshTokenRotationEnabled bool + reuseInterval int + refreshToken string + expectedCode int + expectedBody map[string]interface{} + }{ + { + "Reuse detection enabled, 30s reuse interval", + true, + 30, + oldRefreshToken.Token, + http.StatusOK, + map[string]interface{}{ + "refresh_token": newRefreshToken.Token, + }, + }, + { + "Reuse detection enabled, no reuse interval", + true, + 0, + oldRefreshToken.Token, + http.StatusBadRequest, + map[string]interface{}{ + "error": "invalid_grant", + "error_description": "Invalid Refresh Token", + }, + }, + } - w := httptest.NewRecorder() - ts.API.handler.ServeHTTP(w, req) - assert.Equal(ts.T(), http.StatusOK, w.Code) + for _, c := range cases { + ts.Run(c.desc, func() { + ts.Config.Security.RefreshTokenRotationEnabled = c.refreshTokenRotationEnabled + ts.Config.Security.RefreshTokenReuseInterval = c.reuseInterval + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "refresh_token": c.refreshToken, + })) + req := httptest.NewRequest(http.MethodPost, "http://localhost/token?grant_type=refresh_token", &buffer) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + ts.API.handler.ServeHTTP(w, req) + assert.Equal(ts.T(), c.expectedCode, w.Code) + + data := make(map[string]interface{}) + require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data)) + for k, v := range c.expectedBody { + require.Equal(ts.T(), v, data[k]) + } + }) + } } func (ts *TokenTestSuite) createBannedUser() *models.User { From 85b15137702c03f8e4129402914b00691ed5bf52 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Mon, 2 May 2022 17:04:59 -0700 Subject: [PATCH 06/11] ignore reuse interval if revoked token is last token --- api/token.go | 10 ++++++---- models/refresh_token.go | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/api/token.go b/api/token.go index 55385f5e0a..2d783aec25 100644 --- a/api/token.go +++ b/api/token.go @@ -290,28 +290,30 @@ func (a *API) RefreshTokenGrant(ctx context.Context, w http.ResponseWriter, r *h var newToken *models.RefreshToken if token.Revoked { + a.clearCookieTokens(config, w) err = a.db.Transaction(func(tx *storage.Connection) error { validToken, terr := models.GetCurrentValidToken(tx, token) if terr != nil { + if errors.Is(terr, models.RefreshTokenNotFoundError{}) { + // revoked token has no descendants + return nil + } return terr } // check if token is the last previous revoked token if validToken.Parent == storage.NullString(token.Token) { refreshTokenReuseWindow := token.UpdatedAt.Add(time.Second * time.Duration(config.Security.RefreshTokenReuseInterval)) if time.Now().Before(refreshTokenReuseWindow) { - // if token is the last revoked token and is within the reuse interval - // just return the current valid refresh token newToken = validToken } } return nil }) if err != nil { - return internalServerError(err.Error()) + return internalServerError("Error validating reuse interval").WithInternalError(err) } if newToken == nil { - a.clearCookieTokens(config, w) if config.Security.RefreshTokenRotationEnabled { // Revoke all tokens in token family err = a.db.Transaction(func(tx *storage.Connection) error { diff --git a/models/refresh_token.go b/models/refresh_token.go index 96b1fff73e..0e7056c11f 100644 --- a/models/refresh_token.go +++ b/models/refresh_token.go @@ -85,7 +85,7 @@ func GetCurrentValidToken(tx *storage.Connection, token *RefreshToken) (*Refresh `, token.Token).First(refreshToken) if err != nil { if errors.Cause(err) == sql.ErrNoRows { - return nil, errors.Wrap(err, "no valid refresh token found") + return nil, RefreshTokenNotFoundError{} } return nil, err } From e1359ad1651e823f45eb0d57320fff7eac9bbf66 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Mon, 2 May 2022 17:05:08 -0700 Subject: [PATCH 07/11] add test case --- api/token_test.go | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/api/token_test.go b/api/token_test.go index 56f2ac71ee..763cb49da4 100644 --- a/api/token_test.go +++ b/api/token_test.go @@ -156,9 +156,11 @@ func (ts *TokenTestSuite) TestTokenRefreshTokenRotation() { t := time.Now() u.EmailConfirmedAt = &t require.NoError(ts.T(), ts.API.db.Create(u), "Error saving foo user") - oldRefreshToken, err := models.GrantAuthenticatedUser(ts.API.db, u) + first, err := models.GrantAuthenticatedUser(ts.API.db, u) require.NoError(ts.T(), err) - newRefreshToken, err := models.GrantRefreshTokenSwap(ts.API.db, u, oldRefreshToken) + second, err := models.GrantRefreshTokenSwap(ts.API.db, u, first) + require.NoError(ts.T(), err) + third, err := models.GrantRefreshTokenSwap(ts.API.db, u, second) require.NoError(ts.T(), err) cases := []struct { @@ -170,20 +172,42 @@ func (ts *TokenTestSuite) TestTokenRefreshTokenRotation() { expectedBody map[string]interface{} }{ { - "Reuse detection enabled, 30s reuse interval", + "Valid refresh within reuse interval", true, 30, - oldRefreshToken.Token, + second.Token, http.StatusOK, map[string]interface{}{ - "refresh_token": newRefreshToken.Token, + "refresh_token": third.Token, + }, + }, + { + "Invalid refresh, first token is not the previous revoked token", + true, + 0, + first.Token, + http.StatusBadRequest, + map[string]interface{}{ + "error": "invalid_grant", + "error_description": "Invalid Refresh Token", }, }, { - "Reuse detection enabled, no reuse interval", + "Invalid refresh, revoked third token", true, 0, - oldRefreshToken.Token, + second.Token, + http.StatusBadRequest, + map[string]interface{}{ + "error": "invalid_grant", + "error_description": "Invalid Refresh Token", + }, + }, + { + "Invalid refresh, third token revoked by previous case", + true, + 30, + third.Token, http.StatusBadRequest, map[string]interface{}{ "error": "invalid_grant", From 69cd4b4fd50f90ed604b94132d3d956e2b27a982 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Wed, 4 May 2022 01:04:42 -0700 Subject: [PATCH 08/11] refactor query to get child token --- api/token.go | 2 +- models/refresh_token.go | 13 +++---------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/api/token.go b/api/token.go index 2d783aec25..4c1352f7aa 100644 --- a/api/token.go +++ b/api/token.go @@ -292,7 +292,7 @@ func (a *API) RefreshTokenGrant(ctx context.Context, w http.ResponseWriter, r *h if token.Revoked { a.clearCookieTokens(config, w) err = a.db.Transaction(func(tx *storage.Connection) error { - validToken, terr := models.GetCurrentValidToken(tx, token) + validToken, terr := models.GetValidChildToken(tx, token) if terr != nil { if errors.Is(terr, models.RefreshTokenNotFoundError{}) { // revoked token has no descendants diff --git a/models/refresh_token.go b/models/refresh_token.go index 0e7056c11f..2af42c505b 100644 --- a/models/refresh_token.go +++ b/models/refresh_token.go @@ -72,17 +72,10 @@ func RevokeTokenFamily(tx *storage.Connection, token *RefreshToken) error { return nil } -// GetCurrentValidToken finds the most recent unrevoked token descending from the token provided. -func GetCurrentValidToken(tx *storage.Connection, token *RefreshToken) (*RefreshToken, error) { - tablename := (&pop.Model{Value: RefreshToken{}}).TableName() +// GetValidChildToken returns the child token of the token provided if the child is not revoked. +func GetValidChildToken(tx *storage.Connection, token *RefreshToken) (*RefreshToken, error) { refreshToken := &RefreshToken{} - err := tx.RawQuery(`with recursive token_family as ( - select id, user_id, token, revoked, parent from `+tablename+` where parent = ? - union - select r.id, r.user_id, r.token, r.revoked, r.parent from `+tablename+` r inner join token_family t on t.token = r.parent - ) - select * from token_family where id = (select max(id) from token_family) - `, token.Token).First(refreshToken) + err := tx.Q().Where("parent = ? and revoked = false", token.Token).First(refreshToken) if err != nil { if errors.Cause(err) == sql.ErrNoRows { return nil, RefreshTokenNotFoundError{} From ceebbf497d719681502f31a49e9766e95b41f0cf Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Wed, 4 May 2022 01:05:14 -0700 Subject: [PATCH 09/11] remove unnecessary check in refresh token grant --- api/token.go | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/api/token.go b/api/token.go index 4c1352f7aa..1ce341bee8 100644 --- a/api/token.go +++ b/api/token.go @@ -273,21 +273,6 @@ func (a *API) RefreshTokenGrant(ctx context.Context, w http.ResponseWriter, r *h return oauthError("invalid_grant", "Invalid Refresh Token") } - if !(config.External.Email.Enabled && config.External.Phone.Enabled) { - providers, err := models.FindProvidersByUser(a.db, user) - if err != nil { - return internalServerError(err.Error()) - } - for _, provider := range providers { - if provider == "email" && !config.External.Email.Enabled { - return badRequestError("Email logins are disabled") - } - if provider == "phone" && !config.External.Phone.Enabled { - return badRequestError("Phone logins are disabled") - } - } - } - var newToken *models.RefreshToken if token.Revoked { a.clearCookieTokens(config, w) From a195fc25d8d8c9054f24cec6e1dabd8fe84714bc Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Wed, 4 May 2022 15:08:38 -0700 Subject: [PATCH 10/11] update readme --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 322b8ef52c..b0342ce7c3 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,15 @@ Rate limit the number of emails sent per hr on the following endpoints: `/signup Minimum password length, defaults to 6. +`GOTRUE_SECURITY_REFRESH_TOKEN_ROTATION_ENABLED` - `bool` + +If refresh token rotation is enabled, gotrue will automatically detect malicious attempts to reuse a revoked refresh token. When a malicious attempt is detected, gotrue immediately revokes all tokens that descended from the offending token. + +`GOTRUE_SECURITY_REFRESH_TOKEN_REUSE_INTERVAL` - `string` + +This setting is only applicable if `GOTRUE_SECURITY_REFRESH_TOKEN_ROTATION_ENABLED` is enabled. The reuse interval for a refresh token allows for exchanging the refresh token multiple times during the interval to support concurrency or offline issues. During the reuse interval, gotrue will not consider using a revoked token as a malicious attempt and will simply return the child refresh token. + +Only the previous revoked token can be reused. Using an old refresh token way before the current valid refresh token will trigger the reuse detection. ### API ```properties From 5361c8bd614facc60f239a1e2b0649e37401048e Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Wed, 4 May 2022 15:18:17 -0700 Subject: [PATCH 11/11] update example env file --- example.env | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/example.env b/example.env index e38ef11a44..be0e63122b 100644 --- a/example.env +++ b/example.env @@ -190,7 +190,9 @@ GOTRUE_EXTERNAL_SAML_SIGNING_KEY="" # Additional Security config GOTRUE_LOG_LEVEL="debug" -GOTRUE_REFRESH_TOKEN_ROTATION_ENABLED="false" +GOTRUE_SECURITY_REFRESH_TOKEN_ROTATION_ENABLED="false" +GOTRUE_SECURITY_REFRESH_TOKEN_REUSE_INTERVAL="0" +GOTRUE_SECURITY_UPDATE_PASSWORD_REQUIRE_REAUTHENTICATION="false" GOTRUE_OPERATOR_TOKEN="unused-operator-token" GOTRUE_RATE_LIMIT_HEADER="X-Forwarded-For" GOTRUE_RATE_LIMIT_EMAIL_SENT="100"