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 / DBPurpose
GET /auth/loginInitiate OAuth redirect
GET /auth/callbackExchange authorisation code for session token
GET /configFetch 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 WhenSessionRefreshController 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 WhenSessionRefreshController processes them Thenexactly one POST /auth/refresh request is issued and all three original requests are retried once the shared refresh promise resolves
Endpoint / DBPurpose
POST /auth/refreshExchange 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_to parameter 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 / DBPurpose
GET /auth/callbackReceives 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 / DBPurpose
GET /auth/tenantsList tenants available to the authenticated user
POST /auth/tenantSet 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 ThenReconnectController 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 / DBPurpose
POST /auth/logoutInvalidate server-side session