Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public enum ClientPolicyEvent {
DEVICE_AUTHORIZATION_REQUEST,
DEVICE_TOKEN_REQUEST,
DEVICE_TOKEN_RESPONSE,
TOKEN_EXCHANGE_REQUEST,
RESOURCE_OWNER_PASSWORD_CREDENTIALS_REQUEST,
RESOURCE_OWNER_PASSWORD_CREDENTIALS_RESPONSE,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
import org.keycloak.events.EventType;
import org.keycloak.protocol.oidc.TokenExchangeContext;
import org.keycloak.protocol.oidc.TokenExchangeProvider;
import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.clientpolicy.context.TokenExchangeRequestContext;

/**
* OAuth 2.0 Authorization Code Grant
Expand Down Expand Up @@ -57,14 +60,24 @@ public Response process(Context context) {
tokenManager,
clientAuthAttributes);

return session.getKeycloakSessionFactory()
TokenExchangeProvider tokenExchangeProvider = session.getKeycloakSessionFactory()
.getProviderFactoriesStream(TokenExchangeProvider.class)
.sorted((f1, f2) -> f2.order() - f1.order())
.map(f -> session.getProvider(TokenExchangeProvider.class, f.getId()))
.filter(p -> p.supports(exchange))
.findFirst()
.orElseThrow(() -> new InternalServerErrorException("No token exchange provider available"))
.exchange(exchange);
.orElseThrow(() -> new InternalServerErrorException("No token exchange provider available"));

try {
//trigger if there is a supported token exchange provider
session.clientPolicy().triggerOnEvent(new TokenExchangeRequestContext(exchange));
} catch (ClientPolicyException cpe) {
event.detail(Details.REASON, cpe.getErrorDetail());
event.error(cpe.getError());
throw new CorsErrorResponseException(cors, cpe.getError(), cpe.getErrorDetail(), cpe.getErrorStatus());
}

return tokenExchangeProvider.exchange(exchange);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.oidc.TokenExchangeContext;
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
import org.keycloak.protocol.oidc.grants.ciba.channel.CIBAAuthenticationRequest;
import org.keycloak.protocol.oidc.grants.ciba.clientpolicy.context.BackchannelAuthenticationRequestContext;
Expand All @@ -40,6 +41,7 @@
import org.keycloak.services.clientpolicy.context.AuthorizationRequestContext;
import org.keycloak.services.clientpolicy.context.ServiceAccountTokenRequestContext;
import org.keycloak.services.clientpolicy.context.ServiceAccountTokenResponseContext;
import org.keycloak.services.clientpolicy.context.TokenExchangeRequestContext;
import org.keycloak.services.clientpolicy.context.TokenRequestContext;
import org.keycloak.services.clientpolicy.context.TokenResponseContext;

Expand Down Expand Up @@ -113,6 +115,9 @@ public ClientPolicyVote applyPolicy(ClientPolicyContext context) throws ClientPo
case BACKCHANNEL_TOKEN_RESPONSE:
if (isScopeMatched(((BackchannelTokenResponseContext)context).getParsedRequest())) return ClientPolicyVote.YES;
return ClientPolicyVote.NO;
case TOKEN_EXCHANGE_REQUEST:
if (isScopeMatched(((TokenExchangeRequestContext) context).getTokenExchangeContext())) return ClientPolicyVote.YES;
return ClientPolicyVote.NO;
default:
return ClientPolicyVote.ABSTAIN;
}
Expand All @@ -133,6 +138,11 @@ private boolean isScopeMatched(CIBAAuthenticationRequest request) {
return isScopeMatched(request.getScope(), session.getContext().getRealm().getClientByClientId(request.getClient().getClientId()));
}

private boolean isScopeMatched(TokenExchangeContext context) {
if (context == null) return false;
return isScopeMatched(context.getParams().getScope(), context.getClient());
}

private boolean isScopeMatched(String explicitScopes, ClientModel client) {
if (explicitScopes == null) explicitScopes = "";
Collection<String> explicitSpecifiedScopes = new HashSet<>(Arrays.asList(explicitScopes.split(" ")));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright 2025 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.context;

import org.keycloak.protocol.oidc.TokenExchangeContext;
import org.keycloak.services.clientpolicy.ClientPolicyContext;
import org.keycloak.services.clientpolicy.ClientPolicyEvent;

/**
* @author <a href="mailto:[email protected]">Giuseppe Graziano</a>
*/
public class TokenExchangeRequestContext implements ClientPolicyContext {

private final TokenExchangeContext tokenExchangeContext;

public TokenExchangeRequestContext(TokenExchangeContext tokenExchangeContext) {
this.tokenExchangeContext = tokenExchangeContext;
}

@Override
public ClientPolicyEvent getEvent() {
return ClientPolicyEvent.TOKEN_EXCHANGE_REQUEST;
}


public TokenExchangeContext getTokenExchangeContext() {
return tokenExchangeContext;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,23 @@
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.TokenVerifier;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.common.Profile;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.services.clientpolicy.ClientPolicyEvent;
import org.keycloak.services.clientpolicy.condition.ClientScopesConditionFactory;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.arquillian.annotation.UncaughtServerErrorExpected;
import org.keycloak.testsuite.client.policies.AbstractClientPoliciesTest;
import org.keycloak.testsuite.pages.ConsentPage;
import org.keycloak.testsuite.services.clientpolicy.executor.TestRaiseExceptionExecutorFactory;
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
import org.keycloak.testsuite.util.ClientPoliciesUtil;
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
import org.keycloak.testsuite.util.oauth.TokenExchangeRequest;
import org.keycloak.util.TokenUtil;
Expand All @@ -56,12 +58,14 @@
import static org.junit.Assert.assertNull;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
import static org.keycloak.testsuite.auth.page.AuthRealm.TEST;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createClientScopesConditionConfig;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createTestRaiseExeptionExecutorConfig;

/**
* @author <a href="mailto:[email protected]">Marek Posolda</a>
*/
@EnableFeature(value = Profile.Feature.TOKEN_EXCHANGE_STANDARD_V2, skipRestart = true)
public class StandardTokenExchangeV2Test extends AbstractKeycloakTest {
public class StandardTokenExchangeV2Test extends AbstractClientPoliciesTest {

@Rule
public AssertEvents events = new AssertEvents(this);
Expand Down Expand Up @@ -435,6 +439,10 @@ public void testScopeFilter() throws Exception {
assertEquals(OAuthErrorException.INVALID_REQUEST, response.getError());
assertEquals("Requested audience not available: target-client2", response.getErrorDescription());

oauth.scope("optional-scope2");
response = tokenExchange(accessToken, "requester-client", "secret", List.of("target-client1"), null);
assertAudiencesAndScopes(response, List.of("target-client1"), List.of("default-scope1"));

oauth.scope("optional-scope2");
response = tokenExchange(accessToken, "requester-client", "secret", List.of("target-client2"), null);
assertAudiencesAndScopes(response, List.of("target-client2"), List.of("optional-scope2"));
Expand Down Expand Up @@ -549,6 +557,40 @@ public void testOfflineAccessNotAllowed() throws Exception {
}
}

@Test
public void testClientPolicies() throws Exception {

String json = (new ClientPoliciesUtil.ClientProfilesBuilder()).addProfile(
(new ClientPoliciesUtil.ClientProfileBuilder()).createProfile(PROFILE_NAME, "Profilo")
.addExecutor(TestRaiseExceptionExecutorFactory.PROVIDER_ID,
createTestRaiseExeptionExecutorConfig(List.of(ClientPolicyEvent.TOKEN_EXCHANGE_REQUEST)))
.toRepresentation()
).toString();
updateProfiles(json);

// register policy with condition on client scope optional-scope2
json = (new ClientPoliciesUtil.ClientPoliciesBuilder()).addPolicy(
(new ClientPoliciesUtil.ClientPolicyBuilder()).createPolicy(POLICY_NAME, "Client Scope Policy", Boolean.TRUE)
.addCondition(ClientScopesConditionFactory.PROVIDER_ID,
createClientScopesConditionConfig(ClientScopesConditionFactory.ANY, List.of("optional-scope2")))
.addProfile(PROFILE_NAME)
.toRepresentation()
).toString();
updatePolicies(json);

String accessToken = resourceOwnerLogin("john", "password", "subject-client", "secret");

AccessTokenResponse response = tokenExchange(accessToken, "requester-client", "secret", List.of("target-client1"), null);
assertAudiencesAndScopes(response, List.of("target-client1"), List.of("default-scope1"));

//block token exchange request if optional-scope2 is requested
oauth.scope("optional-scope2");
response = tokenExchange(accessToken, "requester-client", "secret", List.of("target-client2"), null);
assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatusCode());
assertEquals(ClientPolicyEvent.TOKEN_EXCHANGE_REQUEST.toString(), response.getError());
assertEquals("Exception thrown intentionally", response.getErrorDescription());
}

private void assertAudiences(AccessToken token, List<String> expectedAudiences) {
MatcherAssert.assertThat("Incompatible audiences", token.getAudience() == null ? List.of() : List.of(token.getAudience()), containsInAnyOrder(expectedAudiences.toArray()));
MatcherAssert.assertThat("Incompatible resource access", token.getResourceAccess().keySet(), containsInAnyOrder(expectedAudiences.toArray()));
Expand Down
Loading