Client policies : executor for enforcing DPoP
closes #25315 Signed-off-by: Takashi Norimatsu <takashi.norimatsu.ws@hitachi.com>
This commit is contained in:
parent
0ca73829d0
commit
59536becec
7 changed files with 653 additions and 94 deletions
|
@ -127,6 +127,7 @@ One of several purposes for this executor is to realize the security requirement
|
|||
* Enforce checking if a client is the one to which an intent was issued in a use case where an intent is issued before starting an authorization code flow to get an access token like UK OpenBanking
|
||||
* Enforce prohibiting implicit and hybrid flow
|
||||
* Enforce checking if a PAR request includes necessary parameters included by an authorization request
|
||||
* Enforce <<_dpop-bound-tokens,DPoP-binding tokens>> is used (available when `dpop` feature is enabled)
|
||||
|
||||
[[_client_policy_profile]]
|
||||
=== Profile
|
||||
|
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* Copyright 2023 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 org.jboss.logging.Logger;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.common.Profile.Feature;
|
||||
import org.keycloak.http.HttpRequest;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.RefreshToken;
|
||||
import org.keycloak.representations.dpop.DPoP;
|
||||
import org.keycloak.representations.idm.ClientPolicyExecutorConfigurationRepresentation;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyContext;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyException;
|
||||
import org.keycloak.services.clientpolicy.context.ClientCRUDContext;
|
||||
import org.keycloak.services.clientpolicy.context.TokenRefreshContext;
|
||||
import org.keycloak.services.clientpolicy.context.TokenRevokeContext;
|
||||
import org.keycloak.services.clientpolicy.context.UserInfoRequestContext;
|
||||
import org.keycloak.services.util.DPoPUtil;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import jakarta.ws.rs.core.MultivaluedMap;
|
||||
|
||||
public class DPoPBindEnforcerExecutor implements ClientPolicyExecutorProvider<DPoPBindEnforcerExecutor.Configuration> {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(DPoPBindEnforcerExecutor.class);
|
||||
|
||||
private final KeycloakSession session;
|
||||
private Configuration configuration;
|
||||
|
||||
public DPoPBindEnforcerExecutor(KeycloakSession session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupConfiguration(Configuration config) {
|
||||
this.configuration = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<Configuration> getExecutorConfigurationClass() {
|
||||
return Configuration.class;
|
||||
}
|
||||
|
||||
public static class Configuration extends ClientPolicyExecutorConfigurationRepresentation {
|
||||
@JsonProperty("auto-configure")
|
||||
protected Boolean autoConfigure;
|
||||
|
||||
public Boolean isAutoConfigure() {
|
||||
return autoConfigure;
|
||||
}
|
||||
|
||||
public void setAutoConfigure(Boolean autoConfigure) {
|
||||
this.autoConfigure = autoConfigure;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProviderId() {
|
||||
return DPoPBindEnforcerExecutorFactory.PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeOnEvent(ClientPolicyContext context) throws ClientPolicyException {
|
||||
HttpRequest request = session.getContext().getHttpRequest();
|
||||
switch (context.getEvent()) {
|
||||
case REGISTER:
|
||||
case UPDATE:
|
||||
ClientCRUDContext clientUpdateContext = (ClientCRUDContext)context;
|
||||
autoConfigure(clientUpdateContext.getProposedClientRepresentation());
|
||||
validate(clientUpdateContext.getProposedClientRepresentation());
|
||||
break;
|
||||
case TOKEN_REQUEST:
|
||||
case TOKEN_REFRESH:
|
||||
case USERINFO_REQUEST:
|
||||
case BACKCHANNEL_TOKEN_REQUEST:
|
||||
// Codes for processing these requests verifies DPoP.
|
||||
// If this verification is done twice, DPoPReplayCheck fails. Therefore, the executor only checks existence of DPoP Proof
|
||||
if (request.getHttpHeaders().getHeaderString(DPoPUtil.DPOP_HTTP_HEADER) == null) {
|
||||
throw new ClientPolicyException(OAuthErrorException.INVALID_DPOP_PROOF, "DPoP proof is missing");
|
||||
}
|
||||
break;
|
||||
case TOKEN_REVOKE:
|
||||
checkTokenRevoke((TokenRevokeContext) context, request);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void autoConfigure(ClientRepresentation rep) {
|
||||
if (configuration.isAutoConfigure()) {
|
||||
OIDCAdvancedConfigWrapper.fromClientRepresentation(rep).setUseDPoP(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void validate(ClientRepresentation rep) throws ClientPolicyException {
|
||||
boolean useDPoPToken = OIDCAdvancedConfigWrapper.fromClientRepresentation(rep).isUseDPoP();
|
||||
if (!useDPoPToken) {
|
||||
throw new ClientPolicyException(OAuthErrorException.INVALID_CLIENT_METADATA, "Invalid client metadata: DPoP token in disabled");
|
||||
}
|
||||
}
|
||||
|
||||
private void checkTokenRevoke(TokenRevokeContext context, HttpRequest request) throws ClientPolicyException {
|
||||
DPoP dPoP = retrieveAndVerifyDPoP(request);
|
||||
|
||||
MultivaluedMap<String, String> revokeParameters = context.getParams();
|
||||
String encodedRevokeToken = revokeParameters.getFirst("token");
|
||||
|
||||
AccessToken token = session.tokens().decode(encodedRevokeToken, AccessToken.class);
|
||||
if (token == null) {
|
||||
// this executor does not treat this error case.
|
||||
return;
|
||||
}
|
||||
|
||||
validateBinding(token, dPoP);
|
||||
}
|
||||
|
||||
private DPoP retrieveAndVerifyDPoP(HttpRequest request) throws ClientPolicyException {
|
||||
DPoP dPoP = null;
|
||||
try {
|
||||
dPoP = new DPoPUtil.Validator(session).request(request).uriInfo(session.getContext().getUri()).validate();
|
||||
} catch (VerificationException ex) {
|
||||
logger.tracev("dpop verification error = {0}", ex.getMessage());
|
||||
throw new ClientPolicyException(OAuthErrorException.INVALID_DPOP_PROOF, ex.getMessage());
|
||||
}
|
||||
return dPoP;
|
||||
}
|
||||
|
||||
private void validateBinding(AccessToken token, DPoP dPoP) throws ClientPolicyException {
|
||||
try {
|
||||
DPoPUtil.validateBinding(token, dPoP);
|
||||
} catch (VerificationException ex) {
|
||||
logger.tracev("dpop bind refresh token verification error = {0}", ex.getMessage());
|
||||
throw new ClientPolicyException(OAuthErrorException.INVALID_TOKEN, "DPoP proof and token binding verification failed");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright 2023 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.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, EnvironmentDependentProviderFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "dpop-bind-enforcer";
|
||||
|
||||
public static final String AUTO_CONFIGURE = "auto-configure";
|
||||
|
||||
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);
|
||||
|
||||
@Override
|
||||
public ClientPolicyExecutorProvider create(KeycloakSession session) {
|
||||
return new DPoPBindEnforcerExecutor(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 enforces a client to enable DPoP bind token setting.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return Collections.singletonList(AUTO_CONFIGURE_PROPERTY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
return Profile.isFeatureEnabled(Feature.DPOP);
|
||||
}
|
||||
}
|
|
@ -22,3 +22,4 @@ org.keycloak.services.clientpolicy.executor.SuppressRefreshTokenRotationExecutor
|
|||
org.keycloak.services.clientpolicy.executor.RegistrationAccessTokenRotationDisabledExecutorFactory
|
||||
org.keycloak.services.clientpolicy.executor.RejectImplicitGrantExecutorFactory
|
||||
org.keycloak.services.clientpolicy.executor.SecureParContentsExecutorFactory
|
||||
org.keycloak.services.clientpolicy.executor.DPoPBindEnforcerExecutorFactory
|
|
@ -970,6 +970,10 @@ public class OAuthClient {
|
|||
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, clientId));
|
||||
}
|
||||
|
||||
if (dpopProof != null) {
|
||||
post.addHeader("DPoP", dpopProof);
|
||||
}
|
||||
|
||||
UrlEncodedFormEntity formEntity;
|
||||
try {
|
||||
formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
|
||||
|
|
|
@ -17,25 +17,28 @@
|
|||
|
||||
package org.keycloak.testsuite.oauth;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.emptyOrNullString;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.keycloak.jose.jwk.JWKUtil.toIntegerBytes;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createClientAccessTypeConditionConfig;
|
||||
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createDPoPBindEnforcerExecutorConfig;
|
||||
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createEcJwk;
|
||||
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createRsaJwk;
|
||||
import static org.keycloak.testsuite.util.ClientPoliciesUtil.generateEcdsaKey;
|
||||
import static org.keycloak.testsuite.util.ClientPoliciesUtil.generateSignedDPoPProof;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.Key;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.interfaces.ECPublicKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.security.spec.ECGenParameterSpec;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
@ -47,44 +50,54 @@ import org.junit.Test;
|
|||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.admin.client.resource.ClientResource;
|
||||
import org.keycloak.client.registration.Auth;
|
||||
import org.keycloak.client.registration.ClientRegistration;
|
||||
import org.keycloak.client.registration.ClientRegistrationException;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.util.Base64Url;
|
||||
import org.keycloak.common.util.KeyUtils;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.crypto.Algorithm;
|
||||
import org.keycloak.crypto.AsymmetricSignatureSignerContext;
|
||||
import org.keycloak.crypto.KeyType;
|
||||
import org.keycloak.crypto.KeyUse;
|
||||
import org.keycloak.crypto.KeyWrapper;
|
||||
import org.keycloak.crypto.ECDSASignatureSignerContext;
|
||||
import org.keycloak.crypto.SignatureException;
|
||||
import org.keycloak.crypto.SignatureSignerContext;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.jose.jwk.ECPublicJWK;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
import org.keycloak.jose.jwk.RSAPublicJWK;
|
||||
import org.keycloak.jose.jws.JWSHeader;
|
||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.RefreshToken;
|
||||
import org.keycloak.representations.dpop.DPoP;
|
||||
import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
|
||||
import org.keycloak.representations.idm.ClientInitialAccessPresentation;
|
||||
import org.keycloak.representations.idm.ClientPoliciesRepresentation;
|
||||
import org.keycloak.representations.idm.ClientProfilesRepresentation;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.oidc.OIDCClientRepresentation;
|
||||
import org.keycloak.representations.oidc.TokenMetadataRepresentation;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyException;
|
||||
import org.keycloak.services.clientpolicy.condition.ClientAccessTypeConditionFactory;
|
||||
import org.keycloak.services.clientpolicy.executor.DPoPBindEnforcerExecutorFactory;
|
||||
import org.keycloak.services.resources.Cors;
|
||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
import org.keycloak.testsuite.Assert;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||
import org.keycloak.testsuite.util.Matchers;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
import org.keycloak.testsuite.util.OAuthClient.UserInfoResponse;
|
||||
import org.keycloak.testsuite.util.ServerURLs;
|
||||
import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientPoliciesBuilder;
|
||||
import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientPolicyBuilder;
|
||||
import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientProfileBuilder;
|
||||
import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientProfilesBuilder;
|
||||
import org.keycloak.util.JWKSUtils;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.util.TokenUtil;
|
||||
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
import jakarta.ws.rs.BadRequestException;
|
||||
import jakarta.ws.rs.HttpMethod;
|
||||
|
||||
@EnableFeature(value = Profile.Feature.DPOP, skipRestart = true)
|
||||
|
@ -105,6 +118,8 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
|
|||
private JWSHeader jwsRsaHeader;
|
||||
private JWSHeader jwsEcHeader;
|
||||
|
||||
private ClientRegistration reg;
|
||||
|
||||
@Rule
|
||||
public AssertEvents events = new AssertEvents(this);
|
||||
|
||||
|
@ -490,6 +505,170 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
|
|||
oauth.doLogout(response.getRefreshToken(), TEST_CONFIDENTIAL_CLIENT_SECRET);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDPoPBindEnforcerExecutor() throws Exception {
|
||||
setInitialAccessTokenForDynamicClientRegistration();
|
||||
|
||||
KeyPair ecKeyPair = generateEcdsaKey("secp256r1");
|
||||
KeyPair rsaKeyPair = KeyUtils.generateRsaKeyPair(2048);
|
||||
JWK jwkRsa = createRsaJwk(rsaKeyPair.getPublic());
|
||||
JWK jwkEc = createEcJwk(ecKeyPair.getPublic());
|
||||
|
||||
// register profiles
|
||||
String json = (new ClientProfilesBuilder()).addProfile(
|
||||
(new ClientProfileBuilder()).createProfile("MyProfile", "Le Premier Profil")
|
||||
.addExecutor(DPoPBindEnforcerExecutorFactory.PROVIDER_ID, createDPoPBindEnforcerExecutorConfig(Boolean.FALSE))
|
||||
.toRepresentation()
|
||||
).toString();
|
||||
updateProfiles(json);
|
||||
|
||||
// register policies
|
||||
json = (new ClientPoliciesBuilder()).addPolicy(
|
||||
(new ClientPolicyBuilder()).createPolicy("MyPolicy", "La Primera Plitica", Boolean.TRUE)
|
||||
.addCondition(ClientAccessTypeConditionFactory.PROVIDER_ID,
|
||||
createClientAccessTypeConditionConfig(Arrays.asList(ClientAccessTypeConditionFactory.TYPE_PUBLIC)))
|
||||
.addProfile("MyProfile")
|
||||
.toRepresentation()
|
||||
).toString();
|
||||
updatePolicies(json);
|
||||
|
||||
// register by Admin REST API - fail
|
||||
try {
|
||||
createClientByAdmin(generateSuffixedName("App-by-Admin"), (ClientRepresentation rep) -> {
|
||||
rep.setPublicClient(Boolean.TRUE);
|
||||
});
|
||||
fail();
|
||||
} catch (ClientPolicyException e) {
|
||||
assertEquals(OAuthErrorException.INVALID_CLIENT_METADATA, e.getMessage());
|
||||
}
|
||||
|
||||
// register by Admin REST API - success
|
||||
String cAppAdminAlphaId = createClientByAdmin(generateSuffixedName("App-by-Admin-Alpha"), (ClientRepresentation clientRep) -> {
|
||||
clientRep.setPublicClient(Boolean.TRUE);
|
||||
clientRep.setAttributes(new HashMap<>());
|
||||
clientRep.getAttributes().put(OIDCConfigAttributes.DPOP_BOUND_ACCESS_TOKENS, Boolean.TRUE.toString());
|
||||
});
|
||||
|
||||
// update by Admin REST API - fail
|
||||
try {
|
||||
updateClientByAdmin(cAppAdminAlphaId, (ClientRepresentation clientRep) -> {
|
||||
clientRep.getAttributes().put(OIDCConfigAttributes.DPOP_BOUND_ACCESS_TOKENS, Boolean.FALSE.toString());
|
||||
});
|
||||
} catch (ClientPolicyException cpe) {
|
||||
assertEquals(OAuthErrorException.INVALID_CLIENT_METADATA, cpe.getError());
|
||||
}
|
||||
ClientRepresentation cRep = getClientByAdmin(cAppAdminAlphaId);
|
||||
assertEquals(Boolean.TRUE.toString(), cRep.getAttributes().get(OIDCConfigAttributes.DPOP_BOUND_ACCESS_TOKENS));
|
||||
String appAlphaClientId = cRep.getClientId();
|
||||
|
||||
json = (new ClientProfilesBuilder()).addProfile(
|
||||
(new ClientProfileBuilder()).createProfile("MyProfile", "Le Premier Profil")
|
||||
.addExecutor(DPoPBindEnforcerExecutorFactory.PROVIDER_ID, createDPoPBindEnforcerExecutorConfig(Boolean.TRUE))
|
||||
.toRepresentation()
|
||||
).toString();
|
||||
updateProfiles(json);
|
||||
|
||||
// register by Dynamic Client Registration - success
|
||||
String cAppDynamicBetaId = createClientDynamically(generateSuffixedName("App-in-Dynamic-Beta"), (OIDCClientRepresentation clientRep) -> {
|
||||
clientRep.setTokenEndpointAuthMethod("none");
|
||||
clientRep.setDpopBoundAccessTokens(Boolean.FALSE);
|
||||
});
|
||||
events.expect(EventType.CLIENT_REGISTER).client(cAppDynamicBetaId).user(is(emptyOrNullString())).assertEvent();
|
||||
OIDCClientRepresentation oidcClientRep = getClientDynamically(cAppDynamicBetaId);
|
||||
assertEquals(Boolean.TRUE, oidcClientRep.getDpopBoundAccessTokens());
|
||||
|
||||
// token request without a DPoP proof - fail
|
||||
oauth.clientId(appAlphaClientId);
|
||||
oauth.doLogin(TEST_USER_NAME, TEST_USER_PASSWORD);
|
||||
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||
|
||||
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, null);
|
||||
assertEquals(400, response.getStatusCode());
|
||||
assertEquals(OAuthErrorException.INVALID_DPOP_PROOF, response.getError());
|
||||
assertEquals("DPoP proof is missing", response.getErrorDescription());
|
||||
oauth.idTokenHint(response.getIdToken()).openLogout();
|
||||
|
||||
// token request with a valid DPoP proof - success
|
||||
// EC key for client alpha
|
||||
oauth.doSilentLogin();
|
||||
code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||
|
||||
JWSHeader jwsEcHeader = new JWSHeader(org.keycloak.jose.jws.Algorithm.ES256, DPOP_JWT_HEADER_TYPE, jwkEc.getKeyId(), jwkEc);
|
||||
String dpopProofEcEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.POST.toString(), oauth.getAccessTokenUrl(), Long.valueOf(Time.currentTime()), Algorithm.ES256, jwsEcHeader, ecKeyPair.getPrivate());
|
||||
oauth.dpopProof(dpopProofEcEncoded);
|
||||
response = oauth.doAccessTokenRequest(code, null);
|
||||
|
||||
assertEquals(Status.OK.getStatusCode(), response.getStatusCode());
|
||||
String encodedAccessToken = response.getAccessToken();
|
||||
String encodedRefreshToken = response.getRefreshToken();
|
||||
String encodedIdToken = response.getIdToken();
|
||||
AccessToken accessToken = oauth.verifyToken(encodedAccessToken);
|
||||
jwkEc.getOtherClaims().put(ECPublicJWK.CRV, ((ECPublicJWK)jwkEc).getCrv());
|
||||
jwkEc.getOtherClaims().put(ECPublicJWK.X, ((ECPublicJWK)jwkEc).getX());
|
||||
jwkEc.getOtherClaims().put(ECPublicJWK.Y, ((ECPublicJWK)jwkEc).getY());
|
||||
String jkt = JWKSUtils.computeThumbprint(jwkEc);
|
||||
assertEquals(jkt, accessToken.getConfirmation().getKeyThumbprint());
|
||||
RefreshToken refreshToken = oauth.parseRefreshToken(encodedRefreshToken);
|
||||
assertEquals(jkt, refreshToken.getConfirmation().getKeyThumbprint());
|
||||
|
||||
// userinfo request without a DPoP proof - fail
|
||||
oauth.dpopProof(null);
|
||||
OAuthClient.UserInfoResponse userInfoResponse = oauth.doUserInfoRequestByGet(encodedAccessToken);
|
||||
assertEquals(401, userInfoResponse.getStatusCode());
|
||||
|
||||
// userinfo request with a valid DPoP proof - success
|
||||
jwsEcHeader = new JWSHeader(org.keycloak.jose.jws.Algorithm.ES256, DPOP_JWT_HEADER_TYPE, jwkEc.getKeyId(), jwkEc);
|
||||
dpopProofEcEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.GET.toString(), oauth.getUserInfoUrl(), Long.valueOf(Time.currentTime()), Algorithm.ES256, jwsEcHeader, ecKeyPair.getPrivate());
|
||||
oauth.dpopProof(dpopProofEcEncoded);
|
||||
userInfoResponse = oauth.doUserInfoRequestByGet(encodedAccessToken);
|
||||
assertEquals(200, userInfoResponse.getStatusCode());
|
||||
assertEquals(TEST_USER_NAME, userInfoResponse.getUserInfo().getPreferredUsername());
|
||||
|
||||
// token refresh without a DPoP Proof - fail
|
||||
oauth.dpopProof(null);
|
||||
response = oauth.doRefreshTokenRequest(encodedRefreshToken, null);
|
||||
assertEquals(400, response.getStatusCode());
|
||||
assertEquals(OAuthErrorException.INVALID_DPOP_PROOF, response.getError());
|
||||
assertEquals("DPoP proof is missing", response.getErrorDescription());
|
||||
|
||||
// token refresh with a valid DPoP Proof - success
|
||||
dpopProofEcEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.POST.toString(), oauth.getAccessTokenUrl(), Long.valueOf(Time.currentTime()), Algorithm.ES256, jwsEcHeader, ecKeyPair.getPrivate());
|
||||
oauth.dpopProof(dpopProofEcEncoded);
|
||||
response = oauth.doRefreshTokenRequest(encodedRefreshToken, null);
|
||||
assertEquals(Status.OK.getStatusCode(), response.getStatusCode());
|
||||
encodedAccessToken = response.getAccessToken();
|
||||
encodedRefreshToken = response.getRefreshToken();
|
||||
accessToken = oauth.verifyToken(encodedAccessToken);
|
||||
jwkEc.getOtherClaims().put(ECPublicJWK.CRV, ((ECPublicJWK)jwkEc).getCrv());
|
||||
jwkEc.getOtherClaims().put(ECPublicJWK.X, ((ECPublicJWK)jwkEc).getX());
|
||||
jwkEc.getOtherClaims().put(ECPublicJWK.Y, ((ECPublicJWK)jwkEc).getY());
|
||||
jkt = JWKSUtils.computeThumbprint(jwkEc);
|
||||
assertEquals(jkt, accessToken.getConfirmation().getKeyThumbprint());
|
||||
refreshToken = oauth.parseRefreshToken(encodedRefreshToken);
|
||||
assertEquals(jkt, refreshToken.getConfirmation().getKeyThumbprint());
|
||||
|
||||
// revoke token without a valid DPoP proof - fail
|
||||
JWSHeader jwsRsaHeader = new JWSHeader(org.keycloak.jose.jws.Algorithm.PS256, DPOP_JWT_HEADER_TYPE, jwkRsa.getKeyId(), jwkRsa);
|
||||
String dpopProofRsaEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.POST.toString(), oauth.getTokenRevocationUrl(), Long.valueOf(Time.currentTime()), Algorithm.PS256, jwsRsaHeader, rsaKeyPair.getPrivate());
|
||||
oauth.dpopProof(dpopProofRsaEncoded);
|
||||
CloseableHttpResponse closableHttpResponse = oauth.doTokenRevoke(encodedAccessToken, "access_token", null);
|
||||
assertThat(closableHttpResponse, Matchers.statusCodeIsHC(Status.BAD_REQUEST));
|
||||
|
||||
// revoke token with a valid DPoP proof - success
|
||||
dpopProofEcEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.POST.toString(), oauth.getTokenRevocationUrl(), Long.valueOf(Time.currentTime()), Algorithm.ES256, jwsEcHeader, ecKeyPair.getPrivate());
|
||||
oauth.dpopProof(dpopProofEcEncoded);
|
||||
closableHttpResponse = oauth.doTokenRevoke(encodedAccessToken, "access_token", null);
|
||||
assertThat(closableHttpResponse, Matchers.statusCodeIsHC(Status.OK));
|
||||
String introspectionResponse = oauth.introspectAccessTokenWithClientCredential(appAlphaClientId, null, encodedAccessToken);
|
||||
TokenMetadataRepresentation tokenMetadataRepresentation = JsonSerialization.readValue(introspectionResponse, TokenMetadataRepresentation.class);
|
||||
assertFalse(tokenMetadataRepresentation.isActive());
|
||||
|
||||
updatePolicies("{}");
|
||||
updateProfiles("{}");
|
||||
|
||||
oauth.idTokenHint(encodedIdToken).openLogout();
|
||||
}
|
||||
|
||||
private OAuthClient.AccessTokenResponse getDPoPBindAccessToken(KeyPair rsaKeyPair) throws Exception {
|
||||
oauth.clientId(TEST_CONFIDENTIAL_CLIENT_ID);
|
||||
oauth.doLogin(TEST_USER_NAME, TEST_USER_PASSWORD);
|
||||
|
@ -534,79 +713,6 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
|
|||
assertEquals(errorDescription, response.getErrorDescription());
|
||||
}
|
||||
|
||||
private JWK createRsaJwk(Key publicKey) {
|
||||
RSAPublicKey rsaKey = (RSAPublicKey) publicKey;
|
||||
|
||||
RSAPublicJWK k = new RSAPublicJWK();
|
||||
k.setKeyType(KeyType.RSA);
|
||||
k.setModulus(Base64Url.encode(toIntegerBytes(rsaKey.getModulus())));
|
||||
k.setPublicExponent(Base64Url.encode(toIntegerBytes(rsaKey.getPublicExponent())));
|
||||
|
||||
return k;
|
||||
}
|
||||
|
||||
private JWK createEcJwk(Key publicKey) {
|
||||
ECPublicKey ecKey = (ECPublicKey) publicKey;
|
||||
|
||||
int fieldSize = ecKey.getParams().getCurve().getField().getFieldSize();
|
||||
ECPublicJWK k = new ECPublicJWK();
|
||||
k.setKeyType(KeyType.EC);
|
||||
k.setCrv("P-" + fieldSize);
|
||||
k.setX(Base64Url.encode(toIntegerBytes(ecKey.getW().getAffineX(), fieldSize)));
|
||||
k.setY(Base64Url.encode(toIntegerBytes(ecKey.getW().getAffineY(), fieldSize)));
|
||||
|
||||
return k;
|
||||
}
|
||||
|
||||
private static KeyPair generateEcdsaKey(String ecDomainParamName) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException {
|
||||
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC");
|
||||
SecureRandom randomGen = SecureRandom.getInstance("SHA1PRNG");
|
||||
ECGenParameterSpec ecSpec = new ECGenParameterSpec(ecDomainParamName);
|
||||
keyGen.initialize(ecSpec, randomGen);
|
||||
KeyPair keyPair = keyGen.generateKeyPair();
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
private static SignatureSignerContext createSignatureSignerContext(KeyWrapper keyWrapper) {
|
||||
switch (keyWrapper.getType()) {
|
||||
case KeyType.RSA:
|
||||
return new AsymmetricSignatureSignerContext(keyWrapper);
|
||||
case KeyType.EC:
|
||||
return new ECDSASignatureSignerContext(keyWrapper);
|
||||
default:
|
||||
throw new IllegalArgumentException("No signer provider for key algorithm type " + keyWrapper.getType());
|
||||
}
|
||||
}
|
||||
|
||||
private static String generateSignedDPoPProof(String jti, String htm, String htu, Long iat, String algorithm, JWSHeader jwsHeader, PrivateKey privateKey) throws IOException {
|
||||
|
||||
String dpopProofHeaderEncoded = Base64Url.encode(JsonSerialization.writeValueAsBytes(jwsHeader));
|
||||
|
||||
DPoP dpop = new DPoP();
|
||||
dpop.id(jti);
|
||||
dpop.setHttpMethod(htm);
|
||||
dpop.setHttpUri(htu);
|
||||
dpop.iat(iat);
|
||||
|
||||
String dpopProofPayloadEncoded = Base64Url.encode(JsonSerialization.writeValueAsBytes(dpop));
|
||||
|
||||
try {
|
||||
KeyWrapper keyWrapper = new KeyWrapper();
|
||||
keyWrapper.setKid(jwsHeader.getKeyId());
|
||||
keyWrapper.setAlgorithm(algorithm);
|
||||
keyWrapper.setPrivateKey(privateKey);
|
||||
keyWrapper.setType(privateKey.getAlgorithm());
|
||||
keyWrapper.setUse(KeyUse.SIG);
|
||||
SignatureSignerContext sigCtx = createSignatureSignerContext(keyWrapper);
|
||||
|
||||
String data = dpopProofHeaderEncoded + "." + dpopProofPayloadEncoded;
|
||||
byte[] signatureByteArray = sigCtx.sign(data.getBytes());
|
||||
return data + "." + Base64Url.encode(signatureByteArray);
|
||||
} catch (SignatureException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void changeDPoPBound(String clientId, boolean isEnabled) {
|
||||
ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm(REALM_NAME), clientId);
|
||||
ClientRepresentation clientRep = clientResource.toRepresentation();
|
||||
|
@ -614,7 +720,7 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
|
|||
clientResource.update(clientRep);
|
||||
}
|
||||
|
||||
private String createClientByAdmin(String clientName, Consumer<ClientRepresentation> op) {
|
||||
private String createClientByAdmin(String clientName, Consumer<ClientRepresentation> op) throws ClientPolicyException {
|
||||
ClientRepresentation clientRep = new ClientRepresentation();
|
||||
clientRep.setClientId(clientName);
|
||||
clientRep.setName(clientName);
|
||||
|
@ -626,6 +732,16 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
|
|||
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setPostLogoutRedirectUris(Collections.singletonList("+"));
|
||||
op.accept(clientRep);
|
||||
Response resp = adminClient.realm(REALM_NAME).clients().create(clientRep);
|
||||
if (resp.getStatus() == Response.Status.BAD_REQUEST.getStatusCode()) {
|
||||
String respBody = resp.readEntity(String.class);
|
||||
Map<String, String> responseJson = null;
|
||||
try {
|
||||
responseJson = JsonSerialization.readValue(respBody, Map.class);
|
||||
} catch (IOException e) {
|
||||
fail();
|
||||
}
|
||||
throw new ClientPolicyException(responseJson.get(OAuth2Constants.ERROR), responseJson.get(OAuth2Constants.ERROR_DESCRIPTION));
|
||||
}
|
||||
resp.close();
|
||||
assertEquals(Response.Status.CREATED.getStatusCode(), resp.getStatus());
|
||||
// registered components will be removed automatically when a test method finishes regardless of its success or failure.
|
||||
|
@ -633,4 +749,94 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
|
|||
testContext.getOrCreateCleanup(REALM_NAME).addClientUuid(cId);
|
||||
return cId;
|
||||
}
|
||||
|
||||
private void updateProfiles(String json) throws ClientPolicyException {
|
||||
try {
|
||||
ClientProfilesRepresentation clientProfiles = JsonSerialization.readValue(json, ClientProfilesRepresentation.class);
|
||||
adminClient.realm(REALM_NAME).clientPoliciesProfilesResource().updateProfiles(clientProfiles);
|
||||
} catch (BadRequestException e) {
|
||||
throw new ClientPolicyException("update profiles failed", e.getResponse().getStatusInfo().toString());
|
||||
} catch (Exception e) {
|
||||
throw new ClientPolicyException("update profiles failed", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePolicies(String json) throws ClientPolicyException {
|
||||
try {
|
||||
ClientPoliciesRepresentation clientPolicies = json==null ? null : JsonSerialization.readValue(json, ClientPoliciesRepresentation.class);
|
||||
adminClient.realm(REALM_NAME).clientPoliciesPoliciesResource().updatePolicies(clientPolicies);
|
||||
} catch (BadRequestException e) {
|
||||
throw new ClientPolicyException("update policies failed", e.getResponse().getStatusInfo().toString());
|
||||
} catch (IOException e) {
|
||||
throw new ClientPolicyException("update policies failed", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String generateSuffixedName(String name) {
|
||||
return name + "-" + UUID.randomUUID().toString().subSequence(0, 7);
|
||||
}
|
||||
|
||||
private void updateClientByAdmin(String cId, Consumer<ClientRepresentation> op) throws ClientPolicyException {
|
||||
ClientResource clientResource = adminClient.realm(REALM_NAME).clients().get(cId);
|
||||
ClientRepresentation clientRep = clientResource.toRepresentation();
|
||||
op.accept(clientRep);
|
||||
try {
|
||||
clientResource.update(clientRep);
|
||||
} catch (BadRequestException bre) {
|
||||
processClientPolicyExceptionByAdmin(bre);
|
||||
}
|
||||
}
|
||||
|
||||
private void processClientPolicyExceptionByAdmin(BadRequestException bre) throws ClientPolicyException {
|
||||
Response resp = bre.getResponse();
|
||||
if (resp.getStatus() != Response.Status.BAD_REQUEST.getStatusCode()) {
|
||||
resp.close();
|
||||
return;
|
||||
}
|
||||
|
||||
String respBody = resp.readEntity(String.class);
|
||||
Map<String, String> responseJson = null;
|
||||
try {
|
||||
responseJson = JsonSerialization.readValue(respBody, Map.class);
|
||||
} catch (IOException e) {
|
||||
fail();
|
||||
}
|
||||
throw new ClientPolicyException(responseJson.get(OAuth2Constants.ERROR), responseJson.get(OAuth2Constants.ERROR_DESCRIPTION));
|
||||
}
|
||||
|
||||
private ClientRepresentation getClientByAdmin(String cId) throws ClientPolicyException {
|
||||
ClientResource clientResource = adminClient.realm(REALM_NAME).clients().get(cId);
|
||||
try {
|
||||
return clientResource.toRepresentation();
|
||||
} catch (BadRequestException bre) {
|
||||
processClientPolicyExceptionByAdmin(bre);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String createClientDynamically(String clientName, Consumer<OIDCClientRepresentation> op) throws ClientRegistrationException {
|
||||
OIDCClientRepresentation clientRep = new OIDCClientRepresentation();
|
||||
clientRep.setClientName(clientName);
|
||||
clientRep.setClientUri(ServerURLs.getAuthServerContextRoot());
|
||||
clientRep.setRedirectUris(Collections.singletonList(ServerURLs.getAuthServerContextRoot() + "/auth/realms/master/app/auth"));
|
||||
op.accept(clientRep);
|
||||
OIDCClientRepresentation response = reg.oidc().create(clientRep);
|
||||
reg.auth(Auth.token(response));
|
||||
// registered components will be removed automatically when a test method finishes regardless of its success or failure.
|
||||
String clientId = response.getClientId();
|
||||
testContext.getOrCreateCleanup(REALM_NAME).addClientUuid(clientId);
|
||||
return clientId;
|
||||
}
|
||||
|
||||
private void setInitialAccessTokenForDynamicClientRegistration() {
|
||||
// get initial access token for Dynamic Client Registration with authentication
|
||||
reg = ClientRegistration.create().url(suiteContext.getAuthServerInfo().getContextRoot() + "/auth", REALM_NAME).build();
|
||||
ClientInitialAccessPresentation token = adminClient.realm(REALM_NAME).clientInitialAccess().create(new ClientInitialAccessCreatePresentation(0, 10));
|
||||
reg.auth(Auth.token(token));
|
||||
}
|
||||
|
||||
private OIDCClientRepresentation getClientDynamically(String clientId) throws ClientRegistrationException {
|
||||
return reg.oidc().get(clientId);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -21,7 +21,22 @@ import com.fasterxml.jackson.core.JsonProcessingException;
|
|||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.keycloak.admin.client.resource.ClientResource;
|
||||
import org.keycloak.common.util.Base64Url;
|
||||
import org.keycloak.crypto.AsymmetricSignatureSignerContext;
|
||||
import org.keycloak.crypto.ECDSASignatureSignerContext;
|
||||
import org.keycloak.crypto.KeyType;
|
||||
import org.keycloak.crypto.KeyUse;
|
||||
import org.keycloak.crypto.KeyWrapper;
|
||||
import org.keycloak.crypto.SignatureException;
|
||||
import org.keycloak.crypto.SignatureSignerContext;
|
||||
import org.keycloak.jose.jwk.ECPublicJWK;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
import org.keycloak.jose.jwk.RSAPublicJWK;
|
||||
import org.keycloak.jose.jws.JWSHeader;
|
||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||
import org.keycloak.protocol.oidc.grants.ciba.clientpolicy.executor.SecureCibaAuthenticationRequestSigningAlgorithmExecutor;
|
||||
import org.keycloak.representations.dpop.DPoP;
|
||||
import org.keycloak.representations.idm.ClientPoliciesRepresentation;
|
||||
import org.keycloak.representations.idm.ClientPolicyConditionConfigurationRepresentation;
|
||||
import org.keycloak.representations.idm.ClientPolicyConditionRepresentation;
|
||||
|
@ -30,6 +45,7 @@ import org.keycloak.representations.idm.ClientPolicyExecutorRepresentation;
|
|||
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.services.clientpolicy.ClientPolicyEvent;
|
||||
import org.keycloak.services.clientpolicy.condition.ClientAccessTypeCondition;
|
||||
import org.keycloak.services.clientpolicy.condition.ClientRolesCondition;
|
||||
|
@ -39,6 +55,7 @@ import org.keycloak.services.clientpolicy.condition.ClientUpdaterSourceGroupsCon
|
|||
import org.keycloak.services.clientpolicy.condition.ClientUpdaterSourceHostsCondition;
|
||||
import org.keycloak.services.clientpolicy.condition.ClientUpdaterSourceRolesCondition;
|
||||
import org.keycloak.services.clientpolicy.executor.ConsentRequiredExecutor;
|
||||
import org.keycloak.services.clientpolicy.executor.DPoPBindEnforcerExecutor;
|
||||
import org.keycloak.services.clientpolicy.executor.FullScopeDisabledExecutor;
|
||||
import org.keycloak.services.clientpolicy.executor.HolderOfKeyEnforcerExecutor;
|
||||
import org.keycloak.services.clientpolicy.executor.IntentClientBindCheckExecutor;
|
||||
|
@ -50,14 +67,27 @@ import org.keycloak.services.clientpolicy.executor.SecureRequestObjectExecutor;
|
|||
import org.keycloak.services.clientpolicy.executor.SecureResponseTypeExecutor;
|
||||
import org.keycloak.services.clientpolicy.executor.SecureSigningAlgorithmExecutor;
|
||||
import org.keycloak.services.clientpolicy.executor.SecureSigningAlgorithmForSignedJwtExecutor;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.services.clientpolicy.condition.TestRaiseExceptionCondition;
|
||||
import org.keycloak.testsuite.services.clientpolicy.executor.TestRaiseExceptionExecutor;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.Key;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.interfaces.ECPublicKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.security.spec.ECGenParameterSpec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.keycloak.jose.jwk.JWKUtil.toIntegerBytes;
|
||||
|
||||
public final class ClientPoliciesUtil {
|
||||
|
||||
|
@ -235,6 +265,12 @@ public final class ClientPoliciesUtil {
|
|||
return config;
|
||||
}
|
||||
|
||||
public static DPoPBindEnforcerExecutor.Configuration createDPoPBindEnforcerExecutorConfig(Boolean autoConfigure) {
|
||||
DPoPBindEnforcerExecutor.Configuration config = new DPoPBindEnforcerExecutor.Configuration();
|
||||
config.setAutoConfigure(autoConfigure);
|
||||
return config;
|
||||
}
|
||||
|
||||
public static class ClientPoliciesBuilder {
|
||||
private final ClientPoliciesRepresentation policiesRep;
|
||||
|
||||
|
@ -381,4 +417,78 @@ public final class ClientPoliciesUtil {
|
|||
config.setRoles(roles);
|
||||
return config;
|
||||
}
|
||||
|
||||
// DPoP
|
||||
public static JWK createRsaJwk(Key publicKey) {
|
||||
RSAPublicKey rsaKey = (RSAPublicKey) publicKey;
|
||||
|
||||
RSAPublicJWK k = new RSAPublicJWK();
|
||||
k.setKeyType(KeyType.RSA);
|
||||
k.setModulus(Base64Url.encode(toIntegerBytes(rsaKey.getModulus())));
|
||||
k.setPublicExponent(Base64Url.encode(toIntegerBytes(rsaKey.getPublicExponent())));
|
||||
|
||||
return k;
|
||||
}
|
||||
|
||||
public static JWK createEcJwk(Key publicKey) {
|
||||
ECPublicKey ecKey = (ECPublicKey) publicKey;
|
||||
|
||||
int fieldSize = ecKey.getParams().getCurve().getField().getFieldSize();
|
||||
ECPublicJWK k = new ECPublicJWK();
|
||||
k.setKeyType(KeyType.EC);
|
||||
k.setCrv("P-" + fieldSize);
|
||||
k.setX(Base64Url.encode(toIntegerBytes(ecKey.getW().getAffineX(), fieldSize)));
|
||||
k.setY(Base64Url.encode(toIntegerBytes(ecKey.getW().getAffineY(), fieldSize)));
|
||||
|
||||
return k;
|
||||
}
|
||||
|
||||
public static KeyPair generateEcdsaKey(String ecDomainParamName) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException {
|
||||
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC");
|
||||
SecureRandom randomGen = SecureRandom.getInstance("SHA1PRNG");
|
||||
ECGenParameterSpec ecSpec = new ECGenParameterSpec(ecDomainParamName);
|
||||
keyGen.initialize(ecSpec, randomGen);
|
||||
KeyPair keyPair = keyGen.generateKeyPair();
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
public static String generateSignedDPoPProof(String jti, String htm, String htu, Long iat, String algorithm, JWSHeader jwsHeader, PrivateKey privateKey) throws IOException {
|
||||
|
||||
String dpopProofHeaderEncoded = Base64Url.encode(JsonSerialization.writeValueAsBytes(jwsHeader));
|
||||
|
||||
DPoP dpop = new DPoP();
|
||||
dpop.id(jti);
|
||||
dpop.setHttpMethod(htm);
|
||||
dpop.setHttpUri(htu);
|
||||
dpop.iat(iat);
|
||||
|
||||
String dpopProofPayloadEncoded = Base64Url.encode(JsonSerialization.writeValueAsBytes(dpop));
|
||||
|
||||
try {
|
||||
KeyWrapper keyWrapper = new KeyWrapper();
|
||||
keyWrapper.setKid(jwsHeader.getKeyId());
|
||||
keyWrapper.setAlgorithm(algorithm);
|
||||
keyWrapper.setPrivateKey(privateKey);
|
||||
keyWrapper.setType(privateKey.getAlgorithm());
|
||||
keyWrapper.setUse(KeyUse.SIG);
|
||||
SignatureSignerContext sigCtx = createSignatureSignerContext(keyWrapper);
|
||||
|
||||
String data = dpopProofHeaderEncoded + "." + dpopProofPayloadEncoded;
|
||||
byte[] signatureByteArray = sigCtx.sign(data.getBytes());
|
||||
return data + "." + Base64Url.encode(signatureByteArray);
|
||||
} catch (SignatureException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static SignatureSignerContext createSignatureSignerContext(KeyWrapper keyWrapper) {
|
||||
switch (keyWrapper.getType()) {
|
||||
case KeyType.RSA:
|
||||
return new AsymmetricSignatureSignerContext(keyWrapper);
|
||||
case KeyType.EC:
|
||||
return new ECDSASignatureSignerContext(keyWrapper);
|
||||
default:
|
||||
throw new IllegalArgumentException("No signer provider for key algorithm type " + keyWrapper.getType());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue