# Research: JWT Bearer Token Authentication

**Feature**: `004-jwt-bearer-auth` | **Date**: 2026-05-03

---

## Decision 1 — JWT library

**Decision**: `PyJWT>=2.8`

**Rationale**: The project needs only HS256 signing with a single symmetric
secret key — the simplest possible JWT profile. `PyJWT` is the de-facto
standard Python JWT library for this use case: no additional crypto
dependencies, actively maintained, wide community adoption. `python-jose` was
the alternative; it has broader JOSE/JWE support but has had maintenance gaps
and brings extra dependencies that we do not need.

For Phase 3 (OIDC), token issuance is replaced by the external identity
provider. The `JWTAuthProvider` will be replaced by an OIDC-aware provider;
the library choice for Phase 2 does not constrain Phase 3.

**Alternatives considered**: `python-jose[cryptography]` — wider JOSE support
but heavier dependency tree and slower maintenance cadence. Rejected.

---

## Decision 2 — Password storage for the single owner account

**Decision**: Store plaintext `OWNER_USERNAME` and `OWNER_PASSWORD` in
environment variables; compare at login time using `secrets.compare_digest`
to prevent timing attacks.

**Rationale**: This is a single-user self-hosted application accessed over a
trusted local network. The password is already known to the person deploying
the application (they set it). Bcrypt pre-hashing would require operators to
run a separate tool to generate the hash before setting the env var, adding
friction with no meaningful security benefit for this threat model. In Phase 3
the owner credentials are replaced by an external OIDC provider entirely, so
this is a temporary mechanism with limited lifetime.

`secrets.compare_digest` is used instead of `==` to prevent any theoretical
timing oracle.

**Alternatives considered**: `passlib[bcrypt]` with a pre-hashed password env
var — more "correct" in absolute terms but adds operator complexity for a
single-user local app. Rejected.

---

## Decision 3 — JWT algorithm and claims

**Decision**: HS256 (HMAC-SHA256) with claims: `sub` (fixed string `"owner"`),
`iat` (issued-at epoch seconds), `exp` (expiry epoch seconds).

**Rationale**: HS256 is symmetric — a single `JWT_SECRET_KEY` env var is used
for both signing and verification. This is appropriate for a single-server
deployment where only the API ever validates tokens. RS256 (asymmetric) would
be needed if a second service needed to verify tokens independently; that is
not the case here and would be added complexity.

The `sub` claim carries the owner identifier. `exp` enables configurable
expiry. `iat` is included for auditability.

**Alternatives considered**: RS256 — appropriate when multiple services verify
tokens. Overkill for this single-server deployment. Rejected.

---

## Decision 4 — Login endpoint request format

**Decision**: JSON body `{"username": "...", "password": "..."}` at
`POST /api/v1/auth/token`. Response: `{"access_token": "...", "token_type": "bearer", "expires_in": <seconds>}`.

**Rationale**: The OAuth2 `application/x-www-form-urlencoded` format
(`grant_type=password, username, password`) is the spec-compliant form for
the Resource Owner Password Credentials grant. However, we are not building a
full OAuth2 authorization server — this is a simplified login endpoint for a
single-user SPA. A JSON body is simpler to consume from Angular's
`HttpClient`, avoids `URLSearchParams` boilerplate, and does not mislead
consumers into thinking this is a full OAuth2 endpoint.

The response shape (`access_token`, `token_type`) follows the OAuth2 bearer
token response convention because Phase 3 (OIDC) will also produce tokens in
this shape — the Angular `AuthService` does not need to change its
token-parsing logic.

**Alternatives considered**: OAuth2 password grant form format — interoperable
but unnecessarily strict for this use case. Rejected.

---

## Decision 5 — `AuthProvider` interface evolution

**Decision**: Evolve `get_identity()` to accept a single optional string
argument: `async def get_identity(self, authorization: str | None) -> Identity`.
`NoOpAuthProvider` ignores the argument and returns the anonymous identity as
before. `JWTAuthProvider` parses `"Bearer <token>"`, validates the JWT, and
returns a non-anonymous `Identity`, or raises a `401` via `HTTPException` if
the token is invalid or expired.

A new `require_auth` dependency in `dependencies.py` calls
`auth.get_identity(authorization_header)` and raises `401` if the returned
identity is anonymous. Protected routes inject `Depends(require_auth)`.
Public routes continue to bypass auth entirely — they neither inject auth nor
call `get_identity`.

**Rationale**: Minimal interface change that preserves backward compatibility
(`NoOpAuthProvider` continues to work unchanged) while allowing the JWT
provider to access the request header cleanly through FastAPI's `Header`
dependency. An alternative would be injecting `Request` directly into the
provider, but that couples the provider to the ASGI framework; a string header
value keeps the provider framework-agnostic.

**Alternatives considered**: Pass `Request` to `get_identity()` — couples the
provider to FastAPI/ASGI. Rejected. Create a separate `validate_token(token)`
method — more interface surface, no clear benefit over the chosen approach.
Rejected.

---

## Decision 6 — Token storage in the browser

**Decision**: `sessionStorage` — tokens are discarded when the browser tab is
closed.

**Rationale**: `localStorage` persists across browser sessions and is
accessible to any JavaScript on the page, making it a wider XSS target.
`sessionStorage` is scoped to the tab and cleared on close, giving better
security for a shared or semi-public machine. For a personal app used by the
owner on their own machine, the loss of persistence across browser restarts is
a minor inconvenience that is well worth the security improvement.
`HttpOnly` cookies would be more secure still but require CSRF protection and
server-side session management, which conflicts with the stateless JWT design.

**Alternatives considered**: `localStorage` — persistent but wider XSS
exposure. Rejected. `HttpOnly` cookie — strongest XSS protection but requires
CSRF mitigation and session server state. Rejected.

---

## Decision 7 — Angular interceptor API

**Decision**: Functional HTTP interceptor registered via
`provideHttpClient(withInterceptors([authInterceptor]))` in `app.config.ts`.
The interceptor reads the token from `AuthService.getToken()` and adds
`Authorization: Bearer <token>` to every outbound request if a token is
present.

**Rationale**: Angular 17+ prefers functional interceptors over class-based
ones (`APP_INITIALIZER` / `HTTP_INTERCEPTORS` token). The functional pattern
integrates with standalone components and is the current idiomatic approach.
The interceptor attaches the token to all requests unconditionally (not just
protected endpoints) — the API ignores the header on public endpoints, so this
is safe and avoids the complexity of URL matching in the interceptor.

**Alternatives considered**: Class-based `HttpInterceptor` — legacy pattern,
not aligned with Angular 17+ standalone idiom. Rejected.

---

## Decision 8 — No database migration required

**Decision**: JWTs are stateless — no token storage or blocklist is introduced.

**Rationale**: The tokens carry their own expiry; the API validates them by
signature and expiry on each request. A token blocklist (for logout
invalidation) would require a database table and lookup on every protected
request, adding complexity disproportionate to the threat model of a
single-user local application. On logout, the Angular client discards the
token from `sessionStorage`; the token technically remains valid until its
`exp`, but since there is no other client that holds it, this is acceptable.

**Alternatives considered**: Token blocklist table — true server-side logout.
Out of scope for Phase 2; noted as a potential hardening step.

---

## Summary of new dependencies

| Package | Where | Purpose |
|---------|-------|---------|
| `PyJWT>=2.8` | `api/pyproject.toml` | JWT signing and verification |

No new UI dependencies — Angular's `HttpClient` and `Router` cover all
interceptor, guard, and HTTP needs.
