Skip to main content

Providers

WIP

Password, magic-link, six OAuth providers, WebAuthn passkeys, API keys, and SAML. Every provider is an instance of AuthProvider.IProvider.

What is a provider?

A provider is a typed sign-in method. Every provider implements the same two-step interface:

interface AuthProvider.IProvider<Begin, Complete, Profile> {
  id: string
  begin(input: Begin, ctx: ProviderContext): Promise<Intent[]>
  complete(input: Complete, ctx: ProviderContext): Promise<Intent[]>
}
  • begin - start the flow. May redirect (OAuth), send a message (magic-link), or be a no-op (password, api-key).
  • complete - verify the result. Returns Intent[] - usually a startSession intent followed by a redirect.

You register providers on AuthEngine:

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

export const auth = new AuthEngine({
  // ...
  providers: [
    password({ ... }),
    authGoogle({ ... }),
  ],
})

// or after construction:
auth.providers.use(magicLink({ ... }))

The shipped providers

ProviderImportWhen to pick
Password@gentleduck/auth/providers/passwordDefault. Email + password.
Magic link@gentleduck/auth/providers/magic-linkPasswordless. Email, SMS, or WebPush.
Google OAuth@gentleduck/auth/providers/oauth/googleConsumer SaaS.
GitHub OAuth@gentleduck/auth/providers/oauth/githubDeveloper tooling.
LinkedIn OAuth@gentleduck/auth/providers/oauth/linkedinB2B / professional.
Microsoft OAuth@gentleduck/auth/providers/oauth/microsoftEnterprise. Entra ID.
Discord OAuth@gentleduck/auth/providers/oauth/discordCommunities.
Apple OAuth@gentleduck/auth/providers/oauth/appleiOS apps.
Passkey@gentleduck/auth/providers/passkeyStrongest factor. WebAuthn.
API key@gentleduck/auth/providers/api-keyLong-lived bearer tokens for integrations.
SAML@gentleduck/auth/providers/samlEnterprise SSO. IdP-initiated.

The provider context

Every begin and complete call receives a ProviderContext:

interface ProviderContext {
  auth: AuthEngine
  req: { headers: Headers; method: string; url: string }
  tenantId?: string
  events: AuthEvents.IBus
  limiter: AuthLimiter.ILimiter
  i18n?: I18n.IResolver
  trace?: { spanId: string; traceId: string }
}

Use ctx.auth.identities to look up identities, ctx.auth.sessions to mint sessions, and ctx.events.emit(...) to broadcast custom audit events.

Common features across providers

Rate limiting

Every provider takes a limiterKeyPrefix:

password({
  limiterKeyPrefix: 'signin:password:',  // default
  // ...
})

Combined with the configured limiter, this bucket trips AUTH/RATE_LIMITED after the threshold and emits a lockout event.

onSignIn hook

For OAuth, SAML, and other federated providers, onSignIn is the extension point that lets you provision identities just-in-time:

authGoogle({
  // ...
  onSignIn: async ({ profile, tenantId }) => {
    let identity = await auth.identities.findByProviderSub({
      providerId: 'google',
      sub: profile.sub,
    })
    if (!identity) {
      identity = await auth.identities.create({
        email: profile.email,
        emailVerified: profile.email_verified,
        profile: { displayName: profile.name, avatarUrl: profile.picture },
      })
      await auth.identities.linkProvider(identity.id, {
        providerId: 'google',
        sub: profile.sub,
      })
    }
    return { identityId: identity.id }
  },
})

Always returns { identityId: string } - the runtime then mints the session and emits the intents.

Profile mapping

profileToIdentityProfile lets you control how the IdP profile maps to your typed Profile generic:

authGoogle({
  // ...
  profileToIdentityProfile: (profile) => ({
    displayName: profile.name,
    avatarUrl: profile.picture,
    locale: profile.locale,
  }),
})

Writing a custom provider

Implement AuthProvider.IProvider:

import type { AuthProvider } from '@gentleduck/auth/core'

interface IBeginInput { email: string }
interface ICompleteInput { token: string }

export function myProvider<Profile>(opts: {
  findIdentityByEmail: (email: string) => Promise<{ id: string } | null>
}): AuthProvider.IProvider<IBeginInput, ICompleteInput, Profile> {
  return {
    id: 'my-provider',
    async begin({ email }, ctx) {
      const identity = await opts.findIdentityByEmail(email)
      if (!identity) return [{ kind: 'json', status: 404, body: { error: 'not found' } }]
      // mint and dispatch a token...
      return [{ kind: 'json', status: 200, body: { sent: true } }]
    },
    async complete({ token }, ctx) {
      // verify the token...
      const session = await ctx.auth.sessions.create({ identityId, aal: 'aal1', amr: ['custom'] })
      return [{ kind: 'startSession', session }]
    },
  }
}

Register the same way:

auth.providers.use(myProvider({ findIdentityByEmail: ... }))