Skip to content

Commit f3336b1

Browse files
authored
refactor: refresh token rotation (#838)
Previously, the refresh token handler was using a combination of delete/update storage primitives. This made optimizing and implementing the refresh token handling difficult. Going forward, the RefreshTokenStorage must implement `RotateRefreshToken`. Token creation continues to be separated. BREAKING CHANGES: Method `RevokeRefreshTokenMaybeGracePeriod` was removed from `handler/fosite/TokenRevocationStorage`. Interface `handler/fosite/RefreshTokenStorage` has changed: - `CreateRefreshToken` now takes an additional argument `accessSignature` to keep track of refresh/access token pairs: - A new method `RotateRefreshToken` was added, which revokes old refresh tokens and associated access tokens: ```diff // handler/fosite/storage.go type RefreshTokenStorage interface { - CreateRefreshTokenSession(ctx context.Context, signature string, request fosite.Requester) (err error) + CreateRefreshTokenSession(ctx context.Context, signature string, accessSignature string, request fosite.Requester) (err error) GetRefreshTokenSession(ctx context.Context, signature string, session fosite.Session) (request fosite.Requester, err error) DeleteRefreshTokenSession(ctx context.Context, signature string) (err error) + RotateRefreshToken(ctx context.Context, requestID string, refreshTokenSignature string) (err error) } ```
1 parent 6c26dc5 commit f3336b1

39 files changed

+151
-336
lines changed

.github/workflows/oidc-conformity.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
with:
1515
fetch-depth: 2
1616
repository: ory/hydra
17-
ref: 2866a0499d02341ed0603601cfe4e63b24506fcb
17+
ref: a35e78e364a26c4f87f37d9f545ef10b3ffa468a
1818
- uses: actions/setup-go@v2
1919
with:
2020
go-version: "1.21"

Makefile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
export PATH := .bin:${PATH}
2+
13
format: .bin/goimports .bin/ory node_modules # formats the source code
24
.bin/ory dev headers copyright --type=open-source
35
.bin/goimports -w .
@@ -18,6 +20,9 @@ test: # runs all tests
1820
.bin/licenses: Makefile
1921
curl https://raw.githubusercontent.com/ory/ci/master/licenses/install | sh
2022

23+
.bin/mockgen:
24+
go build -o .bin/mockgen github.com/golang/mock/mockgen
25+
2126
.bin/ory: Makefile
2227
curl https://raw.githubusercontent.com/ory/meta/master/install.sh | bash -s -- -b .bin ory v0.1.48
2328
touch .bin/ory
@@ -26,4 +31,7 @@ node_modules: package-lock.json
2631
npm ci
2732
touch node_modules
2833

34+
gen: .bin/goimports .bin/mockgen # generates mocks
35+
./generate-mocks.sh
36+
2937
.DEFAULT_GOAL := help

handler/oauth2/flow_authorize_code_token.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ func (c *AuthorizeExplicitGrantHandler) PopulateTokenEndpointResponse(ctx contex
169169
} else if err = c.CoreStorage.CreateAccessTokenSession(ctx, accessSignature, requester.Sanitize([]string{})); err != nil {
170170
return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error()))
171171
} else if refreshSignature != "" {
172-
if err = c.CoreStorage.CreateRefreshTokenSession(ctx, refreshSignature, requester.Sanitize([]string{})); err != nil {
172+
if err = c.CoreStorage.CreateRefreshTokenSession(ctx, refreshSignature, accessSignature, requester.Sanitize([]string{})); err != nil {
173173
return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error()))
174174
}
175175
}

handler/oauth2/flow_authorize_code_token_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,7 @@ func TestAuthorizeCodeTransactional_HandleTokenEndpointRequest(t *testing.T) {
498498
Times(1)
499499
mockCoreStore.
500500
EXPECT().
501-
CreateRefreshTokenSession(propagatedContext, gomock.Any(), gomock.Any()).
501+
CreateRefreshTokenSession(propagatedContext, gomock.Any(), gomock.Any(), gomock.Any()).
502502
Return(nil).
503503
Times(1)
504504
mockTransactional.
@@ -627,7 +627,7 @@ func TestAuthorizeCodeTransactional_HandleTokenEndpointRequest(t *testing.T) {
627627
Times(1)
628628
mockCoreStore.
629629
EXPECT().
630-
CreateRefreshTokenSession(propagatedContext, gomock.Any(), gomock.Any()).
630+
CreateRefreshTokenSession(propagatedContext, gomock.Any(), gomock.Any(), gomock.Any()).
631631
Return(nil).
632632
Times(1)
633633
mockTransactional.

handler/oauth2/flow_client_credentials.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ func (c *ClientCredentialsGrantHandler) PopulateTokenEndpointResponse(ctx contex
6464
}
6565

6666
atLifespan := fosite.GetEffectiveLifespan(request.GetClient(), fosite.GrantTypeClientCredentials, fosite.AccessToken, c.Config.GetAccessTokenLifespan(ctx))
67-
return c.IssueAccessToken(ctx, atLifespan, request, response)
67+
_, err := c.IssueAccessToken(ctx, atLifespan, request, response)
68+
return err
6869
}
6970

7071
func (c *ClientCredentialsGrantHandler) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool {

handler/oauth2/flow_refresh.go

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -51,25 +51,31 @@ func (c *RefreshTokenGrantHandler) HandleTokenEndpointRequest(ctx context.Contex
5151
return errorsx.WithStack(rErr)
5252
}
5353

54-
return errorsx.WithStack(fosite.ErrInactiveToken.WithWrap(err).WithDebug(err.Error()))
54+
return fosite.ErrInvalidGrant.WithWrap(err).
55+
WithHint("The refresh token was already used.").
56+
WithDebugf("Refresh token re-use was detected. All related tokens have been revoked.")
5557
} else if errors.Is(err, fosite.ErrNotFound) {
56-
return errorsx.WithStack(fosite.ErrInvalidGrant.WithWrap(err).WithDebugf("The refresh token has not been found: %s", err.Error()))
58+
return fosite.ErrInvalidGrant.WithWrap(err).
59+
WithHint("The refresh token is malformed or not valid.").
60+
WithDebug("The refresh token can not be found.")
5761
} else if err != nil {
58-
return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error()))
59-
} else if err := c.RefreshTokenStrategy.ValidateRefreshToken(ctx, originalRequest, refresh); err != nil {
62+
return fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())
63+
}
64+
65+
if err := c.RefreshTokenStrategy.ValidateRefreshToken(ctx, originalRequest, refresh); err != nil {
6066
// The authorization server MUST ... validate the refresh token.
6167
// This needs to happen after store retrieval for the session to be hydrated properly
6268
if errors.Is(err, fosite.ErrTokenExpired) {
63-
return errorsx.WithStack(fosite.ErrInvalidGrant.WithWrap(err).WithDebug(err.Error()))
69+
return fosite.ErrInvalidGrant.WithWrap(err).
70+
WithHint("The refresh token expired.")
6471
}
65-
return errorsx.WithStack(fosite.ErrInvalidRequest.WithWrap(err).WithDebug(err.Error()))
72+
return fosite.ErrInvalidRequest.WithWrap(err).WithDebug(err.Error())
6673
}
6774

