mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-10 15:32:05 -03:30
Possibility to enforce authorization code binding to DPoP
closes #42740 Signed-off-by: mposolda <mposolda@gmail.com>
This commit is contained in:
parent
47f85631f3
commit
45fa5edbbb
@ -32,6 +32,7 @@ import org.keycloak.representations.idm.ClientPolicyExecutorConfigurationReprese
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyContext;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyException;
|
||||
import org.keycloak.services.clientpolicy.context.AuthorizationRequestContext;
|
||||
import org.keycloak.services.clientpolicy.context.ClientCRUDContext;
|
||||
import org.keycloak.services.clientpolicy.context.TokenRevokeContext;
|
||||
import org.keycloak.services.util.DPoPUtil;
|
||||
@ -65,6 +66,9 @@ public class DPoPBindEnforcerExecutor implements ClientPolicyExecutorProvider<DP
|
||||
@JsonProperty("auto-configure")
|
||||
protected Boolean autoConfigure;
|
||||
|
||||
@JsonProperty("enforce-authorization-code-binding-to-dpop")
|
||||
protected Boolean enforceAuthorizationCodeBindingToDpop;
|
||||
|
||||
public Boolean isAutoConfigure() {
|
||||
return autoConfigure;
|
||||
}
|
||||
@ -72,6 +76,14 @@ public class DPoPBindEnforcerExecutor implements ClientPolicyExecutorProvider<DP
|
||||
public void setAutoConfigure(Boolean autoConfigure) {
|
||||
this.autoConfigure = autoConfigure;
|
||||
}
|
||||
|
||||
public Boolean getEnforceAuthorizationCodeBindingToDpop() {
|
||||
return enforceAuthorizationCodeBindingToDpop;
|
||||
}
|
||||
|
||||
public void setEnforceAuthorizationCodeBindingToDpop(Boolean enforceAuthorizationCodeBindingToDpop) {
|
||||
this.enforceAuthorizationCodeBindingToDpop = enforceAuthorizationCodeBindingToDpop;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -95,6 +107,10 @@ public class DPoPBindEnforcerExecutor implements ClientPolicyExecutorProvider<DP
|
||||
autoConfigure(clientUpdateContext.getProposedClientRepresentation());
|
||||
validate(clientUpdateContext.getProposedClientRepresentation());
|
||||
break;
|
||||
case AUTHORIZATION_REQUEST:
|
||||
AuthorizationRequestContext authzRequestContext = (AuthorizationRequestContext) context;
|
||||
checkOnAuthorizationRequest(authzRequestContext);
|
||||
break;
|
||||
case TOKEN_REQUEST:
|
||||
case TOKEN_REFRESH:
|
||||
case USERINFO_REQUEST:
|
||||
@ -140,6 +156,13 @@ public class DPoPBindEnforcerExecutor implements ClientPolicyExecutorProvider<DP
|
||||
validateBinding(token, dPoP);
|
||||
}
|
||||
|
||||
private void checkOnAuthorizationRequest(AuthorizationRequestContext authzRequestContext) throws ClientPolicyException {
|
||||
if (configuration.getEnforceAuthorizationCodeBindingToDpop() != null && configuration.getEnforceAuthorizationCodeBindingToDpop() && (authzRequestContext.getAuthorizationEndpointRequest().getDpopJkt() == null)) {
|
||||
// Checking only the presence of the parameter here. As long as parameter is present, it is automatically saved to authenticationSession and checked later in token request
|
||||
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Missing parameter: dpop_jkt");
|
||||
}
|
||||
}
|
||||
|
||||
private DPoP retrieveAndVerifyDPoP(HttpRequest request) throws ClientPolicyException {
|
||||
DPoP dPoP = null;
|
||||
try {
|
||||
|
||||
@ -17,15 +17,11 @@
|
||||
|
||||
package org.keycloak.services.clientpolicy.executor;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.Config.Scope;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.Profile.Feature;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
public class DPoPBindEnforcerExecutorFactory implements ClientPolicyExecutorProviderFactory {
|
||||
@ -34,9 +30,14 @@ public class DPoPBindEnforcerExecutorFactory implements ClientPolicyExecutorPro
|
||||
|
||||
public static final String AUTO_CONFIGURE = "auto-configure";
|
||||
|
||||
public static final String ENFORCE_AUTHORIZATION_CODE_BINDING_TO_DPOP = "enforce-authorization-code-binding-to-dpop";
|
||||
|
||||
private static final ProviderConfigProperty AUTO_CONFIGURE_PROPERTY = new ProviderConfigProperty(
|
||||
AUTO_CONFIGURE, "Auto-configure", "If On, then the during client creation or update, the configuration of the client will be auto-configured to use DPoP bind token", ProviderConfigProperty.BOOLEAN_TYPE, false);
|
||||
|
||||
private static final ProviderConfigProperty ENFORCE_AUTHORIZATION_CODE_BINDING_TO_DPOP_KEY = new ProviderConfigProperty(
|
||||
ENFORCE_AUTHORIZATION_CODE_BINDING_TO_DPOP, "Enforce Authorization Code binding to DPoP key", "If On, then there is enforced authorization code binding to DPoP key. This means that parameter 'dpop_jkt' will be required in the OIDC/OAuth2 authentication requests and will be verified during token request if it matches DPoP proof. When this is false, it is still possible to use 'dpop_jkt' parameter, but it will not be required", ProviderConfigProperty.BOOLEAN_TYPE, false);
|
||||
|
||||
@Override
|
||||
public ClientPolicyExecutorProvider create(KeycloakSession session) {
|
||||
return new DPoPBindEnforcerExecutor(session);
|
||||
@ -66,6 +67,6 @@ public class DPoPBindEnforcerExecutorFactory implements ClientPolicyExecutorPro
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return Collections.singletonList(AUTO_CONFIGURE_PROPERTY);
|
||||
return List.of(AUTO_CONFIGURE_PROPERTY, ENFORCE_AUTHORIZATION_CODE_BINDING_TO_DPOP_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
@ -368,7 +368,8 @@
|
||||
{
|
||||
"executor": "dpop-bind-enforcer",
|
||||
"configuration": {
|
||||
"auto-configure": "true"
|
||||
"auto-configure": "true",
|
||||
"enforce-authorization-code-binding-to-dpop": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -454,7 +455,8 @@
|
||||
{
|
||||
"executor": "dpop-bind-enforcer",
|
||||
"configuration": {
|
||||
"auto-configure": "true"
|
||||
"auto-configure": "true",
|
||||
"enforce-authorization-code-binding-to-dpop": "false"
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -532,7 +534,8 @@
|
||||
{
|
||||
"executor": "dpop-bind-enforcer",
|
||||
"configuration": {
|
||||
"auto-configure": "true"
|
||||
"auto-configure": "true",
|
||||
"enforce-authorization-code-binding-to-dpop": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@ -42,6 +42,7 @@ import org.keycloak.common.util.Time;
|
||||
import org.keycloak.crypto.Algorithm;
|
||||
import org.keycloak.crypto.KeyUse;
|
||||
import org.keycloak.crypto.KeyWrapper;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.jose.jwk.ECPublicJWK;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
@ -81,6 +82,7 @@ import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientProfileBuilder;
|
||||
import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientProfilesBuilder;
|
||||
import org.keycloak.testsuite.util.UserBuilder;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
import org.keycloak.testsuite.util.oauth.AuthorizationEndpointResponse;
|
||||
import org.keycloak.testsuite.util.oauth.IntrospectionResponse;
|
||||
import org.keycloak.testsuite.util.oauth.UserInfoResponse;
|
||||
import org.keycloak.testsuite.util.ServerURLs;
|
||||
@ -644,7 +646,7 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
|
||||
// register profiles
|
||||
String json = (new ClientProfilesBuilder()).addProfile(
|
||||
(new ClientProfileBuilder()).createProfile("MyProfile", "Le Premier Profil")
|
||||
.addExecutor(DPoPBindEnforcerExecutorFactory.PROVIDER_ID, createDPoPBindEnforcerExecutorConfig(Boolean.FALSE))
|
||||
.addExecutor(DPoPBindEnforcerExecutorFactory.PROVIDER_ID, createDPoPBindEnforcerExecutorConfig(Boolean.FALSE, Boolean.FALSE))
|
||||
.toRepresentation()
|
||||
).toString();
|
||||
updateProfiles(json);
|
||||
@ -686,7 +688,7 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
|
||||
|
||||
json = (new ClientProfilesBuilder()).addProfile(
|
||||
(new ClientProfileBuilder()).createProfile("MyProfile", "Le Premier Profil")
|
||||
.addExecutor(DPoPBindEnforcerExecutorFactory.PROVIDER_ID, createDPoPBindEnforcerExecutorConfig(Boolean.TRUE))
|
||||
.addExecutor(DPoPBindEnforcerExecutorFactory.PROVIDER_ID, createDPoPBindEnforcerExecutorConfig(Boolean.TRUE, Boolean.FALSE))
|
||||
.toRepresentation()
|
||||
).toString();
|
||||
updateProfiles(json);
|
||||
@ -783,6 +785,47 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
|
||||
oauth.logoutForm().idTokenHint(encodedIdToken).open();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDPoPBindEnforcerExecutorWithEnforcedAuthzCodeBinding() throws Exception {
|
||||
// register profiles
|
||||
String json = (new ClientProfilesBuilder()).addProfile(
|
||||
(new ClientProfileBuilder()).createProfile("MyProfile", "Le Premier Profil")
|
||||
.addExecutor(DPoPBindEnforcerExecutorFactory.PROVIDER_ID, createDPoPBindEnforcerExecutorConfig(Boolean.FALSE, Boolean.TRUE))
|
||||
.toRepresentation()
|
||||
).toString();
|
||||
updateProfiles(json);
|
||||
|
||||
// register policies
|
||||
json = (new ClientPoliciesBuilder()).addPolicy(
|
||||
(new ClientPolicyBuilder()).createPolicy("MyPolicy", "La Primera Plitica", Boolean.TRUE)
|
||||
.addCondition(ClientAccessTypeConditionFactory.PROVIDER_ID,
|
||||
createClientAccessTypeConditionConfig(List.of(ClientAccessTypeConditionFactory.TYPE_PUBLIC)))
|
||||
.addProfile("MyProfile")
|
||||
.toRepresentation()
|
||||
).toString();
|
||||
updatePolicies(json);
|
||||
|
||||
// Login without dpop_jkt - failure
|
||||
oauth.client(TEST_PUBLIC_CLIENT_ID);
|
||||
oauth.openLoginForm();
|
||||
AuthorizationEndpointResponse response = oauth.parseLoginResponse();
|
||||
assertEquals(OAuthErrorException.INVALID_REQUEST, response.getError());
|
||||
assertEquals("Missing parameter: dpop_jkt", response.getErrorDescription());
|
||||
events.expectClientPolicyError(EventType.LOGIN_ERROR, OAuthErrorException.INVALID_REQUEST,
|
||||
Details.CLIENT_POLICY_ERROR, OAuthErrorException.INVALID_REQUEST,
|
||||
"Missing parameter: dpop_jkt").client(oauth.getClientId()).user((String) null)
|
||||
.assertEvent();
|
||||
|
||||
// Login with dpop_jkt -- should be OK
|
||||
long clockSkew = 10;
|
||||
sendAuthorizationRequestWithDPoPJkt(jktEc);
|
||||
String dpopProofEcEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.POST, oauth.getEndpoints().getToken(), (long) (Time.currentTime() + clockSkew), Algorithm.ES256, jwsEcHeader, ecKeyPair.getPrivate(), null);
|
||||
successTokenProceduresWithDPoP(dpopProofEcEncoded, jktEc);
|
||||
|
||||
updatePolicies("{}");
|
||||
updateProfiles("{}");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDPoPProofWithClientCredentialsGrant() throws Exception {
|
||||
modifyClient(TEST_CONFIDENTIAL_CLIENT_ID, (clientRepresentation, configWrapper) -> {
|
||||
|
||||
@ -267,9 +267,10 @@ public final class ClientPoliciesUtil {
|
||||
return config;
|
||||
}
|
||||
|
||||
public static DPoPBindEnforcerExecutor.Configuration createDPoPBindEnforcerExecutorConfig(Boolean autoConfigure) {
|
||||
public static DPoPBindEnforcerExecutor.Configuration createDPoPBindEnforcerExecutorConfig(Boolean autoConfigure, Boolean enforceAuthorizationCodeBindingToDpop) {
|
||||
DPoPBindEnforcerExecutor.Configuration config = new DPoPBindEnforcerExecutor.Configuration();
|
||||
config.setAutoConfigure(autoConfigure);
|
||||
config.setEnforceAuthorizationCodeBindingToDpop(enforceAuthorizationCodeBindingToDpop);
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user