# Aurora Catalyst Backend

NestJS 11 + Sequelize + PostgreSQL + GraphQL + REST. Generated from YAML schemas in `../cliter/` via
`aurora-catalyst-cli`.

## Authentication

The backend selects an OAuth2 strategy at startup via the `OAUTH_STRATEGY` env var. The strategy is
fixed for the process lifetime.

> **BREAKING CHANGE**: the default has changed from `aurora-hub` to **`none`**. Deployments that
> relied on the implicit `aurora-hub` default MUST now set `OAUTH_STRATEGY=aurora-hub` explicitly.
> A prominent warning is logged at boot whenever the mode is `none`.

| `OAUTH_STRATEGY`              | What it does                                                                                                                                                                                                        | Use when                                                                                 |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `none` (**new default**)      | No authentication. All requests arrive with an anonymous account (`dPermissions.all=[]`). Endpoints without declared permissions are public. Use as a transient bootstrap state before configuring a real provider. | Fresh install with no identity provider yet configured.                                  |
| `aurora-hub`                  | Validates Bearer tokens against an external Aurora HUB via JWKS + `iamMeAccount` GraphQL hydration. The HUB issues the tokens; this backend only consumes them.                                                     | This catalyst install is a satellite; another Aurora instance is the identity authority. |
| `local-provider`              | This backend ISSUES tokens itself via `POST /api/o-auth/token` (OAuth2 RFC 6749 Password + Refresh grants). Exposes its public key at `GET /.well-known/jwks.json` so other catalysts can validate tokens it emits. | This catalyst is the identity authority.                                                 |

A missing or empty `OAUTH_STRATEGY` defaults to `none`. An invalid value causes the app to
crash at bootstrap with a message enumerating the valid values.

### Transitioning from `none` to a real provider

`none` is only meant for bootstrap. Once your identity infrastructure is ready:

1. Set `OAUTH_STRATEGY=aurora-hub` (and `OAUTH_HUB_SERVER_URL`) or
   `OAUTH_STRATEGY=local-provider` (and generate keys per the section below).
2. Restart the backend.

### Required setup for `local-provider` and `aurora-hub`

> **NOT required for `none` mode** — the backend boots without any key files when `OAUTH_STRATEGY=none`.

The two RSA keys at `backend/.keys/oauth-private.key` and `backend/.keys/oauth-public.key` are
required for `aurora-hub` and `local-provider` modes. Both keys are gitignored; each deploy
generates its own.

```bash
# From the project root:
catalyst keys --out ./backend/.keys/
```

This produces `oauth-private.key` and `oauth-public.key` (RSA 2048, PEM, PKCS#1). The private key
MUST be protected; the public key may be served publicly via the JWKS endpoint.

### Local Provider Setup

To run this backend as the identity authority:

1. **Generate keys** (one time per deploy):

   ```bash
   catalyst keys --out ./backend/.keys/
   ```

2. **Configure env vars** in `backend/.env`:

   ```sh
   # Strategy selection
   OAUTH_STRATEGY=local-provider

   # Key paths (defaults shown — override only if you store keys elsewhere).
   # Paths are resolved relative to the backend's cwd, which is `backend/` at runtime.
   OAUTH_PRIVATE_KEY_PATH=./.keys/oauth-private.key
   OAUTH_PUBLIC_KEY_PATH=./.keys/oauth-public.key

   # JWT lifetimes (defaults shown)
   JWT_ACCESS_TTL_SECONDS=3600       # 1 hour
   JWT_REFRESH_TTL_SECONDS=2592000   # 30 days

   # Retention windows for the daily 03:00 UTC purge job (defaults shown)
   OAUTH_ACCESS_TOKEN_RETENTION_DAYS=7
   OAUTH_REFRESH_TOKEN_RETENTION_DAYS=7
   OAUTH_CREDENTIAL_RETENTION_DAYS=90

   # Rate limiting on /oauth/token (defaults shown — 5 req/min per IP)
   THROTTLE_OAUTH_TOKEN_LIMIT=5
   THROTTLE_OAUTH_TOKEN_TTL_MS=60000
   ```

3. **Ensure an `OAuthApplication` + a linked `OAuthClient` exist in the database.** Per RFC 6749
   §2.2, the public `client_id` is the `OAuthApplication.code` (e.g. `aurora`), and the
   `client_secret` is the `OAuthApplication.secret`. The application is linked to one technical
   `OAuthClient` per grant type via the `OAuthApplicationClient` pivot table. Confidential clients
   send `Authorization: Basic <base64(application_code:application_secret)>` with every
   `/oauth/token` request. Minimum seed for password + refresh login:
   - `OAuthApplication { code: 'aurora', secret: '<plaintext>', isMaster: true }`
   - `OAuthClient { grantType: 'PASSWORD', ... }` +
     `OAuthApplicationClient { applicationId, clientId }`
   - `OAuthClient { grantType: 'REFRESH_TOKEN', ... }` + its own pivot row

   The token-issuance flow authenticates the application, then resolves the single client matching
   the requested grant type. `OAuthClient.secret` is NOT read for authentication.

4. **Ensure user accounts exist** with hashed passwords (the `IamUser.password` field stores bcrypt
   hashes; create users via the standard `iam/user` mutations).

5. **Boot the backend**:

   ```bash
   pnpm back:dev
   ```

6. **Test the password grant**:

   ```bash
   curl -X POST http://localhost:9080/api/o-auth/token \
     -u "<client_id>:<client_secret>" \
     -d "grant_type=password&username=<username>&password=<password>"
   ```

   Successful response (HTTP 200):

   ```json
   {
     "access_token": "eyJhbGciOiJSUzI1NiIs...",
     "token_type": "Bearer",
     "expires_in": 3600,
     "refresh_token": "abc123...XYZ"
   }
   ```

7. **Use the access token**:

   ```bash
   curl http://localhost:9080/api/auth/verify \
     -H "Authorization: Bearer <access_token>"
   ```

8. **Refresh when the access token nears expiration**:

   ```bash
   curl -X POST http://localhost:9080/api/o-auth/token \
     -u "<client_id>:<client_secret>" \
     -d "grant_type=refresh_token&refresh_token=<refresh_token>"
   ```

   The old access + refresh pair is revoked atomically; reusing the old refresh token returns
   `invalid_grant` (single-use rotation per RFC 6819 §5.2.2.3).

### Validating tokens from another catalyst (HUB mode)

A second catalyst instance can run in `aurora-hub` mode pointing to this one as its HUB. That second
instance will:

- Fetch the JWKS from `<this-instance>/.well-known/jwks.json` to verify token signatures.
- Hydrate accounts by calling `<this-instance>/graphql` with the standard `iamMeAccount` query,
  using the user's Bearer token as authorization.

Set on the consumer instance:

```sh
OAUTH_STRATEGY=aurora-hub
OAUTH_HUB_SERVER_URL=http://<this-instance>:9080
```

### Error responses

All errors follow RFC 6749 §5.2 strictly (no extra fields beyond `error` and an optional
NestJS-injected `statusCode`):

| HTTP | Body                                            | When                                                                                                                                                                      |
| ---- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400  | `{ "error": "invalid_grant" }`                  | User-not-found, wrong password, inactive account, expired/revoked/unknown refresh token. All collapse to the same response to prevent user enumeration (RFC 6819 §5.3.4). |
| 400  | `{ "error": "unsupported_grant_type" }`         | `grant_type` value other than `password` or `refresh_token`.                                                                                                              |
| 401  | `{ "error": "invalid_client" }`                 | Missing/malformed `Authorization: Basic`, unknown `client_id`, or wrong `client_secret`.                                                                                  |
| 415  | (default NestJS body)                           | Request `Content-Type` is not `application/x-www-form-urlencoded`.                                                                                                        |
| 429  | (default NestJS body, `Retry-After` header set) | Rate limit exceeded — defaults to 5 requests per minute per IP.                                                                                                           |

## Standard commands

```bash
pnpm dev               # back + front concurrently
pnpm back:dev          # backend only (watch mode)
pnpm back:test         # unit tests
pnpm back:test:e2e     # e2e tests (see note below)
pnpm back:lint         # eslint --fix
```

> **E2E note:** Only `test/authentication-none-mode.e2e-spec.ts` passes today.
> `test/app.e2e-spec.ts` (NestJS CLI starter) imports the full `AppModule`, which transitively
> pulls in `jwks-rsa` — an ESM-only package that ts-jest cannot transform — and fails at load
> time. This is a pre-existing issue, not introduced by the authentication-none-mode change.
> The `jest-e2e.json` `moduleNameMapper` / `rootDir` settings correctly resolve all
> `@aurora/*`, `@app/*`, and `@api/*` path aliases (genuine improvement in this change).
