# Cidaas Authentication Integration Guide

## Übersicht

Diese Dokumentation beschreibt die vollständige Integration von **Cidaas** (OAuth2/OIDC Identity Provider) in die my.experimenta.science E-Commerce-Plattform.

**Cidaas** wird ausschließlich für Authentifizierung verwendet. Nutzerprofile, Rollen und Kaufhistorie werden lokal in PostgreSQL gespeichert.

---

## Architektur-Entscheidungen

| Aspekt              | Technologie                        | Begründung                                         |
| ------------------- | ---------------------------------- | -------------------------------------------------- |
| **Auth Module**     | `nuxt-auth-utils`                  | Offiziell, lightweight, perfekt für Custom OAuth2  |
| **OAuth2 Provider** | Cidaas (OIDC)                      | Unternehmens-Anforderung, Enterprise-CIAM          |
| **Auth Flow**       | Authorization Code + PKCE          | Sicherheitsstandard (verhindert Code Interception) |
| **Session Storage** | Encrypted HTTP-only Cookies        | Stateless, sicher, von `nuxt-auth-utils` verwaltet |
| **Session Dauer**   | 30 Tage                            | E-Commerce Standard (Balance Sicherheit/UX)        |
| **User Storage**    | PostgreSQL (lokal)                 | Vollständige Kontrolle über Nutzerdaten            |
| **JWT Validation**  | `jose` Library                     | Modern, sicher, gut gewartet                       |
| **UI Approach**     | Custom Forms (eine Seite mit Tabs) | Volle Kontrolle über Design, experimenta Branding  |

---

## Architektur-Diagramm

```
┌────────────────────────────────────────────────────────────────────────────┐
│                           USER LOGIN FLOW                                   │
└────────────────────────────────────────────────────────────────────────────┘

┌─────────────┐         ┌─────────────┐         ┌─────────────┐         ┌──────────────┐
│   Browser   │  HTTPS  │ Nuxt 4 App  │  OIDC   │   Cidaas    │  Link   │  PostgreSQL  │
│  (Client)   │◄───────►│  (Server)   │◄───────►│ (Identity)  │         │ (User Store) │
└─────────────┘         └─────────────┘         └─────────────┘         └──────────────┘
      │                       │                       │                         │
      │  1. Navigate /auth    │                       │                         │
      ├──────────────────────►│                       │                         │
      │                       │                       │                         │
      │  2. Render Auth Page  │                       │                         │
      │     (Login Tab)       │                       │                         │
      │◄──────────────────────┤                       │                         │
      │                       │                       │                         │
      │  3. Submit Email/Pass │                       │                         │
      │     POST /api/auth/   │                       │                         │
      │     login             │                       │                         │
      ├──────────────────────►│                       │                         │
      │                       │                       │                         │
      │                       │  4. Generate PKCE     │                         │
      │                       │     (verifier + challenge)                     │
      │                       │                       │                         │
      │                       │  5. Store verifier    │                         │
      │                       │     in cookie (5min)  │                         │
      │                       │                       │                         │
      │                       │  6. Build OAuth2 URL  │                         │
      │                       │     with challenge    │                         │
      │                       │                       │                         │
      │  7. Redirect to       │                       │                         │
      │     Cidaas Authz      │                       │                         │
      │◄──────────────────────┤                       │                         │
      │                       │                       │                         │
      │  8. Submit credentials                        │                         │
      │     to Cidaas         │                       │                         │
      ├──────────────────────────────────────────────►│                         │
      │                       │                       │                         │
      │                       │  9. Validate credentials                        │
      │                       │     (email + password)                          │
      │                       │                       │                         │
      │  10. Redirect to      │                       │                         │
      │      callback with    │                       │                         │
      │      auth code        │                       │                         │
      │◄──────────────────────────────────────────────┤                         │
      │                       │                       │                         │
      │  11. GET /api/auth/   │                       │                         │
      │      callback?code=xxx│                       │                         │
      ├──────────────────────►│                       │                         │
      │                       │                       │                         │
      │                       │  12. Validate state   │                         │
      │                       │      (CSRF protection)│                         │
      │                       │                       │                         │
      │                       │  13. Retrieve PKCE    │                         │
      │                       │      verifier from    │                         │
      │                       │      cookie           │                         │
      │                       │                       │                         │
      │                       │  14. Exchange code +  │                         │
      │                       │      verifier for     │                         │
      │                       │      tokens           │                         │
      │                       ├──────────────────────►│                         │
      │                       │                       │                         │
      │                       │  15. Access Token +   │                         │
      │                       │      ID Token (JWT)   │                         │
      │                       │◄──────────────────────┤                         │
      │                       │                       │                         │
      │                       │  16. Validate JWT     │                         │
      │                       │      (signature, exp, │                         │
      │                       │       issuer, aud)    │                         │
      │                       │                       │                         │
      │                       │  17. Fetch UserInfo   │                         │
      │                       │      with access token│                         │
      │                       ├──────────────────────►│                         │
      │                       │                       │                         │
      │                       │  18. User Profile     │                         │
      │                       │      (sub, email,     │                         │
      │                       │       name, ...)      │                         │
      │                       │◄──────────────────────┤                         │
      │                       │                       │                         │
      │                       │  19. Query DB by experimenta_id = sub           │
      │                       ├─────────────────────────────────────────────────►│
      │                       │                       │                         │
      │                       │  20. User exists?                               │
      │                       │◄─────────────────────────────────────────────────┤
      │                       │                       │                         │
      │                       │  21. If new: INSERT user                        │
      │                       │      If exists: UPDATE updated_at               │
      │                       ├─────────────────────────────────────────────────►│
      │                       │                       │                         │
      │                       │  22. User record                                │
      │                       │◄─────────────────────────────────────────────────┤
      │                       │                       │                         │
      │                       │  23. Create Session   │                         │
      │                       │      (encrypted cookie│                         │
      │                       │       with user data) │                         │
      │                       │                       │                         │
      │  24. Set Session      │                       │                         │
      │      Cookie &         │                       │                         │
      │      Redirect to /    │                       │                         │
      │◄──────────────────────┤                       │                         │
      │                       │                       │                         │
      │  25. Navigate Home    │                       │                         │
      │      (logged in)      │                       │                         │
      ├──────────────────────►│                       │                         │
      │                       │                       │                         │
```

---

## Setup-Anleitung

### 1. Dependencies installieren

```bash
pnpm add nuxt-auth-utils jose
```

**Packages:**

- `nuxt-auth-utils` - Offizielles Nuxt Auth Module (Session Management)
- `jose` - JWT Validation Library (OIDC ID Token Verification)

---

### 2. Cidaas Admin Panel Konfiguration

Bevor die Integration funktioniert, muss ein **OAuth2 Client** in Cidaas konfiguriert werden.

**Checklist:**

- [ ] **1. OAuth2 Application erstellen**
  - Login im Cidaas Admin Panel
  - Navigiere zu: Applications → Create New Application
  - Type: "Web Application"
  - Name: "my.experimenta.science"

- [ ] **2. Grant Types konfigurieren**
  - ✅ Enable: `authorization_code`
  - ✅ Enable: `refresh_token` (optional, für Token Refresh)
  - ❌ Disable: `implicit`, `client_credentials`

- [ ] **3. PKCE aktivieren**
  - ✅ Require PKCE: `true`
  - Code Challenge Method: `S256` (SHA-256)

- [ ] **4. Redirect URIs konfigurieren**
  - **Development:** `http://localhost:3000/api/auth/callback`
  - **Staging:** `https://staging.my.experimenta.science/api/auth/callback`
  - **Production:** `https://my.experimenta.science/api/auth/callback`
  - ⚠️ **Wichtig:** Exakte URIs, keine Wildcards!

- [ ] **5. Scopes konfigurieren**
  - ✅ `openid` (mandatory für OIDC)
  - ✅ `profile` (Name, etc.)
  - ✅ `email` (E-Mail-Adresse)

- [ ] **6. Token Lifetimes**
  - Access Token Lifetime: `3600` Sekunden (1 Stunde)
  - Refresh Token Lifetime: `2592000` Sekunden (30 Tage)
  - ID Token Lifetime: `3600` Sekunden (1 Stunde)

