parent
603a55796d
commit
0a832fc744
12 changed files with 638 additions and 6 deletions
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* Copyright 2022 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.mappers;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.ClientSessionContext;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ProtocolMapperModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.representations.ClaimsRepresentation;
|
||||
import org.keycloak.representations.IDToken;
|
||||
import org.keycloak.services.clientpolicy.executor.IntentClientBindCheckExecutor;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.util.TokenUtil;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
||||
*/
|
||||
public class ClaimsParameterWithValueIdTokenMapper extends AbstractOIDCProtocolMapper implements OIDCIDTokenMapper {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(ClaimsParameterWithValueIdTokenMapper.class);
|
||||
|
||||
public static final String PROVIDER_ID = "oidc-claims-param-value-idtoken-mapper";
|
||||
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<>();
|
||||
|
||||
public static final String CLAIM_NAME = "claim.name";
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property = new ProviderConfigProperty();
|
||||
property.setName(CLAIM_NAME);
|
||||
property.setLabel("Claim name");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
property.setHelpText("Name of the claim you want to set its value. 'true' and 'false can be used for boolean values.");
|
||||
configProperties.add(property);
|
||||
|
||||
OIDCAttributeMapperHelper.addIncludeInTokensConfig(configProperties, ClaimsParameterWithValueIdTokenMapper.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return TOKEN_MAPPER_CATEGORY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Claims parameter with value ID Token";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Claims specified by Claims parameter with value are put into an ID token.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession keycloakSession, ClientSessionContext clientSessionCtx) {
|
||||
String claims = clientSessionCtx.getClientSession().getNote(OIDCLoginProtocol.CLAIMS_PARAM);
|
||||
if (claims == null) return;
|
||||
|
||||
if (TokenUtil.TOKEN_TYPE_ID.equals(token.getType())) {
|
||||
putClaims(ClaimsRepresentation.ClaimContext.ID_TOKEN, claims, token, mappingModel, userSession);
|
||||
}
|
||||
}
|
||||
|
||||
private void putClaims(ClaimsRepresentation.ClaimContext tokenType, String claims, IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
|
||||
String claimName = mappingModel.getConfig().get(CLAIM_NAME);
|
||||
if (claimName == null) return;
|
||||
|
||||
ClaimsRepresentation claimsRep = null;
|
||||
|
||||
try {
|
||||
claimsRep = JsonSerialization.readValue(claims, ClaimsRepresentation.class);
|
||||
} catch (IOException e) {
|
||||
LOGGER.warn("Invalid claims parameter", e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!claimsRep.isPresent(claimName, tokenType) || claimsRep.isPresentAsNullClaim(claimName, tokenType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ClaimsRepresentation.ClaimValue<String> claimValue = claimsRep.getClaimValue(claimName, tokenType, String.class);
|
||||
if (!claimValue.isEssential()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String claim = claimValue.getValue();
|
||||
if (claim == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
HardcodedClaim hardcodedClaimMapper = new HardcodedClaim();
|
||||
hardcodedClaimMapper.setClaim(token, HardcodedClaim.create("hard", claimName, claim, "String", false, true), userSession);
|
||||
}
|
||||
|
||||
public static ProtocolMapperModel createMapper(String name, String attributeValue, boolean idToken) {
|
||||
ProtocolMapperModel mapper = new ProtocolMapperModel();
|
||||
mapper.setName(name);
|
||||
mapper.setProtocolMapper(PROVIDER_ID);
|
||||
mapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
Map<String, String> config = new HashMap<String, String>();
|
||||
config.put(CLAIM_NAME, attributeValue);
|
||||
if (idToken) config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true");
|
||||
mapper.setConfig(config);
|
||||
return mapper;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,220 @@
|
|||
/*
|
||||
* Copyright 2022 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.services.clientpolicy.executor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
|
||||
import org.keycloak.representations.ClaimsRepresentation;
|
||||
import org.keycloak.representations.idm.ClientPolicyExecutorConfigurationRepresentation;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyContext;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyException;
|
||||
import org.keycloak.services.clientpolicy.context.AuthorizationRequestContext;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
||||
*/
|
||||
public class IntentClientBindCheckExecutor implements ClientPolicyExecutorProvider<IntentClientBindCheckExecutor.Configuration> {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(IntentClientBindCheckExecutor.class);
|
||||
|
||||
private final KeycloakSession session;
|
||||
private Configuration configuration;
|
||||
|
||||
public IntentClientBindCheckExecutor(KeycloakSession session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProviderId() {
|
||||
return IntentClientBindCheckExecutorFactory.PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupConfiguration(IntentClientBindCheckExecutor.Configuration config) {
|
||||
this.configuration = Optional.ofNullable(config).orElse(createDefaultConfiguration());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<Configuration> getExecutorConfigurationClass() {
|
||||
return Configuration.class;
|
||||
}
|
||||
|
||||
public static class Configuration extends ClientPolicyExecutorConfigurationRepresentation {
|
||||
|
||||
@JsonProperty("intent-client-bind-check-endpoint")
|
||||
protected String intentClientBindCheckEndpoint;
|
||||
|
||||
@JsonProperty("intent-name")
|
||||
protected String intentName;
|
||||
|
||||
public String getIntentClientBindCheckEndpoint() {
|
||||
return intentClientBindCheckEndpoint;
|
||||
}
|
||||
|
||||
public void setIntentClientBindCheckEndpoint(String intentClientBindCheckEndpoint) {
|
||||
this.intentClientBindCheckEndpoint = intentClientBindCheckEndpoint;
|
||||
}
|
||||
|
||||
public String getIntentName() {
|
||||
return intentName;
|
||||
}
|
||||
|
||||
public void setIntentName(String intentName) {
|
||||
this.intentName = intentName;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeOnEvent(ClientPolicyContext context) throws ClientPolicyException {
|
||||
switch (context.getEvent()) {
|
||||
case AUTHORIZATION_REQUEST:
|
||||
checkIntentClientBind((AuthorizationRequestContext)context);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private Configuration createDefaultConfiguration() {
|
||||
Configuration conf = new Configuration();
|
||||
return conf;
|
||||
}
|
||||
|
||||
private void checkIntentClientBind(AuthorizationRequestContext context) throws ClientPolicyException {
|
||||
if (!isValidIntentClientBindCheckEndpoint()) {
|
||||
throw new ClientPolicyException(OAuthErrorException.SERVER_ERROR, "invalid Intent Client Bind Check Endpoint setting");
|
||||
}
|
||||
ClientModel client = session.getContext().getClient();
|
||||
String clientId = client.getClientId();
|
||||
String intentId = retrieveIntentId(context.getAuthorizationEndpointRequest());
|
||||
IntentBindCheckRequest request = new IntentBindCheckRequest();
|
||||
request.setClientId(clientId);
|
||||
request.setIntentId(intentId);
|
||||
SimpleHttp simpleHttp = SimpleHttp.doPost(configuration.getIntentClientBindCheckEndpoint(), session)
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
|
||||
.json(request);
|
||||
IntentBindCheckResponse response = null;
|
||||
try {
|
||||
response = simpleHttp.asJson(IntentBindCheckResponse.class);
|
||||
} catch (IOException e) {
|
||||
logger.warnv("HTTP connection failure: {0}", e);
|
||||
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "checking intent bound with client failed");
|
||||
}
|
||||
if (!response.isBound.booleanValue()) {
|
||||
logger.tracev("Not Bound: intentName = {0}, intentId = {1}, clientId = {2}", configuration.getIntentName(), intentId, clientId);
|
||||
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "The intent is not bound with the client");
|
||||
}
|
||||
logger.tracev("Bound: intentName = {0}, intentId = {1}, clientId = {2}", configuration.getIntentName(), intentId, clientId);
|
||||
}
|
||||
|
||||
private String retrieveIntentId(AuthorizationEndpointRequest request) throws ClientPolicyException {
|
||||
String claims = request.getClaims();
|
||||
if (claims == null || claims.isEmpty()) {
|
||||
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "no claim for an intent value in an authorization request");
|
||||
}
|
||||
|
||||
String intentName = configuration.getIntentName();
|
||||
if (intentName == null || intentName.isEmpty()) {
|
||||
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "invalid intent name setting");
|
||||
}
|
||||
|
||||
ClaimsRepresentation claimsRep = null;
|
||||
|
||||
try {
|
||||
claimsRep = JsonSerialization.readValue(claims, ClaimsRepresentation.class);
|
||||
} catch (IOException e) {
|
||||
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "invalid claim for an intent value");
|
||||
}
|
||||
|
||||
if(!claimsRep.isPresent(intentName, ClaimsRepresentation.ClaimContext.ID_TOKEN) || claimsRep.isPresentAsNullClaim(intentName, ClaimsRepresentation.ClaimContext.ID_TOKEN)) {
|
||||
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "no claim for an intent value for ID token");
|
||||
}
|
||||
|
||||
ClaimsRepresentation.ClaimValue<String> claimValue = claimsRep.getClaimValue(intentName, ClaimsRepresentation.ClaimContext.ID_TOKEN, String.class);
|
||||
if (!claimValue.isEssential()) {
|
||||
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "not specifying a claim for an intent as essential claim");
|
||||
}
|
||||
|
||||
String value = claimValue.getValue();
|
||||
if (value == null) {
|
||||
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "invalid intent value");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private boolean isValidIntentClientBindCheckEndpoint() {
|
||||
String endpoint = configuration.getIntentClientBindCheckEndpoint();
|
||||
if (endpoint == null) return false;
|
||||
if (!endpoint.startsWith("http://") && !endpoint.startsWith("https://")) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static class IntentBindCheckRequest implements Serializable {
|
||||
|
||||
@JsonProperty("intent_id")
|
||||
private String intentId;
|
||||
|
||||
@JsonProperty("client_id")
|
||||
private String clientId;
|
||||
|
||||
public String getIntentId() {
|
||||
return intentId;
|
||||
}
|
||||
|
||||
public void setIntentId(String intentId) {
|
||||
this.intentId = intentId;
|
||||
}
|
||||
|
||||
public String getClientId() {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
public void setClientId(String clientId) {
|
||||
this.clientId = clientId;
|
||||
}
|
||||
}
|
||||
|
||||
public static class IntentBindCheckResponse implements Serializable {
|
||||
|
||||
@JsonProperty("is_bound")
|
||||
private Boolean isBound;
|
||||
|
||||
public Boolean getIsBound() {
|
||||
return isBound;
|
||||
}
|
||||
|
||||
public void setIsBound(Boolean isBound) {
|
||||
this.isBound = isBound;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright 2022 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.services.clientpolicy.executor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.Config.Scope;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
public class IntentClientBindCheckExecutorFactory implements ClientPolicyExecutorProviderFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "intent-client-bind-checker";
|
||||
|
||||
public static final String INTENT_CLIENT_BIND_CHECK_ENDPOINT = "intent-client-bind-check-endpoint";
|
||||
|
||||
private static final ProviderConfigProperty INTENT_CLIENT_BIND_CHECK_ENDPOINT_PROPERTY = new ProviderConfigProperty(
|
||||
INTENT_CLIENT_BIND_CHECK_ENDPOINT, "Intent Client Bind Check Endpoint", "Endpoint for checking if openbanking_intent_id is bound with a client.",
|
||||
ProviderConfigProperty.STRING_TYPE, "https://rs.keycloak-fapi.org/check-intent-client-bound");
|
||||
|
||||
@Override
|
||||
public ClientPolicyExecutorProvider create(KeycloakSession session) {
|
||||
return new IntentClientBindCheckExecutor(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Scope config) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "The executor checks if openbanking_intent_id is bound with a client.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return new ArrayList<>(Arrays.asList(INTENT_CLIENT_BIND_CHECK_ENDPOINT_PROPERTY));
|
||||
}
|
||||
|
||||
}
|
|
@ -44,3 +44,4 @@ org.keycloak.protocol.saml.mappers.SAMLAudienceProtocolMapper
|
|||
org.keycloak.protocol.saml.mappers.SAMLAudienceResolveProtocolMapper
|
||||
org.keycloak.protocol.oidc.mappers.ClaimsParameterTokenMapper
|
||||
org.keycloak.protocol.saml.mappers.UserAttributeNameIdMapper
|
||||
org.keycloak.protocol.oidc.mappers.ClaimsParameterWithValueIdTokenMapper
|
|
@ -17,3 +17,4 @@ org.keycloak.services.clientpolicy.executor.SecureLogoutExecutorFactory
|
|||
org.keycloak.services.clientpolicy.executor.RejectResourceOwnerPasswordCredentialsGrantExecutorFactory
|
||||
org.keycloak.services.clientpolicy.executor.ClientSecretRotationExecutorFactory
|
||||
org.keycloak.services.clientpolicy.executor.RejectRequestExecutorFactory
|
||||
org.keycloak.services.clientpolicy.executor.IntentClientBindCheckExecutorFactory
|
|
@ -67,6 +67,7 @@ public class TestApplicationResourceProvider implements RealmResourceProvider {
|
|||
|
||||
private final ConcurrentMap<String, TestAuthenticationChannelRequest> authenticationChannelRequests;
|
||||
private final ConcurrentMap<String, ClientNotificationEndpointRequest> cibaClientNotifications;
|
||||
private final ConcurrentMap<String, String> intentClientBindings;
|
||||
|
||||
@Context
|
||||
HttpRequest request;
|
||||
|
@ -78,7 +79,8 @@ public class TestApplicationResourceProvider implements RealmResourceProvider {
|
|||
BlockingQueue<TestAvailabilityAction> adminTestAvailabilityAction,
|
||||
TestApplicationResourceProviderFactory.OIDCClientData oidcClientData,
|
||||
ConcurrentMap<String, TestAuthenticationChannelRequest> authenticationChannelRequests,
|
||||
ConcurrentMap<String, ClientNotificationEndpointRequest> cibaClientNotifications) {
|
||||
ConcurrentMap<String, ClientNotificationEndpointRequest> cibaClientNotifications,
|
||||
ConcurrentMap<String, String> intentClientBindings) {
|
||||
this.session = session;
|
||||
this.adminLogoutActions = adminLogoutActions;
|
||||
this.backChannelLogoutTokens = backChannelLogoutTokens;
|
||||
|
@ -88,6 +90,7 @@ public class TestApplicationResourceProvider implements RealmResourceProvider {
|
|||
this.oidcClientData = oidcClientData;
|
||||
this.authenticationChannelRequests = authenticationChannelRequests;
|
||||
this.cibaClientNotifications = cibaClientNotifications;
|
||||
this.intentClientBindings = intentClientBindings;
|
||||
}
|
||||
|
||||
@POST
|
||||
|
@ -256,7 +259,7 @@ public class TestApplicationResourceProvider implements RealmResourceProvider {
|
|||
|
||||
@Path("/oidc-client-endpoints")
|
||||
public TestingOIDCEndpointsApplicationResource getTestingOIDCClientEndpoints() {
|
||||
return new TestingOIDCEndpointsApplicationResource(oidcClientData, authenticationChannelRequests, cibaClientNotifications);
|
||||
return new TestingOIDCEndpointsApplicationResource(oidcClientData, authenticationChannelRequests, cibaClientNotifications, intentClientBindings);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -54,11 +54,12 @@ public class TestApplicationResourceProviderFactory implements RealmResourceProv
|
|||
private final OIDCClientData oidcClientData = new OIDCClientData();
|
||||
private ConcurrentMap<String, TestAuthenticationChannelRequest> authenticationChannelRequests = new ConcurrentHashMap<>();
|
||||
private ConcurrentMap<String, ClientNotificationEndpointRequest> cibaClientNotifications = new ConcurrentHashMap<>();
|
||||
private ConcurrentMap<String, String> intentClientBindings = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public RealmResourceProvider create(KeycloakSession session) {
|
||||
TestApplicationResourceProvider provider = new TestApplicationResourceProvider(session, adminLogoutActions,
|
||||
backChannelLogoutTokens, frontChannelLogoutTokens, pushNotBeforeActions, testAvailabilityActions, oidcClientData, authenticationChannelRequests, cibaClientNotifications);
|
||||
backChannelLogoutTokens, frontChannelLogoutTokens, pushNotBeforeActions, testAvailabilityActions, oidcClientData, authenticationChannelRequests, cibaClientNotifications, intentClientBindings);
|
||||
|
||||
ResteasyProviderFactory.getInstance().injectProperties(provider);
|
||||
|
||||
|
|
|
@ -56,6 +56,7 @@ import org.keycloak.protocol.oidc.grants.ciba.endpoints.ClientNotificationEndpoi
|
|||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.services.ErrorResponseException;
|
||||
import org.keycloak.services.clientpolicy.executor.IntentClientBindCheckExecutor;
|
||||
import org.keycloak.services.managers.AppAuthManager;
|
||||
import org.keycloak.testsuite.rest.TestApplicationResourceProviderFactory;
|
||||
import org.keycloak.testsuite.rest.representation.TestAuthenticationChannelRequest;
|
||||
|
@ -103,13 +104,15 @@ public class TestingOIDCEndpointsApplicationResource {
|
|||
private final TestApplicationResourceProviderFactory.OIDCClientData clientData;
|
||||
private final ConcurrentMap<String, TestAuthenticationChannelRequest> authenticationChannelRequests;
|
||||
private final ConcurrentMap<String, ClientNotificationEndpointRequest> cibaClientNotifications;
|
||||
|
||||
private final ConcurrentMap<String, String> intentClientBindings;
|
||||
|
||||
public TestingOIDCEndpointsApplicationResource(TestApplicationResourceProviderFactory.OIDCClientData oidcClientData,
|
||||
ConcurrentMap<String, TestAuthenticationChannelRequest> authenticationChannelRequests, ConcurrentMap<String, ClientNotificationEndpointRequest> cibaClientNotifications) {
|
||||
ConcurrentMap<String, TestAuthenticationChannelRequest> authenticationChannelRequests, ConcurrentMap<String, ClientNotificationEndpointRequest> cibaClientNotifications,
|
||||
ConcurrentMap<String, String> intentClientBindings) {
|
||||
this.clientData = oidcClientData;
|
||||
this.authenticationChannelRequests = authenticationChannelRequests;
|
||||
this.cibaClientNotifications = cibaClientNotifications;
|
||||
this.intentClientBindings = intentClientBindings;
|
||||
}
|
||||
|
||||
@GET
|
||||
|
@ -728,4 +731,27 @@ public class TestingOIDCEndpointsApplicationResource {
|
|||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/bind-intent-with-client")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@NoCache
|
||||
public Response bindIntentWithClient(@QueryParam("intentId") String intentId, @QueryParam("clientId") String clientId) {
|
||||
intentClientBindings.put(intentId, clientId);
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/check-intent-client-bound")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@NoCache
|
||||
public IntentClientBindCheckExecutor.IntentBindCheckResponse checkIntentClientBound(IntentClientBindCheckExecutor.IntentBindCheckRequest request) {
|
||||
IntentClientBindCheckExecutor.IntentBindCheckResponse response = new IntentClientBindCheckExecutor.IntentBindCheckResponse();
|
||||
response.setIsBound(Boolean.FALSE);
|
||||
if (intentClientBindings.containsKey(request.getIntentId()) && intentClientBindings.get(request.getIntentId()).equals(request.getClientId())) {
|
||||
response.setIsBound(Boolean.TRUE);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,4 +58,11 @@ public class TestApplicationResourceUrls {
|
|||
|
||||
return builder.build().toString();
|
||||
}
|
||||
|
||||
public static String checkIntentClientBoundUri() {
|
||||
UriBuilder builder = oidcClientEndpoints()
|
||||
.path(TestOIDCEndpointsApplicationResource.class, "checkIntentClientBound");
|
||||
|
||||
return builder.build().toString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ package org.keycloak.testsuite.client.resources;
|
|||
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||
import org.keycloak.protocol.oidc.grants.ciba.endpoints.ClientNotificationEndpointRequest;
|
||||
import org.keycloak.services.clientpolicy.executor.IntentClientBindCheckExecutor;
|
||||
import org.keycloak.testsuite.rest.representation.TestAuthenticationChannelRequest;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
|
@ -145,4 +146,16 @@ public interface TestOIDCEndpointsApplicationResource {
|
|||
@NoCache
|
||||
ClientNotificationEndpointRequest getPushedCibaClientNotification(@QueryParam("clientNotificationToken") String clientNotificationToken);
|
||||
|
||||
@GET
|
||||
@Path("/bind-intent-with-client")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@NoCache
|
||||
Response bindIntentWithClient(@QueryParam("intentId") String intentId, @QueryParam("clientId") String clientId);
|
||||
|
||||
@POST
|
||||
@Path("/check-intent-client-bound")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@NoCache
|
||||
IntentClientBindCheckExecutor.IntentBindCheckResponse checkIntentClientBound(IntentClientBindCheckExecutor.IntentBindCheckRequest request);
|
||||
}
|
||||
|
|
|
@ -32,7 +32,13 @@ import java.util.Optional;
|
|||
import javax.ws.rs.BadRequestException;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.TreeNode;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.TextNode;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
|
@ -46,6 +52,7 @@ import org.junit.Test;
|
|||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.admin.client.resource.ClientResource;
|
||||
import org.keycloak.admin.client.resource.ProtocolMappersResource;
|
||||
import org.keycloak.admin.client.resource.RolesResource;
|
||||
import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
|
||||
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
|
||||
|
@ -59,18 +66,23 @@ import org.keycloak.crypto.Algorithm;
|
|||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.jose.jws.JWSBuilder;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.models.AdminRoles;
|
||||
import org.keycloak.models.CibaConfig;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.OAuth2DeviceConfig;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.ModelToRepresentation;
|
||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.mappers.ClaimsParameterTokenMapper;
|
||||
import org.keycloak.protocol.oidc.mappers.ClaimsParameterWithValueIdTokenMapper;
|
||||
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.AuthorizationResponseToken;
|
||||
import org.keycloak.representations.ClaimsRepresentation;
|
||||
import org.keycloak.representations.IDToken;
|
||||
import org.keycloak.representations.RefreshToken;
|
||||
import org.keycloak.representations.idm.ClientPolicyExecutorConfigurationRepresentation;
|
||||
|
@ -99,6 +111,7 @@ import org.keycloak.services.clientpolicy.executor.ConfidentialClientAcceptExecu
|
|||
import org.keycloak.services.clientpolicy.executor.ConsentRequiredExecutorFactory;
|
||||
import org.keycloak.services.clientpolicy.executor.FullScopeDisabledExecutorFactory;
|
||||
import org.keycloak.services.clientpolicy.executor.HolderOfKeyEnforcerExecutorFactory;
|
||||
import org.keycloak.services.clientpolicy.executor.IntentClientBindCheckExecutorFactory;
|
||||
import org.keycloak.services.clientpolicy.executor.PKCEEnforcerExecutorFactory;
|
||||
import org.keycloak.services.clientpolicy.executor.RejectRequestExecutorFactory;
|
||||
import org.keycloak.services.clientpolicy.executor.RejectResourceOwnerPasswordCredentialsGrantExecutorFactory;
|
||||
|
@ -115,6 +128,7 @@ import org.keycloak.testsuite.admin.ApiUtil;
|
|||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||
import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls;
|
||||
import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResource;
|
||||
import org.keycloak.testsuite.pages.AppPage;
|
||||
import org.keycloak.testsuite.pages.ErrorPage;
|
||||
import org.keycloak.testsuite.pages.LogoutConfirmPage;
|
||||
import org.keycloak.testsuite.pages.OAuth2DeviceVerificationPage;
|
||||
|
@ -124,9 +138,11 @@ import org.keycloak.testsuite.services.clientpolicy.condition.TestRaiseException
|
|||
import org.keycloak.testsuite.services.clientpolicy.executor.TestRaiseExceptionExecutorFactory;
|
||||
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
|
||||
import org.keycloak.testsuite.util.ClientBuilder;
|
||||
import org.keycloak.testsuite.util.ClientManager;
|
||||
import org.keycloak.testsuite.util.ClientPoliciesUtil;
|
||||
import org.keycloak.testsuite.util.MutualTLSUtils;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
import org.keycloak.testsuite.util.OAuthClient.AuthorizationEndpointResponse;
|
||||
import org.keycloak.testsuite.util.RoleBuilder;
|
||||
import org.keycloak.testsuite.util.ServerURLs;
|
||||
import org.keycloak.testsuite.util.UserBuilder;
|
||||
|
@ -143,6 +159,7 @@ import static org.junit.Assert.assertNotNull;
|
|||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
|
||||
import static org.keycloak.testsuite.admin.ApiUtil.findClientResourceByClientId;
|
||||
import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername;
|
||||
import static org.keycloak.testsuite.util.ClientPoliciesUtil.ClientPoliciesBuilder;
|
||||
import static org.keycloak.testsuite.util.ClientPoliciesUtil.ClientPolicyBuilder;
|
||||
|
@ -159,6 +176,7 @@ import static org.keycloak.testsuite.util.ClientPoliciesUtil.createClientUpdateS
|
|||
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createConsentRequiredExecutorConfig;
|
||||
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createFullScopeDisabledExecutorConfig;
|
||||
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createHolderOfKeyEnforceExecutorConfig;
|
||||
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createIntentClientBindCheckExecutorConfig;
|
||||
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createPKCEEnforceExecutorConfig;
|
||||
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createRejectisResourceOwnerPasswordCredentialsGrantExecutorConfig;
|
||||
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createSecureClientAuthenticatorExecutorConfig;
|
||||
|
@ -2990,6 +3008,128 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
|
|||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIntentClientBindCheck() throws Exception {
|
||||
final String intentName = "openbanking_intent_id";
|
||||
|
||||
// register profiles
|
||||
String json = (new ClientProfilesBuilder()).addProfile(
|
||||
(new ClientProfileBuilder()).createProfile(PROFILE_NAME, "Het Eerste Profiel")
|
||||
.addExecutor(IntentClientBindCheckExecutorFactory.PROVIDER_ID,
|
||||
createIntentClientBindCheckExecutorConfig(intentName, TestApplicationResourceUrls.checkIntentClientBoundUri()))
|
||||
.toRepresentation()
|
||||
).toString();
|
||||
updateProfiles(json);
|
||||
|
||||
// register policies
|
||||
json = (new ClientPoliciesBuilder()).addPolicy(
|
||||
(new ClientPolicyBuilder()).createPolicy(POLICY_NAME, "Het Eerste Beleid", Boolean.TRUE)
|
||||
.addCondition(ClientScopesConditionFactory.PROVIDER_ID,
|
||||
createClientScopesConditionConfig(ClientScopesConditionFactory.OPTIONAL, Arrays.asList("microprofile-jwt")))
|
||||
.addProfile(PROFILE_NAME)
|
||||
.toRepresentation()
|
||||
).toString();
|
||||
updatePolicies(json);
|
||||
|
||||
// create a client
|
||||
String clientId = generateSuffixedName(CLIENT_NAME);
|
||||
String clientSecret = "secret";
|
||||
createClientByAdmin(clientId, (ClientRepresentation clientRep) -> {
|
||||
clientRep.setSecret(clientSecret);
|
||||
clientRep.setStandardFlowEnabled(Boolean.TRUE);
|
||||
clientRep.setImplicitFlowEnabled(Boolean.TRUE);
|
||||
});
|
||||
ClientResource app = findClientResourceByClientId(adminClient.realm("test"), clientId);
|
||||
ProtocolMappersResource res = app.getProtocolMappers();
|
||||
res.createMapper(ModelToRepresentation.toRepresentation(ClaimsParameterWithValueIdTokenMapper.createMapper("claimsParameterWithValueIdTokenMapper", "openbanking_intent_id", true))).close();
|
||||
|
||||
// register a binding of an intent with different client
|
||||
String intentId = "123abc456xyz";
|
||||
String differentClientId = "test-app";
|
||||
Response r = testingClient.testApp().oidcClientEndpoints().bindIntentWithClient(intentId, differentClientId);
|
||||
assertEquals(204, r.getStatus());
|
||||
|
||||
// create a request object with claims
|
||||
String nonce = "naodfejawi37d";
|
||||
|
||||
ClaimsRepresentation claimsRep = new ClaimsRepresentation();
|
||||
ClaimsRepresentation.ClaimValue<String> claimValue = new ClaimsRepresentation.ClaimValue<>();
|
||||
claimValue.setEssential(Boolean.TRUE);
|
||||
claimValue.setValue(intentId);
|
||||
claimsRep.setIdTokenClaims(Collections.singletonMap(intentName, claimValue));
|
||||
|
||||
Map<String, Object> oidcRequest = new HashMap<>();
|
||||
oidcRequest.put(OIDCLoginProtocol.CLIENT_ID_PARAM, clientId);
|
||||
oidcRequest.put(OIDCLoginProtocol.NONCE_PARAM, nonce);
|
||||
oidcRequest.put(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, OIDCResponseType.CODE + " " + OIDCResponseType.ID_TOKEN);
|
||||
oidcRequest.put(OIDCLoginProtocol.REDIRECT_URI_PARAM, oauth.getRedirectUri());
|
||||
oidcRequest.put(OIDCLoginProtocol.CLAIMS_PARAM, claimsRep);
|
||||
oidcRequest.put(OIDCLoginProtocol.SCOPE_PARAM, "openid" + " " + "microprofile-jwt");
|
||||
String request = new JWSBuilder().jsonContent(oidcRequest).none();
|
||||
|
||||
// send an authorization request
|
||||
oauth.scope("openid" + " " + "microprofile-jwt");
|
||||
oauth.request(request);
|
||||
oauth.clientId(clientId);
|
||||
oauth.nonce(nonce);
|
||||
oauth.responseType(OIDCResponseType.CODE + " " + OIDCResponseType.ID_TOKEN);
|
||||
oauth.openLoginForm();
|
||||
assertEquals(OAuthErrorException.INVALID_REQUEST, oauth.getCurrentFragment().get(OAuth2Constants.ERROR));
|
||||
assertEquals("The intent is not bound with the client", oauth.getCurrentFragment().get(OAuth2Constants.ERROR_DESCRIPTION));
|
||||
|
||||
// register a binding of an intent with a valid client
|
||||
r = testingClient.testApp().oidcClientEndpoints().bindIntentWithClient(intentId, clientId);
|
||||
assertEquals(204, r.getStatus());
|
||||
|
||||
// send an authorization request
|
||||
oauth.doLogin(TEST_USER_NAME, TEST_USER_PASSWORD);
|
||||
|
||||
// check an authorization response
|
||||
EventRepresentation loginEvent = events.expectLogin().client(clientId).assertEvent();
|
||||
String sessionId = loginEvent.getSessionId();
|
||||
String codeId = loginEvent.getDetails().get(Details.CODE_ID);
|
||||
String code = oauth.getCurrentFragment().get(OAuth2Constants.CODE);
|
||||
OAuthClient.AuthorizationEndpointResponse authzResponse = new OAuthClient.AuthorizationEndpointResponse(oauth, true);
|
||||
JWSInput idToken = new JWSInput(authzResponse.getIdToken());
|
||||
ObjectMapper mapper = JsonSerialization.mapper;
|
||||
JsonParser parser = mapper.getFactory().createParser(idToken.readContentAsString());
|
||||
TreeNode treeNode = mapper.readTree(parser);
|
||||
String clientBoundIntentId = ((TextNode) treeNode.get(intentName)).asText();
|
||||
assertEquals(intentId, clientBoundIntentId);
|
||||
|
||||
// send a token request
|
||||
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, clientSecret);
|
||||
|
||||
// check a token response
|
||||
assertEquals(200, response.getStatusCode());
|
||||
events.expectCodeToToken(codeId, sessionId).client(clientId).assertEvent();
|
||||
idToken = new JWSInput(response.getIdToken());
|
||||
mapper = JsonSerialization.mapper;
|
||||
parser = mapper.getFactory().createParser(idToken.readContentAsString());
|
||||
treeNode = mapper.readTree(parser);
|
||||
clientBoundIntentId = ((TextNode) treeNode.get(intentName)).asText();
|
||||
assertEquals(intentId, clientBoundIntentId);
|
||||
|
||||
// logout
|
||||
oauth.doLogout(response.getRefreshToken(), clientSecret);
|
||||
events.expectLogout(response.getSessionState()).client(clientId).clearDetails().assertEvent();
|
||||
|
||||
// create a request object with invalid claims
|
||||
claimsRep = new ClaimsRepresentation();
|
||||
claimValue = new ClaimsRepresentation.ClaimValue<>();
|
||||
claimValue.setEssential(Boolean.TRUE);
|
||||
claimValue.setValue(intentId);
|
||||
claimsRep.setIdTokenClaims(Collections.singletonMap("other_intent_id", claimValue));
|
||||
oidcRequest.put(OIDCLoginProtocol.CLAIMS_PARAM, claimsRep);
|
||||
request = new JWSBuilder().jsonContent(oidcRequest).none();
|
||||
|
||||
// send an authorization request
|
||||
oauth.request(request);
|
||||
oauth.openLoginForm();
|
||||
assertEquals(OAuthErrorException.INVALID_REQUEST, oauth.getCurrentFragment().get(OAuth2Constants.ERROR));
|
||||
assertEquals("no claim for an intent value for ID token" , oauth.getCurrentFragment().get(OAuth2Constants.ERROR_DESCRIPTION));
|
||||
}
|
||||
|
||||
private void openVerificationPage(String verificationUri) {
|
||||
driver.navigate().to(verificationUri);
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ import org.keycloak.services.clientpolicy.condition.ClientUpdaterSourceRolesCond
|
|||
import org.keycloak.services.clientpolicy.executor.ConsentRequiredExecutor;
|
||||
import org.keycloak.services.clientpolicy.executor.FullScopeDisabledExecutor;
|
||||
import org.keycloak.services.clientpolicy.executor.HolderOfKeyEnforcerExecutor;
|
||||
import org.keycloak.services.clientpolicy.executor.IntentClientBindCheckExecutor;
|
||||
import org.keycloak.services.clientpolicy.executor.PKCEEnforcerExecutor;
|
||||
import org.keycloak.services.clientpolicy.executor.RejectResourceOwnerPasswordCredentialsGrantExecutor;
|
||||
import org.keycloak.services.clientpolicy.executor.SecureClientAuthenticatorExecutor;
|
||||
|
@ -220,6 +221,13 @@ public final class ClientPoliciesUtil {
|
|||
return config;
|
||||
}
|
||||
|
||||
public static IntentClientBindCheckExecutor.Configuration createIntentClientBindCheckExecutorConfig(String intentName, String endpoint) {
|
||||
IntentClientBindCheckExecutor.Configuration config = new IntentClientBindCheckExecutor.Configuration();
|
||||
config.setIntentName(intentName);
|
||||
config.setIntentClientBindCheckEndpoint(endpoint);
|
||||
return config;
|
||||
}
|
||||
|
||||
public static class ClientPoliciesBuilder {
|
||||
private final ClientPoliciesRepresentation policiesRep;
|
||||
|
||||
|
|
Loading…
Reference in a new issue