Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Bill Burke 2016-09-23 16:50:16 -04:00
commit 27e86e36c4
25 changed files with 686 additions and 297 deletions

View file

@ -18,11 +18,13 @@
package org.keycloak.broker.oidc.mappers;
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProvider;
import org.keycloak.broker.oidc.OIDCIdentityProvider;
import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.representations.JsonWebToken;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.List;
import java.util.Map;
@ -71,6 +73,12 @@ public abstract class AbstractClaimMapper extends AbstractIdentityProviderMapper
}
}
{
// Search the OIDC UserInfo claim set (if any)
JsonNode profileJsonNode = (JsonNode) context.getContextData().get(OIDCIdentityProvider.USER_INFO);
String value = AbstractJsonUserAttributeMapper.getJsonValue(profileJsonNode, claim);
if (value != null) return value;
}
return null;
}

View file

@ -126,7 +126,7 @@ public class UserAttributeMapper extends AbstractClaimMapper {
@Override
public String getHelpText() {
return "Import declared claim if it exists in ID or access token into the specified user property or attribute.";
return "Import declared claim if it exists in ID, access token or the claim set returned by the user profile endpoint into the specified user property or attribute.";
}
}

View file

@ -19,7 +19,6 @@ package org.keycloak.protocol.oidc;
import org.keycloak.jose.jws.Algorithm;
import org.keycloak.models.ClientModel;
import org.keycloak.protocol.oidc.utils.SubjectType;
import org.keycloak.representations.idm.ClientRepresentation;
import java.util.HashMap;
@ -33,11 +32,6 @@ public class OIDCAdvancedConfigWrapper {
private static final String REQUEST_OBJECT_SIGNATURE_ALG = "request.object.signature.alg";
private static final String SUBJECT_TYPE = "oidc.subject_type";
private static final String SECTOR_IDENTIFIER_URI = "oidc.sector_identifier_uri";
private static final String PUBLIC = "public";
private static final String PAIRWISE = "pairwise";
private final ClientModel clientModel;
private final ClientRepresentation clientRep;
@ -80,27 +74,6 @@ public class OIDCAdvancedConfigWrapper {
setAttribute(REQUEST_OBJECT_SIGNATURE_ALG, algStr);
}
public void setSubjectType(SubjectType subjectType) {
if (subjectType == null) {
setAttribute(SUBJECT_TYPE, SubjectType.PUBLIC.toString());
return;
}
setAttribute(SUBJECT_TYPE, subjectType.toString());
}
public SubjectType getSubjectType() {
String subjectType = getAttribute(SUBJECT_TYPE);
return subjectType == null ? SubjectType.PUBLIC : Enum.valueOf(SubjectType.class, subjectType);
}
public void setSectorIdentifierUri(String sectorIdentifierUri) {
setAttribute(SECTOR_IDENTIFIER_URI, sectorIdentifierUri);
}
public String getSectorIdentifierUri() {
return getAttribute(SECTOR_IDENTIFIER_URI);
}
private String getAttribute(String attrKey) {
if (clientModel != null) {

View file

@ -130,9 +130,6 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory {
ProtocolMapperModel address = AddressMapper.createAddressMapper();
builtins.add(address);
ProtocolMapperModel pairwise = SHA265PairwiseSubMapper.createPairwiseMapper();
builtins.add(pairwise);
model = UserSessionNoteMapper.createClaimMapper(KerberosConstants.GSS_DELEGATION_CREDENTIAL_DISPLAY_NAME,
KerberosConstants.GSS_DELEGATION_CREDENTIAL,
KerberosConstants.GSS_DELEGATION_CREDENTIAL, "String",

View file

@ -108,12 +108,6 @@ public abstract class AbstractPairwiseSubMapper extends AbstractOIDCProtocolMapp
PairwiseSubMapperValidator.validate(session, client, mapperModel);
}
validateAdditionalConfig(session, realm, mapperContainer, mapperModel);
if (client != null) {
// Propagate changes to the sector identifier uri
OIDCAdvancedConfigWrapper configWrapper = OIDCAdvancedConfigWrapper.fromClientModel(client);
configWrapper.setSectorIdentifierUri(PairwiseSubMapperHelper.getSectorIdentifierUri(mapperModel));
}
}
@Override

View file

@ -2,6 +2,7 @@ package org.keycloak.protocol.oidc.mappers;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.services.ServicesLogger;
public class PairwiseSubMapperHelper {
@ -15,6 +16,14 @@ public class PairwiseSubMapperHelper {
public static final String PAIRWISE_SUB_ALGORITHM_SALT_LABEL = "pairwiseSubAlgorithmSalt.label";
public static final String PAIRWISE_SUB_ALGORITHM_SALT_HELP_TEXT = "pairwiseSubAlgorithmSalt.tooltip";
public static String getSectorIdentifierUri(ProtocolMapperRepresentation mappingModel) {
return mappingModel.getConfig().get(SECTOR_IDENTIFIER_URI);
}
public static void setSectorIdentifierUri(ProtocolMapperModel mappingModel, String sectorIdentifierUri) {
mappingModel.getConfig().put(SECTOR_IDENTIFIER_URI, sectorIdentifierUri);
}
public static String getSectorIdentifierUri(ProtocolMapperModel mappingModel) {
return mappingModel.getConfig().get(SECTOR_IDENTIFIER_URI);
}

View file

@ -1,62 +1,61 @@
package org.keycloak.protocol.oidc.mappers;
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 org.keycloak.services.ServicesLogger;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.*;
public class SHA265PairwiseSubMapper extends AbstractPairwiseSubMapper {
public class SHA256PairwiseSubMapper extends AbstractPairwiseSubMapper {
public static final String PROVIDER_ID = "sha256";
private static final String HASH_ALGORITHM = "SHA-256";
private static final String ALPHA_NUMERIC = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
private static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
private final Charset charset;
public SHA265PairwiseSubMapper() throws NoSuchAlgorithmException {
public SHA256PairwiseSubMapper() throws NoSuchAlgorithmException {
charset = Charset.forName("UTF-8");
MessageDigest.getInstance(HASH_ALGORITHM);
}
public static ProtocolMapperModel createPairwiseMapper() {
return createPairwiseMapper(null);
}
public static ProtocolMapperModel createPairwiseMapper(String sectorIdentifierUri) {
public static ProtocolMapperRepresentation createPairwiseMapper(String sectorIdentifierUri, String salt) {
Map<String, String> config;
ProtocolMapperModel pairwise = new ProtocolMapperModel();
pairwise.setName("pairwise subject identifier");
pairwise.setProtocolMapper(AbstractPairwiseSubMapper.getId(PROVIDER_ID));
pairwise.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
pairwise.setConsentRequired(false);
config = new HashMap<>();
config.put(PairwiseSubMapperHelper.SECTOR_IDENTIFIER_URI, sectorIdentifierUri);
pairwise.setConfig(config);
return pairwise;
}
public static ProtocolMapperModel createPairwiseMapper(String sectorIdentifierUri, String salt) {
Map<String, String> config;
ProtocolMapperModel pairwise = new ProtocolMapperModel();
ProtocolMapperRepresentation pairwise = new ProtocolMapperRepresentation();
pairwise.setName("pairwise subject identifier");
pairwise.setProtocolMapper(AbstractPairwiseSubMapper.getId(PROVIDER_ID));
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.";
return "Calculates a pairwise subject identifier using a salted sha-256 hash. See OpenID Connect specification for more info about pairwise subject identifiers.";
}
@Override
@ -68,7 +67,10 @@ public class SHA265PairwiseSubMapper extends AbstractPairwiseSubMapper {
@Override
public String generateSub(ProtocolMapperModel mappingModel, String sectorIdentifier, String localSub) {
String saltStr = getSalt(mappingModel);
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);
@ -90,21 +92,8 @@ public class SHA265PairwiseSubMapper extends AbstractPairwiseSubMapper {
return UUID.nameUUIDFromBytes(hash).toString();
}
private String getSalt(ProtocolMapperModel mappingModel) {
String salt = PairwiseSubMapperHelper.getSalt(mappingModel);
if (salt == null || salt.trim().isEmpty()) {
salt = createSalt(32);
PairwiseSubMapperHelper.setSalt(mappingModel, salt);
}
return salt;
}
private String createSalt(int len) {
Random rnd = new SecureRandom();
StringBuilder sb = new StringBuilder(len);
for (int i = 0; i < len; i++)
sb.append(ALPHA_NUMERIC.charAt(rnd.nextInt(ALPHA_NUMERIC.length())));
return sb.toString();
private static String generateSalt() {
return KeycloakModelUtils.generateId();
}
@Override

View file

@ -1,5 +1,7 @@
package org.keycloak.protocol.oidc.utils;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.protocol.oidc.mappers.AbstractPairwiseSubMapper;
import org.keycloak.protocol.oidc.mappers.PairwiseSubMapperHelper;
import org.keycloak.representations.idm.ClientRepresentation;
@ -9,6 +11,7 @@ import org.keycloak.services.ServicesLogger;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@ -142,18 +145,18 @@ public class PairwiseSubMapperUtils {
return relative;
}
public static ProtocolMapperRepresentation getPairwiseSubMapperRepresentation(ClientRepresentation client) {
public static List<ProtocolMapperRepresentation> getPairwiseSubMappers(ClientRepresentation client) {
List<ProtocolMapperRepresentation> pairwiseMappers = new LinkedList<>();
List<ProtocolMapperRepresentation> mappers = client.getProtocolMappers();
if (mappers == null) {
return null;
}
for (ProtocolMapperRepresentation mapper : mappers) {
if (mapper.getProtocolMapper().endsWith(AbstractPairwiseSubMapper.PROVIDER_ID_SUFFIX)) return mapper;
}
return null;
}
public static String getSubjectIdentifierUri(ProtocolMapperRepresentation pairwiseMapper) {
return pairwiseMapper.getConfig().get(PairwiseSubMapperHelper.SECTOR_IDENTIFIER_URI);
if (mappers != null) {
client.getProtocolMappers().stream().filter((ProtocolMapperRepresentation mapping) -> {
return mapping.getProtocolMapper().endsWith(AbstractPairwiseSubMapper.PROVIDER_ID_SUFFIX);
}).forEach((ProtocolMapperRepresentation mapping) -> {
pairwiseMappers.add(mapping);
});
}
return pairwiseMappers;
}
}

View file

@ -22,6 +22,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.saml.EntityDescriptorDescriptionConverter;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.services.clientregistration.AbstractClientRegistrationProvider;
import org.keycloak.services.clientregistration.ClientRegistrationContext;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
@ -44,7 +45,8 @@ public class EntityDescriptorClientRegistrationProvider extends AbstractClientRe
@Produces(MediaType.APPLICATION_JSON)
public Response createSaml(String descriptor) {
ClientRepresentation client = session.getProvider(ClientDescriptionConverter.class, EntityDescriptorDescriptionConverter.ID).convertToInternal(descriptor);
client = create(client);
ClientRegistrationContext context = new ClientRegistrationContext(client);
client = create(context);
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
return Response.created(uri).entity(client).build();
}

View file

@ -22,16 +22,10 @@ import org.keycloak.events.EventType;
import org.keycloak.models.*;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.mappers.AbstractPairwiseSubMapper;
import org.keycloak.protocol.oidc.mappers.PairwiseSubMapperHelper;
import org.keycloak.protocol.oidc.mappers.SHA265PairwiseSubMapper;
import org.keycloak.protocol.oidc.utils.SubjectType;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.ForbiddenException;
import org.keycloak.services.validation.ClientValidator;
import org.keycloak.services.validation.PairwiseClientValidator;
import org.keycloak.services.validation.ValidationMessages;
import javax.ws.rs.core.Response;
@ -49,13 +43,15 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
this.session = session;
}
public ClientRepresentation create(ClientRepresentation client) {
public ClientRepresentation create(ClientRegistrationContext context) {
ClientRepresentation client = context.getClient();
event.event(EventType.CLIENT_REGISTER);
auth.requireCreate();
ValidationMessages validationMessages = new ValidationMessages();
if (!ClientValidator.validate(client, validationMessages) || !PairwiseClientValidator.validate(session, client, validationMessages)) {
if (!validateClient(context, validationMessages)) {
String errorCode = validationMessages.fieldHasError("redirectUris") ? ErrorCodes.INVALID_REDIRECT_URI : ErrorCodes.INVALID_CLIENT_METADATA;
throw new ErrorResponseException(
errorCode,
@ -66,10 +62,6 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
try {
ClientModel clientModel = RepresentationToModel.createClient(session, session.getContext().getRealm(), client, true);
OIDCAdvancedConfigWrapper configWrapper = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
if (configWrapper.getSubjectType().equals(SubjectType.PAIRWISE)) {
addPairwiseSubMapper(clientModel, configWrapper.getSectorIdentifierUri());
}
client = ModelToRepresentation.toRepresentation(clientModel);
@ -112,7 +104,9 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
return rep;
}
public ClientRepresentation update(String clientId, ClientRepresentation rep) {
public ClientRepresentation update(String clientId, ClientRegistrationContext context) {
ClientRepresentation rep = context.getClient();
event.event(EventType.CLIENT_UPDATE).client(clientId);
ClientModel client = session.getContext().getRealm().getClientByClientId(clientId);
@ -123,7 +117,7 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
}
ValidationMessages validationMessages = new ValidationMessages();
if (!ClientValidator.validate(rep, validationMessages) || !PairwiseClientValidator.validate(session, rep, validationMessages)) {
if (!validateClient(context, validationMessages)) {
String errorCode = validationMessages.fieldHasError("redirectUris") ? ErrorCodes.INVALID_REDIRECT_URI : ErrorCodes.INVALID_CLIENT_METADATA;
throw new ErrorResponseException(
errorCode,
@ -132,7 +126,6 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
);
}
updateSubjectType(rep, client);
RepresentationToModel.updateClient(rep, client);
rep = ModelToRepresentation.toRepresentation(client);
@ -145,42 +138,6 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
return rep;
}
private void updateSubjectType(ClientRepresentation rep, ClientModel client) {
OIDCAdvancedConfigWrapper repConfigWrapper = OIDCAdvancedConfigWrapper.fromClientRepresentation(rep);
SubjectType repSubjectType = repConfigWrapper.getSubjectType();
OIDCAdvancedConfigWrapper clientConfigWrapper = OIDCAdvancedConfigWrapper.fromClientModel(client);
SubjectType clientSubjectType = clientConfigWrapper.getSubjectType();
if (repSubjectType.equals(SubjectType.PAIRWISE) && clientSubjectType.equals(SubjectType.PAIRWISE)) {
updateSectorIdentifier(client, repConfigWrapper.getSectorIdentifierUri());
}
if (repSubjectType.equals(SubjectType.PAIRWISE) && clientSubjectType.equals(SubjectType.PUBLIC)) {
addPairwiseSubMapper(client, repConfigWrapper.getSectorIdentifierUri());
}
if (repSubjectType.equals(SubjectType.PUBLIC) && clientSubjectType.equals(SubjectType.PAIRWISE)) {
removePairwiseSubMapper(client);
}
}
private void updateSectorIdentifier(ClientModel client, String sectorIdentifierUri) {
client.getProtocolMappers().stream().filter(mapping -> mapping.getProtocolMapper().endsWith(AbstractPairwiseSubMapper.PROVIDER_ID_SUFFIX)).forEach(mapping -> {
mapping.getConfig().put(PairwiseSubMapperHelper.SECTOR_IDENTIFIER_URI, sectorIdentifierUri);
});
}
private void addPairwiseSubMapper(ClientModel client, String sectorIdentifierUri) {
client.addProtocolMapper(SHA265PairwiseSubMapper.createPairwiseMapper(sectorIdentifierUri));
}
private void removePairwiseSubMapper(ClientModel client) {
for (ProtocolMapperModel mapping : client.getProtocolMappers()) {
if (mapping.getProtocolMapper().endsWith(AbstractPairwiseSubMapper.PROVIDER_ID_SUFFIX)) {
client.removeProtocolMapper(mapping);
}
}
}
public void delete(String clientId) {
event.event(EventType.CLIENT_DELETE).client(clientId);
@ -209,4 +166,9 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
public void close() {
}
protected boolean validateClient(ClientRegistrationContext context, ValidationMessages validationMessages) {
ClientRepresentation client = context.getClient();
return ClientValidator.validate(client, validationMessages);
}
}

View file

@ -0,0 +1,36 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.services.clientregistration;
import org.keycloak.representations.idm.ClientRepresentation;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ClientRegistrationContext {
private final ClientRepresentation client;
public ClientRegistrationContext(ClientRepresentation client) {
this.client = client;
}
public ClientRepresentation getClient() {
return client;
}
}

View file

@ -20,6 +20,8 @@ package org.keycloak.services.clientregistration;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.KeycloakSession;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.services.validation.PairwiseClientValidator;
import org.keycloak.services.validation.ValidationMessages;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
@ -39,7 +41,8 @@ public class DefaultClientRegistrationProvider extends AbstractClientRegistratio
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response createDefault(ClientRepresentation client) {
client = create(client);
ClientRegistrationContext context = new ClientRegistrationContext(client);
client = create(context);
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
return Response.created(uri).entity(client).build();
}
@ -56,7 +59,8 @@ public class DefaultClientRegistrationProvider extends AbstractClientRegistratio
@Path("{clientId}")
@Consumes(MediaType.APPLICATION_JSON)
public Response updateDefault(@PathParam("clientId") String clientId, ClientRepresentation client) {
client = update(clientId, client);
ClientRegistrationContext context = new ClientRegistrationContext(client);
client = update(clientId, context);
return Response.ok(client).build();
}
@ -80,4 +84,9 @@ public class DefaultClientRegistrationProvider extends AbstractClientRegistratio
public void close() {
}
@Override
protected boolean validateClient(ClientRegistrationContext context, ValidationMessages validationMessages) {
ClientRepresentation client = context.getClient();
return super.validateClient(context, validationMessages) && PairwiseClientValidator.validate(session, client, validationMessages);
}
}

View file

@ -29,12 +29,15 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.mappers.PairwiseSubMapperHelper;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
import org.keycloak.protocol.oidc.utils.JWKSUtils;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.protocol.oidc.utils.PairwiseSubMapperUtils;
import org.keycloak.protocol.oidc.utils.SubjectType;
import org.keycloak.representations.idm.CertificateRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.services.clientregistration.ClientRegistrationException;
import org.keycloak.services.util.CertificateInfoHelper;
@ -115,12 +118,6 @@ public class DescriptionConverter {
configWrapper.setRequestObjectSignatureAlg(algorithm);
}
SubjectType subjectType = SubjectType.parse(clientOIDC.getSubjectType());
configWrapper.setSubjectType(subjectType);
if (subjectType.equals(SubjectType.PAIRWISE)) {
configWrapper.setSectorIdentifierUri(clientOIDC.getSectorIdentifierUri());
}
return client;
}
@ -180,9 +177,13 @@ public class DescriptionConverter {
response.setRequestObjectSigningAlg(config.getRequestObjectSignatureAlg().toString());
}
response.setSubjectType(config.getSubjectType().toString().toLowerCase());
if (config.getSubjectType().equals(SubjectType.PAIRWISE)) {
response.setSectorIdentifierUri(config.getSectorIdentifierUri());
List<ProtocolMapperRepresentation> foundPairwiseMappers = PairwiseSubMapperUtils.getPairwiseSubMappers(client);
SubjectType subjectType = foundPairwiseMappers.isEmpty() ? SubjectType.PUBLIC : SubjectType.PAIRWISE;
response.setSubjectType(subjectType.toString().toLowerCase());
if (subjectType.equals(SubjectType.PAIRWISE)) {
// Get sectorIdentifier from 1st found
String sectorIdentifierUri = PairwiseSubMapperHelper.getSectorIdentifierUri(foundPairwiseMappers.get(0));
response.setSectorIdentifierUri(sectorIdentifierUri);
}
return response;

View file

@ -0,0 +1,39 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.services.clientregistration.oidc;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.services.clientregistration.ClientRegistrationContext;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class OIDCClientRegistrationContext extends ClientRegistrationContext {
private final OIDCClientRepresentation oidcRep;
public OIDCClientRegistrationContext(ClientRepresentation client, OIDCClientRepresentation oidcRep) {
super(client);
this.oidcRep = oidcRep;
}
public OIDCClientRepresentation getOidcRep() {
return oidcRep;
}
}

View file

@ -18,20 +18,46 @@ package org.keycloak.services.clientregistration.oidc;
import org.keycloak.common.util.Time;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.ProtocolMapperConfigException;
import org.keycloak.protocol.oidc.mappers.AbstractPairwiseSubMapper;
import org.keycloak.protocol.oidc.mappers.PairwiseSubMapperHelper;
import org.keycloak.protocol.oidc.mappers.SHA256PairwiseSubMapper;
import org.keycloak.protocol.oidc.utils.PairwiseSubMapperValidator;
import org.keycloak.protocol.oidc.utils.SubjectType;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.clientregistration.AbstractClientRegistrationProvider;
import org.keycloak.services.clientregistration.ClientRegistrationAuth;
import org.keycloak.services.clientregistration.ClientRegistrationContext;
import org.keycloak.services.clientregistration.ClientRegistrationException;
import org.keycloak.services.clientregistration.ErrorCodes;
import org.keycloak.services.validation.PairwiseClientValidator;
import org.keycloak.services.validation.ValidationMessages;
import javax.ws.rs.*;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.net.URI;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -54,7 +80,13 @@ public class OIDCClientRegistrationProvider extends AbstractClientRegistrationPr
try {
ClientRepresentation client = DescriptionConverter.toInternal(session, clientOIDC);
client = create(client);
OIDCClientRegistrationContext oidcContext = new OIDCClientRegistrationContext(client, clientOIDC);
client = create(oidcContext);
ClientModel clientModel = session.getContext().getRealm().getClientByClientId(client.getClientId());
updatePairwiseSubMappers(clientModel, SubjectType.parse(clientOIDC.getSubjectType()), clientOIDC.getSectorIdentifierUri());
updateClientRepWithProtocolMappers(clientModel, client);
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
clientOIDC = DescriptionConverter.toExternalResponse(session, client, uri);
clientOIDC.setClientIdIssuedAt(Time.currentTime());
@ -80,7 +112,13 @@ public class OIDCClientRegistrationProvider extends AbstractClientRegistrationPr
public Response updateOIDC(@PathParam("clientId") String clientId, OIDCClientRepresentation clientOIDC) {
try {
ClientRepresentation client = DescriptionConverter.toInternal(session, clientOIDC);
client = update(clientId, client);
OIDCClientRegistrationContext oidcContext = new OIDCClientRegistrationContext(client, clientOIDC);
client = update(clientId, oidcContext);
ClientModel clientModel = session.getContext().getRealm().getClientByClientId(client.getClientId());
updatePairwiseSubMappers(clientModel, SubjectType.parse(clientOIDC.getSubjectType()), clientOIDC.getSectorIdentifierUri());
updateClientRepWithProtocolMappers(clientModel, client);
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
clientOIDC = DescriptionConverter.toExternalResponse(session, client, uri);
return Response.ok(clientOIDC).build();
@ -110,4 +148,70 @@ public class OIDCClientRegistrationProvider extends AbstractClientRegistrationPr
public void close() {
}
private void updatePairwiseSubMappers(ClientModel clientModel, SubjectType subjectType, String sectorIdentifierUri) {
if (subjectType == SubjectType.PAIRWISE) {
// See if we have existing pairwise mapper and update it. Otherwise create new
AtomicBoolean foundPairwise = new AtomicBoolean(false);
clientModel.getProtocolMappers().stream().filter((ProtocolMapperModel mapping) -> {
if (mapping.getProtocolMapper().endsWith(AbstractPairwiseSubMapper.PROVIDER_ID_SUFFIX)) {
foundPairwise.set(true);
return true;
} else {
return false;
}
}).forEach((ProtocolMapperModel mapping) -> {
PairwiseSubMapperHelper.setSectorIdentifierUri(mapping, sectorIdentifierUri);
clientModel.updateProtocolMapper(mapping);
});
// We don't have existing pairwise mapper. So create new
if (!foundPairwise.get()) {
ProtocolMapperRepresentation newPairwise = SHA256PairwiseSubMapper.createPairwiseMapper(sectorIdentifierUri, null);
clientModel.addProtocolMapper(RepresentationToModel.toModel(newPairwise));
}
} else {
// Rather find and remove all pairwise mappers
clientModel.getProtocolMappers().stream().filter((ProtocolMapperModel mapperRep) -> {
return mapperRep.getProtocolMapper().endsWith(AbstractPairwiseSubMapper.PROVIDER_ID_SUFFIX);
}).forEach((ProtocolMapperModel mapping) -> {
clientModel.getProtocolMappers().remove(mapping);
});
}
}
@Override
protected boolean validateClient(ClientRegistrationContext context, ValidationMessages validationMessages) {
OIDCClientRegistrationContext oidcContext = (OIDCClientRegistrationContext) context;
OIDCClientRepresentation oidcRep = oidcContext.getOidcRep();
boolean valid = super.validateClient(context, validationMessages);
ClientRepresentation client = oidcContext.getClient();
String rootUrl = client.getRootUrl();
Set<String> redirectUris = new HashSet<>();
if (client.getRedirectUris() != null) redirectUris.addAll(client.getRedirectUris());
SubjectType subjectType = SubjectType.parse(oidcRep.getSubjectType());
String sectorIdentifierUri = oidcRep.getSectorIdentifierUri();
// If sector_identifier_uri is in oidc config, then always validate it
if (SubjectType.PAIRWISE == subjectType || (sectorIdentifierUri != null && !sectorIdentifierUri.isEmpty())) {
valid = valid && PairwiseClientValidator.validate(session, rootUrl, redirectUris, oidcRep.getSectorIdentifierUri(), validationMessages);
}
return valid;
}
private void updateClientRepWithProtocolMappers(ClientModel clientModel, ClientRepresentation rep) {
List<ProtocolMapperRepresentation> mappings = new LinkedList<>();
for (ProtocolMapperModel model : clientModel.getProtocolMappers()) {
mappings.add(ModelToRepresentation.toRepresentation(model));
}
rep.setProtocolMappers(mappings);
}
}

View file

@ -2,12 +2,14 @@ package org.keycloak.services.validation;
import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.ProtocolMapperConfigException;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.mappers.PairwiseSubMapperHelper;
import org.keycloak.protocol.oidc.utils.PairwiseSubMapperUtils;
import org.keycloak.protocol.oidc.utils.PairwiseSubMapperValidator;
import org.keycloak.protocol.oidc.utils.SubjectType;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@ -17,14 +19,18 @@ import java.util.Set;
public class PairwiseClientValidator {
public static boolean validate(KeycloakSession session, ClientRepresentation client, ValidationMessages messages) {
OIDCAdvancedConfigWrapper configWrapper = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
if (configWrapper.getSubjectType().equals(SubjectType.PAIRWISE)) {
String sectorIdentifierUri = configWrapper.getSectorIdentifierUri();
String rootUrl = client.getRootUrl();
Set<String> redirectUris = new HashSet<>();
String rootUrl = client.getRootUrl();
Set<String> redirectUris = new HashSet<>();
boolean valid = true;
List<ProtocolMapperRepresentation> foundPairwiseMappers = PairwiseSubMapperUtils.getPairwiseSubMappers(client);
for (ProtocolMapperRepresentation foundPairwise : foundPairwiseMappers) {
String sectorIdentifierUri = PairwiseSubMapperHelper.getSectorIdentifierUri(foundPairwise);
if (client.getRedirectUris() != null) redirectUris.addAll(client.getRedirectUris());
return validate(session, rootUrl, redirectUris, sectorIdentifierUri, messages);
valid = valid && validate(session, rootUrl, redirectUris, sectorIdentifierUri, messages);
}
return true;
}

View file

@ -34,5 +34,5 @@ org.keycloak.protocol.saml.mappers.UserSessionNoteStatementMapper
org.keycloak.protocol.saml.mappers.GroupMembershipMapper
org.keycloak.protocol.oidc.mappers.UserClientRoleMappingMapper
org.keycloak.protocol.oidc.mappers.UserRealmRoleMappingMapper
org.keycloak.protocol.oidc.mappers.SHA265PairwiseSubMapper
org.keycloak.protocol.oidc.mappers.SHA256PairwiseSubMapper

View file

@ -89,6 +89,14 @@ public abstract class AbstractClientRegistrationTest extends AbstractKeycloakTes
rep.getUsers().add(user3);
UserRepresentation appUser = new UserRepresentation();
appUser.setEnabled(true);
appUser.setUsername("test-user");
appUser.setEmail("test-user@localhost");
appUser.setCredentials(credentials);
rep.getUsers().add(appUser);
testRealms.add(rep);
}

View file

@ -61,7 +61,6 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import static org.junit.Assert.*;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -282,137 +281,6 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
Assert.assertEquals(response.getClientId(), accessToken.getAudience()[0]);
}
@Test
public void createPairwiseClient() throws Exception {
OIDCClientRepresentation clientRep = createRep();
clientRep.setSubjectType("pairwise");
OIDCClientRepresentation response = reg.oidc().create(clientRep);
Assert.assertEquals("pairwise", response.getSubjectType());
}
@Test
public void updateClientToPairwise() throws Exception {
OIDCClientRepresentation response = create();
Assert.assertEquals("public", response.getSubjectType());
reg.auth(Auth.token(response));
response.setSubjectType("pairwise");
OIDCClientRepresentation updated = reg.oidc().update(response);
Assert.assertEquals("pairwise", updated.getSubjectType());
}
@Test
public void updateSectorIdentifierUri() throws Exception {
OIDCClientRepresentation clientRep = createRep();
clientRep.setSubjectType("pairwise");
OIDCClientRepresentation response = reg.oidc().create(clientRep);
Assert.assertEquals("pairwise", response.getSubjectType());
Assert.assertNull(response.getSectorIdentifierUri());
reg.auth(Auth.token(response));
// Push redirect uris to the sector identifier URI
List<String> sectorRedirects = new ArrayList<>();
sectorRedirects.addAll(response.getRedirectUris());
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
oidcClientEndpointsResource.setSectorIdentifierRedirectUris(sectorRedirects);
response.setSectorIdentifierUri(TestApplicationResourceUrls.pairwiseSectorIdentifierUri());
OIDCClientRepresentation updated = reg.oidc().update(response);
Assert.assertEquals("pairwise", updated.getSubjectType());
Assert.assertEquals(TestApplicationResourceUrls.pairwiseSectorIdentifierUri(), updated.getSectorIdentifierUri());
}
@Test
public void createPairwiseClientWithSectorIdentifierURI() throws Exception {
OIDCClientRepresentation clientRep = createRep();
// Push redirect uris to the sector identifier URI
List<String> sectorRedirects = new ArrayList<>();
sectorRedirects.addAll(clientRep.getRedirectUris());
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
oidcClientEndpointsResource.setSectorIdentifierRedirectUris(sectorRedirects);
clientRep.setSubjectType("pairwise");
clientRep.setSectorIdentifierUri(TestApplicationResourceUrls.pairwiseSectorIdentifierUri());
OIDCClientRepresentation response = reg.oidc().create(clientRep);
Assert.assertEquals("pairwise", response.getSubjectType());
Assert.assertEquals(TestApplicationResourceUrls.pairwiseSectorIdentifierUri(), response.getSectorIdentifierUri());
}
@Test
public void createPairwiseClientWithRedirectsToMultipleHostsWithoutSectorIdentifierURI() throws Exception {
OIDCClientRepresentation clientRep = createRep();
List<String> redirects = new ArrayList<>();
redirects.add("http://redirect1");
redirects.add("http://redirect2");
clientRep.setSubjectType("pairwise");
clientRep.setRedirectUris(redirects);
assertCreateFail(clientRep, 400, "Without a configured Sector Identifier URI, client redirect URIs must not contain multiple host components.");
}
@Test
public void createPairwiseClientWithRedirectsToMultipleHosts() throws Exception {
OIDCClientRepresentation clientRep = createRep();
// Push redirect URIs to the sector identifier URI
List<String> redirects = new ArrayList<>();
redirects.add("http://redirect1");
redirects.add("http://redirect2");
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
oidcClientEndpointsResource.setSectorIdentifierRedirectUris(redirects);
clientRep.setSubjectType("pairwise");
clientRep.setSectorIdentifierUri(TestApplicationResourceUrls.pairwiseSectorIdentifierUri());
clientRep.setRedirectUris(redirects);
OIDCClientRepresentation response = reg.oidc().create(clientRep);
Assert.assertEquals("pairwise", response.getSubjectType());
Assert.assertEquals(TestApplicationResourceUrls.pairwiseSectorIdentifierUri(), response.getSectorIdentifierUri());
Assert.assertNames(response.getRedirectUris(), "http://redirect1", "http://redirect2");
}
@Test
public void createPairwiseClientWithSectorIdentifierURIContainingMismatchedRedirects() throws Exception {
OIDCClientRepresentation clientRep = createRep();
// Push redirect uris to the sector identifier URI
List<String> sectorRedirects = new ArrayList<>();
sectorRedirects.add("http://someotherredirect");
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
oidcClientEndpointsResource.setSectorIdentifierRedirectUris(sectorRedirects);
clientRep.setSubjectType("pairwise");
clientRep.setSectorIdentifierUri(TestApplicationResourceUrls.pairwiseSectorIdentifierUri());
assertCreateFail(clientRep, 400, "Client redirect URIs does not match redirect URIs fetched from the Sector Identifier URI.");
}
@Test
public void createPairwiseClientWithInvalidSectorIdentifierURI() throws Exception {
OIDCClientRepresentation clientRep = createRep();
clientRep.setSubjectType("pairwise");
clientRep.setSectorIdentifierUri("malformed");
assertCreateFail(clientRep, 400, "Invalid Sector Identifier URI.");
}
@Test
public void createPairwiseClientWithUnreachableSectorIdentifierURI() throws Exception {
OIDCClientRepresentation clientRep = createRep();
clientRep.setSubjectType("pairwise");
clientRep.setSectorIdentifierUri("http://localhost/dummy");
assertCreateFail(clientRep, 400, "Failed to get redirect URIs from the Sector Identifier URI.");
}
@Test
public void testSignaturesRequired() throws Exception {
OIDCClientRepresentation clientRep = createRep();

View file

@ -0,0 +1,328 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.client;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.ws.rs.core.Response;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.client.registration.Auth;
import org.keycloak.client.registration.ClientRegistrationException;
import org.keycloak.client.registration.HttpErrorException;
import org.keycloak.protocol.oidc.mappers.SHA256PairwiseSubMapper;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
import org.keycloak.representations.idm.ClientInitialAccessPresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls;
import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResource;
import org.keycloak.testsuite.util.ClientManager;
import org.keycloak.testsuite.util.OAuthClient;
import static org.junit.Assert.assertTrue;
public class OIDCPairwiseClientRegistrationTest extends AbstractClientRegistrationTest {
@Before
public void before() throws Exception {
super.before();
ClientInitialAccessPresentation token = adminClient.realm(REALM_NAME).clientInitialAccess().create(new ClientInitialAccessCreatePresentation(0, 10));
reg.auth(Auth.token(token));
}
private OIDCClientRepresentation createRep() {
OIDCClientRepresentation client = new OIDCClientRepresentation();
client.setClientName("RegistrationAccessTokenTest");
client.setClientUri(OAuthClient.APP_ROOT);
client.setRedirectUris(Collections.singletonList(oauth.getRedirectUri()));
return client;
}
public OIDCClientRepresentation create() throws ClientRegistrationException {
OIDCClientRepresentation client = createRep();
OIDCClientRepresentation response = reg.oidc().create(client);
return response;
}
private void assertCreateFail(OIDCClientRepresentation client, int expectedStatusCode, String expectedErrorContains) {
try {
reg.oidc().create(client);
Assert.fail("Not expected to successfuly register client");
} catch (ClientRegistrationException expected) {
HttpErrorException httpEx = (HttpErrorException) expected.getCause();
Assert.assertEquals(expectedStatusCode, httpEx.getStatusLine().getStatusCode());
if (expectedErrorContains != null) {
assertTrue("Error response doesn't contain expected text", httpEx.getErrorResponse().contains(expectedErrorContains));
}
}
}
@Test
public void createPairwiseClient() throws Exception {
OIDCClientRepresentation clientRep = createRep();
clientRep.setSubjectType("pairwise");
OIDCClientRepresentation response = reg.oidc().create(clientRep);
Assert.assertEquals("pairwise", response.getSubjectType());
}
@Test
public void updateClientToPairwise() throws Exception {
OIDCClientRepresentation response = create();
Assert.assertEquals("public", response.getSubjectType());
reg.auth(Auth.token(response));
response.setSubjectType("pairwise");
OIDCClientRepresentation updated = reg.oidc().update(response);
Assert.assertEquals("pairwise", updated.getSubjectType());
}
@Test
public void updateSectorIdentifierUri() throws Exception {
OIDCClientRepresentation clientRep = createRep();
clientRep.setSubjectType("pairwise");
OIDCClientRepresentation response = reg.oidc().create(clientRep);
Assert.assertEquals("pairwise", response.getSubjectType());
Assert.assertNull(response.getSectorIdentifierUri());
reg.auth(Auth.token(response));
// Push redirect uris to the sector identifier URI
List<String> sectorRedirects = new ArrayList<>();
sectorRedirects.addAll(response.getRedirectUris());
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
oidcClientEndpointsResource.setSectorIdentifierRedirectUris(sectorRedirects);
response.setSectorIdentifierUri(TestApplicationResourceUrls.pairwiseSectorIdentifierUri());
OIDCClientRepresentation updated = reg.oidc().update(response);
Assert.assertEquals("pairwise", updated.getSubjectType());
Assert.assertEquals(TestApplicationResourceUrls.pairwiseSectorIdentifierUri(), updated.getSectorIdentifierUri());
}
@Test
public void updateToPairwiseThroughAdminRESTSuccess() throws Exception {
OIDCClientRepresentation response = create();
Assert.assertEquals("public", response.getSubjectType());
Assert.assertNull(response.getSectorIdentifierUri());
// Push redirect uris to the sector identifier URI
List<String> sectorRedirects = new ArrayList<>();
sectorRedirects.addAll(response.getRedirectUris());
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
oidcClientEndpointsResource.setSectorIdentifierRedirectUris(sectorRedirects);
String sectorIdentifierUri = TestApplicationResourceUrls.pairwiseSectorIdentifierUri();
// Add protocolMapper through admin REST endpoint
String clientId = response.getClientId();
ProtocolMapperRepresentation pairwiseProtMapper = SHA256PairwiseSubMapper.createPairwiseMapper(sectorIdentifierUri, null);
RealmResource realmResource = realmsResouce().realm("test");
ClientManager.realm(realmResource).clientId(clientId).addProtocolMapper(pairwiseProtMapper);
reg.auth(Auth.token(response));
OIDCClientRepresentation rep = reg.oidc().get(response.getClientId());
Assert.assertEquals("pairwise", rep.getSubjectType());
Assert.assertEquals(sectorIdentifierUri, rep.getSectorIdentifierUri());
}
@Test
public void updateToPairwiseThroughAdminRESTFailure() throws Exception {
OIDCClientRepresentation response = create();
Assert.assertEquals("public", response.getSubjectType());
Assert.assertNull(response.getSectorIdentifierUri());
// Push empty list to the sector identifier URI
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
oidcClientEndpointsResource.setSectorIdentifierRedirectUris(new ArrayList<>());
String sectorIdentifierUri = TestApplicationResourceUrls.pairwiseSectorIdentifierUri();
// Add protocolMapper through admin REST endpoint
String clientId = response.getClientId();
ProtocolMapperRepresentation pairwiseProtMapper = SHA256PairwiseSubMapper.createPairwiseMapper(sectorIdentifierUri, null);
RealmResource realmResource = realmsResouce().realm("test");
ClientResource clientResource = ApiUtil.findClientByClientId(realmsResouce().realm("test"), clientId);
Response resp = clientResource.getProtocolMappers().createMapper(pairwiseProtMapper);
Assert.assertEquals(400, resp.getStatus());
// Assert still public
reg.auth(Auth.token(response));
OIDCClientRepresentation rep = reg.oidc().get(response.getClientId());
Assert.assertEquals("public", rep.getSubjectType());
Assert.assertNull(rep.getSectorIdentifierUri());
}
@Test
public void createPairwiseClientWithSectorIdentifierURI() throws Exception {
OIDCClientRepresentation clientRep = createRep();
// Push redirect uris to the sector identifier URI
List<String> sectorRedirects = new ArrayList<>();
sectorRedirects.addAll(clientRep.getRedirectUris());
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
oidcClientEndpointsResource.setSectorIdentifierRedirectUris(sectorRedirects);
clientRep.setSubjectType("pairwise");
clientRep.setSectorIdentifierUri(TestApplicationResourceUrls.pairwiseSectorIdentifierUri());
OIDCClientRepresentation response = reg.oidc().create(clientRep);
Assert.assertEquals("pairwise", response.getSubjectType());
Assert.assertEquals(TestApplicationResourceUrls.pairwiseSectorIdentifierUri(), response.getSectorIdentifierUri());
}
@Test
public void createPairwiseClientWithRedirectsToMultipleHostsWithoutSectorIdentifierURI() throws Exception {
OIDCClientRepresentation clientRep = createRep();
List<String> redirects = new ArrayList<>();
redirects.add("http://redirect1");
redirects.add("http://redirect2");
clientRep.setSubjectType("pairwise");
clientRep.setRedirectUris(redirects);
assertCreateFail(clientRep, 400, "Without a configured Sector Identifier URI, client redirect URIs must not contain multiple host components.");
}
@Test
public void createPairwiseClientWithRedirectsToMultipleHosts() throws Exception {
OIDCClientRepresentation clientRep = createRep();
// Push redirect URIs to the sector identifier URI
List<String> redirects = new ArrayList<>();
redirects.add("http://redirect1");
redirects.add("http://redirect2");
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
oidcClientEndpointsResource.setSectorIdentifierRedirectUris(redirects);
clientRep.setSubjectType("pairwise");
clientRep.setSectorIdentifierUri(TestApplicationResourceUrls.pairwiseSectorIdentifierUri());
clientRep.setRedirectUris(redirects);
OIDCClientRepresentation response = reg.oidc().create(clientRep);
Assert.assertEquals("pairwise", response.getSubjectType());
Assert.assertEquals(TestApplicationResourceUrls.pairwiseSectorIdentifierUri(), response.getSectorIdentifierUri());
Assert.assertNames(response.getRedirectUris(), "http://redirect1", "http://redirect2");
}
@Test
public void createPairwiseClientWithSectorIdentifierURIContainingMismatchedRedirects() throws Exception {
OIDCClientRepresentation clientRep = createRep();
// Push redirect uris to the sector identifier URI
List<String> sectorRedirects = new ArrayList<>();
sectorRedirects.add("http://someotherredirect");
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
oidcClientEndpointsResource.setSectorIdentifierRedirectUris(sectorRedirects);
clientRep.setSubjectType("pairwise");
clientRep.setSectorIdentifierUri(TestApplicationResourceUrls.pairwiseSectorIdentifierUri());
assertCreateFail(clientRep, 400, "Client redirect URIs does not match redirect URIs fetched from the Sector Identifier URI.");
}
@Test
public void createPairwiseClientWithSectorIdentifierURIContainingMismatchedRedirectsPublicSubject() throws Exception {
OIDCClientRepresentation clientRep = createRep();
// Push redirect uris to the sector identifier URI
List<String> sectorRedirects = new ArrayList<>();
sectorRedirects.add("http://someotherredirect");
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
oidcClientEndpointsResource.setSectorIdentifierRedirectUris(sectorRedirects);
clientRep.setSubjectType("public");
clientRep.setSectorIdentifierUri(TestApplicationResourceUrls.pairwiseSectorIdentifierUri());
assertCreateFail(clientRep, 400, "Client redirect URIs does not match redirect URIs fetched from the Sector Identifier URI.");
}
@Test
public void createPairwiseClientWithInvalidSectorIdentifierURI() throws Exception {
OIDCClientRepresentation clientRep = createRep();
clientRep.setSubjectType("pairwise");
clientRep.setSectorIdentifierUri("malformed");
assertCreateFail(clientRep, 400, "Invalid Sector Identifier URI.");
}
@Test
public void createPairwiseClientWithUnreachableSectorIdentifierURI() throws Exception {
OIDCClientRepresentation clientRep = createRep();
clientRep.setSubjectType("pairwise");
clientRep.setSectorIdentifierUri("http://localhost/dummy");
assertCreateFail(clientRep, 400, "Failed to get redirect URIs from the Sector Identifier URI.");
}
@Test
public void loginUserToPairwiseClient() throws Exception {
// Create public client
OIDCClientRepresentation publicClient = create();
// Login to public client
oauth.clientId(publicClient.getClientId());
OAuthClient.AuthorizationEndpointResponse loginResponse = oauth.doLogin("test-user@localhost", "password");
OAuthClient.AccessTokenResponse accessTokenResponse = oauth.doAccessTokenRequest(loginResponse.getCode(), publicClient.getClientSecret());
AccessToken accessToken = oauth.verifyToken(accessTokenResponse.getAccessToken());
Assert.assertEquals("test-user", accessToken.getPreferredUsername());
Assert.assertEquals("test-user@localhost", accessToken.getEmail());
String tokenUserId = accessToken.getSubject();
// Assert public client has same subject like userId
UserRepresentation user = realmsResouce().realm("test").users().search("test-user", 0, 1).get(0);
Assert.assertEquals(user.getId(), tokenUserId);
// Create pairwise client
OIDCClientRepresentation clientRep = createRep();
clientRep.setSubjectType("pairwise");
OIDCClientRepresentation pairwiseClient = reg.oidc().create(clientRep);
Assert.assertEquals("pairwise", pairwiseClient.getSubjectType());
// Login to pairwise client
oauth.clientId(pairwiseClient.getClientId());
oauth.openLoginForm();
loginResponse = new OAuthClient.AuthorizationEndpointResponse(oauth);
accessTokenResponse = oauth.doAccessTokenRequest(loginResponse.getCode(), pairwiseClient.getClientSecret());
accessToken = oauth.verifyToken(accessTokenResponse.getAccessToken());
Assert.assertEquals("test-user", accessToken.getPreferredUsername());
Assert.assertEquals("test-user@localhost", accessToken.getEmail());
// Assert pairwise client has different subject like userId
String pairwiseUserId = accessToken.getSubject();
Assert.assertNotEquals(pairwiseUserId, user.getId());
}
}

View file

@ -316,6 +316,19 @@ public abstract class AbstractKeycloakIdentityProviderTest extends AbstractIdent
}
}
/**
* Test for KEYCLOAK-3505 - Verify the claims from the claim set returned by the OIDC UserInfo are correctly mapped
* by the user attribute mapper
*
*/
protected void verifyAttributeMapperHandlesUserInfoClaims() {
IdentityProviderModel identityProviderModel = getIdentityProviderModel();
setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_ON);
UserModel user = assertSuccessfulAuthentication(identityProviderModel, "test-user", "new@email.com", true);
Assert.assertEquals("A00", user.getFirstAttribute("tenantid"));
}
@Test
public void testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername() {
RealmModel realm = getRealm();

View file

@ -100,6 +100,16 @@ public class OIDCBrokerUserPropertyTest extends AbstractKeycloakIdentityProvider
}
}
/**
* Test for KEYCLOAK-3505 - Verify the claims from the claim set returned by the OIDC UserInfo are correctly mapped
* by the user attribute mapper
*
*/
@Test
public void testSuccessfulAuthentication_verifyAttributeMapperHandlesUserInfoClaims() {
verifyAttributeMapperHandlesUserInfoClaims();
}
@Override
@Test
public void testSuccessfulAuthenticationWithoutUpdateProfile() {

View file

@ -17,6 +17,20 @@
"http://localhost:8081/auth/realms/realm-with-broker/broker/kc-oidc-idp-property-mappers/endpoint/*"
],
"protocolMappers": [
{
"name": "tenantid",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"user.attribute": "tenantid",
"claim.name": "tenantid",
"Claim JSON Type": "String",
"access.token.claim": "false",
"id.token.claim": "false",
"userinfo.token.claim": "true"
}
},
{
"name": "mobile",
"protocol": "openid-connect",
@ -109,7 +123,8 @@
],
"realmRoles": ["manager"],
"attributes": {
"mobile": "617-666-7777"
"mobile": "617-666-7777",
"tenantid": "A00"
}
},
{

View file

@ -243,6 +243,15 @@
"claim": "family_name"
}
},
{
"name": "kc-tenantid-mapper",
"identityProviderAlias": "kc-oidc-idp-property-mappers",
"identityProviderMapper": "oidc-user-attribute-idp-mapper",
"config": {
"user.attribute": "tenantid",
"claim": "tenantid"
}
},
{
"name": "manager-mapper",
"identityProviderAlias": "kc-oidc-idp",

View file

@ -1774,6 +1774,12 @@ module.controller('ClientProtocolMapperCreateCtrl', function($scope, realm, serv
var id = l.substring(l.lastIndexOf("/") + 1);
$location.url("/realms/" + realm.realm + '/clients/' + client.id + "/mappers/" + id);
Notifications.success("Mapper has been created.");
}, function(error) {
if (error.status == 400 && error.data.error_description) {
Notifications.error(error.data.error_description);
} else {
Notifications.error('Unexpected error when updating protocol mapper');
}
});
};