# Auth Guide

## Why auth matters in Vona

Vona treats authentication as a framework-level capability rather than a one-off login endpoint.

That matters because real systems often need more than one authentication method, more than one credential source, and more than one post-auth workflow.

## Core auth model

The `a-auth` module provides a general authentication system built around **auth providers**.

This model supports:

- multiple authentication methods such as username/password and OAuth
- multiple clients per provider
- association of multiple authentication methods to one user
- migration of authentication methods from one user to another

## `bean.auth`

Vona exposes a global bean `bean.auth` so backend code can use all registered auth providers through one unified entrypoint.

Representative provider-generation workflow:

```bash
npm run vona :create:bean authProvider simple -- --module=auth-simple
npm run vona :create:bean authProvider oauth -- --module=auth-oauth
```

Those providers still plug into the shared `:create:bean` command surface, which keeps auth extension aligned with the rest of Vona’s bean architecture.

### Username/password example

```typescript
const jwt = await this.bean.auth.authenticate('auth-simple:simple', {
  clientOptions: { username: 'admin', password: '123456' },
});
```

### OAuth example

```typescript
await this.bean.auth.authenticate('auth-oauth:oauth', {
  clientName: 'github',
  state: { redirect: '/' },
});
```

## Auth provider types

### `auth-simple`

`auth-simple` provides an out-of-the-box username/password provider.

Representative flows include:

- register a new user by calling `bean.auth.authenticate(..., { state: { intention: 'register' } })`
- log in by calling `bean.auth.authenticate(..., { state: { intention: 'login' } })`
- sign out through `bean.passport.signout()`

It also exposes password-hash configuration through app config.

### `auth-oauth`

`auth-oauth` provides an OAuth-based provider model.

Representative capabilities include:

- built-in support for GitHub
- adding new OAuth strategies such as Google through client configuration and interface merging
- unified callback handling
- exchanging the returned OAuth code for a JWT token through the Passport API flow
- optional mock login behavior in development

## Provider and client model

A provider can expose multiple clients.

For example, one OAuth provider can expose `github`, `google`, or other client entries, each with its own strategy and credentials.

That means auth configuration is not hardwired to one provider/credential pair.

## State and auth intent

Authentication calls can carry state such as:

- `register`
- `login`
- `associate`
- `migrate`
- redirect target after successful OAuth flow

This is one of the reasons Vona auth can support richer account flows without scattering those rules across unrelated controller code.

## Profile-driven user creation

Auth providers usually return profile data rather than directly creating a final user shape by hand.

That profile is then consumed by the broader user and passport system.

This separation helps Vona keep provider logic, user logic, and business customization decoupled.

A practical rule is that the provider should prove identity and return a stable profile, while user creation and passport assembly remain downstream responsibilities handled by the user/passport layers.

## OAuth credentials and callback flow

OAuth credentials are configured through app config under the relevant auth provider client entry.

The typical flow is:

1. redirect to the provider
2. receive the OAuth callback code
3. exchange the code for a JWT token
4. continue through the passport and user flow

The out-of-the-box Passport controller in `home-user` exposes that exchange as `createPassportJwtFromOauthCode`, which is why frontend OAuth flows usually stay thin: the provider redirect happens first, and the frontend then asks the Passport API to finish JWT creation.

## Passport API relationship

The `home-user` module exposes out-of-the-box Passport APIs for common account flows such as:

- current passport retrieval
- login
- logout
- registration
- OAuth login
- associate / migrate auth methods
- token refresh
- temporary auth token creation

In the current repo implementation, these flows live in `home-user/src/controller/passport.ts`, where register/login call `bean.auth.authenticate(...)`, OAuth login uses provider/module path parameters, and token-oriented follow-up endpoints stay concentrated in one controller surface.

### Auth-token lifecycle and adapter selection

Ordinary signin assembles the current Passport, emits the signin event and serializes its user/auth identity. Passport then applies the auth-token policy and signs the resulting payload through the JWT access/refresh pair. Checks verify the selected JWT purpose, deserialize the Passport, reject disabled accounts and, for stateful tokens, verify the adapter's current stored state. Logout removes that state through its owning adapter before clearing the current Passport. Provider/client options and `auth`/`session` mode continue to govern ordinary credentials.

A scoped token flow reuses that lifecycle without adding an auth provider. Trusted application configuration can set `modules['a-jwt'].clients[clientName].authTokenAdapter` to a registered service name, such as `mcp-auth:authTokenAdapter` when that module is installed. Without that field, Passport uses the ordinary adapter selected by `modules['a-user'].adapter.authToken`. Select JWT clients in trusted server code; never let request input choose a client, adapter or identity override.

The Passport entrypoints are:

```typescript
// Defaults remain the ordinary access and refresh clients.
await this.bean.passport.createAuthToken(passport, payloadData, clientName, input);
await this.bean.passport.checkAuthToken(token, clientName, {
  path,
  operation: 'check', // Or 'refresh' to verify without attaching current state.
  input,
});
await this.bean.passport.refreshAuthToken(refreshToken, refreshClientName, input);
```

`createAuthToken(...)` issues from an already validated Passport. Serialized user/auth identity takes precedence over supplied payload fields; this method does not authenticate a provider, create another identity or emit a signin event. `input?: unknown` carries operation data, not executable behavior or configuration. Each adapter must validate any data it accepts.

`IAuthTokenAdapter` keeps its original `create`, `retrieve`, `verify`, `refresh`, `remove` and `removeAll` lifecycle. Optional seams specialize it without a separate token-flow abstraction:

