# Authentication Guide

Guren ships with a Laravel-inspired authentication stack that sits on top of the session middleware and the ORM layer. The goal is to match the expressiveness of Laravel's guards and user providers while staying idiomatic to TypeScript and Bun.

## Core Concepts

- **AuthManager** – central registry for guards and user providers. Resolved from the service container via `this.container.make<AuthManager>('auth')`.
- **Guards** – runtime objects responsible for authenticating a request. The default `SessionGuard` persists the logged-in user's identifier inside the session and supports optional "remember me" tokens.
- **User Providers** – data access adapters used by guards to load and validate users. `ModelUserProvider` integrates with Guren's `Model` abstraction so you can back authentication with Drizzle ORM tables.
- **Auth Context** – per-request facade that surfaces guard helpers (`auth.check()`, `auth.user()`, `auth.login()`, etc.). The context is attached automatically by `AuthServiceProvider`; it is available in controllers via the `this.auth` helper and in middleware through `attachAuthContext`.
- **OAuthManager** – social login helper that manages provider configs, CSRF-safe OAuth state, code exchange, and user profile fetch.

## Quickstart via CLI

For new applications, run the bundled scaffolder with automatic installation (the session middleware will be auto-attached by default):

```bash
bunx guren make:auth --install
```

This command generates login, registration, and password reset controllers, Inertia pages, a layout, `AuthProvider`, `MailProvider`, user model, SQL migration, and a demo seeder. The `--install` flag automatically:

1. Registers `AuthProvider` and `MailProvider` in your `Application` providers array
2. Adds `createSessionMiddleware` with development-friendly defaults (uses `cookieSecure: true` in production)
3. Wires `registerAuthRoutes(router)` into `routes/web.ts`
4. Updates `db/schema.ts` to include password and remember-token columns

After scaffolding, simply run:

```bash
bun run db:migrate
bun run db:seed
bun run dev
```

Visit `http://localhost:3000/login` and sign in with `demo@example.com` / `secret`, or visit `/register` to create a new account.

Pass `--minimal` to skip the registration and password reset scaffold and generate the login-only experience instead:

```bash
bunx guren make:auth --install --minimal
```

### Password reset

Clicking "Forgot your password?" on the login page walks through `ForgotPasswordController` and `ResetPasswordController`, which use the framework's `createPasswordResetToken` / `verifyPasswordResetToken` primitives under the hood. The reset token is stored with the generated `app/Auth/PasswordResetStore.ts` (an in-memory store — swap it for a Redis-backed store in production or any multi-instance deployment) and emailed via the generated `config/mail.ts`, which defaults to the `log` driver: reset links print straight to the console, so the flow works with zero setup in development. Set `MAIL_DRIVER=smtp` (and the `SMTP_*` environment variables) once you're ready to send real email.

### Email verification

Pass `--verify` to also scaffold an email verification flow:

```bash
bunx guren make:auth --install --verify
```

This adds an `emailVerifiedAt` column to the `users` table, a `VerifyEmailController` (shows a "check your email" notice, resends the verification link, and confirms the token), and a `VerifyEmail` page. Registering now sends a verification email and redirects to `/verify-email` instead of `/dashboard`, and the generated `/dashboard` route is guarded with `requireVerifiedEmail` — unverified users are redirected back to `/verify-email` until they confirm. Verification links use the same in-memory store and `log`-driver mail setup as password reset, so this also works with zero setup in development. `--verify` requires the default (non-minimal) experience, since it builds on the registration flow.

### OAuth login buttons

Pass `--oauth` with a comma-separated provider list to also scaffold "Continue with GitHub / Google / Discord" buttons on the login (and, unless `--minimal` is set, registration) pages:

```bash
bunx guren make:auth --install --oauth github,google
```

