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>
12 KiB
Feature Specification: JWT Bearer Token Authentication
Feature Branch: 004-jwt-bearer-auth
Created: 2026-05-03
Status: Draft
Input: User description: "Implement authentication with JWT bearer tokens. Image uploads, image deletion, and image tag updates should be protected. Non-authenticated users should still be able see images and tags, including searching for tags."
User Scenarios & Testing (mandatory)
User Story 1 — Log In and Receive a Token (Priority: P1)
The owner visits the application and logs in with their username and password. On success, the application silently stores a credential that it will attach to all future requests. The owner is taken to the library without needing to take any further action.
Why this priority: Every protected action depends on having a valid credential in hand. Without a working login flow, uploads, deletions, and tag edits are all inaccessible.
Independent Test: Submit valid credentials via the login form. Confirm the application navigates to the library and that subsequent protected actions (upload, delete, tag edit) succeed without a second login prompt.
Acceptance Scenarios:
-
Given the owner is not logged in, When they open the application, Then they are presented with a login form before they can reach any protected action.
-
Given the owner submits their correct username and password, When the submission is processed, Then they are authenticated, taken to the library, and the credential is retained for the current session.
-
Given the owner submits an incorrect username or password, When the submission is processed, Then an inline error is shown ("Invalid credentials"), the credential is not stored, and the user remains on the login page.
-
Given the owner is logged in and their session has expired, When they attempt a protected action, Then they are redirected to the login page and informed their session has ended.
-
Given the owner submits the login form with an empty username or password field, When the submission is attempted, Then a validation error is shown and no authentication request is made.
User Story 2 — Protected Write Actions Require Authentication (Priority: P1)
An authenticated owner can upload images, delete images, and update tags as before. An unauthenticated visitor who attempts these actions is turned away.
Why this priority: This is the core security requirement. Until protected actions reliably reject unauthenticated requests, the feature has not delivered its value.
Independent Test: Without logging in, attempt to upload an image via the API or UI. Confirm the attempt is rejected with an authentication error. Log in and repeat — confirm the upload succeeds.
Acceptance Scenarios:
-
Given an authenticated owner, When they upload an image, delete an image, or update tags on an image, Then the action succeeds as it did before authentication was introduced.
-
Given an unauthenticated visitor, When they attempt to upload an image via the UI or API, Then the request is rejected with a clear authentication error and no image is stored.
-
Given an unauthenticated visitor, When they attempt to delete an image via the UI or API, Then the request is rejected and the image remains in the library.
-
Given an unauthenticated visitor, When they attempt to update tags on an image via the UI or API, Then the request is rejected and the tags are unchanged.
-
Given a request that carries a malformed, expired, or tampered credential, When it reaches a protected endpoint, Then it is rejected with an authentication error, not silently ignored.
User Story 3 — Public Read Access (Priority: P1)
Unauthenticated visitors can browse the image library, view individual images, and search or filter by tags — no login required for read-only use.
Why this priority: The user explicitly requires this behaviour. Forcing login for read-only access would break the browse-without-an-account use case and is not part of the security model for this application.
Independent Test: Without a credential, call the list-images, get-image, serve-image, serve-thumbnail, and list-tags endpoints. All should return successful responses.
Acceptance Scenarios:
-
Given an unauthenticated visitor, When they open the library, Then all images and their tags are visible without a login prompt.
-
Given an unauthenticated visitor, When they apply tag filters, Then the filtered results are shown without requiring authentication.
-
Given an unauthenticated visitor, When they open an image detail page, Then the full-size image and its tags are displayed without a login prompt.
-
Given an unauthenticated visitor, When they browse the tag list or search for tags by prefix, Then results are returned without requiring authentication.
User Story 4 — Log Out (Priority: P2)
The owner can end their authenticated session. After logging out, the browser no longer retains their credential and protected actions are blocked until they log in again.
Why this priority: Important for shared or public machines, but secondary to the core login and protection flows.
Independent Test: Log in, then log out. Attempt a protected action and confirm it is rejected. Refresh the page and confirm the login screen is shown.
Acceptance Scenarios:
-
Given the owner is logged in, When they choose to log out, Then their credential is discarded, they are returned to the login page, and subsequent protected actions are rejected.
-
Given the owner has logged out, When they navigate directly to a protected page (e.g., the upload form), Then they are redirected to the login page.
Edge Cases
- What happens when the owner's credential expires mid-session? → The next protected action fails with an authentication error; the UI redirects to the login page.
- What happens when an attacker replays a valid but expired credential? → The request is rejected; expired credentials are never accepted.
- What happens when the login endpoint is called many times with wrong credentials? → The spec does not require rate limiting or lockout in v1; this is noted as a future hardening concern.
- What happens if the owner forgets their password? → Password reset is out of scope for v1; credentials are set via server-side configuration only.
- What happens if the login endpoint is called while already authenticated? → A new credential is issued; the old one may be discarded by the client.
- What happens when the UI receives a 401 on a read (public) endpoint? → This should not occur; read endpoints must never require authentication.
Requirements (mandatory)
Functional Requirements
- FR-001: The system MUST provide an endpoint that accepts a username and password and, on success, returns a time-limited credential the client can use to prove identity on subsequent requests.
- FR-002: The system MUST reject login attempts that supply an incorrect username or password with a clear error; no credential is issued.
- FR-003: The following actions MUST be protected — they MUST reject any
request that does not carry a valid, unexpired credential:
- Upload an image
- Delete an image
- Update the tags on an image
- FR-004: The following actions MUST remain publicly accessible without any
credential:
- List images (with or without tag filters)
- Retrieve a single image's metadata
- Retrieve image file content
- Retrieve image thumbnail content
- List tags (with or without prefix filter)
- FR-005: Credentials MUST have a finite lifetime; a credential issued before a configurable expiry window MUST be rejected.
- FR-006: The system MUST reject credentials that have been tampered with or are otherwise invalid.
- FR-007: The UI MUST automatically attach the owner's credential to every request that targets a protected action, without requiring the owner to manually supply it each time.
- FR-008: The UI MUST redirect unauthenticated users to the login page when they attempt to reach a protected action or page.
- FR-009: After a successful login, the UI MUST navigate the owner to the library (or to the page they originally tried to reach, if redirected from there).
- FR-010: The owner MUST be able to log out; after logout the credential is discarded and protected actions are blocked until the owner logs in again.
- FR-011: The owner's username and password MUST be configurable without changing application code (e.g., via environment variables or a configuration file read at startup).
- FR-012: Only one set of owner credentials is required in v1; multi-user support is explicitly out of scope.
Key Entities
- Credential: A time-limited proof of identity issued to the owner after successful login. Key attributes: subject (owner identifier), issued-at timestamp, expiry timestamp, validity state (valid / expired / invalid).
- Login Request: The combination of username and password submitted by the user to obtain a credential.
- Protected Endpoint: An API endpoint that MUST reject requests that lack a valid credential.
- Public Endpoint: An API endpoint that MUST accept requests regardless of whether a credential is present.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001: An unauthenticated visitor can browse the full image library and tag list without being prompted to log in.
- SC-002: An unauthenticated attempt to upload, delete, or edit tags is rejected every time — 0% of such attempts succeed.
- SC-003: An authenticated owner can complete a login-to-upload round trip in under 15 seconds on a local network connection.
- SC-004: An expired credential is rejected on the first use after expiry; no grace period or retry is granted.
- SC-005: After logging out, 100% of subsequent protected actions are rejected until the owner logs in again.
- SC-006: The library, detail, tag list, and image-serving pages all load correctly without a credential present.
Assumptions
- A single owner account is sufficient for v1. No user registration flow is required; credentials are set via environment variables or configuration at deployment time.
- The application is accessed over a trusted local network connection for v1; HTTPS is not mandated by this spec but is assumed for any production deployment.
- Credential lifetime is configurable but defaults to 24 hours. The exact value is a deployment decision, not a product requirement.
- Password reset, account management, and credential revocation are out of scope for v1.
- Rate limiting and account lockout after repeated failed login attempts are out of scope for v1; they are noted as future hardening work.
- The UI maintains the owner's credential for the duration of the browser session. Behaviour after the browser is closed (persist vs. discard) follows a secure default for the credential storage mechanism chosen during implementation.
- This is Phase 2 of a planned three-phase auth progression (no-auth → username/password → OIDC). The implementation MUST be structured so that replacing the credential issuance and validation mechanism in Phase 3 does not require changes to protected business logic.
- The detail page and upload form are considered "protected pages" in the UI sense (require login to interact with write actions), but their read content (viewing image, viewing tags) remains publicly accessible at the API level.