MAJOR: Replace keycloak-js with nuxt-oidc-auth for seamless SSO integration
## **SOLUTION: Migrate to Server-Side OIDC Authentication**
This completely replaces the problematic keycloak-js client-side implementation
with nuxt-oidc-auth, eliminating all CORS and iframe issues.
### **Benefits:**
- **No more CORS errors** - Server-side OAuth flow
- **No iframe dependencies** - Eliminates cross-domain issues
- **Works with nginx proxy** - No proxy configuration conflicts
- **Better security** - Tokens handled server-side
- **Cleaner integration** - Native Nuxt patterns
- **Maintains Directus compatibility** - Dual auth support
### **Installation & Configuration:**
- Added
uxt-oidc-auth module to nuxt.config.ts
- Configured Keycloak provider with proper OIDC settings
- Updated environment variables for security keys
### **Code Changes:**
#### **Authentication Flow:**
- **middleware/authentication.ts** - Updated to check both Directus + OIDC auth
- **composables/useUnifiedAuth.ts** - Migrated to use useOidcAuth()
- **pages/login.vue** - Updated SSO button to use oidcLogin('keycloak')
#### **Configuration:**
- **nuxt.config.ts** - Added OIDC provider configuration
- **.env.example** - Updated with nuxt-oidc-auth environment variables
- Removed old Keycloak runtime config
#### **Cleanup:**
- Removed keycloak-js dependency from package.json
- Deleted obsolete files:
- composables/useKeycloak.ts
- pages/auth/callback.vue
- server/utils/keycloak-oauth.ts
- server/api/debug/ directory
### **Authentication Routes (Auto-Generated):**
- /auth/keycloak/login - SSO login endpoint
- /auth/keycloak/logout - SSO logout endpoint
- /auth/keycloak/callback - OAuth callback (handled automatically)
### **Security Setup Required:**
Environment variables needed for production:
- NUXT_OIDC_PROVIDERS_KEYCLOAK_CLIENT_SECRET
- NUXT_OIDC_TOKEN_KEY (base64 encoded 32-byte key)
- NUXT_OIDC_SESSION_SECRET (48-character random string)
- NUXT_OIDC_AUTH_SESSION_SECRET (48-character random string)
### **Expected Results:**
SSO login should work without CORS errors
Compatible with nginx proxy setup
Maintains existing Directus authentication
Server-side session management
Automatic token refresh
Ready for container rebuild and production testing!
This commit is contained in:
@@ -1,211 +0,0 @@
|
||||
import type Keycloak from 'keycloak-js'
|
||||
|
||||
export const useKeycloak = () => {
|
||||
const config = useRuntimeConfig()
|
||||
const keycloak = ref<Keycloak | null>(null)
|
||||
const isAuthenticated = ref(false)
|
||||
const user = ref<any>(null)
|
||||
const token = ref<string | null>(null)
|
||||
const isInitialized = ref(false)
|
||||
|
||||
// Get the correct base URL considering proxy setup
|
||||
const getBaseUrl = () => {
|
||||
if (process.server) {
|
||||
// In production, always use the configured HTTPS base URL
|
||||
return config.public.baseUrl
|
||||
}
|
||||
|
||||
// Client-side: detect if we're behind a proxy
|
||||
const currentOrigin = window.location.origin
|
||||
const isProduction = !currentOrigin.includes('localhost') && !currentOrigin.includes('127.0.0.1')
|
||||
|
||||
if (isProduction) {
|
||||
// Force HTTPS in production environments
|
||||
return config.public.baseUrl
|
||||
}
|
||||
|
||||
// Development: use current origin
|
||||
return currentOrigin
|
||||
}
|
||||
|
||||
const logDebug = (message: string, data?: any) => {
|
||||
if (config.public.keycloakDebug) {
|
||||
console.log(`[KEYCLOAK] ${message}`, data)
|
||||
}
|
||||
}
|
||||
|
||||
const initKeycloak = async () => {
|
||||
if (process.server) return false
|
||||
|
||||
try {
|
||||
const baseUrl = getBaseUrl()
|
||||
logDebug('Initializing Keycloak', {
|
||||
baseUrl,
|
||||
keycloakUrl: config.public.keycloak.url,
|
||||
realm: config.public.keycloak.realm,
|
||||
clientId: config.public.keycloak.clientId
|
||||
})
|
||||
|
||||
// Dynamically import keycloak-js
|
||||
const KeycloakModule = await import('keycloak-js')
|
||||
const KeycloakConstructor = KeycloakModule.default
|
||||
|
||||
keycloak.value = new KeycloakConstructor({
|
||||
url: config.public.keycloak.url,
|
||||
realm: config.public.keycloak.realm,
|
||||
clientId: config.public.keycloak.clientId,
|
||||
})
|
||||
|
||||
const authenticated = await keycloak.value.init({
|
||||
onLoad: 'check-sso',
|
||||
// Use proper HTTPS redirect URI
|
||||
redirectUri: `${baseUrl}/auth/callback`,
|
||||
// Disable all iframe-based features that cause CORS issues
|
||||
checkLoginIframe: false,
|
||||
silentCheckSsoRedirectUri: undefined, // Disable silent SSO check
|
||||
enableLogging: false, // Reduce console noise
|
||||
// Use standard flow compatible with proxy setups
|
||||
flow: 'standard',
|
||||
responseMode: 'query', // Use query params instead of fragments
|
||||
// Disable third-party cookie checks
|
||||
checkLoginIframeInterval: 0,
|
||||
// PKCE for security
|
||||
pkceMethod: 'S256',
|
||||
// Timeout settings
|
||||
messageReceiveTimeout: 10000,
|
||||
// Disable adapter features that can cause issues in proxied environments
|
||||
adapter: 'default'
|
||||
})
|
||||
|
||||
logDebug('Keycloak initialization result', {
|
||||
authenticated,
|
||||
redirectUri: `${baseUrl}/auth/callback`,
|
||||
checkLoginIframe: false,
|
||||
silentSso: 'disabled'
|
||||
})
|
||||
|
||||
isAuthenticated.value = authenticated
|
||||
isInitialized.value = true
|
||||
|
||||
if (authenticated && keycloak.value.token) {
|
||||
token.value = keycloak.value.token || null
|
||||
user.value = {
|
||||
id: keycloak.value.subject,
|
||||
username: keycloak.value.tokenParsed?.preferred_username,
|
||||
email: keycloak.value.tokenParsed?.email,
|
||||
firstName: keycloak.value.tokenParsed?.given_name,
|
||||
lastName: keycloak.value.tokenParsed?.family_name,
|
||||
fullName: keycloak.value.tokenParsed?.name,
|
||||
roles: keycloak.value.tokenParsed?.realm_access?.roles || [],
|
||||
}
|
||||
|
||||
// Set up token refresh
|
||||
keycloak.value.onTokenExpired = () => {
|
||||
keycloak.value?.updateToken(30).then((refreshed) => {
|
||||
if (refreshed) {
|
||||
token.value = keycloak.value?.token || null
|
||||
console.log('Token refreshed')
|
||||
} else {
|
||||
console.log('Token still valid')
|
||||
}
|
||||
}).catch(() => {
|
||||
console.log('Failed to refresh token')
|
||||
logout()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return authenticated
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize Keycloak:', error)
|
||||
isInitialized.value = true
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const login = async () => {
|
||||
console.log('[KEYCLOAK] Login function called')
|
||||
|
||||
if (!keycloak.value) {
|
||||
console.log('[KEYCLOAK] Keycloak not initialized, initializing now...')
|
||||
await initKeycloak()
|
||||
}
|
||||
|
||||
if (keycloak.value) {
|
||||
try {
|
||||
const baseUrl = getBaseUrl()
|
||||
console.log('[KEYCLOAK] Starting login', {
|
||||
redirectUri: `${baseUrl}/dashboard`,
|
||||
keycloakInitialized: !!keycloak.value,
|
||||
keycloakAuthenticated: keycloak.value.authenticated
|
||||
})
|
||||
|
||||
await keycloak.value.login({
|
||||
redirectUri: `${baseUrl}/dashboard`
|
||||
})
|
||||
|
||||
console.log('[KEYCLOAK] Login method completed successfully')
|
||||
} catch (error) {
|
||||
console.error('[KEYCLOAK] Login failed:', error)
|
||||
|
||||
if (error instanceof Error) {
|
||||
console.error('[KEYCLOAK] Error details:', {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
name: error.name
|
||||
})
|
||||
} else {
|
||||
console.error('[KEYCLOAK] Unknown error type:', error)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
} else {
|
||||
console.error('[KEYCLOAK] No keycloak instance available for login')
|
||||
throw new Error('Keycloak not available')
|
||||
}
|
||||
}
|
||||
|
||||
const logout = async () => {
|
||||
if (keycloak.value) {
|
||||
try {
|
||||
await keycloak.value.logout({
|
||||
redirectUri: window.location.origin
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Logout failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear local state
|
||||
isAuthenticated.value = false
|
||||
user.value = null
|
||||
token.value = null
|
||||
}
|
||||
|
||||
const getToken = () => {
|
||||
return token.value
|
||||
}
|
||||
|
||||
const hasRole = (role: string) => {
|
||||
return user.value?.roles?.includes(role) || false
|
||||
}
|
||||
|
||||
const hasAnyRole = (roles: string[]) => {
|
||||
return roles.some(role => hasRole(role))
|
||||
}
|
||||
|
||||
return {
|
||||
keycloak: readonly(keycloak),
|
||||
isAuthenticated: readonly(isAuthenticated),
|
||||
user: readonly(user),
|
||||
token: readonly(token),
|
||||
isInitialized: readonly(isInitialized),
|
||||
initKeycloak,
|
||||
login,
|
||||
logout,
|
||||
getToken,
|
||||
hasRole,
|
||||
hasAnyRole,
|
||||
}
|
||||
}
|
||||
@@ -11,29 +11,27 @@ export const useUnifiedAuth = () => {
|
||||
// Get both auth systems
|
||||
const directusAuth = useDirectusAuth();
|
||||
const directusUser = useDirectusUser();
|
||||
const keycloak = useKeycloak();
|
||||
const oidc = useOidcAuth();
|
||||
|
||||
// Create unified user object
|
||||
const user = computed<UnifiedUser | null>(() => {
|
||||
// Check Keycloak user first
|
||||
if (keycloak.user?.value) {
|
||||
const keycloakUser = keycloak.user.value;
|
||||
// Check OIDC (Keycloak) user first
|
||||
if (oidc.loggedIn?.value && oidc.user?.value) {
|
||||
const oidcUser = oidc.user.value as any; // Type cast for flexibility
|
||||
|
||||
// Construct name from available fields
|
||||
let name = keycloakUser.fullName;
|
||||
if (!name && (keycloakUser.firstName || keycloakUser.lastName)) {
|
||||
name = `${keycloakUser.firstName || ''} ${keycloakUser.lastName || ''}`.trim();
|
||||
}
|
||||
if (!name) {
|
||||
name = keycloakUser.username || keycloakUser.email;
|
||||
let name = oidcUser.name || oidcUser.preferred_username || oidcUser.email || 'User';
|
||||
if (!name && (oidcUser.given_name || oidcUser.family_name)) {
|
||||
name = `${oidcUser.given_name || ''} ${oidcUser.family_name || ''}`.trim();
|
||||
}
|
||||
|
||||
return {
|
||||
id: keycloakUser.id,
|
||||
email: keycloakUser.email,
|
||||
id: oidcUser.sub || oidcUser.id || 'unknown',
|
||||
email: oidcUser.email || '',
|
||||
name: name,
|
||||
tier: 'basic', // Could be enhanced with Keycloak attributes
|
||||
authSource: 'keycloak',
|
||||
raw: keycloakUser
|
||||
raw: oidcUser
|
||||
};
|
||||
}
|
||||
|
||||
@@ -55,8 +53,8 @@ export const useUnifiedAuth = () => {
|
||||
// Unified logout function
|
||||
const logout = async () => {
|
||||
if (user.value?.authSource === 'keycloak') {
|
||||
// Keycloak logout
|
||||
await keycloak.logout();
|
||||
// OIDC logout
|
||||
await oidc.logout();
|
||||
} else if (user.value?.authSource === 'directus') {
|
||||
// Directus logout
|
||||
await directusAuth.logout();
|
||||
|
||||
Reference in New Issue
Block a user