Skip to content

Commit

Permalink
Internalize nonce generation method
Browse files Browse the repository at this point in the history
We need to do this to adapt to the changes in keycloak code and still be compatible with the 15.0.0 version

Fixes #66

Signed-off-by: Cédric Couralet <[email protected]>
  • Loading branch information
micedre committed Jan 4, 2022
1 parent 57e2477 commit 9ff1c9f
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 62 deletions.
32 changes: 22 additions & 10 deletions src/main/java/fr/insee/keycloak/providers/common/Utils.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package fr.insee.keycloak.providers.common;

import fr.insee.keycloak.mappers.FranceConnectUserAttributeMapper;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Properties;
import java.util.Random;
import org.jboss.logging.Logger;
import org.keycloak.broker.oidc.mappers.AbstractClaimMapper;
import org.keycloak.broker.oidc.mappers.UserAttributeMapper;
Expand All @@ -11,19 +16,16 @@
import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.oidc.utils.JWKSHttpUtils;

import java.io.IOException;
import java.util.HashMap;
import java.util.Properties;

public final class Utils {

private static final Logger logger = Logger.getLogger(Utils.class);

private Utils() {
}
private static ThreadLocal<Random> random = ThreadLocal.withInitial(() -> new SecureRandom());

private Utils() {}

public static IdentityProviderMapperModel createUserAttributeMapper(String providerId, String mapperName,
String claimAttributeName, String userAttributeName) {
public static IdentityProviderMapperModel createUserAttributeMapper(
String providerId, String mapperName, String claimAttributeName, String userAttributeName) {
var mapper = new IdentityProviderMapperModel();

mapper.setName(mapperName);
Expand All @@ -37,8 +39,8 @@ public static IdentityProviderMapperModel createUserAttributeMapper(String provi
return mapper;
}

public static IdentityProviderMapperModel createHardcodedAttributeMapper(String providerId, String mapperName,
String attributeName, String attributeValue) {
public static IdentityProviderMapperModel createHardcodedAttributeMapper(
String providerId, String mapperName, String attributeName, String attributeValue) {

var mapper = new IdentityProviderMapperModel();

Expand Down Expand Up @@ -140,4 +142,14 @@ public static byte[] transcodeSignatureToDER(byte[] jwsSignature) {

return derSignature;
}

public static byte[] generateRandomBytes(int length) {
if (length < 1) {
throw new IllegalArgumentException();
}

byte[] buf = new byte[length];
random.get().nextBytes(buf);
return buf;
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
package fr.insee.keycloak.providers.franceconnect;

import static fr.insee.keycloak.providers.common.EidasLevel.EIDAS1;
import static javax.ws.rs.core.Response.Status.OK;

import com.fasterxml.jackson.databind.JsonNode;
import fr.insee.keycloak.providers.common.AbstractBaseIdentityProvider;
import fr.insee.keycloak.providers.common.Utils;
import java.io.IOException;
import java.util.Optional;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.UriBuilder;
import javax.xml.bind.DatatypeConverter;
import org.keycloak.OAuth2Constants;
import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
import org.keycloak.broker.provider.AuthenticationRequest;
Expand All @@ -15,51 +24,41 @@
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.util.JsonSerialization;

import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.UriBuilder;
import javax.xml.bind.DatatypeConverter;
import java.io.IOException;
import java.util.Optional;

import static fr.insee.keycloak.providers.common.EidasLevel.EIDAS1;
import static javax.ws.rs.core.Response.Status.OK;

final class FranceConnectIdentityProvider extends AbstractBaseIdentityProvider<FranceConnectIdentityProviderConfig> {
final class FranceConnectIdentityProvider
extends AbstractBaseIdentityProvider<FranceConnectIdentityProviderConfig> {

private static final String BROKER_NONCE_PARAM = "BROKER_NONCE";
private static final MediaType APPLICATION_JWT_TYPE = MediaType.valueOf("application/jwt");

FranceConnectIdentityProvider(KeycloakSession session, FranceConnectIdentityProviderConfig config) {
FranceConnectIdentityProvider(
KeycloakSession session, FranceConnectIdentityProviderConfig config) {
super(
session, config,
useJwks(config) ? Utils.getJsonWebKeySetFrom(config.getJwksUrl(), session) : null
);
session,
config,
useJwks(config) ? Utils.getJsonWebKeySetFrom(config.getJwksUrl(), session) : null);
}

private static boolean useJwks(FranceConnectIdentityProviderConfig config) {
return config.isUseJwksUrl() && config.getJwksUrl() != null;
}

/**
* France connect requires nonce to be exactly 64 char long, so...yes
*/
/** France connect requires nonce to be exactly 64 char long, so...yes */
@Override
protected UriBuilder createAuthorizationUrl(AuthenticationRequest request) {
var config = getConfig();
var authenticationSession = request.getAuthenticationSession();

authenticationSession.setClientNote(OAuth2Constants.ACR_VALUES, config.getEidasLevel().toString());
authenticationSession.setClientNote(
OAuth2Constants.ACR_VALUES, config.getEidasLevel().toString());
var uriBuilder = super.createAuthorizationUrl(request);

var nonce = DatatypeConverter.printHexBinary(KeycloakModelUtils.generateSecret(32));
var nonce = DatatypeConverter.printHexBinary(Utils.generateRandomBytes(32));
authenticationSession.setClientNote(BROKER_NONCE_PARAM, nonce);
uriBuilder.replaceQueryParam(OIDCLoginProtocol.NONCE_PARAM, nonce);

Expand All @@ -77,8 +76,9 @@ public JsonWebToken validateToken(String encodedToken) {
}

@Override
protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenResponse, String accessToken,
JsonWebToken idToken) throws IOException {
protected BrokeredIdentityContext extractIdentity(
AccessTokenResponse tokenResponse, String accessToken, JsonWebToken idToken)
throws IOException {
var id = idToken.getSubject();
var identity = new BrokeredIdentityContext(id);

Expand All @@ -93,8 +93,11 @@ protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenRespo
if (userInfoUrl != null && !userInfoUrl.isEmpty()) {

if (accessToken != null) {
var response = executeRequest(userInfoUrl,
SimpleHttp.doGet(userInfoUrl, session).header("Authorization", "Bearer " + accessToken));
var response =
executeRequest(
userInfoUrl,
SimpleHttp.doGet(userInfoUrl, session)
.header("Authorization", "Bearer " + accessToken));
var contentType = response.getFirstHeader(HttpHeaders.CONTENT_TYPE);

MediaType contentMediaType;
Expand All @@ -103,9 +106,15 @@ protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenRespo
} catch (IllegalArgumentException ex) {
contentMediaType = null;
}
if (contentMediaType == null || contentMediaType.isWildcardSubtype() || contentMediaType.isWildcardType()) {
if (contentMediaType == null
|| contentMediaType.isWildcardSubtype()
|| contentMediaType.isWildcardType()) {
throw new RuntimeException(
"Unsupported content-type [" + contentType + "] in response from [" + userInfoUrl + "].");
"Unsupported content-type ["
+ contentType
+ "] in response from ["
+ userInfoUrl
+ "].");
}

JsonNode userInfo;
Expand All @@ -114,16 +123,24 @@ protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenRespo
userInfo = response.asJson();
} else if (APPLICATION_JWT_TYPE.isCompatible(contentMediaType)) {
try {
var jwt = isJWETokenFormatRequired(getConfig())
? decryptJWE(response.asString())
: response.asString();
var jwt =
isJWETokenFormatRequired(getConfig())
? decryptJWE(response.asString())
: response.asString();

userInfo = getJsonFromJWT(jwt);
} catch (IdentityBrokerException ex) {
throw new RuntimeException("Failed to verify signature of userinfo response from [" + userInfoUrl + "].", ex);
throw new RuntimeException(
"Failed to verify signature of userinfo response from [" + userInfoUrl + "].",
ex);
}
} else {
throw new RuntimeException("Unsupported content-type [" + contentType + "] in response from [" + userInfoUrl + "].");
throw new RuntimeException(
"Unsupported content-type ["
+ contentType
+ "] in response from ["
+ userInfoUrl
+ "].");
}

id = getJsonProperty(userInfo, "sub");
Expand All @@ -132,7 +149,8 @@ protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenRespo
familyName = getJsonProperty(userInfo, IDToken.FAMILY_NAME);
preferredUsername = getUsernameFromUserInfo(userInfo);
email = getJsonProperty(userInfo, "email");
AbstractJsonUserAttributeMapper.storeUserProfileForMapper(identity, userInfo, getConfig().getAlias());
AbstractJsonUserAttributeMapper.storeUserProfileForMapper(
identity, userInfo, getConfig().getAlias());
}
}
}
Expand All @@ -151,9 +169,7 @@ protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenRespo
identity.setBrokerUserId(getConfig().getAlias() + "." + id);

var emailOptional = Optional.ofNullable(email);
preferredUsername = Optional.ofNullable(preferredUsername)
.or(() -> emailOptional)
.orElse(id);
preferredUsername = Optional.ofNullable(preferredUsername).or(() -> emailOptional).orElse(id);
identity.setUsername(preferredUsername);

if (tokenResponse != null && tokenResponse.getSessionState() != null) {
Expand All @@ -179,12 +195,14 @@ private String decryptJWE(String encryptedJWE) {
var kid = jwe.getHeader().getKeyId();

// Finding the key from all the realms keys
var key = session.keys()
.getKeysStream(session.getContext().getRealm())
.filter(k -> k.getKid().equalsIgnoreCase(kid))
.findFirst()
.map(KeyWrapper::getPrivateKey)
.orElseThrow(() -> new IdentityBrokerException("No key found for kid " + kid));
var key =
session
.keys()
.getKeysStream(session.getContext().getRealm())
.filter(k -> k.getKid().equalsIgnoreCase(kid))
.findFirst()
.map(KeyWrapper::getPrivateKey)
.orElseThrow(() -> new IdentityBrokerException("No key found for kid " + kid));

logger.debug("Found corresponding secret key for kid " + kid);
jwe.getKeyStorage().setDecryptionKey(key);
Expand All @@ -198,7 +216,8 @@ private SimpleHttp.Response executeRequest(String url, SimpleHttp request) throw
var response = request.asResponse();

if (response.getStatus() != OK.getStatusCode()) {
throw new IdentityBrokerException("Failed to invoke url [" + url + "]: " + response.asString());
throw new IdentityBrokerException(
"Failed to invoke url [" + url + "]: " + response.asString());
}

return response;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package fr.insee.keycloak.providers.franceconnect;

import static fr.insee.keycloak.providers.franceconnect.FranceConnectIdentityProviderFactory.DEFAULT_FC_ENVIRONMENT;
import static fr.insee.keycloak.providers.franceconnect.FranceConnectIdentityProviderFactory.FC_PROVIDER_MAPPERS;

import fr.insee.keycloak.providers.common.AbstractBaseProviderConfig;
import java.util.List;
import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.IdentityProviderModel;

import java.util.List;

import static fr.insee.keycloak.providers.franceconnect.FranceConnectIdentityProviderFactory.DEFAULT_FC_ENVIRONMENT;
import static fr.insee.keycloak.providers.franceconnect.FranceConnectIdentityProviderFactory.FC_PROVIDER_MAPPERS;

final class FranceConnectIdentityProviderConfig extends AbstractBaseProviderConfig {

FranceConnectIdentityProviderConfig(IdentityProviderModel identityProviderModel) {
Expand All @@ -21,10 +20,9 @@ final class FranceConnectIdentityProviderConfig extends AbstractBaseProviderConf

@Override
protected String getEnvironmentProperty(String key) {
var franceConnectEnvironment = FCEnvironment.getOrDefault(
getConfig().get(FCEnvironment.ENVIRONMENT_PROPERTY_NAME),
DEFAULT_FC_ENVIRONMENT
);
var franceConnectEnvironment =
FCEnvironment.getOrDefault(
getConfig().get(FCEnvironment.ENVIRONMENT_PROPERTY_NAME), DEFAULT_FC_ENVIRONMENT);

return franceConnectEnvironment.getProperty(key);
}
Expand Down

0 comments on commit 9ff1c9f

Please sign in to comment.