KEYCLOAK-14200 Client Policy - Executor : Enforce Holder-of-Key Token
Co-authored-by: Hryhorii Hevorkian <hhe@adorsys.com.ua>
This commit is contained in:
parent
ab1dba5fa6
commit
5f445ec18e
9 changed files with 423 additions and 7 deletions
|
@ -17,10 +17,16 @@
|
||||||
|
|
||||||
package org.keycloak.services.clientpolicy;
|
package org.keycloak.services.clientpolicy;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import javax.ws.rs.core.Response.Status;
|
||||||
|
|
||||||
|
import org.keycloak.OAuthErrorException;
|
||||||
|
|
||||||
public class ClientPolicyException extends Exception {
|
public class ClientPolicyException extends Exception {
|
||||||
|
|
||||||
private String error;
|
private String error = OAuthErrorException.INVALID_REQUEST;
|
||||||
private String errorDetail;
|
private String errorDetail;
|
||||||
|
private Status errorStatus = Response.Status.BAD_REQUEST;
|
||||||
|
|
||||||
public ClientPolicyException(String error, String errorDetail) {
|
public ClientPolicyException(String error, String errorDetail) {
|
||||||
super(error);
|
super(error);
|
||||||
|
@ -28,12 +34,26 @@ public class ClientPolicyException extends Exception {
|
||||||
setErrorDetail(errorDetail);
|
setErrorDetail(errorDetail);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ClientPolicyException(String error, String errorDetail, Status errorStatus) {
|
||||||
|
super(error);
|
||||||
|
setError(error);
|
||||||
|
setErrorDetail(errorDetail);
|
||||||
|
setErrorStatus(errorStatus);
|
||||||
|
}
|
||||||
|
|
||||||
public ClientPolicyException(String error, String errorDetail, Throwable throwable) {
|
public ClientPolicyException(String error, String errorDetail, Throwable throwable) {
|
||||||
super(throwable);
|
super(throwable);
|
||||||
setError(error);
|
setError(error);
|
||||||
setErrorDetail(errorDetail);
|
setErrorDetail(errorDetail);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ClientPolicyException(String error, String errorDetail, Status errorStatus, Throwable throwable) {
|
||||||
|
super(throwable);
|
||||||
|
setError(error);
|
||||||
|
setErrorDetail(errorDetail);
|
||||||
|
setErrorStatus(errorStatus);
|
||||||
|
}
|
||||||
|
|
||||||
public String getError() {
|
public String getError() {
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
@ -50,6 +70,14 @@ public class ClientPolicyException extends Exception {
|
||||||
this.errorDetail = errorDetail;
|
this.errorDetail = errorDetail;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Status getErrorStatus() {
|
||||||
|
return errorStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setErrorStatus(Status errorStatus) {
|
||||||
|
this.errorStatus = errorStatus;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If {@link ClientPolicyException} is used to notify the event so that it needs not to have stack trace.
|
* If {@link ClientPolicyException} is used to notify the event so that it needs not to have stack trace.
|
||||||
* @return always null
|
* @return always null
|
||||||
|
|
|
@ -212,7 +212,7 @@ public class LogoutEndpoint {
|
||||||
try {
|
try {
|
||||||
session.clientPolicy().triggerOnEvent(new LogoutRequestContext(form));
|
session.clientPolicy().triggerOnEvent(new LogoutRequestContext(form));
|
||||||
} catch (ClientPolicyException cpe) {
|
} catch (ClientPolicyException cpe) {
|
||||||
throw new ErrorResponseException(Errors.INVALID_REQUEST, cpe.getErrorDetail(), Response.Status.BAD_REQUEST);
|
throw new ErrorResponseException(cpe.getError(), cpe.getErrorDetail(), cpe.getErrorStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
RefreshToken token = null;
|
RefreshToken token = null;
|
||||||
|
|
|
@ -539,7 +539,7 @@ public class TokenEndpoint {
|
||||||
session.clientPolicy().triggerOnEvent(new TokenRefreshContext(formParams));
|
session.clientPolicy().triggerOnEvent(new TokenRefreshContext(formParams));
|
||||||
} catch (ClientPolicyException cpe) {
|
} catch (ClientPolicyException cpe) {
|
||||||
event.error(cpe.getError());
|
event.error(cpe.getError());
|
||||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, cpe.getErrorDetail(), Response.Status.BAD_REQUEST);
|
throw new CorsErrorResponseException(cors, cpe.getError(), cpe.getErrorDetail(), cpe.getErrorStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
AccessTokenResponse res;
|
AccessTokenResponse res;
|
||||||
|
|
|
@ -52,6 +52,7 @@ import org.keycloak.services.clientpolicy.TokenRevokeContext;
|
||||||
import org.keycloak.services.managers.UserSessionCrossDCManager;
|
import org.keycloak.services.managers.UserSessionCrossDCManager;
|
||||||
import org.keycloak.services.managers.UserSessionManager;
|
import org.keycloak.services.managers.UserSessionManager;
|
||||||
import org.keycloak.services.resources.Cors;
|
import org.keycloak.services.resources.Cors;
|
||||||
|
import org.keycloak.services.util.MtlsHoKTokenUtil;
|
||||||
import org.keycloak.util.TokenUtil;
|
import org.keycloak.util.TokenUtil;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -102,7 +103,7 @@ public class TokenRevocationEndpoint {
|
||||||
session.clientPolicy().triggerOnEvent(new TokenRevokeContext(formParams));
|
session.clientPolicy().triggerOnEvent(new TokenRevokeContext(formParams));
|
||||||
} catch (ClientPolicyException cpe) {
|
} catch (ClientPolicyException cpe) {
|
||||||
event.error(cpe.getError());
|
event.error(cpe.getError());
|
||||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, cpe.getErrorDetail(), Response.Status.BAD_REQUEST);
|
throw new CorsErrorResponseException(cors, cpe.getError(), cpe.getErrorDetail(), cpe.getErrorStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
checkToken();
|
checkToken();
|
||||||
|
|
|
@ -43,6 +43,7 @@ import org.keycloak.models.UserSessionModel;
|
||||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
|
import org.keycloak.services.CorsErrorResponseException;
|
||||||
import org.keycloak.services.ErrorResponseException;
|
import org.keycloak.services.ErrorResponseException;
|
||||||
import org.keycloak.services.Urls;
|
import org.keycloak.services.Urls;
|
||||||
import org.keycloak.services.clientpolicy.ClientPolicyException;
|
import org.keycloak.services.clientpolicy.ClientPolicyException;
|
||||||
|
@ -136,7 +137,7 @@ public class UserInfoEndpoint {
|
||||||
try {
|
try {
|
||||||
session.clientPolicy().triggerOnEvent(new UserInfoRequestContext(tokenString));
|
session.clientPolicy().triggerOnEvent(new UserInfoRequestContext(tokenString));
|
||||||
} catch (ClientPolicyException cpe) {
|
} catch (ClientPolicyException cpe) {
|
||||||
throw new ErrorResponseException(Errors.INVALID_REQUEST, cpe.getErrorDetail(), Response.Status.BAD_REQUEST);
|
throw new ErrorResponseException(cpe.getError(), cpe.getErrorDetail(), cpe.getErrorStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
EventBuilder event = new EventBuilder(realm, session, clientConnection)
|
EventBuilder event = new EventBuilder(realm, session, clientConnection)
|
||||||
|
|
|
@ -0,0 +1,162 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 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.resteasy.spi.HttpRequest;
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
|
import org.keycloak.OAuthErrorException;
|
||||||
|
import org.keycloak.component.ComponentModel;
|
||||||
|
import org.keycloak.events.Errors;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||||
|
import org.keycloak.representations.AccessToken;
|
||||||
|
import org.keycloak.representations.RefreshToken;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.services.clientpolicy.*;
|
||||||
|
import org.keycloak.services.util.MtlsHoKTokenUtil;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
|
public class HolderOfKeyEnforceExecutor extends AbstractAugumentingClientRegistrationPolicyExecutor {
|
||||||
|
|
||||||
|
private final KeycloakSession session;
|
||||||
|
private final ComponentModel componentModel;
|
||||||
|
|
||||||
|
public HolderOfKeyEnforceExecutor(KeycloakSession session, ComponentModel componentModel) {
|
||||||
|
super(session, componentModel);
|
||||||
|
this.session = session;
|
||||||
|
this.componentModel = componentModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return componentModel.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getProviderId() {
|
||||||
|
return componentModel.getProviderId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void augment(ClientRepresentation rep) {
|
||||||
|
if (Boolean.parseBoolean(componentModel.getConfig().getFirst(AbstractAugumentingClientRegistrationPolicyExecutor.IS_AUGMENT))) {
|
||||||
|
OIDCAdvancedConfigWrapper.fromClientRepresentation(rep).setUseMtlsHoKToken(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void validate(ClientRepresentation rep) throws ClientPolicyException {
|
||||||
|
boolean useMtlsHokToken = OIDCAdvancedConfigWrapper.fromClientRepresentation(rep).isUseMtlsHokToken();
|
||||||
|
if (!useMtlsHokToken) {
|
||||||
|
throw new ClientPolicyException(OAuthErrorException.INVALID_CLIENT_METADATA, "Invalid client metadata: MTLS token in disabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void executeOnEvent(ClientPolicyContext context) throws ClientPolicyException {
|
||||||
|
super.executeOnEvent(context);
|
||||||
|
HttpRequest request = session.getContext().getContextObject(HttpRequest.class);
|
||||||
|
|
||||||
|
switch (context.getEvent()) {
|
||||||
|
|
||||||
|
case TOKEN_REQUEST:
|
||||||
|
AccessToken.CertConf certConf = MtlsHoKTokenUtil.bindTokenWithClientCertificate(request, session);
|
||||||
|
if (certConf == null) {
|
||||||
|
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Client Certification missing for MTLS HoK Token Binding");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TOKEN_REFRESH:
|
||||||
|
checkTokenRefresh((TokenRefreshContext) context, request);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TOKEN_REVOKE:
|
||||||
|
checkTokenRevoke((TokenRevokeContext) context, request);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case USERINFO_REQUEST:
|
||||||
|
checkUserInfo((UserInfoRequestContext) context, request);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LOGOUT_REQUEST:
|
||||||
|
checkLogout((LogoutRequestContext) context, request);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkLogout(LogoutRequestContext context, HttpRequest request) throws ClientPolicyException {
|
||||||
|
MultivaluedMap<String, String> formParameters = context.getParams();
|
||||||
|
String encodedRefreshToken = formParameters.getFirst(OAuth2Constants.REFRESH_TOKEN);
|
||||||
|
|
||||||
|
RefreshToken refreshToken = session.tokens().decode(encodedRefreshToken, RefreshToken.class);
|
||||||
|
if (refreshToken == null) {
|
||||||
|
// this executor does not treat this error case.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MtlsHoKTokenUtil.verifyTokenBindingWithClientCertificate(refreshToken, request, session)) {
|
||||||
|
throw new ClientPolicyException(Errors.NOT_ALLOWED, MtlsHoKTokenUtil.CERT_VERIFY_ERROR_DESC, Response.Status.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkUserInfo(UserInfoRequestContext context, HttpRequest request) throws ClientPolicyException {
|
||||||
|
String encodedAccessToken = context.getTokenString();
|
||||||
|
|
||||||
|
AccessToken accessToken = session.tokens().decode(encodedAccessToken, AccessToken.class);
|
||||||
|
if (accessToken == null) {
|
||||||
|
// this executor does not treat this error case.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MtlsHoKTokenUtil.verifyTokenBindingWithClientCertificate(accessToken, request, session)) {
|
||||||
|
throw new ClientPolicyException(Errors.NOT_ALLOWED, MtlsHoKTokenUtil.CERT_VERIFY_ERROR_DESC, Response.Status.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkTokenRevoke(TokenRevokeContext context, HttpRequest request) throws ClientPolicyException {
|
||||||
|
MultivaluedMap<String, String> revokeParameters = context.getParams();
|
||||||
|
String encodedRevokeToken = revokeParameters.getFirst("token");
|
||||||
|
|
||||||
|
RefreshToken refreshToken = session.tokens().decode(encodedRevokeToken, RefreshToken.class);
|
||||||
|
if (refreshToken == null) {
|
||||||
|
// this executor does not treat this error case.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MtlsHoKTokenUtil.verifyTokenBindingWithClientCertificate(refreshToken, request, session)) {
|
||||||
|
throw new ClientPolicyException(Errors.NOT_ALLOWED, MtlsHoKTokenUtil.CERT_VERIFY_ERROR_DESC, Response.Status.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkTokenRefresh(TokenRefreshContext context, HttpRequest request) throws ClientPolicyException {
|
||||||
|
MultivaluedMap<String, String> formParameters = context.getParams();
|
||||||
|
String encodedRefreshToken = formParameters.getFirst(OAuth2Constants.REFRESH_TOKEN);
|
||||||
|
|
||||||
|
RefreshToken refreshToken = session.tokens().decode(encodedRefreshToken, RefreshToken.class);
|
||||||
|
if (refreshToken == null) {
|
||||||
|
// this executor does not treat this error case.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MtlsHoKTokenUtil.verifyTokenBindingWithClientCertificate(refreshToken, request, session)) {
|
||||||
|
throw new ClientPolicyException(Errors.NOT_ALLOWED, MtlsHoKTokenUtil.CERT_VERIFY_ERROR_DESC, Response.Status.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 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.keycloak.Config.Scope;
|
||||||
|
import org.keycloak.component.ComponentModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class HolderOfKeyEnforceExecutorFactory implements ClientPolicyExecutorProviderFactory {
|
||||||
|
|
||||||
|
public static final String PROVIDER_ID = "holder-of-key-enforce-executor";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientPolicyExecutorProvider create(KeycloakSession session, ComponentModel model) {
|
||||||
|
return new HolderOfKeyEnforceExecutor(session, model);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 prohibits the client whose MTLS certificate does not match with the certificate thumbprint from the tokens.";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ProviderConfigProperty> getConfigProperties() {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -5,4 +5,5 @@ org.keycloak.services.clientpolicy.executor.PKCEEnforceExecutorFactory
|
||||||
org.keycloak.services.clientpolicy.executor.SecureSessionEnforceExecutorFactory
|
org.keycloak.services.clientpolicy.executor.SecureSessionEnforceExecutorFactory
|
||||||
org.keycloak.services.clientpolicy.executor.SecureSigningAlgorithmEnforceExecutorFactory
|
org.keycloak.services.clientpolicy.executor.SecureSigningAlgorithmEnforceExecutorFactory
|
||||||
org.keycloak.services.clientpolicy.executor.SecureRedirectUriEnforceExecutorFactory
|
org.keycloak.services.clientpolicy.executor.SecureRedirectUriEnforceExecutorFactory
|
||||||
org.keycloak.services.clientpolicy.executor.SecureSigningAlgorithmForSignedJwtEnforceExecutorFactory
|
org.keycloak.services.clientpolicy.executor.SecureSigningAlgorithmForSignedJwtEnforceExecutorFactory
|
||||||
|
org.keycloak.services.clientpolicy.executor.HolderOfKeyEnforceExecutorFactory
|
|
@ -124,6 +124,7 @@ import org.keycloak.services.clientpolicy.condition.ClientUpdateSourceRolesCondi
|
||||||
import org.keycloak.services.clientpolicy.condition.ClientRolesConditionFactory;
|
import org.keycloak.services.clientpolicy.condition.ClientRolesConditionFactory;
|
||||||
import org.keycloak.services.clientpolicy.condition.ClientScopesConditionFactory;
|
import org.keycloak.services.clientpolicy.condition.ClientScopesConditionFactory;
|
||||||
import org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProvider;
|
import org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProvider;
|
||||||
|
import org.keycloak.services.clientpolicy.executor.HolderOfKeyEnforceExecutorFactory;
|
||||||
import org.keycloak.services.clientpolicy.executor.SecureClientAuthEnforceExecutorFactory;
|
import org.keycloak.services.clientpolicy.executor.SecureClientAuthEnforceExecutorFactory;
|
||||||
import org.keycloak.services.clientpolicy.executor.SecureRedirectUriEnforceExecutorFactory;
|
import org.keycloak.services.clientpolicy.executor.SecureRedirectUriEnforceExecutorFactory;
|
||||||
import org.keycloak.services.clientpolicy.executor.PKCEEnforceExecutorFactory;
|
import org.keycloak.services.clientpolicy.executor.PKCEEnforceExecutorFactory;
|
||||||
|
@ -144,6 +145,7 @@ import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResou
|
||||||
import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource;
|
import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource;
|
||||||
import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource.AuthorizationEndpointRequestObject;
|
import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource.AuthorizationEndpointRequestObject;
|
||||||
import org.keycloak.testsuite.services.clientpolicy.condition.TestRaiseExeptionConditionFactory;
|
import org.keycloak.testsuite.services.clientpolicy.condition.TestRaiseExeptionConditionFactory;
|
||||||
|
import org.keycloak.testsuite.util.MutualTLSUtils;
|
||||||
import org.keycloak.testsuite.util.OAuthClient;
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
@ -1351,6 +1353,162 @@ public class ClientPolicyBasicsTest extends AbstractKeycloakTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testHolderOfKeyEnforceExecutor() throws Exception {
|
||||||
|
String policyName = "MyPolicy";
|
||||||
|
createPolicy(policyName, DefaultClientPolicyProviderFactory.PROVIDER_ID, null, null, null);
|
||||||
|
logger.info("... Created Policy : " + policyName);
|
||||||
|
|
||||||
|
createCondition("ClientRolesCondition", ClientRolesConditionFactory.PROVIDER_ID, null, (ComponentRepresentation provider) -> {
|
||||||
|
setConditionClientRoles(provider, Collections.singletonList("sample-client-role"));
|
||||||
|
});
|
||||||
|
registerCondition("ClientRolesCondition", policyName);
|
||||||
|
logger.info("... Registered Condition : ClientRolesCondition");
|
||||||
|
|
||||||
|
createExecutor("HolderOfKeyEnforceExecutor", HolderOfKeyEnforceExecutorFactory.PROVIDER_ID, null, (ComponentRepresentation provider) -> {
|
||||||
|
setExecutorAugmentActivate(provider);
|
||||||
|
});
|
||||||
|
registerExecutor("HolderOfKeyEnforceExecutor", policyName);
|
||||||
|
logger.info("... Registered Executor : HolderOfKeyEnforceExecutor");
|
||||||
|
|
||||||
|
String clientName = "Zahlungs-App";
|
||||||
|
String userPassword = "password";
|
||||||
|
String clientId = createClientDynamically(clientName, (OIDCClientRepresentation clientRep) -> {
|
||||||
|
clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.TLS_CLIENT_AUTH);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
checkMtlsFlow(userPassword);
|
||||||
|
} finally {
|
||||||
|
deleteClientByAdmin(clientId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkMtlsFlow(String password) throws IOException {
|
||||||
|
ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm(REALM_NAME), "test-app");
|
||||||
|
ClientRepresentation clientRep = clientResource.toRepresentation();
|
||||||
|
clientRep.setDefaultRoles(new String[]{"sample-client-role"});
|
||||||
|
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setUseMtlsHoKToken(true);
|
||||||
|
|
||||||
|
clientResource.update(clientRep);
|
||||||
|
|
||||||
|
// Check login.
|
||||||
|
OAuthClient.AuthorizationEndpointResponse loginResponse = oauth.doLogin("test-user@localhost", password);
|
||||||
|
Assert.assertNull(loginResponse.getError());
|
||||||
|
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
|
||||||
|
// Check token obtaining.
|
||||||
|
OAuthClient.AccessTokenResponse accessTokenResponse;
|
||||||
|
try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
|
||||||
|
accessTokenResponse = oauth.doAccessTokenRequest(code, password, client);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new RuntimeException(ioe);
|
||||||
|
}
|
||||||
|
assertEquals(200, accessTokenResponse.getStatusCode());
|
||||||
|
|
||||||
|
// Check token refresh.
|
||||||
|
OAuthClient.AccessTokenResponse accessTokenResponseRefreshed;
|
||||||
|
try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
|
||||||
|
accessTokenResponseRefreshed = oauth.doRefreshTokenRequest(accessTokenResponse.getRefreshToken(), password, client);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new RuntimeException(ioe);
|
||||||
|
}
|
||||||
|
assertEquals(200, accessTokenResponseRefreshed.getStatusCode());
|
||||||
|
|
||||||
|
// Check token introspection.
|
||||||
|
String tokenResponse;
|
||||||
|
try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
|
||||||
|
tokenResponse = oauth.introspectTokenWithClientCredential(TEST_CLIENT, password, "access_token", accessTokenResponse.getAccessToken(), client);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new RuntimeException(ioe);
|
||||||
|
}
|
||||||
|
Assert.assertNotNull(tokenResponse);
|
||||||
|
TokenMetadataRepresentation tokenMetadataRepresentation = JsonSerialization.readValue(tokenResponse, TokenMetadataRepresentation.class);
|
||||||
|
Assert.assertTrue(tokenMetadataRepresentation.isActive());
|
||||||
|
|
||||||
|
// Check token revoke.
|
||||||
|
CloseableHttpResponse tokenRevokeResponse;
|
||||||
|
try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
|
||||||
|
tokenRevokeResponse = oauth.doTokenRevoke(accessTokenResponse.getRefreshToken(), "refresh_token", password, client);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new RuntimeException(ioe);
|
||||||
|
}
|
||||||
|
assertEquals(200, tokenRevokeResponse.getStatusLine().getStatusCode());
|
||||||
|
|
||||||
|
// Check logout.
|
||||||
|
CloseableHttpResponse logoutResponse;
|
||||||
|
try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
|
||||||
|
logoutResponse = oauth.doLogout(accessTokenResponse.getRefreshToken(), password, client);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new RuntimeException(ioe);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(204, logoutResponse.getStatusLine().getStatusCode());
|
||||||
|
|
||||||
|
// Check login.
|
||||||
|
loginResponse = oauth.doLogin("test-user@localhost", password);
|
||||||
|
Assert.assertNull(loginResponse.getError());
|
||||||
|
|
||||||
|
code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
|
||||||
|
// Check token obtaining without certificate
|
||||||
|
try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithoutKeyStoreAndTrustStore()) {
|
||||||
|
accessTokenResponse = oauth.doAccessTokenRequest(code, password, client);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new RuntimeException(ioe);
|
||||||
|
}
|
||||||
|
assertEquals(400, accessTokenResponse.getStatusCode());
|
||||||
|
assertEquals(OAuthErrorException.INVALID_GRANT, accessTokenResponse.getError());
|
||||||
|
|
||||||
|
// Check frontchannel logout and login.
|
||||||
|
oauth.openLogout();
|
||||||
|
loginResponse = oauth.doLogin("test-user@localhost", password);
|
||||||
|
Assert.assertNull(loginResponse.getError());
|
||||||
|
|
||||||
|
code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
|
||||||
|
// Check token obtaining.
|
||||||
|
try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
|
||||||
|
accessTokenResponse = oauth.doAccessTokenRequest(code, password, client);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new RuntimeException(ioe);
|
||||||
|
}
|
||||||
|
assertEquals(200, accessTokenResponse.getStatusCode());
|
||||||
|
|
||||||
|
// Check token refresh with other certificate
|
||||||
|
try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithOtherKeyStoreAndTrustStore()) {
|
||||||
|
accessTokenResponseRefreshed = oauth.doRefreshTokenRequest(accessTokenResponse.getRefreshToken(), password, client);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new RuntimeException(ioe);
|
||||||
|
}
|
||||||
|
assertEquals(401, accessTokenResponseRefreshed.getStatusCode());
|
||||||
|
assertEquals(Errors.NOT_ALLOWED, accessTokenResponseRefreshed.getError());
|
||||||
|
|
||||||
|
// Check token revoke with other certificate
|
||||||
|
try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithOtherKeyStoreAndTrustStore()) {
|
||||||
|
tokenRevokeResponse = oauth.doTokenRevoke(accessTokenResponse.getRefreshToken(), "refresh_token", password, client);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new RuntimeException(ioe);
|
||||||
|
}
|
||||||
|
assertEquals(401, tokenRevokeResponse.getStatusLine().getStatusCode());
|
||||||
|
|
||||||
|
// Check logout without certificate
|
||||||
|
try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithoutKeyStoreAndTrustStore()) {
|
||||||
|
logoutResponse = oauth.doLogout(accessTokenResponse.getRefreshToken(), password, client);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new RuntimeException(ioe);
|
||||||
|
}
|
||||||
|
assertEquals(401, logoutResponse.getStatusLine().getStatusCode());
|
||||||
|
|
||||||
|
// Check logout.
|
||||||
|
try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
|
||||||
|
logoutResponse = oauth.doLogout(accessTokenResponse.getRefreshToken(), password, client);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new RuntimeException(ioe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private CloseableHttpResponse sendRequest(String requestUrl, List<NameValuePair> parameters) throws Exception {
|
private CloseableHttpResponse sendRequest(String requestUrl, List<NameValuePair> parameters) throws Exception {
|
||||||
CloseableHttpClient client = new DefaultHttpClient();
|
CloseableHttpClient client = new DefaultHttpClient();
|
||||||
try {
|
try {
|
||||||
|
@ -2065,4 +2223,4 @@ public class ClientPolicyBasicsTest extends AbstractKeycloakTest {
|
||||||
private void setExecutorAugmentedClientAuthMethod(ComponentRepresentation provider, String augmentedClientAuthMethod) {
|
private void setExecutorAugmentedClientAuthMethod(ComponentRepresentation provider, String augmentedClientAuthMethod) {
|
||||||
provider.getConfig().putSingle(SecureClientAuthEnforceExecutorFactory.CLIENT_AUTHNS_AUGMENT, augmentedClientAuthMethod);
|
provider.getConfig().putSingle(SecureClientAuthEnforceExecutorFactory.CLIENT_AUTHNS_AUGMENT, augmentedClientAuthMethod);
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue