Merge pull request #3257 from mposolda/pairwise
KEYCLOAK-3422 Pairwise subjects : few fixes and bit of refactoring
This commit is contained in:
commit
5fc7149aac
19 changed files with 629 additions and 295 deletions
|
@ -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) {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
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);
|
||||
pairwise.setConfig(config);
|
||||
return pairwise;
|
||||
if (salt == null) {
|
||||
salt = KeycloakModelUtils.generateId();
|
||||
}
|
||||
|
||||
public static ProtocolMapperModel 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);
|
||||
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
|
|
@ -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;
|
||||
|
||||
if (mappers != null) {
|
||||
client.getProtocolMappers().stream().filter((ProtocolMapperRepresentation mapping) -> {
|
||||
return mapping.getProtocolMapper().endsWith(AbstractPairwiseSubMapper.PROVIDER_ID_SUFFIX);
|
||||
}).forEach((ProtocolMapperRepresentation mapping) -> {
|
||||
pairwiseMappers.add(mapping);
|
||||
});
|
||||
}
|
||||
|
||||
public static String getSubjectIdentifierUri(ProtocolMapperRepresentation pairwiseMapper) {
|
||||
return pairwiseMapper.getConfig().get(PairwiseSubMapperHelper.SECTOR_IDENTIFIER_URI);
|
||||
return pairwiseMappers;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<>();
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue