# Auth

> Give an account one or more login methods. Issue bearer sessions, reset passwords and revoke access without mixing identity with credentials.

- Package: `@db3.ai/app/auth`
- Canonical page: [https://db3.ai/docs/auth](https://db3.ai/docs/auth)
- Markdown: [https://db3.ai/docs/auth.md](https://db3.ai/docs/auth.md)
- Framework source of truth: `packages/app/src/auth/README.md`

<a id="setup"></a>

## Set up accounts and providers

An account is a `UserIdentity`. A password or Google login is an `AuthProvider` attached to that account. `AuthToken` represents a bearer session; `PasswordResetToken` represents an expiring reset request.

Create one App and enable password authentication under `config.auth.providers.password`. The example uses the built-in identity model. To add application fields, extend `UserIdentity`, retain its inherited fields, and pass your class through `auth.identityModel`.

Your application schema needs the identity, provider, auth-token and reset-token tables. The lab installs them only into its disposable database. Use committed migrations for an application; do not install or reshape tables in a sign-in request.

- [Install packages and configure a test database](https://db3.ai/docs/installation.md#database-labs)
- [App and request context](https://db3.ai/docs/app.md)

<a id="copy"></a>

## Copy the working example

Start in the independent app directory from Installation. This copies a complete lab, including setup and cleanup. The example credentials are local test data, not a default administrator account.

### Copy the shipped example

```bash
mkdir -p examples
cp -R node_modules/@db3.ai/app/src/auth/examples/. examples/
```

<a id="run"></a>

## Run the auth workflow

The result contains only Boolean outcomes, all true. It never prints passwords, bearer tokens or reset tokens. No email is sent. A new test database is created and removed on every run.

### Run the lab

```bash
npx tsx examples/runPasswordAuth.ts
```

<a id="passwords"></a>

## Register and sign in

`registerWithPassword()` creates the account, password provider and first bearer session. `issueTokenForProvider("password", credentials)` authenticates an existing account and creates another session. Invalid credentials return null; a duplicate identity or missing provider configuration raises an error.

Passwords are hashed on the provider record and hidden from JSON. Treat submitted data as untrusted: validate the raw request before registration, rate-limit the endpoint and return a generic invalid-credentials response.

### examples/runPasswordAuth.ts

```typescript
import { pathToFileURL } from 'node:url';
import { AuthProvider, AuthToken, PasswordResetToken, UserIdentity, PASSWORD_AUTH_PROVIDER } from '@db3.ai/app/auth';
import { createGeneratedTestDatabase } from '@db3.ai/app/db/test/db';
import { App } from '@db3.ai/app/server';

/**
 * Exercises password registration, sessions, reset and revocation in a disposable database.
 *
 * Requires a test SQL account allowed to create/drop db3_app_test_* databases.
 * This is a service lab, not a public sign-up route. Real routes must validate
 * requests, rate-limit access and choose a secure session transport.
 *
 * @returns Non-secret outcomes; passwords and issued tokens are never logged.
 */
export async function runPasswordAuth() {
	const database = await createGeneratedTestDatabase('auth_guide');
	const application = new App({ db: database.db, config: { auth: { providers: { password: true } } } });
	try {
		// Lab-only schema setup. Use committed migrations in an application.
		await application.db.install(UserIdentity, AuthProvider, AuthToken, PasswordResetToken);
		const credentials = { name: 'Ada', email: 'ada@example.test', password: 'example-only-password-123' };
		const issued = await application.auth.registerWithPassword(credentials, { expiresInMs: 60 * 60 * 1000 });
		const wrongPasswordRejected = await application.auth.issueTokenForProvider(PASSWORD_AUTH_PROVIDER, { ...credentials, password: 'wrong-password' }) === null;
		const signedIn = await application.auth.issueTokenForProvider(PASSWORD_AUTH_PROVIDER, credentials);
		const authenticated = await application.requestContext.run(async () => {
			const user = await application.auth.authenticateToken(issued.token);
			return user?.id === issued.user.id;
		});
		const reset = await application.auth.createPasswordResetToken(issued.user);
		const replacement = { token: reset.token, password: 'replacement-example-password-456' };
		const passwordChanged = Boolean(await application.auth.resetPassword(replacement));
		const usedResetRejected = await application.auth.resetPassword(replacement) === null;
		const oldPasswordRejected = await application.auth.issueTokenForProvider(PASSWORD_AUTH_PROVIDER, credentials) === null;
		const sessions = await application.auth.tokensFor(issued.user);
		for (const session of sessions) await application.auth.revokeToken(issued.user, session.id!);
		const revokedTokenRejected = await application.requestContext.run(async () => await application.auth.authenticateToken(issued.token) === null);
		return { authenticated, signedIn: Boolean(signedIn), wrongPasswordRejected, passwordChanged, usedResetRejected, oldPasswordRejected, revokedTokenRejected };
	} finally {
		try { await application.close(); } finally { await database.destroy(); }
	}
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
	console.log(JSON.stringify(await runPasswordAuth(), null, 2));
}
```

<a id="sessions"></a>

## Authenticate and revoke sessions

An issued token contains the plaintext bearer value once. Set a lifetime with `expiresInMs` or `expiresAt`. Return it only through your deliberate session transport, and do not log the issued object.

At the HTTP boundary, extract the bearer token and call `authenticateToken()` inside `requestContext.run()`. A null result means 401. Use `requireUser()` only after authentication. Current user/token state and repeated authentication are scoped to that request.

`tokensFor(user)` lists active sessions. Render `toSessionData()` for account settings. `revokeToken(user, sessionId)` checks ownership; `revokeCurrentToken()` revokes the token authenticated in the current request. `logout()` only clears authentication state, so it is not a substitute for durable revocation.

<a id="reset"></a>

## Reset a password

Find the account without revealing whether its email exists. Call `createPasswordResetToken(user)`, then send the plaintext token through your email service. Redeem it with `resetPassword({ token, password })`; an expired, invalid or already-used token returns null.

The example checks sequential reuse, then explicitly revokes existing sessions. Password reset does not automatically revoke those bearer sessions. Your application chooses that policy. Concurrent reset redemption and atomic registration are not covered by this lab; do not infer race-safety from this happy-path example.

<a id="providers"></a>

## Google and additional login methods

Enable the Google provider with the accepted client IDs, then pass a Google Identity Services `credential` to `issueTokenForProvider("google", input)`. The driver verifies that ID token; access/refresh tokens for Google APIs are a separate integration.

Do not merge accounts just because emails match. An existing account without that provider triggers `AuthIdentityExistsError`. Require the signed-in user to link the provider explicitly with `linkProvider()`. List safe settings data through `providersFor()` and `toSummary()`; `unlinkProvider()` refuses to remove the final login method.

Custom drivers implement `AuthProviderDriver.verify()` and return a normalized profile. They prove identity; Auth owns accounts and sessions. The source-backed reference includes Google setup, provider configuration and the complete custom-driver shape. This lab does not call Google or implement a magic-link provider.

<a id="production"></a>

## Run Auth behind your application routes

Auth does not create HTTP routes, choose cookie settings or supply a login UI. Your server must own input validation, rate limiting, HTTPS, trusted proxies, generic recovery responses and authorization after sign-in.

For cookie sessions, choose Secure, HttpOnly and SameSite settings and add CSRF protection for state-changing requests. Do not assume a bearer token in localStorage is protected from injected JavaScript. Require recent authentication before linking or removing a login method.

- [Run the HTTP application](https://db3.ai/docs/create-app.md)
- [Store application records](https://db3.ai/docs/active-record.md)

<a id="testing"></a>

## Testing

Create a `tests/auth` directory and save the test below as `runPasswordAuth.test.ts`. It imports the example you copied into `examples/`. Keep the same folder layout so the relative import resolves.

Run from the application root with the development dependencies from Installation. These are consumer tests, not commands that assume a framework checkout. This database lab needs the same test-only SQL credentials when run through Vitest.

### tests/auth/runPasswordAuth.test.ts

```typescript
import { expect, it } from 'vitest';
import { runPasswordAuth } from '../../examples/runPasswordAuth';

it('runs registration, login, reset and revocation using real SQL and password hashing', async () => {
	expect(await runPasswordAuth()).toEqual({
		authenticated: true, signedIn: true, wrongPasswordRejected: true,
		passwordChanged: true, usedResetRejected: true, oldPasswordRejected: true,
		revokedTokenRejected: true,
	});
});
```

<a id="run-tests"></a>

## Run and extend the test

Run the copied test with the test database configured. Then change a password, try a used reset token, or revoke a session and authenticate it in a new request context. These are real database operations and real password hashes, not mocked authentication.

### Run your copied test and check types

```bash
npx vitest run tests/auth/runPasswordAuth.test.ts
npx tsc --noEmit --target ES2022 --module ESNext --moduleResolution Bundler --types node --skipLibCheck examples/*.ts
```

<a id="coverage"></a>

## Coverage and next steps

Tested here: password registration, sign-in, invalid credentials, bearer authentication, password reset, sequential reset-token reuse rejection and revocation. Explained: custom identities, session projection, provider linking, Google and HTTP security boundaries.

The starter guides now trace rate-limited HTTP login, cookies, logout and private note access. Email/reset delivery, concurrent registration/reset and live Google sign-in still need separate trials. The source-backed service reference and emitted API below cover advanced contracts.

- [Cookie-session login walkthrough](https://db3.ai/docs/guide-auth.md)
- [Authenticated notes API](https://db3.ai/docs/guide-api.md)
- [Auth API](https://db3.ai/docs/auth-api.md)

## Behavioural verification
Exercises password registration, valid/invalid sign-in, bearer authentication, reset-token reuse and session revocation.
- Behaviour test: `packages/app/src/auth/tests/examples/runPasswordAuth.test.ts`
- Repository test command (framework checkout only): `npm run test:service --workspace @platform/app -- auth --maxWorkers=1`
- In an installed application, use the walkthrough commands instead of this repository test.
- Expected outcome: The guide test passes against the real framework components.
- Environment: A dedicated MariaDB/MySQL test account with CREATE/DROP privileges. No email or Google requests.

## Related documentation
- [Install the framework](https://db3.ai/docs/installation.md): Install one runtime package, then build a small HTTP app. Add a database and other services when your feature needs them.
- [Create your first app](https://db3.ai/docs/create-app.md): Start an HTTP server, return a JSON response, reject invalid input and test it without opening a port.
- [ActiveRecord](https://db3.ai/docs/active-record.md): Define your fields once. Create, validate, query and save records without repeating database conversion in every endpoint.
- [Mail](https://db3.ai/docs/mail.md): Build and preview an application email locally, then select a transport when you are ready to send it.

## Framework-owned source: `packages/app/src/auth/README.md`

This is the exact source document captured by the documentation build. Use it for detailed API and workflow guidance, subject to the public package exports and behavioural evidence identified above.

````markdown
# @db3.ai/app/auth

`@db3.ai/app/auth` provides account identities, pluggable login providers,
bearer tokens, password reset tokens, password hashing, and request-scoped
authenticated-user state.

The package is deliberately HTTP-framework agnostic. An app owns its routes,
request validation, cookies or bearer-token transport, rate limiting, and UI.
The auth service owns provider verification, account/provider persistence, and
token issuance.

## Mental Model

An account and a login method are different things:

- `UserIdentity` is the account. It stores the stable application identity:
  name, email, email verification time, account avatar, and account timestamps.
- `AuthProvider` is one way to prove control of that account. A user may have a
  password provider, Google provider, and future providers at the same time.
- `AuthProviderDriver` verifies provider-specific proof and returns one
  normalized `AuthProviderProfile`.
- `Auth` connects verified profiles to accounts, manages provider links, and
  issues sessions.
- `AuthToken` stores hashed bearer tokens. The plaintext token is returned only
  when it is created.
- `PasswordResetToken` stores hashed, expiring, single-use reset tokens.

The important relationship is:

```text
users (account)
  1
  +-- many auth_providers (password, google, future providers)
  +-- many auth_tokens (bearer sessions)
  +-- many password_reset_tokens
```

Password authentication is a provider even though it is first-party rather
than OAuth. This keeps application routes and account settings consistent:
every login starts with a provider, and providers can be listed, linked, or
removed through the same auth service.

## App Setup

Apps normally extend `UserIdentity` and pass the model to the framework app:

```ts
import { UserIdentity } from '@db3.ai/app/auth';
import type { FieldBuilder } from '@db3.ai/app/db';

export class User extends UserIdentity {
	static override table = 'users';

	static override fields(field: FieldBuilder) {
		return {
			...UserIdentity.fields(field),
			selectedWebsiteId: field.string({
				column: 'selected_website_id',
			}),
		};
	}
}
```

```ts
import { App } from '@db3.ai/app/server';
import auth from './config/auth.js';
import { User } from './models/User.js';

const app = new App({
	config: {
		auth,
	},
	auth: {
		identityModel: User,
	},
});
```

The framework `App` constructs one `Auth` service with the application
database, config repository, identity model, and request context. Normal app
code accesses it through `app.auth`.

`UserIdentity.avatarUrl` is the account-owned avatar. An external provider can
supply its initial value. Later provider logins only backfill a missing account
avatar, so a user-selected avatar is not overwritten. The provider's latest
reported image remains available on its `AuthProvider.avatarUrl` row.

## Provider Configuration

Apps configure enabled providers under `auth.providers`. A provider entry can
be `true`, `false`, an options object, or a concrete custom driver instance.

```ts
import { GOOGLE_AUTH_PROVIDER, PASSWORD_AUTH_PROVIDER, type AuthProviderRegistry } from '@db3.ai/app/auth';
import { defineConfig, env } from '@db3.ai/app/config';
import type { User } from '../models/User.js';

const googleClientIds = [
	...env.array('GOOGLE_AUTH_CLIENT_IDS', []),
	env.string('GOOGLE_AUTH_CLIENT_ID'),
].filter((value): value is string => Boolean(value));

export default defineConfig({
	providers: {
		[PASSWORD_AUTH_PROVIDER]: {
			driver: PASSWORD_AUTH_PROVIDER,
		},
		[GOOGLE_AUTH_PROVIDER]: {
			driver: GOOGLE_AUTH_PROVIDER,
			enabled: googleClientIds.length > 0,
			clientIds: googleClientIds,
			hostedDomain: env.string('GOOGLE_AUTH_HOSTED_DOMAIN'),
		},
	},
} satisfies {
	providers: AuthProviderRegistry<User>;
});
```

The auth package reads config values; it does not impose environment variable
names. The names above are the convention used by Scout and are a useful
default for other apps.

## Password Provider

Enable the built-in password driver with:

```ts
[PASSWORD_AUTH_PROVIDER]: {
	driver: PASSWORD_AUTH_PROVIDER,
},
```

Register a password-backed account and issue its first bearer token:

```ts
const issued = await app.auth.registerWithPassword({
	name: 'Ada Lovelace',
	email: 'ada@example.com',
	password: submittedPassword,
});
```

Authenticate an existing password provider:

```ts
const issued = await app.auth.issueTokenForProvider(PASSWORD_AUTH_PROVIDER, {
	email: submittedEmail,
	password: submittedPassword,
});

if (!issued) {
	// Return the app's generic invalid-credentials response.
}
```

Password hashes live on `auth_providers.password`, not on the user row. The
field hashes plaintext during database serialization and never exposes the
stored hash through JSON.

Password-reset routes should find the user without revealing whether the email
exists, call `createPasswordResetToken(user)`, send the plaintext token once,
and later redeem it with `resetPassword(...)`.

## Google Provider

The built-in Google driver accepts a Google Identity Services ID token. It
verifies the signature against Google's JWK set and validates the issuer,
audience, expiry, subject, and optional hosted-domain restriction. It then
normalizes Google claims such as email, name, picture, locale, and Google `sub`.

### Google Cloud setup

1. Create or select a project in Google Cloud Console.
2. Configure the OAuth consent screen and branding.
3. Create an OAuth client with application type **Web application**.
4. Add every browser origin under **Authorized JavaScript origins**, including
   scheme and port for development, such as `http://localhost:5173`.
5. Put the web client id in `GOOGLE_AUTH_CLIENT_ID`. Use
   `GOOGLE_AUTH_CLIENT_IDS` as a comma-separated list while accepting multiple
   deployments or rotating client ids.
6. Optionally set `GOOGLE_AUTH_HOSTED_DOMAIN` to restrict authentication to one
   Google Workspace domain.
7. Load Google Identity Services in the browser, render its official button,
   and send the callback's `credential` ID token to the app's backend.

Useful Google guides:

- Setup and create a web client:
  <https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid>
- Render the official sign-in/sign-up button:
  <https://developers.google.com/identity/gsi/web/guides/display-button>
- Verify Google ID tokens on a backend:
  <https://developers.google.com/identity/gsi/web/guides/verify-google-id-token>

No Google client secret is required to verify a Google Sign-In ID token. OAuth
access and refresh tokens for Search Console, Analytics, Calendar, or other
Google APIs are a separate integration concern and should not be stored on the
authentication provider row.

Authenticate or create the account from a Google credential:

```ts
const issued = await app.auth.issueTokenForProvider(GOOGLE_AUTH_PROVIDER, {
	credential: request.body.credential,
});
```

The first verified use creates a user and Google provider row. Later uses find
the account through Google's stable `sub` claim. If an account already exists
with the same email but Google is not linked, auth throws
`AuthIdentityExistsError`; the app should ask the authenticated user to link
Google explicitly rather than automatically merging accounts by email.

### AI assistant prompt

An app developer can give an AI coding assistant this focused instruction:

```text
Configure Google Sign-In using @db3.ai/app/auth and the app config repository.
Use Google Identity Services in the browser and send its credential ID token to
the backend. Configure the Google provider with GOOGLE_AUTH_CLIENT_ID or
GOOGLE_AUTH_CLIENT_IDS, verify through app.auth.issueTokenForProvider('google',
...), and return the app's normal session response. Do not store Google access
or refresh tokens in auth_providers, do not auto-link accounts by matching email,
and add explicit rate limiting to the public auth route.
```

## Provider Account Management

List linked login methods:

```ts
const providers = await app.auth.providersFor(user);
const summaries = providers.map(provider => provider.toSummary());
```

`toSummary()` is the settings-safe API projection. It excludes password hashes,
provider-owned identifiers, and raw provider profile metadata.

Link a verified provider to an already-authenticated account:

```ts
const provider = await app.auth.linkProvider(user, GOOGLE_AUTH_PROVIDER, {
	credential: googleCredential,
});
```

Remove a provider by row id or provider name:

```ts
await app.auth.unlinkProvider(user, GOOGLE_AUTH_PROVIDER);
```

`unlinkProvider` throws `AuthLastProviderError` when removal would leave the
account with no login method. Provider linking and removal routes must require a
recently authenticated user; the package does not make that HTTP policy choice
for the app.

## Custom Providers

A custom provider implements `AuthProviderDriver` and translates its proof into
an `AuthProviderProfile`. Drivers verify proof only; they do not create users or
issue sessions.

```ts
import type { AuthProviderDriver, AuthProviderProfile } from '@db3.ai/app/auth';

export class ExampleAuthProvider implements AuthProviderDriver<{
	token: string;
}> {
	readonly provider = 'example';

	async verify(input: { token: string }): Promise<AuthProviderProfile | null> {
		const externalUser = await verifyExampleToken(input.token);

		if (!externalUser) return null;

		return {
			provider: this.provider,
			providerUserId: externalUser.id,
			email: externalUser.email,
			emailVerifiedAt: externalUser.emailVerified ? new Date() : null,
			name: externalUser.name,
			avatarUrl: externalUser.avatarUrl,
		};
	}
}
```

Register the concrete driver through `AuthOptions.providers` or the app's auth
provider config. Magic-link authentication can use the same normalized profile
contract, but token generation, email delivery, expiry, and single-use
redemption must be implemented as a dedicated first-party driver/service.

## Bearer Sessions

`issueTokenForProvider(...)` returns an `IssuedAuthToken` containing the
plaintext bearer token once. Persist only the hashed `AuthToken` record.

At an HTTP boundary, read the bearer value and authenticate it:

```ts
const user = await app.auth.authenticateToken(bearerToken);

if (!user) {
	// Return 401.
}
```

With the framework request context enabled, repeated authentication of the same
token during one request is memoized and `app.auth.user` is request-scoped.
Use `app.auth.requireUser()` only after the route has established authentication.

Browser apps can attach session metadata through `AuthTokenOptions` when issuing
a token:

```ts
const issued = await app.auth.issueTokenForProvider('password', credentials, {
	name: 'Chrome on macOS',
	ipAddress: request.ip,
	userAgent: request.headers['user-agent'],
	browser: 'Chrome',
	operatingSystem: 'macOS',
	device: 'Desktop',
});
```

The framework stores the raw user agent for server-side diagnostics but hides it,
along with the token hash, from model JSON. `AuthToken.toSessionData()` returns a
safe account-management projection.

List and revoke active sessions through the authenticated account:

```ts
const sessions = await app.auth.tokensFor(user);
await app.auth.revokeToken(user, sessionId);
await app.auth.revokeCurrentToken();
```

Revocation sets `revokedAt`; the next bearer-token authentication rejects the
session before loading its user. `tokensFor(...)` excludes revoked and expired
sessions. The current authenticated token is available as `app.auth.token`
inside the request context so an app can mark the current browser session.

## Security Responsibilities

The auth package validates credentials and tokens, but the application still
owns HTTP security controls:

- Rate-limit public sign-in, sign-up, provider, forgot-password, and reset routes.
- Use generic invalid-credential and forgot-password responses to reduce account
  enumeration.
- Accept bearer tokens only over HTTPS in production.
- Configure Fastify `trustProxy` only for known reverse proxies before using
  request IPs as rate-limit keys.
- Use a shared rate-limit store when API traffic is handled by multiple processes.
- Let users inspect and revoke active bearer sessions, and revoke the current
  token during sign-out rather than only deleting browser storage.
- Keep provider client secrets and integration refresh tokens out of JSON,
  browser config, and `auth_providers.profile`.
- Require an authenticated user before linking or unlinking providers.
- Do not automatically link an external provider to an existing account solely
  because an email claim matches.

Applications can provide explicit route middleware with tools such as
`@fastify/rate-limit`; keep the concrete limits and store configuration in the
consuming application's server configuration.

Revocable bearer sessions do not prevent JavaScript from reading a token stored
in `localStorage`. Browser apps with an XSS-sensitive threat model should use a
Secure, `HttpOnly`, `SameSite` cookie transport and add CSRF protection for
state-changing requests. That transport is app-owned and is not automatically
selected by this HTTP-framework-agnostic package.

## Verification

The [password auth lab](./examples/runPasswordAuth.ts) exercises real SQL
registration, correct/incorrect sign-in, bearer authentication, password reset,
sequential reset-token reuse rejection and revocation. Copy it from the installed
package into `examples` and run `npx tsx examples/runPasswordAuth.ts` with a
dedicated test account allowed to create/drop `db3_app_test_*` databases.
It creates a unique database and removes it in `finally`; no email is sent and
no token is logged. It is not a public HTTP auth endpoint.

The lab does not establish race-safety for concurrent registration or reset
redemption. Resetting a password does not automatically revoke existing bearer
sessions; the application must choose and enforce that policy. The website's
Auth page includes the exact test to copy into an independent application.

Run the auth package tests from the repository root:

```sh
npm run test:service --workspace packages/app -- auth
npm run check --workspace packages/app
```
````

## Guidance for AI tools
Use the documented public import `@db3.ai/app/auth` and its exported types. Prefer the source-backed examples and behavioural outcomes above over invented APIs or source-relative internal imports.
