Epic 1 — Authentication & Session Management
Covers OAuth login, GitHub App install handoff, organisation/tenant selection, session expiry, transparent token refresh, deep-link re-authentication with Open-Redirect protection, and sign-out.
Personas: BU AP OP AU (all roles)
Shared modules:
SessionRefreshController
ReturnToValidator
CorrelationChip
EnvProvenance
Story 1.1 — Log In via OAuth
- As a
- BU / AP / OP / AU
- I want
- to authenticate via the configured OAuth provider (GitHub OAuth or enterprise SSO)
- So that
- I receive a scoped session and land on the correct view
Scenario: Successful login — no prior session
Giventhe user navigates to the app root with no active session and the login surface is rendered
Whenthe user clicks Sign in with GitHub
Thenthe browser is redirected to the OAuth provider authorisation URL with the minimum required scopes
Andon callback the API Gateway exchanges the code for tokens and issues a session cookie or bearer token
Andthe UI navigates to the user's default view (project list) or the
return_to target if one is present
Scenario: Login with a deep-link return_to
Giventhe user follows a deep link (e.g.
/runs/123) while unauthenticated and ReturnToValidator confirms the path is on the internal-route whitelist with a valid HMAC
Whenauthentication completes
Thenthe UI navigates to /runs/123, not the default dashboard
Scenario: Loading state during OAuth callback processing
Giventhe OAuth callback is being processed server-side
Whenthe callback page mounts
Thena loading skeleton is displayed with a 0.5 s opacity fade-in
Andno partial authenticated state is rendered before the session is confirmed
| Endpoint / DB | Purpose |
|---|---|
GET /auth/login | Initiate OAuth redirect |
GET /auth/callback | Exchange authorisation code for session token |
GET /config | Fetch runtime environment config |
Story 1.2 — Transparent Token Refresh on 401
- As a
- BU / AP / OP / AU
- I want
- the app to silently refresh my session when my access token expires mid-use
- So that
- I never lose in-flight work due to a stale token
Scenario: Silent refresh on 401 — no UI disruption
Giventhe user has an authenticated session with an expired access token and
SessionRefreshController is mounted
Whenany API request returns HTTP 401
ThenSessionRefreshController calls POST /auth/refresh exactly once; all concurrent 401s share the single in-flight refresh promise
Andon a 200 response, the original request is retried with the new token with no navigation change or UI disruption
Scenario: In-flight form work preserved across silent refresh
Giventhe user has entered data in a run-trigger form and a 401 interrupts the form-submission request mid-flight
When
SessionRefreshController completes the refresh successfully
Thenthe form payload is re-submitted automatically with the new token; no data entered by the user is lost or reset
Scenario: Refresh failure — form preservation to sessionStorage + deep-link redirect
Giventhe refresh request to
POST /auth/refresh returns 401 or 403 and the user had an open run-trigger form with payload P
Whenthe refresh fails
Thenpayload P is serialised to sessionStorage under a route-scoped key and the UI redirects to the login surface with ?return_to=<exact-resource-path>
Andon successful re-login the form re-hydrates from sessionStorage and the user lands on the original resource
Scenario: Concurrency guard — exactly one refresh request
Giventhree concurrent API requests all receive 401 simultaneously
When
SessionRefreshController processes them
Thenexactly one POST /auth/refresh request is issued and all three original requests are retried once the shared refresh promise resolves
| Endpoint / DB | Purpose |
|---|---|
POST /auth/refresh | Exchange refresh token for new access token |
GET /runs/{run_id} | Re-fetched after re-auth to restore deep-link target |
Story 1.3 — Open-Redirect Protection on return_to
- As a
- BU / AP / OP / AU
- I want
- the
return_toparameter to be validated against an internal-route whitelist and HMAC signature before any redirect - So that
- a tampered or externally crafted link cannot redirect to an attacker-controlled host
Scenario: Valid internal return_to — user lands on target
Giventhe login callback receives
?return_to=/runs/550e8400-e29b-41d4-a716-446655440000, the path matches the whitelist, and the HMAC is valid
Whenauthentication completes
Thenthe UI navigates to the specified run path
Regression: External host, protocol-relative URL, javascript: URI, and tampered HMAC all rejected
Givenany of: external host, protocol-relative URL,
javascript: URI, or modified HMAC in return_to
WhenReturnToValidator evaluates the parameter
Thenthe value is rejected and the user lands on the safe default (project list / zero-state CTA)
Andno redirect to an external host is made (regression assertion)
| Endpoint / DB | Purpose |
|---|---|
GET /auth/callback | Receives and validates return_to parameter via ReturnToValidator |
Story 1.4 — Tenant / Organisation Selection
- As a
- BU / AP / OP
- I want
- to select my organisation or tenant after login when I belong to multiple
- So that
- all subsequent operations are scoped to the correct tenant
Scenario: Single-tenant user — automatic scope, no selection screen
Giventhe authenticated user belongs to exactly one tenant
Whenlogin completes
Thenthe tenant is automatically selected and the user lands on the project list without a selection step
Scenario: Multi-tenant user — selection screen shown
Giventhe authenticated user belongs to more than one tenant
Whenlogin completes
Thena tenant-selection screen is rendered listing all available organisations; selecting a tenant issues
POST /auth/tenant and navigates to the project list
| Endpoint / DB | Purpose |
|---|---|
GET /auth/tenants | List tenants available to the authenticated user |
POST /auth/tenant | Set active tenant scope |
Story 1.5 — Sign Out
- As a
- BU / AP / OP / AU
- I want
- to sign out and fully clear my session from the browser
- So that
- no residual credentials or state remain accessible
Scenario: Successful sign-out
Giventhe user has an active authenticated session
Whenthe user clicks Sign out
Thenthe UI issues
POST /auth/logout; all session cookies and bearer tokens are cleared; any sessionStorage entries for form preservation are cleared; the user is redirected to the login surface
Scenario: Sign-out while an SSE stream is open (Epic 4 cross-link)
Giventhe user is viewing a live run timeline with an active SSE connection
Whenthe user clicks Sign out
Then
ReconnectController is torn down and the SSE connection is closed before the session is cleared
Andno further reconciliation fetches or state mutations are issued after sign-out begins
| Endpoint / DB | Purpose |
|---|---|
POST /auth/logout | Invalidate server-side session |