Merge pull request #3231 from TeliaSoneraNorge/pr/KEYCLOAK-3422
KEYCLOAK-3422 support pairwise subject identifier in oidc
This commit is contained in:
commit
5afe93552a
31 changed files with 995 additions and 103 deletions
|
@ -17,17 +17,6 @@
|
|||
|
||||
package org.keycloak;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAnyGetter;
|
||||
import com.fasterxml.jackson.annotation.JsonAnySetter;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonUnwrapped;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.representations.IDToken;
|
||||
|
@ -36,6 +25,13 @@ import org.keycloak.representations.adapters.config.AdapterConfig;
|
|||
import org.keycloak.representations.oidc.OIDCClientRepresentation;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
|
@ -138,6 +134,18 @@ public class JsonParserTest {
|
|||
Assert.assertNull(clientRep.getJwks());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadOIDCClientRepWithPairwise() throws IOException {
|
||||
String stringRep = "{\"subject_type\": \"pairwise\", \"jwks_uri\": \"https://op.certification.openid.net:60720/export/jwk_60720.json\", \"contacts\": [\"roland.hedberg@umu.se\"], \"application_type\": \"web\", \"grant_types\": [\"authorization_code\"], \"post_logout_redirect_uris\": [\"https://op.certification.openid.net:60720/logout\"], \"redirect_uris\": [\"https://op.certification.openid.net:60720/authz_cb\"], \"response_types\": [\"code\"], \"require_auth_time\": true, \"default_max_age\": 3600}";
|
||||
OIDCClientRepresentation clientRep = JsonSerialization.readValue(stringRep, OIDCClientRepresentation.class);
|
||||
Assert.assertEquals("pairwise", clientRep.getSubjectType());
|
||||
Assert.assertTrue(clientRep.getRequireAuthTime());
|
||||
Assert.assertEquals(3600, clientRep.getDefaultMaxAge().intValue());
|
||||
Assert.assertEquals(1, clientRep.getRedirectUris().size());
|
||||
Assert.assertEquals("https://op.certification.openid.net:60720/authz_cb", clientRep.getRedirectUris().get(0));
|
||||
Assert.assertNull(clientRep.getJwks());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadOIDCClientRepWithJWKS() throws IOException {
|
||||
String stringRep = "{\"token_endpoint_auth_method\": \"private_key_jwt\", \"subject_type\": \"public\", \"jwks_uri\": null, \"jwks\": {\"keys\": [{\"use\": \"enc\", \"e\": \"AQAB\", \"d\": \"lZQv0_81euRLeUYU84Aodh0ar7ymDlzWP5NMra4Jklkb-lTBWkI-u4RMsPqGYyW3KHRoL_pgzZXSzQx8RLQfER6timRWb--NxMMKllZubByU3RqH2ooNuocJurspYiXkznPW1Mg9DaNXL0C2hwWPQHTeUVISpjgi5TCOV1ccWVyksFruya_VNL1CIByB-L0GL1rqbKv32cDwi2A3_jJa61cpzfLSIBe-lvCO6tuiDsR4qgJnUwnndQFwEI_4mLmD3iNWXrc8N-poleV8mBfMqBB5fWwy_ZTFCpmQ5AywGmctaik_wNhMoWuA4tUfY6_1LdKld-5Cjq55eLtuJjtvuQ\", \"n\": \"tx3Hjdbc19lkTiohbJrNj4jf2_90MEE122CRrwtFu6saDywKcG7Bi7w2FMAK2oTkuWfqhWRb5BEGmnSXdiCEPO5d-ytqP3nwlZXHaCDYscpP8bB4YLhvCn7R8Efw6gwQle24QPRP3lYoFeuUbDUq7GKA5SfaZUvWoeWjqyLIaBspKQsC26_Umx1E4IXLrMSL6nkRnrYcVZBAXrYCeTP1XtsV38_lZVJfHSaJaUy4PKaj3yvgm93EV2CXybPti7CCMXZ34VqqWiF64pQjZsPu3ZTr7ha_TTQq499-zYRQNDvIVsBDLQQIgrbctuGqj6lrXb31Jj3JIEYqH_4h5X9d0Q\", \"q\": \"1q-r-bmMFbIzrLK2U3elksZq8CqUqZxlSfkGMZuVkxgYMS-e4FPzEp2iirG-eO11aa0cpMMoBdTnVdGJ_ZUR93w0lGf9XnQAJqxP7eOsrUoiW4VWlWH4WfOiLgpO-pFtyTz_JksYYaotc_Z3Zy-Szw6a39IDbuYGy1qL-15oQuc\", \"p\": \"2lrYPppRbcQWu4LtWN6tOVUrtCOPv1eLTKTc7q8vCMcem1Ox5QFB7KnUtNZ5Ni7wnZUeVDfimNebtjNsGvDSrpgIlo9dEnFBQsQIkzZ2SkoYfgmF8hNdi6P-BfRjdgYouy4c6xAnGDgSMTip1YnPRyvbMaoYT9E_tEcBW5wOeoc\", \"kid\": \"a0\", \"kty\": \"RSA\"}, {\"use\": \"sig\", \"e\": \"AQAB\", \"d\": \"DodXDEtkovWWGsMEXYy_nEEMCWyROMOebCnCv0ey3i4M4bh2dmwqgz0e-IKQAFlGiMkidGL1lNbq0uFS04FbuRAR06dYw1cbrNbDdhrWFxKTd1L5D9p-x-gW-YDWhpI8rUGRa76JXkOSxZUbg09_QyUd99CXAHh-FXi_ZkIKD8hK6FrAs68qhLf8MNkUv63DTduw7QgeFfQivdopePxyGuMk5n8veqwsUZsklQkhNlTYQqeM1xb2698ZQcNYkl0OssEsSJKRjXt-LRPowKrdvTuTo2p--HMI0pIEeFs7H_u5OW3jihjvoFClGPynHQhgWmQzlQRvWRXh6FhDVqFeGQ\", \"n\": \"zfZzttF7HmnTYwSMPdxKs5AoczbNS2mOPz-tN1g4ljqI_F1DG8cgQDcN_VDufxoFGRERo2FK6WEN41LhbGEyP6uL6wW6Cy29qE9QZcvY5mXrncndRSOkNcMizvuEJes_fMYrmP_lPiC6kWiqItTk9QBWqJfiYKhCx9cSDXsBmJXn3KWQCVHvj1ANFWW0CWLMKlWN-_NMNLIWJN_pEAocTZMzxSFBK1b5_5J8ZS7hfWRF6MQmjsJcz2jzA21SQZNpre3kwnTGRSwo05sAS-TyeadDqQPWgbqX69UzcGq5irhzN8cpZ_JaTk3Y_uV6owanTZLVvCgdjaAnMYeZhb0KFw\", \"q\": \"5E5XKK5njT-zzRqqTeY2tgP9PJBACeaH_xQRHZ_1ydE7tVd7HdgdaEHfQ1jvKIHFkknWWOBAY1mlBc4YDirLShB_voShD8C-Hx3nF5sne5fleVfU-sZy6Za4B2U75PcE62oZgCPauOTAEm9Xuvrt5aMMovyzR8ecJZhm9bw7naU\", \"p\": \"5vJHCSM3H3q4RltYzENC9RyZZV8EUmpkv9moyguT5t-BUGA-T4W_FGIxzOPXRWOckIplKkoDKhavUeNmTZMCUcue0nkICSJpvNE4Nb2p5PZk_QqSdQNvCasQtdojEG0AmfVD85SU551CYxJdLdDFOqyK2entpMr8lhokem189As\", \"kid\": \"a1\", \"kty\": \"RSA\"}, {\"d\": \"S4_OufhLBgXFMgIDMI1zlVe2uCExpcEAQ80J_lXfS8I\", \"use\": \"sig\", \"crv\": \"P-256\", \"kty\": \"EC\", \"y\": \"DBdNyq30mXmUs_BIvKMqaTTNO7HDhCi0YiC8GciwNYk\", \"x\": \"cYwzBoyjRjxj334bRTqanONf7DUYK-6TgiuN0DixJAk\", \"kid\": \"a2\"}, {\"d\": \"33TnYgdJtWAiVosKqUnz0zSmvWTbsx5-6pceynW6Xck\", \"use\": \"enc\", \"crv\": \"P-256\", \"kty\": \"EC\", \"y\": \"Cula95Eix1Ia77St3OULe6-UKWs5I06nmdfUzhXUQTs\", \"x\": \"wk8HBVxNNzj1gJBxPmmx9XYW1L61ObBGzxpRa6_OqWU\", \"kid\": \"a3\"}]}, \"application_type\": \"web\", \"contacts\": [\"roland.hedberg@umu.se\"], \"post_logout_redirect_uris\": [\"https://op.certification.openid.net:60784/logout\"], \"redirect_uris\": [\"https://op.certification.openid.net:60784/authz_cb\"], \"response_types\": [\"code\"], \"require_auth_time\": true, \"grant_types\": [\"authorization_code\"], \"default_max_age\": 3600}";
|
||||
|
|
|
@ -1041,6 +1041,8 @@ public class RepresentationToModel {
|
|||
client.updateDefaultRoles(resourceRep.getDefaultRoles());
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (resourceRep.getProtocolMappers() != null) {
|
||||
// first, remove all default/built in mappers
|
||||
Set<ProtocolMapperModel> mappers = client.getProtocolMappers();
|
||||
|
@ -1049,6 +1051,9 @@ public class RepresentationToModel {
|
|||
for (ProtocolMapperRepresentation mapper : resourceRep.getProtocolMappers()) {
|
||||
client.addProtocolMapper(toModel(mapper));
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
if (resourceRep.getClientTemplate() != null) {
|
||||
|
|
|
@ -22,21 +22,42 @@ package org.keycloak.protocol;
|
|||
*/
|
||||
public class ProtocolMapperConfigException extends Exception {
|
||||
|
||||
private String messageKey;
|
||||
private Object[] parameters;
|
||||
|
||||
public ProtocolMapperConfigException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ProtocolMapperConfigException(String message, String messageKey) {
|
||||
super(message);
|
||||
this.messageKey = messageKey;
|
||||
}
|
||||
|
||||
public ProtocolMapperConfigException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public ProtocolMapperConfigException(String message, String messageKey, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.messageKey = messageKey;
|
||||
}
|
||||
|
||||
public ProtocolMapperConfigException(String message, Object ... parameters) {
|
||||
super(message);
|
||||
this.parameters = parameters;
|
||||
}
|
||||
|
||||
public ProtocolMapperConfigException(String messageKey, String message, Object ... parameters) {
|
||||
super(message);
|
||||
this.messageKey = messageKey;
|
||||
this.parameters = parameters;
|
||||
}
|
||||
|
||||
public String getMessageKey() {
|
||||
return messageKey;
|
||||
}
|
||||
|
||||
public Object[] getParameters() {
|
||||
return parameters;
|
||||
}
|
||||
|
|
|
@ -17,12 +17,13 @@
|
|||
|
||||
package org.keycloak.protocol.oidc;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
|
@ -32,6 +33,11 @@ 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;
|
||||
|
||||
|
@ -74,6 +80,27 @@ 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) {
|
||||
|
|
|
@ -19,31 +19,15 @@ package org.keycloak.protocol.oidc;
|
|||
import org.keycloak.common.constants.KerberosConstants;
|
||||
import org.keycloak.common.util.UriUtils;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientTemplateModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ProtocolMapperModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.*;
|
||||
import org.keycloak.protocol.AbstractLoginProtocolFactory;
|
||||
import org.keycloak.protocol.LoginProtocol;
|
||||
import org.keycloak.protocol.oidc.mappers.AddressMapper;
|
||||
import org.keycloak.protocol.oidc.mappers.FullNameMapper;
|
||||
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
|
||||
import org.keycloak.protocol.oidc.mappers.UserPropertyMapper;
|
||||
import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper;
|
||||
import org.keycloak.protocol.oidc.mappers.*;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.ClientTemplateRepresentation;
|
||||
import org.keycloak.services.ServicesLogger;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.keycloak.protocol.oidc.mappers.UserAttributeMapper;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
|
@ -146,6 +130,9 @@ 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",
|
||||
|
|
|
@ -56,7 +56,7 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
|
|||
|
||||
public static final List<String> DEFAULT_RESPONSE_TYPES_SUPPORTED = list(OAuth2Constants.CODE, OIDCResponseType.NONE, OIDCResponseType.ID_TOKEN, OIDCResponseType.TOKEN, "id_token token", "code id_token", "code token", "code id_token token");
|
||||
|
||||
public static final List<String> DEFAULT_SUBJECT_TYPES_SUPPORTED = list("public");
|
||||
public static final List<String> DEFAULT_SUBJECT_TYPES_SUPPORTED = list("public", "pairwise");
|
||||
|
||||
public static final List<String> DEFAULT_RESPONSE_MODES_SUPPORTED = list("query", "fragment", "form_post");
|
||||
|
||||
|
|
|
@ -19,10 +19,9 @@ package org.keycloak.protocol.oidc.endpoints;
|
|||
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.jboss.resteasy.spi.HttpResponse;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.RSATokenVerifier;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
|
@ -30,20 +29,15 @@ import org.keycloak.events.EventBuilder;
|
|||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.jose.jws.Algorithm;
|
||||
import org.keycloak.jose.jws.JWSBuilder;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.*;
|
||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||
import org.keycloak.protocol.oidc.TokenManager;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.services.ErrorResponseException;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.services.managers.AppAuthManager;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.resources.Cors;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.utils.MediaType;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
|
@ -54,7 +48,6 @@ import javax.ws.rs.core.Context;
|
|||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
import java.security.PrivateKey;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
@ -179,8 +172,8 @@ public class UserInfoEndpoint {
|
|||
tokenManager.transformUserInfoAccessToken(session, userInfo, realm, clientModel, userModel, userSession, clientSession);
|
||||
|
||||
Map<String, Object> claims = new HashMap<String, Object>();
|
||||
claims.putAll(userInfo.getOtherClaims());
|
||||
claims.put("sub", userModel.getId());
|
||||
claims.putAll(userInfo.getOtherClaims());
|
||||
|
||||
Response.ResponseBuilder responseBuilder;
|
||||
OIDCAdvancedConfigWrapper cfg = OIDCAdvancedConfigWrapper.fromClientModel(clientModel);
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
package org.keycloak.protocol.oidc.mappers;
|
||||
|
||||
import org.keycloak.models.*;
|
||||
import org.keycloak.protocol.ProtocolMapperConfigException;
|
||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||
import org.keycloak.protocol.oidc.utils.PairwiseSubMapperValidator;
|
||||
import org.keycloak.protocol.oidc.utils.PairwiseSubMapperUtils;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.IDToken;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Set the 'sub' claim to pairwise .
|
||||
*
|
||||
* @author <a href="mailto:martin.hardselius@gmail.com">Martin Hardselius</a>
|
||||
*/
|
||||
public abstract class AbstractPairwiseSubMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper, UserInfoTokenMapper {
|
||||
public static final String PROVIDER_ID_SUFFIX = "-pairwise-sub-mapper";
|
||||
|
||||
public static String getId(String prefix) {
|
||||
return prefix + PROVIDER_ID_SUFFIX;
|
||||
}
|
||||
|
||||
public abstract String getIdPrefix();
|
||||
|
||||
/**
|
||||
* Generates a pairwise subject identifier.
|
||||
*
|
||||
* @param mappingModel
|
||||
* @param sectorIdentifier client sector identifier
|
||||
* @param localSub local subject identifier (user id)
|
||||
* @return A pairwise subject identifier
|
||||
*/
|
||||
public abstract String generateSub(ProtocolMapperModel mappingModel, String sectorIdentifier, String localSub);
|
||||
|
||||
/**
|
||||
* Override to add additional provider configuration properties. By default, a pairwise sub mapper will only contain configuration for a sector identifier URI.
|
||||
*
|
||||
* @return A list of provider configuration properties.
|
||||
*/
|
||||
public List<ProviderConfigProperty> getAdditionalConfigProperties() {
|
||||
return new LinkedList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to add additional configuration validation. Called when instance of mapperModel is created/updated for this protocolMapper through admin endpoint.
|
||||
*
|
||||
* @param session
|
||||
* @param realm
|
||||
* @param mapperContainer client or clientTemplate
|
||||
* @param mapperModel
|
||||
* @throws ProtocolMapperConfigException if configuration provided in mapperModel is not valid
|
||||
*/
|
||||
public void validateAdditionalConfig(KeycloakSession session, RealmModel realm, ProtocolMapperContainerModel mapperContainer, ProtocolMapperModel mapperModel) throws ProtocolMapperConfigException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String getDisplayCategory() {
|
||||
return AbstractOIDCProtocolMapper.TOKEN_MAPPER_CATEGORY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final IDToken transformIDToken(IDToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
|
||||
setSubject(token, generateSub(mappingModel, getSectorIdentifier(clientSession.getClient(), mappingModel), userSession.getUser().getId()));
|
||||
return token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
|
||||
setSubject(token, generateSub(mappingModel, getSectorIdentifier(clientSession.getClient(), mappingModel), userSession.getUser().getId()));
|
||||
return token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final AccessToken transformUserInfoToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
|
||||
setSubject(token, generateSub(mappingModel, getSectorIdentifier(clientSession.getClient(), mappingModel), userSession.getUser().getId()));
|
||||
return token;
|
||||
}
|
||||
|
||||
private void setSubject(IDToken token, String pairwiseSub) {
|
||||
token.getOtherClaims().put("sub", pairwiseSub);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final List<ProviderConfigProperty> getConfigProperties() {
|
||||
List<ProviderConfigProperty> configProperties = new LinkedList<>();
|
||||
configProperties.add(PairwiseSubMapperHelper.createSectorIdentifierConfig());
|
||||
configProperties.addAll(getAdditionalConfigProperties());
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
private String getSectorIdentifier(ClientModel client, ProtocolMapperModel mappingModel) {
|
||||
String sectorIdentifierUri = PairwiseSubMapperHelper.getSectorIdentifierUri(mappingModel);
|
||||
if (sectorIdentifierUri != null && !sectorIdentifierUri.isEmpty()) {
|
||||
return PairwiseSubMapperUtils.resolveValidSectorIdentifier(sectorIdentifierUri);
|
||||
}
|
||||
return PairwiseSubMapperUtils.resolveValidSectorIdentifier(client.getRootUrl(), client.getRedirectUris());
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void validateConfig(KeycloakSession session, RealmModel realm, ProtocolMapperContainerModel mapperContainer, ProtocolMapperModel mapperModel) throws ProtocolMapperConfigException {
|
||||
ClientModel client = null;
|
||||
if (mapperContainer instanceof ClientModel) {
|
||||
client = (ClientModel) mapperContainer;
|
||||
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
|
||||
public final String getId() {
|
||||
return getIdPrefix() + PROVIDER_ID_SUFFIX;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package org.keycloak.protocol.oidc.mappers;
|
||||
|
||||
import org.keycloak.models.ProtocolMapperModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.services.ServicesLogger;
|
||||
|
||||
public class PairwiseSubMapperHelper {
|
||||
private static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
|
||||
|
||||
public static final String SECTOR_IDENTIFIER_URI = "sectorIdentifierUri";
|
||||
public static final String SECTOR_IDENTIFIER_URI_LABEL = "sectorIdentifierUri.label";
|
||||
public static final String SECTOR_IDENTIFIER_URI_HELP_TEXT = "sectorIdentifierUri.tooltip";
|
||||
|
||||
public static final String PAIRWISE_SUB_ALGORITHM_SALT = "pairwiseSubAlgorithmSalt";
|
||||
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(ProtocolMapperModel mappingModel) {
|
||||
return mappingModel.getConfig().get(SECTOR_IDENTIFIER_URI);
|
||||
}
|
||||
|
||||
public static String getSalt(ProtocolMapperModel mappingModel) {
|
||||
return mappingModel.getConfig().get(PAIRWISE_SUB_ALGORITHM_SALT);
|
||||
}
|
||||
|
||||
public static void setSalt(ProtocolMapperModel mappingModel, String salt) {
|
||||
mappingModel.getConfig().put(PAIRWISE_SUB_ALGORITHM_SALT, salt);
|
||||
}
|
||||
|
||||
public static ProviderConfigProperty createSectorIdentifierConfig() {
|
||||
ProviderConfigProperty property = new ProviderConfigProperty();
|
||||
property.setName(SECTOR_IDENTIFIER_URI);
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
property.setLabel(SECTOR_IDENTIFIER_URI_LABEL);
|
||||
property.setHelpText(SECTOR_IDENTIFIER_URI_HELP_TEXT);
|
||||
return property;
|
||||
}
|
||||
|
||||
public static ProviderConfigProperty createSaltConfig() {
|
||||
ProviderConfigProperty property = new ProviderConfigProperty();
|
||||
property.setName(PAIRWISE_SUB_ALGORITHM_SALT);
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
property.setLabel(PAIRWISE_SUB_ALGORITHM_SALT_LABEL);
|
||||
property.setHelpText(PAIRWISE_SUB_ALGORITHM_SALT_HELP_TEXT);
|
||||
return property;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
package org.keycloak.protocol.oidc.mappers;
|
||||
|
||||
import org.keycloak.models.ProtocolMapperModel;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
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 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 {
|
||||
charset = Charset.forName("UTF-8");
|
||||
MessageDigest.getInstance(HASH_ALGORITHM);
|
||||
}
|
||||
|
||||
public static ProtocolMapperModel createPairwiseMapper() {
|
||||
return createPairwiseMapper(null);
|
||||
}
|
||||
|
||||
public static ProtocolMapperModel createPairwiseMapper(String sectorIdentifierUri) {
|
||||
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();
|
||||
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 String getHelpText() {
|
||||
return "Calculates a pairwise subject identifier using a salted sha-256 hash.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getAdditionalConfigProperties() {
|
||||
List<ProviderConfigProperty> configProperties = new LinkedList<>();
|
||||
configProperties.add(PairwiseSubMapperHelper.createSaltConfig());
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateSub(ProtocolMapperModel mappingModel, String sectorIdentifier, String localSub) {
|
||||
String saltStr = getSalt(mappingModel);
|
||||
|
||||
Charset charset = Charset.forName("UTF-8");
|
||||
byte[] salt = saltStr.getBytes(charset);
|
||||
String pairwiseSub = generateSub(sectorIdentifier, localSub, salt);
|
||||
logger.infof("local sub = '%s', pairwise sub = '%s'", localSub, pairwiseSub);
|
||||
return pairwiseSub;
|
||||
}
|
||||
|
||||
private String generateSub(String sectorIdentifier, String localSub, byte[] salt) {
|
||||
MessageDigest sha256;
|
||||
try {
|
||||
sha256 = MessageDigest.getInstance(HASH_ALGORITHM);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException(e.getMessage(), e);
|
||||
}
|
||||
sha256.update(sectorIdentifier.getBytes(charset));
|
||||
sha256.update(localSub.getBytes(charset));
|
||||
byte[] hash = sha256.digest(salt);
|
||||
return UUID.nameUUIDFromBytes(hash).toString();
|
||||
}
|
||||
|
||||
private 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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Pairwise subject identifier";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIdPrefix() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
package org.keycloak.protocol.oidc.utils;
|
||||
|
||||
import org.keycloak.protocol.oidc.mappers.AbstractPairwiseSubMapper;
|
||||
import org.keycloak.protocol.oidc.mappers.PairwiseSubMapperHelper;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||
import org.keycloak.services.ServicesLogger;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class PairwiseSubMapperUtils {
|
||||
private static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
|
||||
|
||||
/**
|
||||
* Returns a set of valid redirect URIs from the root url and redirect URIs registered on a client.
|
||||
*
|
||||
* @param clientRootUrl
|
||||
* @param clientRedirectUris
|
||||
* @return
|
||||
*/
|
||||
public static Set<String> resolveValidRedirectUris(String clientRootUrl, Set<String> clientRedirectUris) {
|
||||
Set<String> validRedirects = new HashSet<String>();
|
||||
for (String redirectUri : clientRedirectUris) {
|
||||
if (redirectUri.startsWith("/")) {
|
||||
redirectUri = relativeToAbsoluteURI(clientRootUrl, redirectUri);
|
||||
logger.debugv("replacing relative valid redirect with: {0}", redirectUri);
|
||||
}
|
||||
if (redirectUri != null) {
|
||||
validRedirects.add(redirectUri);
|
||||
}
|
||||
}
|
||||
return validRedirects.stream()
|
||||
.filter(r -> r != null && !r.trim().isEmpty())
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to resolve a valid sector identifier from a sector identifier URI.
|
||||
*
|
||||
* @param sectorIdentifierUri
|
||||
* @return a sector identifier iff. the sector identifier URI is a valid URI, contains a valid scheme and contains a valid host component.
|
||||
*/
|
||||
public static String resolveValidSectorIdentifier(String sectorIdentifierUri) {
|
||||
URI uri;
|
||||
try {
|
||||
uri = new URI(sectorIdentifierUri);
|
||||
} catch (URISyntaxException e) {
|
||||
logger.debug("Invalid sector identifier URI", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (uri.getScheme() == null) {
|
||||
logger.debugv("Invalid sector identifier URI: {0}", sectorIdentifierUri);
|
||||
return null;
|
||||
}
|
||||
|
||||
/*if (!uri.getScheme().equalsIgnoreCase("https")) {
|
||||
logger.debugv("The sector identifier URI scheme must be HTTPS. Was '{0}'", uri.getScheme());
|
||||
return null;
|
||||
}*/
|
||||
|
||||
if (uri.getHost() == null) {
|
||||
logger.debug("The sector identifier URI must specify a host");
|
||||
return null;
|
||||
}
|
||||
|
||||
return uri.getHost();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to resolve a valid sector identifier from the redirect URIs registered on a client.
|
||||
*
|
||||
* @param clientRootUrl Root url registered on the client.
|
||||
* @param clientRedirectUris Redirect URIs registered on the client.
|
||||
* @return a sector identifier iff. all the registered redirect URIs are located at the same host, otherwise {@code null}.
|
||||
*/
|
||||
public static String resolveValidSectorIdentifier(String clientRootUrl, Set<String> clientRedirectUris) {
|
||||
Set<String> hosts = new HashSet<>();
|
||||
for (String redirectUri : resolveValidRedirectUris(clientRootUrl, clientRedirectUris)) {
|
||||
try {
|
||||
URI uri = new URI(redirectUri);
|
||||
hosts.add(uri.getHost());
|
||||
} catch (URISyntaxException e) {
|
||||
logger.debugv("client redirect uris contained an invalid uri: {0}", redirectUri);
|
||||
}
|
||||
}
|
||||
if (hosts.isEmpty()) {
|
||||
logger.debug("could not infer any valid sector_identifiers from client redirect uris");
|
||||
return null;
|
||||
}
|
||||
if (hosts.size() > 1) {
|
||||
logger.debug("the client redirect uris contained multiple hosts");
|
||||
return null;
|
||||
}
|
||||
return hosts.iterator().next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the the registered client redirect URIs matches the set of redirect URIs from the sector identifier URI.
|
||||
*
|
||||
* @param clientRootUrl root url registered on the client.
|
||||
* @param clientRedirectUris redirect URIs registered on the client.
|
||||
* @param sectorRedirects value of the sector identifier URI.
|
||||
* @return {@code true} iff. the all the redirect URIs can be described by the {@code sectorRedirects}, i.e if the registered redirect URIs is a subset of the {@code sectorRedirects}, otherwise {@code false}.
|
||||
*/
|
||||
public static boolean matchesRedirects(String clientRootUrl, Set<String> clientRedirectUris, Set<String> sectorRedirects) {
|
||||
Set<String> validRedirects = resolveValidRedirectUris(clientRootUrl, clientRedirectUris);
|
||||
for (String redirect : validRedirects) {
|
||||
if (!matchesRedirect(sectorRedirects, redirect)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean matchesRedirect(Set<String> validRedirects, String redirect) {
|
||||
for (String validRedirect : validRedirects) {
|
||||
if (validRedirect.endsWith("*") && !validRedirect.contains("?")) {
|
||||
// strip off the query component - we don't check them when wildcards are effective
|
||||
String r = redirect.contains("?") ? redirect.substring(0, redirect.indexOf("?")) : redirect;
|
||||
// strip off *
|
||||
int length = validRedirect.length() - 1;
|
||||
validRedirect = validRedirect.substring(0, length);
|
||||
if (r.startsWith(validRedirect)) return true;
|
||||
// strip off trailing '/'
|
||||
if (length - 1 > 0 && validRedirect.charAt(length - 1) == '/') length--;
|
||||
validRedirect = validRedirect.substring(0, length);
|
||||
if (validRedirect.equals(r)) return true;
|
||||
} else if (validRedirect.equals(redirect)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String relativeToAbsoluteURI(String rootUrl, String relative) {
|
||||
if (rootUrl == null || rootUrl.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
relative = rootUrl + relative;
|
||||
return relative;
|
||||
}
|
||||
|
||||
public static ProtocolMapperRepresentation getPairwiseSubMapperRepresentation(ClientRepresentation client) {
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
package org.keycloak.protocol.oidc.utils;
|
||||
|
||||
import org.keycloak.connections.httpclient.HttpClientProvider;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ProtocolMapperModel;
|
||||
import org.keycloak.protocol.ProtocolMapperConfigException;
|
||||
import org.keycloak.protocol.oidc.mappers.PairwiseSubMapperHelper;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:martin.hardselius@gmail.com">Martin Hardselius</a>
|
||||
*/
|
||||
public class PairwiseSubMapperValidator {
|
||||
|
||||
public static final String PAIRWISE_MALFORMED_CLIENT_REDIRECT_URI = "pairwiseMalformedClientRedirectURI";
|
||||
public static final String PAIRWISE_CLIENT_REDIRECT_URIS_MISSING_HOST = "pairwiseClientRedirectURIsMissingHost";
|
||||
public static final String PAIRWISE_CLIENT_REDIRECT_URIS_MULTIPLE_HOSTS = "pairwiseClientRedirectURIsMultipleHosts";
|
||||
public static final String PAIRWISE_MALFORMED_SECTOR_IDENTIFIER_URI = "pairwiseMalformedSectorIdentifierURI";
|
||||
public static final String PAIRWISE_FAILED_TO_GET_REDIRECT_URIS = "pairwiseFailedToGetRedirectURIs";
|
||||
public static final String PAIRWISE_REDIRECT_URIS_MISMATCH = "pairwiseRedirectURIsMismatch";
|
||||
|
||||
public static void validate(KeycloakSession session, ClientModel client, ProtocolMapperModel mapperModel) throws ProtocolMapperConfigException {
|
||||
String sectorIdentifierUri = PairwiseSubMapperHelper.getSectorIdentifierUri(mapperModel);
|
||||
String rootUrl = client.getRootUrl();
|
||||
Set<String> redirectUris = client.getRedirectUris();
|
||||
validate(session, rootUrl, redirectUris, sectorIdentifierUri);
|
||||
}
|
||||
|
||||
public static void validate(KeycloakSession session, String rootUrl, Set<String> redirectUris, String sectorIdentifierUri) throws ProtocolMapperConfigException {
|
||||
if (sectorIdentifierUri == null || sectorIdentifierUri.isEmpty()) {
|
||||
validateClientRedirectUris(rootUrl, redirectUris);
|
||||
return;
|
||||
}
|
||||
validateSectorIdentifierUri(sectorIdentifierUri);
|
||||
validateSectorIdentifierUri(session, rootUrl, redirectUris, sectorIdentifierUri);
|
||||
}
|
||||
|
||||
private static void validateClientRedirectUris(String rootUrl, Set<String> redirectUris) throws ProtocolMapperConfigException {
|
||||
Set<String> hosts = new HashSet<>();
|
||||
for (String redirectUri : PairwiseSubMapperUtils.resolveValidRedirectUris(rootUrl, redirectUris)) {
|
||||
try {
|
||||
URI uri = new URI(redirectUri);
|
||||
hosts.add(uri.getHost());
|
||||
} catch (URISyntaxException e) {
|
||||
throw new ProtocolMapperConfigException("Client contained an invalid redirect URI.",
|
||||
PAIRWISE_MALFORMED_CLIENT_REDIRECT_URI, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (hosts.isEmpty()) {
|
||||
throw new ProtocolMapperConfigException("Client redirect URIs must contain a valid host component.",
|
||||
PAIRWISE_CLIENT_REDIRECT_URIS_MISSING_HOST);
|
||||
}
|
||||
|
||||
if (hosts.size() > 1) {
|
||||
throw new ProtocolMapperConfigException("Without a configured Sector Identifier URI, client redirect URIs must not contain multiple host components.", PAIRWISE_CLIENT_REDIRECT_URIS_MULTIPLE_HOSTS);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateSectorIdentifierUri(String sectorIdentifierUri) throws ProtocolMapperConfigException {
|
||||
URI uri;
|
||||
try {
|
||||
uri = new URI(sectorIdentifierUri);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new ProtocolMapperConfigException("Invalid Sector Identifier URI.",
|
||||
PAIRWISE_MALFORMED_SECTOR_IDENTIFIER_URI, e);
|
||||
}
|
||||
if (uri.getScheme() == null || uri.getHost() == null) {
|
||||
throw new ProtocolMapperConfigException("Invalid Sector Identifier URI.",
|
||||
PAIRWISE_MALFORMED_SECTOR_IDENTIFIER_URI);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateSectorIdentifierUri(KeycloakSession session, String rootUrl, Set<String> redirectUris, String sectorIdentifierUri) throws ProtocolMapperConfigException {
|
||||
Set<String> sectorRedirects = getSectorRedirects(session, sectorIdentifierUri);
|
||||
if (!PairwiseSubMapperUtils.matchesRedirects(rootUrl, redirectUris, sectorRedirects)) {
|
||||
throw new ProtocolMapperConfigException("Client redirect URIs does not match redirect URIs fetched from the Sector Identifier URI.",
|
||||
PAIRWISE_REDIRECT_URIS_MISMATCH);
|
||||
}
|
||||
}
|
||||
|
||||
private static Set<String> getSectorRedirects(KeycloakSession session, String sectorIdentifierUri) throws ProtocolMapperConfigException {
|
||||
InputStream is = null;
|
||||
try {
|
||||
is = session.getProvider(HttpClientProvider.class).get(sectorIdentifierUri);
|
||||
List<String> sectorRedirects = JsonSerialization.readValue(is, TypedList.class);
|
||||
return new HashSet<>(sectorRedirects);
|
||||
} catch (IOException e) {
|
||||
throw new ProtocolMapperConfigException("Failed to get redirect URIs from the Sector Identifier URI.",
|
||||
PAIRWISE_FAILED_TO_GET_REDIRECT_URIS, e);
|
||||
} finally {
|
||||
if (is != null) {
|
||||
try {
|
||||
is.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class TypedList extends ArrayList<String> {}
|
||||
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package org.keycloak.protocol.oidc.utils;
|
||||
|
||||
public enum SubjectType {
|
||||
PUBLIC,
|
||||
PAIRWISE;
|
||||
|
||||
public static SubjectType parse(String subjectTypeStr) {
|
||||
if (subjectTypeStr == null) {
|
||||
return PUBLIC;
|
||||
}
|
||||
return Enum.valueOf(SubjectType.class, subjectTypeStr.toUpperCase());
|
||||
}
|
||||
}
|
|
@ -434,4 +434,8 @@ public interface ServicesLogger extends BasicLogger {
|
|||
@Message(id=97, value="Invalid request")
|
||||
void invalidRequest(@Cause Throwable t);
|
||||
|
||||
@LogMessage(level = ERROR)
|
||||
@Message(id=98, value="Failed to get redirect uris from sector identifier URI: %s")
|
||||
void failedToGetRedirectUrisFromSectorIdentifierUri(@Cause Throwable t, String sectorIdentifierUri);
|
||||
|
||||
}
|
||||
|
|
|
@ -19,22 +19,22 @@ package org.keycloak.services.clientregistration;
|
|||
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.ClientInitialAccessModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientRegistrationTrustedHostModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ModelDuplicateException;
|
||||
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.resources.admin.AdminRoot;
|
||||
import org.keycloak.services.validation.ClientValidator;
|
||||
import org.keycloak.services.validation.PairwiseClientValidator;
|
||||
import org.keycloak.services.validation.ValidationMessages;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
|
@ -55,7 +55,7 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
|
|||
auth.requireCreate();
|
||||
|
||||
ValidationMessages validationMessages = new ValidationMessages();
|
||||
if (!ClientValidator.validate(client, validationMessages)) {
|
||||
if (!ClientValidator.validate(client, validationMessages) || !PairwiseClientValidator.validate(session, client, validationMessages)) {
|
||||
String errorCode = validationMessages.fieldHasError("redirectUris") ? ErrorCodes.INVALID_REDIRECT_URI : ErrorCodes.INVALID_CLIENT_METADATA;
|
||||
throw new ErrorResponseException(
|
||||
errorCode,
|
||||
|
@ -66,6 +66,10 @@ 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);
|
||||
|
||||
|
@ -119,7 +123,7 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
|
|||
}
|
||||
|
||||
ValidationMessages validationMessages = new ValidationMessages();
|
||||
if (!ClientValidator.validate(rep, validationMessages)) {
|
||||
if (!ClientValidator.validate(rep, validationMessages) || !PairwiseClientValidator.validate(session, rep, validationMessages)) {
|
||||
String errorCode = validationMessages.fieldHasError("redirectUris") ? ErrorCodes.INVALID_REDIRECT_URI : ErrorCodes.INVALID_CLIENT_METADATA;
|
||||
throw new ErrorResponseException(
|
||||
errorCode,
|
||||
|
@ -128,6 +132,7 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
|
|||
);
|
||||
}
|
||||
|
||||
updateSubjectType(rep, client);
|
||||
RepresentationToModel.updateClient(rep, client);
|
||||
rep = ModelToRepresentation.toRepresentation(client);
|
||||
|
||||
|
@ -140,6 +145,43 @@ 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);
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
|||
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.SubjectType;
|
||||
import org.keycloak.representations.idm.CertificateRepresentation;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.oidc.OIDCClientRepresentation;
|
||||
|
@ -53,6 +54,7 @@ public class DescriptionConverter {
|
|||
|
||||
public static ClientRepresentation toInternal(KeycloakSession session, OIDCClientRepresentation clientOIDC) throws ClientRegistrationException {
|
||||
ClientRepresentation client = new ClientRepresentation();
|
||||
|
||||
client.setClientId(clientOIDC.getClientId());
|
||||
client.setName(clientOIDC.getClientName());
|
||||
client.setRedirectUris(clientOIDC.getRedirectUris());
|
||||
|
@ -113,6 +115,12 @@ 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;
|
||||
}
|
||||
|
||||
|
@ -172,6 +180,11 @@ public class DescriptionConverter {
|
|||
response.setRequestObjectSigningAlg(config.getRequestObjectSignatureAlg().toString());
|
||||
}
|
||||
|
||||
response.setSubjectType(config.getSubjectType().toString().toLowerCase());
|
||||
if (config.getSubjectType().equals(SubjectType.PAIRWISE)) {
|
||||
response.setSectorIdentifierUri(config.getSectorIdentifierUri());
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@ import org.keycloak.services.resources.KeycloakApplication;
|
|||
import org.keycloak.services.ErrorResponse;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.services.validation.ClientValidator;
|
||||
import org.keycloak.services.validation.PairwiseClientValidator;
|
||||
import org.keycloak.services.validation.ValidationMessages;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
|
@ -127,7 +128,7 @@ public class ClientResource {
|
|||
}
|
||||
|
||||
ValidationMessages validationMessages = new ValidationMessages();
|
||||
if (!ClientValidator.validate(rep, validationMessages)) {
|
||||
if (!ClientValidator.validate(rep, validationMessages) || !PairwiseClientValidator.validate(session, rep, validationMessages)) {
|
||||
Properties messages = AdminRoot.getMessages(session, realm, auth.getAuth().getToken().getLocale());
|
||||
throw new ErrorResponseException(
|
||||
validationMessages.getStringMessages(),
|
||||
|
|
|
@ -31,6 +31,7 @@ import org.keycloak.services.ErrorResponseException;
|
|||
import org.keycloak.services.ServicesLogger;
|
||||
import org.keycloak.services.managers.ClientManager;
|
||||
import org.keycloak.services.validation.ClientValidator;
|
||||
import org.keycloak.services.validation.PairwiseClientValidator;
|
||||
import org.keycloak.services.validation.ValidationMessages;
|
||||
|
||||
import javax.ws.rs.*;
|
||||
|
@ -120,7 +121,7 @@ public class ClientsResource {
|
|||
auth.requireManage();
|
||||
|
||||
ValidationMessages validationMessages = new ValidationMessages();
|
||||
if (!ClientValidator.validate(rep, validationMessages)) {
|
||||
if (!ClientValidator.validate(rep, validationMessages) || !PairwiseClientValidator.validate(session, rep, validationMessages)) {
|
||||
Properties messages = AdminRoot.getMessages(session, realm, auth.getAuth().getToken().getLocale());
|
||||
throw new ErrorResponseException(
|
||||
validationMessages.getStringMessages(),
|
||||
|
|
|
@ -20,15 +20,7 @@ import org.jboss.resteasy.annotations.cache.NoCache;
|
|||
import org.jboss.resteasy.spi.NotFoundException;
|
||||
import org.keycloak.events.admin.OperationType;
|
||||
import org.keycloak.events.admin.ResourceType;
|
||||
import org.keycloak.mappers.FederationConfigValidationException;
|
||||
import org.keycloak.mappers.UserFederationMapper;
|
||||
import org.keycloak.mappers.UserFederationMapperFactory;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ModelDuplicateException;
|
||||
import org.keycloak.models.ProtocolMapperContainerModel;
|
||||
import org.keycloak.models.ProtocolMapperModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserFederationMapperModel;
|
||||
import org.keycloak.models.*;
|
||||
import org.keycloak.models.utils.ModelToRepresentation;
|
||||
import org.keycloak.models.utils.RepresentationToModel;
|
||||
import org.keycloak.protocol.ProtocolMapper;
|
||||
|
@ -39,19 +31,11 @@ import org.keycloak.services.ErrorResponseException;
|
|||
import org.keycloak.services.ServicesLogger;
|
||||
import org.keycloak.services.resources.admin.RealmAuth.Resource;
|
||||
|
||||
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.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
@ -269,7 +253,7 @@ public class ProtocolMappersResource {
|
|||
} catch (ProtocolMapperConfigException ex) {
|
||||
logger.error(ex.getMessage());
|
||||
Properties messages = AdminRoot.getMessages(session, realm, auth.getAuth().getToken().getLocale());
|
||||
throw new ErrorResponseException(ex.getMessage(), MessageFormat.format(messages.getProperty(ex.getMessage(), ex.getMessage()), ex.getParameters()),
|
||||
throw new ErrorResponseException(ex.getMessage(), MessageFormat.format(messages.getProperty(ex.getMessageKey(), ex.getMessage()), ex.getParameters()),
|
||||
Response.Status.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
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.utils.PairwiseSubMapperValidator;
|
||||
import org.keycloak.protocol.oidc.utils.SubjectType;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:martin.hardselius@gmail.com">Martin Hardselius</a>
|
||||
*/
|
||||
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<>();
|
||||
if (client.getRedirectUris() != null) redirectUris.addAll(client.getRedirectUris());
|
||||
return validate(session, rootUrl, redirectUris, sectorIdentifierUri, messages);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean validate(KeycloakSession session, String rootUrl, Set<String> redirectUris, String sectorIdentifierUri, ValidationMessages messages) {
|
||||
try {
|
||||
PairwiseSubMapperValidator.validate(session, rootUrl, redirectUris, sectorIdentifierUri);
|
||||
} catch (ProtocolMapperConfigException e) {
|
||||
messages.add(e.getMessage(), e.getMessageKey());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
|
@ -57,8 +57,11 @@ public class ValidationMessages {
|
|||
}
|
||||
|
||||
public boolean fieldHasError(String fieldId) {
|
||||
if (fieldId == null) {
|
||||
return false;
|
||||
}
|
||||
for (ValidationMessage message : messages) {
|
||||
if (message.getFieldId().equals(fieldId)) {
|
||||
if (fieldId.equals(message.getFieldId())) {
|
||||
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
|
||||
|
||||
|
|
|
@ -18,10 +18,8 @@
|
|||
package org.keycloak.testsuite.rest;
|
||||
|
||||
import org.keycloak.Config.Scope;
|
||||
import org.keycloak.events.admin.AdminEvent;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.representations.adapters.action.AdminAction;
|
||||
import org.keycloak.representations.adapters.action.LogoutAction;
|
||||
import org.keycloak.representations.adapters.action.PushNotBeforeAction;
|
||||
import org.keycloak.representations.adapters.action.TestAvailabilityAction;
|
||||
|
@ -29,6 +27,7 @@ import org.keycloak.services.resource.RealmResourceProvider;
|
|||
import org.keycloak.services.resource.RealmResourceProviderFactory;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingDeque;
|
||||
|
||||
|
@ -70,6 +69,7 @@ public class TestApplicationResourceProviderFactory implements RealmResourceProv
|
|||
|
||||
private KeyPair signingKeyPair;
|
||||
private String oidcRequest;
|
||||
private List<String> sectorIdentifierRedirectUris;
|
||||
|
||||
public KeyPair getSigningKeyPair() {
|
||||
return signingKeyPair;
|
||||
|
@ -86,5 +86,13 @@ public class TestApplicationResourceProviderFactory implements RealmResourceProv
|
|||
public void setOidcRequest(String oidcRequest) {
|
||||
this.oidcRequest = oidcRequest;
|
||||
}
|
||||
|
||||
public List<String> getSectorIdentifierRedirectUris() {
|
||||
return sectorIdentifierRedirectUris;
|
||||
}
|
||||
|
||||
public void setSectorIdentifierRedirectUris(List<String> sectorIdentifierRedirectUris) {
|
||||
this.sectorIdentifierRedirectUris = sectorIdentifierRedirectUris;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,19 +17,6 @@
|
|||
|
||||
package org.keycloak.testsuite.rest.resource;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||
import org.jboss.resteasy.spi.BadRequestException;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
|
@ -42,6 +29,19 @@ import org.keycloak.models.utils.KeycloakModelUtils;
|
|||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.testsuite.rest.TestApplicationResourceProviderFactory;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
|
@ -134,4 +134,19 @@ public class TestingOIDCEndpointsApplicationResource {
|
|||
public String getOIDCRequest() {
|
||||
return clientData.getOidcRequest();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/set-sector-identifier-redirect-uris")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public void setSectorIdentifierRedirectUris(@QueryParam("redirectUris") List<String> redirectUris) {
|
||||
clientData.setSectorIdentifierRedirectUris(new ArrayList<>());
|
||||
clientData.getSectorIdentifierRedirectUris().addAll(redirectUris);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/get-sector-identifier-redirect-uris")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public List<String> getSectorIdentifierRedirectUris() {
|
||||
return clientData.getSectorIdentifierRedirectUris();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,10 +17,10 @@
|
|||
|
||||
package org.keycloak.testsuite.client.resources;
|
||||
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
|
@ -45,4 +45,10 @@ public class TestApplicationResourceUrls {
|
|||
|
||||
return builder.build().toString();
|
||||
}
|
||||
|
||||
public static String pairwiseSectorIdentifierUri() {
|
||||
UriBuilder builder = oidcClientEndpoints()
|
||||
.path(TestOIDCEndpointsApplicationResource.class, "getSectorIdentifierRedirectUris");
|
||||
return builder.build().toString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,15 +17,15 @@
|
|||
|
||||
package org.keycloak.testsuite.client.resources;
|
||||
|
||||
import java.util.Map;
|
||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
|
@ -55,4 +55,15 @@ public interface TestOIDCEndpointsApplicationResource {
|
|||
@Path("/get-oidc-request")
|
||||
@Produces(org.keycloak.utils.MediaType.APPLICATION_JWT)
|
||||
String getOIDCRequest();
|
||||
|
||||
@GET
|
||||
@Path("/set-sector-identifier-redirect-uris")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
void setSectorIdentifierRedirectUris(@QueryParam("redirectUris") List<String> redirectUris);
|
||||
|
||||
@GET
|
||||
@Path("/get-sector-identifier-redirect-uris")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
List<String> getSectorIdentifierRedirectUris();
|
||||
|
||||
}
|
||||
|
|
|
@ -55,11 +55,7 @@ import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResou
|
|||
import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
import java.security.PrivateKey;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
|
@ -286,6 +282,137 @@ 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();
|
||||
|
|
|
@ -95,7 +95,7 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
|
|||
assertContains(oidcConfig.getGrantTypesSupported(), OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.IMPLICIT);
|
||||
assertContains(oidcConfig.getResponseModesSupported(), "query", "fragment");
|
||||
|
||||
Assert.assertNames(oidcConfig.getSubjectTypesSupported(), "public");
|
||||
Assert.assertNames(oidcConfig.getSubjectTypesSupported(), "pairwise", "public");
|
||||
Assert.assertNames(oidcConfig.getIdTokenSigningAlgValuesSupported(), Algorithm.RS256.toString());
|
||||
Assert.assertNames(oidcConfig.getUserInfoSigningAlgValuesSupported(), Algorithm.RS256.toString());
|
||||
Assert.assertNames(oidcConfig.getRequestObjectSigningAlgValuesSupported(), Algorithm.none.toString(), Algorithm.RS256.toString());
|
||||
|
|
|
@ -164,6 +164,12 @@ usermodel.clientRoleMapping.rolePrefix.label=Client Role prefix
|
|||
usermodel.clientRoleMapping.rolePrefix.tooltip=A prefix for each client role (optional).
|
||||
usermodel.realmRoleMapping.rolePrefix.label=Realm Role prefix
|
||||
usermodel.realmRoleMapping.rolePrefix.tooltip=A prefix for each Realm Role (optional).
|
||||
sectorIdentifierUri.label=Sector Identifier URI
|
||||
sectorIdentifierUri.tooltip=Providers that use pairwise sub values and support Dynamic Client Registration SHOULD use the sector_identifier_uri parameter. It provides a way for a group of websites under common administrative control to have consistent pairwise sub values independent of the individual domain names. It also provides a way for Clients to change redirect_uri domains without having to reregister all of their users.
|
||||
pairwiseSubAlgorithmSalt.label=Salt
|
||||
pairwiseSubAlgorithmSalt.tooltip=Salt used when calculating the pairwise subject identifier. If left blank, a salt will be generated.
|
||||
|
||||
|
||||
|
||||
# client details
|
||||
clients.tooltip=Clients are trusted browser apps and web services in a realm. These clients can request a login. You can also define client specific roles.
|
||||
|
|
|
@ -14,4 +14,11 @@ ldapErrorCantWriteOnlyForReadOnlyLdap=Can't set write only when LDAP provider mo
|
|||
ldapErrorCantWriteOnlyAndReadOnly=Can't set write-only and read-only together
|
||||
|
||||
clientRedirectURIsFragmentError=Redirect URIs must not contain an URI fragment
|
||||
clientRootURLFragmentError=Root URL must not contain an URL fragment
|
||||
clientRootURLFragmentError=Root URL must not contain an URL fragment
|
||||
|
||||
pairwiseMalformedClientRedirectURI=Client contained an invalid redirect URI.
|
||||
pairwiseClientRedirectURIsMissingHost=Client redirect URIs must contain a valid host component.
|
||||
pairwiseClientRedirectURIsMultipleHosts=Without a configured Sector Identifier URI, client redirect URIs must not contain multiple host components.
|
||||
pairwiseMalformedSectorIdentifierURI=Malformed Sector Identifier URI.
|
||||
pairwiseFailedToGetRedirectURIs=Failed to get redirect URIs from the Sector Identifier URI.
|
||||
pairwiseRedirectURIsMismatch=Client redirect URIs does not match redirect URIs fetched from the Sector Identifier URI.
|
||||
|
|
|
@ -1703,6 +1703,12 @@ module.controller('ClientProtocolMapperCtrl', function($scope, realm, serverInfo
|
|||
mapper = angular.copy($scope.mapper);
|
||||
$location.url("/realms/" + realm.realm + '/clients/' + client.id + "/mappers/" + $scope.model.mapper.id);
|
||||
Notifications.success("Your changes have been saved.");
|
||||
}, 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