keycloak-aplcache

Merge pull request #3174 from mposolda/master KEYCLOAK-3416

8/30/2016 5:18:09 PM

Details

diff --git a/core/src/main/java/org/keycloak/representations/UserInfo.java b/core/src/main/java/org/keycloak/representations/UserInfo.java
index 200e8e8..7849718 100755
--- a/core/src/main/java/org/keycloak/representations/UserInfo.java
+++ b/core/src/main/java/org/keycloak/representations/UserInfo.java
@@ -16,12 +16,31 @@
  */
 package org.keycloak.representations;
 
+import java.util.HashMap;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonAnyGetter;
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import org.keycloak.json.StringOrArrayDeserializer;
+import org.keycloak.json.StringOrArraySerializer;
 
 /**
  * @author pedroigor
  */
 public class UserInfo {
+
+    // Should be in signed UserInfo response
+    @JsonProperty("iss")
+    protected String issuer;
+    @JsonProperty("aud")
+    @JsonSerialize(using = StringOrArraySerializer.class)
+    @JsonDeserialize(using = StringOrArrayDeserializer.class)
+    protected String[] audience;
+
     @JsonProperty("sub")
     protected String sub;
 
@@ -85,6 +104,34 @@ public class UserInfo {
     @JsonProperty("claims_locales")
     protected String claimsLocales;
 
+    protected Map<String, Object> otherClaims = new HashMap<>();
+
+    public String getIssuer() {
+        return issuer;
+    }
+
+    public void setIssuer(String issuer) {
+        this.issuer = issuer;
+    }
+
+    @JsonIgnore
+    public String[] getAudience() {
+        return audience;
+    }
+
+    public boolean hasAudience(String audience) {
+        for (String a : this.audience) {
+            if (a.equals(audience)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public void setAudience(String... audience) {
+        this.audience = audience;
+    }
+
     public String getSubject() {
         return this.sub;
     }
@@ -260,4 +307,19 @@ public class UserInfo {
     public void setClaimsLocales(String claimsLocales) {
         this.claimsLocales = claimsLocales;
     }
+
+    /**
+     * This is a map of any other claims and data that might be in the UserInfo.  Could be custom claims set up by the auth server
+     *
+     * @return
+     */
+    @JsonAnyGetter
+    public Map<String, Object> getOtherClaims() {
+        return otherClaims;
+    }
+
+    @JsonAnySetter
+    public void setOtherClaims(String name, Object value) {
+        otherClaims.put(name, value);
+    }
 }
diff --git a/server-spi/src/main/java/org/keycloak/events/Details.java b/server-spi/src/main/java/org/keycloak/events/Details.java
index e5da713..772eaa7 100755
--- a/server-spi/src/main/java/org/keycloak/events/Details.java
+++ b/server-spi/src/main/java/org/keycloak/events/Details.java
@@ -58,4 +58,7 @@ public interface Details {
 
     String CLIENT_AUTH_METHOD = "client_auth_method";
 
+    String SIGNATURE_REQUIRED = "signature_required";
+    String SIGNATURE_ALGORITHM = "signature_algorithm";
+
 }
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 ad23283..f5e4caa 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,6 +19,7 @@ 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;
@@ -27,12 +28,15 @@ import org.keycloak.events.Details;
 import org.keycloak.events.Errors;
 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.protocol.oidc.OIDCAdvancedConfigWrapper;
 import org.keycloak.protocol.oidc.TokenManager;
 import org.keycloak.representations.AccessToken;
 import org.keycloak.services.ErrorResponseException;
@@ -40,17 +44,18 @@ 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;
 import javax.ws.rs.OPTIONS;
 import javax.ws.rs.POST;
 import javax.ws.rs.Path;
-import javax.ws.rs.Produces;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.HttpHeaders;
-import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriInfo;
+
+import java.security.PrivateKey;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -86,7 +91,6 @@ public class UserInfoEndpoint {
 
     @Path("/")
     @OPTIONS
-    @Produces(MediaType.APPLICATION_JSON)
     public Response issueUserInfoPreflight() {
         return Cors.add(this.request, Response.ok()).auth().preflight().build();
     }
@@ -94,7 +98,6 @@ public class UserInfoEndpoint {
     @Path("/")
     @GET
     @NoCache
-    @Produces(MediaType.APPLICATION_JSON)
     public Response issueUserInfoGet(@Context final HttpHeaders headers) {
         String accessToken = this.appAuthManager.extractAuthorizationHeaderToken(headers);
         return issueUserInfo(accessToken);
@@ -103,7 +106,6 @@ public class UserInfoEndpoint {
     @Path("/")
     @POST
     @NoCache
-    @Produces(MediaType.APPLICATION_JSON)
     public Response issueUserInfoPost() {
         // Try header first
         HttpHeaders headers = request.getHttpHeaders();
@@ -176,12 +178,39 @@ public class UserInfoEndpoint {
         AccessToken userInfo = new AccessToken();
         tokenManager.transformUserInfoAccessToken(session, userInfo, realm, clientModel, userModel, userSession, clientSession);
 
-        event.success();
-
         Map<String, Object> claims = new HashMap<String, Object>();
         claims.putAll(userInfo.getOtherClaims());
         claims.put("sub", userModel.getId());
-        return Cors.add(request, Response.ok(claims)).auth().allowedOrigins(token).build();
+
+        Response.ResponseBuilder responseBuilder;
+        OIDCAdvancedConfigWrapper cfg = OIDCAdvancedConfigWrapper.fromClientModel(clientModel);
+
+        if (cfg.isUserInfoSignatureRequired()) {
+            String issuerUrl = Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName());
+            String audience = clientModel.getClientId();
+            claims.put("iss", issuerUrl);
+            claims.put("aud", audience);
+
+            Algorithm signatureAlg = cfg.getUserInfoSignedResponseAlg();
+            PrivateKey privateKey = realm.getPrivateKey();
+
+            String signedUserInfo = new JWSBuilder()
+                    .jsonContent(claims)
+                    .sign(signatureAlg, privateKey);
+
+            responseBuilder = Response.ok(signedUserInfo).header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JWT);
+
+            event.detail(Details.SIGNATURE_REQUIRED, "true");
+            event.detail(Details.SIGNATURE_ALGORITHM, cfg.getUserInfoSignedResponseAlg().toString());
+        } else {
+            responseBuilder = Response.ok(claims).header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON);
+
+            event.detail(Details.SIGNATURE_REQUIRED, "false");
+        }
+
+        event.success();
+
+        return Cors.add(request, responseBuilder).auth().allowedOrigins(token).build();
     }
 
 }
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java
new file mode 100644
index 0000000..37fe204
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.protocol.oidc;
+
+import java.util.HashMap;
+
+import org.keycloak.jose.jws.Algorithm;
+import org.keycloak.models.ClientModel;
+import org.keycloak.representations.idm.ClientRepresentation;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OIDCAdvancedConfigWrapper {
+
+    private static final String USER_INFO_RESPONSE_SIGNATURE_ALG = "user.info.response.signature.alg";
+
+    private final ClientModel clientModel;
+    private final ClientRepresentation clientRep;
+
+    private OIDCAdvancedConfigWrapper(ClientModel client, ClientRepresentation clientRep) {
+        this.clientModel = client;
+        this.clientRep = clientRep;
+    }
+
+
+    public static OIDCAdvancedConfigWrapper fromClientModel(ClientModel client) {
+        return new OIDCAdvancedConfigWrapper(client, null);
+    }
+
+    public static OIDCAdvancedConfigWrapper fromClientRepresentation(ClientRepresentation clientRep) {
+        return new OIDCAdvancedConfigWrapper(null, clientRep);
+    }
+
+
+    public Algorithm getUserInfoSignedResponseAlg() {
+        String alg = getAttribute(USER_INFO_RESPONSE_SIGNATURE_ALG);
+        return alg==null ? null : Enum.valueOf(Algorithm.class, alg);
+    }
+
+    public void setUserInfoSignedResponseAlg(Algorithm alg) {
+        String algStr = alg==null ? null : alg.toString();
+        setAttribute(USER_INFO_RESPONSE_SIGNATURE_ALG, algStr);
+    }
+
+    public boolean isUserInfoSignatureRequired() {
+        return getUserInfoSignedResponseAlg() != null;
+    }
+
+
+    private String getAttribute(String attrKey) {
+        if (clientModel != null) {
+            return clientModel.getAttribute(attrKey);
+        } else {
+            return clientRep.getAttributes()==null ? null : clientRep.getAttributes().get(attrKey);
+        }
+    }
+
+    private void setAttribute(String attrKey, String attrValue) {
+        if (clientModel != null) {
+            if (attrValue != null) {
+                clientModel.setAttribute(attrKey, attrValue);
+            } else {
+                clientModel.removeAttribute(attrKey);
+            }
+        } else {
+            if (attrValue != null) {
+                if (clientRep.getAttributes() == null) {
+                    clientRep.setAttributes(new HashMap<>());
+                }
+                clientRep.getAttributes().put(attrKey, attrValue);
+            } else {
+                if (clientRep.getAttributes() != null) {
+                    clientRep.getAttributes().put(attrKey, null);
+                }
+            }
+        }
+    }
+}
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 eb5b5b4..dfca144 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java
@@ -48,6 +48,8 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
 
     public static final List<String> DEFAULT_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED = list(Algorithm.RS256.toString());
 
+    public static final List<String> DEFAULT_USER_INFO_SIGNING_ALG_VALUES_SUPPORTED  = list(Algorithm.RS256.toString());
+
     public static final List<String> DEFAULT_GRANT_TYPES_SUPPORTED = list(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.IMPLICIT, OAuth2Constants.REFRESH_TOKEN, OAuth2Constants.PASSWORD, OAuth2Constants.CLIENT_CREDENTIALS);
 
     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");
@@ -90,6 +92,7 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
         config.setRegistrationEndpoint(RealmsResource.clientRegistrationUrl(uriInfo).path(ClientRegistrationService.class, "provider").build(realm.getName(), OIDCClientRegistrationProviderFactory.ID).toString());
 
         config.setIdTokenSigningAlgValuesSupported(DEFAULT_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED);
+        config.setUserInfoSigningAlgValuesSupported(DEFAULT_USER_INFO_SIGNING_ALG_VALUES_SUPPORTED);
         config.setResponseTypesSupported(DEFAULT_RESPONSE_TYPES_SUPPORTED);
         config.setSubjectTypesSupported(DEFAULT_SUBJECT_TYPES_SUPPORTED);
         config.setResponseModesSupported(DEFAULT_RESPONSE_MODES_SUPPORTED);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java b/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java
index 0421e16..ee5241b 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java
@@ -64,6 +64,9 @@ public class OIDCConfigurationRepresentation {
     @JsonProperty("id_token_signing_alg_values_supported")
     private List<String> idTokenSigningAlgValuesSupported;
 
+    @JsonProperty("userinfo_signing_alg_values_supported")
+    private List<String> userInfoSigningAlgValuesSupported;
+
     @JsonProperty("response_modes_supported")
     private List<String> responseModesSupported;
 
@@ -184,6 +187,14 @@ public class OIDCConfigurationRepresentation {
         this.idTokenSigningAlgValuesSupported = idTokenSigningAlgValuesSupported;
     }
 
+    public List<String> getUserInfoSigningAlgValuesSupported() {
+        return userInfoSigningAlgValuesSupported;
+    }
+
+    public void setUserInfoSigningAlgValuesSupported(List<String> userInfoSigningAlgValuesSupported) {
+        this.userInfoSigningAlgValuesSupported = userInfoSigningAlgValuesSupported;
+    }
+
     public List<String> getResponseModesSupported() {
         return responseModesSupported;
     }
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 3ddcc59..a95cc09 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
@@ -24,8 +24,10 @@ import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthen
 import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
 import org.keycloak.jose.jwk.JSONWebKeySet;
 import org.keycloak.jose.jwk.JWK;
+import org.keycloak.jose.jws.Algorithm;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
 import org.keycloak.protocol.oidc.OIDCLoginProtocol;
 import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
 import org.keycloak.protocol.oidc.utils.JWKSUtils;
@@ -102,6 +104,13 @@ public class DescriptionConverter {
             CertificateInfoHelper.updateClientRepresentationCertificateInfo(client, rep, JWTClientAuthenticator.ATTR_PREFIX);
         }
 
+        if (clientOIDC.getUserinfoSignedResponseAlg() != null) {
+            String userInfoSignedResponseAlg = clientOIDC.getUserinfoSignedResponseAlg();
+            Algorithm algorithm = Enum.valueOf(Algorithm.class, userInfoSignedResponseAlg);
+
+            OIDCAdvancedConfigWrapper.fromClientRepresentation(client).setUserInfoSignedResponseAlg(algorithm);
+        }
+
         return client;
     }
 
@@ -152,6 +161,12 @@ public class DescriptionConverter {
         response.setRegistrationClientUri(uri.toString());
         response.setResponseTypes(getOIDCResponseTypes(client));
         response.setGrantTypes(getOIDCGrantTypes(client));
+
+        OIDCAdvancedConfigWrapper config = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
+        if (config.isUserInfoSignatureRequired()) {
+            response.setUserinfoSignedResponseAlg(config.getUserInfoSignedResponseAlg().toString());
+        }
+
         return response;
     }
 
diff --git a/services/src/main/java/org/keycloak/utils/MediaType.java b/services/src/main/java/org/keycloak/utils/MediaType.java
index 31ab972..c34858d 100644
--- a/services/src/main/java/org/keycloak/utils/MediaType.java
+++ b/services/src/main/java/org/keycloak/utils/MediaType.java
@@ -31,4 +31,7 @@ public class MediaType {
     public static final String APPLICATION_FORM_URLENCODED = javax.ws.rs.core.MediaType.APPLICATION_FORM_URLENCODED;
     public static final javax.ws.rs.core.MediaType APPLICATION_FORM_URLENCODED_TYPE = javax.ws.rs.core.MediaType.APPLICATION_FORM_URLENCODED_TYPE;
 
+    public static final String APPLICATION_JWT = "application/jwt";
+    public static final javax.ws.rs.core.MediaType APPLICATION_JWT_TYPE = new javax.ws.rs.core.MediaType("application", "jwt");
+
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/UserInfoClientUtil.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/UserInfoClientUtil.java
index dd6a3db..b5af7c9 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/UserInfoClientUtil.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/UserInfoClientUtil.java
@@ -28,6 +28,7 @@ import javax.ws.rs.core.UriBuilder;
 import org.junit.Assert;
 import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
 import org.keycloak.representations.UserInfo;
+import org.keycloak.utils.MediaType;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -51,6 +52,7 @@ public class UserInfoClientUtil {
 
     public static void testSuccessfulUserInfoResponse(Response response, String expectedUsername, String expectedEmail) {
         Assert.assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+        Assert.assertEquals(response.getHeaderString(HttpHeaders.CONTENT_TYPE), MediaType.APPLICATION_JSON);
 
         UserInfo userInfo = response.readEntity(UserInfo.class);
 
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 1c6b85b..9c78c4b 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
@@ -35,7 +35,9 @@ import org.keycloak.common.util.CollectionUtil;
 import org.keycloak.common.util.KeycloakUriBuilder;
 import org.keycloak.constants.ServiceUrlConstants;
 import org.keycloak.jose.jwk.JSONWebKeySet;
+import org.keycloak.jose.jws.Algorithm;
 import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
 import org.keycloak.protocol.oidc.OIDCLoginProtocol;
 import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
 import org.keycloak.protocol.oidc.utils.OIDCResponseType;
@@ -44,6 +46,7 @@ import org.keycloak.representations.JsonWebToken;
 import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
 import org.keycloak.representations.idm.ClientInitialAccessPresentation;
 import org.keycloak.representations.idm.ClientRegistrationTrustedHostRepresentation;
+import org.keycloak.representations.idm.ClientRepresentation;
 import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.representations.oidc.OIDCClientRepresentation;
 import org.keycloak.testsuite.Assert;
@@ -155,6 +158,7 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
         assertEquals(Arrays.asList("code", "none"), response.getResponseTypes());
         assertEquals(Arrays.asList(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.REFRESH_TOKEN), response.getGrantTypes());
         assertEquals(OIDCLoginProtocol.CLIENT_SECRET_BASIC, response.getTokenEndpointAuthMethod());
+        Assert.assertNull(response.getUserinfoSignedResponseAlg());
     }
 
     @Test
@@ -255,6 +259,21 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
         Assert.assertEquals(response.getClientId(), accessToken.getAudience()[0]);
     }
 
+    @Test
+    public void testSignaturesRequired() throws Exception {
+        OIDCClientRepresentation clientRep = createRep();
+        clientRep.setUserinfoSignedResponseAlg(Algorithm.RS256.toString());
+
+        OIDCClientRepresentation response = reg.oidc().create(clientRep);
+        Assert.assertEquals(Algorithm.RS256.toString(), response.getUserinfoSignedResponseAlg());
+        Assert.assertNotNull(response.getClientSecret());
+
+        // Test Keycloak representation
+        ClientRepresentation kcClient = getClient(response.getClientId());
+        OIDCAdvancedConfigWrapper config = OIDCAdvancedConfigWrapper.fromClientRepresentation(kcClient);
+        Assert.assertEquals(config.getUserInfoSignedResponseAlg(), Algorithm.RS256);
+    }
+
 
     // Client auth with signedJWT - helper methods
 
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 ab5c230..91ccc7e 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
@@ -86,6 +86,7 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
 
             Assert.assertNames(oidcConfig.getSubjectTypesSupported(), "public");
             Assert.assertNames(oidcConfig.getIdTokenSigningAlgValuesSupported(), Algorithm.RS256.toString());
+            Assert.assertNames(oidcConfig.getUserInfoSigningAlgValuesSupported(), Algorithm.RS256.toString());
 
             // Client authentication
             Assert.assertNames(oidcConfig.getTokenEndpointAuthMethodsSupported(), "client_secret_basic", "client_secret_post", "private_key_jwt");
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java
index 268333f..e0588e7 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java
@@ -21,19 +21,31 @@ import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.keycloak.OAuth2Constants;
+import org.keycloak.admin.client.resource.ClientResource;
 import org.keycloak.events.Details;
 import org.keycloak.events.Errors;
 import org.keycloak.events.EventType;
+import org.keycloak.jose.jws.Algorithm;
+import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.crypto.RSAProvider;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
 import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
 import org.keycloak.representations.AccessTokenResponse;
 import org.keycloak.representations.UserInfo;
+import org.keycloak.representations.idm.ClientRepresentation;
 import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.services.Urls;
 import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.Assert;
 import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.admin.ApiUtil;
 import org.keycloak.testsuite.util.ClientManager;
 import org.keycloak.testsuite.util.RealmBuilder;
 import org.keycloak.testsuite.util.UserInfoClientUtil;
 import org.keycloak.util.BasicAuthHelper;
+import org.keycloak.util.JsonSerialization;
+import org.keycloak.utils.MediaType;
 
 import javax.ws.rs.client.Client;
 import javax.ws.rs.client.ClientBuilder;
@@ -45,6 +57,7 @@ import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
 import javax.ws.rs.core.UriBuilder;
 import java.net.URI;
+import java.security.PublicKey;
 import java.util.List;
 
 import static org.junit.Assert.assertEquals;
@@ -153,6 +166,62 @@ public class UserInfoTest extends AbstractKeycloakTest {
     }
 
     @Test
+    public void testSuccessSignedResponse() throws Exception {
+        // Require signed userInfo request
+        ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
+        ClientRepresentation clientRep = clientResource.toRepresentation();
+        OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setUserInfoSignedResponseAlg(Algorithm.RS256);
+        clientResource.update(clientRep);
+
+        // test signed response
+        Client client = ClientBuilder.newClient();
+
+        try {
+            AccessTokenResponse accessTokenResponse = executeGrantAccessTokenRequest(client);
+
+            Response response = UserInfoClientUtil.executeUserInfoRequest_getMethod(client, accessTokenResponse.getToken());
+
+            events.expect(EventType.USER_INFO_REQUEST)
+                    .session(Matchers.notNullValue(String.class))
+                    .detail(Details.AUTH_METHOD, Details.VALIDATE_ACCESS_TOKEN)
+                    .detail(Details.USERNAME, "test-user@localhost")
+                    .detail(Details.SIGNATURE_REQUIRED, "true")
+                    .detail(Details.SIGNATURE_ALGORITHM, Algorithm.RS256.toString())
+                    .assertEvent();
+
+            // Check signature and content
+            RealmRepresentation realmRep = adminClient.realm("test").toRepresentation();
+            PublicKey publicKey = KeycloakModelUtils.getPublicKey(realmRep.getPublicKey());
+
+            Assert.assertEquals(200, response.getStatus());
+            Assert.assertEquals(response.getHeaderString(HttpHeaders.CONTENT_TYPE), MediaType.APPLICATION_JWT);
+            String signedResponse = response.readEntity(String.class);
+            response.close();
+
+            JWSInput jwsInput = new JWSInput(signedResponse);
+            Assert.assertTrue(RSAProvider.verify(jwsInput, publicKey));
+
+            UserInfo userInfo = JsonSerialization.readValue(jwsInput.getContent(), UserInfo.class);
+
+            Assert.assertNotNull(userInfo);
+            Assert.assertNotNull(userInfo.getSubject());
+            Assert.assertEquals("test-user@localhost", userInfo.getEmail());
+            Assert.assertEquals("test-user@localhost", userInfo.getPreferredUsername());
+
+            Assert.assertTrue(userInfo.hasAudience("test-app"));
+            String expectedIssuer = Urls.realmIssuer(new URI(AUTH_SERVER_ROOT), "test");
+            Assert.assertEquals(expectedIssuer, userInfo.getIssuer());
+
+        } finally {
+            client.close();
+        }
+
+        // Revert signed userInfo request
+        OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setUserInfoSignedResponseAlg(null);
+        clientResource.update(clientRep);
+    }
+
+    @Test
     public void testSessionExpired() throws Exception {
         Client client = ClientBuilder.newClient();
 
@@ -235,6 +304,7 @@ public class UserInfoTest extends AbstractKeycloakTest {
                 .session(Matchers.notNullValue(String.class))
                 .detail(Details.AUTH_METHOD, Details.VALIDATE_ACCESS_TOKEN)
                 .detail(Details.USERNAME, "test-user@localhost")
+                .detail(Details.SIGNATURE_REQUIRED, "false")
                 .assertEvent();
         UserInfoClientUtil.testSuccessfulUserInfoResponse(response, "test-user@localhost", "test-user@localhost");
     }
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 a753ae9..b1b08de 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
@@ -236,6 +236,10 @@ idp-sso-relay-state=IDP Initiated SSO Relay State
 idp-sso-relay-state.tooltip=Relay state you want to send with SAML request when you want to do IDP Initiated SSO.
 web-origins=Web Origins
 web-origins.tooltip=Allowed CORS origins. To permit all origins of Valid Redirect URIs add '+'. To permit all origins add '*'.
+fine-oidc-endpoint-conf=Fine Grain OpenID Connect Configuration
+fine-oidc-endpoint-conf.tooltip=Expand this section to configure advanced settings of this client related to OpenID Connect protocol
+user-info-signed-response-alg=User Info Signed Response Algorithm
+user-info-signed-response-alg.tooltip=JWA algorithm used for signed User Info Endpoint response. If set to 'unsigned', then User Info Response won't be signed and will be returned in application/json format.
 fine-saml-endpoint-conf=Fine Grain SAML Endpoint Configuration
 fine-saml-endpoint-conf.tooltip=Expand this section to configure exact URLs for Assertion Consumer and Single Logout Service.
 assertion-consumer-post-binding-url=Assertion Consumer Service POST Binding URL
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 acbe6ec..3f13573 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
@@ -792,6 +792,11 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
         {name: "INCLUSIVE_WITH_COMMENTS", value: "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"}
     ];
 
+    $scope.oidcSignatureAlgorithms = [
+        "unsigned",
+        "RS256"
+    ];
+
     $scope.realm = realm;
     $scope.samlAuthnStatement = false;
     $scope.samlMultiValuedRoles = false;
@@ -892,6 +897,8 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
                 $scope.samlForcePostBinding = false;
             }
         }
+
+        $scope.userInfoSignedResponseAlg = getSignatureAlgorithm('user.info.response');
     }
 
     if (!$scope.create) {
@@ -956,6 +963,25 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
         $scope.client.attributes['saml_name_id_format'] = $scope.nameIdFormat;
     };
 
+    $scope.changeUserInfoSignedResponseAlg = function() {
+        changeSignatureAlgorithm('user.info.response', $scope.userInfoSignedResponseAlg);
+    };
+
+    function changeSignatureAlgorithm(attrPrefix, attrValue) {
+        var attrName = attrPrefix + '.signature.alg';
+        if (attrValue === 'unsigned') {
+            $scope.client.attributes[attrName] = null;
+        } else {
+            $scope.client.attributes[attrName] = attrValue;
+        }
+    }
+
+    function getSignatureAlgorithm(attrPrefix) {
+        var attrName = attrPrefix + '.signature.alg';
+        var attrVal = $scope.client.attributes[attrName];
+        return attrVal==null ? 'unsigned' : attrVal;
+    }
+
     $scope.$watch(function() {
         return $location.path();
     }, function() {
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
index 1378f78..af10a22 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
@@ -333,6 +333,23 @@
             </div>
         </fieldset>
 
+        <fieldset data-ng-show="protocol == 'openid-connect'">
+            <legend collapsed><span class="text">{{:: 'fine-oidc-endpoint-conf' | translate}}</span>  <kc-tooltip>{{:: 'fine-oidc-endpoint-conf.tooltip' | translate}}</kc-tooltip></legend>
+            <div class="form-group clearfix block" data-ng-show="protocol == 'openid-connect'">
+                <label class="col-md-2 control-label" for="userInfoSignedResponseAlg">{{:: 'user-info-signed-response-alg' | translate}}</label>
+                <div class="col-sm-6">
+                    <div>
+                        <select class="form-control" id="userInfoSignedResponseAlg"
+                                ng-change="changeUserInfoSignedResponseAlg()"
+                                ng-model="userInfoSignedResponseAlg"
+                                ng-options="sig for sig in oidcSignatureAlgorithms">
+                        </select>
+                    </div>
+                </div>
+                <kc-tooltip>{{:: 'user-info-signed-response-alg.tooltip' | translate}}</kc-tooltip>
+            </div>
+        </fieldset>
+
         <div class="form-group">
             <div class="col-md-10 col-md-offset-2" data-ng-show="access.manageClients">
                 <button kc-save  data-ng-disabled="!changed">{{:: 'save' | translate}}</button>