This is a draft for feedback, and follows on from this discussion earlier in the year: https://github.com/fastapi-users/fastapi-users/discussions/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:
- Determine a token's "fresh" status
- Distinguish between an access token and a refresh token
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 for RedisStrategy
and adding additional fields for DatabaseStrategy
.
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 to UserProtocol
) and its metadata. In this first draft I've created four metadata fields:
| Field | Description |
| --- | --- |
| created_at: datetime
| the UTC datetime when the token was issued |
| expires_at: Optional[datetime]
| the UTC datetime when the token expires - this is no longer set by the Strategy
but passed in by the AuthenticationBackend
(see below) |
| last_authenticated: datetime
| the UTC datetime when the user was last explicitly authenticated (not with a refresh token) - a token is considered "fresh" when created_at == last_authenticated
|
| scopes: Set[str]
| distinguishes between an access and refresh token, and can be extended for other purposes later |
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 model
class TransportTokenResponse(BaseModel):
access_token: str
refresh_token: Optional[str] = None
which replaces the previous str
type expected by Transport.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 in AuthenticationBackend
, as well as whether refresh tokens should be generated and accepted:
access_token_lifetime_seconds: Optional[int] = 3600,
refresh_token_enabled: bool = False,
refresh_token_lifetime_seconds: Optional[int] = 86400,
New features
New refresh router
I've added an OAuth2-compatible token refresh router, get_refresh_router
in refresh.py
for processing refresh tokens.
New "fresh" keyword arg in Authenticator
methods
- The public methods in
Authenticator
now have a fresh: bool
keyword arg, which, when true, will throw 403 Forbidden
if the token is not fresh.
- I've also added an additional method,
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.
| Enum | String | Description |
| --- | --- | --- |
| SystemScope.USER
| "fastapi-users:user"
| An access token belonging to an active user |
| SystemScope.SUPERUSER
| "fastapi-users:superuser"
| An access token belonging to an active superuser |
| SystemScope.VERIFIED
| "fastapi-users:verified"
| An access token belonging to an active and verified user |
| SystemScope.REFRESH
| "fastapi-users:refresh"
| A refresh token |
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:
- Preventing refresh token reuse: store the
created_at
datetime for the most recently used refresh token so that it (and any older refresh token) cannot be reused.
- Refreshing OAuth2 tokens: on token refresh, refresh an associated OAuth2 token with the original provider if it has expired
- Checking for revoked OAuth2 tokens: on token refresh, re-verify an associated OAuth2 token with the original provider
Open questions
- How should
CookieTransport
handle the concept of refresh tokens? Currently it ignores them entirely.
Alternative ideas
- This could be implemented in a non-breaking way by implementing it only for
IWTStrategy
and BearerTransport
and having any use of refresh tokens / freshness with other strategies raise a NotImplementedError
, but I do think it's possible that users will want this for other strategies and transports.
- I also considered using separate strategies for access and refresh tokens by adding a
get_refresh_strategy
to AuthenticationBackend
, 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!