keycloak-aplcache

Changes

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 7fea657..314047d 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
@@ -84,7 +84,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 c015dfa..71f2165 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');
+            }
         });
     };