Files
agatha 5fbbc1e67f 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>
2026-05-03 19:12:38 +00:00

254 lines
12 KiB
Markdown

# 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**:
1. **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.
2. **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.
3. **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.
4. **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.
5. **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**:
1. **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.
2. **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.
3. **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.
4. **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.
5. **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**:
1. **Given** an unauthenticated visitor, **When** they open the library,
**Then** all images and their tags are visible without a login prompt.
2. **Given** an unauthenticated visitor, **When** they apply tag filters,
**Then** the filtered results are shown without requiring authentication.
3. **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.
4. **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**:
1. **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.
2. **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.