keycloak-aplcache
Merge pull request #3231 from TeliaSoneraNorge/pr/KEYCLOAK-3422 KEYCLOAK-3422 …
Changes
services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractPairwiseSubMapper.java 125(+125 -0)
services/src/main/java/org/keycloak/protocol/oidc/utils/PairwiseSubMapperValidator.java 113(+113 -0)
services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java 60(+51 -9)
services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java 13(+13 -0)
services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java 22(+3 -19)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java 41(+28 -13)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProviderFactory.java 12(+10 -2)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResourceUrls.java 10(+8 -2)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java 17(+14 -3)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java 137(+132 -5)
Details
diff --git a/core/src/test/java/org/keycloak/JsonParserTest.java b/core/src/test/java/org/keycloak/JsonParserTest.java
index e346fe6..965a13c 100755
--- a/core/src/test/java/org/keycloak/JsonParserTest.java
+++ b/core/src/test/java/org/keycloak/JsonParserTest.java
@@ -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>
*/
@@ -139,6 +135,18 @@ public class JsonParserTest {
}
@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}";
OIDCClientRepresentation clientRep = JsonSerialization.readValue(stringRep, OIDCClientRepresentation.class);
diff --git a/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index 51b1f4e..8ea1e97 100755
--- a/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -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) {
diff --git a/server-spi/src/main/java/org/keycloak/protocol/ProtocolMapperConfigException.java b/server-spi/src/main/java/org/keycloak/protocol/ProtocolMapperConfigException.java
index f50a73d..3f1f676 100644
--- a/server-spi/src/main/java/org/keycloak/protocol/ProtocolMapperConfigException.java
+++ b/server-spi/src/main/java/org/keycloak/protocol/ProtocolMapperConfigException.java
@@ -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;
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java
index f5e4caa..28d9514 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java
@@ -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);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractPairwiseSubMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractPairwiseSubMapper.java
new file mode 100644
index 0000000..b153fe4
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractPairwiseSubMapper.java
@@ -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;
+ }
+}
+
+
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/PairwiseSubMapperHelper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/PairwiseSubMapperHelper.java
new file mode 100644
index 0000000..a2aa4b3
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/PairwiseSubMapperHelper.java
@@ -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;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/SHA265PairwiseSubMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/SHA265PairwiseSubMapper.java
new file mode 100644
index 0000000..bc1caaa
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/SHA265PairwiseSubMapper.java
@@ -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;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java
index 3233690..e16d7b8 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java
@@ -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) {
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java
index 087b1f6..aa56dc7 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java
@@ -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",
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java
index 800630c..2653b9b 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java
@@ -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");
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/PairwiseSubMapperUtils.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/PairwiseSubMapperUtils.java
new file mode 100644
index 0000000..5e2dadc
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/PairwiseSubMapperUtils.java
@@ -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);
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/PairwiseSubMapperValidator.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/PairwiseSubMapperValidator.java
new file mode 100644
index 0000000..e6d8a6e
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/PairwiseSubMapperValidator.java
@@ -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> {}
+
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/SubjectType.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/SubjectType.java
new file mode 100644
index 0000000..ec1ba97
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/SubjectType.java
@@ -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());
+ }
+}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java b/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java
index d634b00..f5ad8b0 100755
--- a/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java
@@ -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);
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java b/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java
index e1939ef..c377313 100644
--- a/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java
@@ -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;
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
index 9b6593f..d208b50 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
@@ -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(),
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java
index dacea26..1be0cac 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java
@@ -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(),
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java
index 86dcefc..8040820 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java
@@ -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);
}
}
diff --git a/services/src/main/java/org/keycloak/services/ServicesLogger.java b/services/src/main/java/org/keycloak/services/ServicesLogger.java
index af6c4c1..0595abb 100644
--- a/services/src/main/java/org/keycloak/services/ServicesLogger.java
+++ b/services/src/main/java/org/keycloak/services/ServicesLogger.java
@@ -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);
+
}
diff --git a/services/src/main/java/org/keycloak/services/validation/PairwiseClientValidator.java b/services/src/main/java/org/keycloak/services/validation/PairwiseClientValidator.java
new file mode 100644
index 0000000..6cc2033
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/validation/PairwiseClientValidator.java
@@ -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;
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/validation/ValidationMessages.java b/services/src/main/java/org/keycloak/services/validation/ValidationMessages.java
index e26ebff..a7e82c8 100644
--- a/services/src/main/java/org/keycloak/services/validation/ValidationMessages.java
+++ b/services/src/main/java/org/keycloak/services/validation/ValidationMessages.java
@@ -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;
}
}
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper
index 9d1e10e..77830dc 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper
+++ b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper
@@ -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
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java
index 6ea488f..8b5df6c 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java
@@ -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();
+ }
}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProviderFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProviderFactory.java
index 6bd7dc2..d8d2a8d 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProviderFactory.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProviderFactory.java
@@ -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;
+ }
}
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResourceUrls.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResourceUrls.java
index 8c5f98b..88b7b38 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResourceUrls.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResourceUrls.java
@@ -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();
+ }
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java
index 54d6c35..9c4f324 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java
@@ -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();
+
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java
index 19d6413..d13bfa1 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java
@@ -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;
@@ -287,6 +283,137 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
}
@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();
clientRep.setUserinfoSignedResponseAlg(Algorithm.RS256.toString());
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java
index 4592eea..e1877b8 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java
@@ -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());
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index 998eff8..ea34268 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -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.
diff --git a/themes/src/main/resources/theme/base/admin/messages/messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/messages_en.properties
index 345cb25..f4044ab 100644
--- a/themes/src/main/resources/theme/base/admin/messages/messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/messages_en.properties
@@ -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
\ No newline at end of file
+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.
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
index cdb8b48..12af561 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
@@ -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');
+ }
});
};