6875
if !(len(c.Config.GetRefreshTokenScopes(ctx)) == 0 || originalRequest.GetGrantedScopes().HasOneOf(c.Config.GetRefreshTokenScopes(ctx)...)) {
6976
scopeNames := strings.Join(c.Config.GetRefreshTokenScopes(ctx), " or ")
7077
hint := fmt.Sprintf("The OAuth 2.0 Client was not granted scope %s and may thus not perform the 'refresh_token' authorization grant.", scopeNames)
7178
return errorsx.WithStack(fosite.ErrScopeNotGranted.WithHint(hint))
72-
7379
}
7480

7581
// The authorization server MUST ... and ensure that the refresh token was issued to the authenticated client
@@ -130,30 +136,20 @@ func (c *RefreshTokenGrantHandler) PopulateTokenEndpointResponse(ctx context.Con
130136
if err != nil {
131137
return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error()))
132138
}
133-
defer func() {
134-
err = c.handleRefreshTokenEndpointStorageError(ctx, err)
135-
}()
136139

137-
ts, err := c.TokenRevocationStorage.GetRefreshTokenSession(ctx, signature, nil)
138-
if err != nil {
139-
return err
140-
} else if err := c.TokenRevocationStorage.RevokeAccessToken(ctx, ts.GetID()); err != nil {
141-
return err
142-
}
140+
storeReq := requester.Sanitize([]string{})
141+
storeReq.SetID(requester.GetID())
143142

144-
if err := c.TokenRevocationStorage.RevokeRefreshTokenMaybeGracePeriod(ctx, ts.GetID(), signature); err != nil {
145-
return err
143+
if err = c.TokenRevocationStorage.RotateRefreshToken(ctx, requester.GetID(), signature); err != nil {
144+
return c.handleRefreshTokenEndpointStorageError(ctx, err)
146145
}
147146

148-
storeReq := requester.Sanitize([]string{})
149-
storeReq.SetID(ts.GetID())
150-
151147
if err = c.TokenRevocationStorage.CreateAccessTokenSession(ctx, accessSignature, storeReq); err != nil {
152-
return err
148+
return c.handleRefreshTokenEndpointStorageError(ctx, err)
153149
}
154150

155-
if err = c.TokenRevocationStorage.CreateRefreshTokenSession(ctx, refreshSignature, storeReq); err != nil {
156-
return err
151+
if err = c.TokenRevocationStorage.CreateRefreshTokenSession(ctx, refreshSignature, accessSignature, storeReq); err != nil {
152+
return c.handleRefreshTokenEndpointStorageError(ctx, err)
157153
}
158154

159155
responder.SetAccessToken(accessToken)
@@ -164,7 +160,7 @@ func (c *RefreshTokenGrantHandler) PopulateTokenEndpointResponse(ctx context.Con
164160
responder.SetExtra("refresh_token", refreshToken)
165161

166162
if err = storage.MaybeCommitTx(ctx, c.TokenRevocationStorage); err != nil {
167-
return err
163+
return c.handleRefreshTokenEndpointStorageError(ctx, err)
168164
}
169165

170166
return nil
@@ -222,14 +218,14 @@ func (c *RefreshTokenGrantHandler) handleRefreshTokenEndpointStorageError(ctx co
222218
return errorsx.WithStack(fosite.ErrInvalidRequest.
223219
WithDebugf(storageErr.Error()).
224220
WithWrap(storageErr).
225-
WithHint("Failed to refresh token because of multiple concurrent requests using the same token which is not allowed."))
221+
WithHint("Failed to refresh token because of multiple concurrent requests using the same token. Please retry the request."))
226222
}
227223

228224
if errors.Is(storageErr, fosite.ErrNotFound) || errors.Is(storageErr, fosite.ErrInactiveToken) {
229225
return errorsx.WithStack(fosite.ErrInvalidRequest.
230226
WithDebugf(storageErr.Error()).
231227
WithWrap(storageErr).
232-
WithHint("Failed to refresh token because of multiple concurrent requests using the same token which is not allowed."))
228+
WithHint("Failed to refresh token. Please retry the request."))
233229
}
234230

235231
return errorsx.WithStack(fosite.ErrServerError.WithWrap(storageErr).WithDebug(storageErr.Error()))

0 commit comments

Comments
 (0)