Binding brokering OIDC user sessions with the issuer of the ID Token to avoid looking up sessions by iterating over all brokers in a realm

Closes #32091

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Pedro Igor 2024-09-03 11:41:32 -03:00 committed by Alexander Schwartz
parent 88a5c96fff
commit 079242c398
3 changed files with 29 additions and 21 deletions

View file

@ -32,6 +32,7 @@ public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig {
public static final String USE_JWKS_URL = "useJwksUrl"; public static final String USE_JWKS_URL = "useJwksUrl";
public static final String VALIDATE_SIGNATURE = "validateSignature"; public static final String VALIDATE_SIGNATURE = "validateSignature";
public static final String IS_ACCESS_TOKEN_JWT = "isAccessTokenJWT"; public static final String IS_ACCESS_TOKEN_JWT = "isAccessTokenJWT";
public static final String ISSUER = "issuer";
public OIDCIdentityProviderConfig(IdentityProviderModel identityProviderModel) { public OIDCIdentityProviderConfig(IdentityProviderModel identityProviderModel) {
super(identityProviderModel); super(identityProviderModel);
@ -49,10 +50,10 @@ public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig {
} }
public String getIssuer() { public String getIssuer() {
return getConfig().get("issuer"); return getConfig().get(ISSUER);
} }
public void setIssuer(String issuer) { public void setIssuer(String issuer) {
getConfig().put("issuer", issuer); getConfig().put(ISSUER, issuer);
} }
public String getLogoutUrl() { public String getLogoutUrl() {
return getConfig().get("logoutUrl"); return getConfig().get("logoutUrl");

View file

@ -20,6 +20,7 @@ package org.keycloak.protocol.oidc;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
import org.keycloak.common.Profile.Feature; import org.keycloak.common.Profile.Feature;
import org.keycloak.http.HttpRequest; import org.keycloak.http.HttpRequest;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
@ -1440,21 +1441,21 @@ public class TokenManager {
} }
} }
public LogoutTokenValidationCode verifyLogoutToken(KeycloakSession session, RealmModel realm, String encodedLogoutToken) { public LogoutTokenValidationCode verifyLogoutToken(KeycloakSession session, String encodedLogoutToken) {
Optional<LogoutToken> logoutTokenOptional = toLogoutToken(encodedLogoutToken); Optional<LogoutToken> logoutTokenOptional = toLogoutToken(encodedLogoutToken);
if (!logoutTokenOptional.isPresent()) { if (logoutTokenOptional.isEmpty()) {
return LogoutTokenValidationCode.DECODE_TOKEN_FAILED; return LogoutTokenValidationCode.DECODE_TOKEN_FAILED;
} }
LogoutToken logoutToken = logoutTokenOptional.get(); LogoutToken logoutToken = logoutTokenOptional.get();
List<OIDCIdentityProvider> identityProviders = getOIDCIdentityProviders(realm, session).toList(); List<OIDCIdentityProvider> identityProviders = getOIDCIdentityProviders(logoutToken, session).toList();
if (identityProviders.isEmpty()) { if (identityProviders.isEmpty()) {
return LogoutTokenValidationCode.COULD_NOT_FIND_IDP; return LogoutTokenValidationCode.COULD_NOT_FIND_IDP;
} }
Stream<OIDCIdentityProvider> validOidcIdentityProviders = Stream<OIDCIdentityProvider> validOidcIdentityProviders =
validateLogoutTokenAgainstIdpProvider(identityProviders.stream(), encodedLogoutToken, logoutToken); validateLogoutTokenAgainstIdpProvider(identityProviders.stream(), encodedLogoutToken);
if (validOidcIdentityProviders.count() == 0) { if (validOidcIdentityProviders.findAny().isEmpty()) {
return LogoutTokenValidationCode.TOKEN_VERIFICATION_WITH_IDP_FAILED; return LogoutTokenValidationCode.TOKEN_VERIFICATION_WITH_IDP_FAILED;
} }
@ -1491,15 +1492,13 @@ public class TokenManager {
} }
public Stream<OIDCIdentityProvider> getValidOIDCIdentityProvidersForBackchannelLogout(RealmModel realm, KeycloakSession session, String encodedLogoutToken, LogoutToken logoutToken) { public Stream<OIDCIdentityProvider> getValidOIDCIdentityProvidersForBackchannelLogout(KeycloakSession session, String encodedLogoutToken, LogoutToken logoutToken) {
return validateLogoutTokenAgainstIdpProvider(getOIDCIdentityProviders(realm, session), encodedLogoutToken, logoutToken); return validateLogoutTokenAgainstIdpProvider(getOIDCIdentityProviders(logoutToken, session), encodedLogoutToken);
} }
public Stream<OIDCIdentityProvider> validateLogoutTokenAgainstIdpProvider(Stream<OIDCIdentityProvider> oidcIdps, String encodedLogoutToken, LogoutToken logoutToken) { public Stream<OIDCIdentityProvider> validateLogoutTokenAgainstIdpProvider(Stream<OIDCIdentityProvider> oidcIdps, String encodedLogoutToken) {
return oidcIdps return oidcIdps
.filter(oidcIdp -> oidcIdp.getConfig().getIssuer() != null)
.filter(oidcIdp -> oidcIdp.isIssuer(logoutToken.getIssuer(), null))
.filter(oidcIdp -> { .filter(oidcIdp -> {
try { try {
oidcIdp.validateToken(encodedLogoutToken); oidcIdp.validateToken(encodedLogoutToken);
@ -1511,13 +1510,22 @@ public class TokenManager {
}); });
} }
private Stream<OIDCIdentityProvider> getOIDCIdentityProviders(RealmModel realm, KeycloakSession session) { private Stream<OIDCIdentityProvider> getOIDCIdentityProviders(LogoutToken logoutToken, KeycloakSession session) {
try { try {
return session.identityProviders().getAllStream() return session.identityProviders()
.map(idpModel -> .getAllStream(Map.of(
IdentityBrokerService.getIdentityProviderFactory(session, idpModel).create(session, idpModel)) OIDCIdentityProviderConfig.ISSUER, logoutToken.getIssuer()
.filter(OIDCIdentityProvider.class::isInstance) ), -1, -1)
.map(OIDCIdentityProvider.class::cast); .map(model -> {
var idp = IdentityBrokerService.getIdentityProviderFactory(session, model).create(session, model);
if (idp instanceof OIDCIdentityProvider oidcIdp) {
return oidcIdp;
}
return null;
})
.filter(Objects::nonNull);
} catch (IdentityBrokerException e) { } catch (IdentityBrokerException e) {
logger.warnf("LogoutToken verification with identity provider failed", e.getMessage()); logger.warnf("LogoutToken verification with identity provider failed", e.getMessage());
} }

View file

@ -554,7 +554,7 @@ public class LogoutEndpoint {
Response.Status.BAD_REQUEST); Response.Status.BAD_REQUEST);
} }
LogoutTokenValidationCode validationCode = tokenManager.verifyLogoutToken(session, realm, encodedLogoutToken); LogoutTokenValidationCode validationCode = tokenManager.verifyLogoutToken(session, encodedLogoutToken);
if (!validationCode.equals(LogoutTokenValidationCode.VALIDATION_SUCCESS)) { if (!validationCode.equals(LogoutTokenValidationCode.VALIDATION_SUCCESS)) {
String errorMessage = validationCode.getErrorMessage(); String errorMessage = validationCode.getErrorMessage();
event.detail(Details.REASON, errorMessage); event.detail(Details.REASON, errorMessage);
@ -565,8 +565,7 @@ public class LogoutEndpoint {
LogoutToken logoutToken = tokenManager.toLogoutToken(encodedLogoutToken).get(); LogoutToken logoutToken = tokenManager.toLogoutToken(encodedLogoutToken).get();
Stream<String> identityProviderAliases = tokenManager.getValidOIDCIdentityProvidersForBackchannelLogout(realm, Stream<String> identityProviderAliases = tokenManager.getValidOIDCIdentityProvidersForBackchannelLogout(session, encodedLogoutToken, logoutToken)
session, encodedLogoutToken, logoutToken)
.map(idp -> idp.getConfig().getAlias()); .map(idp -> idp.getConfig().getAlias());
boolean logoutOfflineSessions = Boolean.parseBoolean(logoutToken.getEvents() boolean logoutOfflineSessions = Boolean.parseBoolean(logoutToken.getEvents()