# wcz-layout auth

## Rules

- Everything is public by default; protection is opt-in on both layers. The route guard only
  hides UI, the middleware is the security boundary (see `routing` and `server-functions`).
- `requireAuth` redirects a signed-out user to `/auth/login?returnTo=…`; a signed-in user
  without the permission gets an error, not a redirect.
- `hasPermission(user, key)` is for conditional UI only (navigation, buttons), never a guard.
- Read signed-in user from `Route.useRouteContext()` on client or `context.user` on server.
- `userMiddleware` gives `context.user: User | null` without throwing.
  It is a single instance, deduped with `authMiddleware`; in server routes list both in the
  same `middleware` array so the caller is resolved once.
- Permissions are evaluated per request, so moving an existing group between keys applies
  immediately. A group name new to `permissions.ts` applies only after the user signs in
  again (the cookie keeps only groups listed there).
- AWS users have no groups, so `requireAuth(key)` / `authMiddleware(key)` always reject them.
  Use the bare form for routes AWS users must reach.
- Offline, guards use a cached user snapshot from `localStorage` — UI state only, the server still authorizes every call.

```ts
// src/lib/auth/permissions.ts — permission key -> group names (sAMAccountName)
import type { Permissions } from "wcz-layout/models";

export const permissions = {
  admin: ["wcz-developers"],
} as const satisfies Permissions;

// src/lib/auth/scopes.ts — scope key -> one scope per API
import type { Scopes } from "wcz-layout/models";

export const scopes = {
  graph: "User.Read", // bare string = Entra only
  file: {
    entra: "api://wistron.com/file-api/access_as_user",
    aws: "https://file-api.wistron.com/access_as_user",
  },
} as const satisfies Scopes;
```

## Gates

| Gate                    | Route `beforeLoad` | Server function / API route |
| ----------------------- | ------------------ | --------------------------- |
| Anyone (default)        | —                  | — or `userMiddleware`       |
| Signed in, any provider | `requireAuth()`    | `authMiddleware()`          |
| Signed in + permission  | `requireAuth(key)` | `authMiddleware(key)`       |

## Tokens for downstream APIs

From `wcz-layout/auth`, server-only (call inside a server function or server route):

- `getAccessToken(scopeKey)` — delegated token for the signed-in user from their session.
  Requires a browser session (a Bearer caller has none). An expired session clears the cookie
  and throws a 401 `Response`.
- `getAppToken(scopeKey)` — app-only Entra token (client credentials, `<resource>/.default`),
  cached in memory. For background jobs and calls with no user.
- Downstream APIs must trust both issuers to accept AWS users; Cognito tokens have no `aud`
  and are authorized by the `scope` claim.

# Examples

```ts
import { createServerFn } from "@tanstack/react-start";
import { authMiddleware } from "wcz-layout/middleware";

export const getData = createServerFn()
  .middleware([authMiddleware("admin")])
  .handler(async () => {
    // user is guaranteed to have "admin" permission
    const user = context.user;
    return { data: "Secret admin information" };
  });
```
