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" fromfastapi-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
ForJWTStrategy
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 theStrategy
. 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:
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 theStrategy but passed in by theAuthenticationBackend (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" whencreated_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 aTransport
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 previousstr
type expected byTransport.get_login_response
.
Moving token lifetime toAuthenticationBackend
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 fromStrategy
and instead setting it inAuthenticationBackend
, 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
inrefresh.py
for processing refresh tokens.
New "fresh" keyword arg inAuthenticator
methods
- The public methods in
Authenticator
now have afresh: bool
keyword arg, which, when true, will throw403 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
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. - I also considered using separate strategies for access and refresh tokens by adding a
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!
Uh oh!
There was an error while loading.Please reload this page.
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!