Skip to main content

OAuth providers

WIP

Six OAuth providers (Google, GitHub, LinkedIn, Microsoft, Discord, Apple), all with PKCE-S256, signed state, and RFC 6749 refresh-token reuse detection.

Shipped providers

Every OAuth provider duck-auth ships is a thin wrapper around the generic oauthProvider core, pre-configured with the IdP's URLs and default scopes.

ProviderImportOIDC?Default scopes
Google@gentleduck/auth/providers/oauth/googleyesopenid, email, profile
GitHub@gentleduck/auth/providers/oauth/githubnoread:user, user:email
LinkedIn@gentleduck/auth/providers/oauth/linkedinyesopenid, profile, email
Microsoft@gentleduck/auth/providers/oauth/microsoftyesopenid, email, profile
Discord@gentleduck/auth/providers/oauth/discordnoidentify, email
Apple@gentleduck/auth/providers/oauth/appleyesemail, name

Shared shape

Every OAuth provider takes the same shape (with provider-specific defaults already applied):

import { authGoogle } from '@gentleduck/auth/providers/oauth/google'

authGoogle({
  clientId: process.env.GOOGLE_CLIENT_ID!,
  clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
  redirectUri: 'https://app.example.com/auth/google/callback',
  stateSigningSecret: process.env.OAUTH_STATE_SECRET!,
  scopes: ['openid', 'email', 'profile'],
  onSignIn: async ({ profile, tenantId }) => {
    // Provision identity here. Must return { identityId }.
    return { identityId: '...' }
  },
  profileToIdentityProfile: (profile) => ({
    displayName: profile.name,
    avatarUrl: profile.picture,
  }),
})

What duck-auth handles for you

Every OAuth provider gets:

PKCE S256

A code verifier is minted at begin, the S256 challenge is sent to the IdP, and the verifier is round-tripped through state. At complete, the verifier is sent on the token exchange. This is not optional - duck-auth always uses PKCE, even for confidential clients.

Signed state

The state parameter is signed (HMAC-SHA256) by stateSigningSecret, includes the PKCE verifier, an expiry timestamp (10 minutes), and a nonce. At complete, the signature is verified and the timestamp checked. Tampering or replay returns AUTH/OAUTH_STATE_MISMATCH.

OIDC nonce (for OIDC providers)

For providers that issue an id_token (Google, LinkedIn, Microsoft, Apple), a nonce is generated at begin, included in the authorize URL, and verified against the JWT claim at complete. Re-use of the same nonce returns AUTH/OAUTH_NONCE_REPLAY.

Refresh-token family reuse detection

Per RFC 6749 section 10.4, refresh tokens are rotated on every use and tracked in a family. If a refresh token is presented twice (once by the legitimate client, once by a stolen copy), duck-auth revokes the entire family and returns AUTH/OAUTH_REUSE_DETECTED. The user is forced through a fresh authorize hop.

CSRF defence via signed state

state is the OAuth CSRF defence: the request returns to your callback with the signed state, which must match what was minted at begin. duck-auth uses HMAC, not opaque storage, so the defence works without a session store hit.

Wiring sign-in

// Client
await client.beginProvider('google', {})
// -> response includes a redirect intent; the framework adapter sets
//   the Location header and the browser follows.

// After Google bounces back:
// (your /auth/google/callback handler)
await client.signIn({
  providerId: 'google',
  input: { code, state, codeVerifier },
})
// -> 200 { session, identity }

The mounted server adapter handles the OAuth callback parsing for you; you only need to call signIn.

Generic OAuth

For an IdP not yet shipped, use oauthProvider directly:

import { oauthProvider } from '@gentleduck/auth/providers/oauth'

const customIdp = oauthProvider({
  providerId: 'my-idp',
  authorizationEndpoint: 'https://idp.example.com/authorize',
  tokenEndpoint: 'https://idp.example.com/token',
  userInfoEndpoint: 'https://idp.example.com/userinfo',
  // ... same options as shipped providers
})

Errors

CodeWhen
AUTH/PROVIDER_FAILEDIdP returned a non-2xx (Google rejected the code, Apple JWKS unreachable).
AUTH/OAUTH_STATE_MISMATCHSigned state failed verification or expired.
AUTH/OAUTH_NONCE_REPLAYOIDC nonce already seen.
AUTH/OAUTH_REUSE_DETECTEDRefresh-token family reuse - entire family revoked.