This adds a `githubId` / `googleId` column per provider to the `users` table, an `OAuthProvider` that registers each provider against the shared `OAuthManager` (only once its client ID, secret, and redirect URI are all set — see [OAuth / Social login](#oauth-social-login) below for the env var names), and an `OAuthController` with `redirectToProvider` and `callback` actions. The callback looks up the user by provider ID, links to an existing account of the same email only when a fresh signup is unambiguous (otherwise it rejects the login with "sign in with your password instead" rather than silently linking an unverified email), and otherwise creates a new account with a random password before logging the user in. Unlike `--verify`, `--oauth` works with `--minimal` — it doesn't depend on the registration scaffold.

`--oauth` shares its `OAuthController` / `OAuthProvider` file paths and wiring conventions with `guren add oauth` below, just with a complete (not stub) callback — don't run both against the same app, since the second run either aborts (no `--force`) or overwrites the first (`--force`).

## OAuth / Social login

Guren ships first-party OAuth primitives plus provider presets for GitHub / Google / Discord. This is a lower-level, standalone scaffold — for OAuth buttons wired directly into `make:auth`'s login/registration pages with automatic account creation, see [OAuth login buttons](#oauth-login-buttons) above instead.

### Scaffold OAuth in an app

```bash
bunx guren add oauth
```

This creates:

- `app/Providers/OAuthProvider.ts`
- `app/Http/Controllers/Auth/OAuthController.ts`
- `routes/oauth.ts`

and wires `CoreOAuthServiceProvider` + `OAuthProvider` into `src/app.ts`.

### Configure provider credentials

```bash
OAUTH_GITHUB_CLIENT_ID=...
OAUTH_GITHUB_CLIENT_SECRET=...
OAUTH_GITHUB_REDIRECT_URI=https://your-app.test/auth/github/callback
```

Equivalent env names exist for `GOOGLE` and `DISCORD`.

### Route flow

```ts
router.get('/auth/:provider', [OAuthController, 'redirectToProvider'])
router.get('/auth/:provider/callback', [OAuthController, 'callback'])
```

`redirectToProvider` creates a signed state and redirects to the provider consent screen.  
`callback` validates state, exchanges code for token, then fetches the remote profile.

### Post-login redirect (`redirectTo`)

Pass `redirectTo` when starting the flow and read it back — sanitized — after the callback. In the scaffolded `OAuthController` (which resolves the manager with `this.oauth()`):

```ts
// /auth/github?redirectTo=/settings
async redirectToProvider(): Promise<Response> {
  const { url } = await this.oauth().authorize('github', {
    redirectTo: this.request.query('redirectTo'),
  })
  return this.redirect(url)
}

async callback(): Promise<Response> {
  const { profile, redirectTo } = await this.oauth().handleCallback('github', { code, state })
  // ...log the user in...
  return this.redirect(redirectTo ?? '/')
}
```

`redirectTo` is guarded against open redirects on both ends of the flow: only app-relative paths (`/settings`) survive by default. Protocol-relative URLs (`//evil.com`), backslash variants, non-http schemes, and unlisted hosts are dropped — `redirectTo` comes back as `undefined` and your fallback applies.

To allow specific external hosts (wildcards supported), bind the manager with an allowlist before anything resolves it — in a scaffolded app, at the top of `app/Providers/OAuthProvider.ts`'s `register()`:

```ts
this.container.singleton('oauth', () =>
  createOAuthManager({
    stateConfig: { allowedRedirectHosts: ['accounts.example.com', '*.example.org'] },
  }),
)
```

> **Note:** `createRedirectSafetyMiddleware` (opt-in) validates `Location` headers with its own separate `allowedHosts` option. If you mount it, keep both allowlists in agreement — otherwise the middleware rewrites an approved external redirect to `/`.

### Manual Setup

If you prefer to configure manually or already have partial setup, omit the `--install` flag:

```bash
bunx guren make:auth
```

Then manually:
1. Register `AuthProvider` in `src/app.ts`
2. Add `createSessionMiddleware` to your middleware stack (auto-added by `AuthServiceProvider` unless you opt out)
3. Register `registerAuthRoutes(router)` from `routes/web.ts`

The `--install` flag is safe and idempotent – it won't duplicate existing configuration.

## Enabling Sessions

Guards need access to the session. By default, `AuthServiceProvider` will attach `createSessionMiddleware` for you. To customize or disable it, pass auth options to `createApp()`:

```ts
import { createApp } from '@guren/core'

const app = createApp({
  auth: {
    autoSession: true, // set false to opt out
    sessionOptions: {
      cookieSecure: process.env.NODE_ENV === 'production',
    },
  },
})
```

If you need manual control, register the middleware explicitly in `src/app.ts`:

```ts
import { createApp, createSessionMiddleware } from '@guren/core'

const app = createApp()
app.use('*', createSessionMiddleware())
```

`cookieSecure` controls whether the session cookie is marked `Secure` (only sent over HTTPS). In production you should keep it `true`; in local development it is set to `false` by default so cookies work over http://localhost.

**Application auth options**
- `autoSession` (default `true`): automatically attaches `createSessionMiddleware`.
- `sessionOptions` (forwarded to `createSessionMiddleware`):
  - `cookieName` (default `guren.session`)
  - `cookieSecure` (default `true` in production, `false` in dev/local)
  - `cookieSameSite` (default `Lax`)
  - `cookieHttpOnly` (default `true`)
  - `cookieMaxAgeSeconds` (optional; falls back to `ttlSeconds`)
  - `ttlSeconds` (default 2 hours)
  - `store` (default in-memory; swap for your own implementation for multi-instance deployments)

## Configuring Providers & Guards

### Using the `auth.useModel()` Shorthand (Recommended)

The simplest way to configure authentication is using the `auth.useModel()` helper, which registers both a `ModelUserProvider` and `SessionGuard` in one call:

```ts
import { ServiceProvider } from '@guren/core'
import type { AuthManager } from '@guren/core'
import { User } from '@/app/Models/User'

export default class AuthProvider extends ServiceProvider {
  register(): void {
    const auth = this.container.make<AuthManager>('auth')
    auth.useModel(User, {
      usernameColumn: 'email',
      passwordColumn: 'passwordHash',
      rememberTokenColumn: 'rememberToken',
      credentialsPasswordField: 'password',
    })
  }
}
```

This single method call:
- Registers a `ModelUserProvider` with the specified columns
- Creates a `SessionGuard` with proper session handling
- Sets up the default guard as 'web'
- Uses `ScryptHasher` (based on Bun's native scrypt) by default

### Manual Configuration (Advanced)

For advanced use cases requiring custom providers or guards, you can still configure them manually:

```ts
import { ServiceProvider } from '@guren/core'
import { ModelUserProvider, SessionGuard } from '@guren/core'
import type { AuthManager } from '@guren/core'
import { User } from '@/app/Models/User'

export default class AuthProvider extends ServiceProvider {
  register(): void {
    const auth = this.container.make<AuthManager>('auth')

    // Register the provider
    auth.registerProvider('users', () => new ModelUserProvider(User, {
      usernameColumn: 'email',
      passwordColumn: 'passwordHash',
      rememberTokenColumn: 'rememberToken',
      credentialsPasswordField: 'password',
    }))

    // Register a custom guard
    auth.registerGuard('web', ({ session, manager }) => {
      const provider = manager.getProvider('users')
      return new SessionGuard({ provider, session })
    })

    auth.setDefaultGuard('web')
  }
}
```

Pair this with the `AuthenticatableModel` base class (see below) to get automatic password hashing and validation helpers.

### Authenticatable Models

Models that extend `AuthenticatableModel` receive first-class password handling. Providing a plain `password` property when calling `create` or `update` automatically hashes and stores it in the `passwordHash` column (configurable via static properties). The framework never persists the plain text password, and authentication continues to rely on the same hashing algorithm as the providers.

```ts
import { AuthenticatableModel } from '@guren/core'
import { users } from '@/db/schema.js'

export type UserRecord = typeof users.$inferSelect

export class User extends AuthenticatableModel<UserRecord> {
  static override table = users
  static override readonly recordType = {} as UserRecord
  // Optional:
  // static override passwordField = 'plainPassword'
  // static override passwordHashField = 'password_digest'
}
```

The default `AuthServiceProvider` automatically registers a `web` guard that uses the `users` provider. If you need additional guards (e.g. token-based APIs), call `auth.registerGuard('api', factory)` inside the provider and set it as default via `auth.setDefaultGuard('api')` when appropriate.

## Controllers & Routes

Controllers now expose an `auth` helper:

```ts
import { pages } from '@/.guren/pages.gen'

export default class DashboardController extends Controller {
  async index() {
    const user = await this.auth.user()       // returns user or null
    return this.inertia(pages.dashboard.Index, { user }, { url: this.request.path })
  }

  async store() {
    const user = await this.auth.userOrFail()  // throws 401 if not authenticated
    // user is guaranteed non-null here
    await Post.create({ authorId: user.id, ...data })
    return this.redirect('/posts')
  }
}
```

Use `this.validateBody()` / `this.validateQuery()` / `this.validateParams()` with Zod schemas for typed validation. Reserve `FormRequest` for compatibility code.

To surface the logged-in user on every Inertia page without repeating controller code, register shared props during app boot:

```ts
// config/inertia.ts
import { setInertiaSharedProps, AUTH_CONTEXT_KEY, type AuthContext } from '@guren/core'

setInertiaSharedProps(async (ctx) => {
  const auth = ctx.get(AUTH_CONTEXT_KEY) as AuthContext | undefined
  return { auth: { user: await auth?.user() } }
})
```

Sharing `auth.user()` this way is safe by default: the record is sanitized before it leaves the auth layer, so the password hash never reaches the browser (see [Sanitized User Records](#sanitized-user-records)).

Augment `InertiaSharedProps` (see the Controllers guide) to type this `auth` payload for React pages.

> `setInertiaSharedProps` replaces the whole resolver. When several parts of
> your app contribute shared props (auth, i18n, flash, …), use
> `shareInertiaProps` instead — it merges its props over whatever was
> registered before:
>
> ```ts
> import { shareInertiaProps } from '@guren/core'
>
> shareInertiaProps((ctx) => ({ i18n: { locale: detectLocale(ctx) } }))
> ```

Route middleware makes protecting endpoints straightforward:

```ts
import { Router, requireAuthenticated, requireGuest } from '@guren/core'
import LoginController from '@/app/Http/Controllers/Auth/LoginController'
import DashboardController from '@/app/Http/Controllers/DashboardController'

export function registerWebRoutes(router: Router): void {
  router.aliasMiddleware('auth', requireAuthenticated({ redirectTo: '/login' }))
  router.aliasMiddleware('guest', requireGuest({ redirectTo: '/dashboard' }))

  router.middleware('guest').group((guest) => {
    guest.get('/login', [LoginController, 'show'])
    guest.post('/login', [LoginController, 'store'])
  })

  router.middleware('auth').group((auth) => {
    auth.post('/logout', [LoginController, 'destroy'])
    auth.get('/dashboard', [DashboardController, 'index'])
  })
}
```

## Session Guard Helpers

- `auth.check()` – resolves to `true` when a user is authenticated.
- `auth.user()` – returns the current user record (or `null`), sanitized: the password hash, remember token, and the model's `hidden` fields are stripped.
- `auth.userOrFail()` – returns the current user or throws `AuthenticationException` (401). Useful when you know the route is protected and want to avoid null checks.
- `auth.login(user, remember?)` – logs in the given user and optionally issues a remember token.
- `auth.attempt(credentials, remember?)` – validates credentials using the active guard and logs in on success.
- `auth.logout()` – clears the session and remember token.

## Sanitized User Records

`auth.user()` — and the cached user available right after `login()` or `attempt()` — never exposes credential material. `ModelUserProvider` strips the password column, the remember-token column, and any fields listed in the model's `static hidden` before the record leaves the auth layer:

```ts
export class User extends AuthenticatableModel<UserRecord> {
  static override table = users
  static override readonly recordType = {} as UserRecord
  static override hidden = ['passwordHash', 'rememberToken']
}
```

The `make:auth` scaffolder generates the user model with this `hidden` declaration out of the box.

Credential validation still runs on the raw database record internally, so login and remember-me tokens are unaffected — sanitization only changes what `auth.user()` exposes to application code.

Custom user providers can opt in by implementing the optional `sanitize(user)` method on the `UserProvider` interface. `SessionGuard` calls it before caching or returning the user:

```ts
sanitize(user: AuthUser): AuthUser {
  const { passwordHash, ...safe } = user
  return safe as AuthUser
}
```

### Typing Sanitized Users

Sanitization happens at runtime, so a plain `auth.user<UserRecord>()` would still *type* the record as if the credential fields were present. Use the `Sanitized<T>` helper to strip the conventional credential keys from the type:

```ts
import type { Sanitized } from '@guren/core'

// Strips password/passwordHash/rememberToken-style keys from the type
const user = await this.auth.userOrFail<Sanitized<UserRecord>>()

user.email        // ✅ string
user.passwordHash // ❌ compile error — stripped at runtime
```

If your model hides additional fields via `static hidden`, or your credential columns use names outside the conventions (`password`, `passwordHash`, `password_hash`, `rememberToken`, `remember_token`), list them in the second type parameter:

```ts
type SafeUser = Sanitized<UserRecord, 'twoFactorSecret' | 'credentialDigest'>
```

The runtime strips exactly the columns your provider is configured with plus the model's `static hidden` — a static type cannot see that configuration, so `Sanitized<T>` reflects the conventional names and relies on you to pass anything else. `guren audit` warns about sensitive columns missing from `static hidden`, which keeps the runtime side honest.

## Remember Tokens

`SessionGuard` manages remember tokens automatically when your user provider implements `setRememberToken` / `getRememberToken`. `ModelUserProvider` handles this when the `rememberTokenColumn` option is supplied.

## Example Application

The blog example now includes:

- `AuthProvider` for guard/provider setup
- `LoginController` & `DashboardController`
- Inertia pages at `resources/js/pages/auth/Login.tsx` and `resources/js/pages/dashboard/Index.tsx`
- Database schema, migration, and seeder for `users`

Run the demo with:

```bash
bun run dev
```

Visit `http://localhost:3000/login` and sign in using the seeded credentials `demo@example.com` / `secret`.
