mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-10 15:32:05 -03:30
Client policy to enforce only downscoping in Token Exchange (#44030)
Closes #43931 Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
parent
281ced0ca8
commit
de49500393
@ -149,6 +149,7 @@ One of several purposes for this executor is to realize the security requirement
|
||||
* Enforce that <<_refresh_token_rotation,refresh token rotation>> is skipped and there is no refresh token returned from the refresh token response
|
||||
* Enforce a valid redirect URI that the OAuth 2.1 specification requires
|
||||
* Enforce SAML Redirect binding cannot be used or SAML requests and assertions are signed
|
||||
* Enforce scopes granted in link:{securing_apps_token_exchange_link}#_standard-token-exchange[Standard token exchange] are restricted to the ones present in the initial `subject_token`. This executor only allows downscoping of the presented assertion. An error is returned if any extra scope, not originally granted to the JWT, is requested.
|
||||
|
||||
Another available executor is the `auth-flow-enforce`, which can be used to enforce an authentication flow during an authentication request. For instance, it can be used to select a flow based on certain conditions, such as a specific scope or an ACR value. For more details, see the <<_client-policy-auth-flow, related documentation>>.
|
||||
|
||||
|
||||
@ -123,6 +123,13 @@ If a user is not permitted to use the client scope, no protocol mappers or role
|
||||
== Realm default client scopes
|
||||
include::proc-updating-default-scopes.adoc[]
|
||||
|
||||
[[_downscoping]]
|
||||
== Downscoping
|
||||
|
||||
In OAuth/OIDC, *downscoping* is the process of exchanging an existing JWT access token for a new one with a more restricted set of permissions (scopes) and/or a narrower audience. In the OAuth 2.0 refresh token grant type, the https://datatracker.ietf.org/doc/html/rfc6749#section-6[RFC 6749] itself restricts the requested scope, saying that it must not include any scope not originally granted by the resource owner. So, this *downscoping* concept is very common and very recommended for security reasons.
|
||||
|
||||
{project_name} provides a <<_client_policies,client policy>> executor that ensures this *downscoping* idea for other grant types. The client executor is called `downscope-assertion-grant-enforcer` and, for the moment, applies for the link:{securing_apps_token_exchange_link}#_standard-token-exchange[Standard token exchange]. When this client executor is enforced, the token exchange is only allowed for the scopes that are already present in the initial JWT (`subject_token` parameter). An error is returned if any other extra scope is requested, no matter if the client configuration permits this scope as optional or default. Default scopes that are configured as *include in token scope* set to *false* (for example `basic` or `acr` in the default configuration) are the only exception. Those scopes are invisible for the requester and are considered compulsory for any grant type. Once this executor is applied for the client, *downscoping* is the only option when exchanging an access token, no additional scopes will ever be granted.
|
||||
|
||||
== Scopes explained
|
||||
|
||||
The term _scope_ has multiple meanings within {project_name} and across the OAuth/OIDC specifications. Below is a clarification of the different _scopes_ used in {project_name}:
|
||||
|
||||
@ -169,6 +169,8 @@ NOTE: The `audience` parameter can be used to filter the audiences that are comi
|
||||
no filtering occurs. As a result, the `audience` parameter is effectively used for "downscoping" the token to make sure that it contains only the requested audiences. However, the `scope` parameter is used
|
||||
to add optional client scopes and hence it can be used for "upscoping" and adding more scopes.
|
||||
|
||||
NOTE: By default, token exchange can be used to request extra scopes and audiences that are not present in the initial `subject_token`. If, for security reasons, you want to ensure that scopes are limited to the ones already granted to the `subject_token`, the `downscope-assertion-grant-enforcer` policy executor can be applied to the client. This executor enforces that only downscoping is allowed for token exchange. See link:{adminguide_link}#_downscoping[Downscoping] and link:{adminguide_link}#_client_policies[Client Policies] chapters in the {adminguide_name} for more information.
|
||||
|
||||
==== Examples
|
||||
|
||||
Here are some examples to better illustrate the behavior for scopes and audiences.
|
||||
|
||||
@ -30,6 +30,7 @@ import jakarta.ws.rs.core.MultivaluedMap;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Token exchange context
|
||||
@ -53,6 +54,7 @@ public class TokenExchangeContext {
|
||||
private final Map<String, String> clientAuthAttributes;
|
||||
|
||||
private final Params params = new Params();
|
||||
private Set<String> restrictedScopes;
|
||||
|
||||
// Reason why the particular tokenExchange provider cannot be supported
|
||||
private String unsupportedReason;
|
||||
@ -123,6 +125,14 @@ public class TokenExchangeContext {
|
||||
return params;
|
||||
}
|
||||
|
||||
public Set<String> getRestrictedScopes() {
|
||||
return restrictedScopes;
|
||||
}
|
||||
|
||||
public void setRestrictedScopes(Set<String> restrictedScopes) {
|
||||
this.restrictedScopes = restrictedScopes;
|
||||
}
|
||||
|
||||
public String getUnsupportedReason() {
|
||||
return unsupportedReason;
|
||||
}
|
||||
|
||||
@ -529,11 +529,16 @@ public class TokenManager {
|
||||
}
|
||||
|
||||
public static ClientSessionContext attachAuthenticationSession(KeycloakSession session, UserSessionModel userSession, AuthenticationSessionModel authSession) {
|
||||
return attachAuthenticationSession(session, userSession, authSession, false);
|
||||
return attachAuthenticationSession(session, userSession, authSession, null, false);
|
||||
}
|
||||
|
||||
public static ClientSessionContext attachAuthenticationSession(KeycloakSession session, UserSessionModel userSession,
|
||||
AuthenticationSessionModel authSession, boolean createTransientIfMissing) {
|
||||
return attachAuthenticationSession(session, userSession, authSession, null, createTransientIfMissing);
|
||||
}
|
||||
|
||||
public static ClientSessionContext attachAuthenticationSession(KeycloakSession session, UserSessionModel userSession,
|
||||
AuthenticationSessionModel authSession, Set<String> restrictedScopes, boolean createTransientIfMissing) {
|
||||
ClientModel client = authSession.getClient();
|
||||
|
||||
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
|
||||
@ -579,7 +584,7 @@ public class TokenManager {
|
||||
// Remove authentication session now (just current tab, not whole "rootAuthenticationSession" in case we have more browser tabs with "authentications in progress")
|
||||
new AuthenticationSessionManager(session).updateAuthenticationSessionAfterSuccessfulAuthentication(realm, authSession);
|
||||
|
||||
return DefaultClientSessionContext.fromClientSessionAndClientScopes(clientSession, clientScopes, session);
|
||||
return DefaultClientSessionContext.fromClientSessionAndClientScopes(clientSession, clientScopes, restrictedScopes, session);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -229,7 +229,7 @@ public class StandardTokenExchangeProvider extends AbstractTokenExchangeProvider
|
||||
|
||||
try {
|
||||
ClientSessionContext clientSessionCtx = TokenManager.attachAuthenticationSession(this.session, targetUserSession, authSession,
|
||||
!OAuth2Constants.REFRESH_TOKEN_TYPE.equals(requestedTokenType)); // create transient session if needed except for refresh
|
||||
context.getRestrictedScopes(), !OAuth2Constants.REFRESH_TOKEN_TYPE.equals(requestedTokenType)); // create transient session if needed except for refresh
|
||||
|
||||
if (requestedTokenType.equals(OAuth2Constants.REFRESH_TOKEN_TYPE)
|
||||
&& clientSessionCtx.getClientScopesStream().filter(s -> OAuth2Constants.OFFLINE_ACCESS.equals(s.getName())).findAny().isPresent()) {
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
|
||||
package org.keycloak.services.clientpolicy.condition;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
@ -48,7 +48,7 @@ public class GrantTypeCondition extends AbstractClientPolicyConditionProvider<Gr
|
||||
|
||||
public static class Configuration extends ClientPolicyConditionConfigurationRepresentation {
|
||||
|
||||
@JsonAlias("grant_types")
|
||||
@JsonProperty("grant_types")
|
||||
protected List<String> grantTypes;
|
||||
|
||||
public List<String> getGrantTypes() {
|
||||
|
||||
@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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.executor;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.jose.jws.JWSInputException;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientScopeModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.protocol.oidc.TokenExchangeContext;
|
||||
import org.keycloak.protocol.oidc.TokenManager;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyContext;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyException;
|
||||
import org.keycloak.services.clientpolicy.context.TokenExchangeRequestContext;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
public class DownscopeAssertionGrantEnforcerExecutor implements ClientPolicyExecutorProvider {
|
||||
|
||||
private final KeycloakSession session;
|
||||
|
||||
public DownscopeAssertionGrantEnforcerExecutor(KeycloakSession session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProviderId() {
|
||||
return DownscopeAssertionGrantEnforcerExecutorFactory.PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeOnEvent(ClientPolicyContext context) throws ClientPolicyException {
|
||||
switch (context.getEvent()) {
|
||||
case TOKEN_EXCHANGE_REQUEST -> {
|
||||
TokenExchangeContext tokenExchangeContext = ((TokenExchangeRequestContext) context).getTokenExchangeContext();
|
||||
Set<String> restrictedScopes = checkDownscope(tokenExchangeContext.getClient(),
|
||||
getAccessTokenFromSubjectToken(tokenExchangeContext),
|
||||
tokenExchangeContext.getParams().getScope());
|
||||
tokenExchangeContext.setRestrictedScopes(restrictedScopes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private AccessToken getAccessTokenFromSubjectToken(TokenExchangeContext context) throws ClientPolicyException {
|
||||
if (!OAuth2Constants.ACCESS_TOKEN_TYPE.equals(context.getParams().getSubjectTokenType())) {
|
||||
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Parameter 'subject_token' should be access_token for the executor");
|
||||
}
|
||||
try {
|
||||
return new JWSInput(context.getParams().getSubjectToken())
|
||||
.readJsonContent(AccessToken.class);
|
||||
} catch (JWSInputException e) {
|
||||
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Parameter 'subject_token' contains an invalid access token");
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> checkDownscope(ClientModel client, AccessToken token, String scopeParam) throws ClientPolicyException {
|
||||
Set<String> tokenScopes = token.getScope() != null
|
||||
? TokenManager.parseScopeParameter(token.getScope()).collect(Collectors.toSet())
|
||||
: Collections.emptySet();
|
||||
|
||||
if (scopeParam != null) {
|
||||
// the user requested specific scopes, check they are allowed
|
||||
Set<String> requestedScopes = TokenManager.parseScopeParameter(scopeParam).collect(Collectors.toSet());
|
||||
// check all requested scopes are inside the token
|
||||
requestedScopes.removeAll(tokenScopes);
|
||||
if (!requestedScopes.isEmpty()) {
|
||||
throw new ClientPolicyException(OAuthErrorException.INVALID_SCOPE,
|
||||
String.format("Scopes %s not present in the initial access token %s", requestedScopes, tokenScopes));
|
||||
}
|
||||
}
|
||||
|
||||
// always add as allowed restricted scopes the ones that are default and not included in token
|
||||
Set<String> restrictedScopes = client.getClientScopes(true).values().stream()
|
||||
.filter(Predicate.not(ClientScopeModel::isIncludeInTokenScope))
|
||||
.map(ClientScopeModel::getName)
|
||||
.collect(Collectors.toSet());
|
||||
restrictedScopes.addAll(tokenScopes);
|
||||
return restrictedScopes;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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.executor;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.keycloak.Config.Scope;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
/**
|
||||
* <p>Factory that enforces the grant to only use scopes that are already present in
|
||||
* the initial assertion. For the moment it can only be used in the Token Exchange
|
||||
* context.</p>
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
public class DownscopeAssertionGrantEnforcerExecutorFactory implements ClientPolicyExecutorProviderFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "downscope-assertion-grant-enforcer";
|
||||
|
||||
@Override
|
||||
public ClientPolicyExecutorProvider create(KeycloakSession session) {
|
||||
return new DownscopeAssertionGrantEnforcerExecutor(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 """
|
||||
It ensures that the scopes in the final access token are limited to the ones already present in the JWT assertion passed.
|
||||
For the moment, the executor applies to certain grants where some initial token/assertion is passed (for example
|
||||
subject_token in case of Standard Token Exchange grant). The limitation is done over the scopes that are
|
||||
present in the initial token/assertion, returning an error if any extra scope is requested.
|
||||
""";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@ -73,8 +73,11 @@ public class DefaultClientSessionContext implements ClientSessionContext {
|
||||
private Set<String> clientScopeIds;
|
||||
private String scopeString;
|
||||
|
||||
private DefaultClientSessionContext(AuthenticatedClientSessionModel clientSession, Set<ClientScopeModel> requestedScopes, KeycloakSession session) {
|
||||
private final Set<String> restrictedScopes;
|
||||
|
||||
private DefaultClientSessionContext(AuthenticatedClientSessionModel clientSession, Set<ClientScopeModel> requestedScopes, Set<String> restrictedScopes, KeycloakSession session) {
|
||||
this.requestedScopes = requestedScopes;
|
||||
this.restrictedScopes = restrictedScopes;
|
||||
this.clientSession = clientSession;
|
||||
this.session = session;
|
||||
this.session.setAttribute(ClientSessionContext.class.getName(), this);
|
||||
@ -97,12 +100,13 @@ public class DefaultClientSessionContext implements ClientSessionContext {
|
||||
} else {
|
||||
requestedScopes = TokenManager.getRequestedClientScopes(session, scopeParam, clientSession.getClient(), clientSession.getUserSession().getUser());
|
||||
}
|
||||
return new DefaultClientSessionContext(clientSession, requestedScopes.collect(Collectors.toSet()), session);
|
||||
return new DefaultClientSessionContext(clientSession, requestedScopes.collect(Collectors.toSet()), null, session);
|
||||
}
|
||||
|
||||
|
||||
public static DefaultClientSessionContext fromClientSessionAndClientScopes(AuthenticatedClientSessionModel clientSession, Set<ClientScopeModel> requestedScopes, KeycloakSession session) {
|
||||
return new DefaultClientSessionContext(clientSession, requestedScopes, session);
|
||||
public static DefaultClientSessionContext fromClientSessionAndClientScopes(AuthenticatedClientSessionModel clientSession,
|
||||
Set<ClientScopeModel> requestedScopes, Set<String> restrictedScopes, KeycloakSession session) {
|
||||
return new DefaultClientSessionContext(clientSession, requestedScopes, restrictedScopes, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -245,16 +249,20 @@ public class DefaultClientSessionContext implements ClientSessionContext {
|
||||
// Loading data
|
||||
|
||||
private boolean isAllowed(ClientScopeModel clientScope) {
|
||||
if (isClientScopePermittedForUser(clientScope)) {
|
||||
return true;
|
||||
if (restrictedScopes != null && !restrictedScopes.contains(clientScope.getName())) {
|
||||
logger.tracef("Client scope '%s' is not among the restricted scopes list and will not be processed", clientScope.getName());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracef("User '%s' not permitted to have client scope '%s'",
|
||||
clientSession.getUserSession().getUser().getUsername(), clientScope.getName());
|
||||
if (!isClientScopePermittedForUser(clientScope)) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracef("User '%s' not permitted to have client scope '%s'",
|
||||
clientSession.getUserSession().getUser().getUsername(), clientScope.getName());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Return true if clientScope can be used by the user.
|
||||
|
||||
@ -30,3 +30,4 @@ org.keycloak.services.clientpolicy.executor.SamlAvoidRedirectBindingExecutorFact
|
||||
org.keycloak.services.clientpolicy.executor.SamlSignatureEnforcerExecutorFactory
|
||||
org.keycloak.services.clientpolicy.executor.AuthenticationFlowSelectorExecutorFactory
|
||||
org.keycloak.services.clientpolicy.executor.SecureClientAuthenticationAssertionExecutorFactory
|
||||
org.keycloak.services.clientpolicy.executor.DownscopeAssertionGrantEnforcerExecutorFactory
|
||||
|
||||
@ -58,6 +58,7 @@ import org.keycloak.representations.oidc.TokenMetadataRepresentation;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyEvent;
|
||||
import org.keycloak.services.clientpolicy.condition.ClientScopesConditionFactory;
|
||||
import org.keycloak.services.clientpolicy.condition.GrantTypeConditionFactory;
|
||||
import org.keycloak.services.clientpolicy.executor.DownscopeAssertionGrantEnforcerExecutorFactory;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.arquillian.annotation.UncaughtServerErrorExpected;
|
||||
@ -1046,6 +1047,65 @@ public class StandardTokenExchangeV2Test extends AbstractClientPoliciesTest {
|
||||
assertEquals("Exception thrown intentionally", response.getErrorDescription());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDownscopeClientPolicies() throws Exception {
|
||||
|
||||
String json = (new ClientPoliciesUtil.ClientProfilesBuilder()).addProfile((new ClientPoliciesUtil.ClientProfileBuilder()).createProfile(PROFILE_NAME, "Profile")
|
||||
.addExecutor(DownscopeAssertionGrantEnforcerExecutorFactory.PROVIDER_ID, null)
|
||||
.toRepresentation()).toString();
|
||||
updateProfiles(json);
|
||||
|
||||
// register policy with condition on token exchange grant
|
||||
json = (new ClientPoliciesUtil.ClientPoliciesBuilder()).addPolicy(
|
||||
(new ClientPoliciesUtil.ClientPolicyBuilder()).createPolicy(POLICY_NAME, "Client Scope Policy", Boolean.TRUE)
|
||||
.addCondition(GrantTypeConditionFactory.PROVIDER_ID,
|
||||
createGrantTypeConditionConfig(List.of(OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)))
|
||||
.addProfile(PROFILE_NAME)
|
||||
.toRepresentation()).toString();
|
||||
updatePolicies(json);
|
||||
|
||||
// request initial token with optional scope optional-scope2
|
||||
final UserRepresentation john = ApiUtil.findUserByUsername(adminClient.realm(TEST), "john");
|
||||
String accessToken = resourceOwnerLogin("john", "password", "subject-client", "secret", "optional-scope2").getAccessToken();
|
||||
AccessToken token = TokenVerifier.create(accessToken, AccessToken.class).parse().getToken();
|
||||
assertScopes(token, List.of("email", "profile", "optional-scope2"));
|
||||
|
||||
// request with the all the scopes allowed in the initial token, all are optional in requester-client
|
||||
// only those should be there, even default-scope1 is supressed
|
||||
oauth.scope("email profile optional-scope2");
|
||||
AccessTokenResponse response = tokenExchange(accessToken, "requester-client", "secret", null, null);
|
||||
assertAudiencesAndScopes(response, john, List.of("target-client2"), List.of("email", "profile", "optional-scope2"));
|
||||
|
||||
// exchange with downscope to only optional-scope2
|
||||
oauth.scope("optional-scope2");
|
||||
response = tokenExchange(accessToken, "requester-client", "secret", null, null);
|
||||
assertAudiencesAndScopes(response, john, List.of("target-client2"), List.of("optional-scope2"));
|
||||
|
||||
// exchange for a invisible scope returns error although it is added by default
|
||||
oauth.scope("basic optional-scope2");
|
||||
response = tokenExchange(accessToken, "requester-client", "secret", null, null);
|
||||
assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatusCode());
|
||||
assertEquals(OAuthErrorException.INVALID_SCOPE, response.getError());
|
||||
assertEquals("Scopes [basic] not present in the initial access token [optional-scope2, profile, email]",
|
||||
response.getErrorDescription());
|
||||
|
||||
// exchange for another optional that is not in the token
|
||||
oauth.scope("optional-requester-scope");
|
||||
response = tokenExchange(accessToken, "requester-client", "secret", null, null);
|
||||
assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatusCode());
|
||||
assertEquals(OAuthErrorException.INVALID_SCOPE, response.getError());
|
||||
assertEquals("Scopes [optional-requester-scope] not present in the initial access token [optional-scope2, profile, email]",
|
||||
response.getErrorDescription());
|
||||
|
||||
// exchange for a optional that is not in initial token
|
||||
oauth.scope("default-scope1");
|
||||
response = tokenExchange(accessToken, "requester-client", "secret", null, null);
|
||||
assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatusCode());
|
||||
assertEquals(OAuthErrorException.INVALID_SCOPE, response.getError());
|
||||
assertEquals("Scopes [default-scope1] not present in the initial access token [optional-scope2, profile, email]",
|
||||
response.getErrorDescription());
|
||||
}
|
||||
|
||||
@Test
|
||||
@UncaughtServerErrorExpected
|
||||
public void testTokenRevocation() throws Exception {
|
||||
|
||||
@ -891,7 +891,7 @@
|
||||
"fullScopeAllowed" : false,
|
||||
"nodeReRegistrationTimeout" : -1,
|
||||
"defaultClientScopes" : [ "service_account", "acr", "default-scope1", "roles", "basic" ],
|
||||
"optionalClientScopes" : [ "optional-scope2", "optional-requester-scope", "offline_access" ]
|
||||
"optionalClientScopes" : [ "optional-scope2", "optional-requester-scope", "offline_access", "profile", "email" ]
|
||||
}, {
|
||||
"id" : "952643a3-2943-4734-9b51-8fa5956ebf55",
|
||||
"clientId" : "requester-client-2",
|
||||
@ -1091,8 +1091,8 @@
|
||||
"userinfo.token.claim" : "false"
|
||||
}
|
||||
} ],
|
||||
"defaultClientScopes" : [ "service_account", "acr", "roles", "profile", "basic" ],
|
||||
"optionalClientScopes" : [ "optional-requester-scope" ]
|
||||
"defaultClientScopes" : [ "service_account", "acr", "roles", "profile", "basic", "email" ],
|
||||
"optionalClientScopes" : [ "optional-scope2", "optional-requester-scope" ]
|
||||
}, {
|
||||
"id" : "192692cd-c5e4-42ff-a7ec-d5cb6228c0c7",
|
||||
"clientId" : "target-client1",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user