Skip to content

Hooks

gau exposes hooks to let you intercept and customize the OAuth flow. You can modify user profiles, block linking, or take over the response entirely.

Hooks are configured in createAuth and receive context about the current request (provider, tokens, user info, etc.).

Hook syntax:

auth.ts
import { createAuth } from '@rttnd/gau/core'
import { GitHub } from '@rttnd/gau/oauth'
export const auth = createAuth({
providers: [
GitHub({ clientId: process.env.AUTH_GITHUB_ID!, clientSecret: process.env.AUTH_GITHUB_SECRET! }),
],
onHook: async (context) => {
return { handled: false }
},
})

Two common paths run through the callback handler:

  • User initiates sign-in (GET /api/auth/:provider)
  • Browser is redirected back with ?code=&state=
  • gau validates state/CSRF + PKCE, finds provider
  • Provider validates code -> returns { tokens, user (provider profile) }
  • onOAuthExchange runs
    • If it returns { handled: true, response }: gau clears temp cookies and returns your response (STOP)
    • Otherwise, continue
  • mapExternalProfile runs to adjust providerUser
  • If provider is link-only: gau returns 400 (STOP)
  • gau finds/creates a user and links account (first time) or updates tokens (existing)
  • onAfterLinkAccount runs with action ‘link’ or ‘update’
  • Session is created and response is returned
  • User initiates linking while signed in (GET /api/auth/link/:provider)
  • Browser is redirected back with ?code=&state=
  • gau validates state/CSRF + PKCE, finds provider
  • Provider validates code → returns { tokens, user (provider profile) }
  • onOAuthExchange runs
    • If it returns { handled: true, response }: gau clears temp cookies and returns your response (STOP)
    • Otherwise, continue
  • mapExternalProfile runs to adjust providerUser
  • onBeforeLinkAccount runs: may block linking (return allow:false or custom response)
  • gau links the account and persists tokens
  • onAfterLinkAccount runs with action ‘link’
  • Session is (re)created and response is returned

Receives { request, providerId, state, code, codeVerifier, callbackUri, redirectTo, cookies, providerUser, tokens, isLinking, sessionUserId } Return { handled: true, response } to stop; { handled: false } to continue.

When it runs:

  • After provider.validateCallback returns tokens and provider profile.
  • Before any profile mapping, link-only checks, user lookup/linking/creation, or token update.

Use it to:

  • Capture raw provider tokens and short-circuit with a custom Response.
  • Perform a custom token exchange and return results directly.

Snippet:

onOAuthExchange: async ({ tokens }) => {
const access = tokens.accessToken()
// optionally: const refresh = tokens.refreshToken?.()
return { handled: false }
}

When it runs (timeline):

  • After onOAuthExchange (if not short-circuited)
  • Before link-only enforcement and any persistence.

Use it to:

  • Normalize or augment providerUser (name/email/avatar fields).
mapExternalProfile: async ({ providerUser }) => ({
name: providerUser.name?.trim(),
avatar: providerUser.avatar ?? undefined,
})

When it runs (timeline):

  • Only when an account is about to be linked (new link for a user).
  • After mapExternalProfile and (for sign-in) after user lookup/creation.

Use it to:

  • Gate account linking with custom business logic (entitlements, org membership, etc.).
onBeforeLinkAccount: async ({ userId, providerId }) => {
const allowed = await checkEntitlements(userId, providerId)
if (!allowed)
return { allow: false }
return { allow: true }
}

You can also return a custom Response:

return { allow: false, response: new Response('blocked', { status: 403 }) }

When it runs (timeline):

  • After the account has been linked for the first time (action: ‘link’).
  • After an existing linked account’s tokens were updated on sign-in (action: ‘update’).

Use it to:

  • Audit/log, notify, enqueue background work, or sync data.
onAfterLinkAccount: async ({ action, providerId, userId }) => {
// audit/log, enqueue jobs, etc.
}