Add OAuth Login
Add an OAuth or OIDC provider to an existing cookie-session application.
Provider login makes your application an OAuth client of Google, GitHub, or another identity provider. It is not effect-auth's provider mode: OAuthProviderAuthorizationHttpApiLive, OAuthTokenHttpApiLive, client registration, consent, introspection, and OIDC discovery are for turning your app into an OAuth server. This recipe uses OAuthHttpApiLive only for starting an upstream login.
browser your app provider
| POST /auth/oauth/start |
|<-- authorizationUrl (state + PKCE) |
|---------------- authorize ----------->|
|<------------- code -------------------|
| GET /auth/oauth/google/callback |
| verify state |
| exchange code + verifier |
| verify ID token / profile|
| resolve account policy |
|<-- session cookie or continuation ----|Register the provider
Register this exact URI in the provider console and use it everywhere, including scheme, host, path, and trailing slash:
https://app.example/auth/oauth/google/callbackProvider presets supply known endpoints and defaults. makeGenericOidcProvider accepts explicit issuer, authorization, token, userinfo, and JWKS endpoints when no preset fits. Credentials belong in application configuration, not OAuthHttpConfigLive; see Configuration.
import { Layer } from "effect";
import {
makeGoogleOidcProvider,
OAuthProvidersLive,
OAuthStateLive,
} from "@effect-auth/core/OAuth";
import {
OAuthHttpApiLive,
OAuthHttpConfigLive,
} from "@effect-auth/core/HttpApi";
export const google = makeGoogleOidcProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
redirectUri: "https://app.example/auth/oauth/google/callback",
});
const OAuthLoginLive = OAuthHttpApiLive.pipe(
Layer.provide(OAuthHttpConfigLive({
// Store by challengeId in an encrypted, short-lived, one-use server record.
storeCodeVerifier: ({ started }) => verifierStore.put(started),
})),
Layer.provide(OAuthProvidersLive([google])),
Layer.provide(OAuthStateLive),
Layer.provide(ExistingChallengeCryptoAndSessionLive),
);Mount that layer with the existing auth API. OAuthStateLive creates unpredictable state, an S256 PKCE verifier/challenge, and an OIDC nonce. Never put the verifier in a browser-readable cookie or trust a callback-supplied redirect URI. The challenge store must consume state and its verifier once and expire both promptly.
Start in the browser
import { createOAuthClient } from "@effect-auth/core/Client";
const oauth = createOAuthClient();
const started = await oauth.authorization.start({
providerId: "google",
includeNonce: true,
metadata: { returnTo: "/settings" },
});
location.assign(started.authorizationUrl);Allow-list returnTo as a local application path before redirecting after login. Do not accept arbitrary continuation URLs.
Own the callback
The callback is application-specific. Parse provider errors, load and consume the verifier associated with verified state, then use public OAuth primitives:
const callback = yield* completeOAuthCallback({
provider: google,
state: query.state,
code: OAuthAuthorizationCode(query.code),
codeVerifier: storedVerifier,
});
const verified = yield* verifyOidcIdToken({
provider: google,
idToken: callback.tokens.idToken!,
nonce: callback.state.nonce,
});
if (!verified.valid) return { status: "denied" as const };
const profile = yield* normalizeOAuthProfile({
provider: google,
claims: verified.claims,
}).pipe(Effect.provide(OAuthOidcProfileNormalizerLive));
const resolution = yield* resolveOAuthIdentityBridge({
identity: verifiedOAuthIdentityFromProfile(profile),
currentUserId,
makeUser,
makeAccount,
makeEmailIdentity,
});For OAuth-only providers, fetch their user endpoint and use the matching normalizer rather than treating unverified JSON as OIDC claims. Always key accounts by (providerId, providerAccountId), not email. OAuthIdentityBridge checks that provider account first, so a known account signs in even if the current callback has no email.
Provide OAuthIdentityBridgePolicy to decide whether JIT user creation is allowed, whether this provider's verified-email claim is trusted, and whether to create a local email identity. Email is ignored for bridging unless the policy trusts it and the provider marked it verified. A new provider account may therefore atomically create a bare user with no email. When trusted email already belongs to another user, the bridge returns require-explicit-linking; it never silently merges users. Continue through link confirmation and any required step-up.
The maintained OAuthIdentityBridgeStore transaction creates the user, optional verified email identity, and OAuth account together. Provider-account races resolve to the existing winner and email collisions return explicit linking, with no partial user. Custom storage must preserve that atomic boundary. On success, pass trusted server-produced oauthEvidence({ providerId, providerAccountId, verifiedAt }) through AuthFlow; it derives local aal1 and canonical oauth. If MFA or approval is required, return that continuation instead. See Sessions and Security Policies.
Provider access, refresh, and ID tokens are not session tokens. Discard them unless the app calls the provider later. If persisted, use OAuthProviderTokenLifecycle with OAuthProviderTokenVault: it encrypts token ciphertext before storage. Keep its encryption key separate, restrict decrypt access, rotate keys deliberately, and never log tokens.
Test and ship
- Test unknown providers, denied consent, missing/replayed/expired state, PKCE mismatch, nonce/issuer/audience/signature failure, and an exact redirect-URI mismatch.
- Test existing provider-account sign-in without email, email-less JIT creation, trusted verified-email creation, ignored unverified/untrusted email, email collision requiring explicit linking, cross-user takeover denial, MFA continuation, session cookie commitment, and safe
returnTohandling. - Test token responses and userinfo as hostile input; rate-limit starts/callbacks, redact secrets, use HTTPS and
HttpOnly,Secure, appropriateSameSitecookies. - Test the production challenge/account stores under concurrent callbacks and encrypt any retained provider tokens.
See HTTP Operations, Browser Client, and the Production Checklist.