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 @@ -76,9 +76,8 @@ requested_issuer::
requested_subject::
_OPTIONAL._ This specifies a username or user id if your client wants to impersonate a different user.
scope::
_NOT IMPLEMENTED._ This parameter represents the target set of OAuth and OpenID Connect scopes the client
is requesting. It is not implemented at this time but will be once {project_name} has better support for
scopes in general.
_OPTIONAL._ This parameter represents the target set of OAuth and OpenID Connect scopes the client
is requesting. Returned scope is the Cartesian product of scope parameter and access token scope.

NOTE: We currently only support OpenID Connect and OAuth exchanges. Support for SAML based clients and identity providers may be added in the future depending on user demand.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.keycloak.broker.provider.IdentityProviderMapper;
import org.keycloak.broker.provider.IdentityProviderMapperSyncModeDelegate;
import org.keycloak.common.ClientConnection;
import org.keycloak.common.Profile;
import org.keycloak.common.constants.ServiceAccountConstants;
import org.keycloak.common.util.Base64Url;
import org.keycloak.events.Details;
Expand Down Expand Up @@ -77,6 +78,8 @@
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_ID;
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_USERNAME;

import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
Expand Down Expand Up @@ -337,6 +340,30 @@ protected Response exchangeClientToClient(UserModel targetUser, UserSessionModel
}

String scope = formParams.getFirst(OAuth2Constants.SCOPE);
if (token != null && token.getScope() != null && scope == null) {
scope = token.getScope();

Set<String> targetClientScopes = new HashSet<String>();
targetClientScopes.addAll(targetClient.getClientScopes(true).keySet());
targetClientScopes.addAll(targetClient.getClientScopes(false).keySet());
//from return scope remove scopes that are not default or optional scopes for targetClient
scope = Arrays.stream(scope.split(" ")).filter(s -> "openid".equals(s) || (targetClientScopes.contains(Profile.isFeatureEnabled(Profile.Feature.DYNAMIC_SCOPES) ? s.split(":")[0] : s))).collect(Collectors.joining(" "));
} else if (token != null && token.getScope() != null) {
String subjectTokenScopes = token.getScope();
if (Profile.isFeatureEnabled(Profile.Feature.DYNAMIC_SCOPES)) {
Set<String> subjectTokenScopesSet = Arrays.stream(subjectTokenScopes.split(" ")).map(s -> s.split(":")[0]).collect(Collectors.toSet());
scope = Arrays.stream(scope.split(" ")).filter(sc -> subjectTokenScopesSet.contains(sc.split(":")[0])).collect(Collectors.joining(" "));
} else {
Set<String> subjectTokenScopesSet = Arrays.stream(subjectTokenScopes.split(" ")).collect(Collectors.toSet());
scope = Arrays.stream(scope.split(" ")).filter(sc -> subjectTokenScopesSet.contains(sc)).collect(Collectors.joining(" "));
}

Set<String> targetClientScopes = new HashSet<String>();
targetClientScopes.addAll(targetClient.getClientScopes(true).keySet());
targetClientScopes.addAll(targetClient.getClientScopes(false).keySet());
//from return scope remove scopes that are not default or optional scopes for targetClient
scope = Arrays.stream(scope.split(" ")).filter(s -> "openid".equals(s) || (targetClientScopes.contains(Profile.isFeatureEnabled(Profile.Feature.DYNAMIC_SCOPES) ? s.split(":")[0] : s))).collect(Collectors.joining(" "));
}

switch (requestedTokenType) {
case OAuth2Constants.ACCESS_TOKEN_TYPE:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@
import jakarta.ws.rs.core.Form;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;

import java.util.Arrays;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -128,6 +130,16 @@ public static void setupRealm(KeycloakSession session) {

RoleModel impersonateRole = management.getRealmManagementClient().getRole(ImpersonationConstants.IMPERSONATION_ROLE);

ClientModel differentScopeClient = realm.addClient("different-scope-client");
differentScopeClient.setClientId("different-scope-client");
differentScopeClient.setPublicClient(false);
differentScopeClient.setDirectAccessGrantsEnabled(true);
differentScopeClient.setEnabled(true);
differentScopeClient.setSecret("secret");
differentScopeClient.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
differentScopeClient.setFullScopeAllowed(false);
differentScopeClient.removeClientScope(realm.getClientScopesStream().filter(scope->"email".equals(scope.getName())).findAny().get());

ClientModel clientExchanger = realm.addClient("client-exchanger");
clientExchanger.setClientId("client-exchanger");
clientExchanger.setPublicClient(false);
Expand All @@ -139,6 +151,7 @@ public static void setupRealm(KeycloakSession session) {
clientExchanger.addScopeMapping(impersonateRole);
clientExchanger.addProtocolMapper(UserSessionNoteMapper.createUserSessionNoteMapper(IMPERSONATOR_ID));
clientExchanger.addProtocolMapper(UserSessionNoteMapper.createUserSessionNoteMapper(IMPERSONATOR_USERNAME));
clientExchanger.addProtocolMapper(AudienceProtocolMapper.createClaimMapper("different-scope-client-audience", differentScopeClient.getClientId(), null, true, false, true));

ClientModel illegal = realm.addClient("illegal");
illegal.setClientId("illegal");
Expand Down Expand Up @@ -223,6 +236,7 @@ public static void setupRealm(KeycloakSession session) {
clientRep.addClient(directLegal.getId());
clientRep.addClient(noRefreshToken.getId());
clientRep.addClient(serviceAccount.getId());
clientRep.addClient(differentScopeClient.getId());

ResourceServer server = management.realmResourceServer();
Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(server, clientRep);
Expand Down Expand Up @@ -333,6 +347,92 @@ public void testExchangeUsingServiceAccount() throws Exception {
}
}

@Test
@UncaughtServerErrorExpected
public void testExchangeDifferentScopes() throws Exception {
testingClient.server().run(ClientTokenExchangeTest::setupRealm);

oauth.realm(TEST);
oauth.clientId("client-exchanger");
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "user", "password");
String accessToken = response.getAccessToken();
TokenVerifier<AccessToken> accessTokenVerifier = TokenVerifier.create(accessToken, AccessToken.class);
AccessToken token = accessTokenVerifier.parse().getToken();
Assert.assertEquals(token.getPreferredUsername(), "user");
Assert.assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));

{
response = oauth.doTokenExchange(TEST, accessToken, null, "different-scope-client", "secret");
String exchangedTokenString = response.getAccessToken();
TokenVerifier<AccessToken> verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class);
AccessToken exchangedToken = verifier.parse().getToken();
Assert.assertEquals("different-scope-client", exchangedToken.getIssuedFor());
Assert.assertNull(exchangedToken.getAudience());
Assert.assertEquals(exchangedToken.getPreferredUsername(), "user");
Assert.assertNames(Arrays.asList(exchangedToken.getScope().split(" ")),"profile","openid");
Assert.assertNull(exchangedToken.getEmailVerified());
}

