OIDC Session Management
ð ãã®ããã¥ã¡ã³ãã®äœçœ®ã¥ãâ
察象èªè : OIDC Session Managementã®å®è£ 詳现ãçè§£ãããéçºè
ãã®ããã¥ã¡ã³ãã§åŠã¹ãããš:
- OIDC Session Managementã®ä»çµã¿
- OPSession / ClientSession ã®èšèšãã¿ãŒã³
- IDP_IDENTITY / IDP_SESSION Cookieã®åœ¹å²
- SSOïŒã·ã³ã°ã«ãµã€ã³ãªã³ïŒã®å®è£
- RP-Initiated Logout / Back-Channel Logout ã®å®è£
åæç¥è:
- OAuth 2.0 / OpenID Connect ã®åºç€ç¥è
- èªå¯ã³ãŒããããŒã®çè§£
ðïž ã»ãã·ã§ã³ç®¡çã¢ãŒããã¯ãã£â
idp-serverã®ã»ãã·ã§ã³ç®¡çã¯ãKeycloakã®ã¢ãŒãã ã¯ãã£ãåèã«èšèšãããŠããŸãã
ã»ãã·ã§ã³ã®çš®é¡â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â Browser Session â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ€
â â
â âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â
â â OPSession â â
â â - ãã©ãŠã¶ãšOPã®éã®ã»ãã·ã§ã³ïŒSSOçšïŒ â â
â â - sub, authTime, acr, amr ãä¿æ â â
â â - è€æ°ã®ClientSessionãæã€ â â
â âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â
â â â â â
â ⌠⌠⌠â
â âââââââââââââââ âââââââââââââââ âââââââââââââââ â
â âClientSessionâ âClientSessionâ âClientSessionâ â
â â Client A â â Client B â â Client C â â
â â sid: xxx â â sid: yyy â â sid: zzz â â
â âââââââââââââââ ââââââââââââ âââ âââââââââââââââ â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ã»ãã·ã§ã³ãšCookieã®é¢ä¿â
| Cookieå | å 容 | HttpOnly | ç®ç |
|---|---|---|---|
IDP_IDENTITY | opSessionId | Yes | SSOèå¥çšïŒãµãŒããŒåŽã§äœ¿çšïŒ |
IDP_SESSION | SHA256(opSessionId) | No | Session Management iframeçš |
åèå®è£ : SessionCookieService.java
Cookieã®ãã¹ã¹ã³ãŒãïŒããã³ãåé¢ïŒâ
Keycloakãšåæ§ã«ãCookieã®ãã¹ã§ããã³ãïŒRealmïŒãåé¢ã§ããŸãã
Browser Cookie Storage:
âââ /tenant-a/
â âââ IDP_IDENTITY = "session-id-for-tenant-a"
â âââ IDP_SESSION = "hash-a..."
â
âââ /tenant-b/
âââ IDP_IDENTITY = "session-id-for-tenant-b"
âââ IDP_SESSION = "hash-b..."
ããã«ãããåäžãã©ãŠã¶ã§è€æ°ããã³ãã«ç¬ç«ããŠãã°ã€ã³ã§ããŸãã
ð ã³ã¢ã¯ã©ã¹â
OPSessionâ
ãã©ãŠã¶ãšOPéã®ã»ãã·ã§ã³ã衚ãã¯ã©ã¹ã§ãããŠãŒã¶ãŒããã°ã€ã³ãããšäœæããããã°ã¢ãŠããŸã§ç¶æãããŸãã
public class OPSession {
private final OPSessionIdentifier id; // ã»ãã·ã§ã³IDïŒUUIDïŒ
private final String sub; // ãŠãŒã¶ãŒèå¥å
private final Instant authTime; // èªèšŒæå»
private final String acr; // èªèšŒã³ã³ããã¹ãã¯ã©ã¹
private final Set<String> amr; // èªèšŒæ¹åŒ
private final Instant createdAt;
private final Instant expiresAt;
private final String ipAddress; // èªèšŒæã®IPã¢ãã¬ã¹
private final String userAgent; // èªèšŒæã®User-Agent
public boolean isExpired() {
return Instant.now().isAfter(expiresAt);
}
}
åèå®è£ : OPSession.java
ClientSessionâ
OPSessionãšç¹å®ã®RPã®éã®ã»ãã·ã§ã³ã衚ãã¯ã©ã¹ã§ããèªå¯ãå®äºãããšäœæãããŸãã
public class ClientSession {
private final ClientSessionIdentifier sid; // ID Token ã® sid ã¯ã¬ãŒã ã«å«ãŸãã
private final OPSessionIdentifier opSessionId;
private final String clientId;
private final Set<String> scopes;
private final String nonce;
private final Instant createdAt;
}
åèå®è£ : ClientSession.java
OIDCSessionHandlerâ
ã»ãã·ã§ã³ç®¡çæäœã調æŽããã¯ã©ã¹ã§ãã
public class OIDCSessionHandler {
// èªèšŒæåæã«OPSessionãäœæïŒRequestAttributesããIP/UAãæœåºïŒ
public OPSession onAuthenticationSuccess(
Tenant tenant, User user, Authentication authentication,
Map<String, Map<String, Object>> interactionResults,
OPSession existingSession, RequestAttributes requestAttributes);
// èªå¯æã«ClientSessionãäœæïŒsidãè¿ãïŒ
public ClientSessionIdentifier onAuthorize(
Tenant tenant, OPSession opSession, String clientId,
Set<String> scopes, String nonce);
// ã»ãã·ã§ã³Cookieãèšå®
public void registerSessionCookies(
Tenant tenant, OPSession opSession, SessionCookieDelegate delegate);
// CookieããOPSessionãååŸ
public Optional<OPSession> getOPSessionFromCookie(
Tenant tenant, SessionCookieDelegate delegate);
// ã»ãã·ã§ã³ããã·ã¥ãèšç®ïŒIDP_SESSION cookieçšïŒ
public String computeSessionHash(String opSessionId);
// ã»ãã·ã§ã³æå¹æ§ãæ€èšŒ
public boolean isSessionValid(OPSession opSession, Long maxAge);
// OPSessionã®çµäº
public ClientSessions terminateOPSession(
Tenant tenant, OPSessionIdentifier opSessionId, TerminationReason reason);
}
åèå®è£ : OIDCSessionHandler.java
ð ã»ãã·ã§ã³ã®ã©ã€ããµã€ã¯ã«â
1. ã»ãã·ã§ã³äœæïŒèªèšŒæåæïŒâ
ââââââââââââ ââââââââââââââââ âââââââââââââââââââââââ
â User ââââââ¶â èªèšŒæå ââââââ¶â OPSessionäœæ â
âââââââ âââââ ââââââââââââââââ âââââââââââââââââââââââ
â
âŒ
âââââââââââââââââââââââ
â Cookieèšå® â
â - IDP_IDENTITY â
â - IDP_SESSION â
âââââââââââââââââââââââ
å®è£
ç®æ: OAuthFlowEntryService.authenticate()
if (updatedTransaction.isSuccess()) {
Authentication authentication = updatedTransaction.authentication();
OPSession opSession = oidcSessionHandler.onAuthenticationSuccess(
tenant, updatedTransaction.user(), authentication,
updatedTransaction.interactionResults().toStorageMap(),
existingSession, requestAttributes);
// Cookieèšå®ïŒOIDCSessionHandlerã«å§è²ïŒ
oidcSessionHandler.registerSessionCookies(tenant, opSession, sessionCookieDelegate);
}
2. ClientSessionäœæïŒèªå¯æïŒâ
ââââââââââââ ââââââââââââââââ âââââââââââââââââââââââ
â èªå¯æ¿èª ââââââ¶â Cookieèªå ââââââ¶â OPSessionååŸ â
ââââââââââââ ââââââââââââââââ âââââââââââââââââââââââ
â
âŒ
âââââââââââââââââââââââ
â ClientSessionäœæ â
â â sidçæ â
âââââââââââââââââââââââ
â
âŒ
âââââââââââââââââââââââ
â ID Token ã« sidå«ã â
âââââââââââââââââââââââ
å®è£
ç®æ: OAuthFlowEntryService.authorize()
oidcSessionHandler
.getOPSessionFromCookie(tenant, sessionCookieDelegate)
.ifPresent(opSession -> {
ClientSessionIdentifier sid = oidcSessionHandler
.onAuthorize(tenant, opSession, clientId, scopes, nonce);
oAuthAuthorizeRequest.setCustomProperties(Map.of("sid", sid.value()));
});
3. SSOïŒã»ãã·ã§ã³åå©çšïŒâ
æ¢åã®ã»ãã·ã§ã³ã䜿çšããŠãåèªèšŒãªãã§èªå¯ãè¡ããŸãã
ââââââââââââ ââââââââââââââââ âââââââââââââââââââââââ
â èªå¯èŠæ± ââââââ¶â Cookieèªå ââââââ¶â OPSessionååŸ â
ââââââââââââ ââââââââââââââââ âââââââââââââââââââââââ
â
âŒ
âââââââââââââââââââââââ
â ã»ãã·ã§ã³æ€èšŒ â
â - æå¹æé â
â - max_age â
âââââââââââââââââââââââ
â
âŒ
âââââââââââââââââââââââ
â èªèšŒã¹ããã â
â â çŽæ¥èªå¯ â
âââââââââââââââââââââââ
å®è£
ç®æ: OAuthFlowEntryService.authorizeWithSession()
// OPSessionãCookieããååŸïŒOIDCSessionHandlerã«å§è²ïŒ
Optional<OPSession> opSessionOpt = oidcSessionHandler
.getOPSessionFromCookie(tenant, sessionCookieDelegate);
if (opSessionOpt.isEmpty()) {
return new OAuthAuthorizeResponse(
OAuthAuthorizeStatus.BAD_REQUEST, "invalid_request", "session not found");
}
OPSession opSession = opSessionOpt.get();
// max_ageã«ããæ€èšŒ
Long maxAge = authorizationRequest.maxAge().exists()
? authorizationRequest.maxAge().toLongValue()
: null;
if (!oidcSessionHandler.isSessionValid(opSession, maxAge)) {
return new OAuthAuthorizeResponse(
OAuthAuthorizeStatus.BAD_REQUEST, "invalid_request", "session expired");
}
// ã»ãã·ã§ã³ããAuthenticationã埩å
LocalDateTime authTime = LocalDateTime.ofInstant(opSession.authTime(), ZoneOffset.UTC);
Authentication authentication = new Authentication()
.setTime(authTime)
.addAcr(opSession.acr())
.addMethods(opSession.amr());
ðª ãã°ã¢ãŠãâ
RP-Initiated Logoutâ
RPãããã°ã¢ãŠããéå§ãããããŒã§ãã
ââââââââââââ ââââââââââââââââ âââââââââââââââââââââââ
â RP ââââââ¶â /logout ââââââ¶â id_token_hintè§£æ â
ââââââââââââ ââââââââââââââââ âââââââââââââââââââââââ
â
âŒ
âââââââââââââââââââââââ
â ClientSession â sid â
â sid â OPSession â
âââââââââââââââââââââââ
â
âŒ
âââââââââââââââââââââââ
â å
šClientSessionã« â
â ãã°ã¢ãŠãéç¥ â
âââââââââââââââââââââââ
â
âŒ
âââââââââââââââââââââââ
â Cookieåé€ â
â ã»ãã·ã§ã³ç¡å¹å â
âââââââââââââââââââââââ
å®è£
ç®æ: OAuthFlowEntryService.logout()
public OAuthLogoutResponse logout(...) {
OAuthLogoutResponse response = oAuthProtocol.logout(oAuthLogoutRequest);
if (response.isOk() && response.hasContext()) {
// ã»ãã·ã§ã³ãã°ã¢ãŠãå®è¡
response = executeSessionLogout(tenant, response);
// Cookieåé€
if (sessionCookieDelegate != null) {
sessionCookieDelegate.clearSessionCookies();
}
}
return response;
}
Back-Channel Logoutâ
OPããRPãžããã¯ãã£ãã«ã§ãã°ã¢ãŠããéç¥ããŸãã
ââââââââââââ ââââââââââââââââââââ âââââââââââââââââââââââ
â Logout ââââââ¶â LogoutOrchestratorââââââ¶â åClientSessionã® â
â éå§ â â â â RPãååŸ â
âââââââ âââââ ââââââââââââââââââââ âââââââââââââââââââââââ
â
ââââââââââââââââââââââââââââŒâââââââââââââââââââââââââââ
⌠⌠âŒ
âââââââââââââââ âââââââââââââââ âââââââââââââââ
â Client A â â Client B â â Client C â
â POST logout â â POST logout â â POST logout â
â token â â token â â token â
âââââââââââââââ âââââââââââââââ âââââââââââââââ
BackChannelLogoutService ã€ã³ã¿ãŒãã§ãŒã¹â
public interface BackChannelLogoutService {
// Logout Tokenããšã³ã³ãŒãïŒåå®å
š: JWKSãStringã§åãåãïŒ
String encodeLogoutToken(
LogoutToken token,
String signingAlgorithm,
String signingKeyJwks); // Object â String ã«å€æŽ
// Logout Tokenãæ€èšŒ
LogoutTokenValidationResult validateLogoutToken(
String token,
String expectedIssuer,
String expectedAudience,
String publicKeyJwks); // Object â String ã«å€æŽ
// RPãžãã°ã¢ãŠãéç¥ãéä¿¡
BackChannelLogoutResult sendLogoutToken(String logoutUri, String logoutToken);
}
åå®å
šæ§ã®æ¹å: 以åã¯Objectåã ã£ãJWKSãã©ã¡ãŒã¿ãStringåã«å€æŽããåå®å
šæ§ãåäžãããŸããã
HttpClientäŸåæ§æ³šå ¥â
DefaultBackChannelLogoutServiceã¯HttpClientãæç€ºçã«åãåããŸãïŒDIãã¬ã³ããªãŒïŒïŒ
public class DefaultBackChannelLogoutService implements BackChannelLogoutService {
private final HttpClient httpClient;
public DefaultBackChannelLogoutService(HttpClient httpClient) {
this.httpClient = httpClient;
}
// ããã©ã«ãHttpClientãäœæãããã¡ã¯ããªã¡ãœãã
public static HttpClient createDefaultHttpClient() {
return HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.followRedirects(HttpClient.Redirect.NEVER)
.build();
}
}
åèå®è£ :
ðª Cookie管çâ
SessionCookieDelegate ã€ã³ã¿ãŒãã§ãŒã¹â
Cookieã®èªã¿æžããæœè±¡åããã€ã³ã¿ãŒãã§ãŒã¹ã§ãã
public interface SessionCookieDelegate {
// ã»ãã·ã§ã³Cookieãèšå®
void setSessionCookies(String identityToken, String sessionHash, long maxAgeSeconds);
// IDP_IDENTITY CookieãååŸ
Optional<String> getIdentityToken();
// IDP_SESSION CookieãååŸ
Optional<String> getSessionHash();
// ã»ãã·ã§ã³Cookieãåé€
void clearSessionCookies();
}
åèå®è£ : SessionCookieDelegate.java
Spring Bootå®è£ â
@Service
public class SessionCookieService implements SessionCookieDelegate {
public static final String IDENTITY_COOKIE_NAME = "IDP_IDENTITY";
public static final String SESSION_COOKIE_NAME = "IDP_SESSION";
@Override
public void setSessionCookies(String identityToken, String sessionHash, long maxAgeSeconds) {
// IDP_IDENTITY cookie (HttpOnly)
Cookie identityCookie = new Cookie(IDENTITY_COOKIE_NAME, identityToken);
identityCookie.setHttpOnly(true);
identityCookie.setSecure(true);
identityCookie.setPath("/");
// IDP_SESSION cookie (NOT HttpOnly - for session management iframe)
Cookie sessionCookie = new Cookie(SESSION_COOKIE_NAME, sessionHash);
sessionCookie.setHttpOnly(false); // JavaScript ããã¢ã¯ã»ã¹å¯èœ
sessionCookie.setSecure(true);
sessionCookie.setPath("/");
// SameSite=Lax ãèšå®
addCookieWithSameSite(identityCookie, "Lax");
addCookieWithSameSite(sessionCookie, "Lax");
}
}
åèå®è£ : SessionCookieService.java
ã»ãã·ã§ã³ããã·ã¥èšç®â
IDP_SESSION Cookieã«ã¯ãã»ãã·ã§ã³IDã®SHA-256ããã·ã¥ãæ ŒçŽããŸãã
public class SessionHashCalculator {
public static String sha256UrlEncodedHash(String input) {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
return Base64.getUrlEncoder().withoutPadding().encodeToString(hash);
}
public static boolean verifySessionHash(String opSessionId, String providedHash) {
if (opSessionId == null || providedHash == null) {
return false;
}
String expectedHash = sha256UrlEncodedHash(opSessionId);
return expectedHash.equals(providedHash);
}
}
åèå®è£ : SessionHashCalculator.java
ðïž ã»ãã·ã§ã³ã¹ãã¬ãŒãžâ
OPSessionRepositoryâ
ãã¹ãŠã®ã¡ãœããã§Tenantã第äžåŒæ°ãšããŠåãåããŸãïŒãã«ãããã³ã察å¿ïŒã
public interface OPSessionRepository {
void save(Tenant tenant, OPSession session);
Optional<OPSession> findById(Tenant tenant, OPSessionIdentifier id);
void updateLastAccessedAt(Tenant tenant, OPSession session);
void delete(Tenant tenant, OPSessionIdentifier id);
}