-
Notifications
You must be signed in to change notification settings - Fork 0
feat(auth): handle oidc provider signout #132
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 PR implements OIDC RP-Initiated Logout to properly sign users out from both the local application session and the OIDC provider (Okta) SSO session. Previously, only local sign-out was performed, leaving the SSO session active.
Key Changes:
- Added OIDC discovery caching for the
end_session_endpointand ID token storage in the encrypted cookie for logout - Implemented server action
getOidcSignOutUrl()to construct the OIDC provider logout URL with proper parameters - Modified client-side
signOut()function to redirect users to the OIDC provider's logout endpoint before completing local cleanup
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
src/lib/auth/types.ts |
Extended OidcTokenData to include idToken and updated OidcDiscovery interface with logout endpoint |
src/lib/auth/constants.ts |
Made OIDC_ISSUER_URL private and added OIDC_DISCOVERY_URL constant; renamed cookie constant |
src/lib/auth/utils.ts |
Added getOidcIdToken() function, updated saveAccountToken() to store ID token, fixed type guard for accessTokenExpiresAt |
src/lib/auth/auth.ts |
Enhanced getOidcDiscovery() to cache logout endpoint, updated refreshAccessToken() signature to handle ID token, enabled debug mode |
src/lib/auth/actions.ts |
New file with server actions for clearOidcTokenAction() and getOidcSignOutUrl() |
src/lib/auth/auth-client.ts |
Refactored signOut() to implement OIDC logout flow with redirect to provider |
src/app/api/auth/refresh-token/route.ts |
Added session validation before token refresh and user ID verification |
src/lib/auth/__tests__/*.test.ts |
Updated test data to match new OidcTokenData structure with additional fields |
src/lib/auth/__tests__/auth-client.test.ts |
New test suite for client-side sign-out flow |
src/app/catalog/page.tsx |
Removed redundant auth check (already protected by getAuthenticatedClient()) |
src/app/signin/signin-button.tsx |
Added cursor-pointer class to button |
biome.json |
Updated schema version to 2.3.8 |
|
@claude could you review the PR? |
|
Claude finished @peppescg's task —— View job Code Review Complete ✅I've completed a thorough review of this PR implementing OIDC RP-Initiated Logout. Overall, this is a solid implementation with good architectural decisions. Below are my findings: 🟢 Strengths
🟡 Critical Issue: Sign-Out Flow LogicLocation: There's a critical logic error in the sign-out sequence. The current code: // 1. Get logout URL FIRST (while session still exists)
const redirectUrl = await getOidcSignOutUrl();
// 2. Clear OIDC token cookie AFTER BA session is gone
await clearOidcTokenAction();
// 3. Redirect to OIDC provider logout
window.location.replace(redirectUrl);
// 4. Sign out from Better Auth (invalidates session)
await authClient.signOut();The Problem: Line 29 calls Why the test passes: The test mocks Author's Response Analysis: You stated "that's not true the
While
Recommended Fix: export const signOut = async () => {
try {
// 1. Get logout URL FIRST (while session still exists)
const redirectUrl = await getOidcSignOutUrl();
// 2. Sign out from Better Auth (invalidates session)
await authClient.signOut();
// 3. Clear OIDC token cookie AFTER BA session is gone
await clearOidcTokenAction();
// 4. Finally redirect to OIDC provider logout
window.location.replace(redirectUrl);
} catch (error) {
console.error("[Auth] Sign out error:", error);
toast.error("Sign out failed", {
description:
error instanceof Error ? error.message : "An unexpected error occurred",
});
// Fallback redirect on error
window.location.replace("/signin");
}
};This ensures all cleanup happens before navigation, making the behavior predictable and testable. 🟡 Medium Priority Issues1. Debug Mode in ProductionLocation: debug: !IS_PRODUCTION,Good! This correctly disables debug mode in production. However, there's still a debug log statement in: Location: console.log("[Save Token] Token cookie saved successfully");Recommendation: Remove this or make it conditional: if (!IS_PRODUCTION) {
console.log("[Save Token] Token cookie saved successfully");
}2. Incomplete Cache ValidationLocation: if (cachedTokenEndpoint) {
return {
tokenEndpoint: cachedTokenEndpoint,
endSessionEndpoint: cachedEndSessionEndpoint,
};
}Issue: The function caches both Recommended Fix: if (cachedTokenEndpoint && cachedEndSessionEndpoint) {
return {
tokenEndpoint: cachedTokenEndpoint,
endSessionEndpoint: cachedEndSessionEndpoint,
};
}3. Type Definition ComplexityLocation: The
Recommendation: Consider defining export interface OidcTokenData {
userId: string;
accessToken: string;
accessTokenExpiresAt: number;
refreshToken?: string;
refreshTokenExpiresAt?: number;
idToken?: string;
// Include only fields you actually store
}This makes the contract clear and avoids implicit dependencies. 🟢 Minor/Nitpick Issues1. Redundant Property AssignmentsLocation: const tokenData: OidcTokenData = {
...account,
accessToken: account.accessToken, // Already spread above
refreshToken: account.refreshToken || undefined, // Already spread above
accessTokenExpiresAt,
refreshTokenExpiresAt,
userId: account.userId, // Already spread above
};The spread operator already includes these properties. Only assign if you're transforming them: const tokenData: OidcTokenData = {
...account,
accessTokenExpiresAt,
refreshTokenExpiresAt,
refreshToken: account.refreshToken || undefined,
};2. OIDC_ISSUER_URL VisibilityLocation: The constant was changed from 🟢 Positive Changes Worth Highlighting
📝 SummaryMust Fix Before Merge:
Should Fix:
Nice to Have:
Question for Author: Job Run: View logs |
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.
Large PR Detected
This PR exceeds 1000 lines of changes and requires justification before it can be reviewed.
How to unblock this PR:
Add a section to your PR description with the following format:
## Large PR Justification
[Explain why this PR must be large, such as:]
- Generated code that cannot be split
- Large refactoring that must be atomic
- Multiple related changes that would break if separated
- Migration or data transformationAlternative:
Consider splitting this PR into smaller, focused changes (< 1000 lines each) for easier review and reduced risk.
See our Contributing Guidelines for more details on the pull request process.
This review will be automatically dismissed once you add the justification section.
Kapture.2025-12-03.at.10.33.10.mp4
OIDC RP-Initiated Logout
Summary
Implements complete sign-out from the OIDC provider (Okta) in addition to local session cleanup. Previously, users were only signed out locally, leaving their SSO session active.
What Changed
end_session_endpointfrom the OIDC providerid_tokenis now stored in the encrypted OIDC cookie for use during logoutid_token_hintandpost_logout_redirect_uri/signinResult
Users are now fully signed out from Okta SSO. On next sign-in, they will be prompted for credentials again.
Tests
Added unit tests to verify the sign-out flow executes in the correct order and handles errors gracefully.
Large PR Justification