From 11b6642237e407ba16fcf5350f8f96661a835c1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Couralet?= Date: Wed, 8 Dec 2021 08:54:54 +0100 Subject: [PATCH] Decrypt token for logout, this is needed for FC+ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Cédric Couralet Signed-off-by: Cédric Couralet --- .../AgentConnectIdentityProvider.java | 10 +- .../common/AbstractBaseIdentityProvider.java | 101 ++++++++++-------- .../FranceConnectIdentityProvider.java | 7 ++ 3 files changed, 70 insertions(+), 48 deletions(-) diff --git a/src/main/java/fr/insee/keycloak/providers/agentconnect/AgentConnectIdentityProvider.java b/src/main/java/fr/insee/keycloak/providers/agentconnect/AgentConnectIdentityProvider.java index 990a485..70c97da 100644 --- a/src/main/java/fr/insee/keycloak/providers/agentconnect/AgentConnectIdentityProvider.java +++ b/src/main/java/fr/insee/keycloak/providers/agentconnect/AgentConnectIdentityProvider.java @@ -2,13 +2,13 @@ import fr.insee.keycloak.providers.common.AbstractBaseIdentityProvider; import fr.insee.keycloak.providers.common.Utils; +import javax.ws.rs.core.UriBuilder; import org.keycloak.OAuth2Constants; import org.keycloak.broker.provider.AuthenticationRequest; import org.keycloak.models.KeycloakSession; -import javax.ws.rs.core.UriBuilder; - -final class AgentConnectIdentityProvider extends AbstractBaseIdentityProvider { +final class AgentConnectIdentityProvider + extends AbstractBaseIdentityProvider { AgentConnectIdentityProvider(KeycloakSession session, AgentConnectIdentityProviderConfig config) { super(session, config, Utils.getJsonWebKeySetFrom(config.getJwksUrl(), session)); @@ -19,7 +19,9 @@ protected UriBuilder createAuthorizationUrl(AuthenticationRequest request) { var config = getConfig(); - request.getAuthenticationSession().setClientNote(OAuth2Constants.ACR_VALUES, config.getEidasLevel().toString()); + request + .getAuthenticationSession() + .setClientNote(OAuth2Constants.ACR_VALUES, config.getEidasLevel().toString()); var uriBuilder = super.createAuthorizationUrl(request); logger.debugv("AgentConnect Authorization Url: {0}", uriBuilder.build().toString()); diff --git a/src/main/java/fr/insee/keycloak/providers/common/AbstractBaseIdentityProvider.java b/src/main/java/fr/insee/keycloak/providers/common/AbstractBaseIdentityProvider.java index a48dd73..8a6e8c8 100644 --- a/src/main/java/fr/insee/keycloak/providers/common/AbstractBaseIdentityProvider.java +++ b/src/main/java/fr/insee/keycloak/providers/common/AbstractBaseIdentityProvider.java @@ -1,5 +1,17 @@ package fr.insee.keycloak.providers.common; +import static fr.insee.keycloak.providers.common.Utils.transcodeSignatureToDER; +import static org.keycloak.util.JWKSUtils.getKeysForUse; + +import java.nio.charset.StandardCharsets; +import java.security.Signature; +import java.util.Optional; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; import org.keycloak.broker.oidc.OIDCIdentityProvider; import org.keycloak.broker.oidc.OIDCIdentityProviderConfig; import org.keycloak.broker.provider.BrokeredIdentityContext; @@ -24,21 +36,8 @@ import org.keycloak.services.resources.IdentityBrokerService; import org.keycloak.services.resources.RealmsResource; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriBuilder; -import javax.ws.rs.core.UriInfo; -import java.nio.charset.StandardCharsets; -import java.security.Signature; -import java.util.Optional; - -import static fr.insee.keycloak.providers.common.Utils.transcodeSignatureToDER; -import static org.keycloak.util.JWKSUtils.getKeysForUse; - -public abstract class AbstractBaseIdentityProvider extends OIDCIdentityProvider - implements SocialIdentityProvider { +public abstract class AbstractBaseIdentityProvider + extends OIDCIdentityProvider implements SocialIdentityProvider { protected static final String ACR_CLAIM_NAME = "acr"; @@ -60,8 +59,8 @@ public Object callback(RealmModel realm, AuthenticationCallback callback, EventB } @Override - public Response keycloakInitiatedBrowserLogout(KeycloakSession session, UserSessionModel userSession, - UriInfo uriInfo, RealmModel realm) { + public Response keycloakInitiatedBrowserLogout( + KeycloakSession session, UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) { var config = getConfig(); @@ -70,7 +69,8 @@ public Response keycloakInitiatedBrowserLogout(KeycloakSession session, UserSess return null; } - var idToken = userSession.getNote(FEDERATED_ID_TOKEN); + var idToken = getIdTokenForLogout(userSession); + if (idToken != null && config.isBackchannelSupported()) { backchannelLogout(userSession, idToken); return null; @@ -82,17 +82,20 @@ public Response keycloakInitiatedBrowserLogout(KeycloakSession session, UserSess if (idToken != null) { logoutUri.queryParam("id_token_hint", idToken); } - var redirectUri = RealmsResource.brokerUrl(uriInfo) - .path(IdentityBrokerService.class, "getEndpoint") - .path(OIDCEndpoint.class, "logoutResponse") - .build(realm.getName(), config.getAlias()) - .toString(); + var redirectUri = + RealmsResource.brokerUrl(uriInfo) + .path(IdentityBrokerService.class, "getEndpoint") + .path(OIDCEndpoint.class, "logoutResponse") + .build(realm.getName(), config.getAlias()) + .toString(); logoutUri.queryParam("post_logout_redirect_uri", redirectUri); - return Response.status(Response.Status.FOUND) - .location(logoutUri.build()) - .build(); + return Response.status(Response.Status.FOUND).location(logoutUri.build()).build(); + } + + protected String getIdTokenForLogout(UserSessionModel userSession) { + return userSession.getNote(FEDERATED_ID_TOKEN); } @Override @@ -113,13 +116,16 @@ protected boolean verify(JWSInput jws) { } try { - var publicKey = Optional.ofNullable(getKeysForUse(jwks, JWK.Use.SIG).get(jws.getHeader().getKeyId())) - .or(() -> { - // Try reloading jwks url - jwks = Utils.getJsonWebKeySetFrom(config.getJwksUrl(), session); - return Optional.ofNullable(getKeysForUse(jwks, JWK.Use.SIG).get(jws.getHeader().getKeyId())); - }) - .orElse(null); + var publicKey = + Optional.ofNullable(getKeysForUse(jwks, JWK.Use.SIG).get(jws.getHeader().getKeyId())) + .or( + () -> { + // Try reloading jwks url + jwks = Utils.getJsonWebKeySetFrom(config.getJwksUrl(), session); + return Optional.ofNullable( + getKeysForUse(jwks, JWK.Use.SIG).get(jws.getHeader().getKeyId())); + }) + .orElse(null); if (publicKey == null) { logger.error("No keys found for kid: " + jws.getHeader().getKeyId()); @@ -160,7 +166,8 @@ public BrokeredIdentityContext getFederatedIdentity(String response) { throw new IdentityBrokerException("The returned eIDAS level cannot be retrieved"); } - logger.debugv("Expecting eIDAS level: {0}, actual: {1}", expectedEidasLevel, fcReturnedEidasLevel); + logger.debugv( + "Expecting eIDAS level: {0}, actual: {1}", expectedEidasLevel, fcReturnedEidasLevel); if (fcReturnedEidasLevel.compareTo(expectedEidasLevel) < 0) { throw new IdentityBrokerException("The returned eIDAS level is insufficient"); @@ -178,7 +185,8 @@ protected class OIDCEndpoint extends Endpoint { private final T config; - public OIDCEndpoint(AuthenticationCallback callback, RealmModel realm, EventBuilder event, T config) { + public OIDCEndpoint( + AuthenticationCallback callback, RealmModel realm, EventBuilder event, T config) { super(callback, realm, event); this.config = config; } @@ -189,20 +197,23 @@ public Response logoutResponse(@QueryParam("state") String state) { if (state == null && config.isIgnoreAbsentStateParameterLogout()) { logger.warn("using usersession from cookie"); - var authResult = AuthenticationManager.authenticateIdentityCookie(session, realm, - false); + var authResult = AuthenticationManager.authenticateIdentityCookie(session, realm, false); if (authResult == null) { return noValidUserSession(); } var userSession = authResult.getSession(); - return AuthenticationManager.finishBrowserLogout(session, realm, userSession, session.getContext().getUri(), - clientConnection, headers); + return AuthenticationManager.finishBrowserLogout( + session, realm, userSession, session.getContext().getUri(), clientConnection, headers); } else if (state == null) { logger.error("no state parameter returned"); sendUserSessionNotFoundEvent(); - return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR); + return ErrorPage.error( + session, + null, + Response.Status.BAD_REQUEST, + Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR); } var userSession = session.sessions().getUserSession(realm, state); @@ -211,18 +222,20 @@ public Response logoutResponse(@QueryParam("state") String state) { } else if (userSession.getState() != UserSessionModel.State.LOGGING_OUT) { logger.error("usersession in different state"); sendUserSessionNotFoundEvent(); - return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.SESSION_NOT_ACTIVE); + return ErrorPage.error( + session, null, Response.Status.BAD_REQUEST, Messages.SESSION_NOT_ACTIVE); } - return AuthenticationManager.finishBrowserLogout(session, realm, userSession, session.getContext().getUri(), - clientConnection, headers); + return AuthenticationManager.finishBrowserLogout( + session, realm, userSession, session.getContext().getUri(), clientConnection, headers); } private Response noValidUserSession() { logger.error("no valid user session"); sendUserSessionNotFoundEvent(); - return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR); + return ErrorPage.error( + session, null, Response.Status.BAD_REQUEST, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR); } private void sendUserSessionNotFoundEvent() { diff --git a/src/main/java/fr/insee/keycloak/providers/franceconnect/FranceConnectIdentityProvider.java b/src/main/java/fr/insee/keycloak/providers/franceconnect/FranceConnectIdentityProvider.java index 80e3153..3584ae8 100644 --- a/src/main/java/fr/insee/keycloak/providers/franceconnect/FranceConnectIdentityProvider.java +++ b/src/main/java/fr/insee/keycloak/providers/franceconnect/FranceConnectIdentityProvider.java @@ -24,6 +24,7 @@ import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInputException; import org.keycloak.models.KeycloakSession; +import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.IDToken; @@ -67,6 +68,12 @@ protected UriBuilder createAuthorizationUrl(AuthenticationRequest request) { return uriBuilder; } + @Override + public String getIdTokenForLogout(UserSessionModel userSession) { + var idToken = super.getIdTokenForLogout(userSession); + return isJWETokenFormatRequired(getConfig()) ? decryptJWE(idToken) : idToken; + } + @Override public JsonWebToken validateToken(String encodedToken) { var ignoreAudience = false;