mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-09 23:12:06 -03:30
Initial integration of the JWT Authorization Grant in client Policies
Using the downscope executor for testing Closes #44201 Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
parent
fc67e54fde
commit
ca205272ba
@ -1,5 +1,7 @@
|
||||
package org.keycloak.protocol.oidc;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
|
||||
@ -12,6 +14,12 @@ public interface JWTAuthorizationGrantValidationContext {
|
||||
|
||||
JWSInput getJws();
|
||||
|
||||
String getScopeParam();
|
||||
|
||||
Set<String> getRestrictedScopes();
|
||||
|
||||
void setRestrictedScopes(Set<String> restrictedScopes);
|
||||
|
||||
default String getIssuer() {
|
||||
return getJWT().getIssuer();
|
||||
}
|
||||
|
||||
@ -54,6 +54,7 @@ public enum ClientPolicyEvent {
|
||||
TOKEN_EXCHANGE_REQUEST,
|
||||
RESOURCE_OWNER_PASSWORD_CREDENTIALS_REQUEST,
|
||||
RESOURCE_OWNER_PASSWORD_CREDENTIALS_RESPONSE,
|
||||
JWT_AUTHORIZATION_GRANT,
|
||||
|
||||
SAML_AUTHN_REQUEST,
|
||||
SAML_LOGOUT_REQUEST,
|
||||
|
||||
@ -38,6 +38,8 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.TokenManager;
|
||||
import org.keycloak.services.CorsErrorResponseException;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyException;
|
||||
import org.keycloak.services.clientpolicy.context.JWTAuthorizationGrantContext;
|
||||
import org.keycloak.services.managers.AuthenticationSessionManager;
|
||||
import org.keycloak.services.managers.UserSessionManager;
|
||||
import org.keycloak.services.resources.IdentityBrokerService;
|
||||
@ -55,7 +57,7 @@ public class JWTAuthorizationGrantType extends OAuth2GrantTypeBase {
|
||||
try {
|
||||
|
||||
JWTAuthorizationGrantValidator authorizationGrantContext = JWTAuthorizationGrantValidator.createValidator(
|
||||
context.getSession(), client, assertion);
|
||||
context.getSession(), client, assertion, formParams.getFirst(OAuth2Constants.SCOPE));
|
||||
|
||||
//client must be confidential
|
||||
authorizationGrantContext.validateClient();
|
||||
@ -107,12 +109,23 @@ public class JWTAuthorizationGrantType extends OAuth2GrantTypeBase {
|
||||
|
||||
String scopeParam = getRequestedScopes();
|
||||
|
||||
try {
|
||||
session.clientPolicy().triggerOnEvent(new JWTAuthorizationGrantContext(authorizationGrantContext, identityProviderModel));
|
||||
} catch (ClientPolicyException cpe) {
|
||||
event.detail(Details.REASON, Details.CLIENT_POLICY_ERROR);
|
||||
event.detail(Details.CLIENT_POLICY_ERROR, cpe.getError());
|
||||
event.detail(Details.CLIENT_POLICY_ERROR_DETAIL, cpe.getErrorDetail());
|
||||
event.error(cpe.getError());
|
||||
throw new CorsErrorResponseException(cors, cpe.getError(), cpe.getErrorDetail(), cpe.getErrorStatus());
|
||||
}
|
||||
|
||||
RootAuthenticationSessionModel rootAuthSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, false);
|
||||
AuthenticationSessionModel authSession = createSessionModel(rootAuthSession, user, client, scopeParam);
|
||||
UserSessionModel userSession = new UserSessionManager(session).createUserSession(authSession.getParentSession().getId(), realm, user, user.getUsername(),
|
||||
clientConnection.getRemoteHost(), "authorization-grant", false, null, null, UserSessionModel.SessionPersistenceState.TRANSIENT);
|
||||
event.session(userSession);
|
||||
ClientSessionContext clientSessionCtx = TokenManager.attachAuthenticationSession(this.session, userSession, authSession);
|
||||
ClientSessionContext clientSessionCtx = TokenManager.attachAuthenticationSession(this.session, userSession,
|
||||
authSession, authorizationGrantContext.getRestrictedScopes(), false);
|
||||
return createTokenResponse(user, userSession, clientSessionCtx, scopeParam, true, null);
|
||||
} catch (CorsErrorResponseException e) {
|
||||
throw e;
|
||||
|
||||
@ -19,6 +19,8 @@
|
||||
|
||||
package org.keycloak.protocol.oidc.grants;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.authentication.authenticators.client.AbstractBaseJWTValidator;
|
||||
import org.keycloak.authentication.authenticators.client.ClientAssertionState;
|
||||
@ -39,7 +41,10 @@ import org.keycloak.representations.JsonWebToken;
|
||||
*/
|
||||
public class JWTAuthorizationGrantValidator extends AbstractBaseJWTValidator implements JWTAuthorizationGrantValidationContext {
|
||||
|
||||
public static JWTAuthorizationGrantValidator createValidator(KeycloakSession session, ClientModel client, String assertion) {
|
||||
private final String scope;
|
||||
private Set<String> restrictedScopes;
|
||||
|
||||
public static JWTAuthorizationGrantValidator createValidator(KeycloakSession session, ClientModel client, String assertion, String scope) {
|
||||
if (assertion == null) {
|
||||
throw new RuntimeException("Missing parameter:" + OAuth2Constants.ASSERTION);
|
||||
}
|
||||
@ -47,14 +52,15 @@ public class JWTAuthorizationGrantValidator extends AbstractBaseJWTValidator imp
|
||||
JWSInput jws = new JWSInput(assertion);
|
||||
JsonWebToken jwt = jws.readJsonContent(JsonWebToken.class);
|
||||
ClientAssertionState clientAssertionState = new ClientAssertionState(client, OAuth2Constants.JWT_AUTHORIZATION_GRANT, assertion, jws, jwt);
|
||||
return new JWTAuthorizationGrantValidator(session, clientAssertionState);
|
||||
return new JWTAuthorizationGrantValidator(session, scope, clientAssertionState);
|
||||
} catch (JWSInputException e) {
|
||||
throw new RuntimeException("The provided assertion is not a valid JWT");
|
||||
}
|
||||
}
|
||||
|
||||
private JWTAuthorizationGrantValidator(KeycloakSession session, ClientAssertionState clientAssertionState) {
|
||||
private JWTAuthorizationGrantValidator(KeycloakSession session, String scope, ClientAssertionState clientAssertionState) {
|
||||
super(session, clientAssertionState);
|
||||
this.scope = scope;
|
||||
}
|
||||
|
||||
public void validateClient() {
|
||||
@ -90,6 +96,19 @@ public class JWTAuthorizationGrantValidator extends AbstractBaseJWTValidator imp
|
||||
return clientAssertionState.getClientAssertion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getScopeParam() {
|
||||
return scope;
|
||||
}
|
||||
|
||||
public Set<String> getRestrictedScopes() {
|
||||
return restrictedScopes;
|
||||
}
|
||||
|
||||
public void setRestrictedScopes(Set<String> restrictedScopes) {
|
||||
this.restrictedScopes = restrictedScopes;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void failureCallback(String errorDescription) {
|
||||
throw new RuntimeException(errorDescription);
|
||||
|
||||
@ -83,6 +83,8 @@ public class ClientAccessTypeCondition extends AbstractClientPolicyConditionProv
|
||||
case UPDATE:
|
||||
case UPDATED:
|
||||
case REGISTERED:
|
||||
case TOKEN_EXCHANGE_REQUEST:
|
||||
case JWT_AUTHORIZATION_GRANT:
|
||||
if (isClientAccessTypeMatched()) return ClientPolicyVote.YES;
|
||||
return ClientPolicyVote.NO;
|
||||
case REGISTER:
|
||||
|
||||
@ -91,6 +91,8 @@ public class ClientAttributesCondition extends AbstractClientPolicyConditionProv
|
||||
case REGISTERED:
|
||||
case UPDATE:
|
||||
case UPDATED:
|
||||
case TOKEN_EXCHANGE_REQUEST:
|
||||
case JWT_AUTHORIZATION_GRANT:
|
||||
case SAML_AUTHN_REQUEST:
|
||||
case SAML_LOGOUT_REQUEST:
|
||||
if (isAttributesMatched(session.getContext().getClient())) return ClientPolicyVote.YES;
|
||||
|
||||
@ -83,6 +83,8 @@ public class ClientProtocolCondition extends AbstractClientPolicyConditionProvid
|
||||
case UPDATE:
|
||||
case UPDATED:
|
||||
case REGISTERED:
|
||||
case TOKEN_REVOKE_RESPONSE:
|
||||
case JWT_AUTHORIZATION_GRANT:
|
||||
case SAML_AUTHN_REQUEST:
|
||||
case SAML_LOGOUT_REQUEST:
|
||||
if (isCorrectProtocolFromContext()) {
|
||||
|
||||
@ -93,6 +93,8 @@ public class ClientRolesCondition extends AbstractClientPolicyConditionProvider<
|
||||
case REGISTERED:
|
||||
case UPDATE:
|
||||
case UPDATED:
|
||||
case TOKEN_EXCHANGE_REQUEST:
|
||||
case JWT_AUTHORIZATION_GRANT:
|
||||
case SAML_AUTHN_REQUEST:
|
||||
case SAML_LOGOUT_REQUEST:
|
||||
if (isRolesMatched(session.getContext().getClient())) return ClientPolicyVote.YES;
|
||||
|
||||
@ -27,6 +27,7 @@ import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.protocol.oidc.JWTAuthorizationGrantValidationContext;
|
||||
import org.keycloak.protocol.oidc.TokenExchangeContext;
|
||||
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
|
||||
import org.keycloak.protocol.oidc.grants.ciba.channel.CIBAAuthenticationRequest;
|
||||
@ -38,6 +39,7 @@ import org.keycloak.services.clientpolicy.ClientPolicyContext;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyException;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyVote;
|
||||
import org.keycloak.services.clientpolicy.context.AuthorizationRequestContext;
|
||||
import org.keycloak.services.clientpolicy.context.JWTAuthorizationGrantContext;
|
||||
import org.keycloak.services.clientpolicy.context.ServiceAccountTokenRequestContext;
|
||||
import org.keycloak.services.clientpolicy.context.ServiceAccountTokenResponseContext;
|
||||
import org.keycloak.services.clientpolicy.context.TokenExchangeRequestContext;
|
||||
@ -119,6 +121,9 @@ public class ClientScopesCondition extends AbstractClientPolicyConditionProvider
|
||||
case TOKEN_EXCHANGE_REQUEST:
|
||||
if (isScopeMatched(((TokenExchangeRequestContext) context).getTokenExchangeContext())) return ClientPolicyVote.YES;
|
||||
return ClientPolicyVote.NO;
|
||||
case JWT_AUTHORIZATION_GRANT:
|
||||
if (isScopeMatched(((JWTAuthorizationGrantContext) context).getAuthorizationGrantContext())) return ClientPolicyVote.YES;
|
||||
return ClientPolicyVote.NO;
|
||||
default:
|
||||
return ClientPolicyVote.ABSTAIN;
|
||||
}
|
||||
@ -144,6 +149,11 @@ public class ClientScopesCondition extends AbstractClientPolicyConditionProvider
|
||||
return isScopeMatched(context.getParams().getScope(), context.getClient());
|
||||
}
|
||||
|
||||
private boolean isScopeMatched(JWTAuthorizationGrantValidationContext context) {
|
||||
if (context == null) return false;
|
||||
return isScopeMatched(context.getScopeParam(), session.getContext().getClient());
|
||||
}
|
||||
|
||||
private boolean isScopeMatched(String explicitScopes, ClientModel client) {
|
||||
if (explicitScopes == null) explicitScopes = "";
|
||||
Collection<String> explicitSpecifiedScopes = new HashSet<>(Arrays.asList(explicitScopes.split(" ")));
|
||||
|
||||
@ -88,6 +88,9 @@ public class GrantTypeCondition extends AbstractClientPolicyConditionProvider<Gr
|
||||
case DEVICE_TOKEN_REQUEST:
|
||||
if (isGrantMatching(OAuth2Constants.DEVICE_CODE_GRANT_TYPE)) return ClientPolicyVote.YES;
|
||||
return ClientPolicyVote.NO;
|
||||
case JWT_AUTHORIZATION_GRANT:
|
||||
if (isGrantMatching(OAuth2Constants.JWT_AUTHORIZATION_GRANT)) return ClientPolicyVote.YES;
|
||||
return ClientPolicyVote.NO;
|
||||
default:
|
||||
return ClientPolicyVote.ABSTAIN;
|
||||
}
|
||||
|
||||
@ -66,6 +66,9 @@ public class GrantTypeConditionFactory extends AbstractClientPolicyConditionProv
|
||||
if (Profile.isFeatureEnabled(Profile.Feature.DEVICE_FLOW)) {
|
||||
DEFAULT_GRANT_TYPES_SUPPORTED.add(OAuth2Constants.DEVICE_CODE_GRANT_TYPE);
|
||||
}
|
||||
if (Profile.isFeatureEnabled(Profile.Feature.JWT_AUTHORIZATION_GRANT)) {
|
||||
DEFAULT_GRANT_TYPES_SUPPORTED.add(OAuth2Constants.JWT_AUTHORIZATION_GRANT);
|
||||
}
|
||||
|
||||
property.setOptions(DEFAULT_GRANT_TYPES_SUPPORTED);
|
||||
configProperties.add(property);
|
||||
|
||||
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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.models.IdentityProviderModel;
|
||||
import org.keycloak.protocol.oidc.JWTAuthorizationGrantValidationContext;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyContext;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyEvent;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
public class JWTAuthorizationGrantContext implements ClientPolicyContext {
|
||||
|
||||
private final JWTAuthorizationGrantValidationContext authorizationGrantContext;
|
||||
private final IdentityProviderModel idp;
|
||||
|
||||
public JWTAuthorizationGrantContext(JWTAuthorizationGrantValidationContext authorizationGrantContext, IdentityProviderModel idp) {
|
||||
this.authorizationGrantContext = authorizationGrantContext;
|
||||
this.idp = idp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientPolicyEvent getEvent() {
|
||||
return ClientPolicyEvent.JWT_AUTHORIZATION_GRANT;
|
||||
}
|
||||
|
||||
public JWTAuthorizationGrantValidationContext getAuthorizationGrantContext() {
|
||||
return authorizationGrantContext;
|
||||
}
|
||||
|
||||
public IdentityProviderModel getIdentityProvider() {
|
||||
return idp;
|
||||
}
|
||||
}
|
||||
@ -28,11 +28,13 @@ 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.JWTAuthorizationGrantValidationContext;
|
||||
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.JWTAuthorizationGrantContext;
|
||||
import org.keycloak.services.clientpolicy.context.TokenExchangeRequestContext;
|
||||
|
||||
/**
|
||||
@ -62,6 +64,22 @@ public class DownscopeAssertionGrantEnforcerExecutor implements ClientPolicyExec
|
||||
tokenExchangeContext.getParams().getScope());
|
||||
tokenExchangeContext.setRestrictedScopes(restrictedScopes);
|
||||
}
|
||||
case JWT_AUTHORIZATION_GRANT -> {
|
||||
JWTAuthorizationGrantContext jwtAuthnGrantContext = ((JWTAuthorizationGrantContext) context);
|
||||
JWTAuthorizationGrantValidationContext jwtContext = jwtAuthnGrantContext.getAuthorizationGrantContext();
|
||||
Set<String> restrictedScopes = checkDownscope(session.getContext().getClient(),
|
||||
getAccessTokenFromAssertion(jwtContext.getAssertion()),
|
||||
jwtContext.getScopeParam());
|
||||
jwtContext.setRestrictedScopes(restrictedScopes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private AccessToken getAccessTokenFromAssertion(String assertion) throws ClientPolicyException {
|
||||
try {
|
||||
return new JWSInput(assertion).readJsonContent(AccessToken.class);
|
||||
} catch (JWSInputException e) {
|
||||
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Assertion contains an invalid access token");
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,12 +87,7 @@ public class DownscopeAssertionGrantEnforcerExecutor implements ClientPolicyExec
|
||||
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");
|
||||
}
|
||||
return getAccessTokenFromAssertion(context.getParams().getSubjectToken());
|
||||
}
|
||||
|
||||
private Set<String> checkDownscope(ClientModel client, AccessToken token, String scopeParam) throws ClientPolicyException {
|
||||
|
||||
@ -0,0 +1,93 @@
|
||||
package org.keycloak.testframework.realm;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.representations.idm.ClientPolicyConditionConfigurationRepresentation;
|
||||
import org.keycloak.representations.idm.ClientPolicyConditionRepresentation;
|
||||
import org.keycloak.representations.idm.ClientPolicyRepresentation;
|
||||
import org.keycloak.services.clientpolicy.condition.GrantTypeCondition;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
public class ClientPolicyBuilder {
|
||||
|
||||
private final ClientPolicyRepresentation rep;
|
||||
|
||||
private ClientPolicyBuilder(ClientPolicyRepresentation rep) {
|
||||
this.rep = rep;
|
||||
}
|
||||
|
||||
public static ClientPolicyBuilder create() {
|
||||
ClientPolicyRepresentation rep = new ClientPolicyRepresentation();
|
||||
rep.setEnabled(true);
|
||||
return new ClientPolicyBuilder(rep);
|
||||
}
|
||||
|
||||
public static GrantTypeCondition.Configuration grantTypeConditionConfiguration(String... types) {
|
||||
GrantTypeCondition.Configuration config = new GrantTypeCondition.Configuration();
|
||||
if (types != null && types.length > 0) {
|
||||
config.setGrantTypes(List.of(types));
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
public static ClientPolicyBuilder update(ClientPolicyRepresentation rep) {
|
||||
return new ClientPolicyBuilder(rep);
|
||||
}
|
||||
|
||||
public ClientPolicyBuilder enabled(boolean enabled) {
|
||||
rep.setEnabled(enabled);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ClientPolicyBuilder name(String name) {
|
||||
rep.setName(name);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ClientPolicyBuilder description(String description) {
|
||||
rep.setDescription(description);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ClientPolicyBuilder condition(String providerId, ClientPolicyConditionConfigurationRepresentation config) {
|
||||
ClientPolicyConditionRepresentation condition = new ClientPolicyConditionRepresentation();
|
||||
condition.setConditionProviderId(providerId);
|
||||
if (config == null) {
|
||||
config = new ClientPolicyConditionConfigurationRepresentation();
|
||||
}
|
||||
try {
|
||||
condition.setConfiguration(JsonSerialization.mapper.readValue(JsonSerialization.mapper.writeValueAsBytes(config), JsonNode.class));
|
||||
} catch(IOException e) {
|
||||
throw new IllegalArgumentException("Invalid configuration", e);
|
||||
}
|
||||
List<ClientPolicyConditionRepresentation> conditions = rep.getConditions();
|
||||
if (conditions == null) {
|
||||
conditions = new LinkedList<>();
|
||||
rep.setConditions(conditions);
|
||||
}
|
||||
conditions.add(condition);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ClientPolicyBuilder profile(String... profile) {
|
||||
List<String> profiles = rep.getProfiles();
|
||||
if (profiles == null) {
|
||||
profiles = new LinkedList<>();
|
||||
rep.setProfiles(profiles);
|
||||
}
|
||||
profiles.addAll(List.of(profile));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ClientPolicyRepresentation build() {
|
||||
return rep;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,68 @@
|
||||
package org.keycloak.testframework.realm;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.representations.idm.ClientPolicyExecutorConfigurationRepresentation;
|
||||
import org.keycloak.representations.idm.ClientPolicyExecutorRepresentation;
|
||||
import org.keycloak.representations.idm.ClientProfileRepresentation;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
public class ClientProfileBuilder {
|
||||
|
||||
private final ClientProfileRepresentation rep;
|
||||
|
||||
private ClientProfileBuilder(ClientProfileRepresentation rep) {
|
||||
this.rep = rep;
|
||||
}
|
||||
|
||||
public static ClientProfileBuilder create() {
|
||||
ClientProfileRepresentation rep = new ClientProfileRepresentation();
|
||||
return new ClientProfileBuilder(rep);
|
||||
}
|
||||
|
||||
public static ClientProfileBuilder update(ClientProfileRepresentation rep) {
|
||||
return new ClientProfileBuilder(rep);
|
||||
}
|
||||
|
||||
public ClientProfileBuilder name(String name) {
|
||||
rep.setName(name);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ClientProfileBuilder description(String description) {
|
||||
rep.setDescription(description);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ClientProfileBuilder executor(String providerId, ClientPolicyExecutorConfigurationRepresentation config) {
|
||||
ClientPolicyExecutorRepresentation executor = new ClientPolicyExecutorRepresentation();
|
||||
executor.setExecutorProviderId(providerId);
|
||||
if (config == null) {
|
||||
config = new ClientPolicyExecutorConfigurationRepresentation();
|
||||
}
|
||||
try {
|
||||
executor.setConfiguration(JsonSerialization.mapper.readValue(JsonSerialization.mapper.writeValueAsBytes(config), JsonNode.class));
|
||||
} catch(IOException e) {
|
||||
throw new IllegalArgumentException("Invalid configuration", e);
|
||||
}
|
||||
List<ClientPolicyExecutorRepresentation> executors = rep.getExecutors();
|
||||
if (executors == null) {
|
||||
executors = new LinkedList<>();
|
||||
rep.setExecutors(executors);
|
||||
}
|
||||
executors.add(executor);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ClientProfileRepresentation build() {
|
||||
return rep;
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,10 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.representations.idm.ClientPoliciesRepresentation;
|
||||
import org.keycloak.representations.idm.ClientPolicyRepresentation;
|
||||
import org.keycloak.representations.idm.ClientProfileRepresentation;
|
||||
import org.keycloak.representations.idm.ClientProfilesRepresentation;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.GroupRepresentation;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
@ -236,6 +240,28 @@ public class RealmConfigBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmConfigBuilder clientPolicy(ClientPolicyRepresentation clienPolicyRep) {
|
||||
ClientPoliciesRepresentation clientPolicies = rep.getParsedClientPolicies();
|
||||
if (clientPolicies == null) {
|
||||
clientPolicies = new ClientPoliciesRepresentation();
|
||||
}
|
||||
List<ClientPolicyRepresentation> policies = clientPolicies.getPolicies();
|
||||
policies.add(clienPolicyRep);
|
||||
rep.setParsedClientPolicies(clientPolicies);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmConfigBuilder clientProfile(ClientProfileRepresentation clienProfileRep) {
|
||||
ClientProfilesRepresentation clientProfiles = rep.getParsedClientProfiles();
|
||||
if (clientProfiles == null) {
|
||||
clientProfiles = new ClientProfilesRepresentation();
|
||||
}
|
||||
List<ClientProfileRepresentation> profiles = clientProfiles.getProfiles();
|
||||
profiles.add(clienProfileRep);
|
||||
rep.setParsedClientProfiles(clientProfiles);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Best practice is to use other convenience methods when configuring a realm, but while the framework is under
|
||||
* active development there may not be a way to perform all updates required. In these cases this method allows
|
||||
|
||||
@ -1,69 +1,22 @@
|
||||
package org.keycloak.tests.oauth;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.crypto.Algorithm;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.IDToken;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.representations.idm.EventRepresentation;
|
||||
import org.keycloak.testframework.annotations.InjectEvents;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.InjectUser;
|
||||
import org.keycloak.testframework.events.EventAssertion;
|
||||
import org.keycloak.testframework.events.Events;
|
||||
import org.keycloak.testframework.oauth.OAuthClient;
|
||||
import org.keycloak.testframework.oauth.OAuthIdentityProvider;
|
||||
import org.keycloak.testframework.oauth.OAuthIdentityProviderConfig;
|
||||
import org.keycloak.testframework.oauth.OAuthIdentityProviderConfigBuilder;
|
||||
import org.keycloak.testframework.oauth.annotations.InjectOAuthClient;
|
||||
import org.keycloak.testframework.oauth.annotations.InjectOAuthIdentityProvider;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
import org.keycloak.testframework.realm.ManagedUser;
|
||||
import org.keycloak.testframework.realm.RealmConfig;
|
||||
import org.keycloak.testframework.realm.RealmConfigBuilder;
|
||||
import org.keycloak.testframework.realm.UserConfig;
|
||||
import org.keycloak.testframework.realm.UserConfigBuilder;
|
||||
import org.keycloak.testframework.remote.timeoffset.InjectTimeOffSet;
|
||||
import org.keycloak.testframework.remote.timeoffset.TimeOffSet;
|
||||
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
|
||||
import org.keycloak.tests.client.authentication.external.ClientAuthIdpServerConfig;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
|
||||
import org.hamcrest.MatcherAssert;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public abstract class AbstractJWTAuthorizationGrantTest {
|
||||
|
||||
public static String IDP_ALIAS = "authorization-grant-idp-alias";
|
||||
public static final String IDP_ISSUER = "https://authorization-grant-issuer";
|
||||
|
||||
@InjectOAuthIdentityProvider(config = AbstractJWTAuthorizationGrantTest.AGIdpConfig.class)
|
||||
OAuthIdentityProvider identityProvider;
|
||||
|
||||
@InjectRealm(config = JWTAuthorizationGrantRealmConfig.class)
|
||||
protected ManagedRealm realm;
|
||||
|
||||
@InjectUser(config = FederatedUserConfiguration.class)
|
||||
ManagedUser user;
|
||||
|
||||
@InjectOAuthClient
|
||||
OAuthClient oAuthClient;
|
||||
|
||||
@InjectEvents
|
||||
Events events;
|
||||
|
||||
@InjectTimeOffSet
|
||||
TimeOffSet timeOffSet;
|
||||
public abstract class AbstractJWTAuthorizationGrantTest extends BaseAbstractJWTAuthorizationGrantTest {
|
||||
|
||||
@Test
|
||||
public void testPublicClient() {
|
||||
@ -115,7 +68,7 @@ public abstract class AbstractJWTAuthorizationGrantTest {
|
||||
assertFailure("Token is not active", response, events.poll());
|
||||
|
||||
//test max exp default settings
|
||||
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + 301L));
|
||||
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + 305L));
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Token expiration is too far in the future and iat claim not present in token", response, events.poll());
|
||||
|
||||
@ -278,102 +231,4 @@ public abstract class AbstractJWTAuthorizationGrantTest {
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertSuccess("test-app", "basic-user", response);
|
||||
}
|
||||
|
||||
protected JsonWebToken createDefaultAuthorizationGrantToken() {
|
||||
return createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + 300L);
|
||||
}
|
||||
|
||||
protected JsonWebToken createAuthorizationGrantToken(String subject, String audience, String issuer) {
|
||||
return createAuthorizationGrantToken(subject, audience, issuer, Time.currentTime() + 300L, (long) Time.currentTime());
|
||||
}
|
||||
|
||||
protected JsonWebToken createAuthorizationGrantToken(String subject, String audience, String issuer, Long exp) {
|
||||
return createAuthorizationGrantToken(subject, audience, issuer, exp, null);
|
||||
}
|
||||
|
||||
protected JsonWebToken createAuthorizationGrantToken(String subject, String audience, String issuer, Long exp, Long iat) {
|
||||
JsonWebToken token = new JsonWebToken();
|
||||
token.id(UUID.randomUUID().toString());
|
||||
token.subject(subject);
|
||||
token.audience(audience);
|
||||
token.issuer(issuer);
|
||||
token.exp(exp);
|
||||
token.iat(iat);
|
||||
return token;
|
||||
}
|
||||
|
||||
public OAuthIdentityProvider getIdentityProvider() {
|
||||
return identityProvider;
|
||||
}
|
||||
|
||||
public static class AGIdpConfig implements OAuthIdentityProviderConfig {
|
||||
|
||||
@Override
|
||||
public OAuthIdentityProviderConfigBuilder configure(OAuthIdentityProviderConfigBuilder config) {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
public static class JWTAuthorizationGrantServerConfig extends ClientAuthIdpServerConfig {
|
||||
|
||||
@Override
|
||||
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
|
||||
return super.configure(config).features(Profile.Feature.JWT_AUTHORIZATION_GRANT);
|
||||
}
|
||||
}
|
||||
|
||||
public static class JWTAuthorizationGrantRealmConfig implements RealmConfig {
|
||||
|
||||
@Override
|
||||
public RealmConfigBuilder configure(RealmConfigBuilder realm) {
|
||||
realm.addClient("test-public").publicClient(true);
|
||||
realm.addClient("authorization-grant-disabled-client").publicClient(false).secret("test-secret");
|
||||
realm.addClient("authorization-grant-not-allowed-idp-client").publicClient(false).attribute(OIDCConfigAttributes.JWT_AUTHORIZATION_GRANT_ENABLED, "true").secret("test-secret");
|
||||
return realm;
|
||||
}
|
||||
}
|
||||
|
||||
public static class FederatedUserConfiguration implements UserConfig {
|
||||
|
||||
@Override
|
||||
public UserConfigBuilder configure(UserConfigBuilder user) {
|
||||
return user
|
||||
.username("basic-user")
|
||||
.password("password")
|
||||
.email("basic@localhost")
|
||||
.name("First", "Last")
|
||||
.federatedLink(IDP_ALIAS, "basic-user-id", "basic-user");
|
||||
}
|
||||
}
|
||||
|
||||
protected AccessToken assertSuccess(String expectedClientId, String username, AccessTokenResponse response) {
|
||||
Assertions.assertTrue(response.isSuccess());
|
||||
Assertions.assertNull(response.getRefreshToken());
|
||||
AccessToken accessToken = oAuthClient.parseToken(response.getAccessToken(), AccessToken.class);
|
||||
Assertions.assertNull(accessToken.getSessionId());
|
||||
MatcherAssert.assertThat(accessToken.getId(), Matchers.startsWith("trrtag:"));
|
||||
Assertions.assertEquals(expectedClientId, accessToken.getIssuedFor());
|
||||
Assertions.assertEquals(username, accessToken.getPreferredUsername());
|
||||
EventAssertion.assertSuccess(events.poll())
|
||||
.type(EventType.LOGIN)
|
||||
.clientId(expectedClientId)
|
||||
.details("grant_type", OAuth2Constants.JWT_AUTHORIZATION_GRANT)
|
||||
.details("username", username);
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
protected void assertFailure(String expectedErrorDescription, AccessTokenResponse response, EventRepresentation event) {
|
||||
assertFailure("invalid_grant", expectedErrorDescription, response, event);
|
||||
}
|
||||
|
||||
protected void assertFailure(String expectedError, String expectedErrorDescription, AccessTokenResponse response, EventRepresentation event) {
|
||||
Assertions.assertFalse(response.isSuccess());
|
||||
Assertions.assertEquals(expectedError, response.getError());
|
||||
Assertions.assertEquals(expectedErrorDescription, response.getErrorDescription());
|
||||
EventAssertion.assertError(event)
|
||||
.type(EventType.LOGIN_ERROR)
|
||||
.error("invalid_request")
|
||||
.details("grant_type", OAuth2Constants.JWT_AUTHORIZATION_GRANT)
|
||||
.details("reason", expectedErrorDescription);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,207 @@
|
||||
/*
|
||||
* 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.tests.oauth;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.representations.idm.EventRepresentation;
|
||||
import org.keycloak.testframework.annotations.InjectEvents;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.InjectUser;
|
||||
import org.keycloak.testframework.events.EventAssertion;
|
||||
import org.keycloak.testframework.events.Events;
|
||||
import org.keycloak.testframework.oauth.OAuthClient;
|
||||
import org.keycloak.testframework.oauth.OAuthIdentityProvider;
|
||||
import org.keycloak.testframework.oauth.OAuthIdentityProviderConfig;
|
||||
import org.keycloak.testframework.oauth.OAuthIdentityProviderConfigBuilder;
|
||||
import org.keycloak.testframework.oauth.annotations.InjectOAuthClient;
|
||||
import org.keycloak.testframework.oauth.annotations.InjectOAuthIdentityProvider;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
import org.keycloak.testframework.realm.ManagedUser;
|
||||
import org.keycloak.testframework.realm.RealmConfig;
|
||||
import org.keycloak.testframework.realm.RealmConfigBuilder;
|
||||
import org.keycloak.testframework.realm.UserConfig;
|
||||
import org.keycloak.testframework.realm.UserConfigBuilder;
|
||||
import org.keycloak.testframework.remote.timeoffset.InjectTimeOffSet;
|
||||
import org.keycloak.testframework.remote.timeoffset.TimeOffSet;
|
||||
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
|
||||
import org.keycloak.tests.client.authentication.external.ClientAuthIdpServerConfig;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
|
||||
import org.hamcrest.MatcherAssert;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
|
||||
import static org.keycloak.tests.oauth.AbstractJWTAuthorizationGrantTest.IDP_ISSUER;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
public class BaseAbstractJWTAuthorizationGrantTest {
|
||||
|
||||
public static String IDP_ALIAS = "authorization-grant-idp-alias";
|
||||
public static final String IDP_ISSUER = "https://authorization-grant-issuer";
|
||||
|
||||
@InjectOAuthIdentityProvider(config = AbstractJWTAuthorizationGrantTest.AGIdpConfig.class)
|
||||
OAuthIdentityProvider identityProvider;
|
||||
|
||||
@InjectRealm(config = AbstractJWTAuthorizationGrantTest.JWTAuthorizationGrantRealmConfig.class)
|
||||
protected ManagedRealm realm;
|
||||
|
||||
@InjectUser(config = AbstractJWTAuthorizationGrantTest.FederatedUserConfiguration.class)
|
||||
ManagedUser user;
|
||||
|
||||
@InjectOAuthClient
|
||||
OAuthClient oAuthClient;
|
||||
|
||||
@InjectEvents
|
||||
Events events;
|
||||
|
||||
@InjectTimeOffSet
|
||||
TimeOffSet timeOffSet;
|
||||
|
||||
protected JsonWebToken createDefaultAuthorizationGrantToken() {
|
||||
return createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + 300L, null, null);
|
||||
}
|
||||
|
||||
protected JsonWebToken createAuthorizationGrantToken(String subject, String audience, String issuer) {
|
||||
return createAuthorizationGrantToken(subject, audience, issuer, Time.currentTime() + 300L, (long) Time.currentTime(), null);
|
||||
}
|
||||
|
||||
protected JsonWebToken createAuthorizationGrantToken(String subject, String audience, String issuer, Long exp) {
|
||||
return createAuthorizationGrantToken(subject, audience, issuer, exp, null, null);
|
||||
}
|
||||
|
||||
protected AccessToken createDefaultAuthorizationGrantToken(String scope) {
|
||||
return createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + 300L, null, scope);
|
||||
}
|
||||
|
||||
protected AccessToken createAuthorizationGrantToken(String subject, String audience, String issuer, Long exp, Long iat) {
|
||||
return createAuthorizationGrantToken(subject, audience, issuer, exp, iat, null);
|
||||
}
|
||||
|
||||
protected AccessToken createAuthorizationGrantToken(String subject, String audience, String issuer, Long exp, Long iat, String scope) {
|
||||
AccessToken token = new AccessToken();
|
||||
token.id(UUID.randomUUID().toString());
|
||||
token.subject(subject);
|
||||
token.audience(audience);
|
||||
token.issuer(issuer);
|
||||
token.exp(exp);
|
||||
token.iat(iat);
|
||||
token.setScope(scope);
|
||||
return token;
|
||||
}
|
||||
|
||||
public OAuthIdentityProvider getIdentityProvider() {
|
||||
return identityProvider;
|
||||
}
|
||||
|
||||
public static class AGIdpConfig implements OAuthIdentityProviderConfig {
|
||||
|
||||
@Override
|
||||
public OAuthIdentityProviderConfigBuilder configure(OAuthIdentityProviderConfigBuilder config) {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
public static class JWTAuthorizationGrantServerConfig extends ClientAuthIdpServerConfig {
|
||||
|
||||
@Override
|
||||
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
|
||||
return super.configure(config).features(Profile.Feature.JWT_AUTHORIZATION_GRANT);
|
||||
}
|
||||
}
|
||||
|
||||
public static class JWTAuthorizationGrantRealmConfig implements RealmConfig {
|
||||
|
||||
@Override
|
||||
public RealmConfigBuilder configure(RealmConfigBuilder realm) {
|
||||
realm.addClient("test-public").publicClient(true);
|
||||
realm.addClient("authorization-grant-disabled-client").publicClient(false).secret("test-secret");
|
||||
realm.addClient("authorization-grant-not-allowed-idp-client").publicClient(false).attribute(OIDCConfigAttributes.JWT_AUTHORIZATION_GRANT_ENABLED, "true").secret("test-secret");
|
||||
return realm;
|
||||
}
|
||||
}
|
||||
|
||||
public static class FederatedUserConfiguration implements UserConfig {
|
||||
|
||||
@Override
|
||||
public UserConfigBuilder configure(UserConfigBuilder user) {
|
||||
return user
|
||||
.username("basic-user")
|
||||
.password("password")
|
||||
.email("basic@localhost")
|
||||
.name("First", "Last")
|
||||
.federatedLink(IDP_ALIAS, "basic-user-id", "basic-user");
|
||||
}
|
||||
}
|
||||
|
||||
protected AccessToken assertSuccess(String expectedClientId, String username, AccessTokenResponse response) {
|
||||
Assertions.assertTrue(response.isSuccess());
|
||||
Assertions.assertNull(response.getRefreshToken());
|
||||
AccessToken accessToken = oAuthClient.parseToken(response.getAccessToken(), AccessToken.class);
|
||||
Assertions.assertNull(accessToken.getSessionId());
|
||||
MatcherAssert.assertThat(accessToken.getId(), Matchers.startsWith("trrtag:"));
|
||||
Assertions.assertEquals(expectedClientId, accessToken.getIssuedFor());
|
||||
Assertions.assertEquals(username, accessToken.getPreferredUsername());
|
||||
EventAssertion.assertSuccess(events.poll())
|
||||
.type(EventType.LOGIN)
|
||||
.clientId(expectedClientId)
|
||||
.details("grant_type", OAuth2Constants.JWT_AUTHORIZATION_GRANT)
|
||||
.details("username", username);
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
protected void assertFailure(String expectedErrorDescription, AccessTokenResponse response, EventRepresentation event) {
|
||||
assertFailure("invalid_grant", expectedErrorDescription, response, event);
|
||||
}
|
||||
|
||||
protected void assertFailure(String expectedError, String expectedErrorDescription, AccessTokenResponse response, EventRepresentation event) {
|
||||
Assertions.assertFalse(response.isSuccess());
|
||||
Assertions.assertEquals(expectedError, response.getError());
|
||||
Assertions.assertEquals(expectedErrorDescription, response.getErrorDescription());
|
||||
EventAssertion.assertError(event)
|
||||
.type(EventType.LOGIN_ERROR)
|
||||
.error("invalid_request")
|
||||
.details("grant_type", OAuth2Constants.JWT_AUTHORIZATION_GRANT)
|
||||
.details("reason", expectedErrorDescription);
|
||||
}
|
||||
|
||||
protected void assertFailurePolicy(String expectedError, String expectedErrorDescription, AccessTokenResponse response, EventRepresentation event) {
|
||||
Assertions.assertFalse(response.isSuccess());
|
||||
Assertions.assertEquals(expectedError, response.getError());
|
||||
Assertions.assertEquals(expectedErrorDescription, response.getErrorDescription());
|
||||
EventAssertion.assertError(event)
|
||||
.type(EventType.LOGIN_ERROR)
|
||||
.error(expectedError)
|
||||
.details("grant_type", OAuth2Constants.JWT_AUTHORIZATION_GRANT)
|
||||
.details("reason", Details.CLIENT_POLICY_ERROR)
|
||||
.details(Details.CLIENT_POLICY_ERROR, expectedError)
|
||||
.details(Details.CLIENT_POLICY_ERROR_DETAIL, expectedErrorDescription);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
package org.keycloak.tests.oauth;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.services.clientpolicy.condition.GrantTypeConditionFactory;
|
||||
import org.keycloak.services.clientpolicy.executor.DownscopeAssertionGrantEnforcerExecutorFactory;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.realm.ClientPolicyBuilder;
|
||||
import org.keycloak.testframework.realm.ClientProfileBuilder;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
import org.keycloak.testframework.realm.RealmConfigBuilder;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
|
||||
import org.hamcrest.MatcherAssert;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
@KeycloakIntegrationTest(config = JWTAuthorizationGrantTest.JWTAuthorizationGrantServerConfig.class)
|
||||
public class JWTAuthorizationGrantDownscopeClientPoliciesTest extends BaseAbstractJWTAuthorizationGrantTest {
|
||||
|
||||
@InjectRealm(config = JWTAuthorizationGranthRealmConfig.class)
|
||||
protected ManagedRealm realm;
|
||||
|
||||
@Test
|
||||
public void testDownscope() throws Exception {
|
||||
// test with all the scopes
|
||||
String jwt = identityProvider.encodeToken(createDefaultAuthorizationGrantToken("email profile address"));
|
||||
AccessTokenResponse response = oAuthClient.openid(false).scope("address").jwtAuthorizationGrantRequest(jwt).send();
|
||||
AccessToken token = assertSuccess("test-app", "basic-user", response);
|
||||
MatcherAssert.assertThat(List.of(token.getScope().split(" ")), Matchers.containsInAnyOrder("email", "profile", "address"));
|
||||
|
||||
// test with less scopes => downscope
|
||||
jwt = identityProvider.encodeToken(createDefaultAuthorizationGrantToken("email profile address"));
|
||||
response = oAuthClient.openid(false).scope(null).jwtAuthorizationGrantRequest(jwt).send();
|
||||
token = assertSuccess("test-app", "basic-user", response);
|
||||
MatcherAssert.assertThat(List.of(token.getScope().split(" ")), Matchers.containsInAnyOrder("email", "profile"));
|
||||
|
||||
// test default scopes are restricted if not present in initial token
|
||||
jwt = identityProvider.encodeToken(createDefaultAuthorizationGrantToken("profile address"));
|
||||
response = oAuthClient.openid(false).scope("address").jwtAuthorizationGrantRequest(jwt).send();
|
||||
token = assertSuccess("test-app", "basic-user", response);
|
||||
MatcherAssert.assertThat(List.of(token.getScope().split(" ")), Matchers.containsInAnyOrder("profile", "address"));
|
||||
|
||||
// test requesting a valid optional scope for the client but not present initially
|
||||
jwt = identityProvider.encodeToken(createDefaultAuthorizationGrantToken("email profile"));
|
||||
response = oAuthClient.openid(false).scope("address").jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailurePolicy("invalid_scope", "Scopes [address] not present in the initial access token [profile, email]", response, events.poll());
|
||||
|
||||
// test requesting a default scope not present in the initial token
|
||||
jwt = identityProvider.encodeToken(createDefaultAuthorizationGrantToken("email address"));
|
||||
response = oAuthClient.openid(false).scope("email profile address").jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailurePolicy("invalid_scope", "Scopes [profile] not present in the initial access token [address, email]", response, events.poll());
|
||||
}
|
||||
|
||||
public static class JWTAuthorizationGranthRealmConfig extends OIDCIdentityProviderJWTAuthorizationGrantTest.JWTAuthorizationGrantRealmConfig {
|
||||
|
||||
@Override
|
||||
public RealmConfigBuilder configure(RealmConfigBuilder realm) {
|
||||
super.configure(realm);
|
||||
|
||||
realm.clientProfile(ClientProfileBuilder.create()
|
||||
.name("executor")
|
||||
.description("executor description")
|
||||
.executor(DownscopeAssertionGrantEnforcerExecutorFactory.PROVIDER_ID, null)
|
||||
.build());
|
||||
|
||||
realm.clientPolicy(ClientPolicyBuilder.create()
|
||||
.name("policy")
|
||||
.description("description of policy")
|
||||
.condition(GrantTypeConditionFactory.PROVIDER_ID, ClientPolicyBuilder.grantTypeConditionConfiguration(
|
||||
OAuth2Constants.JWT_AUTHORIZATION_GRANT))
|
||||
.profile("executor")
|
||||
.build());
|
||||
|
||||
return realm;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user