Update var
This commit is contained in:
parent
f56ca4e982
commit
d2edd23c7a
4
.env
4
.env
@ -100,7 +100,7 @@ REDIS_PORT=6379
|
||||
REDIS_PASSWORD=mySecretPassword
|
||||
|
||||
MICROSOFT_CLIENT_ID="afaffea5-4e10-462a-aa64-e73baf642c57"
|
||||
MICROSOFT_CLIENT_SECRET="eIx8Q~N3ZnXTjTsVM3ECZio4G7t.BO6AYlD1-b2h"
|
||||
MICROSOFT_REDIRECT_URI="https://lab.slm-lab.net/ms"
|
||||
MICROSOFT_CLIENT_SECRET="GOO8Q~.~zJEz5xTSH4OnNgKe.DCuqr~IB~Gb~c0O"
|
||||
MICROSOFT_REDIRECT_URI="https://hub.slm-lab.net/ms"
|
||||
MICROSOFT_TENANT_ID="cb4281a9-4a3e-4ff5-9a85-8425dd04e2b2"
|
||||
N8N_WEBHOOK_URL="https://brain.slm-lab.net/webhook-test/mission-created"
|
||||
|
||||
224
AUTHENTICATION_FIXES.md
Normal file
224
AUTHENTICATION_FIXES.md
Normal file
@ -0,0 +1,224 @@
|
||||
# Authentication Flow Fixes
|
||||
|
||||
## Issues Fixed
|
||||
|
||||
### 1. Logout Loop Issue ✅
|
||||
|
||||
**Problem**:
|
||||
- User couldn't log out - infinite redirect loop
|
||||
- Sign-in page auto-triggered Keycloak login even when user was already authenticated
|
||||
- Keycloak session cookies weren't cleared, causing immediate re-authentication
|
||||
|
||||
**Root Cause**:
|
||||
- `/signin` page had `useEffect(() => { signIn("keycloak") }, [])` that always triggered login
|
||||
- No check for existing authentication status
|
||||
- Keycloak logout endpoint was never called, leaving Keycloak cookies valid
|
||||
|
||||
**Fix Applied**:
|
||||
1. **Sign-in page** (`app/signin/page.tsx`):
|
||||
- Added check for existing session before triggering login
|
||||
- If user is already authenticated, redirect to home
|
||||
- Only trigger Keycloak login if status is "unauthenticated"
|
||||
|
||||
2. **Sign-out handler** (`components/auth/signout-handler.tsx`):
|
||||
- Now properly calls Keycloak logout endpoint
|
||||
- Uses ID token for proper logout
|
||||
- Clears both NextAuth and Keycloak cookies
|
||||
|
||||
3. **Main navigation logout** (`components/main-nav.tsx`):
|
||||
- Fixed to use `idToken` instead of `accessToken` for Keycloak logout
|
||||
- Proper logout flow with Keycloak endpoint
|
||||
|
||||
---
|
||||
|
||||
### 2. Iframe Applications Logging Out ✅
|
||||
|
||||
**Problem**:
|
||||
- Iframe applications were logging out even when user was still authenticated in dashboard
|
||||
- Desynchronization between NextAuth session and Keycloak session
|
||||
|
||||
**Root Cause**:
|
||||
- Sign-out only cleared NextAuth cookies
|
||||
- Keycloak session cookies remained valid but could expire independently
|
||||
- Iframe apps rely on Keycloak cookies for SSO
|
||||
- When Keycloak cookies expired/invalidated, iframes logged out but dashboard stayed logged in
|
||||
|
||||
**Fix Applied**:
|
||||
1. **ID Token Storage** (`app/api/auth/options.ts`):
|
||||
- Now stores `idToken` from Keycloak in JWT
|
||||
- Exposes `idToken` in session object
|
||||
- Preserves ID token during token refresh
|
||||
|
||||
2. **Proper Keycloak Logout**:
|
||||
- Sign-out now calls Keycloak logout endpoint with `id_token_hint`
|
||||
- This properly invalidates Keycloak session and clears Keycloak cookies
|
||||
- Ensures synchronization between dashboard and iframe apps
|
||||
|
||||
3. **Type Definitions** (`types/next-auth.d.ts`):
|
||||
- Added `idToken` to Session and JWT interfaces
|
||||
- Type-safe access to ID token
|
||||
|
||||
---
|
||||
|
||||
## Changes Made
|
||||
|
||||
### Files Modified
|
||||
|
||||
1. **`app/api/auth/options.ts`**
|
||||
- Added `idToken` to JWT interface
|
||||
- Store `account.id_token` in JWT during initial authentication
|
||||
- Expose `idToken` in session callback
|
||||
- Preserve `idToken` during token refresh
|
||||
|
||||
2. **`app/signin/page.tsx`**
|
||||
- Added session status check
|
||||
- Prevent auto-login if already authenticated
|
||||
- Redirect authenticated users to home
|
||||
|
||||
3. **`components/auth/signout-handler.tsx`**
|
||||
- Call Keycloak logout endpoint with ID token
|
||||
- Proper logout flow that clears both NextAuth and Keycloak sessions
|
||||
|
||||
4. **`components/main-nav.tsx`**
|
||||
- Fixed logout button to use `idToken` instead of `accessToken`
|
||||
- Proper Keycloak logout flow
|
||||
|
||||
5. **`types/next-auth.d.ts`**
|
||||
- Added `idToken?: string` to Session interface
|
||||
- Added `idToken?: string` to JWT interface (both modules)
|
||||
|
||||
---
|
||||
|
||||
## How It Works Now
|
||||
|
||||
### Sign-In Flow (Fixed)
|
||||
|
||||
```
|
||||
1. User navigates to /signin
|
||||
2. Check session status:
|
||||
- If authenticated → Redirect to /
|
||||
- If unauthenticated → Trigger Keycloak login
|
||||
3. After Keycloak authentication:
|
||||
- Store tokens (access, refresh, ID token)
|
||||
- Initialize storage
|
||||
- Redirect to dashboard
|
||||
```
|
||||
|
||||
### Sign-Out Flow (Fixed)
|
||||
|
||||
```
|
||||
1. User clicks logout
|
||||
2. Sign out from NextAuth (clears NextAuth cookies)
|
||||
3. Call Keycloak logout endpoint:
|
||||
- URL: ${KEYCLOAK_ISSUER}/protocol/openid-connect/logout
|
||||
- Parameters:
|
||||
* post_logout_redirect_uri: /signin
|
||||
* id_token_hint: <ID token from session>
|
||||
4. Keycloak clears its session and cookies
|
||||
5. Redirect to /signin (no auto-login loop)
|
||||
```
|
||||
|
||||
### Iframe SSO (Fixed)
|
||||
|
||||
```
|
||||
1. User authenticates in dashboard
|
||||
2. Keycloak sets session cookies
|
||||
3. Iframe apps read Keycloak cookies
|
||||
4. When user logs out:
|
||||
- Keycloak logout endpoint is called
|
||||
- Keycloak cookies are cleared
|
||||
- Iframe apps lose access (synchronized logout)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables Required
|
||||
|
||||
Ensure these are set:
|
||||
|
||||
```bash
|
||||
# Required for logout
|
||||
NEXT_PUBLIC_KEYCLOAK_ISSUER=https://keycloak.example.com/realms/neah
|
||||
|
||||
# Already required for authentication
|
||||
KEYCLOAK_CLIENT_ID=neah-dashboard
|
||||
KEYCLOAK_CLIENT_SECRET=<secret>
|
||||
KEYCLOAK_ISSUER=https://keycloak.example.com/realms/neah
|
||||
NEXTAUTH_URL=https://dashboard.example.com
|
||||
NEXTAUTH_SECRET=<secret>
|
||||
```
|
||||
|
||||
**Important**: `NEXT_PUBLIC_KEYCLOAK_ISSUER` must be set for client-side logout to work.
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Logout Flow
|
||||
- [ ] Click logout button
|
||||
- [ ] Should redirect to Keycloak logout
|
||||
- [ ] Should redirect back to /signin
|
||||
- [ ] Should NOT auto-login (no loop)
|
||||
- [ ] Should be able to manually log in again
|
||||
|
||||
### Sign-In Flow
|
||||
- [ ] Navigate to /signin when not authenticated
|
||||
- [ ] Should trigger Keycloak login
|
||||
- [ ] Navigate to /signin when already authenticated
|
||||
- [ ] Should redirect to / (no auto-login trigger)
|
||||
|
||||
### Iframe SSO
|
||||
- [ ] Log in to dashboard
|
||||
- [ ] Open iframe application
|
||||
- [ ] Should be automatically authenticated
|
||||
- [ ] Log out from dashboard
|
||||
- [ ] Iframe application should also lose authentication
|
||||
- [ ] Refresh iframe - should require login
|
||||
|
||||
---
|
||||
|
||||
## Additional Notes
|
||||
|
||||
### ID Token vs Access Token
|
||||
|
||||
- **Access Token**: Used for API calls to Keycloak-protected resources
|
||||
- **ID Token**: Used for user identification and logout
|
||||
- **Refresh Token**: Used to get new access tokens
|
||||
|
||||
The ID token is required for proper Keycloak logout. It tells Keycloak which session to invalidate.
|
||||
|
||||
### Cookie Synchronization
|
||||
|
||||
The fix ensures that:
|
||||
1. NextAuth cookies are cleared (dashboard logout)
|
||||
2. Keycloak cookies are cleared (via logout endpoint)
|
||||
3. Both happen in sequence, maintaining synchronization
|
||||
|
||||
### Token Refresh
|
||||
|
||||
During token refresh, the ID token is preserved (Keycloak doesn't issue new ID tokens on refresh). This ensures logout continues to work even after token refreshes.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### If logout still loops:
|
||||
|
||||
1. Check browser console for errors
|
||||
2. Verify `NEXT_PUBLIC_KEYCLOAK_ISSUER` is set correctly
|
||||
3. Check that Keycloak logout endpoint is accessible
|
||||
4. Verify ID token is present in session: `console.log(session?.idToken)`
|
||||
|
||||
### If iframes still log out independently:
|
||||
|
||||
1. Check Keycloak cookie domain configuration
|
||||
2. Verify iframe apps are configured to use same Keycloak realm
|
||||
3. Check browser cookie settings (third-party cookies may be blocked)
|
||||
4. Verify Keycloak session timeout settings
|
||||
|
||||
---
|
||||
|
||||
**Date**: 2024
|
||||
**Status**: ✅ Fixed
|
||||
**Version**: 1.0
|
||||
|
||||
988
AUTHENTICATION_FLOW_AUDIT.md
Normal file
988
AUTHENTICATION_FLOW_AUDIT.md
Normal file
@ -0,0 +1,988 @@
|
||||
# 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: <current_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 (
|
||||
<ResponsiveIframe
|
||||
src={process.env.NEXT_PUBLIC_IFRAME_PAROLE_URL || ''}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**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 (
|
||||
<div>
|
||||
<SignOutHandler />
|
||||
<p>Déconnexion en cours...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 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=<secret>
|
||||
KEYCLOAK_ISSUER=https://keycloak.example.com/realms/neah
|
||||
KEYCLOAK_REALM=neah
|
||||
|
||||
# NextAuth Configuration
|
||||
NEXTAUTH_URL=https://dashboard.example.com
|
||||
NEXTAUTH_SECRET=<random-secret>
|
||||
|
||||
# Keycloak Admin (optional, for user management)
|
||||
KEYCLOAK_ADMIN_USERNAME=admin
|
||||
KEYCLOAK_ADMIN_PASSWORD=<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
|
||||
|
||||
@ -36,12 +36,14 @@ declare module "next-auth" {
|
||||
nextcloudInitialized?: boolean;
|
||||
};
|
||||
accessToken?: string;
|
||||
idToken?: string;
|
||||
}
|
||||
|
||||
interface JWT {
|
||||
sub?: string;
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
idToken?: string;
|
||||
accessTokenExpires?: number;
|
||||
role?: string[];
|
||||
username?: string;
|
||||
@ -84,6 +86,8 @@ async function refreshAccessToken(token: JWT) {
|
||||
...token,
|
||||
accessToken: refreshedTokens.access_token,
|
||||
refreshToken: refreshedTokens.refresh_token ?? token.refreshToken,
|
||||
// Keep existing ID token (Keycloak doesn't return new ID token on refresh)
|
||||
idToken: token.idToken,
|
||||
accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000,
|
||||
};
|
||||
} catch (error) {
|
||||
@ -152,6 +156,7 @@ export const authOptions: NextAuthOptions = {
|
||||
|
||||
token.accessToken = account.access_token ?? '';
|
||||
token.refreshToken = account.refresh_token ?? '';
|
||||
token.idToken = account.id_token ?? '';
|
||||
token.accessTokenExpires = account.expires_at ?? 0;
|
||||
token.sub = keycloakProfile.sub;
|
||||
token.role = cleanRoles;
|
||||
@ -197,6 +202,7 @@ export const authOptions: NextAuthOptions = {
|
||||
nextcloudInitialized: false,
|
||||
};
|
||||
session.accessToken = token.accessToken;
|
||||
session.idToken = token.idToken;
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
@ -2,15 +2,26 @@
|
||||
|
||||
import { signIn, useSession } from "next-auth/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function SignIn() {
|
||||
const { data: session } = useSession();
|
||||
const { data: session, status } = useSession();
|
||||
const router = useRouter();
|
||||
const [initializationStatus, setInitializationStatus] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Trigger Keycloak sign-in
|
||||
signIn("keycloak", { callbackUrl: "/" });
|
||||
}, []);
|
||||
// If user is already authenticated, redirect to home
|
||||
if (status === "authenticated" && session?.user) {
|
||||
router.push("/");
|
||||
return;
|
||||
}
|
||||
|
||||
// Only trigger Keycloak sign-in if not authenticated and not loading
|
||||
if (status === "unauthenticated") {
|
||||
// Trigger Keycloak sign-in
|
||||
signIn("keycloak", { callbackUrl: "/" });
|
||||
}
|
||||
}, [status, session, router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (session?.user) {
|
||||
|
||||
@ -1,24 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { signOut } from "next-auth/react";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import { clearAuthCookies } from "@/lib/session";
|
||||
|
||||
export function SignOutHandler() {
|
||||
const { data: session } = useSession();
|
||||
|
||||
useEffect(() => {
|
||||
const handleSignOut = async () => {
|
||||
// Clear only auth-related cookies
|
||||
clearAuthCookies();
|
||||
|
||||
// Then sign out from NextAuth
|
||||
await signOut({
|
||||
callbackUrl: "/signin",
|
||||
redirect: true
|
||||
});
|
||||
try {
|
||||
// Get Keycloak issuer from environment
|
||||
const keycloakIssuer = process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER;
|
||||
const idToken = session?.idToken;
|
||||
|
||||
// First, sign out from NextAuth (clears NextAuth cookies)
|
||||
await signOut({
|
||||
callbackUrl: "/signin",
|
||||
redirect: false
|
||||
});
|
||||
|
||||
// Clear NextAuth cookies client-side
|
||||
clearAuthCookies();
|
||||
|
||||
// If we have Keycloak ID token and issuer, call Keycloak logout
|
||||
if (keycloakIssuer && idToken) {
|
||||
const keycloakLogoutUrl = new URL(
|
||||
`${keycloakIssuer}/protocol/openid-connect/logout`
|
||||
);
|
||||
|
||||
// Add required parameters
|
||||
keycloakLogoutUrl.searchParams.append(
|
||||
'post_logout_redirect_uri',
|
||||
window.location.origin + '/signin'
|
||||
);
|
||||
keycloakLogoutUrl.searchParams.append(
|
||||
'id_token_hint',
|
||||
idToken
|
||||
);
|
||||
|
||||
// Redirect to Keycloak logout (this will clear Keycloak cookies)
|
||||
window.location.href = keycloakLogoutUrl.toString();
|
||||
} else {
|
||||
// Fallback: just redirect to signin if we don't have Keycloak info
|
||||
window.location.href = '/signin';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during sign out:', error);
|
||||
// Fallback: redirect to signin on error
|
||||
window.location.href = '/signin';
|
||||
}
|
||||
};
|
||||
|
||||
handleSignOut();
|
||||
}, []);
|
||||
}, [session]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@ -362,29 +362,37 @@ export function MainNav() {
|
||||
className="text-white/80 hover:text-white hover:bg-black/50 cursor-pointer"
|
||||
onClick={async () => {
|
||||
try {
|
||||
// First sign out from NextAuth
|
||||
const keycloakIssuer = process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER;
|
||||
const idToken = session?.idToken;
|
||||
|
||||
// First sign out from NextAuth (clears NextAuth cookies)
|
||||
await signOut({
|
||||
callbackUrl: '/signin',
|
||||
redirect: false
|
||||
});
|
||||
|
||||
// Then redirect to Keycloak logout with proper parameters
|
||||
const keycloakLogoutUrl = new URL(
|
||||
`${process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER}/protocol/openid-connect/logout`
|
||||
);
|
||||
|
||||
// Add required parameters
|
||||
keycloakLogoutUrl.searchParams.append(
|
||||
'post_logout_redirect_uri',
|
||||
window.location.origin
|
||||
);
|
||||
keycloakLogoutUrl.searchParams.append(
|
||||
'id_token_hint',
|
||||
session?.accessToken || ''
|
||||
);
|
||||
|
||||
// Redirect to Keycloak logout
|
||||
window.location.href = keycloakLogoutUrl.toString();
|
||||
|
||||
// If we have Keycloak ID token and issuer, call Keycloak logout
|
||||
if (keycloakIssuer && idToken) {
|
||||
const keycloakLogoutUrl = new URL(
|
||||
`${keycloakIssuer}/protocol/openid-connect/logout`
|
||||
);
|
||||
|
||||
// Add required parameters
|
||||
keycloakLogoutUrl.searchParams.append(
|
||||
'post_logout_redirect_uri',
|
||||
window.location.origin + '/signin'
|
||||
);
|
||||
keycloakLogoutUrl.searchParams.append(
|
||||
'id_token_hint',
|
||||
idToken
|
||||
);
|
||||
|
||||
// Redirect to Keycloak logout (this will clear Keycloak cookies)
|
||||
window.location.href = keycloakLogoutUrl.toString();
|
||||
} else {
|
||||
// Fallback: just redirect to signin if we don't have Keycloak info
|
||||
window.location.href = '/signin';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during logout:', error);
|
||||
// Fallback to simple redirect if something goes wrong
|
||||
|
||||
3
types/next-auth.d.ts
vendored
3
types/next-auth.d.ts
vendored
@ -12,6 +12,7 @@ declare module "next-auth" {
|
||||
} & DefaultSession["user"];
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
idToken?: string;
|
||||
rocketChatToken?: string | null;
|
||||
rocketChatUserId?: string | null;
|
||||
error?: string;
|
||||
@ -20,6 +21,7 @@ declare module "next-auth" {
|
||||
interface JWT {
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
idToken?: string;
|
||||
accessTokenExpires?: number;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
@ -55,6 +57,7 @@ declare module "next-auth/jwt" {
|
||||
interface JWT {
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
idToken?: string;
|
||||
accessTokenExpires?: number;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user