- [ ] **7. Client Credentials notieren**
  - **Client ID:** z.B. `abc123def456` (wird in .env hinterlegt)
  - **Client Secret:** z.B. `secret_xyz789` (wird in .env hinterlegt)

- [ ] **8. Cidaas Endpoints notieren**
  - **Issuer:** `https://experimenta.cidaas.de`
  - **Authorization Endpoint:** `https://experimenta.cidaas.de/authz-srv/authz`
  - **Token Endpoint:** `https://experimenta.cidaas.de/token-srv/token`
  - **UserInfo Endpoint:** `https://experimenta.cidaas.de/users-srv/userinfo`
  - **JWKS Endpoint:** `https://experimenta.cidaas.de/.well-known/jwks.json`

---

### 3. Environment Variables

Erstelle `.env` Datei im Projekt-Root:

```bash
# .env (NEVER commit this file!)

# Cidaas OAuth2 Configuration
CIDAAS_CLIENT_ID=abc123def456
CIDAAS_CLIENT_SECRET=secret_xyz789
CIDAAS_ISSUER=https://experimenta.cidaas.de
CIDAAS_AUTHORIZE_URL=https://experimenta.cidaas.de/authz-srv/authz
CIDAAS_TOKEN_URL=https://experimenta.cidaas.de/token-srv/token
CIDAAS_USERINFO_URL=https://experimenta.cidaas.de/users-srv/userinfo
CIDAAS_JWKS_URL=https://experimenta.cidaas.de/.well-known/jwks.json

# Callback URL (must match Cidaas config!)
CIDAAS_REDIRECT_URI=http://localhost:3000/api/auth/callback  # Dev
# CIDAAS_REDIRECT_URI=https://my.experimenta.science/api/auth/callback  # Production

# Session Encryption Secret (generate with: openssl rand -hex 32)
NUXT_SESSION_PASSWORD=your-64-character-hex-secret-here
```

**Erstelle `.env.example`** (für Git - ohne echte Secrets):

```bash
# .env.example

CIDAAS_CLIENT_ID=your-client-id
CIDAAS_CLIENT_SECRET=your-client-secret
CIDAAS_ISSUER=https://experimenta.cidaas.de
CIDAAS_AUTHORIZE_URL=https://experimenta.cidaas.de/authz-srv/authz
CIDAAS_TOKEN_URL=https://experimenta.cidaas.de/token-srv/token
CIDAAS_USERINFO_URL=https://experimenta.cidaas.de/users-srv/userinfo
CIDAAS_JWKS_URL=https://experimenta.cidaas.de/.well-known/jwks.json
CIDAAS_REDIRECT_URI=http://localhost:3000/api/auth/callback
NUXT_SESSION_PASSWORD=generate-with-openssl-rand-hex-32
```

**Session Secret generieren:**

```bash
openssl rand -hex 32
```

---

### 4. Nuxt Configuration

**nuxt.config.ts:**

```typescript
// nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    'nuxt-auth-utils',
    '@nuxtjs/i18n',
    'shadcn-nuxt',
    // ... weitere Module
  ],

  runtimeConfig: {
    // Private keys (nur Server-seitig verfügbar)
    cidaas: {
      clientId: process.env.CIDAAS_CLIENT_ID,
      clientSecret: process.env.CIDAAS_CLIENT_SECRET,
      issuer: process.env.CIDAAS_ISSUER,
      authorizeUrl: process.env.CIDAAS_AUTHORIZE_URL,
      tokenUrl: process.env.CIDAAS_TOKEN_URL,
      userinfoUrl: process.env.CIDAAS_USERINFO_URL,
      jwksUrl: process.env.CIDAAS_JWKS_URL,
      redirectUri: process.env.CIDAAS_REDIRECT_URI,
    },

    // Session configuration
    // Note: nuxt-auth-utils automatically reads NUXT_SESSION_PASSWORD from process.env
    session: {
      maxAge: 60 * 60 * 24 * 30, // 30 days in seconds
      name: 'experimenta-session',
    },

    // Public keys (auch Client-seitig verfügbar)
    public: {
      appUrl: process.env.APP_URL || 'http://localhost:3000',
    },
  },

  // Security headers
  nitro: {
    routeRules: {
      '/api/auth/**': {
        headers: {
          'X-Frame-Options': 'DENY',
          'X-Content-Type-Options': 'nosniff',
          'Referrer-Policy': 'strict-origin-when-cross-origin',
        },
      },
    },
  },

  compatibilityDate: '2024-10-29',
})
```

---

## Server-Side Implementation

### 1. PKCE Utilities

**File:** `server/utils/pkce.ts`

```typescript
// server/utils/pkce.ts

/**
 * PKCE (Proof Key for Code Exchange) utilities for OAuth2 security.
 *
 * PKCE prevents authorization code interception attacks by requiring
 * the client to prove possession of the original code verifier.
 *
 * Flow:
 * 1. Generate random code_verifier (43-128 chars)
 * 2. Hash verifier with SHA-256 → code_challenge
 * 3. Send challenge to authorization server
 * 4. Server returns authorization code
 * 5. Exchange code + verifier for tokens
 * 6. Server validates: SHA256(verifier) === stored_challenge
 */

/**
 * Generate a random code verifier (43-128 URL-safe characters)
 *
 * @param length - Length of verifier (default: 64)
 * @returns Base64URL-encoded random string
 */
export function generateCodeVerifier(length: number = 64): string {
  // Generate random bytes
  const randomBytes = crypto.getRandomValues(new Uint8Array(length))

  // Convert to base64url (URL-safe base64 without padding)
  return base64UrlEncode(randomBytes)
}

/**
 * Generate SHA-256 hash of code verifier → code challenge
 *
 * @param verifier - The code verifier
 * @returns Base64URL-encoded SHA-256 hash
 */
export async function generateCodeChallenge(verifier: string): Promise<string> {
  // Convert verifier string to Uint8Array
  const encoder = new TextEncoder()
  const data = encoder.encode(verifier)

  // Hash with SHA-256
  const hashBuffer = await crypto.subtle.digest('SHA-256', data)

  // Convert to base64url
  return base64UrlEncode(new Uint8Array(hashBuffer))
}

/**
 * Generate PKCE verifier + challenge pair
 *
 * @returns Object with verifier and challenge
 */
export async function generatePKCE(): Promise<{
  verifier: string
  challenge: string
}> {
  const verifier = generateCodeVerifier()
  const challenge = await generateCodeChallenge(verifier)

  return { verifier, challenge }
}

/**
 * Convert Uint8Array to Base64URL string
 *
 * Base64URL is URL-safe variant of Base64:
 * - Replace '+' with '-'
 * - Replace '/' with '_'
 * - Remove padding '='
 */
function base64UrlEncode(buffer: Uint8Array): string {
  // Convert to base64
  const base64 = btoa(String.fromCharCode(...buffer))

  // Convert to base64url
  return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
}

/**
 * Generate random state parameter for CSRF protection
 *
 * @param length - Length of state string (default: 32)
 * @returns Random URL-safe string
 */
export function generateState(length: number = 32): string {
  const randomBytes = crypto.getRandomValues(new Uint8Array(length))
  return base64UrlEncode(randomBytes)
}
```

---

### 2. Cidaas API Client

**File:** `server/utils/cidaas.ts`

