Full OIDC logout with id_token_hint for provider session termination
Return the OIDC id_token in the callback response so the frontend can store it and pass it as id_token_hint to the provider's end-session endpoint on logout. This lets Authentik (or any OIDC provider) honor the post_logout_redirect_uri and redirect back to the Cameleer login page instead of showing the provider's own logout page. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,5 +7,7 @@ import jakarta.validation.constraints.NotNull;
|
|||||||
public record AuthTokenResponse(
|
public record AuthTokenResponse(
|
||||||
@NotNull String accessToken,
|
@NotNull String accessToken,
|
||||||
@NotNull String refreshToken,
|
@NotNull String refreshToken,
|
||||||
@NotNull String displayName
|
@NotNull String displayName,
|
||||||
|
@Schema(description = "OIDC id_token for end-session logout (only present after OIDC login)")
|
||||||
|
String idToken
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ public class OidcAuthController {
|
|||||||
|
|
||||||
String displayName = oidcUser.name() != null && !oidcUser.name().isBlank()
|
String displayName = oidcUser.name() != null && !oidcUser.name().isBlank()
|
||||||
? oidcUser.name() : oidcUser.email();
|
? oidcUser.name() : oidcUser.email();
|
||||||
return ResponseEntity.ok(new AuthTokenResponse(accessToken, refreshToken, displayName));
|
return ResponseEntity.ok(new AuthTokenResponse(accessToken, refreshToken, displayName, oidcUser.idToken()));
|
||||||
} catch (ResponseStatusException e) {
|
} catch (ResponseStatusException e) {
|
||||||
throw e;
|
throw e;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ public class OidcTokenExchanger {
|
|||||||
this.configRepository = configRepository;
|
this.configRepository = configRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public record OidcUserInfo(String subject, String email, String name, List<String> roles) {}
|
public record OidcUserInfo(String subject, String email, String name, List<String> roles, String idToken) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exchanges an authorization code for validated user info.
|
* Exchanges an authorization code for validated user info.
|
||||||
@@ -100,7 +100,7 @@ public class OidcTokenExchanger {
|
|||||||
List<String> roles = extractRoles(claims, config.rolesClaim());
|
List<String> roles = extractRoles(claims, config.rolesClaim());
|
||||||
|
|
||||||
log.info("OIDC user authenticated: sub={}, email={}", subject, email);
|
log.info("OIDC user authenticated: sub={}, email={}", subject, email);
|
||||||
return new OidcUserInfo(subject, email != null ? email : "", name != null ? name : "", roles);
|
return new OidcUserInfo(subject, email != null ? email : "", name != null ? name : "", roles, idTokenStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ public class UiAuthController {
|
|||||||
String refreshToken = jwtService.createRefreshToken(subject, "user", roles);
|
String refreshToken = jwtService.createRefreshToken(subject, "user", roles);
|
||||||
|
|
||||||
log.info("UI user logged in: {}", request.username());
|
log.info("UI user logged in: {}", request.username());
|
||||||
return ResponseEntity.ok(new AuthTokenResponse(accessToken, refreshToken, request.username()));
|
return ResponseEntity.ok(new AuthTokenResponse(accessToken, refreshToken, request.username(), null));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/refresh")
|
@PostMapping("/refresh")
|
||||||
@@ -108,7 +108,7 @@ public class UiAuthController {
|
|||||||
String displayName = userRepository.findById(result.subject())
|
String displayName = userRepository.findById(result.subject())
|
||||||
.map(UserInfo::displayName)
|
.map(UserInfo::displayName)
|
||||||
.orElse(result.subject());
|
.orElse(result.subject());
|
||||||
return ResponseEntity.ok(new AuthTokenResponse(accessToken, refreshToken, displayName));
|
return ResponseEntity.ok(new AuthTokenResponse(accessToken, refreshToken, displayName, null));
|
||||||
} catch (ResponseStatusException e) {
|
} catch (ResponseStatusException e) {
|
||||||
throw e;
|
throw e;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@@ -1602,6 +1602,10 @@
|
|||||||
},
|
},
|
||||||
"displayName": {
|
"displayName": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"idToken": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "OIDC id_token for end-session logout (only present after OIDC login)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|||||||
2
ui/src/api/schema.d.ts
vendored
2
ui/src/api/schema.d.ts
vendored
@@ -595,6 +595,8 @@ export interface components {
|
|||||||
accessToken: string;
|
accessToken: string;
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
|
/** @description OIDC id_token for end-session logout (only present after OIDC login) */
|
||||||
|
idToken?: string;
|
||||||
};
|
};
|
||||||
CallbackRequest: {
|
CallbackRequest: {
|
||||||
code?: string;
|
code?: string;
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
|||||||
}
|
}
|
||||||
const { accessToken, refreshToken, displayName } = data;
|
const { accessToken, refreshToken, displayName } = data;
|
||||||
localStorage.removeItem('cameleer-oidc-end-session');
|
localStorage.removeItem('cameleer-oidc-end-session');
|
||||||
|
localStorage.removeItem('cameleer-oidc-id-token');
|
||||||
const name = displayName ?? username;
|
const name = displayName ?? username;
|
||||||
persistTokens(accessToken, refreshToken, name);
|
persistTokens(accessToken, refreshToken, name);
|
||||||
set({
|
set({
|
||||||
@@ -93,9 +94,12 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
|||||||
if (error || !data) {
|
if (error || !data) {
|
||||||
throw new Error('OIDC login failed');
|
throw new Error('OIDC login failed');
|
||||||
}
|
}
|
||||||
const { accessToken, refreshToken, displayName } = data;
|
const { accessToken, refreshToken, displayName, idToken } = data;
|
||||||
const username = displayName ?? 'oidc-user';
|
const username = displayName ?? 'oidc-user';
|
||||||
persistTokens(accessToken, refreshToken, username);
|
persistTokens(accessToken, refreshToken, username);
|
||||||
|
if (idToken) {
|
||||||
|
localStorage.setItem('cameleer-oidc-id-token', idToken);
|
||||||
|
}
|
||||||
set({
|
set({
|
||||||
accessToken,
|
accessToken,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
@@ -137,8 +141,10 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
|||||||
|
|
||||||
logout: () => {
|
logout: () => {
|
||||||
const endSessionEndpoint = localStorage.getItem('cameleer-oidc-end-session');
|
const endSessionEndpoint = localStorage.getItem('cameleer-oidc-end-session');
|
||||||
|
const idToken = localStorage.getItem('cameleer-oidc-id-token');
|
||||||
clearTokens();
|
clearTokens();
|
||||||
localStorage.removeItem('cameleer-oidc-end-session');
|
localStorage.removeItem('cameleer-oidc-end-session');
|
||||||
|
localStorage.removeItem('cameleer-oidc-id-token');
|
||||||
set({
|
set({
|
||||||
accessToken: null,
|
accessToken: null,
|
||||||
refreshToken: null,
|
refreshToken: null,
|
||||||
@@ -147,9 +153,10 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
|||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
if (endSessionEndpoint) {
|
if (endSessionEndpoint && idToken) {
|
||||||
const postLogoutRedirect = `${window.location.origin}/login`;
|
const postLogoutRedirect = `${window.location.origin}/login`;
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
|
id_token_hint: idToken,
|
||||||
post_logout_redirect_uri: postLogoutRedirect,
|
post_logout_redirect_uri: postLogoutRedirect,
|
||||||
});
|
});
|
||||||
window.location.href = `${endSessionEndpoint}?${params}`;
|
window.location.href = `${endSessionEndpoint}?${params}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user