Advanced Authorization

Chapter Outline

Chapter 6: OAuth2 Scopes, Token Expiry, and Advanced Authorization

In this chapter, we deepen our understanding of authorization by introducing:

  • OAuth2 scopes for fine-grained access control
  • Token expiration and validation workflows
  • A conceptual overview of refresh tokens and token rotation
  • An introduction to third-party identity providers (IDPs) like Google/Auth0

We'll continue using the mock user database and JWT authentication from earlier chapters. This is a theory-heavy chapter with minimal new endpoints, focusing instead on expanding your ability to design secure and scalable auth flows.

6.1 OAuth 2.0 Scopes

OAuth2 scopes offer a powerful mechanism for fine-grained authorization. While roles (like admin or user) define general categories of access, scopes specify the exact permissions granted to a client or user. In the context of our Todo app, scopes allow us to define explicit actions—such as read:todos for viewing tasks and write:todos for creating or modifying them—thereby enabling greater control over what each token permits.

This model is particularly useful when an application integrates with third-party clients or frontend apps that don’t need full access to the user's data. For example, a mobile app might only request read:todos, while an internal admin dashboard could request both read:todos and write:todos. By validating these scopes on each protected route, we ensure that tokens are not only valid, but also used within the boundaries they were granted. In the upcoming chapter, we’ll demonstrate how to issue scoped tokens and enforce these constraints using FastAPI’s built-in SecurityScopes utility.

For additional information about OAuth2 scopes, refer to the documentation here.

6.2 Scopes with OAuth2PasswordBearer

FastAPI lets us define expected scopes using the OAuth2PasswordBearer class and SecurityScopes.

6.2.1 Defining Scopes

In the following code we are declaring the OAuth2 security scheme with three available scopes, read:todos, write:todos, and admin. The scopes parameter receives a dict with each key as the scope and the value representing the description of the scope.

app/auth/service.py
1from fastapi.security import OAuth2PasswordBearer
2
3oauth2_scheme = OAuth2PasswordBearer(
4 tokenUrl="/token",
5 scopes={
6 "read:todos": "Read access to user's todos",
7 "write:todos": "Write access to user's todos",
8 "admin": "Admin-only operations"
9 }
10)

6.2.2 Issuing scoped tokens

Include the scope assigned to the user in the database as one of the JWT access token's claim.

app/auth/router.py
1@router.post("/token", response_model=Token, response_model_exclude_none=True)
2def login(
3 form_data: OAuth2PasswordRequestForm = Depends(),
4 user_service: UserService = Depends(get_user_service),
5 auth_service: AuthService = Depends(get_auth_service)
6):
7 """Authenticate user and return JWT token."""
8 fetched_user = user_service.get_user(form_data.username)
9
10 if not fetched_user or not auth_service.verify_password(
11 form_data.password, fetched_user.hashed_password):
12 raise HTTPException(status_code=401, detail="Incorrect username or password")
13 user = User(
14 id=fetched_user.id,
15 username=fetched_user.username,
16 disabled=fetched_user.disabled,
17 role=fetched_user.role,
18 name=fetched_user.name,
19 email=fetched_user.email,
20 scopes=fetched_user.scopes,
21 )
22 access_token = auth_service.create_token(
23 {
24 "sub": user.username,
25 "scopes": user.scopes,
26 "role": user.role
27 },
28 expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
29 )
30 refresh_token = auth_service.create_token(
31 {"sub": user.username, "scopes": user.scopes, "type": "refresh"},
32 expires_delta=timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS))
33 return {
34 "access_token": access_token,
35 "token_type": "bearer",
36 "expires_in": ACCESS_TOKEN_EXPIRE_MINUTES * 60,
37 "refresh_token": refresh_token,
38 "info": {
39 "name": user.name,
40 "email": user.email,
41 }
42 }

6.2.3 Validating Scopes from Token

Let's update the existing get_current_user() dependecy function to allow verifying that the user's access token has valid scopes assigned:

app/auth/dependencies.py
1from fastapi import Security, HTTPException, status
2from fastapi.security import SecurityScopes
3from jose import JWTError, jwt
4
5async def get_current_user(security_scopes: SecurityScopes, token: str = Depends(oauth2_scheme)):
6 """Extract 'sub' (username) from JWT token."""
7 payload = AuthService.decode_token(token)
8 token_scopes = payload.get("scopes", [])
9
10 for scope in security_scopes.scopes:
11 if scope not in token_scopes:
12 raise HTTPException(
13 status_code=403,
14 detail=f"Missing required scope: {scope}"
15 )
16
17 return payload # or user object

6.2.4 Protecting Routes with Scope Checks

In the following route handlers for todo items, we are ensuring that the user has necessary permission to access the list_todos, and create_todo functions. The permissions are represented within the scopes array of the security dependencies.

app/todos/router.py
1@router.get("", dependencies=[Security(get_current_user, scopes=["read:todos"])], response_model=List[TodoItem])
2def list_todos(
3 todo_service: TodoService = Depends(get_todo_service),
4) -> List[TodoItem]:
5 """List todos, but only if API key is valid."""
6 return todo_service.list_todos()
7
8@router.post("", dependencies=[Security(get_current_user, scopes=["write:todos"])], response_model=TodoItem)
9def create_todo(
10 todo: TodoCreate,
11 todo_service: TodoService = Depends(get_todo_service),
12) -> TodoItem:
13 """Create a new Todo item."""
14 return todo_service.create_todo(title=todo.title, completed=False)

This is identical to how Google, Microsoft, Auth0, and Amazon issue and validate scopes.

6.3 Token Expiration and Handling

In most modern stacks (including FastAPI), the access token is a JWT with an exp claim:

json
{
"sub": "user_123",
"scopes": ["read:todos", "write:todos"],
"exp": 1732403100
}
  • The exp timestamp says “this token is valid until this time.”
  • On each request, your FastAPI dependency decodes the token; the JWT library checks exp.
  • If the token is expired, you should treat the client as unauthenticated and return 401 Unauthorized.

6.3.1 Generating token that expire and validating expiration

We are already encoding an exp within our JWT when we create it, we should simply aware of it when we decord the token.

app/auth/service.py
1@staticmethod
2def decode_token(token: str) -> Dict:
3 """Decode JWT token; raise HTTPException if invalid or expired."""
4 try:
5 payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
6 return payload
7
8 except ExpiredSignatureError:
9 # Token is structurally valid but expired
10 raise HTTPException(
11 status_code=status.HTTP_401_UNAUTHORIZED,
12 detail="Token expired",
13 )
14 except JWTError:
15 # Invalid signature, malformed token, etc.
16 raise HTTPException(
17 status_code=status.HTTP_401_UNAUTHORIZED,
18 detail="Invalid token",
19 )
20
21@staticmethod
22def create_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
23 """Create JWT token with optional expiration delta."""
24 to_encode = data.copy()
25 expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
26 to_encode.update({"exp": expire})
27 encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
28 return encoded_jwt

6.3.2 Testing Expiration

bash
curl -i -X GET http://127.0.0.1:8000/api/todos \
-H "Authorization: Bearer <expired-token>"

You should see a response similar to the following:

bash
HTTP/1.1 401 Unauthorized
date: ***
server: uvicorn
content-length: 25
content-type: application/json
{"error":"Token expired"}

6.4 Refresh Tokens

If your access tokens are short-lived (e.g. 5–30 minutes), you don’t want users to log in again every time the access token expires. That’s where a refresh token comes in.

6.4.1 How refresh tokens fit in

  • Access token
    • Short-lived (minutes)
    • Sent on every API call via Authorization: Bearer ...
    • If stolen, attacker has limited-time access
  • Refresh token
    • Long-lived (days/weeks)
    • Used only to get a new access token
    • Often stored more securely (e.g. HttpOnly cookie)
    • If stolen, attacker can continuously mint new access tokens → must be protected carefully

Relationship:

  • Access token = “ticket” to call APIs.
  • Refresh token = “membership card” you bring back to the auth server to get a new ticket.

You typically issue them together at login:

json
{
"access_token": "<short-lived-jwt>",
"refresh_token": "<opaque-or-longer-lived-jwt>",
"token_type": "bearer",
"expires_in": 1800
}

6.4.2 How to issue refresh tokens

A refresh token is generated and appended to response for login.

app/auth/router.py
1@router.post("/token", response_model=Token, response_model_exclude_none=True)
2def login(
3 form_data: OAuth2PasswordRequestForm = Depends(),
4 user_service: UserService = Depends(get_user_service),
5 auth_service: AuthService = Depends(get_auth_service)
6):
7 """Authenticate user and return JWT token."""
8 ...
9 access_token = auth_service.create_token(
10 {"sub": user.username, "scopes": user.scopes, "role": user.role},
11 expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
12 )
13 refresh_token = auth_service.create_token(
14 {"sub": user.username, "scopes": user.scopes, "type": "refresh"},
15 expires_delta=timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS))
16 return {
17 "access_token": access_token,
18 "token_type": "bearer",
19 "expires_in": ACCESS_TOKEN_EXPIRE_MINUTES * 60,
20 "refresh_token": refresh_token,
21 "info": {
22 "name": user.name,
23 "email": user.email,
24 }
25 }

6.4.3 How clients know when to use refresh tokens

Two main strategies:

A. Reactive: Refresh after a 401

  1. React app sends request with access token.
  2. API returns 401 with "Token expired" detail.
  3. Client catches this, calls /auth/refresh with refresh token.
  4. If refresh succeeds, client retries the original request with the new access token.

Pros:

  • Simple to implement.
  • No need to track exact expiry time in the client.

Cons:

  • User sees a small delay on the first expired request.
  • If the refresh fails (refresh token revoked/expired), you must log the user out.

B. Proactive: Refresh before expiration

Client keeps track of exp and schedules a refresh a little before expiry (e.g. at exp - 60s):

  • During login, client decodes the access token and extracts exp.
  • Client sets a timer to call /auth/refresh some seconds before expiry.
  • Access token is silently renewed in the background; user rarely sees a 401.

Pros:

  • Smoother UX, fewer 401s.
  • Better suited to SPAs and mobile apps.

Cons:

  • Slightly more complex (timers, token decoding).
  • Must handle edge cases where refresh token is revoked/expired anyway.

In practice, SPAs often do a hybrid: proactive refresh most of the time, reactive refresh as fallback.

6.5 External Identity Providers (Conceptual)

OAuth2 lets users authenticate using services like Google, GitHub, or Auth0.

6.5.1 How It Works (Simplified)

  1. Your app redirects the user to an IDP login page
  2. The user logs in and authorizes your app
  3. The IDP sends your app an authorization code (via redirect)
  4. Your app exchanges that code for an access token
  5. You use the access token to fetch user info and create a local session
sequenceDiagram autonumber participant User as User (Browser) participant Client as Client App (Frontend) participant AS as Authorization Server (IDP) participant Backend as Your API User->>Client: Requests Login Client->>AS: Redirect to /authorize
?client_id&redirect_uri&scope&state AS->>User: Login & Consent Screen User->>AS: Enters Credentials & Approves AS->>Client: Redirect back to redirect_uri
with ?code & state Client->>Backend: Send authorization code Backend->>AS: POST /token
client_id + secret + code + redirect_uri AS->>Backend: Returns Token(s) Backend->>Client: Session established / Token stored Client->>Backend: API request with Access Token Backend->>Client: Protected resource

Fig 6.1: Authorization Code Flow

6.6 Chapter Summary

In this chapter we took looked further into authorization by using OAuth 2. scopes. We also reviewed access token expiration, and using refresh token to generate access tokens in the event an access token has expired without having the user to login again.

Finally we outlined the Autorization Code Grant flow, which uses third party IDPs to issue access token for protecting backend resources.

The topic of authorization and OAuth is vast. Even though we reviewed a limited subset of the topics you should be able to review the documentation of other OAuth 2 grant types, and different authorization mechanisms, and be able to confidently implement them when necessary.

6.7 Further Reading

Check your understanding

Test your knowledge of Advanced Authorization in FastAPI

Feedback