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>
121 lines
3.1 KiB
Markdown
121 lines
3.1 KiB
Markdown
# 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 1–3 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) |
|