OAuth providers
WIPSix 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.
| Provider | Import | OIDC? | Default scopes |
|---|---|---|---|
@gentleduck/auth/providers/oauth/google | yes | openid, email, profile | |
| GitHub | @gentleduck/auth/providers/oauth/github | no | read:user, user:email |
@gentleduck/auth/providers/oauth/linkedin | yes | openid, profile, email | |
| Microsoft | @gentleduck/auth/providers/oauth/microsoft | yes | openid, email, profile |
| Discord | @gentleduck/auth/providers/oauth/discord | no | identify, email |
| Apple | @gentleduck/auth/providers/oauth/apple | yes | email, 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
| Code | When |
|---|---|
AUTH/PROVIDER_FAILED | IdP returned a non-2xx (Google rejected the code, Apple JWKS unreachable). |
AUTH/OAUTH_STATE_MISMATCH | Signed state failed verification or expired. |
AUTH/OAUTH_NONCE_REPLAY | OIDC nonce already seen. |
AUTH/OAUTH_REUSE_DETECTED | Refresh-token family reuse - entire family revoked. |