Feat: Implement JWT bearer token authentication

Protects image upload, delete, and tag-update endpoints behind
Bearer token auth. Public read endpoints remain open. Angular SPA
gains a login page, auth interceptor, and route guard for /upload.

- JWTAuthProvider (HS256, sub/iat/exp, secrets.compare_digest)
- POST /api/v1/auth/token login endpoint
- require_auth FastAPI dependency on all write routes
- AuthService, LoginComponent, authInterceptor, authGuard
- Detail page hides write controls for unauthenticated visitors
- 43 unit tests passing; integration tests require Docker stack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-03 19:12:38 +00:00
parent d91a65abe5
commit 5fbbc1e67f
36 changed files with 3998 additions and 42 deletions

View File

@@ -0,0 +1,120 @@
# API Contracts: JWT Bearer Token Authentication
**Feature**: `004-jwt-bearer-auth` | **Date**: 2026-05-03
All routes remain under `/api/v1/`. Error responses use the existing envelope:
`{ "detail": "<human message>", "code": "<machine code>" }`.
---
## New Endpoint
### `POST /api/v1/auth/token`
Issues a bearer token for the owner after verifying credentials.
**Request**
```
Content-Type: application/json
{
"username": "<string>",
"password": "<string>"
}
```
Both fields are required. A missing or empty field returns `422`.
**Success response**`200 OK`
```json
{
"access_token": "<jwt-string>",
"token_type": "bearer",
"expires_in": 86400
}
```
`expires_in` reflects the configured `JWT_EXPIRY_SECONDS` value.
**Failure responses**
| Status | Code | When |
|---|---|---|
| `401` | `invalid_credentials` | Username or password is wrong |
| `422` | (FastAPI default) | Missing or malformed request body |
---
## Changed Endpoints — Access Control
The following endpoints now require a valid bearer token. Requests without
a token, or with an invalid/expired token, receive a `401`.
| Method | Path | Was | Now |
|---|---|---|---|
| `POST` | `/api/v1/images` | Public | **Protected** |
| `DELETE` | `/api/v1/images/{id}` | Public | **Protected** |
| `PATCH` | `/api/v1/images/{id}/tags` | Public | **Protected** |
**Bearer token transmission**
The client MUST include the token in the `Authorization` header:
```
Authorization: Bearer <access_token>
```
**401 response shape** (returned by all three protected endpoints when
authentication fails):
```json
{
"detail": "Authentication required",
"code": "unauthorized"
}
```
---
## Unchanged Endpoints — Remain Public
The following endpoints require no token and must continue to accept requests
without an `Authorization` header:
| Method | Path | Description |
|---|---|---|
| `GET` | `/api/v1/images` | List / filter images |
| `GET` | `/api/v1/images/{id}` | Get image metadata |
| `GET` | `/api/v1/images/{id}/file` | Serve original image |
| `GET` | `/api/v1/images/{id}/thumbnail` | Serve image thumbnail |
| `GET` | `/api/v1/tags` | List / search tags |
| `GET` | `/api/v1/health` | Health check |
Sending a token on these endpoints is harmless (the server ignores it) but
is not required.
---
## Token Validation Rules
The API validates tokens using the following rules, in order:
1. The `Authorization` header value MUST begin with `Bearer ` (case-sensitive).
2. The token MUST be a valid HS256-signed JWT (verified against `JWT_SECRET_KEY`).
3. The `exp` claim MUST be in the future (at time of request receipt).
4. Any failure in steps 13 returns `401 unauthorized`.
---
## UI Route Contracts
These are Angular SPA routes affected by this feature.
| Route | Guard | Behaviour |
|---|---|---|
| `/login` | None | Login form; redirects to `returnUrl` or `/` on success |
| `/upload` | `authGuard` | Redirects to `/login?returnUrl=/upload` if not authenticated |
| `/images/:id` | None | Always accessible; tag-edit and delete controls visible only when authenticated |
| `/` | None | Always accessible (library) |