Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/docs/.starlight-icons/safelist.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@
"i-ph:rocket-launch-duotone",
"i-ph:rows-duotone",
"i-ph:shield-check-duotone",
"i-ph:shield-warning-duotone",
"i-ph:terminal-window-duotone",
"i-ph:user-check-duotone",
"i-simple-icons:github",
"i-simple-icons:xbox",
"i-starlight-plugin-icons:folder",
"i-starlight-plugin-icons:folder-open"
]
]
1 change: 1 addition & 0 deletions packages/docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export default defineConfig({
{ icon: 'i-ph:user-check-duotone', label: 'Role-Based Access Control', slug: 'guides/role-based-access-control' },
{ icon: 'i-ph:head-circuit-duotone', label: 'Advanced Use Cases', slug: 'guides/advanced' },
{ icon: 'i-ph:plug-duotone', label: 'Hooks', slug: 'guides/hooks' },
{ icon: 'i-ph:shield-warning-duotone', label: 'Error Handling', slug: 'guides/error-handling' },
{ icon: 'i-ph:shield-check-duotone', label: 'Security', slug: 'guides/security' },
],
},
Expand Down
186 changes: 146 additions & 40 deletions packages/docs/src/content/docs/guides/advanced.mdx
Original file line number Diff line number Diff line change
@@ -1,14 +1,154 @@
---

title: Advanced Use Cases
description: Programmatic sessions and custom authentication flows in gau.
---

import { Steps, Tabs, TabItem, Aside, Code } from '@astrojs/starlight/components'

Beyond standard OAuth flows, `gau` exposes APIs for building custom auth flows.
Beyond standard OAuth flows, `gau` exposes APIs for building custom auth flows. This includes built-in **user impersonation** for support staff, **programmatic session issuing** for guest logins and invites, and **session refresh** to keep users signed in.

## The `issueSession` method
## User Impersonation

Allow admins or support staff to sign in as other users for debugging, customer support, or demonstrations. This is an opt-in feature that requires explicit configuration.

### Configuration

Enable impersonation in your `createAuth` configuration:

```ts title="auth.ts" ins={8-16}
export const auth = createAuth({
adapter,
providers,
roles: {
adminRoles: ['admin'],
adminUserIds: ['special-admin-id'],
},
impersonation: {
enabled: true,
allowedRoles: ['admin', 'support'], // Who can impersonate (defaults to adminRoles)
cannotImpersonate: ['admin'], // Who cannot be impersonated (defaults to adminRoles)
maxTTL: 3600, // Max duration in seconds (default: 1 hour)
onImpersonate: ({ adminUserId, targetUserId, reason, timestamp }) => {
console.log(`Audit: ${adminUserId} impersonated ${targetUserId}: ${reason}`)
},
},
})
```

**Options:**

- `enabled` - Set to `true` to enable the impersonation feature
- `allowedRoles` - Array of roles that can impersonate others (defaults to `adminRoles`)
- `cannotImpersonate` - Array of roles that cannot be impersonated (defaults to `adminRoles`)
- `maxTTL` - Maximum duration of an impersonation session in seconds (default: 3600)
- `onImpersonate` - Audit hook called when impersonation starts

### Starting Impersonation

Use the `startImpersonation` method to begin impersonating a user:

```ts title="routes/api/admin/impersonate/+server.ts"
import { auth } from '$lib/server/auth'

export async function POST({ request, locals }) {
const session = await locals.getSession()

// Verify the requester is an admin
if (!session?.user?.isAdmin)
return new Response('Forbidden', { status: 403 })

const { targetUserId, reason } = await request.json()

// Start impersonation
const result = await auth.startImpersonation(session.user.id, targetUserId, {
reason, // Optional: passed to onImpersonate hook
ttl: 60 * 30, // Optional: 30 minutes (capped by maxTTL)
})

if (!result)
return new Response('Impersonation failed', { status: 500 })

// Set both cookies - impersonation session and stash for original session
return new Response(JSON.stringify({ success: true }), {
headers: [
['Set-Cookie', result.cookie],
['Set-Cookie', result.originalCookie],
],
})
}
```

**Result:**

- `token` - The impersonation session JWT containing `impersonatedBy` and `impersonationExpiresAt` claims
- `cookie` - Set-Cookie header for the impersonation session
- `originalCookie` - Set-Cookie header to stash the admin's session info
- `maxAge` - The actual TTL used (capped by maxTTL)

### Detecting Impersonation

Check if the current session is an impersonation session:

