Token Freshness
Token freshness mechanisms provide additional protection for sensitive operations. Fresh tokens should only be generated when the user provides credential information during a login operation.
Every time a user authenticates itself with credentials, it receives a fresh
token. However, when an access token is refreshed (implicitly or explicitly), the generated access token SHOULD NOT be marked as fresh
.
Most protected endpoints should not consider the fresh
state of the access token. However, in specific application cases, the use of a fresh
token enables an additional layer of protection by requiring the user to authenticate itself.
These operations include but are not limited to:
- Password update
- User information update
- Privilege/Permission management
Combined with an explicit refresh mechanism
Let's use the example from the previous section on Refreshing tokens.
from pydantic import BaseModel
from fastapi import FastAPI, Depends, HTTPException
from authx import AuthX, TokenPayload
app = FastAPI()
security = AuthX()
class LoginForm(BaseModel):
username: str
password: str
@app.post('/login')
def login(data: LoginForm):
if data.username == "test" and data.password == "test":
access_token = security.create_access_token(
data.username,
fresh=True
)
refresh_token = security.create_refresh_token(data.username)
return {
"access_token": access_token,
"refresh_token": refresh_token
}
raise HTTPException(401, "Bad username/password")
@app.post('/refresh')
def refresh(
refresh_payload: TokenPayload = Depends(security.refresh_token_required)
):
access_token = security.create_access_token(
refresh_payload.sub,
fresh=False
)
return {"access_token": access_token}
@app.get('/protected', dependencies=[Depends(security.access_token_required)])
def protected():
return "You have access to this protected resource"
@app.get('/sensitive_operation', dependencies=[Depends(security.fresh_token_required)])
def sensitive_operation():
return "You have access to this sensitive operation"
Creating fresh access tokens
To create a fresh
access token, use the fresh=True
argument in the AuthX.create_access_token
method.
In the /login
route, we set the fresh
argument to True
because the token is generated after the user explicitly provides a username/password combo.
@app.post('/login')
def login(data: LoginForm):
if data.username == "test" and data.password == "test":
access_token = security.create_access_token(
data.username,
fresh=True
)
refresh_token = security.create_refresh_token(data.username)
return {
"access_token": access_token,
"refresh_token": refresh_token
}
raise HTTPException(401, "Bad username/password")
Note
By default, AuthX.create_access_token
has fresh=False
to avoid implicit fresh token generation.
Refreshing tokens
When refreshing tokens, you should always generate non-fresh
tokens. In the example below, the fresh
argument is explicitly set to False
, which is also the default behavior for AuthX.create_access_token
.
@app.post('/refresh')
def refresh(
refresh_payload: TokenPayload = Depends(security.refresh_token_required)
):
access_token = security.create_access_token(
refresh_payload.sub,
fresh=False
)
return {"access_token": access_token}
Requiring fresh tokens
The /protected
route behaves as usual, regardless of the fresh
token's state.
In contrast, the /sensitive_operation
route requires a fresh token to be executed. This behavior is defined by the AuthX.fresh_token_required
dependency.
@app.get('/protected', dependencies=[Depends(security.access_token_required)])
def protected():
return "You have access to this protected resource"
@app.get('/sensitive_operation', dependencies=[Depends(security.fresh_token_required)])
def sensitive_operation():
return "You have access to this sensitive operation"
Note on Internal server error
As you might notice, step 4 results in a 500 HTTP Internal Server Error. This is the expected behavior since error handling is by default disabled on AuthX and should be enabled or handled by you to avoid errors.