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:
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 } },})Auth flow
Section titled “Auth flow”Two common paths run through the callback handler:
Sign-in
Section titled “Sign-in”- User initiates sign-in (
GET /api/auth/:provider) - Browser is redirected back with
?code=&state= gauvalidates state/CSRF + PKCE, finds provider- Provider validates
code-> returns{ tokens, user (provider profile) } onOAuthExchangeruns- If it returns
{ handled: true, response }:gauclears temp cookies and returns your response (STOP) - Otherwise, continue
- If it returns
mapExternalProfileruns to adjustproviderUser- If provider is link-only:
gaureturns 400 (STOP) gaufinds/creates a user and links account (first time) or updates tokens (existing)onAfterLinkAccountruns with action ‘link’ or ‘update’- Session is created and response is returned
Linking
Section titled “Linking”- User initiates linking while signed in (
GET /api/auth/link/:provider) - Browser is redirected back with
?code=&state= gauvalidates state/CSRF + PKCE, finds provider- Provider validates
code→ returns{ tokens, user (provider profile) } onOAuthExchangeruns- If it returns
{ handled: true, response }:gauclears temp cookies and returns your response (STOP) - Otherwise, continue
- If it returns
mapExternalProfileruns to adjustproviderUseronBeforeLinkAccountruns: may block linking (returnallow:falseor custom response)gaulinks the account and persists tokensonAfterLinkAccountruns with action ‘link’- Session is (re)created and response is returned
onOAuthExchange
Section titled “onOAuthExchange”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.validateCallbackreturns 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 }}mapExternalProfile
Section titled “mapExternalProfile”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,})onBeforeLinkAccount
Section titled “onBeforeLinkAccount”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 }) }onAfterLinkAccount
Section titled “onAfterLinkAccount”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.}