Skip to content

Conversation

@pranav-super
Copy link
Contributor

This is the main pull request relating to OIDC support, although there are very small accompanying pull requests on Aerie (main) and Aerie-Gateway that go with this.

OIDC Overview

OIDC refers to the addition of authentication to OAuth2.0's authorization mechanisms. When it is used here and throughout this PR, it generally entails authenticating against an Identity Provider (IdP) and using the permissions that it provides to authorize what a given user can and cannot do.

While Aerie and Aerie-UI have existing authorization mechanisms, through Aerie-UI's robust permissions.ts and Aerie's Hasura metadata, which specifies permissions for each table, there isn't much support for authenticating users against an external IdP. Such authentication against an external, likely organizational IdP, is a fairly general need, and it is common for that IdP to use the OIDC framework. As such, the focus of this PR was to add this capability, specifically following the OAuth2.0 specification.

Details on the specifics of OIDC and OAuth2.0 are left out, but can be found here. OAuth2.0, in its specification, defines a variety of "authorization flows", where the end goal for a given user after submitting their credentials is to obtain an access token with which they can access the application. This PR introduces to Aerie-UI support for Authorization Code Flow with PKCE, which seems to be the most recommended and best supported flow for our given use case. That being said, it wouldn't be unimaginably difficult to support other authorization flows, although that was not the focus of this PR and that is generally an item for discussion, below.

How Do I Run This?

This PR introduces a few new pieces to the .env puzzle. Within aerie-ui, the following have been introduced:

  • PUBLIC_AUTH_OIDC_ENABLED: must be set to true to enable OIDC functionality.
  • OIDC_WELL_KNOWN_URL: the "well-known" URL endpoint your IdP. This usually ends or includes in the URL the string .well-known.
  • OIDC_AUTHORIZATION_URL: the endpoint of the IdP where the authorization code is obtained. Typically ends in /auth.
  • OIDC_TOKEN_URL: the endpoint of the IdP where the authorization code is obtained. Typically ends in /token.
  • OIDC_LOGOUT_URL: the endpoint of the IdP where the authorization code is obtained. Typically ends in /logout.
  • OIDC_JWKS_URL: the endpoint of the IdP where the authorization code is obtained. Typically ends in /certs.
  • OIDC_SCOPES: the "scopes" that the token should provide. These are usually details about the user and can depend on the specific IdP being used. They are space separated. Generally, defaulting to some superset of openid profile email, if not just that, is sufficient.
  • OIDC_CLIENT_ID: the ID in the IdP assigned to your client/application. This could be something like "aerie".
  • OIDC_CLIENT_SECRET: this is presently not used, but can be relevant future implementations if a flow other than PKCE is used.
  • OIDC_REDIRECT_URI: the URI to redirect to after obtaining the authorization code.
  • OIDC_AUDIENCE: the audience for the token. Typically matches OIDC_CLIENT_ID.
  • OIDC_ISSUER: the base URL of the IdP for your given "realm".

Finally, if using OIDC, you will likely also need to change your HASURA_GRAPHQL_JWT_SECRET in Aerie's .env to use RS256 as its type, and replace the key with a jwk_url matching the above configuration. If running Aerie-Gateway locally instead of with the rest of Aerie, this environment variable should be updated there as well.

Our Goal Here

Generally, our goal here is to be as minimally invasive as we can. While some pretty big changes are made, we tried to make sure too much wasn't altered. For example, all OIDC functionality is sectioned off to its own folder to ensure there is as little interference with existing auth functionality as possible.

There are also things we considered but ultimately descoped. Namely, phasing out the user cookie. While a review of the code suggesting that this cookie could logically be replaced with an access token, leading to a more uniform implementation overall, doing so would require slightly more sweeping changes than we would like to introduce in this PR at this moment. Doing so for the no-auth option ultimately seems to entail mirroring that implementation to what is done in the SSO/CAM implementation. Doing so would, however, require modification to Aerie-Gateway as well to support such functionality. As such, a cohesive fix for this should be pushed to a separate PR, especially considering that there already might be other things we would like to consider as far as refactoring the existing auth code and potentially integrating it or mirroring it more closely with the OIDC functionality being introduced here. This also implies that any refactor of the CAM/SSO flow is descoped in this PR.

What is Worth Knowing When Reviewing?

OIDC authentication in Aerie-UI does not rely as heavily on Aerie-Gateway as the other options do. It uses it briefly, in token/session validation, and as such required a small update to the gateway to support JWKs-based decoding. Otherwise, however, the UI operates largely independently of the gateway in performing authentication.

We also tried to reduce the reliance on redirects and silent logouts/errors, which were present in a lot of error cases. For example, if a JWT token expires, the current behavior was that as soon as it was caught in a Hasura request or in a Hasura subscription, the user would be logged out. This led to some very difficult-to-trace errors during development, and we thought it might be more intuitive to the user that, instead of immediately logging out, we make the errors more explicit and suggest that the user should log out rather than doing it for them.

Subscriptions have also changed fairly significantly. Prior to this PR, all references to the user were passed down using the Svelte PageData or LayoutData construct. While this makes perfect sense if the user is static and its token and other parts of that object are entirely static, it stops working very well when the token and the user object do change. Since our OIDC flow introduces the concept of refreshes, which mean that at the very least, the token assigned to a user can change, then something else becomes preferable. We make use of a user store instead to reflect these changes, and therefore all subscriptions, instead of having a user (or nothing) passed to them, now have the user obtained from the store. This change wasn't made, however, to the request framework, as there are queries (namely getUserQueries and getRolePermissions) that require a user to be passed to them when the user store will not yet have a value (as these are invoked in the server hooks). In doing this, a lot of uses of PageData were found to exist solely to propagate a static value of the user down to various components. As a store was being used instead, those uses were cleaned up, which led to the change and removal of a lot of +page.ts and +layout.ts files.

Discussion Items

A large unknown here is testing! Testing has proven to be quite difficult using typical frameworks for mock IdPs, like Dex (which doesn't seem to support users providing custom token refresh intervals, instead only allowing a default of 24 hours), and using a custom setup with a real IdP, like Keycloak, seems far too heavyweight. As such, comprehensive end-to-end testing is something that isn't yet included in this PR and we welcome any suggestions. We posted this PR without said tests just because there is a fair amount to review, so we didn't want to wait until testing was figured out to get any reviewing started, especially since if there are suggestions for additional sweeping changes, those tests would soon be made obsolete!

The idea was floated in development to introduce a (protected) folder to wrap all non-authentication-related routes. This would be purely organizational and wouldn't affect any URLs. It isn't included in this PR because it would make any future rebases extremely difficult, so that was left as a potential final change.

As mentioned before, we might want to consider supporting other authentication flows. While that is not in scope of this PR, we would like to suggest the intention here as a discussion item.

Finally, we mentioned earlier a potential refactor of the existing SSO/CAM and no-auth flows. While it isn't clear when we would do that and exactly what that would look like, we would like to make sure the interest in doing so is documented here. Such a change would also affect gateway somewhat significantly, so we would probably want to make a separate discussion post and meet about it following this PR!

@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
6 Security Hotspots

See analysis details on SonarQube Cloud

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
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
The nonce parameter binds the ID token to a specific authentication
request, preventing attackers from reusing previously issued tokens.

Changes:
- Add generateNonce() function using crypto.randomBytes
- Include nonce in authorization URL via createAuthorizationURLWithPKCE()
- Store nonce in httpOnly cookie during login
- Add verifyNonce() to validate ID token nonce claim on callback
- Update callback check() to require nonce presence
Validate the 'aud' claim in tokens to prevent confused deputy attacks
where tokens issued for other applications could be accepted.

Set OIDC_AUDIENCE env var to enable validation (recommended for production).
- Split JWT verification options: BASE_VERIFY_OPTS (no audience) for
  access tokens, ID_TOKEN_VERIFY_OPTS (with audience) for ID tokens
- Access tokens are treated as opaque per OIDC spec - audience
  validation is the resource server's responsibility
- ID tokens require audience validation to prevent confused deputy attacks
- Also fixes secure cookie flag for local development (secure: !dev)
Validate the `back` query parameter in the OIDC login route to prevent
open redirect attacks. Only allow relative paths starting with '/'
but not '//' (protocol-relative URLs).

Rejected examples: 'https://evil.com', '//evil.com', 'javascript:alert(1)'
Ensures auth cookies are only transmitted over HTTPS in production.
Uses `secure: !dev` pattern to allow HTTP in local development.

Updated files:
- oidc.ts: accessToken, idToken, refreshToken
- hooks.server.ts: activeRole (OIDC and SSO handlers)
- oidc/login/+page.server.ts: back cookie
- auth/login/+server.ts: activeRole, user
- auth/changeRole/+server.ts: activeRole
Using 'lax' instead of 'strict' because:
- OIDC flow requires cookies to persist across the redirect back from Keycloak
- 'lax' allows cookies on top-level GET navigations (safe for redirects)
- 'lax' still blocks cross-site POST requests (CSRF protection)

Note: SSO handler keeps sameSite='none' for cross-site SSO compatibility.
Allows operators to specify supported JWT signing algorithms via
space-separated OIDC_ALGORITHMS environment variable (e.g., "RS256 RS384").
Defaults to RS256, the most common algorithm for OIDC providers.
- Remove token values from user registration logs (only log username)
- Remove event object from cookie change log (contained token values)
- Remove token from error messages in callback
- Log only error messages, not full error objects that may contain tokens
- Downgrade verbose logs to console.debug
- Change informational logs from console.log to console.debug for consistency
- Remove response body from refresh error (may contain sensitive details)
- Only log error messages, not full error objects in scheduled refresh
- Remove timeout ID from log output (not useful for debugging)
- Improve log message clarity ("Scheduling token refresh" vs "Delay changed")
Adds CSP and other security headers to all responses:
- Content-Security-Policy-Report-Only: monitors violations without blocking
- X-Content-Type-Options: nosniff
- X-Frame-Options: DENY
- Referrer-Policy: strict-origin-when-cross-origin

CSP directives configured for:
- Scripts: self + unsafe-inline + unsafe-eval (Monaco editor requires these)
- Styles: self + unsafe-inline (Svelte scoped styles)
- Connect: self + Hasura + Gateway + Action + Workspace URLs
- Workers: self + blob (Monaco editor)
- Images/Fonts: self + data + blob

Change header to 'Content-Security-Policy' to enforce after testing.
Add verifyIdToken() function to validate ID tokens with full OIDC-compliant
checks (signature, issuer, expiration, audience) before passing to IdP.

If verification fails during logout, proceed without the id_token_hint
parameter rather than sending an unverified token.
Add environment variables to configure JWT claim namespace and paths:
- OIDC_CLAIMS_NAMESPACE (server) / PUBLIC_OIDC_CLAIMS_NAMESPACE (client)
- OIDC_CLAIMS_USER_ID / PUBLIC_OIDC_CLAIMS_USER_ID
- OIDC_CLAIMS_ALLOWED_ROLES / PUBLIC_OIDC_CLAIMS_ALLOWED_ROLES
- OIDC_CLAIMS_DEFAULT_ROLE / PUBLIC_OIDC_CLAIMS_DEFAULT_ROLE

Defaults to Hasura's standard claim structure:
  https://hasura.io/jwt/claims -> x-hasura-user-id, x-hasura-allowed-roles, x-hasura-default-role

Add extractClaims() helper functions in both oidc.ts (server) and auth.ts (client)
to centralize claim extraction with proper validation.

IMPORTANT: These settings must match:
- Hasura's HASURA_GRAPHQL_JWT_SECRET claims_map
- Aerie Gateway's JWT parsing logic
- Your IdP's token mapper configuration
@jmorton jmorton force-pushed the feature/oidc-support branch from 63e9fe2 to 981b01a Compare December 9, 2025 16:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants