Legacy OAuth Flow
Before BluAuth shipped its OIDC provider, Blu apps integrated through a custom broker route pair: /oauth2/login → upstream provider → /oauth2/callback → redirect back to the client with ud-* query parameters carrying identity claims.
These routes are preserved for compatibility with apps that predate BluAuth's OIDC implementation. Do not use them for new integrations. Use the modern OIDC flow instead.
Source: server/routes/oauth2/login.get.ts, server/routes/oauth2/callback.ts.
When to use this path
Use the legacy flow if — and only if — one of these applies:
- Your app was already integrated with BluAuth's
/oauth2/loginbroker before the OIDC endpoints existed, and you have not finished migrating. - You are explicitly planning a short-lived migration cutover and need both flows to coexist temporarily.
Use OIDC for everything else. The /oauth2/* routes:
- Emit unsigned identity parameters. An intermediary who controls the
redirect_urican forge them. - Provide no access token, no ID token, no JWKS, no introspection, no revocation.
- Have no standard library support — every integration is bespoke.
- Do not participate in session federation; each downstream app maintains its own session independently.
Flow overview
[Your App] [BluAuth] [Upstream IdP]
| | |
| 1. redirect /oauth2/login ------------------------> |
| | 2. provider authorize --> |
| | |
| | 3. user authenticates |
| | |
| | 4. <-- provider callback |
| 5. <-- redirect to redirect_uri?ud-* -------------- |
1. Redirect to /oauth2/login
GET https://auth.example.com/oauth2/login
?provider=google
&returnTo=https%3A%2F%2Fapp.example.com%2Fcallback
&client_id=7f9b0e7e-...-a11c
Query parameters:
| Parameter | Required | Notes |
|---|---|---|
provider | Yes | The upstream provider slug (google, github, microsoft, or a configured custom OIDC slug). Must correspond to an active row in identity_providers. |
returnTo | Yes | Absolute URL where BluAuth will redirect after sign-in. Must match one of the client's registered redirect URIs, or a same-origin relative path. |
client_id | Recommended | Client UUID. Required when requesting additional_scopes (provider-token brokering). When present, returnTo is validated against that client's redirect URI list. |
additional_scopes | Optional | Space-separated upstream scopes. Only honored if the client's allowedProviderTokens includes the provider. |
prompt | Optional | OIDC prompt value. Automatically downgraded if the upstream IdP doesn't advertise it. |
intent | Optional | link to link an additional provider to an already-signed-in user. |
2. BluAuth redirects to the upstream provider
BluAuth resolves the provider config (Valkey-cached from identity_providers), loads the upstream's discovery document, and redirects the user to the upstream authorization endpoint with BluAuth's own redirect_uri pointing at /oauth2/callback.
3. User authenticates upstream
The user completes sign-in with Google, GitHub, Microsoft, or the configured custom OIDC provider. If the provider rejects the request (e.g. access_denied), BluAuth propagates a sanitized error=consent_denied or error=provider_error back to your returnTo.
4. BluAuth handles the callback
At /oauth2/callback, BluAuth:
- Validates the
stateparameter against a short-lived record inupstream_oauth_state. - Exchanges the code with the upstream provider's token endpoint.
- Validates the upstream
id_tokensignature (when the provider is configured for strict verification). - Resolves or provisions the local BluAuth user via
resolveIdentityFromProviderLogin— matching on verified email aliases, handling link-mode consent, or creating a new canonical user. - Issues a local BluAuth session (sets the Better Auth session cookie).
- Redirects to
returnTowithud-*parameters carrying identity claims.
5. Redirect back to your app
https://app.example.com/callback
?state=<echoed-if-you-sent-one>
&ud-sub=<user-uuid>
&ud-email=user%40example.com
&ud-email_verified=true
&ud-name=User%20Name
&ud-picture=https%3A%2F%2F...
The ud-* parameters are URL-encoded strings carrying the same identity facts that the OIDC ID token carries. They are not signed. Apps historically accepted them at face value because the broker ran on a trusted boundary, but any new app reading these should migrate.
Error redirects
When something fails before BluAuth can identify the user, the callback handler redirects to returnTo (or /login as a last resort) with a sanitized error code:
error= value | Meaning |
|---|---|
consent_denied | Upstream provider returned access_denied. |
provider_error | Upstream provider returned a non-access_denied error. |
missing_params | Upstream callback arrived with no code or state. |
account_claimed_by_other_user | The upstream identity is already linked to a different BluAuth user. |
provider_already_linked | Provider is already linked — link-mode requested but redundant. |
ambiguous_email_alias | Multiple verified aliases match; manual resolution required. |
user_not_found | Target user (link-mode) no longer exists. |
user_create_failed | Canonical user creation failed — check logs with the request ID. |
session_failed / internal | Uncategorized internal error. |
These values are stable and safe to branch on in downstream UX.
Security implications
- The
ud-*parameters are visible in the browser address bar, referer headers, and access logs. Do not put anything beyond the identity claims above into them. - They are not signed. A compromised TLS terminator or a man-in-the-browser extension can tamper with them. OIDC's
id_token(JWS) defends against this;ud-*does not. - The
stateparameter is round-tripped but the identity claims are not bound to state — an attacker who can forge a redirect can substitute values. - Because the broker sets a local BluAuth session cookie as part of the callback, downstream apps that share the
auth.example.comorigin can additionally call/api/auth/security-stateto reconfirm the authenticated user server-side.
Treat ud-* as presentation-only hints and reconfirm identity server-side with a session check or (preferably) an OIDC round-trip.
Migrating off
If you own an existing Blu app on the legacy flow:
- Ask a BluAuth admin to register an OIDC client for your app (
/admin/clients). Reuse the redirect URI you already have registered for the legacy path. - Pick an OIDC library for your stack (recommendations).
- Run both flows side-by-side behind a feature flag. Compare the
ud-subvalue with the OIDC ID token'ssub— they will differ (OIDC uses pairwise subjects per client), butuserinfo.emailswill match theud-emailalias list. - Migrate session handling to rely on the ID token / userinfo response rather than
ud-*params. Keep server-side session validation idempotent during cutover. - Cut traffic over to OIDC. Remove the legacy
/oauth2/loginredirect after one full release cycle of zero-traffic on the old handler.
OIDC vs. legacy quick reference
| Capability | OIDC flow | Legacy /oauth2/* flow |
|---|---|---|
| Signed identity | ID token (JWS, ES256) | None — plain ud-* query params |
| Access token | ES256 JWT, 1-hour TTL | None |
| Userinfo endpoint | Yes | No |
| Token revocation (RFC 7009) | Yes | No |
| Token introspection (RFC 7662) | Yes | No |
| Logout endpoint | RP-initiated (RFC 8414) | Implicit; clear session at BluAuth cookie |
| Pairwise subjects | Yes | No — raw user UUID in ud-sub |
| PKCE | Required | N/A |
| Library support | Any conformant OIDC client | Bespoke integration per app |
| Provider-token brokering | Supported | Not supported |
| Webhook-eligible | Same events | Same events |
The two flows share identity resolution, provider config, session creation, and webhook emission. The only real difference is how identity is handed back to the calling app — a signed JWT vs. unsigned query params.
Session side-effects
Completing either flow leaves the user with an active BluAuth session (Better Auth cookie on the auth.example.com origin) and a row in the sessions table. The legacy flow does not grant the downstream app any cryptographic proof of that session — the proof lives only in the unsigned ud-* params. Downstream apps wanting stronger guarantees should either:
- Reconfirm identity server-side by calling
/api/auth/security-stateon the BluAuth origin (only works for same-origin apps), or - Switch to OIDC and validate the ID token.
Provider-token brokering is not supported
The legacy path cannot carry upstream provider tokens. If your app needs brokered tokens for Google Drive, Microsoft Graph, or any other upstream API, you must integrate via OIDC — Provider Token Brokering is keyed on BluAuth-issued access tokens, which the legacy flow does not produce.
Webhook emission
The legacy flow still publishes the same lifecycle events as OIDC:
session.createdfires on successful sign-in through/oauth2/callback.user.createdfires on first sign-in when a new canonical user is provisioned.account.linkedfires whenintent=linkcompletes.
If your downstream app subscribes to these webhooks, it receives events from both flows uniformly — there is no separate legacy event stream.
Lifecycle
The legacy routes read from the same identity_providers and oauth_clients tables as the OIDC flow — changes to a provider's discovery document or a client's redirect URI list affect both paths simultaneously. The routes are not deprecated on any fixed timeline. BluAuth will keep them operational as long as at least one Blu app depends on them.
That said: there is no new feature investment in this path. Improvements to consent UX, token telemetry, session revocation, and provider-token brokering all target OIDC first. If you file an issue against the legacy flow, the likely answer is "migrate to OIDC."
Checklist before shipping a legacy-flow integration
Only if you absolutely cannot use OIDC today:
- Confirm with a BluAuth admin that no OIDC client can be registered for your app.
- Register your
returnTowith the client'sredirectUris. - Verify every downstream consumer of
ud-*treats the values as hints, not facts. - Ensure your app does a server-side identity reconfirmation before any sensitive action.
- Open a migration ticket referencing this doc — the legacy flow is not a destination.