Concepts

Authentication

Sessions are cookie-based, not bearer tokens. Knowing this is critical when calling the API from a browser or from a server-side render.

Session transport

When the user logs in (POST /api/auth/login) or registers (POST /api/auth/register) the backend issues a JWT and writes it into an HTTP-only cookie named access_token. The response body returns the user profile; the token itself never leaves the cookie.

Every subsequent request to /api/* must include that cookie. The frontend does this automatically because both browser fetches and server-side fetches forward cookies; if you ever call the API from outside the application you need to do the same.

GitHub OAuth

GitHub is the only third-party identity provider. The OAuth flow starts at POST /api/auth/github, which returns a state-signed URL to GitHub. GitHub redirects back to GET /api/auth/github/callback, the backend resolves the code and ends in one of two modes:

  • Login mode (no active session): a user is created or matched by their github_id; a session cookie is set; the user is redirected to /issues.
  • Link mode (active session): the existing user is linked to that GitHub identity. The session is not replaced. If the GitHub account already belongs to a different user the request is rejected — this is the fix to an earlier session-takeover bug.

The access token obtained from GitHub is encrypted at rest with AES-GCM (github_token_encrypted, github_token_nonce) and only decrypted in memory at the moment of an API call.

Handling 401

A 401 Unauthorized on any application endpoint means the session has expired or the cookie is missing. The convention throughout the frontend is:

  • Server-side: the response is intercepted in the shared fetch wrapper and the user is redirected to /login through Next.js' redirect() helper.
  • Client-side: the user sees a “Session expired” toast and the browser navigates to /login.
  • Exception: /api/auth/login and /api/auth/registerare excluded from this rule. A 401 there means “bad credentials”, which is a normal form error.

Calling the API manually

When testing endpoints from curl or similar you need to (1) authenticate first to obtain the cookie, and (2) reuse the same cookie jar on subsequent calls:

# 1. Log in and persist the cookie
curl -c cookies.txt -X POST \
  -H 'Content-Type: application/json' \
  -d '{"email":"you@example.com","password":"<password>"}' \
  http://localhost:8000/api/auth/login

# 2. Call any endpoint reusing the cookie
curl -b cookies.txt http://localhost:8000/api/issues

SSE and CORS

Server-Sent Events deserve a separate note. The browser's EventSource does not allow custom headers, so authentication cannot be put in an Authorizationheader. The cookie-based scheme is what makes the streams work: the browser attaches the cookie automatically as long as the backend serves CORS with Access-Control-Allow-Credentialsset to true and an explicit origin. Both are configured in the FastAPI app.