Skip to content

refactor: refresh token rotation#838

Merged
aeneasr merged 3 commits intomasterfrom
improve-refresh
Dec 12, 2024
Merged

refactor: refresh token rotation#838
aeneasr merged 3 commits intomasterfrom
improve-refresh

Conversation

@aeneasr
Copy link
Copy Markdown
Member

@aeneasr aeneasr commented Nov 29, 2024

Related Issue or Design Document

Checklist

  • I have read the contributing guidelines and signed the CLA.
  • I have referenced an issue containing the design document if my change introduces a new feature.
  • I have read the security policy.
  • I confirm that this pull request does not address a security vulnerability.
    If this pull request addresses a security vulnerability,
    I confirm that I got approval (please contact [email protected]) from the maintainers to push the changes.
  • I have added tests that prove my fix is effective or that my feature works.
  • I have added the necessary documentation within the code base (if appropriate).

Further comments

@aeneasr aeneasr marked this pull request as ready for review December 2, 2024 19:30
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:

```patch
// 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)
}
```
@aeneasr aeneasr changed the title Improve refresh refactor: refresh token rotation Dec 4, 2024
@aeneasr aeneasr self-assigned this Dec 4, 2024
@aeneasr aeneasr merged commit f3336b1 into master Dec 12, 2024
@aeneasr aeneasr deleted the improve-refresh branch December 12, 2024 13:21

if err := c.TokenRevocationStorage.RevokeRefreshTokenMaybeGracePeriod(ctx, ts.GetID(), signature); err != nil {
return err
if err = c.TokenRevocationStorage.RotateRefreshToken(ctx, requester.GetID(), signature); err != nil {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 requester here has a completely new and random ID, which of course has nothing with the existing refresh token.

Previous code had it right, it retrieved refresh token requester with GetRefreshTokenSession and used ts.GetID() here. I think this should be fixed and correct requester ID should be passed here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My current workaround is to fix this at store level:

func (s *MemoryStore) RotateRefreshToken(ctx context.Context, _ string, refreshTokenSignature string) error {
	ts, err := s.GetRefreshTokenSession(ctx, refreshTokenSignature, nil)
	if err != nil {
		return err
	}
	if err := s.RevokeRefreshToken(ctx, ts.GetID()); err != nil {
		return err
	}
	return s.RevokeAccessToken(ctx, ts.GetID())
}

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 uuid.New().String() provides. I will have to find another way to do that. I could not find an easy way to patch GetID() to use a different ID constructor. :-(

Thanks.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fosite’s not the easiest library to work with

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, it is great!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants