Refresh tokens, freshness pattern and scopes #1075
Draft
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This is a draft for feedback, and follows on from this discussion earlier in the year: #350
I've made a first attempt here at implementing refresh tokens and the "freshness pattern" from
fastapi-jwt-auth
. It doesn't yet have any updates to docs, etc, as I'd like to get your initial input first.Breaking changes
Any implementation of these features involves breaking changes to parts of the API. This is, unfortunately, inevitable because any solution will need to address these challenges:
Token metadata
It's no longer sufficient to determine whether a token simply exists for a given user and strategy, because we now also need to:
For
JWTStrategy
this is straightforward (adding additional claims to the token), but for other strategies this requires non-backward-compatible changes. In this solution, that includes storing JSON in the Redis value forRedisStrategy
and adding additional fields forDatabaseStrategy
.We also need to consider how to store and retrieve this metadata with the
Strategy
. For this I propose a Pydantic model,UserTokenData
, which wraps the user object (conforming toUserProtocol
) and its metadata. In this first draft I've created four metadata fields:created_at: datetime
expires_at: Optional[datetime]
Strategy
but passed in by theAuthenticationBackend
(see below)last_authenticated: datetime
created_at == last_authenticated
scopes: Set[str]
Token response model
It's now no longer sufficient for a
Transport
instance to receive a string as a token, as it now needs to process an access tokan and (optionally) a refresh token. In this draft I've created a modelwhich replaces the previous
str
type expected byTransport.get_login_response
.Moving token lifetime to
AuthenticationBackend
As access tokens and refresh tokens have different lifetimes - and this could be extended to other token types in future - I've proposed removing the token lifetime configuration from
Strategy
and instead setting it inAuthenticationBackend
, as well as whether refresh tokens should be generated and accepted:New features
New refresh router
I've added an OAuth2-compatible token refresh router,
get_refresh_router
inrefresh.py
for processing refresh tokens.New "fresh" keyword arg in
Authenticator
methodsAuthenticator
now have afresh: bool
keyword arg, which, when true, will throw403 Forbidden
if the token is not fresh.current_token
, for users who need to inspect the token metadata.Scopes
I've borrowed the concept of OAuth2 scopes to distinguish between access tokens and refresh tokens, and I've also defined some additional scopes to distinguish between classes of users.
SystemScope.USER
"fastapi-users:user"
SystemScope.SUPERUSER
"fastapi-users:superuser"
SystemScope.VERIFIED
"fastapi-users:verified"
SystemScope.REFRESH
"fastapi-users:refresh"
This could be developed further - for example, both system- and user-defined routes could have "required scopes" that restrict what routes a particular token is permitted to access. By adding user-defined scopes, this could be used as a basis for a general-purpose user permissions system.
Potential additional features
The following additional security measures might be valuable but would require additional work:
created_at
datetime for the most recently used refresh token so that it (and any older refresh token) cannot be reused.Open questions
CookieTransport
handle the concept of refresh tokens? Currently it ignores them entirely.Alternative ideas
IWTStrategy
andBearerTransport
and having any use of refresh tokens / freshness with other strategies raise aNotImplementedError
, but I do think it's possible that users will want this for other strategies and transports.get_refresh_strategy
toAuthenticationBackend
, but this adds additional complexity. If this is something that user feedback indicates would be likely to be used I could add it back in.Feedback welcome
Please let me know whether this is heading in the right direction and what other changes / different approaches you might have in mind!