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

View file

@ -20,6 +20,7 @@ package org.keycloak.protocol.oidc;
import java.util.Collections;
import java.util.HashMap;
import org.jboss.logging.Logger;
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
import org.keycloak.common.Profile.Feature;
import org.keycloak.http.HttpRequest;
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);
if (!logoutTokenOptional.isPresent()) {
if (logoutTokenOptional.isEmpty()) {
return LogoutTokenValidationCode.DECODE_TOKEN_FAILED;
}
LogoutToken logoutToken = logoutTokenOptional.get();
List<OIDCIdentityProvider> identityProviders = getOIDCIdentityProviders(realm, session).toList();
List<OIDCIdentityProvider> identityProviders = getOIDCIdentityProviders(logoutToken, session).toList();
if (identityProviders.isEmpty()) {
return LogoutTokenValidationCode.COULD_NOT_FIND_IDP;
}
Stream<OIDCIdentityProvider> validOidcIdentityProviders =
validateLogoutTokenAgainstIdpProvider(identityProviders.stream(), encodedLogoutToken, logoutToken);
if (validOidcIdentityProviders.count() == 0) {
validateLogoutTokenAgainstIdpProvider(identityProviders.stream(), encodedLogoutToken);
if (validOidcIdentityProviders.findAny().isEmpty()) {
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) {
return validateLogoutTokenAgainstIdpProvider(getOIDCIdentityProviders(realm, session), encodedLogoutToken, logoutToken);
public Stream<OIDCIdentityProvider> getValidOIDCIdentityProvidersForBackchannelLogout(KeycloakSession session, String encodedLogoutToken, LogoutToken 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
.filter(oidcIdp -> oidcIdp.getConfig().getIssuer() != null)
.filter(oidcIdp -> oidcIdp.isIssuer(logoutToken.getIssuer(), null))
.filter(oidcIdp -> {
try {
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 {
return session.identityProviders().getAllStream()
.map(idpModel ->
IdentityBrokerService.getIdentityProviderFactory(session, idpModel).create(session, idpModel))
.filter(OIDCIdentityProvider.class::isInstance)
.map(OIDCIdentityProvider.class::cast);
return session.identityProviders()
.getAllStream(Map.of(
OIDCIdentityProviderConfig.ISSUER, logoutToken.getIssuer()
), -1, -1)
.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) {
logger.warnf("LogoutToken verification with identity provider failed", e.getMessage());
}

View file

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