Merge pull request #3174 from mposolda/master
KEYCLOAK-3416 Add support for signed Userinfo requests
This commit is contained in:
commit
e8efe10fc8
15 changed files with 367 additions and 8 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,4 +58,7 @@ public interface Details {
|
|||
|
||||
String CLIENT_AUTH_METHOD = "client_auth_method";
|
||||
|
||||
String SIGNATURE_REQUIRED = "signature_required";
|
||||
String SIGNATURE_ALGORITHM = "signature_algorithm";
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
|
@ -152,6 +165,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");
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue