Skip to content

Adopt JWT Authentication using Client Credentials Flow for protected API routes #387

Open
@nanotaboada

Description

@nanotaboada

Description

Secure the API's mutating endpoints (POST, PUT, and DELETE) by introducing JWT-based authentication following the OAuth 2.0 Client Credentials Flow.

In this model, trusted machine-to-machine clients can obtain a short-lived JWT by submitting their client_id and client_secret to a dedicated authentication endpoint. This token is then included in the Authorization header to access protected resources.

sequenceDiagram
    participant Client as Client (Machine-to-Machine app)
    participant Server as Server (FastAPI RESTful API) 

    Note over Client,Server: Step 1 - Obtain Access Token

    Client->>Server: POST /auth/token (client_id, client_secret)
    Server-->>Client: 200 (OK) { access_token, expires_in, token_type }

    Note over Client,Server: Step 2 - Use Token to Access Protected Resources

    Client->>Server: POST /{resource}/{id} (Authorization: Bearer {access_token})
    Server-->>Client: 201 (Created)

    Client->>Server: PUT /{resource}/{id} (Authorization: Bearer {access_token})
    Server-->>Client: 204 (No Content)

    Client->>Server: DELETE /{resource}/{id} (Authorization: Bearer {access_token})
    Server-->>Client: 204 (No Content)
Loading

This mechanism enhances API security by ensuring only authenticated clients can perform data mutations, while maintaining statelessness.

Proposed Solution

  • Add an /auth/token route to issue JWTs to clients with valid credentials.
  • Introduce a simple in-memory (or configurable) client registry (client_id, client_secret).
  • Secure mutating routes in player_route.py using a dependency that validates and decodes the incoming JWT.
  • Configure token expiration (e.g., 60 minutes) and signing via a secret key from environment/config.
  • Include test coverage and update Postman collection for authentication.

Suggested Approach

1. Install dependencies

pip install python-jose[cryptography] python-dotenv

2. Create routes/auth_route.py:

from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel
from jose import jwt
from datetime import datetime, timedelta

router = APIRouter(prefix="/auth", tags=["Auth"])

ALGORITHM = "HS256"
MINUTES = 60

# Ideally, load these from secure config or environment variables
CLIENT_ID = "foobarbaz"
CLIENT_SECRET = "#!_7h3qu1ck8r0wnf0xjump50v3r7h3l42yd09=@42"
KEY = "1LnBfWcu7gTDmqT41QCW4ANu1xsHMcseingKWruVveM="


class TokenModel(BaseModel):
    grant_type: str
    client_id: str
    client_secret: str


@router.post("/token")
def get_token(data: TokenModel):
    if data.grant_type != "client_credentials":
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Unsupported grant_type. Must be 'client_credentials'."
        )

    if data.client_id != CLIENT_ID or data.client_secret != CLIENT_SECRET:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid client credentials"
        )

    expire = datetime.utcnow() + timedelta(minutes=MINUTES)
    payload = {
        "sub": data.client_id,
        "exp": expire,
        "scope": "write"
    }

    token = jwt.encode(payload, KEY, algorithm=ALGORITHM)

    return {
        "access_token": token,
        "token_type": "bearer",
        "expires_in": MINUTES * 60
    }

3. Create services/auth_dependency.py:

from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import jwt, JWTError
import os

security = HTTPBearer()
ALGORITHM = "HS256"

# These values should be loaded from a secure configuration or environment variables
KEY = "1LnBfWcu7gTDmqT41QCW4ANu1xsHMcseingKWruVveM="


def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
    token = credentials.credentials
    try:
        payload = jwt.decode(token, KEY, algorithms=[ALGORITHM])
    except JWTError:
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid or expired token")
    return payload

4. Secure mutating routes in routes/player_route.py:

from services.auth_dependency import verify_token

@router.post("/", dependencies=[Depends(verify_token)])
def create_player(...): ...

5. Sample token request with curl

curl -X POST http://localhost:9000/auth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "client_credentials",
    "client_id": "foobarbaz",
    "client_secret": "#!_7h3qu1ck8r0wnf0xjump50v3r7h3l42yd09_@42"
  }'

Acceptance Criteria

  • /auth/token endpoint issues valid JWT for known clients
  • POST, PUT, DELETE routes are secured and reject unauthenticated requests
  • Token expires after configured time; expired tokens are rejected
  • Protected routes return 403 Forbidden for invalid/missing JWTs
  • Configuration (e.g., JWT_SECRET) is sourced from .env or config
  • Unit tests cover both authentication and route protection
  • Postman collection updated with new auth flow

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requestpythonPull requests that update Python code

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions