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:
120
specs/004-jwt-bearer-auth/contracts/api.md
Normal file
120
specs/004-jwt-bearer-auth/contracts/api.md
Normal 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 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) |
|
||||
Reference in New Issue
Block a user