{
response = oauth.doTokenExchange(TEST, accessToken, "target", "different-scope-client", "secret");
String exchangedTokenString = response.getAccessToken();
TokenVerifier<AccessToken> verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class);
AccessToken exchangedToken = verifier.parse().getToken();
Assert.assertEquals("different-scope-client", exchangedToken.getIssuedFor());
Assert.assertEquals("target", exchangedToken.getAudience()[0]);
Assert.assertEquals(exchangedToken.getPreferredUsername(), "user");
Assert.assertTrue(exchangedToken.getRealmAccess().isUserInRole("example"));
Assert.assertNames(Arrays.asList(exchangedToken.getScope().split(" ")),"profile","email","openid");
Assert.assertFalse(exchangedToken.getEmailVerified());
}

}

@Test
@UncaughtServerErrorExpected
public void testExchangeDifferentScopesWithScopeParameter() throws Exception {
testingClient.server().run(ClientTokenExchangeTest::setupRealm);

oauth.realm(TEST);
oauth.clientId("client-exchanger");
oauth.scope("openid profile email phone");
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "user", "password");
String accessToken = response.getAccessToken();
TokenVerifier<AccessToken> accessTokenVerifier = TokenVerifier.create(accessToken, AccessToken.class);
AccessToken token = accessTokenVerifier.parse().getToken();
Assert.assertEquals(token.getPreferredUsername(), "user");
Assert.assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));
Assert.assertNames(Arrays.asList(token.getScope().split(" ")),"profile", "email", "openid", "phone");
//change scopes for token exchange - profile,phone must be removed
oauth.scope("openid profile email");

{
response = oauth.doTokenExchange(TEST, accessToken, null, "different-scope-client", "secret");
String exchangedTokenString = response.getAccessToken();
TokenVerifier<AccessToken> verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class);
AccessToken exchangedToken = verifier.parse().getToken();
Assert.assertEquals("different-scope-client", exchangedToken.getIssuedFor());
Assert.assertNull(exchangedToken.getAudience());
Assert.assertEquals(exchangedToken.getPreferredUsername(), "user");
Assert.assertNames(Arrays.asList(exchangedToken.getScope().split(" ")),"profile", "openid");
Assert.assertNull(exchangedToken.getEmailVerified());
}

{
response = oauth.doTokenExchange(TEST, accessToken, "target", "different-scope-client", "secret");
String exchangedTokenString = response.getAccessToken();
TokenVerifier<AccessToken> verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class);
AccessToken exchangedToken = verifier.parse().getToken();
Assert.assertEquals("different-scope-client", exchangedToken.getIssuedFor());
Assert.assertEquals("target", exchangedToken.getAudience()[0]);
Assert.assertEquals(exchangedToken.getPreferredUsername(), "user");
Assert.assertTrue(exchangedToken.getRealmAccess().isUserInRole("example"));
Assert.assertNames(Arrays.asList(exchangedToken.getScope().split(" ")),"profile", "email","openid");
Assert.assertFalse(exchangedToken.getEmailVerified());
}
oauth.scope(null);
}

@Test
@UncaughtServerErrorExpected
public void testExchangeFromPublicClient() throws Exception {
Expand Down Expand Up @@ -413,7 +513,7 @@ public void testImpersonation() throws Exception {
TokenVerifier<AccessToken> verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class);
AccessToken exchangedToken = verifier.parse().getToken();
Assert.assertEquals("client-exchanger", exchangedToken.getIssuedFor());
Assert.assertNull(exchangedToken.getAudience());
assertNotNull(exchangedToken.getAudience());
Assert.assertEquals("impersonated-user", exchangedToken.getPreferredUsername());
Assert.assertNull(exchangedToken.getRealmAccess());

Expand Down