Skip to content

Authentication

Miam uses Google OAuth for identity, then issues its own short-lived JWT stored as an HttpOnly cookie. An Authorization: Bearer header is accepted as a fallback for non-browser clients.

How it works, in plain words

The core idea: let Google handle "who are you?", and let our backend handle "what can you do here?"

When a user clicks "Sign in with Google", Google pops up its consent screen and hands the browser a signed token that proves "yes, this is louis@example.com". The frontend forwards that token to our backend. Our backend doesn't trust the browser, so it verifies the token directly against Google's public keys. Once verified, we either look up the user in our database or create them on the fly, then we mint our own session token — a JWT — and send it back as an HttpOnly cookie. From that point on, every API call automatically carries that cookie, and a single FastAPI dependency decodes it to figure out which user is making the request.

Building blocks

Layer Component Source
Frontend AuthProvider / useAuth frontend/src/contexts/AuthContext.tsx
API routes POST /auth/google, GET /auth/me, POST /auth/logout backend/src/miam/api/routes/auth.py
Dependency get_current_user_id (cookie / Bearer extraction) backend/src/miam/api/deps.py
Domain service AuthService (login + token validation) backend/src/miam/domain/services.py
Google adapter GoogleTokenVerifier backend/src/miam/infra/google_auth.py
JWT adapter JwtTokenHandler (PyJWT, HS256) backend/src/miam/infra/jwt_handler.py

The split follows the project's hexagonal architecture: AuthService only knows about ports (GoogleTokenVerifierPort, JwtTokenPort, UserRepositoryPort) — concrete adapters are wired in deps.py.

Two token types

It is easy to confuse them:

  • Google ID token — issued by Google in the browser, used once to prove identity to the backend.
  • App JWT — minted by the backend (sub = user_id, 24 h expiry, HS256), used for all subsequent calls. The frontend never reads it; it lives in an HttpOnly cookie.

Set in auth.py:

response.set_cookie(
    key="miam_auth_token",
    value=token,
    max_age=jwt_expiration_minutes * 60,
    httponly=True,
    secure=True,
    samesite="lax",
    path="/api",
)

Sequence diagram

sequenceDiagram
    autonumber
    actor U as User (browser)
    participant FE as Frontend (AuthContext)
    participant G as Google Identity
    participant API as FastAPI (/api/auth)
    participant SVC as AuthService
    participant GV as GoogleTokenVerifier
    participant JWT as JwtTokenHandler
    participant DB as UserRepository (Postgres)

    rect rgb(235, 245, 255)
    Note over U,DB: 1) Login with Google
    U->>G: Sign in with Google (GSI button)
    G-->>FE: Google ID token (JWT signed by Google)
    FE->>API: POST /auth/google { id_token }
    API->>SVC: login_with_google(id_token)
    SVC->>GV: verify(id_token)
    GV->>G: fetch public keys + verify signature/aud
    G-->>GV: claims (email, name, sub, picture)
    GV-->>SVC: GoogleUserInfo
    SVC->>DB: get_user_by_provider(google, sub)
    alt user not found
        SVC->>DB: create_user(email, name, google, sub, avatar)
        DB-->>SVC: User
    else user found
        DB-->>SVC: User
    end
    SVC->>JWT: create_access_token(user.id)
    JWT-->>SVC: signed JWT (HS256, 24h)
    SVC-->>API: jwt
    API-->>FE: 200 + Set-Cookie: miam_auth_token (HttpOnly, Secure, SameSite=Lax, path=/api)
    FE->>FE: store display info in localStorage
    end

    rect rgb(240, 255, 240)
    Note over U,DB: 2) Authenticated request (e.g. page reload calls /auth/me)
    U->>FE: open app
    FE->>API: GET /auth/me  (cookie auto-sent)
    API->>API: get_current_user_id<br/>token = cookie ?? Bearer header
    API->>SVC: validate_token(token)
    SVC->>JWT: decode_access_token(token)
    JWT-->>SVC: user_id (UUID)
    SVC->>DB: get_user_by_id(user_id)
    DB-->>SVC: User | None
    alt user exists
        SVC-->>API: user_id
        API-->>FE: 200 { user_id }
    else invalid / expired / missing
        SVC-->>API: ValueError
        API-->>FE: 401 Unauthorized
        FE->>FE: clear localStorage, show login
    end
    end

    rect rgb(255, 245, 240)
    Note over U,DB: 3) Logout
    U->>FE: click logout
    FE->>API: POST /auth/logout
    API-->>FE: 200 + Delete-Cookie miam_auth_token
    FE->>FE: clear localStorage, reset React Query cache
    end

Endpoints

POST /auth/google

Exchange a Google ID token for an app JWT.

Request body:

{ "id_token": "<google id token>" }

Response: 200 with Set-Cookie: miam_auth_token=... and a JSON body { "access_token": "..." }. Returns 401 if the Google token is invalid or its email is not verified.

GET /auth/me

Returns { "user_id": "<uuid>" } if the cookie/Bearer token is valid, 401 otherwise. Used by the frontend on load to verify the session.

POST /auth/logout

Clears the miam_auth_token cookie. Returns { "detail": "logged out" }.

Required environment variables

Loaded by AuthSettings from .env:

Variable Default Description
JWT_SECRET_KEY — (required) HMAC secret used to sign app JWTs
JWT_ALGORITHM HS256 JWT signing algorithm
JWT_EXPIRATION_MINUTES 1440 Token + cookie lifetime in minutes
GOOGLE_CLIENT_ID — (required) OAuth client ID used to validate the aud claim of the Google ID token