# Authentication

## Model

- **No self-registration.** Users can only be created by an admin via the `createInvitation` mutation.
- **No password login.** Only Google and Microsoft OAuth are supported.
- **Invitation-first**: a user record (inactive) and an invitation are created when an admin invites someone. The user activates on first OAuth login.

## Login Flow

1. Frontend redirects user to `GET /auth/google` or `GET /auth/microsoft`
2. Backend generates OAuth state + PKCE code verifier, stores them in HTTP-only cookies, and redirects to the provider
3. Provider redirects back to `GET /auth/google/callback` or `GET /auth/microsoft/callback`
4. Backend validates the OAuth code and fetches the user profile
5. Backend checks if the email matches an existing user record:
   - If no user exists → reject with `{ error: "NO_ACCOUNT", message: "..." }`
   - If user exists but is inactive and has a pending invitation → activate user, accept invitation, assign role
   - If user exists but is inactive with no pending invitation → reject with `{ error: "INACTIVE", message: "..." }`
   - If user exists and is active → proceed
6. Backend links the OAuth provider account if not already linked
7. Backend creates a session and returns `{ sessionToken, userId }`

## E2E And Local Test Login

For frontend e2e tests and local automation, you can skip OAuth entirely by
calling the `devLogin` GraphQL mutation.

This returns the same kind of bearer token that the normal login flow produces,
so frontend tests can authenticate without opening Google or Microsoft.

Example:

```graphql
mutation DevLogin($email: String!) {
   devLogin(email: $email) {
      token
      user {
         id
         email
      }
   }
}
```

Use the returned token like this:

```http
Authorization: Bearer <sessionToken>
```

Constraints:

- Only available outside production.
- The target user must already exist.
- The target user must already be active.
- If the user was only invited, activate them first before using `devLogin`.

## Session Management

- Sessions are stored in the `sessions` table
- Session ID is a SHA-256 hash of the token (the plain token is returned to the client, never stored)
- Default duration: 30 days
- Sessions are auto-extended when more than half the duration has elapsed
- Invalidate via `logout` mutation or by deleting the session server-side

## Using the Session Token

Include the token in all GraphQL requests:

```
Authorization: Bearer <sessionToken>
```

If the token is missing or invalid, the request context will have `userId: null` and an empty permission set. Queries/mutations that require auth will return an error.

## Auth Endpoints (REST)

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/auth/google` | GET | Start Google OAuth flow |
| `/auth/google/callback` | GET | Google OAuth callback |
| `/auth/microsoft` | GET | Start Microsoft OAuth flow |
| `/auth/microsoft/callback` | GET | Microsoft OAuth callback |
| `/health` | GET | Health check (no auth required) |

## Edge Cases

- **Re-invitation**: If a user's invitation expired, an admin can create a new invitation for the same email. The old invitation is replaced.
- **Multiple OAuth providers**: A user can link both Google and Microsoft accounts to the same user record.
- **Deactivated user**: If a user is deactivated (via `deactivateUser` mutation), their existing sessions remain valid until expiry but `validateSession` will reject them because `isActive` is checked on every request.
