keycloak-aplcache
Changes
services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java 11(+11 -0)
services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java 15(+15 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/UserInfoClientUtil.java 2(+2 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java 19(+19 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java 1(+1 -0)
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>