# Authentication Flow Audit - NextAuth with Keycloak & SSO for Iframe Applications ## Executive Summary This document provides a comprehensive audit of the authentication architecture in the Neah dashboard application. The system uses **NextAuth.js v4** with **Keycloak** as the OAuth provider, implementing JWT-based sessions and supporting Single Sign-On (SSO) for multiple iframe-embedded applications via cookie-based authentication. --- ## Architecture Overview ### Components 1. **NextAuth.js** - Authentication framework 2. **Keycloak** - Identity Provider (IdP) via OAuth 2.0/OpenID Connect 3. **JWT Strategy** - Session management (no database sessions) 4. **Iframe Applications** - Multiple embedded applications using SSO via cookies 5. **Keycloak Admin Client** - Server-side user management --- ## 1. Authentication Entry Points ### 1.1 Sign-In Page (`/app/signin/page.tsx`) **Location**: `app/signin/page.tsx` **Flow**: ```typescript 1. User navigates to /signin 2. Component automatically triggers: signIn("keycloak", { callbackUrl: "/" }) 3. Redirects to Keycloak authorization endpoint 4. After Keycloak authentication, initializes storage via /api/storage/init 5. Redirects to home page ``` **Key Methods**: - `signIn("keycloak")` - NextAuth client-side method - Automatic redirect to Keycloak OAuth flow - Storage initialization after successful authentication **Dependencies**: - `next-auth/react` - Client-side NextAuth hooks - Storage API endpoint for user space initialization --- ## 2. NextAuth Configuration ### 2.1 Route Handler (`/app/api/auth/[...nextauth]/route.ts`) **Location**: `app/api/auth/[...nextauth]/route.ts` **Purpose**: NextAuth API route handler for all authentication endpoints **Endpoints Handled**: - `GET/POST /api/auth/signin` - Sign in - `GET/POST /api/auth/signout` - Sign out - `GET /api/auth/session` - Get current session - `GET /api/auth/csrf` - CSRF token - `GET /api/auth/providers` - Available providers - `GET /api/auth/callback/keycloak` - OAuth callback **Implementation**: ```typescript import NextAuth from "next-auth"; import { authOptions } from "../options"; const handler = NextAuth(authOptions); export { handler as GET, handler as POST }; ``` --- ### 2.2 Auth Options Configuration (`/app/api/auth/options.ts`) **Location**: `app/api/auth/options.ts` **This is the core authentication configuration file.** #### 2.2.1 Keycloak Provider Setup ```typescript KeycloakProvider({ clientId: getRequiredEnvVar("KEYCLOAK_CLIENT_ID"), clientSecret: getRequiredEnvVar("KEYCLOAK_CLIENT_SECRET"), issuer: getRequiredEnvVar("KEYCLOAK_ISSUER"), authorization: { params: { scope: "openid profile email roles" // Requested OAuth scopes } }, profile(profile) { /* Profile transformation */ } }) ``` **Environment Variables Required**: - `KEYCLOAK_CLIENT_ID` - OAuth client identifier - `KEYCLOAK_CLIENT_SECRET` - OAuth client secret - `KEYCLOAK_ISSUER` - Keycloak realm issuer URL (e.g., `https://keycloak.example.com/realms/neah`) **OAuth Scopes Requested**: - `openid` - OpenID Connect core - `profile` - User profile information - `email` - User email address - `roles` - User roles from Keycloak realm #### 2.2.2 Profile Callback **Location**: Lines 109-137 in `options.ts` **Purpose**: Transforms Keycloak user profile into NextAuth user object **Process**: 1. Receives Keycloak profile with `realm_access.roles` 2. Extracts roles from `realm_access.roles` array 3. Cleans roles by: - Removing `ROLE_` prefix (if present) - Converting to lowercase 4. Maps Keycloak profile fields to NextAuth user: - `sub` → `id` - `name` or `preferred_username` → `name` - `email` → `email` - `given_name` → `first_name` - `family_name` → `last_name` - `preferred_username` → `username` - Cleaned roles → `role[]` **Code Flow**: ```typescript profile(profile) { const roles = profile.realm_access?.roles || []; const cleanRoles = roles.map((role: string) => role.replace(/^ROLE_/, '').toLowerCase() ); return { id: profile.sub, name: profile.name ?? profile.preferred_username, email: profile.email, first_name: profile.given_name ?? '', last_name: profile.family_name ?? '', username: profile.preferred_username ?? profile.email?.split('@')[0] ?? '', role: cleanRoles, } } ``` #### 2.2.3 Session Configuration **Location**: Lines 140-143 ```typescript session: { strategy: "jwt", // JWT-based sessions (no database) maxAge: 30 * 24 * 60 * 60, // 30 days } ``` **Characteristics**: - **Strategy**: JWT (stateless, no database lookups) - **Max Age**: 30 days (2,592,000 seconds) - **Storage**: Encrypted JWT stored in HTTP-only cookies #### 2.2.4 JWT Callback **Location**: Lines 145-181 **Purpose**: Handles JWT token creation and refresh **Flow**: **Initial Authentication (account & profile present)**: ```typescript if (account && profile) { 1. Extract roles from Keycloak profile 2. Clean roles (remove ROLE_ prefix, lowercase) 3. Store in JWT token: - accessToken: account.access_token (Keycloak access token) - refreshToken: account.refresh_token (Keycloak refresh token) - accessTokenExpires: account.expires_at (expiration timestamp) - sub: Keycloak user ID - role: cleaned roles array - username, first_name, last_name: from profile } ``` **Subsequent Requests (token refresh check)**: ```typescript else if (token.accessToken) { 1. Decode JWT to extract roles (if not already in token) 2. Check if token is expired: - If expired: Call refreshAccessToken() - If valid: Return existing token } ``` **Token Expiration Check**: ```typescript if (Date.now() < (token.accessTokenExpires as number) * 1000) { return token; // Token still valid } return refreshAccessToken(token); // Token expired, refresh ``` **Note**: There's a **BUG** in line 176 - it multiplies `accessTokenExpires` by 1000, but `expires_at` from Keycloak is already in seconds since epoch. This should be checked. #### 2.2.5 Token Refresh Function **Location**: Lines 64-96 **Purpose**: Refreshes expired Keycloak access tokens **Implementation**: ```typescript async function refreshAccessToken(token: JWT) { 1. POST to Keycloak token endpoint: - URL: ${KEYCLOAK_ISSUER}/protocol/openid-connect/token - Method: POST - Body: * client_id: KEYCLOAK_CLIENT_ID * client_secret: KEYCLOAK_CLIENT_SECRET * grant_type: refresh_token * refresh_token: token.refreshToken 2. On Success: - Update accessToken - Update refreshToken (if new one provided) - Update accessTokenExpires: Date.now() + expires_in * 1000 3. On Error: - Set token.error = "RefreshAccessTokenError" - Return token with error flag } ``` **Error Handling**: Sets `token.error` flag which is checked in session callback #### 2.2.6 Session Callback **Location**: Lines 182-202 **Purpose**: Transforms JWT token into session object for client-side use **Flow**: ```typescript async session({ session, token }) { 1. Check for refresh errors: if (token.error) throw new Error(token.error) 2. Build session.user object: - id: token.sub (Keycloak user ID) - email: token.email - name: token.name - image: null - username: token.username - first_name: token.first_name - last_name: token.last_name - role: token.role (array) - nextcloudInitialized: false (default) 3. Add accessToken to session: session.accessToken = token.accessToken 4. Return session } ``` **Important**: The `accessToken` (Keycloak OAuth token) is exposed in the session object, making it available client-side via `useSession()` hook. #### 2.2.7 Custom Pages **Location**: Lines 204-207 ```typescript pages: { signIn: '/signin', error: '/signin', } ``` **Custom Routes**: - Sign-in page: `/signin` (instead of default `/api/auth/signin`) - Error page: `/signin` (redirects to sign-in on errors) --- ## 3. Authentication Flow Step-by-Step ### 3.1 Initial Sign-In Flow ``` ┌─────────────┐ │ Browser │ └──────┬──────┘ │ │ 1. GET /signin ▼ ┌─────────────────────┐ │ /app/signin/page.tsx │ │ - Auto-triggers │ │ signIn("keycloak") │ └──────┬──────────────┘ │ │ 2. Redirect to NextAuth ▼ ┌──────────────────────────────┐ │ /api/auth/signin/keycloak │ │ - Generates OAuth state │ │ - Redirects to Keycloak │ └──────┬───────────────────────┘ │ │ 3. GET /realms/{realm}/protocol/openid-connect/auth │ ?client_id=... │ &redirect_uri=... │ &response_type=code │ &scope=openid profile email roles │ &state=... ▼ ┌─────────────────────┐ │ Keycloak Server │ │ - Login page │ │ - User credentials │ └──────┬──────────────┘ │ │ 4. User authenticates │ │ 5. POST /realms/{realm}/protocol/openid-connect/token │ (Authorization code exchange) │ │ 6. Keycloak returns: │ - access_token │ - refresh_token │ - id_token │ - expires_in ▼ ┌──────────────────────────────┐ │ /api/auth/callback/keycloak │ │ - Receives authorization code│ │ - Exchanges for tokens │ │ - Fetches user profile │ └──────┬───────────────────────┘ │ │ 7. JWT Callback │ - Stores tokens in JWT │ - Extracts user info │ - Cleans roles │ │ 8. Session Callback │ - Builds session object │ │ 9. Sets NextAuth cookies: │ - next-auth.session-token (encrypted JWT) │ - next-auth.csrf-token ▼ ┌─────────────────────┐ │ Browser (Client) │ │ - Cookies set │ │ - Redirect to / │ └──────┬──────────────┘ │ │ 10. GET / (home page) │ - getServerSession() validates JWT │ - Session available ▼ ┌─────────────────────┐ │ Dashboard Loaded │ └─────────────────────┘ ``` ### 3.2 Subsequent Request Flow (Authenticated) ``` ┌─────────────┐ │ Browser │ └──────┬──────┘ │ │ 1. GET /any-page │ Cookie: next-auth.session-token=... ▼ ┌──────────────────────────────┐ │ Next.js Server │ │ getServerSession(authOptions)│ └──────┬───────────────────────┘ │ │ 2. Decrypt JWT from cookie │ │ 3. Check token expiration │ │ 4a. If valid: │ - Extract user info │ - Return session │ │ 4b. If expired: │ - Call refreshAccessToken() │ - POST to Keycloak /token │ - Update JWT with new tokens │ - Return session ▼ ┌─────────────────────┐ │ Page Component │ │ - session available│ └─────────────────────┘ ``` ### 3.3 Token Refresh Flow ``` ┌─────────────────────┐ │ JWT Callback │ │ (Token expired) │ └──────┬──────────────┘ │ │ 1. Call refreshAccessToken() │ │ 2. POST ${KEYCLOAK_ISSUER}/protocol/openid-connect/token │ Body: │ - client_id │ - client_secret │ - grant_type: refresh_token │ - refresh_token: ▼ ┌─────────────────────┐ │ Keycloak Server │ │ - Validates refresh│ │ token │ │ - Issues new tokens│ └──────┬──────────────┘ │ │ 3. Returns: │ - access_token (new) │ - refresh_token (new, optional) │ - expires_in ▼ ┌─────────────────────┐ │ Update JWT Token │ │ - New accessToken │ │ - New refreshToken │ │ - New expires time │ └──────┬──────────────┘ │ │ 4. Return updated token │ │ 5. Session callback builds session ▼ ┌─────────────────────┐ │ Session Available │ └─────────────────────┘ ``` --- ## 4. Iframe SSO Architecture ### 4.1 Overview The dashboard embeds multiple applications in iframes. These applications rely on **cookie-based SSO** to authenticate users automatically using the Keycloak session established in the parent dashboard. ### 4.2 Iframe Application Pages **Pattern**: All iframe pages follow the same structure: ```typescript // Example: app/parole/page.tsx export default async function Page() { const session = await getServerSession(authOptions); if (!session) { redirect("/signin"); } return ( ); } ``` **Iframe Applications Identified**: 1. **Parole** (`/parole`) - `NEXT_PUBLIC_IFRAME_PAROLE_URL` 2. **Agilite** (`/agilite`) - `NEXT_PUBLIC_IFRAME_AGILITY_URL` 3. **Alma** (`/alma`) - `NEXT_PUBLIC_IFRAME_AI_ASSISTANT_URL` 4. **Vision** (`/vision`) - `NEXT_PUBLIC_IFRAME_CONFERENCE_URL` 5. **The Message** (`/the-message`) - `NEXT_PUBLIC_IFRAME_THEMESSAGE_URL` 6. **WP Admin** (`/wp-admin`) - `NEXT_PUBLIC_IFRAME_MISSIONVIEW_URL` 7. **Mediation** (`/mediation`) - `NEXT_PUBLIC_IFRAME_MEDIATIONS_URL` 8. **Apprendre** (`/apprendre`) - `NEXT_PUBLIC_IFRAME_LEARN_URL` 9. **Gite** (`/gite`) - `NEXT_PUBLIC_IFRAME_GITE_URL` 10. **Artlab** (`/artlab`) - `NEXT_PUBLIC_IFRAME_ARTLAB_URL` 11. **Calcul** (`/calcul`) - `NEXT_PUBLIC_IFRAME_CALCULATION_URL` 12. **Chapitre** (`/chapitre`) - `NEXT_PUBLIC_IFRAME_CHAPTER_URL` 13. **Dossiers** (`/dossiers`) - `NEXT_PUBLIC_IFRAME_DRIVE_URL` 14. **CRM** (`/crm`) - `NEXT_PUBLIC_IFRAME_MEDIATIONS_URL` 15. **Livres** (`/livres`) - `NEXT_PUBLIC_IFRAME_LIVRE_URL` 16. **Showcase** (`/showcase`) - `NEXT_PUBLIC_IFRAME_SHOWCASE_URL` 17. **Radio** (`/radio`) - `NEXT_PUBLIC_IFRAME_RADIO_URL` 18. **Press** (`/press`) - `NEXT_PUBLIC_IFRAME_SHOWCASE_URL` 19. **Observatory** - `NEXT_PUBLIC_IFRAME_OBSERVATORY_URL` 20. **Time Tracker** - `NEXT_PUBLIC_IFRAME_TIMETRACKER_URL` 21. **Missions Board** - `NEXT_PUBLIC_IFRAME_MISSIONSBOARD_URL` 22. **Carnet** - `NEXT_PUBLIC_IFRAME_CARNET_URL` ### 4.3 SSO Cookie Mechanism **How It Works**: 1. **Parent Dashboard Authentication**: - User authenticates via Keycloak in the dashboard - Keycloak sets authentication cookies (domain: Keycloak domain) - NextAuth sets session cookies (domain: dashboard domain) 2. **Iframe Cookie Sharing**: - When iframe loads, browser sends cookies for the iframe's domain - If iframe application is on **same domain** or **subdomain** of Keycloak: - Keycloak cookies are automatically sent - Application can read Keycloak session cookies - SSO works automatically 3. **Cross-Domain Considerations**: - If iframe apps are on different domains, they need: - Same Keycloak realm configuration - Proper CORS settings - Cookie domain configuration in Keycloak - `SameSite=None; Secure` cookie attributes for cross-site ### 4.4 ResponsiveIframe Component **Location**: `app/components/responsive-iframe.tsx` **Features**: - Auto-resizing based on viewport - Hash synchronization (URL fragments) - Full-screen support **Important**: This component does **NOT** handle authentication - it's purely presentational. SSO relies on browser cookie behavior. --- ## 5. Sign-Out Flow ### 5.1 Sign-Out Page **Location**: `app/signout/page.tsx` **Implementation**: ```typescript export default function SignOut() { return (

