-
Notifications
You must be signed in to change notification settings - Fork 3
feat(auth): implement back-channel logout and session invalidation logic #79
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request implements OpenID Connect back-channel logout functionality for the dashboard application, allowing Keycloak to notify the application when a user's session should be invalidated. The implementation adds session tracking, logout token handling, and session invalidation logic.
Changes:
- Implements a back-channel logout endpoint that accepts logout tokens from Keycloak
- Adds session invalidation tracking using an in-memory global Set
- Integrates session invalidation checks into the JWT callback flow
- Updates middleware and components to handle invalidated sessions gracefully
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
apps/dashboard/typings/next-auth/next-auth.d.ts |
Adds optional sessionId field to JWT type for session tracking |
apps/dashboard/src/util/invalidatedSessions.ts |
New utility module for tracking and checking invalidated sessions globally |
apps/dashboard/src/util/auth.ts |
Integrates session invalidation checks, adds authorization scope configuration, updates error handling |
apps/dashboard/src/middleware.ts |
Replaces default middleware with custom withAuth callback to block invalidated tokens |
apps/dashboard/src/components/layout/index.tsx |
Adds optional chaining for safer access to session.user properties |
apps/dashboard/src/components/layout/header/HeaderProfile.tsx |
Adds additional null checks for session.user.username |
apps/dashboard/src/components/AuthProvider.tsx |
Updates logout logic to handle invalidated sessions and removes unused pathname check |
apps/dashboard/src/app/(sideNavbar)/auth/signin/page.tsx |
Replaces auto-redirect with manual sign-in button |
apps/dashboard/src/app/(sideNavbar)/api/auth/backchannel-logout/route.ts |
New endpoint to receive and process logout tokens from Keycloak |
| // Decode the JWT to get the session ID (sid) without verification | ||
| const [, payload] = logoutToken.split('.'); | ||
| const decodedPayload = JSON.parse(Buffer.from(payload, 'base64').toString()); |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical security vulnerability: The JWT logout token is decoded without signature verification. This means the endpoint will accept logout tokens from any source, not just Keycloak. An attacker could craft a malicious JWT with any session ID or user ID to invalidate arbitrary sessions.
The JWT should be verified using Keycloak's public key before processing. Consider using a library like 'jose' or 'jsonwebtoken' to verify the token signature against Keycloak's JWKS endpoint.
| export async function POST(request: NextRequest) { | ||
| try { | ||
| const formData = await request.formData(); | ||
| const logoutToken = formData.get('logout_token'); | ||
|
|
||
| if (!logoutToken || typeof logoutToken !== 'string') { | ||
| return NextResponse.json({ error: 'Missing logout_token' }, { status: 400 }); | ||
| } |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical security vulnerability: The backchannel-logout endpoint has no authentication or authorization checks. Any external party can send requests to this endpoint to invalidate arbitrary sessions.
This endpoint should be protected with authentication (e.g., verify the request comes from Keycloak using IP allowlists, shared secrets, or by verifying the JWT signature). According to OpenID Connect Back-Channel Logout specs, the logout token should be validated to ensure it comes from a trusted identity provider.
| const [, payload] = logoutToken.split('.'); | ||
| const decodedPayload = JSON.parse(Buffer.from(payload, 'base64').toString()); |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: The JWT payload decoding doesn't handle base64url padding correctly. JWTs use base64url encoding which may not include padding characters. The current code will throw an error if the payload doesn't have proper padding.
Use base64url decoding with padding handling, or replace '-' with '+', '_' with '/', and add padding if necessary before decoding.
apps/dashboard/src/app/(sideNavbar)/api/auth/backchannel-logout/route.ts
Show resolved
Hide resolved
| export function markSessionAsChecked(sessionId: string | undefined, userId: string | undefined): void { | ||
| // Remove from invalidated sessions after checking to prevent memory buildup | ||
| if (sessionId) { | ||
| globalThis.invalidatedSessions?.delete(sessionId); | ||
| } | ||
| if (userId) { | ||
| globalThis.invalidatedSessions?.delete(userId); | ||
| } |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential memory leak: Sessions invalidated by userId (when sessionId is not present) are never cleaned up. The markSessionAsChecked function is only called when a token is validated, but if a user has multiple sessions or the session is never accessed after invalidation, the userId entry will remain in the Set indefinitely.
Consider implementing a time-based expiration strategy or limiting the size of the invalidated sessions Set to prevent unbounded memory growth.
| }; | ||
| } catch (error) { | ||
| console.error('Failed to refresh access token', error); | ||
| console.log('Failed to refresh access token', error); |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing console.error to console.log reduces error visibility in production logs and monitoring systems. Error logging is important for debugging token refresh failures, especially in production environments where these errors indicate authentication issues.
Consider keeping console.error for actual error conditions to maintain proper log levels and facilitate debugging.
| console.log('Failed to refresh access token', error); | |
| console.error('Failed to refresh access token', error); |
| console.log('Back-channel logout received'); | ||
|
|
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Debug console.log statement should be removed before merging to production. This log provides minimal value and will add noise to production logs.
Consider removing this log statement.
| console.log('Back-channel logout received'); |
| return { | ||
| expires: session.expires, | ||
| error: 'ForceLogout', | ||
| } as Session; |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential issue: The return type is cast to Session but the returned object only contains 'expires' and 'error', missing required Session fields like 'user'. While TypeScript will accept this due to the cast, it may cause runtime issues in components expecting a properly typed Session.
Consider checking if this partial Session object is properly handled by all consuming code, or define a more specific type that extends Session to include the error property without misleading type information.
| issuer: process.env.NEXT_PUBLIC_KEYCLOAK_URL || '', | ||
| authorization: { | ||
| params: { | ||
| scope: 'openid email profile', |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The authorization scope has been explicitly set to 'openid email profile', but for back-channel logout to work properly with session_state tracking, the scope may need to include additional parameters. Verify that Keycloak is configured to include the session_state (sid) in the logout token with these scopes.
According to OpenID Connect Back-Channel Logout specifications, some identity providers require specific configuration or scopes to enable back-channel logout support.
| scope: 'openid email profile', | |
| // Include offline_access to better support session management and back-channel logout. | |
| scope: 'openid email profile offline_access', |
No description provided.