```typescript
// server/utils/cidaas.ts

/**
 * Cidaas API Client for OAuth2/OIDC integration
 *
 * Provides functions to interact with Cidaas endpoints:
 * - Token exchange (authorization code → access/ID tokens)
 * - UserInfo fetch
 * - User registration
 */

import type { H3Error } from 'h3'

/**
 * Cidaas Token Response
 */
export interface CidaasTokenResponse {
  access_token: string
  token_type: string
  expires_in: number
  refresh_token?: string
  id_token: string // JWT with user identity
  scope: string
}

/**
 * Cidaas UserInfo Response
 */
export interface CidaasUserInfo {
  sub: string // Unique user ID (experimenta_id)
  email: string
  email_verified: boolean
  given_name?: string
  family_name?: string
  name?: string
  phone_number?: string
  updated_at?: number
}

/**
 * Cidaas Registration Request
 */
export interface CidaasRegistrationRequest {
  email: string
  password: string
  given_name: string
  family_name: string
  locale?: string
}

/**
 * Exchange authorization code for access/ID tokens
 *
 * @param code - Authorization code from callback
 * @param codeVerifier - PKCE code verifier
 * @returns Token response
 * @throws H3Error if exchange fails
 */
export async function exchangeCodeForToken(
  code: string,
  codeVerifier: string
): Promise<CidaasTokenResponse> {
  const config = useRuntimeConfig()

  // Prepare token request
  const params = new URLSearchParams({
    grant_type: 'authorization_code',
    code,
    redirect_uri: config.cidaas.redirectUri,
    client_id: config.cidaas.clientId,
    client_secret: config.cidaas.clientSecret,
    code_verifier: codeVerifier, // PKCE proof
  })

  try {
    const response = await fetch(config.cidaas.tokenUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: params.toString(),
    })

    if (!response.ok) {
      const errorData = await response.json().catch(() => ({}))
      console.error('Cidaas token exchange failed:', errorData)

      throw createError({
        statusCode: response.status,
        statusMessage: 'Token exchange failed',
        data: errorData,
      })
    }

    const tokens: CidaasTokenResponse = await response.json()
    return tokens
  } catch (error) {
    console.error('Token exchange error:', error)

    if ((error as H3Error).statusCode) {
      throw error // Re-throw H3Error
    }

    throw createError({
      statusCode: 500,
      statusMessage: 'Failed to exchange authorization code',
    })
  }
}

/**
 * Fetch user info from Cidaas UserInfo endpoint
 *
 * @param accessToken - OAuth2 access token
 * @returns User profile data
 * @throws H3Error if fetch fails
 */
export async function fetchUserInfo(accessToken: string): Promise<CidaasUserInfo> {
  const config = useRuntimeConfig()

  try {
    const response = await fetch(config.cidaas.userinfoUrl, {
      method: 'GET',
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
    })

    if (!response.ok) {
      console.error('Cidaas UserInfo fetch failed:', response.status)

      throw createError({
        statusCode: response.status,
        statusMessage: 'Failed to fetch user info',
      })
    }

    const userInfo: CidaasUserInfo = await response.json()
    return userInfo
  } catch (error) {
    console.error('UserInfo fetch error:', error)

    if ((error as H3Error).statusCode) {
      throw error
    }

    throw createError({
      statusCode: 500,
      statusMessage: 'Failed to fetch user information',
    })
  }
}

/**
 * Register new user via Cidaas Registration API
 *
 * @param data - Registration data
 * @returns Success indicator (user must verify email before login)
 * @throws H3Error if registration fails
 */
export async function registerUser(
  data: CidaasRegistrationRequest
): Promise<{ success: boolean; message: string }> {
  const config = useRuntimeConfig()

  // Cidaas registration endpoint (adjust based on actual API)
  const registrationUrl = `${config.cidaas.issuer}/users-srv/register`

  try {
    const response = await fetch(registrationUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email: data.email,
        password: data.password,
        given_name: data.given_name,
        family_name: data.family_name,
        locale: data.locale || 'de',
      }),
    })

    if (!response.ok) {
      const errorData = await response.json().catch(() => ({}))
      console.error('Cidaas registration failed:', errorData)

      // Handle specific errors
      if (response.status === 409) {
        throw createError({
          statusCode: 409,
          statusMessage: 'Email already registered',
        })
      }

      throw createError({
        statusCode: response.status,
        statusMessage: 'Registration failed',
        data: errorData,
      })
    }

    return {
      success: true,
      message: 'Registration successful. Please verify your email.',
    }
  } catch (error) {
    console.error('Registration error:', error)

    if ((error as H3Error).statusCode) {
      throw error
    }

    throw createError({
      statusCode: 500,
      statusMessage: 'Failed to register user',
    })
  }
}

/**
 * Refresh access token using refresh token
 *
 * @param refreshToken - Refresh token from previous login
 * @returns New token response
 * @throws H3Error if refresh fails
 */
export async function refreshAccessToken(refreshToken: string): Promise<CidaasTokenResponse> {
  const config = useRuntimeConfig()

  const params = new URLSearchParams({
    grant_type: 'refresh_token',
    refresh_token: refreshToken,
    client_id: config.cidaas.clientId,
    client_secret: config.cidaas.clientSecret,
  })

  try {
    const response = await fetch(config.cidaas.tokenUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: params.toString(),
    })

    if (!response.ok) {
      throw createError({
        statusCode: response.status,
        statusMessage: 'Token refresh failed',
      })
    }

    const tokens: CidaasTokenResponse = await response.json()
    return tokens
  } catch (error) {
    console.error('Token refresh error:', error)

    if ((error as H3Error).statusCode) {
      throw error
    }

    throw createError({
      statusCode: 500,
      statusMessage: 'Failed to refresh token',
    })
  }
}
```

---

### 3. JWT Validation

**File:** `server/utils/jwt.ts`

```typescript
// server/utils/jwt.ts

/**
 * JWT Token Validation using jose library
 *
 * Validates Cidaas ID tokens (OIDC JWT) to ensure:
 * - Signature is valid (using Cidaas public keys from JWKS)
 * - Token has not expired
 * - Issuer matches expected Cidaas instance
 * - Audience matches our client ID
 */

import { jwtVerify, createRemoteJWKSet, type JWTPayload } from 'jose'

// Cache JWKS (Cidaas public keys) to avoid fetching on every request
let jwksCache: ReturnType<typeof createRemoteJWKSet> | null = null

/**
 * Get or create JWKS cache
 *
 * JWKS (JSON Web Key Set) contains public keys used to verify JWT signatures.
 * We cache this to improve performance.
 */
function getJWKS() {
  if (!jwksCache) {
    const config = useRuntimeConfig()
    jwksCache = createRemoteJWKSet(new URL(config.cidaas.jwksUrl))
  }
  return jwksCache
}

/**
 * Extended JWT payload with OIDC claims
 */
export interface CidaasJWTPayload extends JWTPayload {
  sub: string // User ID (experimenta_id)
  email?: string
  email_verified?: boolean
  given_name?: string
  family_name?: string
  name?: string
}

/**
 * Verify Cidaas ID token
 *
 * @param idToken - JWT ID token from Cidaas
 * @returns Decoded and verified JWT payload
 * @throws Error if verification fails
 */
export async function verifyIdToken(idToken: string): Promise<CidaasJWTPayload> {
  const config = useRuntimeConfig()
  const JWKS = getJWKS()

  try {
    const { payload } = await jwtVerify(idToken, JWKS, {
      issuer: config.cidaas.issuer, // Must match Cidaas issuer
      audience: config.cidaas.clientId, // Must match our client ID
    })

    return payload as CidaasJWTPayload
  } catch (error) {
    console.error('JWT verification failed:', error)

    // Provide specific error messages
    if (error instanceof Error) {
      if (error.message.includes('expired')) {
        throw createError({
          statusCode: 401,
          statusMessage: 'Token has expired',
        })
      }

      if (error.message.includes('signature')) {
        throw createError({
          statusCode: 401,
          statusMessage: 'Invalid token signature',
        })
      }
    }

    throw createError({
      statusCode: 401,
      statusMessage: 'Invalid ID token',
    })
  }
}

/**
 * Decode JWT without verification (for debugging only!)
 *
 * ⚠️ WARNING: Only use for debugging. Never trust unverified tokens!
 *
 * @param token - JWT token
 * @returns Decoded payload (unverified!)
 */
export function decodeJWT(token: string): JWTPayload | null {
  try {
    const parts = token.split('.')
    if (parts.length !== 3) {
      return null
    }

    const payload = parts[1]
    const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString('utf-8'))

    return decoded
  } catch (error) {
    console.error('JWT decode failed:', error)
    return null
  }
}
```

---

## Auth API Endpoints

### 1. Login Endpoint

**File:** `server/api/auth/login.post.ts`

```typescript
// server/api/auth/login.post.ts

/**
 * POST /api/auth/login
 *
 * Initiates OAuth2 Authorization Code Flow with PKCE
 *
 * Request body:
 * {
 *   "email": "user@example.com"
 * }
 *
 * Response:
 * {
 *   "redirectUrl": "https://experimenta.cidaas.de/authz-srv/authz?..."
 * }
 *
 * Client should redirect user to redirectUrl
 */

import { z } from 'zod'

const loginSchema = z.object({
  email: z.string().email('Invalid email address'),
})

export default defineEventHandler(async (event) => {
  // 1. Validate request body
  const body = await readBody(event)
  const { email } = loginSchema.parse(body)

  // 2. Generate PKCE challenge
  const { verifier, challenge } = await generatePKCE()

  // 3. Generate state for CSRF protection
  const state = generateState(32)

  // 4. Store PKCE verifier in encrypted cookie (5 min TTL)
  setCookie(event, 'pkce_verifier', verifier, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 300, // 5 minutes
    path: '/',
  })

  // 5. Store state in cookie for validation
  setCookie(event, 'oauth_state', state, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 300, // 5 minutes
    path: '/',
  })

  // 6. Build Cidaas authorization URL
  const config = useRuntimeConfig()
  const authUrl = new URL(config.cidaas.authorizeUrl)

  authUrl.searchParams.set('client_id', config.cidaas.clientId)
  authUrl.searchParams.set('redirect_uri', config.cidaas.redirectUri)
  authUrl.searchParams.set('response_type', 'code')
  authUrl.searchParams.set('scope', 'openid profile email')
  authUrl.searchParams.set('state', state)
  authUrl.searchParams.set('code_challenge', challenge)
  authUrl.searchParams.set('code_challenge_method', 'S256')
  authUrl.searchParams.set('login_hint', email) // Pre-fill email in Cidaas form

  // 7. Return redirect URL to client
  return {
    redirectUrl: authUrl.toString(),
  }
})
```

---

### 2. Callback Endpoint

**File:** `server/api/auth/callback.get.ts`

```typescript
// server/api/auth/callback.get.ts

/**
 * GET /api/auth/callback
 *
 * OAuth2 callback handler - receives authorization code from Cidaas
 *
 * Query params:
 * - code: Authorization code
 * - state: CSRF protection token
 *
 * Flow:
 * 1. Validate state parameter
 * 2. Exchange code for tokens
 * 3. Validate ID token
 * 4. Fetch user info
 * 5. Create/update user in PostgreSQL
 * 6. Create session
 * 7. Redirect to homepage
 */

import { eq } from 'drizzle-orm'
import { users } from '~/server/database/schema'

export default defineEventHandler(async (event) => {
  // 1. Extract query parameters
  const query = getQuery(event)
  const { code, state } = query

  if (!code || !state) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Missing code or state parameter',
    })
  }

  // 2. Validate state (CSRF protection)
  const storedState = getCookie(event, 'oauth_state')

  if (!storedState || state !== storedState) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Invalid state parameter - possible CSRF attack',
    })
  }

  // 3. Retrieve PKCE verifier
  const verifier = getCookie(event, 'pkce_verifier')

  if (!verifier) {
    throw createError({
      statusCode: 400,
      statusMessage: 'PKCE verifier not found - session expired',
    })
  }

  try {
    // 4. Exchange authorization code for tokens
    const tokens = await exchangeCodeForToken(code as string, verifier)

    // 5. Validate ID token (JWT)
    const idTokenPayload = await verifyIdToken(tokens.id_token)

    // 6. Fetch detailed user info from Cidaas
    const cidaasUser = await fetchUserInfo(tokens.access_token)

    // 7. Get database instance
    const db = useDatabase()

    // 8. Check if user already exists in our database
    let user = await db.query.users.findFirst({
      where: eq(users.experimentaId, cidaasUser.sub),
    })

    if (!user) {
      // First time login - create new user
      const [newUser] = await db
        .insert(users)
        .values({
          experimentaId: cidaasUser.sub, // Cidaas user ID
          email: cidaasUser.email,
          firstName: cidaasUser.given_name || null,
          lastName: cidaasUser.family_name || null,
        })
        .returning()

      user = newUser

      console.log('New user created:', user.id)
    } else {
      // Existing user - update last login timestamp
      await db.update(users).set({ updatedAt: new Date() }).where(eq(users.id, user.id))

      console.log('User logged in:', user.id)
    }

    // 9. Create encrypted session (nuxt-auth-utils)
    await setUserSession(event, {
      user: {
        id: user.id,
        experimentaId: user.experimentaId,
        email: user.email,
        firstName: user.firstName,
        lastName: user.lastName,
      },
      loggedInAt: new Date().toISOString(),
    })

    // 10. Clean up temporary cookies
    deleteCookie(event, 'oauth_state')
    deleteCookie(event, 'pkce_verifier')

    // 11. Redirect to homepage (or original requested page)
    const redirectTo = getCookie(event, 'redirect_after_login') || '/'
    deleteCookie(event, 'redirect_after_login')

    return sendRedirect(event, redirectTo)
  } catch (error) {
    console.error('OAuth callback error:', error)

    // Clean up cookies on error
    deleteCookie(event, 'oauth_state')
    deleteCookie(event, 'pkce_verifier')

    // Redirect to login page with error
    return sendRedirect(event, '/auth?error=login_failed')
  }
})
```

---

### 3. Register Endpoint

**File:** `server/api/auth/register.post.ts`

```typescript
// server/api/auth/register.post.ts

/**
 * POST /api/auth/register
 *
 * Register new user via Cidaas Registration API
 *
 * Request body:
 * {
 *   "email": "user@example.com",
 *   "password": "SecurePassword123!",
 *   "firstName": "Max",
 *   "lastName": "Mustermann"
 * }
 *
 * Response:
 * {
 *   "success": true,
 *   "message": "Registration successful. Please verify your email."
 * }
 *
 * Note: User must verify email before they can log in
 */

import { z } from 'zod'

const registerSchema = z.object({
  email: z.string().email('Invalid email address'),
  password: z
    .string()
    .min(8, 'Password must be at least 8 characters')
    .regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
    .regex(/[a-z]/, 'Password must contain at least one lowercase letter')
    .regex(/[0-9]/, 'Password must contain at least one number'),
  firstName: z.string().min(2, 'First name must be at least 2 characters'),
  lastName: z.string().min(2, 'Last name must be at least 2 characters'),
})

export default defineEventHandler(async (event) => {
  // 1. Validate request body
  const body = await readBody(event)

  let validatedData
  try {
    validatedData = registerSchema.parse(body)
  } catch (error) {
    if (error instanceof z.ZodError) {
      throw createError({
        statusCode: 400,
        statusMessage: 'Validation failed',
        data: error.errors,
      })
    }
    throw error
  }

  // 2. Register user via Cidaas API
  try {
    const result = await registerUser({
      email: validatedData.email,
      password: validatedData.password,
      given_name: validatedData.firstName,
      family_name: validatedData.lastName,
      locale: 'de', // Default to German
    })

    return result
  } catch (error) {
    // Handle specific registration errors
    if ((error as any).statusCode === 409) {
      throw createError({
        statusCode: 409,
        statusMessage: 'Email address already registered',
      })
    }

    throw error
  }
})
```

---

### 4. Logout Endpoint

**File:** `server/api/auth/logout.post.ts`

```typescript
// server/api/auth/logout.post.ts

/**
 * POST /api/auth/logout
 *
 * End user session and clear session cookie
 *
 * Response:
 * {
 *   "success": true
 * }
 */

export default defineEventHandler(async (event) => {
  // Clear session (nuxt-auth-utils)
  await clearUserSession(event)

  // Optional: Revoke Cidaas tokens (Single Sign-Out)
  // This would require storing refresh_token in session and calling Cidaas revoke endpoint

  return {
    success: true,
  }
})
```

---

### 5. Current User Endpoint

**File:** `server/api/auth/me.get.ts`

```typescript
// server/api/auth/me.get.ts

/**
 * GET /api/auth/me
 *
 * Get current authenticated user
 *
 * Response:
 * {
 *   "id": "uuid",
 *   "experimentaId": "cidaas-sub",
 *   "email": "user@example.com",
 *   "firstName": "Max",
 *   "lastName": "Mustermann",
 *   ...
 * }
 *
 * Returns 401 if not authenticated
 */

import { eq } from 'drizzle-orm'
import { users } from '~/server/database/schema'

export default defineEventHandler(async (event) => {
  // 1. Require authentication (throws 401 if not logged in)
  const { user: sessionUser } = await requireUserSession(event)

  // 2. Fetch fresh user data from database
  const db = useDatabase()
  const user = await db.query.users.findFirst({
    where: eq(users.id, sessionUser.id),
  })

  if (!user) {
    throw createError({
      statusCode: 404,
      statusMessage: 'User not found',
    })
  }

  // 3. Return user profile (exclude sensitive fields if any)
  return {
    id: user.id,
    experimentaId: user.experimentaId,
    email: user.email,
    firstName: user.firstName,
    lastName: user.lastName,
    phone: user.phone,

    // Billing address
    salutation: user.salutation,
    dateOfBirth: user.dateOfBirth,
    street: user.street,
    postCode: user.postCode,
    city: user.city,
    countryCode: user.countryCode,

    createdAt: user.createdAt,
    updatedAt: user.updatedAt,
  }
})
```

---

## Client-Side Implementation

### 1. Auth Composable

**File:** `composables/useAuth.ts`

```typescript
// composables/useAuth.ts

/**
 * Authentication composable
 *
 * Wrapper around nuxt-auth-utils useUserSession() with convenience methods
 *
 * Usage:
 * const { user, loggedIn, login, logout } = useAuth()
 */

export function useAuth() {
  const { loggedIn, user, clear, fetch } = useUserSession()

  /**
   * Login with email
   * Initiates OAuth2 flow
   */
  async function login(email: string) {
    try {
      // Call login endpoint to get redirect URL
      const { redirectUrl } = await $fetch('/api/auth/login', {
        method: 'POST',
        body: { email },
      })

      // Redirect to Cidaas
      navigateTo(redirectUrl, { external: true })
    } catch (error) {
      console.error('Login failed:', error)
      throw error
    }
  }

  /**
   * Register new user
   */
  async function register(data: {
    email: string
    password: string
    firstName: string
    lastName: string
  }) {
    try {
      const result = await $fetch('/api/auth/register', {
        method: 'POST',
        body: data,
      })

      return result
    } catch (error) {
      console.error('Registration failed:', error)
      throw error
    }
  }

  /**
   * Logout
   * Clears session and redirects to homepage
   */
  async function logout() {
    try {
      await $fetch('/api/auth/logout', { method: 'POST' })
      await clear() // Clear client-side state
      navigateTo('/') // Redirect to homepage
    } catch (error) {
      console.error('Logout failed:', error)
      throw error
    }
  }

  /**
   * Refresh user data from server
   */
  async function refreshUser() {
    try {
      await fetch() // Re-fetch session from server
    } catch (error) {
      console.error('Refresh user failed:', error)
    }
  }

  return {
    user,
    loggedIn,
    login,
    register,
    logout,
    refreshUser,
  }
}
```

---

### 2. Auth Page (Login + Register Tabs)

**File:** `pages/auth.vue`

```vue
<!-- pages/auth.vue -->

<script setup lang="ts">
/**
 * Combined Authentication Page
 *
 * Features:
 * - Tab navigation (Login / Register)
 * - Redirects logged-in users to homepage
 * - Stores intended destination for post-login redirect
 */

const route = useRoute()
const { loggedIn } = useAuth()

// Redirect if already logged in
if (loggedIn.value) {
  navigateTo('/')
}

// Active tab state
const activeTab = ref<'login' | 'register'>('login')

// Set tab from query param if present
onMounted(() => {
  if (route.query.tab === 'register') {
    activeTab.value = 'register'
  }
})

// Error message from OAuth callback
const errorMessage = computed(() => {
  if (route.query.error === 'login_failed') {
    return 'Login fehlgeschlagen. Bitte versuchen Sie es erneut.'
  }
  return null
})

// Set page meta
definePageMeta({
  layout: 'auth', // Optional: Use separate layout for auth pages
})

// i18n
const { t } = useI18n()
</script>

<template>
  <div class="container mx-auto max-w-md px-4 py-16">
    <div class="mb-8 text-center">
      <h1 class="text-3xl font-bold">
        {{ t('auth.welcome') }}
      </h1>
      <p class="mt-2 text-muted-foreground">
        {{ t('auth.subtitle') }}
      </p>
    </div>

    <!-- Error Alert -->
    <Alert v-if="errorMessage" variant="destructive" class="mb-6">
      <AlertCircle class="h-4 w-4" />
      <AlertTitle>{{ t('auth.error') }}</AlertTitle>
      <AlertDescription>{{ errorMessage }}</AlertDescription>
    </Alert>

    <!-- Tabs -->
    <Tabs v-model="activeTab" class="w-full">
      <TabsList class="grid w-full grid-cols-2">
        <TabsTrigger value="login">
          {{ t('auth.login') }}
        </TabsTrigger>
        <TabsTrigger value="register">
          {{ t('auth.register') }}
        </TabsTrigger>
      </TabsList>

      <!-- Login Tab -->
      <TabsContent value="login">
        <Card>
          <CardHeader>
            <CardTitle>{{ t('auth.loginTitle') }}</CardTitle>
            <CardDescription>
              {{ t('auth.loginDescription') }}
            </CardDescription>
          </CardHeader>
          <CardContent>
            <AuthLoginForm />
          </CardContent>
        </Card>
      </TabsContent>

      <!-- Register Tab -->
      <TabsContent value="register">
        <Card>
          <CardHeader>
            <CardTitle>{{ t('auth.registerTitle') }}</CardTitle>
            <CardDescription>
              {{ t('auth.registerDescription') }}
            </CardDescription>
          </CardHeader>
          <CardContent>
            <AuthRegisterForm />
          </CardContent>
        </Card>
      </TabsContent>
    </Tabs>
  </div>
</template>
```

---

### 3. Login Form Component

**File:** `components/auth/LoginForm.vue`

```vue
<!-- components/auth/LoginForm.vue -->

<script setup lang="ts">
import { z } from 'zod'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'

const { login } = useAuth()
const { t } = useI18n()

// Validation schema
const loginSchema = toTypedSchema(
  z.object({
    email: z.string().email(t('auth.validation.invalidEmail')),
  })
)

// Form state
const { handleSubmit, isSubmitting, errors } = useForm({
  validationSchema: loginSchema,
})

// Success/error state
const submitError = ref<string | null>(null)

// Form submit handler
const onSubmit = handleSubmit(async (values) => {
  submitError.value = null

  try {
    await login(values.email)
    // Redirect happens in login() function
  } catch (error: any) {
    console.error('Login error:', error)
    submitError.value = error.data?.message || t('auth.loginError')
  }
})
</script>

<template>
  <form @submit="onSubmit" class="space-y-4">
    <!-- Error Alert -->
    <Alert v-if="submitError" variant="destructive">
      <AlertCircle class="h-4 w-4" />
      <AlertDescription>{{ submitError }}</AlertDescription>
    </Alert>

    <!-- Email Field -->
    <FormField v-slot="{ componentField }" name="email">
      <FormItem>
        <FormLabel>{{ t('auth.email') }}</FormLabel>
        <FormControl>
          <Input type="email" :placeholder="t('auth.emailPlaceholder')" v-bind="componentField" />
        </FormControl>
        <FormMessage />
      </FormItem>
    </FormField>

    <!-- Submit Button -->
    <Button type="submit" class="w-full" :disabled="isSubmitting">
      <Loader2 v-if="isSubmitting" class="mr-2 h-4 w-4 animate-spin" />
      {{ isSubmitting ? t('auth.loggingIn') : t('auth.loginButton') }}
    </Button>

    <!-- Info Text -->
    <p class="text-sm text-muted-foreground text-center">
      {{ t('auth.loginInfo') }}
    </p>
  </form>
</template>
```

---

### 4. Register Form Component

**File:** `components/auth/RegisterForm.vue`

