Scope parameter in Oauth 2.0 token exchange
Closes #21578 Signed-off-by: cgeorgilakis-grnet <cgeorgilakis@admin.grnet.gr>
This commit is contained in:
parent
778847a3ce
commit
ba8c22eaf0
3 changed files with 130 additions and 4 deletions
|
@ -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.
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ import org.keycloak.broker.provider.IdentityProviderFactory;
|
|||
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;
|
||||
|
@ -77,6 +78,8 @@ import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_CLIENT;
|
|||
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;
|
||||
|
@ -337,6 +340,30 @@ public class DefaultTokenExchangeProvider implements TokenExchangeProvider {
|
|||
}
|
||||
|
||||
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:
|
||||
|
|
|
@ -61,6 +61,8 @@ import jakarta.ws.rs.client.WebTarget;
|
|||
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;
|
||||
|
||||
|
@ -128,6 +130,16 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest {
|
|||
|
||||
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);
|
||||
|
@ -139,6 +151,7 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest {
|
|||
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");
|
||||
|
@ -223,6 +236,7 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest {
|
|||
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);
|
||||
|
@ -333,6 +347,92 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest {
|
|||
}
|
||||
}
|
||||
|
||||
@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 {
|
||||
|
@ -413,7 +513,7 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest {
|
|||
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());
|
||||
|
||||
|
|
Loading…
Reference in a new issue