- `options?: Readonly<IAuthTokenOptions>` supplies fixed adapter policy in place of ordinary defaults/provider overrides. For example, MCP uses `{ stateless: false, strategy: 'reissue' }`, so provider stateless settings cannot bypass its stored grant validation and ordinary session mode does not change its grant lifecycle.
- `deserialize?(payloadData)` reloads identity/authority when the ordinary Passport deserializer is not appropriate.
- `getJwtOptions?(payloadData)` selects the registered access/refresh pair and independent lifetimes after state creation/rotation. Both clients must select the same adapter.
- `create(payloadData, ttl?, input?)` returns the payload to sign; `verify(payloadData, { operation, clientName, input })` performs read-only validation. Existing adapters need not consume the added arguments.

Refresh uses the same check pipeline with `operation: 'refresh'`, without attaching current state. It then applies the selected strategy: `reuse` retains the payload, `refresh` extends stored state through `refresh(...): Promise<void>`, and `reissue` calls `create(...)` for a replacement payload. Creation/rotation and JWT signing must succeed before the resulting Passport and payload are attached. A scoped adapter's `create(...)` owns any atomic rotation and replay handling, not its read-only `verify(...)` method.

The attached scalar JWT client name keeps subsequent issuance and logout tied to the selected adapter; clearing current state also clears that provenance and payload. Ordinary signin, refresh, temporary-token and OAuth helpers must not convert a currently attached scoped credential into ordinary login authority. For example, MCP's grant family stays separate from ordinary login/session credentials. See [JWT Guide](/backend/jwt-guide#explicit-pairs-and-scoped-passport-flows) for the token-mechanics boundary.

## Auth-token mode

The auth-token mode selects how Vona issues, stores, reuses, and refreshes server-side authentication state. By default, logins for the same user and authentication record share one token identity. Applications that need independently managed login sessions can select `session` mode. Ordinary logout revocation follows the selected mode; this is not a logout-only setting.

Set the backend environment value to one of `auth` or `session`:

```dotenv
AUTH_TOKEN_MODE=session
```

The default is `auth`. The equivalent typed project-config override is:

```typescript
import type { VonaConfigOptional } from 'vona';

const config: VonaConfigOptional = {
  modules: {
    'a-user': {
      authToken: {
        mode: 'session',
      },
    },
  },
};
```

The setting is `modules['a-user'].authToken.mode`. Backend code can read the validated effective value through the read-only `bean.passport.authTokenMode` getter. See [Config Guide](/backend/config-guide) for configuration layering.

### Authentication-record mode: `auth`

`auth` preserves the existing behavior: server-side tokens are stored by user and authentication record. With the default signin strategy, separate logins for that pair reuse the same token identity and extend its lifetime. Logout revokes their shared login state, rather than only the requesting browser's login. It does not mean every authentication method associated with the user is revoked.

### Login-session mode: `session`

`session` gives each successful login a fresh session identity in the existing auth-token `token` field. Logout revokes that login's server-side state without revoking independent logins using the same user and authentication record. Token refresh preserves the session identity; receiving a refreshed JWT does not create a new login session.

This is **per login, not per tab**. Tabs or clients that share the same credential share the same session and are signed out together. A separate login creates an independent session.

Session mode requires stateful auth tokens. It is incompatible with `authToken.stateless: true`, including provider/client overrides that select stateless tokens. The refresh strategy `authToken.strategy.refreshAuthToken: 'reissue'` is also unsupported in session mode: refresh must retain the existing session identity rather than replace it. Keep the default `refresh` strategy unless the application has a specific reason to use `reuse`.

The narrower revocation in session mode applies to ordinary logout, not account-wide security operations. Password changes, password resets, and initial password setup retain their broader security invalidation behavior; they do not become session-only operations.

### Switching modes

Treat a change between `auth` and `session` as a coordinated deployment change. All workers serving the same application must use the same effective mode; do not run mixed modes against shared token state. Restart the workers together and require users to log in again. Existing credentials are not transparently migrated between modes.

For shared demonstration deployments, session mode also lets ordinary visitors log out without revoking other visitors' independent logins. See [Demonstration Mode Guide](/backend/demonstration-mode-guide#logout-policy).

## Registration and password recovery in Cabloy Basic

The Home Login experience uses the Passport registration flow for new users. Registration collects the standard username, email, password, confirmation, and CAPTCHA fields, then continues through the ordinary signed-in navigation flow after it succeeds.

If a user has lost an existing local password, the Login page also provides **Forgot password?**. The request form asks for an email address and CAPTCHA. After valid CAPTCHA verification, it always shows the same generic confirmation, whether or not a reset email can be sent. This protects account privacy and avoids exposing account eligibility through the interface.

When a reset email is available, its link opens a public page where the user chooses a new local password. Completing the reset signs the user out of existing server-side sessions, so they must sign in again with the new password. Password reset replaces an existing local password; it is not a way for an OAuth-only account to add its first local password. That journey remains the authenticated **Set password** account-security action.

This guide focuses on the framework-level auth model. For the user/passport side, see [User Access Guide](/backend/user-access-guide).

## Relationship to controller AOP

Auth behavior is closely connected to controller AOP, especially guards.

Built-in passport-oriented guard shorthands such as `@Passport.public()` are documented from the request-path perspective in [Controller AOP Guide](/backend/controller-aop-guide).

## Implementation checks for authentication changes

When editing authentication behavior, ask:

1. is the right layer `bean.auth`, `bean.passport`, or controller guard configuration?
2. does this flow belong to username/password auth, OAuth auth, or both?
3. does a provider already exist instead of needing custom authentication code?
4. does the flow need register, login, associate, migrate, or redirect-aware state?

That helps AI keep auth logic aligned with Vona’s real backend architecture.
