package org.keycloak.protocol.oidc.mappers;
import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperContainerModel;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.ProtocolMapperConfigException;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public class SHA256PairwiseSubMapper extends AbstractPairwiseSubMapper {
public static final String PROVIDER_ID = "sha256";
private static final String HASH_ALGORITHM = "SHA-256";
private static final Logger logger = Logger.getLogger(SHA256PairwiseSubMapper.class);
private final Charset charset;
public SHA256PairwiseSubMapper() {
charset = Charset.forName("UTF-8");
}
public static ProtocolMapperRepresentation createPairwiseMapper(String sectorIdentifierUri, String salt) {
Map<String, String> config;
ProtocolMapperRepresentation pairwise = new ProtocolMapperRepresentation();
pairwise.setName("pairwise subject identifier");
pairwise.setProtocolMapper(new SHA256PairwiseSubMapper().getId());
pairwise.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
pairwise.setConsentRequired(false);
config = new HashMap<>();
config.put(PairwiseSubMapperHelper.SECTOR_IDENTIFIER_URI, sectorIdentifierUri);
if (salt == null) {
salt = KeycloakModelUtils.generateId();
}
config.put(PairwiseSubMapperHelper.PAIRWISE_SUB_ALGORITHM_SALT, salt);
pairwise.setConfig(config);
return pairwise;
}
@Override
public void validateAdditionalConfig(KeycloakSession session, RealmModel realm, ProtocolMapperContainerModel mapperContainer, ProtocolMapperModel mapperModel) throws ProtocolMapperConfigException {
// Generate random salt if needed
String salt = PairwiseSubMapperHelper.getSalt(mapperModel);
if (salt == null || salt.trim().isEmpty()) {
salt = generateSalt();
PairwiseSubMapperHelper.setSalt(mapperModel, salt);
}
}
@Override
public String getHelpText() {
return "Calculates a pairwise subject identifier using a salted sha-256 hash. See OpenID Connect specification for more info about pairwise subject identifiers.";
}
@Override
public List<ProviderConfigProperty> getAdditionalConfigProperties() {
List<ProviderConfigProperty> configProperties = new LinkedList<>();
configProperties.add(PairwiseSubMapperHelper.createSaltConfig());
return configProperties;
}
@Override
public String generateSub(ProtocolMapperModel mappingModel, String sectorIdentifier, String localSub) {
String saltStr = PairwiseSubMapperHelper.getSalt(mappingModel);
if (saltStr == null) {
throw new IllegalStateException("Salt not available on mappingModel. Please update protocol mapper");
}
Charset charset = Charset.forName("UTF-8");
byte[] salt = saltStr.getBytes(charset);
String pairwiseSub = generateSub(sectorIdentifier, localSub, salt);
logger.infof("local sub = '%s', pairwise sub = '%s'", localSub, pairwiseSub);
return pairwiseSub;
}
private String generateSub(String sectorIdentifier, String localSub, byte[] salt) {
MessageDigest sha256;
try {
sha256 = MessageDigest.getInstance(HASH_ALGORITHM);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e.getMessage(), e);
}
sha256.update(sectorIdentifier.getBytes(charset));
sha256.update(localSub.getBytes(charset));
byte[] hash = sha256.digest(salt);
return UUID.nameUUIDFromBytes(hash).toString();
}
private static String generateSalt() {
return KeycloakModelUtils.generateId();
}
@Override
public String getDisplayType() {
return "Pairwise subject identifier";
}
@Override
public String getIdPrefix() {
return PROVIDER_ID;
}
}