```ts title="routes/+layout.server.ts"
import { isImpersonating } from '@rttnd/gau'

export async function load({ locals }) {
const session = await locals.getSession()

return {
user: session?.user,
isImpersonating: isImpersonating(session?.session),
impersonatedBy: session?.session?.impersonatedBy,
}
}
```

Use this to show a visual indicator in your UI that the user is in impersonation mode.

### Ending Impersonation

Restore the admin's original session when they're done:

```ts title="routes/api/admin/unimpersonate/+server.ts"
import { auth } from '$lib/server/auth'

export async function POST({ request }) {
const result = await auth.endImpersonation(request)

if (!result)
return new Response('No active impersonation', { status: 400 })

// Set the restored session cookie and clear the stash
const headers = new Headers()
headers.set('Set-Cookie', result.cookie)
result.clearCookies.forEach(cookie => headers.append('Set-Cookie', cookie))

return new Response(JSON.stringify({ success: true }), { headers })
}
```

**Result:**

- `token` - The restored admin session JWT
- `cookie` - Set-Cookie header for the restored session
- `clearCookies` - Array of Set-Cookie headers to clear the stash cookie

### Security Considerations

<Aside type="caution">
Always validate admin status in your route handlers before calling `startImpersonation`. The feature performs its own validation, but defense in depth is recommended.
</Aside>

- Log all impersonation events via the `onImpersonate` hook
- Set a reasonable `maxTTL` (default 1 hour is recommended)
- Display a clear visual indicator when impersonation is active
- Consider requiring additional authentication (e.g., re-entering password) before allowing impersonation
- Regularly audit impersonation logs for suspicious activity

---

## The `issueSession` method
This method lets you programmatically create sessions for any user. It returns both a JWT token and a ready-to-use `Set-Cookie` header.

```ts
Expand All @@ -27,10 +167,13 @@ headers: { Authorization: `Bearer ${token}` }
This enables use cases that OAuth alone can't handle:
- **Guest sessions** - Let users use your app without signing up, and still save their data in the db
- **Invite redemption** - Auto sign-in after claiming an invite
- **Admin impersonation** - Debug issues as a specific user
- **Device claiming** - Authenticate IoT devices or kiosks
- **Magic links / OTP** - Build your own email auth flow

<Aside type="tip">
For **admin impersonation**, use the built-in [User Impersonation](#user-impersonation) feature instead of manually issuing sessions with custom claims. It provides proper session management, audit hooks, and secure session restoration.
</Aside>

## The `refreshSession` method

Extend an existing session's lifetime by issuing a new token. Preserves all custom claims from the original.
Expand Down Expand Up @@ -243,43 +386,6 @@ Build a closed beta where users need a valid invite token to join. When they red

---

## Admin Impersonation

Let admins sign in as other users for debugging or support purposes.

```ts title="routes/api/admin/impersonate/+server.ts"
import { auth } from '$lib/server/auth'

export async function POST({ request, locals }) {
const session = await locals.getSession()

// Verify the requester is an admin
if (!session?.user?.isAdmin)
return new Response('Forbidden', { status: 403 })

const { targetUserId } = await request.json()

// Issue a short-lived session for the target user
const { cookie } = await auth.issueSession(targetUserId, {
data: {
impersonatedBy: session.user.id,
impersonatedAt: Date.now(),
},
ttl: 60 * 30, // 30 minute impersonation session
})

return new Response(JSON.stringify({ success: true }), {
headers: { 'Set-Cookie': cookie },
})
}
```

<Aside type="caution">
Always log impersonation sessions and consider adding an "exit impersonation" feature that restores the admin's original session.
</Aside>

---

## Device / Kiosk Authentication

Authenticate devices that can't perform OAuth flows (IoT devices, kiosks, POS terminals).
Expand Down
15 changes: 15 additions & 0 deletions packages/docs/src/content/docs/guides/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,21 @@ Determines how sessions are handled. See the [Session Management](/guides/sessio

These hooks let you customize the OAuth callback lifecycle, see [Hooks](/guides/hooks/).


## onError

> **Type**: `(context: { error: GauError, request: Request }) => Response | Promise<Response | undefined> | undefined`
> **Required**: No

A hook to intercept and handle errors. See [Error Handling](/guides/error-handling#onerror-hook).

## errorRedirect

> **Type**: `string`
> **Required**: No

The URL to redirect to for user-facing errors. See [Error Handling](/guides/error-handling#errorredirect-option).

## jwt

> **Type**: `object`,
Expand Down
Loading
Loading