feat(auth): enhance logout process to support RP-Initiated Logout for OIDC users
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m35s
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m35s
This commit is contained in:
@@ -35,7 +35,7 @@ export async function authenticateToken(req, res, next) {
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
const user = await db.get('SELECT id, name, display_name, email, role, theme, language, avatar_color, avatar_image, email_verified FROM users WHERE id = ?', [decoded.userId]);
|
||||
const user = await db.get('SELECT id, name, display_name, email, role, theme, language, avatar_color, avatar_image, email_verified, oauth_provider FROM users WHERE id = ?', [decoded.userId]);
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { getDb } from '../config/database.js';
|
||||
import redis from '../config/redis.js';
|
||||
import { authenticateToken, generateToken, getBaseUrl } from '../middleware/auth.js';
|
||||
import { isMailerConfigured, sendVerificationEmail } from '../config/mailer.js';
|
||||
import { getOAuthConfig, discoverOIDC } from '../config/oauth.js';
|
||||
import { log } from '../config/logger.js';
|
||||
|
||||
if (!process.env.JWT_SECRET) {
|
||||
@@ -379,7 +380,31 @@ router.post('/logout', authenticateToken, async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ message: 'Logged out successfully' });
|
||||
// ── RP-Initiated Logout for OIDC/Keycloak users ──────────────────────
|
||||
let keycloakLogoutUrl = null;
|
||||
if (req.user.oauth_provider === 'oidc') {
|
||||
try {
|
||||
const config = await getOAuthConfig();
|
||||
if (config) {
|
||||
const oidc = await discoverOIDC(config.issuer);
|
||||
if (oidc.end_session_endpoint) {
|
||||
const idToken = await redis.get(`oidc:id_token:${req.user.id}`);
|
||||
await redis.del(`oidc:id_token:${req.user.id}`);
|
||||
const baseUrl = getBaseUrl(req);
|
||||
const params = new URLSearchParams({
|
||||
post_logout_redirect_uri: `${baseUrl}/`,
|
||||
client_id: config.clientId,
|
||||
});
|
||||
if (idToken) params.set('id_token_hint', idToken);
|
||||
keycloakLogoutUrl = `${oidc.end_session_endpoint}?${params.toString()}`;
|
||||
}
|
||||
}
|
||||
} catch (oidcErr) {
|
||||
log.auth.warn(`Could not build Keycloak logout URL: ${oidcErr.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ message: 'Logged out successfully', keycloakLogoutUrl });
|
||||
} catch (err) {
|
||||
log.auth.error(`Logout error: ${err.message}`);
|
||||
res.status(500).json({ error: 'Logout failed' });
|
||||
|
||||
@@ -21,6 +21,7 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
import { getDb } from '../config/database.js';
|
||||
import { generateToken, getBaseUrl } from '../middleware/auth.js';
|
||||
import { log } from '../config/logger.js';
|
||||
import redis from '../config/redis.js';
|
||||
import {
|
||||
getOAuthConfig,
|
||||
discoverOIDC,
|
||||
@@ -248,6 +249,15 @@ router.get('/callback', callbackLimiter, async (req, res) => {
|
||||
// Generate JWT
|
||||
const token = generateToken(user.id);
|
||||
|
||||
// Store id_token in Redis for RP-Initiated Logout (Keycloak SLO)
|
||||
if (tokenResponse.id_token) {
|
||||
try {
|
||||
await redis.setex(`oidc:id_token:${user.id}`, 7 * 24 * 3600, tokenResponse.id_token);
|
||||
} catch (redisErr) {
|
||||
log.auth.warn(`Failed to cache OIDC id_token: ${redisErr.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect to frontend callback page with token.
|
||||
// Use a hash fragment so the token is never sent to the server (not logged, not in Referer headers).
|
||||
const returnTo = stateData.return_to || '/dashboard';
|
||||
|
||||
@@ -42,7 +42,13 @@ export function AuthProvider({ children }) {
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
try {
|
||||
await api.post('/auth/logout');
|
||||
const res = await api.post('/auth/logout');
|
||||
localStorage.removeItem('token');
|
||||
setUser(null);
|
||||
if (res.data?.keycloakLogoutUrl) {
|
||||
window.location.href = res.data.keycloakLogoutUrl;
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// ignore — token is removed locally regardless
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user