Déconnexion en cours...

); } ``` ### 5.2 Sign-Out Handler **Location**: `components/auth/signout-handler.tsx` **Flow**: ```typescript 1. clearAuthCookies() - Clears NextAuth cookies client-side 2. signOut({ callbackUrl: "/signin", redirect: true }) - Calls NextAuth signout endpoint - Invalidates session - Redirects to /signin ``` ### 5.3 Cookie Clearing **Location**: `lib/session.ts` - `clearAuthCookies()` **Implementation**: ```typescript export function clearAuthCookies() { const cookies = document.cookie.split(';'); for (const cookie of cookies) { const [name] = cookie.split('='); if (name.trim().startsWith('next-auth.') || name.trim().startsWith('__Secure-next-auth.') || name.trim().startsWith('__Host-next-auth.')) { document.cookie = `${name.trim()}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; } } } ``` **Note**: This only clears NextAuth cookies. Keycloak cookies remain unless: - User manually logs out of Keycloak - Keycloak session expires - Application calls Keycloak logout endpoint ### 5.4 Service Token Invalidation **Location**: `lib/session.ts` - `invalidateServiceTokens()` **Purpose**: Logs out from integrated services (RocketChat, Leantime, etc.) **Services Handled**: - RocketChat: `/api/v1/logout` - Leantime: JSON-RPC logout method **Note**: This function exists but may not be called during standard sign-out flow. --- ## 6. Server-Side Session Access ### 6.1 getServerSession() **Usage Pattern** (seen in all iframe pages): ```typescript import { getServerSession } from "next-auth/next"; import { authOptions } from "@/app/api/auth/options"; const session = await getServerSession(authOptions); ``` **How It Works**: 1. Reads `next-auth.session-token` cookie from request 2. Decrypts JWT using `NEXTAUTH_SECRET` 3. Validates token signature and expiration 4. If expired, triggers refresh (via JWT callback) 5. Returns session object **Location**: Used in: - All iframe page components - Root layout (`app/layout.tsx`) - Any server component needing authentication ### 6.2 Client-Side Session Access **Usage Pattern**: ```typescript import { useSession } from "next-auth/react"; const { data: session, status } = useSession(); ``` **How It Works**: 1. `useSession()` hook calls `/api/auth/session` 2. Server decrypts JWT and returns session 3. Client receives session object 4. Automatically refetches when token refreshes **Location**: Used in: - `app/signin/page.tsx` - `components/auth/auth-check.tsx` - Any client component needing authentication --- ## 7. Keycloak Admin Client ### 7.1 Purpose **Location**: `lib/keycloak.ts` The Keycloak Admin Client is used for **server-side user management**, not for user authentication. It's a separate administrative interface. ### 7.2 Authentication Methods **Two Methods Supported**: 1. **Client Credentials** (Preferred): ```typescript grant_type: 'client_credentials' client_id: KEYCLOAK_CLIENT_ID client_secret: KEYCLOAK_CLIENT_SECRET ``` 2. **Password Grant** (Fallback): ```typescript grant_type: 'password' client_id: KEYCLOAK_CLIENT_ID username: KEYCLOAK_ADMIN_USERNAME password: KEYCLOAK_ADMIN_PASSWORD ``` ### 7.3 Caching **Token Caching**: 5 minutes - Validates cached token before reuse - Creates new client if token invalid/expired ### 7.4 Functions - `getKeycloakAdminClient()` - Get authenticated admin client - `getUserById(userId)` - Get user by Keycloak ID - `getUserByEmail(email)` - Get user by email - `getAllRoles()` - Get all realm roles - `getUserRoles(userId)` - Get user's role mappings --- ## 8. Security Considerations ### 8.1 Cookie Security **NextAuth Cookie Configuration** (implicit): - **HttpOnly**: Yes (prevents XSS access) - **Secure**: Yes (if `NEXTAUTH_URL` starts with `https://`) - **SameSite**: Lax (default) - **Path**: `/` - **Domain**: Dashboard domain **Keycloak Cookie Configuration** (Keycloak-controlled): - Set by Keycloak server - Typically `SameSite=Lax` or `SameSite=None` (for cross-site) - Domain: Keycloak domain or configured domain ### 8.2 Token Storage - **Access Token**: Stored in encrypted JWT (server-side only accessible) - **Refresh Token**: Stored in encrypted JWT - **Session Token**: Encrypted JWT in HTTP-only cookie **Client-Side Access**: - `session.accessToken` is exposed to client via `useSession()` - This is the Keycloak OAuth access token - Can be used for API calls to Keycloak-protected resources ### 8.3 CORS & CSP **Content Security Policy** (`next.config.mjs`): ```typescript 'Content-Security-Policy': "frame-ancestors 'self' https://espace.slm-lab.net https://connect.slm-lab.net" ``` **Allows framing from**: - Same origin (`'self'`) - `https://espace.slm-lab.net` - `https://connect.slm-lab.net` ### 8.4 Role-Based Access Control **Role Extraction**: - Roles come from Keycloak `realm_access.roles` - Cleaned: `ROLE_` prefix removed, lowercased - Stored in session: `session.user.role[]` **Usage**: Roles are available but not actively enforced in the codebase audit. Applications should implement RBAC checks. --- ## 9. Environment Variables ### 9.1 Required for Authentication ```bash # Keycloak OAuth Configuration KEYCLOAK_CLIENT_ID=neah-dashboard KEYCLOAK_CLIENT_SECRET= KEYCLOAK_ISSUER=https://keycloak.example.com/realms/neah KEYCLOAK_REALM=neah # NextAuth Configuration NEXTAUTH_URL=https://dashboard.example.com NEXTAUTH_SECRET= # Keycloak Admin (optional, for user management) KEYCLOAK_ADMIN_USERNAME=admin KEYCLOAK_ADMIN_PASSWORD= KEYCLOAK_BASE_URL=https://keycloak.example.com ``` ### 9.2 Iframe Application URLs All iframe applications require `NEXT_PUBLIC_IFRAME_*` environment variables: - `NEXT_PUBLIC_IFRAME_PAROLE_URL` - `NEXT_PUBLIC_IFRAME_AGILITY_URL` - `NEXT_PUBLIC_IFRAME_AI_ASSISTANT_URL` - `NEXT_PUBLIC_IFRAME_CONFERENCE_URL` - ... (see section 4.2 for complete list) --- ## 10. Potential Issues & Recommendations ### 10.1 Token Expiration Bug **Location**: `app/api/auth/options.ts:176` ```typescript if (Date.now() < (token.accessTokenExpires as number) * 1000) { ``` **Issue**: `accessTokenExpires` from Keycloak `account.expires_at` is already in seconds since epoch. Multiplying by 1000 assumes it's in milliseconds, which may cause incorrect expiration checks. **Recommendation**: Verify Keycloak's `expires_at` format. If it's in seconds, remove the `* 1000`. If it's in milliseconds, keep it. ### 10.2 Cookie SameSite for Cross-Domain Iframes **Issue**: If iframe applications are on different domains, Keycloak cookies may not be sent due to `SameSite` restrictions. **Recommendation**: - Configure Keycloak cookies with `SameSite=None; Secure` - Ensure all domains use HTTPS - Consider using a shared parent domain for cookies ### 10.3 Access Token Exposure **Issue**: `session.accessToken` (Keycloak OAuth token) is exposed client-side. **Recommendation**: - Only expose if needed for client-side API calls - Consider using proxy endpoints instead - Implement token rotation if exposed ### 10.4 No Explicit Cookie Configuration **Issue**: NextAuth cookie settings are implicit (defaults). **Recommendation**: Explicitly configure cookies in `authOptions`: ```typescript cookies: { sessionToken: { name: `next-auth.session-token`, options: { httpOnly: true, sameSite: 'lax', path: '/', secure: process.env.NEXTAUTH_URL?.startsWith('https://') ?? false, } } } ``` ### 10.5 Storage Initialization **Issue**: Storage initialization happens client-side after authentication, which may cause race conditions. **Recommendation**: Move storage initialization to server-side or use a more robust initialization pattern. ### 10.6 Service Token Invalidation Not Called **Issue**: `invalidateServiceTokens()` exists but may not be called during sign-out. **Recommendation**: Integrate service token invalidation into the sign-out flow. --- ## 11. Flow Diagrams ### 11.1 Complete Authentication Flow ``` User → /signin → signIn("keycloak") → /api/auth/signin/keycloak → Keycloak Authorization Endpoint → User Login (Keycloak) → Keycloak Token Endpoint → /api/auth/callback/keycloak → JWT Callback (store tokens) → Session Callback (build session) → Set Cookies → Redirect to / → Storage Init → Dashboard Loaded ``` ### 11.2 Iframe SSO Flow ``` Dashboard (authenticated) → User clicks iframe app link → Server checks session (getServerSession) → If authenticated: Load iframe → Browser sends cookies to iframe domain → Iframe app reads Keycloak cookies → Iframe app validates session → Iframe app loads authenticated ``` ### 11.3 Token Refresh Flow ``` Request with expired token → getServerSession() → Decrypt JWT → Check expiration → If expired: JWT Callback → refreshAccessToken() → POST to Keycloak /token → Get new tokens → Update JWT → Return session ``` --- ## 12. File Reference Map ### Core Authentication Files | File | Purpose | |------|---------| | `app/api/auth/[...nextauth]/route.ts` | NextAuth route handler | | `app/api/auth/options.ts` | **Main auth configuration** | | `app/signin/page.tsx` | Sign-in page | | `app/signout/page.tsx` | Sign-out page | | `components/auth/signout-handler.tsx` | Sign-out logic | | `components/auth/auth-check.tsx` | Client-side auth guard | | `lib/keycloak.ts` | Keycloak admin client | | `lib/session.ts` | Session utilities | | `types/next-auth.d.ts` | TypeScript definitions | ### Iframe Application Files All in `app/*/page.tsx`: - `app/parole/page.tsx` - `app/agilite/page.tsx` - `app/alma/page.tsx` - `app/vision/page.tsx` - ... (see section 4.2) ### Supporting Files | File | Purpose | |------|---------| | `app/components/responsive-iframe.tsx` | Iframe component | | `app/layout.tsx` | Root layout (session check) | | `components/providers.tsx` | SessionProvider wrapper | | `components/layout/layout-wrapper.tsx` | Layout wrapper with auth | --- ## 13. Testing Checklist ### Authentication Flow - [ ] Sign-in redirects to Keycloak - [ ] Keycloak login works - [ ] Callback receives tokens - [ ] Session is created - [ ] Cookies are set - [ ] User redirected to dashboard - [ ] Storage initializes ### Session Management - [ ] Session persists across page reloads - [ ] Token refresh works when expired - [ ] Session expires after 30 days - [ ] Invalid tokens are rejected ### Sign-Out - [ ] Sign-out clears NextAuth cookies - [ ] User redirected to sign-in - [ ] Session invalidated ### Iframe SSO - [ ] Iframe apps receive Keycloak cookies - [ ] Iframe apps authenticate automatically - [ ] Cross-domain cookies work (if applicable) - [ ] Unauthenticated users redirected ### Security - [ ] HttpOnly cookies enforced - [ ] Secure cookies on HTTPS - [ ] CSRF protection active - [ ] Token encryption working --- ## 14. Conclusion The authentication architecture uses a standard NextAuth + Keycloak OAuth 2.0 flow with JWT-based sessions. The system supports SSO for iframe applications via cookie sharing, assuming proper domain configuration. **Key Strengths**: - Standard OAuth 2.0/OpenID Connect implementation - Stateless JWT sessions (scalable) - Automatic token refresh - Role-based user information **Areas for Improvement**: - Explicit cookie configuration - Token expiration bug fix - Service token invalidation integration - Cross-domain cookie configuration verification - Storage initialization robustness --- **Document Version**: 1.0 **Last Updated**: 2024 **Audited By**: AI Assistant **Next Review**: After implementing recommendations