---
title: "Add OAuth Login"
url: "https://effect-auth.itsbroly.com/recipes/add-oauth-login/"
description: "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.

```text
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-the-provider]

Register this exact URI in the provider console and use it everywhere, including scheme, host, path, and trailing slash:

```text
https://app.example/auth/oauth/google/callback
```

Provider 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](/reference/configuration/).

```ts
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 [#start-in-the-browser]

```ts
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 [#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:

```ts
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](/concepts/sessions/) and [Security Policies](/concepts/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-and-ship]

1. Test unknown providers, denied consent, missing/replayed/expired state, PKCE mismatch, nonce/issuer/audience/signature failure, and an exact redirect-URI mismatch.
2. 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 `returnTo` handling.
3. Test token responses and userinfo as hostile input; rate-limit starts/callbacks, redact secrets, use HTTPS and `HttpOnly`, `Secure`, appropriate `SameSite` cookies.
4. Test the production challenge/account stores under concurrent callbacks and encrypt any retained provider tokens.

See [HTTP Operations](/reference/http-operations/), [Browser Client](/clients/browser-client/), and the [Production Checklist](/guides/production-checklist/).

