-
Notifications
You must be signed in to change notification settings - Fork 345
refactor: refresh token rotation #838
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -51,25 +51,31 @@ func (c *RefreshTokenGrantHandler) HandleTokenEndpointRequest(ctx context.Contex | |
| return errorsx.WithStack(rErr) | ||
| } | ||
|
|
||
| return errorsx.WithStack(fosite.ErrInactiveToken.WithWrap(err).WithDebug(err.Error())) | ||
| return fosite.ErrInvalidGrant.WithWrap(err). | ||
| WithHint("The refresh token was already used."). | ||
| WithDebugf("Refresh token re-use was detected. All related tokens have been revoked.") | ||
| } else if errors.Is(err, fosite.ErrNotFound) { | ||
| return errorsx.WithStack(fosite.ErrInvalidGrant.WithWrap(err).WithDebugf("The refresh token has not been found: %s", err.Error())) | ||
| return fosite.ErrInvalidGrant.WithWrap(err). | ||
| WithHint("The refresh token is malformed or not valid."). | ||
| WithDebug("The refresh token can not be found.") | ||
| } else if err != nil { | ||
| return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) | ||
| } else if err := c.RefreshTokenStrategy.ValidateRefreshToken(ctx, originalRequest, refresh); err != nil { | ||
| return fosite.ErrServerError.WithWrap(err).WithDebug(err.Error()) | ||
| } | ||
|
|
||
| if err := c.RefreshTokenStrategy.ValidateRefreshToken(ctx, originalRequest, refresh); err != nil { | ||
| // The authorization server MUST ... validate the refresh token. | ||
| // This needs to happen after store retrieval for the session to be hydrated properly | ||
| if errors.Is(err, fosite.ErrTokenExpired) { | ||
| return errorsx.WithStack(fosite.ErrInvalidGrant.WithWrap(err).WithDebug(err.Error())) | ||
| return fosite.ErrInvalidGrant.WithWrap(err). | ||
| WithHint("The refresh token expired.") | ||
| } | ||
| return errorsx.WithStack(fosite.ErrInvalidRequest.WithWrap(err).WithDebug(err.Error())) | ||
| return fosite.ErrInvalidRequest.WithWrap(err).WithDebug(err.Error()) | ||
| } | ||
|
|
||
| if !(len(c.Config.GetRefreshTokenScopes(ctx)) == 0 || originalRequest.GetGrantedScopes().HasOneOf(c.Config.GetRefreshTokenScopes(ctx)...)) { | ||
| scopeNames := strings.Join(c.Config.GetRefreshTokenScopes(ctx), " or ") | ||
| hint := fmt.Sprintf("The OAuth 2.0 Client was not granted scope %s and may thus not perform the 'refresh_token' authorization grant.", scopeNames) | ||
| return errorsx.WithStack(fosite.ErrScopeNotGranted.WithHint(hint)) | ||
|
|
||
| } | ||
|
|
||
| // 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 | |
| if err != nil { | ||
| return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) | ||
| } | ||
| defer func() { | ||
| err = c.handleRefreshTokenEndpointStorageError(ctx, err) | ||
| }() | ||
|
|
||
| ts, err := c.TokenRevocationStorage.GetRefreshTokenSession(ctx, signature, nil) | ||
| if err != nil { | ||
| return err | ||
| } else if err := c.TokenRevocationStorage.RevokeAccessToken(ctx, ts.GetID()); err != nil { | ||
| return err | ||
| } | ||
| storeReq := requester.Sanitize([]string{}) | ||
| storeReq.SetID(requester.GetID()) | ||
|
|
||
| if err := c.TokenRevocationStorage.RevokeRefreshTokenMaybeGracePeriod(ctx, ts.GetID(), signature); err != nil { | ||
| return err | ||
| if err = c.TokenRevocationStorage.RotateRefreshToken(ctx, requester.GetID(), signature); err != nil { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @aeneasr I think this introduced a bug. After we upgraded our systems to the version with this change, no refresh tokens in our tests (where we use memory storage) got invalidated. The reason I think is that Previous code had it right, it retrieved refresh token requester with
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My current workaround is to fix this at store level: But this is just a workaround, I think, it is completely useless to pass new requester ID into this function. Or is this something you use and need in Hydra?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The request ID is supposed to remain stable for the lifetime duration of a token chain, so this is correct! You can check the implementation in Hydra for reference
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You are right. It works. I had in our code a part where I was unconditionally setting request ID to a new value. The reason is that I want a bit shorter ID representation than what default Thanks.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fosite’s not the easiest library to work with
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, it is great! |
||
| return c.handleRefreshTokenEndpointStorageError(ctx, err) | ||
| } | ||
|
|
||
| storeReq := requester.Sanitize([]string{}) | ||
| storeReq.SetID(ts.GetID()) | ||
|
|
||
| if err = c.TokenRevocationStorage.CreateAccessTokenSession(ctx, accessSignature, storeReq); err != nil { | ||
| return err | ||
| return c.handleRefreshTokenEndpointStorageError(ctx, err) | ||
| } | ||
|
|
||
| if err = c.TokenRevocationStorage.CreateRefreshTokenSession(ctx, refreshSignature, storeReq); err != nil { | ||
| return err | ||
| if err = c.TokenRevocationStorage.CreateRefreshTokenSession(ctx, refreshSignature, accessSignature, storeReq); err != nil { | ||
| return c.handleRefreshTokenEndpointStorageError(ctx, err) | ||
| } | ||
|
|
||
| responder.SetAccessToken(accessToken) | ||
|
|
@@ -164,7 +160,7 @@ func (c *RefreshTokenGrantHandler) PopulateTokenEndpointResponse(ctx context.Con | |
| responder.SetExtra("refresh_token", refreshToken) | ||
|
|
||
| if err = storage.MaybeCommitTx(ctx, c.TokenRevocationStorage); err != nil { | ||
| return err | ||
| return c.handleRefreshTokenEndpointStorageError(ctx, err) | ||
| } | ||
|
|
||
| return nil | ||
|
|
@@ -222,14 +218,14 @@ func (c *RefreshTokenGrantHandler) handleRefreshTokenEndpointStorageError(ctx co | |
| return errorsx.WithStack(fosite.ErrInvalidRequest. | ||
| WithDebugf(storageErr.Error()). | ||
| WithWrap(storageErr). | ||
| WithHint("Failed to refresh token because of multiple concurrent requests using the same token which is not allowed.")) | ||
| WithHint("Failed to refresh token because of multiple concurrent requests using the same token. Please retry the request.")) | ||
| } | ||
|
|
||
| if errors.Is(storageErr, fosite.ErrNotFound) || errors.Is(storageErr, fosite.ErrInactiveToken) { | ||
| return errorsx.WithStack(fosite.ErrInvalidRequest. | ||
| WithDebugf(storageErr.Error()). | ||
| WithWrap(storageErr). | ||
| WithHint("Failed to refresh token because of multiple concurrent requests using the same token which is not allowed.")) | ||
| WithHint("Failed to refresh token. Please retry the request.")) | ||
| } | ||
|
|
||
| return errorsx.WithStack(fosite.ErrServerError.WithWrap(storageErr).WithDebug(storageErr.Error())) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.