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:
253
specs/004-jwt-bearer-auth/spec.md
Normal file
253
specs/004-jwt-bearer-auth/spec.md
Normal file
@@ -0,0 +1,253 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user