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 @@ -199,4 +199,7 @@ public final class Constants {
// Note used to store the acr values if it is matched by client policy condition
public static final String CLIENT_POLICY_REQUESTED_ACR = "client-policy-requested-acr";

//attribute name used to set client ids from requested audience in standard token exchange
public static final String REQUESTED_AUDIENCE_CLIENT_IDS = "audience-client-ids";

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,30 @@
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.common.ClientConnection;
import org.keycloak.common.Profile;
import org.keycloak.common.constants.ServiceAccountConstants;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.Constants;
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.TokenExchangeContext;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.rar.AuthorizationRequestContext;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.cors.Cors;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.services.util.AuthorizationContextUtil;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.RootAuthenticationSessionModel;
import org.keycloak.util.TokenUtil;
Expand Down Expand Up @@ -149,7 +153,24 @@ protected void validateAudience(AccessToken token, boolean disallowOnHolderOfTok
// For now, include "scope" parameter as is
@Override
protected String getRequestedScope(AccessToken token, List<ClientModel> targetAudienceClients) {
return params.getScope();
String scope = formParams.getFirst(OAuth2Constants.SCOPE);

boolean validScopes;
if (Profile.isFeatureEnabled(Profile.Feature.DYNAMIC_SCOPES)) {
AuthorizationRequestContext authorizationRequestContext = AuthorizationContextUtil.getAuthorizationRequestContextFromScopes(session, scope);
validScopes = TokenManager.isValidScope(session, scope, authorizationRequestContext, client, null);
} else {
validScopes = TokenManager.isValidScope(session, scope, client, null);
}

if (!validScopes) {
String errorMessage = "Invalid scopes: " + scope;
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_SCOPE, errorMessage, Response.Status.BAD_REQUEST);
}

return scope;
}

protected void setClientToContext(List<ClientModel> targetAudienceClients) {
Expand All @@ -174,6 +195,10 @@ protected Response exchangeClientToOIDCClient(UserModel targetUser, UserSessionM

updateUserSessionFromClientAuth(targetUserSession);

if (params.getAudience() != null && !targetAudienceClients.isEmpty()) {
clientSessionCtx.setAttribute(Constants.REQUESTED_AUDIENCE_CLIENT_IDS, targetAudienceClients.stream().map(ClientModel::getId).toArray(String[]::new));
}

TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager.responseBuilder(realm, client, event, this.session, targetUserSession, clientSessionCtx)
.generateAccessToken();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@

package org.keycloak.services.util;

import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
Expand All @@ -33,6 +33,7 @@
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RoleModel;
Expand Down Expand Up @@ -265,6 +266,12 @@ private boolean isClientScopePermittedForUser(ClientScopeModel clientScope) {
// Expand (resolve composite roles)
clientScopeRoles = RoleUtils.expandCompositeRoles(clientScopeRoles);

//remove roles that are not contained in requested audience
if (attributes.get(Constants.REQUESTED_AUDIENCE_CLIENT_IDS) != null) {
Set<String> requestedClientIdsFromAudience = Arrays.stream(getAttribute(Constants.REQUESTED_AUDIENCE_CLIENT_IDS, String[].class)).collect(Collectors.toSet());
clientScopeRoles.removeIf(role-> role.isClientRole() && !requestedClientIdsFromAudience.contains(role.getContainerId()));
}

// Check if expanded roles of clientScope has any intersection with expanded roles of user. If not, it is not permitted
clientScopeRoles.retainAll(getUserRoles());
return !clientScopeRoles.isEmpty();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

import java.util.List;

import org.apache.http.HttpStatus;
import jakarta.ws.rs.core.Response;
import org.hamcrest.MatcherAssert;
import org.junit.Assert;
import org.junit.FixMethodOrder;
Expand All @@ -36,9 +36,9 @@
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
import org.keycloak.testsuite.util.oauth.OAuthClient;

import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.junit.Assert.assertEquals;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
import static org.keycloak.testsuite.auth.page.AuthRealm.TEST;

Expand All @@ -60,53 +60,89 @@ public void addTestRealms(List<RealmRepresentation> testRealms) {
}

@Test
public void test01_scopeParamIncludedWithoutAudience() throws Exception {
String accessToken = resourceOwnerLogin();
public void testOptionalScopeParamRequestedWithoutAudience() throws Exception {
String accessToken = resourceOwnerLogin("john", "password", List.of("target-client1"), List.of("default-scope1"));;
oauth.scope("optional-scope2");
AccessTokenResponse response = oauth.doTokenExchange(accessToken, (String) null, "requester-client", "secret", null);
assertAudiencesAndScopes(response, List.of("target-client1", "target-client2"), List.of("default-scope1", "optional-scope2"));
}

@Test
public void test02_scopeParamIncludedAudienceIncluded() throws Exception {
String accessToken = resourceOwnerLogin();
oauth.scope("optional-scope2");
public void testAudienceRequested() throws Exception {
String accessToken = resourceOwnerLogin("john", "password", List.of("target-client1"), List.of("default-scope1"));;
AccessTokenResponse response = oauth.doTokenExchange(accessToken, List.of("target-client1"), "requester-client", "secret", null);
assertAudiencesAndScopes(response, List.of("target-client1"), List.of("default-scope1", "optional-scope2"));
assertAudiencesAndScopes(response, List.of("target-client1"), List.of("default-scope1"));
}

@Test
public void testUnavailableAudienceRequested() throws Exception {
String accessToken = resourceOwnerLogin("john", "password", List.of("target-client1"), List.of("default-scope1"));;
// The "target-client3" is valid client, but unavailable to the user. Request allowed, but "target-client3" audience will not be available
AccessTokenResponse response = oauth.doTokenExchange(accessToken, List.of("target-client1", "target-client3"), "requester-client", "secret", null);
assertAudiencesAndScopes(response, List.of("target-client1"), List.of("default-scope1"));
}

@Test
public void test03_scopeParamIncludedAudienceIncluded_unavailableAudience() throws Exception {
String accessToken = resourceOwnerLogin();
oauth.scope("optional-scope2");
public void testScopeNotAllowed() throws Exception {
String accessToken = resourceOwnerLogin("john", "password", List.of("target-client1"), List.of("default-scope1"));

// The "target-client3" is valid client, but unavailable to the user. Request allowed, but "target-client3" audience will not be available
//scope not allowed
oauth.scope("optional-scope3");
AccessTokenResponse response = oauth.doTokenExchange(accessToken, List.of("target-client1", "target-client3"), "requester-client", "secret", null);
assertAudiencesAndScopes(response, List.of("target-client1"), List.of("default-scope1", "optional-scope2"));
assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatusCode());

//scope that doesn't exist
oauth.scope("bad-scope");
response = oauth.doTokenExchange(accessToken, List.of("target-client1", "target-client3"), "requester-client", "secret", null);
assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatusCode());
}

@Test
public void testScopeFilter() throws Exception {
String accessToken = resourceOwnerLogin("john", "password", List.of("target-client1"), List.of("default-scope1"));
AccessTokenResponse response = oauth.doTokenExchange(accessToken, List.of( "target-client2"), "requester-client", "secret", null);
assertAudiencesAndScopes(response, List.of(), List.of());

oauth.scope("optional-scope2");
response = oauth.doTokenExchange(accessToken, List.of( "target-client2"), "requester-client", "secret", null);
assertAudiencesAndScopes(response, List.of("target-client2"), List.of( "optional-scope2"));

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

//just check that the exchanged token contains the optional-scope2 mapped by the realm role
accessToken = resourceOwnerLogin("mike", "password", List.of("target-client1"), List.of("default-scope1"));
oauth.scope("optional-scope2");
response = oauth.doTokenExchange(accessToken, List.of(), "requester-client", "secret", null);
assertAudiencesAndScopes(response, List.of("target-client1"), List.of("default-scope1", "optional-scope2"));

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

private String resourceOwnerLogin() throws Exception {
private String resourceOwnerLogin(String username, String password, List<String> audience, List<String> scope) throws Exception {
oauth.realm(TEST);
oauth.clientId("requester-client");
oauth.scope(null);
oauth.openid(false);
AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "john", "password");
AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", username, password);
TokenVerifier<AccessToken> accessTokenVerifier = TokenVerifier.create(response.getAccessToken(), AccessToken.class);
AccessToken token = accessTokenVerifier.parse().getToken();
assertAudiences(token, List.of("target-client1"));
assertScopes(token, List.of("default-scope1"));
assertAudiences(token, audience);
assertScopes(token, scope);
return response.getAccessToken();
}

private void assertAudiences(AccessToken token, List<String> expectedAudiences) {
MatcherAssert.assertThat("Incompatible audiences", List.of(token.getAudience()), containsInAnyOrder(expectedAudiences.toArray()));
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()));
}

private void assertScopes(AccessToken token, List<String> expectedScopes) {
MatcherAssert.assertThat("Incompatible scopes", List.of(token.getScope().split(" ")), containsInAnyOrder(expectedScopes.toArray()));
MatcherAssert.assertThat("Incompatible scopes", token.getScope().isEmpty() ? List.of() : List.of(token.getScope().split(" ")), containsInAnyOrder(expectedScopes.toArray()));
}

private void assertAudiencesAndScopes(AccessTokenResponse tokenExchangeResponse, List<String> expectedAudiences, List<String> expectedScopes) throws Exception {
Expand Down
Loading
Loading