```vue
<!-- components/auth/RegisterForm.vue -->

<script setup lang="ts">
import { z } from 'zod'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'

const { register } = useAuth()
const { t } = useI18n()

// Validation schema
const registerSchema = toTypedSchema(
  z.object({
    email: z.string().email(t('auth.validation.invalidEmail')),
    password: z
      .string()
      .min(8, t('auth.validation.passwordMinLength'))
      .regex(/[A-Z]/, t('auth.validation.passwordUppercase'))
      .regex(/[a-z]/, t('auth.validation.passwordLowercase'))
      .regex(/[0-9]/, t('auth.validation.passwordNumber')),
    firstName: z.string().min(2, t('auth.validation.firstNameMinLength')),
    lastName: z.string().min(2, t('auth.validation.lastNameMinLength')),
  })
)

// Form state
const { handleSubmit, isSubmitting } = useForm({
  validationSchema: registerSchema,
})

// Success/error state
const submitError = ref<string | null>(null)
const submitSuccess = ref(false)

// Form submit handler
const onSubmit = handleSubmit(async (values) => {
  submitError.value = null
  submitSuccess.value = false

  try {
    const result = await register(values)

    submitSuccess.value = true

    // Show success message for 3 seconds, then switch to login tab
    setTimeout(() => {
      navigateTo('/auth?tab=login')
    }, 3000)
  } catch (error: any) {
    console.error('Registration error:', error)

    if (error.status === 409) {
      submitError.value = t('auth.emailAlreadyRegistered')
    } else {
      submitError.value = error.data?.message || t('auth.registrationError')
    }
  }
})
</script>

<template>
  <form @submit="onSubmit" class="space-y-4">
    <!-- Success Alert -->
    <Alert v-if="submitSuccess" variant="default" class="border-green-500">
      <CheckCircle class="h-4 w-4 text-green-500" />
      <AlertTitle>{{ t('auth.registrationSuccess') }}</AlertTitle>
      <AlertDescription>
        {{ t('auth.registrationSuccessMessage') }}
      </AlertDescription>
    </Alert>

    <!-- Error Alert -->
    <Alert v-if="submitError" variant="destructive">
      <AlertCircle class="h-4 w-4" />
      <AlertDescription>{{ submitError }}</AlertDescription>
    </Alert>

    <!-- First Name -->
    <FormField v-slot="{ componentField }" name="firstName">
      <FormItem>
        <FormLabel>{{ t('auth.firstName') }}</FormLabel>
        <FormControl>
          <Input
            type="text"
            :placeholder="t('auth.firstNamePlaceholder')"
            v-bind="componentField"
          />
        </FormControl>
        <FormMessage />
      </FormItem>
    </FormField>

    <!-- Last Name -->
    <FormField v-slot="{ componentField }" name="lastName">
      <FormItem>
        <FormLabel>{{ t('auth.lastName') }}</FormLabel>
        <FormControl>
          <Input type="text" :placeholder="t('auth.lastNamePlaceholder')" v-bind="componentField" />
        </FormControl>
        <FormMessage />
      </FormItem>
    </FormField>

    <!-- Email -->
    <FormField v-slot="{ componentField }" name="email">
      <FormItem>
        <FormLabel>{{ t('auth.email') }}</FormLabel>
        <FormControl>
          <Input type="email" :placeholder="t('auth.emailPlaceholder')" v-bind="componentField" />
        </FormControl>
        <FormMessage />
      </FormItem>
    </FormField>

    <!-- Password -->
    <FormField v-slot="{ componentField }" name="password">
      <FormItem>
        <FormLabel>{{ t('auth.password') }}</FormLabel>
        <FormControl>
          <Input
            type="password"
            :placeholder="t('auth.passwordPlaceholder')"
            v-bind="componentField"
          />
        </FormControl>
        <FormDescription>
          {{ t('auth.passwordRequirements') }}
        </FormDescription>
        <FormMessage />
      </FormItem>
    </FormField>

    <!-- Submit Button -->
    <Button type="submit" class="w-full" :disabled="isSubmitting || submitSuccess">
      <Loader2 v-if="isSubmitting" class="mr-2 h-4 w-4 animate-spin" />
      {{ isSubmitting ? t('auth.registering') : t('auth.registerButton') }}
    </Button>

    <!-- Terms & Privacy -->
    <p class="text-xs text-muted-foreground text-center">
      {{ t('auth.termsAgreement') }}
      <a href="/datenschutz" class="underline hover:text-primary">
        {{ t('auth.privacyPolicy') }}
      </a>
      {{ t('auth.and') }}
      <a href="/agb" class="underline hover:text-primary">
        {{ t('auth.termsOfService') }}
      </a>
    </p>
  </form>
</template>
```

---

## Middleware

### 1. Auth Route Protection

**File:** `middleware/auth.ts`

```typescript
// middleware/auth.ts

/**
 * Authentication middleware
 *
 * Protects routes from unauthenticated access
 *
 * Usage in pages:
 *
 * definePageMeta({
 *   middleware: 'auth'
 * })
 */

export default defineNuxtRouteMiddleware(async (to, from) => {
  const { loggedIn } = useUserSession()

  // Not logged in - redirect to auth page
  if (!loggedIn.value) {
    // Store intended destination for post-login redirect
    useCookie('redirect_after_login', {
      maxAge: 600, // 10 minutes
      path: '/',
    }).value = to.fullPath

    return navigateTo('/auth')
  }
})
```

**Usage in pages:**

```vue
<!-- pages/profile.vue -->
<script setup lang="ts">
definePageMeta({
  middleware: 'auth', // Require authentication
})

const { user } = useAuth()
</script>

<template>
  <div>
    <h1>Welcome, {{ user.firstName }}!</h1>
  </div>
</template>
```

---

### 2. Rate Limiting Middleware

**File:** `server/middleware/rate-limit.ts`

```typescript
// server/middleware/rate-limit.ts

/**
 * Rate limiting middleware for auth endpoints
 *
 * Prevents brute force attacks on login/registration
 *
 * Limits:
 * - /api/auth/login: 5 attempts per 15 minutes per IP
 * - /api/auth/register: 3 attempts per hour per IP
 */

interface RateLimitEntry {
  count: number
  resetAt: number
}

// In-memory rate limit store (use Redis in production!)
const rateLimitStore = new Map<string, RateLimitEntry>()

// Clean up expired entries every 5 minutes
setInterval(
  () => {
    const now = Date.now()
    for (const [key, entry] of rateLimitStore.entries()) {
      if (entry.resetAt < now) {
        rateLimitStore.delete(key)
      }
    }
  },
  5 * 60 * 1000
)

export default defineEventHandler((event) => {
  const path = event.path

  // Only apply to auth endpoints
  if (!path.startsWith('/api/auth/')) {
    return
  }

  // Get client IP
  const ip = getRequestIP(event, { xForwardedFor: true }) || 'unknown'

  // Define rate limits per endpoint
  const limits: Record<string, { maxAttempts: number; windowMs: number }> = {
    '/api/auth/login': { maxAttempts: 5, windowMs: 15 * 60 * 1000 }, // 5 per 15min
    '/api/auth/register': { maxAttempts: 3, windowMs: 60 * 60 * 1000 }, // 3 per hour
  }

  const limit = limits[path]
  if (!limit) {
    return // No rate limit for this endpoint
  }

  // Check rate limit
  const key = `${ip}:${path}`
  const now = Date.now()
  const entry = rateLimitStore.get(key)

  if (!entry || entry.resetAt < now) {
    // First attempt or window expired - reset counter
    rateLimitStore.set(key, {
      count: 1,
      resetAt: now + limit.windowMs,
    })
    return
  }

  // Increment counter
  entry.count++

  if (entry.count > limit.maxAttempts) {
    // Rate limit exceeded
    const retryAfter = Math.ceil((entry.resetAt - now) / 1000)

    setResponseStatus(event, 429)
    setResponseHeader(event, 'Retry-After', retryAfter.toString())

    throw createError({
      statusCode: 429,
      statusMessage: 'Too many requests',
      data: {
        retryAfter,
        message: `Too many attempts. Please try again in ${retryAfter} seconds.`,
      },
    })
  }
})
```

**Production Note:** Use Redis for rate limiting in production:

```typescript
// Alternative: Redis-based rate limiting
import { Redis } from 'ioredis'

const redis = new Redis(process.env.REDIS_URL)

async function checkRateLimit(key: string, maxAttempts: number, windowMs: number) {
  const count = await redis.incr(key)

  if (count === 1) {
    await redis.pexpire(key, windowMs)
  }

  return count <= maxAttempts
}
```

---

## i18n Translations

### German Translations

**File:** `locales/de.json`

```json
{
  "auth": {
    "welcome": "Willkommen",
    "subtitle": "Melden Sie sich an oder erstellen Sie ein Konto",
    "login": "Anmelden",
    "register": "Registrieren",
    "loginTitle": "Anmelden",
    "loginDescription": "Melden Sie sich mit Ihrer E-Mail-Adresse an",
    "loginButton": "Anmelden",
    "loggingIn": "Wird angemeldet...",
    "loginInfo": "Sie werden zur sicheren Anmeldeseite weitergeleitet",
    "loginError": "Anmeldung fehlgeschlagen. Bitte versuchen Sie es erneut.",
    "registerTitle": "Konto erstellen",
    "registerDescription": "Erstellen Sie ein neues experimenta-Konto",
    "registerButton": "Konto erstellen",
    "registering": "Wird registriert...",
    "registrationSuccess": "Registrierung erfolgreich!",
    "registrationSuccessMessage": "Bitte bestätigen Sie Ihre E-Mail-Adresse über den Link, den wir Ihnen gesendet haben.",
    "registrationError": "Registrierung fehlgeschlagen. Bitte versuchen Sie es erneut.",
    "emailAlreadyRegistered": "Diese E-Mail-Adresse ist bereits registriert.",
    "error": "Fehler",
    "email": "E-Mail-Adresse",
    "emailPlaceholder": "ihre.email@beispiel.de",
    "password": "Passwort",
    "passwordPlaceholder": "Mindestens 8 Zeichen",
    "passwordRequirements": "Mindestens 8 Zeichen, Groß-/Kleinbuchstaben und eine Zahl",
    "firstName": "Vorname",
    "firstNamePlaceholder": "Max",
    "lastName": "Nachname",
    "lastNamePlaceholder": "Mustermann",
    "termsAgreement": "Mit der Registrierung stimmen Sie unserer",
    "privacyPolicy": "Datenschutzerklärung",
    "and": "und den",
    "termsOfService": "Nutzungsbedingungen",
    "validation": {
      "invalidEmail": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
      "passwordMinLength": "Das Passwort muss mindestens 8 Zeichen lang sein",
      "passwordUppercase": "Das Passwort muss mindestens einen Großbuchstaben enthalten",
      "passwordLowercase": "Das Passwort muss mindestens einen Kleinbuchstaben enthalten",
      "passwordNumber": "Das Passwort muss mindestens eine Zahl enthalten",
      "firstNameMinLength": "Der Vorname muss mindestens 2 Zeichen lang sein",
      "lastNameMinLength": "Der Nachname muss mindestens 2 Zeichen lang sein"
    }
  }
}
```

---

### English Translations

**File:** `locales/en.json`

```json
{
  "auth": {
    "welcome": "Welcome",
    "subtitle": "Sign in or create an account",
    "login": "Sign In",
    "register": "Sign Up",
    "loginTitle": "Sign In",
    "loginDescription": "Sign in with your email address",
    "loginButton": "Sign In",
    "loggingIn": "Signing in...",
    "loginInfo": "You will be redirected to our secure login page",
    "loginError": "Login failed. Please try again.",
    "registerTitle": "Create Account",
    "registerDescription": "Create a new experimenta account",
    "registerButton": "Create Account",
    "registering": "Creating account...",
    "registrationSuccess": "Registration successful!",
    "registrationSuccessMessage": "Please verify your email address using the link we sent you.",
    "registrationError": "Registration failed. Please try again.",
    "emailAlreadyRegistered": "This email address is already registered.",
    "error": "Error",
    "email": "Email Address",
    "emailPlaceholder": "your.email@example.com",
    "password": "Password",
    "passwordPlaceholder": "At least 8 characters",
    "passwordRequirements": "At least 8 characters, upper/lowercase letters and a number",
    "firstName": "First Name",
    "firstNamePlaceholder": "John",
    "lastName": "Last Name",
    "lastNamePlaceholder": "Doe",
    "termsAgreement": "By registering, you agree to our",
    "privacyPolicy": "Privacy Policy",
    "and": "and",
    "termsOfService": "Terms of Service",
    "validation": {
      "invalidEmail": "Please enter a valid email address",
      "passwordMinLength": "Password must be at least 8 characters",
      "passwordUppercase": "Password must contain at least one uppercase letter",
      "passwordLowercase": "Password must contain at least one lowercase letter",
      "passwordNumber": "Password must contain at least one number",
      "firstNameMinLength": "First name must be at least 2 characters",
      "lastNameMinLength": "Last name must be at least 2 characters"
    }
  }
}
```

---

## Security Best Practices

### Security Checklist

- [x] **PKCE implemented** - Prevents authorization code interception
- [x] **State parameter** - CSRF protection for OAuth2 callback
- [x] **HTTP-only cookies** - Session cookie not accessible via JavaScript
- [x] **Secure flag** - Cookies only transmitted over HTTPS (production)
- [x] **SameSite=Lax** - Prevents CSRF attacks
- [x] **Encrypted sessions** - AES-256-GCM via nuxt-auth-utils
- [x] **JWT validation** - Signature, expiry, issuer, audience checks
- [x] **Rate limiting** - Prevents brute force attacks
- [x] **Input validation** - Zod schemas on all user inputs
- [x] **Short-lived temporary cookies** - PKCE verifier, state (5min TTL)
- [x] **HTTPS mandatory** - All auth flows over HTTPS
- [x] **Secrets in environment variables** - Never hardcoded
- [x] **Session expiration** - 30 days with encrypted storage

---

### Threat Model & Mitigations

| Threat                              | Mitigation                            |
| ----------------------------------- | ------------------------------------- |
| **Authorization Code Interception** | PKCE (code_verifier + code_challenge) |
| **CSRF on OAuth callback**          | State parameter validation            |
| **XSS attacks**                     | HTTP-only cookies, input sanitization |
| **Session hijacking**               | Secure + SameSite cookies, HTTPS only |
| **Token forgery**                   | JWT signature validation with JWKS    |
| **Brute force login**               | Rate limiting (5 attempts / 15min)    |
| **Registration spam**               | Rate limiting (3 attempts / hour)     |
| **Man-in-the-middle**               | HTTPS enforcement, HSTS headers       |

---

## Testing Strategy

### Unit Tests

**Example: PKCE Utilities Test**

```typescript
// server/utils/pkce.test.ts

import { describe, it, expect } from 'vitest'
import { generateCodeVerifier, generateCodeChallenge, generatePKCE } from './pkce'

describe('PKCE Utilities', () => {
  it('generates code verifier with correct length', () => {
    const verifier = generateCodeVerifier(64)
    expect(verifier).toHaveLength(86) // Base64URL encoding adds ~33% length
  })

  it('generates consistent code challenge for same verifier', async () => {
    const verifier = 'test-verifier-123'
    const challenge1 = await generateCodeChallenge(verifier)
    const challenge2 = await generateCodeChallenge(verifier)
    expect(challenge1).toBe(challenge2)
  })

  it('generates different challenges for different verifiers', async () => {
    const challenge1 = await generateCodeChallenge('verifier-1')
    const challenge2 = await generateCodeChallenge('verifier-2')
    expect(challenge1).not.toBe(challenge2)
  })

  it('generates PKCE pair with verifier and challenge', async () => {
    const { verifier, challenge } = await generatePKCE()
    expect(verifier).toBeDefined()
    expect(challenge).toBeDefined()
    expect(typeof verifier).toBe('string')
    expect(typeof challenge).toBe('string')
  })
})
```

---

### Integration Tests

**Example: Login Flow Test**

```typescript
// tests/integration/auth-flow.test.ts

import { describe, it, expect, vi } from 'vitest'
import { setup, $fetch } from '@nuxt/test-utils'

describe('Auth Flow Integration', () => {
  await setup({
    // Test configuration
  })

  it('initiates OAuth2 flow on login', async () => {
    const response = await $fetch('/api/auth/login', {
      method: 'POST',
      body: { email: 'test@example.com' },
    })

    expect(response.redirectUrl).toContain('cidaas.de')
    expect(response.redirectUrl).toContain('code_challenge')
    expect(response.redirectUrl).toContain('state')
  })

  it('validates state parameter in callback', async () => {
    // Mock invalid state
    await expect($fetch('/api/auth/callback?code=abc&state=invalid')).rejects.toThrow(
      'Invalid state parameter'
    )
  })

  it('creates user on first login', async () => {
    // Mock Cidaas responses
    vi.mock('~/server/utils/cidaas', () => ({
      exchangeCodeForToken: vi.fn().mockResolvedValue({
        access_token: 'mock-token',
        id_token: 'mock-id-token',
      }),
      fetchUserInfo: vi.fn().mockResolvedValue({
        sub: 'new-user-123',
        email: 'newuser@example.com',
        given_name: 'New',
        family_name: 'User',
      }),
    }))

    // Test callback creates user
    // ... (test implementation)
  })
})
```

---

### E2E Tests

**Example: Playwright E2E Test**

```typescript
// tests/e2e/auth.spec.ts

import { test, expect } from '@playwright/test'

test.describe('Authentication Flow', () => {
  test('user can login successfully', async ({ page }) => {
    // Navigate to auth page
    await page.goto('/auth')

    // Enter email
    await page.fill('input[type="email"]', 'test@example.com')

    // Click login button
    await page.click('button[type="submit"]')

    // Should redirect to Cidaas
    await expect(page).toHaveURL(/cidaas\.de/)

    // Fill Cidaas login form (on Cidaas page)
    await page.fill('input[name="username"]', 'test@example.com')
    await page.fill('input[name="password"]', 'Test123!')
    await page.click('button[type="submit"]')

    // Should redirect back to our app
    await expect(page).toHaveURL('/')

    // Should show user menu
    await expect(page.locator('text=Profil')).toBeVisible()
  })

  test('user can register new account', async ({ page }) => {
    await page.goto('/auth?tab=register')

    await page.fill('input[name="firstName"]', 'Max')
    await page.fill('input[name="lastName"]', 'Mustermann')
    await page.fill('input[name="email"]', 'max@example.com')
    await page.fill('input[name="password"]', 'SecurePassword123!')

    await page.click('button[type="submit"]')

    // Should show success message
    await expect(page.locator('text=Registrierung erfolgreich')).toBeVisible()
  })

  test('protected route redirects to login', async ({ page }) => {
    // Visit protected page while logged out
    await page.goto('/profile')

    // Should redirect to auth page
    await expect(page).toHaveURL('/auth')
  })

  test('user can logout', async ({ page, context }) => {
    // Assume user is logged in (set session cookie)
    // ... setup logged-in state

    await page.goto('/')
    await page.click('button:has-text("Abmelden")')

    // Should redirect to homepage
    await expect(page).toHaveURL('/')

    // Session cookie should be cleared
    const cookies = await context.cookies()
    const sessionCookie = cookies.find((c) => c.name === 'experimenta-session')
    expect(sessionCookie).toBeUndefined()
  })
})
```

---

## Troubleshooting Guide

### Common Errors

#### 1. "Invalid state parameter"

**Cause:** State cookie expired or CSRF attack

**Solution:**

- Ensure cookies are enabled
- Check that cookie domain matches application domain
- Increase state cookie TTL if needed (currently 5 minutes)

---

#### 2. "PKCE verifier not found"

**Cause:** PKCE verifier cookie expired before callback

**Solution:**

- Ensure cookies are enabled
- User must complete OAuth2 flow within 5 minutes
- Check cookie SameSite settings

---

#### 3. "JWT verification failed"

**Cause:** Invalid ID token signature or expired token

**Solution:**

- Verify CIDAAS_JWKS_URL is correct
- Check system clock is synchronized (JWT exp check)
- Verify CIDAAS_ISSUER and CIDAAS_CLIENT_ID match token claims

---

#### 4. "Token exchange failed"

**Cause:** Invalid authorization code or client credentials

**Solution:**

- Verify CIDAAS_CLIENT_ID and CIDAAS_CLIENT_SECRET
- Ensure redirect URI matches exactly (no trailing slash!)
- Check Cidaas admin panel configuration

---

#### 5. "Too many requests (429)"

**Cause:** Rate limit exceeded

**Solution:**

- Wait for rate limit window to expire
- Increase rate limit thresholds if legitimate traffic
- Implement Redis-based rate limiting for production

---

### Debug Tips

**1. Enable detailed logging:**

```typescript
// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    logLevel: 'debug', // Show all logs
  },
})
```

**2. Inspect session cookie:**

```typescript
// In browser console
document.cookie.split(';').find((c) => c.includes('experimenta-session'))
```

**3. Decode JWT (client-side):**

```javascript
// In browser console
const idToken = 'eyJ...' // From network inspector
const payload = JSON.parse(atob(idToken.split('.')[1]))
console.log(payload)
```

**4. Test OAuth2 flow manually:**

```bash
# 1. Generate PKCE challenge
CODE_VERIFIER=$(openssl rand -base64 64 | tr -d '=' | tr '+/' '-_')
CODE_CHALLENGE=$(echo -n $CODE_VERIFIER | openssl dgst -binary -sha256 | base64 | tr -d '=' | tr '+/' '-_')

# 2. Build authorization URL
https://experimenta.cidaas.de/authz-srv/authz?client_id=YOUR_CLIENT_ID&redirect_uri=http://localhost:3000/api/auth/callback&response_type=code&scope=openid%20profile%20email&code_challenge=$CODE_CHALLENGE&code_challenge_method=S256&state=test-state
```

---

## Production Deployment

### Environment Variables (Production)

```bash
# .env.production

CIDAAS_CLIENT_ID=<production-client-id>
CIDAAS_CLIENT_SECRET=<production-client-secret>
CIDAAS_ISSUER=https://experimenta.cidaas.de
CIDAAS_AUTHORIZE_URL=https://experimenta.cidaas.de/authz-srv/authz
CIDAAS_TOKEN_URL=https://experimenta.cidaas.de/token-srv/token
CIDAAS_USERINFO_URL=https://experimenta.cidaas.de/users-srv/userinfo
CIDAAS_JWKS_URL=https://experimenta.cidaas.de/.well-known/jwks.json
CIDAAS_REDIRECT_URI=https://my.experimenta.science/api/auth/callback

# Generate new secret for production!
NUXT_SESSION_PASSWORD=<64-char-hex-secret>

NODE_ENV=production
```

---

### Docker Secrets

**docker-compose.prod.yml:**

```yaml
services:
  app:
    secrets:
      - cidaas_client_id
      - cidaas_client_secret
      - session_secret
    environment:
      - CIDAAS_CLIENT_ID=/run/secrets/cidaas_client_id
      - CIDAAS_CLIENT_SECRET=/run/secrets/cidaas_client_secret
      - NUXT_SESSION_PASSWORD=/run/secrets/session_secret

secrets:
  cidaas_client_id:
    file: ./secrets/cidaas_client_id.txt
  cidaas_client_secret:
    file: ./secrets/cidaas_client_secret.txt
  session_secret:
    file: ./secrets/session_secret.txt
```

---

### HTTPS Configuration

**Nginx Reverse Proxy:**

```nginx
server {
    listen 443 ssl http2;
    server_name my.experimenta.science;

    ssl_certificate /etc/letsencrypt/live/my.experimenta.science/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/my.experimenta.science/privkey.pem;

    # Security headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;

    location / {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

# Redirect HTTP to HTTPS
server {
    listen 80;
    server_name my.experimenta.science;
    return 301 https://$server_name$request_uri;
}
```

---

## Conclusion

Diese vollständige Implementierungs-Dokumentation bietet:

✅ **Produktionsreife Code-Beispiele** für alle Auth-Komponenten
✅ **Schritt-für-Schritt Setup-Anleitung** von Cidaas-Konfiguration bis Deployment
✅ **Sicherheits-Best-Practices** (PKCE, State, Encrypted Sessions, Rate Limiting)
✅ **Vollständige UI-Komponenten** mit shadcn-nuxt + Tailwind
✅ **i18n-Übersetzungen** (Deutsch + Englisch)
✅ **Testing-Strategie** (Unit, Integration, E2E)
✅ **Troubleshooting-Guide** für häufige Probleme

**Nächste Schritte:**

1. Cidaas Admin-Zugang erhalten
2. OAuth2 Client in Cidaas konfigurieren
3. Dependencies installieren (`nuxt-auth-utils`, `jose`)
4. Code aus dieser Dokumentation implementieren
5. Lokal testen mit `pnpm dev`
6. E2E Tests schreiben und durchführen
7. Production Deployment

**Geschätzter Zeitaufwand:** 3-4 Wochen von Setup bis Production-ready

Bei Fragen oder Problemen: siehe Troubleshooting Guide oder Cidaas Support kontaktieren.
