Skip to content

Commit

Permalink
Decrypt token for logout, this is needed for FC+
Browse files Browse the repository at this point in the history
Signed-off-by: Cédric Couralet <[email protected]>
Signed-off-by: Cédric Couralet <[email protected]>
  • Loading branch information
micedre committed Jan 4, 2022
1 parent eedfbed commit 11b6642
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<AgentConnectIdentityProviderConfig> {
final class AgentConnectIdentityProvider
extends AbstractBaseIdentityProvider<AgentConnectIdentityProviderConfig> {

AgentConnectIdentityProvider(KeycloakSession session, AgentConnectIdentityProviderConfig config) {
super(session, config, Utils.getJsonWebKeySetFrom(config.getJwksUrl(), session));
Expand All @@ -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());
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<T extends AbstractBaseProviderConfig> extends OIDCIdentityProvider
implements SocialIdentityProvider<OIDCIdentityProviderConfig> {
public abstract class AbstractBaseIdentityProvider<T extends AbstractBaseProviderConfig>
extends OIDCIdentityProvider implements SocialIdentityProvider<OIDCIdentityProviderConfig> {

protected static final String ACR_CLAIM_NAME = "acr";

Expand All @@ -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();

Expand All @@ -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;
Expand All @@ -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
Expand All @@ -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());
Expand Down Expand Up @@ -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");
Expand All @@ -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;
}
Expand All @@ -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);
Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit 11b6642

Please sign in to comment.