# 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.
