Skip to content

OIDC Support#1746

Open
pranav-super wants to merge 28 commits into
developfrom
feature/oidc-support
Open

OIDC Support#1746
pranav-super wants to merge 28 commits into
developfrom
feature/oidc-support

Conversation

@pranav-super
Copy link
Copy Markdown
Contributor

@pranav-super pranav-super commented Aug 12, 2025

___REQUIRES_GATEWAY_PR___="131"

Summary

Adds OpenID Connect (OIDC) authentication to plandev-ui, alongside the existing username/password (JWT) and SSO/CAM login modes. Operators authenticate through their organization's identity provider (e.g., Keycloak) using the standard Authorization Code + PKCE flow, and the UI handles refresh, role-switching, and logout end-to-end. OIDC is the standard protocol for organizational single sign-on and is supported by most modern identity providers (Keycloak, Okta, Azure AD, etc.). This PR enables PlanDev be deployed in environments where the mission's team manages access centrally through their existing OIDC infrastructure rather than maintaining a per-app user store. OIDC is off by default and opt-in with PUBLIC_AUTH_OIDC_ENABLED=true.

Glossary

  • OIDC — OpenID Connect. A thin identity layer on top of OAuth 2.0. Adds the concept of an "ID token" (proof of who the user is) on top of OAuth's access tokens (what the user is allowed to do).
  • PKCE — Proof Key for Code Exchange. The standard hardening for OAuth's "authorization code" flow when the client (a browser-loaded SvelteKit app, in our case) can't safely hold a client secret. The browser generates a one-time random verifier, sends only its SHA-256 hash to the IdP up front, and reveals the verifier only when exchanging the code for tokens. Prevents an attacker who intercepts the authorization code from being able to redeem it.
  • IdP — Identity Provider. The system that authenticates users and issues tokens (e.g., Keycloak, Okta, Azure AD). plandev defers all "who is this user" decisions to the IdP.
  • JWKS — JSON Web Key Set. A public endpoint the IdP exposes (/protocol/openid-connect/certs on Keycloak) listing the public keys it uses to sign tokens. Anyone validating a token fetches the matching public key from this URL. This is how plandev-ui, plandev-gateway, and Hasura all independently verify JWT signatures without sharing a secret with the IdP.
  • Access token / ID token / refresh token — the three JWTs the IdP issues. Access token authorizes API calls; ID token carries identity claims; refresh token is used to get fresh access/ID tokens without re-prompting the user.

OIDC alongside existing auth modes

PlanDev now supports three mutually exclusive auth modes, selected at runtime via env vars:

Mode Selector Token issuer User provisioning
JWT (default) gateway, signs HS256 from username/password against gateway DB gateway login() → direct DB write
SSO/CAM PUBLIC_AUTH_SSO_ENABLED=true gateway, after validating an external CAM session cookie gateway login() → direct DB write
OIDC PUBLIC_AUTH_OIDC_ENABLED=true external IdP, signs RS256 gateway session() → direct DB write (new, this PR)

The three are mutually exclusive at runtime — Hasura validates JWTs with one configuration (HS256 or RS256+JWKS) and can't accept both simultaneously on the current Hasura v2.12.1.

The auth-mode router lives in src/hooks.server.ts and dispatches to handleJWTAuth, handleSSOAuth, or handleOIDCAuth. Each ends with event.locals.user populated; downstream code is mode-agnostic.

Architecture: how the pieces talk in OIDC mode

Full first-time login flow:

sequenceDiagram
    autonumber
    participant B as Browser
    participant U as plandev-ui<br/>(SvelteKit server)
    participant I as IdP<br/>(Keycloak)
    participant G as plandev-gateway
    participant H as Hasura
    participant P as Postgres

    Note over B,U: User clicks "Login Using OIDC"
    B->>U: GET /oidc/login
    U->>U: Generate PKCE verifier, state, nonce<br/>(set as short-lived cookies)
    U-->>B: 302 → IdP /authorize
    B->>I: GET /authorize (code_challenge, state, nonce)
    I-->>B: 302 → /oidc/callback?code=...&state=...
    B->>U: GET /oidc/callback
    U->>I: POST /token (code + PKCE verifier)
    I-->>U: access_token, id_token, refresh_token
    U->>U: Verify signatures via JWKS<br/>(issuer, audience, nonce)
    U-->>B: Set-Cookie + 302 → /plans

    Note over B,P: Per request from now on
    B->>U: GET /plans
    U->>U: Verify JWT (JWKS)
    U->>G: GET /auth/session (Bearer token)
    G->>G: Verify JWT (JWKS) + extract claims
    G->>P: SELECT users_and_roles<br/>(INSERT if new — lazy upsert)
    G-->>U: { success: true }
    U-->>B: SSR'd page
    B->>H: GraphQL query/subscribe (Bearer token)
    H->>H: Verify JWT (JWKS)
    H->>P: data
    P-->>H: rows
    H-->>B: data
Loading

Notes:

  • The IdP's JWKS endpoint is the single source of truth for signature verification. plandev-ui, plandev-gateway, and Hasura all fetch keys from it independently — none of them share a secret with the IdP or with each other.
  • The gateway is NOT a network gateway despite the name. The browser hits Hasura, action-server, and workspace-server directly. The gateway only sits in the /auth/session path — called once per server request to validate the JWT and (new in this PR) provision first-time users.
  • First-time user provisioning is step 13 in the diagram (gateway → SELECT/INSERT users_and_roles). The gateway writes the row directly via SQL with elevated DB credentials — same pattern JWT/SSO modes use today.

Refresh has its own flow, triggered by a browser-side timer rather than the IdP:

sequenceDiagram
    autonumber
    participant T as Browser timer
    participant B as Browser
    participant U as plandev-ui
    participant I as IdP
    participant H as Hasura (WS)

    Note over T: Set to fire ~10s before<br/>access token's exp claim
    T->>B: setTimeout fires
    B->>U: POST /oidc/refresh
    U->>I: POST /token (refresh_token grant)
    I-->>U: new access/id/refresh tokens
    U->>U: Verify new tokens via JWKS
    U-->>B: Set-Cookie (new tokens)
    B->>B: cookieStore change → restartSharedClient()
    B->>H: close WS (code 4999 "Client Restart") + reconnect<br/>with new accessToken in connectionParams
    B->>B: subscribe(accessTokenDecoded) fires<br/>schedule next refresh for new exp
Loading

Why OIDC lives in the UI rather than the gateway

Two pieces of OIDC have to live in the UI no matter what:

  1. PKCE bookkeeping cookies (verifier, oidc_state, oidc_nonce) must land on the UI's origin because the IdP redirects back to the UI's /oidc/callback. The gateway runs on a different origin (e.g., localhost:9000 vs the UI's localhost:3000, and different hosts/ports in production), so a cookie set by the gateway wouldn't be sent on the IdP→UI redirect. They have to live on the UI's origin to be available when the callback handler reads them.
  2. Refresh trigger has to live in browser JS because the browser hits Hasura directly for both HTTP queries and WebSocket subscriptions. The gateway never sees those requests so it can't intercept a 401 from Hasura and refresh on the user's behalf. A setTimeout in browser JS is the only place we can put "refresh before the access token expires."

What we did move gateway-side is what fits gateway-side naturally: user provisioning (writing permissions.users on first login). That matches what JWT/SSO modes already do today via gateway login() → getUserRoles(). It uses direct DB writes with elevated credentials, bypassing Hasura's per-role insert permissions.

Responsibilities of Services:

  • UI server: PKCE handshake, token exchange, JWKS verification, cookie management, refresh scheduling, WS restart-on-refresh, IdP-initiated logout
  • Gateway: JWT validation on /auth/session, lazy user-row upsert via SQL
  • Hasura: independent JWT validation per request via JWKS

How API users fit in (aerie-cli, scripts, automation)

OIDC support in this PR is for the browser flow only. API users — aerie-cli, scheduled jobs, custom scripts hitting Hasura directly — wouldn't use any of this code path. They authenticate against the IdP separately using flows appropriate for non-browser clients (device code flow for interactive CLI logins, client credentials grant for service-to-service automation, etc.) and end up holding a JWT they can send to Hasura/gateway/action-server as Authorization: Bearer <token>.

That works because Hasura validates every JWT against the IdP's JWKS regardless of where the token came from. So putting browser-side OIDC logic in the UI doesn't restrict API access in any way — aerie-cli and other consumers go directly to the IdP. The UI's OIDC code is a browser convenience layer, not a chokepoint.

When aerie-cli adds OIDC support, it can use this PR's realm config as a reference for the IdP-side setup (test users, role mappers, default-role attribute) but its client-side code will be different (no PKCE/cookies; device code flow + token storage on disk).

Technical breakdown

OIDC flow (UI server-side)

  • src/lib/server/oidc.tsClient singleton (PKCE), JWT verification via JWKS, lazy init() from OIDC_WELL_KNOWN_URL, signature + audience + issuer + nonce checks per OIDC spec.
  • src/routes/oidc/login/+page.server.ts — Generates PKCE verifier, state, nonce; validates the back query param against open-redirect attacks.
  • src/routes/oidc/callback/+page.server.ts — Validates state, exchanges code for tokens, verifies nonce, sets cookies, redirects back to the original page.
  • src/routes/oidc/refresh/+server.ts — Refreshes access/id tokens; returns 401 on refresh-token rejection so the client can detect and log out.
  • src/routes/oidc/logout/+server.ts — Clears local cookies, redirects to IdP logout with id_token_hint.

OIDC flow (UI client-side)

  • src/lib/stores/oidc.ts — Refresh scheduling derived from the access token's exp claim; cookie-store listener proactively restarts the GraphQL WS on accessToken change so Hasura doesn't unilaterally close the connection on expiry.
  • src/utilities/auth.ts — Shared computeRolesFromJWT (used by all three auth modes).
  • src/lib/types/oidc.ts — Shared extractClaims helper + ClaimsConfig type (server and client agree on the JWT claim shape).

Hooks + layout

  • src/hooks.server.ts — Three-mode dispatch (JWT/SSO/OIDC), CSP report-only headers, prefix-anchored "non-protected path" detection.
  • src/routes/+layout.server.ts — Auth-gate via enforce(locals.user, userIsDefined); prefix-anchored to avoid substring-match false negatives.
  • src/routes/+layout.svelte — Reactive user store; OIDC-only cookie-store + refresh wiring.

Gateway-side (cross-repo)

  • src/packages/auth/functions.tssession() now calls getUserRoles (lazy upsert) so OIDC users get a permissions.users row on first sight via direct DB write. Same pattern as JWT/SSO login uses today; bypasses Hasura GraphQL permissions entirely.

User-visible impacts in OIDC mode

What an operator notices that's different from JWT/SSO mode:

  • Login button on /login says "Login Using OIDC" and routes through the IdP.
  • After IdP auth, no extra "first-time setup" step — gateway provisions the user row on first session check.
  • Role switcher in the Nav lists only roles the user was granted in the IdP. The list is sorted alphabetically (deterministic across logins). Single-role users see no switcher at all.
  • Default role on login comes from the IdP user's default_role attribute (per-user, mission-configurable).
  • Session expiry: refresh is automatic until the refresh token's IdP lifespan runs out, then logout → /login?reason=Session expired - please log in again. The reason survives the IdP roundtrip via a short-lived server-side logoutReason cookie set by /oidc/logout and consumed by +layout.server.ts when it redirects to /login. WebSocket subscriptions don't blink during a refresh — the proactive restart cycles cleanly with the new token, and the "Reconnecting..." banner is explicitly suppressed for intentional restarts (gated on close code 4999 + reason "Client Restart", so a foreign close at the same code can't accidentally swallow the banner).
  • IdP-initiated logout: clicking logout sends id_token_hint to the IdP's logout endpoint so the IdP session is also destroyed (not just the local plandev session).

How to run

plandev-ui env vars

Required in plandev-ui's .env (or sourced from .env.test.oidc for local OIDC test runs):

  • PUBLIC_AUTH_OIDC_ENABLED — set to true to enable OIDC. The login page renders the "Login Using OIDC" button when this is set; otherwise the UI falls back to JWT/SSO.
  • OIDC_CLIENT_ID — the ID the IdP assigned to your client (often something like aerie). The PKCE flow sends this on every IdP call.
  • OIDC_REDIRECT_URI — where the IdP sends the browser back after auth. Must match what's registered with the IdP. Locally: http://localhost:3000/oidc/callback.
  • OIDC_ISSUER — the IdP's issuer URL (e.g., http://localhost:8000/realms/aerie-dev for the test realm). Used to validate the iss claim on incoming JWTs.
  • OIDC_JWKS_URL — the IdP's JWKS endpoint (/protocol/openid-connect/certs on Keycloak). The UI fetches public keys from here to verify token signatures.
  • OIDC_AUDIENCE — the expected aud claim on the ID token. Typically equals OIDC_CLIENT_ID. Validated only on the ID token per OIDC spec; access tokens are treated as opaque.
  • OIDC_SCOPES — space-separated scopes requested at login. openid profile email is the typical baseline; defaults to that if unset.

Either supply the well-known discovery URL OR each endpoint individually:

  • OIDC_WELL_KNOWN_URL — the IdP's well-known config URL (usually contains /.well-known/openid-configuration). When set, OIDC_AUTHORIZATION_URL, OIDC_TOKEN_URL, and OIDC_LOGOUT_URL are auto-discovered at startup.
  • OIDC_AUTHORIZATION_URL / OIDC_TOKEN_URL / OIDC_LOGOUT_URL — explicit endpoints. Required only if well-known discovery is unavailable.

Optional advanced configuration:

  • OIDC_ALGORITHMS — space-separated list of allowed JWT signing algorithms. Defaults to RS256, which is what every modern IdP uses. Override only if your IdP signs with something unusual (RS384, RS512, ES256, etc.).
  • OIDC_CLAIMS_NAMESPACE / OIDC_CLAIMS_USER_ID / OIDC_CLAIMS_ALLOWED_ROLES / OIDC_CLAIMS_DEFAULT_ROLE — claim paths within the JWT. Default to the Hasura convention (https://hasura.io/jwt/claims + x-hasura-user-id etc.). Override if your IdP can't be configured to emit Hasura-style claims and you map them in Hasura's claims_map instead.
  • PUBLIC_OIDC_CLAIMS_* — same four claim paths, but read by browser-side code. Must match the non-PUBLIC versions; the duplication is needed because SvelteKit's $env/dynamic/public only exposes PUBLIC_-prefixed vars to the browser.
  • OIDC_CLIENT_SECRET — not used by the current PKCE flow (PKCE is what replaces the need for a client secret in public/browser clients). Present in .env for forward compatibility if a future flow (e.g., client credentials) is added.

Hasura + gateway env vars

If using OIDC, PlanDev's HASURA_GRAPHQL_JWT_SECRET needs to change from HS256 to RS256 with jwk_url matching OIDC_JWKS_URL. Example:

{
  "type": "RS256",
  "jwk_url": "http://aerie_keycloak:8000/realms/aerie-dev/protocol/openid-connect/certs",
  "claims_namespace": "https://hasura.io/jwt/claims"
}

The same HASURA_GRAPHQL_JWT_SECRET is consumed by plandev-gateway, action-server, and workspace-server — all of them need to validate IdP-issued JWTs against the same JWKS.

For local development against the bundled Keycloak test realm:

# Bring up the OIDC stack (Keycloak + Hasura on RS256 + everything else)
npm run test:e2e:oidc:setup

# Start the UI with OIDC env vars sourced
set -a && . ./.env.test.oidc && set +a && npm run dev

Test users (configured in e2e-tests/oauth/realm-export.json):

  • AerieAdmin / password — has all three roles
  • AerieUser / passworduser + viewer
  • AerieViewer / passwordviewer only

Tear down:

npm run test:e2e:oidc:teardown

Testing

# Regular e2e (JWT mode, unchanged)
npm run test:e2e

# OIDC e2e (new — requires the OIDC stack running)
npm run test:e2e:oidc:setup
npm run test:e2e:oidc
npm run test:e2e:oidc:teardown

The OIDC suite covers: login as admin/user/viewer, role switching, refresh, logout, multi-tab refresh coordination, tab-backgrounding refresh.

Manual test scenarios

Prerequisites: OIDC stack up (npm run test:e2e:oidc:setup), UI started with OIDC env vars sourced. Browser must support the Cookie Store API (Chrome/Edge) for automatic token refresh.

Normal operation (happy path)

  1. Navigate to the app → redirected to IdP login
  2. Log in with valid credentials → redirected back to the app
  3. Verify user is authenticated and can access protected pages
  4. Open DevTools Network tab
  5. Wait for the next refresh (~10s before access-token expiry) → /oidc/refresh request appears
  6. Verify WebSocket connection restarts cleanly (look for a new GraphQL WS with fresh connection_init)
  7. Verify no "Reconnecting…" banner appears during normal refresh
  8. Switch user role via dropdown → WS restarts, data reloads

Offline resilience

  1. While logged in with valid tokens, enable "Offline" in DevTools Network
  2. Wait for next scheduled token refresh
  3. Observe token refresh fails in console
  4. Disable "Offline" (back online)
  5. Verify refresh retries automatically and succeeds within ~5s
  6. Verify WS reconnects and subscriptions resume
  7. Verify no data loss or UI errors

Role switching

  1. Log in and open a page with subscription data
  2. Watch WS messages in DevTools
  3. Switch role via the Nav dropdown
  4. Verify WS closes and reopens with new x-hasura-role in headers
  5. Verify subscriptions re-register and data updates for the new role's permissions

Logout flow

  1. Click logout
  2. Verify cookies are cleared (DevTools → Application tab)
  3. Verify the app redirects to the IdP logout endpoint
  4. Verify the IdP redirects back, then app redirects to /login
  5. Check console + server logs — should be clean

Long-running session

  1. Log in and leave the app running for >1 hour (assumes refresh-token TTL > 1h)
  2. Periodically check Network for refresh activity
  3. Verify WS stays healthy across refreshes
  4. Interact with the app (create/edit plans, etc.) to verify subscriptions
  5. Verify no unexpected logouts or connection errors

HMR during development (dev only)

  1. Log in and navigate to a subscription-heavy page
  2. Make a code change to trigger HMR
  3. Verify subscriptions reconnect seamlessly if tokens are fresh
  4. If tokens expired during HMR idle time: verify auto-recovery and that subscriptions reconnect once refresh runs

Big decisions and where the complexity is

  • OIDC lives in the UI; user provisioning lives in the gateway. Why each piece is where it is — see "Why OIDC lives in the UI rather than the gateway" above. Short version: PKCE cookies and refresh trigger have to be in the UI because of how the browser hits the IdP and Hasura; user-row provisioning fits the gateway because that's where every other auth mode writes the row today.

  • WebSocket lifecycle + token refresh — non-obvious behavior worth a closer read, collapsed because it's longest:

    WS Lifecycle & Token Refresh details

    Hasura validates JWT not only at connection_init but also continuously monitors token expiration, closing WebSocket connections when JWTs expire (observed in Hasura logs: "Could not verify JWT: JWTExpired", close codes 1006/4400).

    Implementation:

    • Proactive WebSocket restart on cookie refresh prevents abrupt disconnects when the access token rolls over. The close uses a named code+reason pair (INTENTIONAL_RESTART_CODE = 4999, INTENTIONAL_RESTART_REASON = "Client Restart") so the closed handler can recognize our restart and suppress the transient 'reconnecting' state — otherwise the banner would flash on every refresh. Gating on both code AND reason prevents a foreign close at the same code from accidentally swallowing the banner. Trade-off: if our reconnect ever hangs on a network/server issue, the banner won't surface until graphql-ws's own connectionAckWaitTimeout fires (~15s) and a non-4999 close follows.
    • Automatic token refresh via the Cookie Store API; the refresh subscriber listens on accessTokenDecoded (object, never deduped) rather than the numeric delay store (which Svelte primitive-deduped, causing refreshes to silently stop after identical-delay cycles)
    • Hybrid auto-recovery: subscriptions recover from connection errors using two strategies — a connectionState listener (fast, ~100ms when graphql-ws auto-reconnects) and a 5s fallback timer (kicks graphql-ws out of lazy mode when all subscriptions are terminated)
    • Logout reason survives the IdP roundtrip in OIDC mode. /oidc/refresh returning 401 → logout('Session expired - please log in again')/oidc/logout?reason=... → server stashes the reason in an httpOnly logoutReason cookie (60s TTL) → IdP end_session_endpoint → IdP redirects back to origin (query params stripped) → +layout.server.ts consumes and deletes the cookie, appending &reason=... to the /login redirect → existing /login reason handling surfaces it to the user.
    • Offline resilience: token refresh retries every 5s on network failure until successful
    • HMR resilience: cookie-store listeners and WS client state persist across Hot Module Replacement during development
    • WS auth detection in on.error matches both JWTExpired and JWSError (signature-invalid tokens, e.g., from IdP key rotation or tampering)
  • Refresh trigger lives in the client. The "gateway" can't intercept Hasura HTTP/WS calls (the UI hits Hasura directly, the gateway isn't a network gateway), so the refresh trigger has to live in the browser. setTimeout-based, with refresh-on-401 fallback in the WS error handler.

  • Auth-mode env coupling. Hasura's HASURA_GRAPHQL_JWT_SECRET is process-global; one Hasura instance can't validate both HS256 (gateway-signed) and RS256 (Keycloak-signed) tokens simultaneously on v2.12.1. CI runs two phases: the regular suite with HS256, then OIDC with RS256.

  • Each user's "default role" comes from a setting on their IdP profile, not from a guess. A user can have multiple roles (e.g., an admin who also has user and viewer). When they log in, the UI has to pick one to start in. The previous Keycloak config asked Keycloak to pick a role from the user's list, and Keycloak picked arbitrarily — so an admin could land in viewer-mode unpredictably.

    Now each IdP user has an explicit default_role attribute on their profile (e.g., "default_role": ["aerie_admin"]), and that attribute is what gets stamped into the JWT. Two practical benefits:

    1. Deterministic — same user gets the same starting role every login.
    2. Mission-configurable per user — a mission can default senior planners to viewer for safer day-to-day use even though those users have higher privileges they can opt into via the dropdown. We don't hardcode an "admin > user > viewer" priority anywhere in the UI.

    Configured in e2e-tests/oauth/realm-export.json (test realm); each production deployment configures the same on their own IdP.

Open questions

  • Unify cookie storage across auth modes (descoped from this PR). JWT/SSO modes store the user's data in a base64-encoded user cookie ({ id, token }); OIDC mode stores the access token directly as a plain accessToken cookie. The shared getToken() in src/stores/gqlClient.ts:81-102 has to branch on which shape it sees, and similar mode-specific branching exists in the hooks dispatcher and gateway-side cookie handling. Long-term, unifying on the OIDC-style (accessToken cookie + derive id from JWT claim) would collapse a lot of duplication. Out of scope here because it's cross-repo (gateway constructs the user cookie today) and the refactor logically pulls in SSO/CAM's cookie handling too — both modes whose auth we don't want to risk regressing in this PR. Worth its own coordinated PR after this lands.
  • HTTPS-only cookies in production. src/lib/server/oidc.ts sets secure: !dev on auth cookies — works in dev (HTTP) and production over HTTPS, but breaks if a production deploy ever runs behind HTTP (e.g., internal network without TLS). Worth a follow-up to thread the request URL in and compute secure = url.protocol === 'https:'.
  • Cosmetic trailing duplicate refresh + logout. The refresh setTimeout isn't cancelled when logout() is called, so it can fire once during the navigation to /oidc/logout, producing an extra /oidc/refresh + /oidc/logout pair in logs. No functional impact. Optional fix: cancelScheduledRefresh() called from logout().
  • Plan-revision subscription destructure-of-null (pre-existing, surfaced more often with frequent WS restarts): src/stores/plan.ts:111 destructures revision from data without null-guarding. Out of scope for this PR, separate follow-up.
  • Hasura upgrade path (v2.12 → v2.18+) would enable HASURA_GRAPHQL_JWT_SECRETS (plural) so one Hasura could validate both HS256 and RS256 simultaneously, eliminating the CI two-phase split.

TODOs

  • Make secure flag depend on request protocol, not just dev mode (production-HTTP deploy correctness)
  • cancelScheduledRefresh() in logout() to silence the cosmetic duplicate refresh + logout pair
  • Decide whether to fix plan.ts:111 destructure-of-null here or as a separate PR (it's pre-existing, not OIDC-introduced)
  • Document the local OIDC dev story in docs/TESTING.md (or rely on this PR description; either way, devs need to know about test:e2e:oidc:setup)

@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
6 Security Hotspots

See analysis details on SonarQube Cloud

@jmorton jmorton force-pushed the feature/oidc-support branch from 6d605b8 to 95ca8ef Compare October 10, 2025 23:42
@jmorton jmorton force-pushed the feature/oidc-support branch 2 times, most recently from 63e9fe2 to 981b01a Compare December 9, 2025 16:57
@AaronPlave
Copy link
Copy Markdown
Contributor

@jmorton @pranav-super We should tag up about this PR in relation to my new changes in #1741 where I've added socket consolidation + user store defined in a client-side store passed around via svelte context.

@AaronPlave AaronPlave self-assigned this Jan 20, 2026
Implement complete OIDC authentication flow including:
- Server-side OIDC client using Arctic library with PKCE support
- Login, callback, logout, and token refresh endpoints
- Updated hooks.server.ts to handle OIDC authentication mode
- Modified subscribable stores and effects for token management
- Request utilities updated for authenticated API calls
Migrate from passing user through PageData to using a centralized
userStore for authentication state. This change:
- Removes user parameter threading through page components
- Updates all stores to access user from centralized auth store
- Refactors route layouts to use reactive auth state
- Removes unnecessary +page.ts files that only passed user data
- Enables role changes to propagate without full page reload

# Conflicts:
#	src/components/plan/PlanMergeReview.svelte
#	src/routes/+layout.server.ts
#	src/routes/+layout.svelte
#	src/routes/constraints/+layout.svelte
#	src/routes/constraints/+page.svelte
#	src/routes/constraints/edit/[id]/+page.svelte
#	src/routes/constraints/new/+page.svelte
#	src/routes/dictionaries/+page.svelte
#	src/routes/expansion/+layout.svelte
#	src/routes/expansion/rules/+page.svelte
#	src/routes/expansion/rules/edit/[id]/+page.svelte
#	src/routes/expansion/rules/new/+page.svelte
#	src/routes/expansion/runs/+page.svelte
#	src/routes/expansion/sets/+page.svelte
#	src/routes/expansion/sets/new/+page.svelte
#	src/routes/external-sources/+layout.svelte
#	src/routes/external-sources/sources/+page.svelte
#	src/routes/external-sources/types/+page.svelte
#	src/routes/models/+layout.svelte
#	src/routes/models/+page.svelte
#	src/routes/models/[id]/+page.svelte
#	src/routes/parcels/+layout.svelte
#	src/routes/parcels/+page.svelte
#	src/routes/parcels/edit/[id]/+page.svelte
#	src/routes/parcels/new/+page.svelte
#	src/routes/plans/+page.svelte
#	src/routes/plans/[id]/+page.svelte
#	src/routes/plans/[id]/merge/+page.svelte
#	src/routes/scheduling/+layout.svelte
#	src/routes/scheduling/+page.svelte
#	src/routes/scheduling/conditions/edit/[id]/+page.svelte
#	src/routes/scheduling/conditions/new/+page.svelte
#	src/routes/scheduling/goals/edit/[id]/+page.svelte
#	src/routes/scheduling/goals/new/+page.svelte
#	src/routes/sequence-templates/+layout.svelte
#	src/routes/sequence-templates/+page.svelte
#	src/routes/tags/+page.svelte
#	src/routes/workspaces/+layout.svelte
#	src/routes/workspaces/+page.svelte
#	src/routes/workspaces/[workspaceId]/+layout@.svelte
#	src/routes/workspaces/[workspaceId]/actions/+layout@.svelte
#	src/routes/workspaces/[workspaceId]/actions/runs/[runId]/+layout@.svelte
#	src/routes/workspaces/[workspaceId]/actions/runs/[runId]/+page.svelte
#	src/stores/sequencing.ts
#	src/stores/tags.ts
Create rule.ts module for enforcing authentication requirements
at the route level, enabling consistent access control across
the application.
Implement Playwright tests for OIDC authentication including:
- OIDC fixture for handling auth flows in tests
- Login/logout test scenarios
- Token refresh verification
- Updated helpers and AppNav fixture for OIDC support
Align legacy auth routes (login, logout, changeRole) with OIDC
implementation by using SvelteKit's event-based cookie API
instead of manual header manipulation. Reduces code duplication
and improves consistency.
The Client singleton was initiating a fetch for the well-known
configuration but not awaiting it, causing endpoint values to be
undefined when the constructor completed before the fetch.

Changes:
- Convert Client.instance to async getter returning Promise<Client>
- Move initialization to async init() method that awaits well-known fetch
- Update all call sites to await Client.instance
- Uncomment OIDC_CLIENT_SECRET env var to fix type error
# Conflicts:
#	src/routes/workspaces/[workspaceId]/actions/+layout.ts
#	src/routes/workspaces/[workspaceId]/actions/+page.svelte
#	src/routes/workspaces/[workspaceId]/actions/runs/[runId]/+layout.ts
@AaronPlave AaronPlave changed the title [DRAFT] Add OIDC Support to Aerie-UI Add OIDC Support to Aerie-UI May 28, 2026
@AaronPlave AaronPlave changed the title Add OIDC Support to Aerie-UI Add OIDC Support May 28, 2026
@AaronPlave AaronPlave changed the title Add OIDC Support OIDC Support May 28, 2026
…s reconnecting banner on intentional WS restarts
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
3 Security Hotspots
0.0% Coverage on New Code (required ≥ 80%)
6.1% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants