KEYCLOAK-14204 FAPI-RW Client Policy - Executor : Enforce Request Object satisfying high security level

This commit is contained in:
Takashi Norimatsu 2020-09-15 14:24:38 +09:00 committed by Marek Posolda
parent 006b98ae13
commit 6596811d5d
11 changed files with 662 additions and 10 deletions

View file

@ -158,7 +158,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
}
try {
session.clientPolicy().triggerOnEvent(new AuthorizationRequestContext(parsedResponseType, request, redirectUri));
session.clientPolicy().triggerOnEvent(new AuthorizationRequestContext(parsedResponseType, request, redirectUri, params));
} catch (ClientPolicyException cpe) {
return redirectErrorToClient(parsedResponseMode, cpe.getError(), cpe.getErrorDetail());
}

View file

@ -77,7 +77,6 @@ public class AuthorizationEndpointRequestParserProcessor {
} else if (requestUriParam != null) {
try (InputStream is = session.getProvider(HttpClientProvider.class).get(requestUriParam)) {
String retrievedRequest = StreamUtil.readString(is);
new AuthzEndpointRequestObjectParser(session, retrievedRequest, client).parseRequest(request);
}
}

View file

@ -60,6 +60,7 @@ class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser {
throw new RuntimeException("Failed to verify signature on 'request' object");
}
}
session.setAttribute(AuthzEndpointRequestParser.AUTHZ_REQUEST_OBJECT, requestParams);
}
@Override

View file

@ -30,7 +30,7 @@ import java.util.Set;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
abstract class AuthzEndpointRequestParser {
public abstract class AuthzEndpointRequestParser {
private static final Logger logger = Logger.getLogger(AuthzEndpointRequestParser.class);
@ -46,8 +46,10 @@ abstract class AuthzEndpointRequestParser {
*/
public static final int ADDITIONAL_REQ_PARAMS_MAX_SIZE = 200;
public static final String AUTHZ_REQUEST_OBJECT = "ParsedRequestObject";
/** Set of known protocol GET params not to be stored into additionalReqParams} */
private static final Set<String> KNOWN_REQ_PARAMS = new HashSet<>();
public static final Set<String> KNOWN_REQ_PARAMS = new HashSet<>();
static {
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.CLIENT_ID_PARAM);
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);

View file

@ -17,6 +17,8 @@
package org.keycloak.services.clientpolicy;
import javax.ws.rs.core.MultivaluedMap;
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.services.clientpolicy.ClientPolicyContext;
@ -27,13 +29,16 @@ public class AuthorizationRequestContext implements ClientPolicyContext {
private final OIDCResponseType parsedResponseType;
private final AuthorizationEndpointRequest request;
private final String redirectUri;
private final MultivaluedMap<String, String> requestParameters;
public AuthorizationRequestContext(OIDCResponseType parsedResponseType,
AuthorizationEndpointRequest request,
String redirectUri) {
String redirectUri,
MultivaluedMap<String, String> requestParameters) {
this.parsedResponseType = parsedResponseType;
this.request = request;
this.redirectUri = redirectUri;
this.requestParameters = requestParameters;
}
@Override
@ -53,4 +58,7 @@ public class AuthorizationRequestContext implements ClientPolicyContext {
return redirectUri;
}
public MultivaluedMap<String, String> getRequestParameters() {
return requestParameters;
}
}

View file

@ -0,0 +1,185 @@
/*
* 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 java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.ws.rs.core.MultivaluedMap;
import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.common.util.StreamUtil;
import org.keycloak.common.util.Time;
import org.keycloak.component.ComponentModel;
import org.keycloak.connections.httpclient.HttpClientProvider;
import org.keycloak.constants.AdapterConstants;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
import org.keycloak.protocol.oidc.endpoints.request.AuthzEndpointRequestParser;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.Urls;
import org.keycloak.services.clientpolicy.AuthorizationRequestContext;
import org.keycloak.services.clientpolicy.ClientPolicyContext;
import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.clientpolicy.ClientPolicyLogger;
import org.keycloak.util.JsonSerialization;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
public class SecureRequestObjectExecutor implements ClientPolicyExecutorProvider {
private static final Logger logger = Logger.getLogger(SecureRequestObjectExecutor.class);
private final KeycloakSession session;
private final ComponentModel componentModel;
public static final String INVALID_REQUEST_OBJECT = "invalid_request_object";
public SecureRequestObjectExecutor(KeycloakSession session, ComponentModel componentModel) {
this.session = session;
this.componentModel = componentModel;
}
@Override
public void executeOnEvent(ClientPolicyContext context) throws ClientPolicyException {
switch (context.getEvent()) {
case AUTHORIZATION_REQUEST:
AuthorizationRequestContext authorizationRequestContext = (AuthorizationRequestContext)context;
executeOnAuthorizationRequest(authorizationRequestContext.getparsedResponseType(),
authorizationRequestContext.getAuthorizationEndpointRequest(),
authorizationRequestContext.getRedirectUri(),
authorizationRequestContext.getRequestParameters());
break;
default:
return;
}
}
private void executeOnAuthorizationRequest(
OIDCResponseType parsedResponseType,
AuthorizationEndpointRequest request,
String redirectUri,
MultivaluedMap<String, String> params) throws ClientPolicyException {
ClientPolicyLogger.log(logger, "Authz Endpoint - authz request");
if (params == null) {
ClientPolicyLogger.log(logger, "request parameter not exist.");
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Missing parameters");
}
String requestParam = params.getFirst(OIDCLoginProtocol.REQUEST_PARAM);
String requestUriParam = params.getFirst(OIDCLoginProtocol.REQUEST_URI_PARAM);
// check whether whether request object exists
if (requestParam == null && requestUriParam == null) {
ClientPolicyLogger.log(logger, "request object not exist.");
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Invalid parameter");
}
JsonNode requestObject = (JsonNode)session.getAttribute(AuthzEndpointRequestParser.AUTHZ_REQUEST_OBJECT);
// check whether request object exists
if (requestObject == null || requestObject.isEmpty()) {
ClientPolicyLogger.log(logger, "request object not exist.");
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Invalid parameter");
}
// check whether scope exists in both query parameter and request object
if (params.getFirst(OIDCLoginProtocol.SCOPE_PARAM) == null || requestObject.get(OIDCLoginProtocol.SCOPE_PARAM) == null) {
ClientPolicyLogger.log(logger, "scope does not exists.");
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Missing parameter : scope");
}
// check whether "exp" claim exists
if (requestObject.get("exp") == null) {
ClientPolicyLogger.log(logger, "exp claim not incuded.");
throw new ClientPolicyException(INVALID_REQUEST_OBJECT, "Missing parameter : exp");
}
// check whether request object not expired
long exp = requestObject.get("exp").asLong();
if (Time.currentTime() > exp) { // TODO: Time.currentTime() is int while exp is long...
ClientPolicyLogger.log(logger, "request object expired.");
throw new ClientPolicyException(INVALID_REQUEST_OBJECT, "Request Expired");
}
// check whether "aud" claim exists
List<String> aud = new ArrayList<String>();
JsonNode audience = requestObject.get("aud");
if (audience == null) {
ClientPolicyLogger.log(logger, "aud claim not incuded.");
throw new ClientPolicyException(INVALID_REQUEST_OBJECT, "Missing parameter : aud");
}
if (audience.isArray()) {
for (JsonNode node : audience) aud.add(node.asText());
} else {
aud.add(audience.asText());
}
if (aud.isEmpty()) {
ClientPolicyLogger.log(logger, "aud claim not incuded.");
throw new ClientPolicyException(INVALID_REQUEST_OBJECT, "Missing parameter : aud");
}
// check whether "aud" claim points to this keycloak as authz server
String iss = Urls.realmIssuer(session.getContext().getUri().getBaseUri(), session.getContext().getRealm().getName());
if (!aud.contains(iss)) {
ClientPolicyLogger.log(logger, "aud not points to the intended realm.");
throw new ClientPolicyException(INVALID_REQUEST_OBJECT, "Invalid parameter : aud");
}
// confirm whether all parameters in query string are included in the request object, and have the same values
// argument "request" are parameters overridden by parameters in request object
if (AuthzEndpointRequestParser.KNOWN_REQ_PARAMS.stream().filter(s->params.containsKey(s)).anyMatch(s->!isSameParameterIncluded(s, params.getFirst(s), requestObject))) {
ClientPolicyLogger.log(logger, "not all parameters in query string are included in the request object, and have the same values.");
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Invalid parameter");
}
ClientPolicyLogger.log(logger, "Passed.");
}
private boolean isSameParameterIncluded(String param, String value, JsonNode requestObject) {
if (param.equals(OIDCLoginProtocol.REQUEST_PARAM) || param.equals(OIDCLoginProtocol.REQUEST_URI_PARAM)) return true;
if (requestObject.hasNonNull(param)) return requestObject.get(param).asText().equals(value);
return false;
}
@Override
public String getName() {
return componentModel.getName();
}
@Override
public String getProviderId() {
return componentModel.getProviderId();
}
}

View file

@ -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 java.util.Collections;
import java.util.List;
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;
public class SecureRequestObjectExecutorFactory implements ClientPolicyExecutorProviderFactory {
public static final String PROVIDER_ID = "secure-reqobj-executor";
@Override
public ClientPolicyExecutorProvider create(KeycloakSession session, ComponentModel model) {
return new SecureRequestObjectExecutor(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 "The executor checks whether the client treats the request object in its authorization request by following Financial-grade API Security Profile : Read and Write API Security Profile.";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return Collections.emptyList();
}
}

View file

@ -1 +1,2 @@
org.keycloak.services.clientpolicy.executor.SecureResponseTypeExecutorFactory
org.keycloak.services.clientpolicy.executor.SecureRequestObjectExecutorFactory

View file

@ -21,8 +21,10 @@ import org.jboss.resteasy.annotations.cache.NoCache;
import javax.ws.rs.BadRequestException;
import org.keycloak.OAuth2Constants;
import org.keycloak.common.util.Base64;
import org.keycloak.common.util.Base64Url;
import org.keycloak.common.util.KeyUtils;
import org.keycloak.common.util.PemUtils;
import org.keycloak.constants.AdapterConstants;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.AsymmetricSignatureSignerContext;
import org.keycloak.crypto.KeyType;
@ -35,8 +37,13 @@ import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKBuilder;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.models.Constants;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.testsuite.rest.TestApplicationResourceProviderFactory;
import org.keycloak.util.JsonSerialization;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
@ -44,6 +51,7 @@ import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
@ -203,6 +211,26 @@ public class TestingOIDCEndpointsApplicationResource {
oidcRequest.put(OIDCLoginProtocol.MAX_AGE_PARAM, Integer.parseInt(maxAge));
}
setOidcRequest(oidcRequest, jwaAlgorithm);
}
@GET
@Path("/register-oidc-request")
@Produces(org.keycloak.utils.MediaType.APPLICATION_JWT)
@NoCache
public void registerOIDCRequest(@QueryParam("requestObject") String encodedRequestObject, @QueryParam("jwaAlgorithm") String jwaAlgorithm) {
byte[] serializedRequestObject = Base64Url.decode(encodedRequestObject);
AuthorizationEndpointRequestObject oidcRequest = null;
try {
oidcRequest = JsonSerialization.readValue(serializedRequestObject, AuthorizationEndpointRequestObject.class);
} catch (IOException e) {
throw new BadRequestException("deserialize request object failed : " + e.getMessage());
}
setOidcRequest(oidcRequest, jwaAlgorithm);
}
private void setOidcRequest(Object oidcRequest, String jwaAlgorithm) {
if (!isSupportedAlgorithm(jwaAlgorithm)) throw new BadRequestException("Unknown argument: " + jwaAlgorithm);
if ("none".equals(jwaAlgorithm)) {
@ -252,7 +280,6 @@ public class TestingOIDCEndpointsApplicationResource {
return ret;
}
@GET
@Path("/get-oidc-request")
@Produces(org.keycloak.utils.MediaType.APPLICATION_JWT)
@ -275,4 +302,194 @@ public class TestingOIDCEndpointsApplicationResource {
public List<String> getSectorIdentifierRedirectUris() {
return clientData.getSectorIdentifierRedirectUris();
}
public static class AuthorizationEndpointRequestObject extends JsonWebToken {
@JsonProperty(OIDCLoginProtocol.CLIENT_ID_PARAM)
String clientId;
@JsonProperty(OIDCLoginProtocol.RESPONSE_TYPE_PARAM)
String responseType;
@JsonProperty(OIDCLoginProtocol.RESPONSE_MODE_PARAM)
String responseMode;
@JsonProperty(OIDCLoginProtocol.REDIRECT_URI_PARAM)
String redirectUriParam;
@JsonProperty(OIDCLoginProtocol.STATE_PARAM)
String state;
@JsonProperty(OIDCLoginProtocol.SCOPE_PARAM)
String scope;
@JsonProperty(OIDCLoginProtocol.LOGIN_HINT_PARAM)
String loginHint;
@JsonProperty(OIDCLoginProtocol.PROMPT_PARAM)
String prompt;
@JsonProperty(OIDCLoginProtocol.NONCE_PARAM)
String nonce;
Integer max_age;
@JsonProperty(OIDCLoginProtocol.UI_LOCALES_PARAM)
String uiLocales;
@JsonProperty(OIDCLoginProtocol.ACR_PARAM)
String acr;
@JsonProperty(OAuth2Constants.DISPLAY)
String display;
@JsonProperty(OIDCLoginProtocol.CODE_CHALLENGE_PARAM)
String codeChallenge;
@JsonProperty(OIDCLoginProtocol.CODE_CHALLENGE_METHOD_PARAM)
String codeChallengeMethod;
@JsonProperty(AdapterConstants.KC_IDP_HINT)
String idpHint;
@JsonProperty(Constants.KC_ACTION)
String action;
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getResponseType() {
return responseType;
}
public void setResponseType(String responseType) {
this.responseType = responseType;
}
public String getResponseMode() {
return responseMode;
}
public void setResponseMode(String responseMode) {
this.responseMode = responseMode;
}
public String getRedirectUriParam() {
return redirectUriParam;
}
public void setRedirectUriParam(String redirectUriParam) {
this.redirectUriParam = redirectUriParam;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public String getScope() {
return scope;
}
public void setScope(String scope) {
this.scope = scope;
}
public String getLoginHint() {
return loginHint;
}
public void setLoginHint(String loginHint) {
this.loginHint = loginHint;
}
public String getPrompt() {
return prompt;
}
public void setPrompt(String prompt) {
this.prompt = prompt;
}
public String getNonce() {
return nonce;
}
public void getNonce(String nonce) {
this.nonce = nonce;
}
public Integer getMax_age() {
return max_age;
}
public void setMax_age(Integer max_age) {
this.max_age = max_age;
}
public String getUiLocales() {
return uiLocales;
}
public void setUiLocales(String uiLocales) {
this.uiLocales = uiLocales;
}
public String getAcr() {
return acr;
}
public void setAcr(String acr) {
this.acr = acr;
}
public String getCodeChallenge() {
return codeChallenge;
}
public void setCodeChallenge(String codeChallenge) {
this.codeChallenge = codeChallenge;
}
public String getCodeChallengeMethod() {
return codeChallengeMethod;
}
public void setCodeChallengeMethod(String codeChallengeMethod) {
this.codeChallengeMethod = codeChallengeMethod;
}
public String getDisplay() {
return display;
}
public void setDisplay(String display) {
this.display = display;
}
public String getIdpHint() {
return idpHint;
}
public void setIdpHint(String idpHint) {
this.idpHint = idpHint;
}
public String getAction() {
return action;
}
public void setAction(String action) {
this.action = action;
}
}
}

View file

@ -60,6 +60,11 @@ public interface TestOIDCEndpointsApplicationResource {
@QueryParam("redirectUri") String redirectUri, @QueryParam("maxAge") String maxAge,
@QueryParam("jwaAlgorithm") String jwaAlgorithm);
@GET
@Path("/register-oidc-request")
@Produces(org.keycloak.utils.MediaType.APPLICATION_JWT)
void registerOIDCRequest(@QueryParam("requestObject") String encodedRequestObject, @QueryParam("jwaAlgorithm") String jwaAlgorithm);
@GET
@Path("/get-oidc-request")
@Produces(org.keycloak.utils.MediaType.APPLICATION_JWT)

View file

@ -23,6 +23,8 @@ import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
@ -53,9 +55,12 @@ import org.keycloak.client.registration.ClientRegistrationException;
import org.keycloak.common.Profile;
import org.keycloak.common.util.Base64Url;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.Time;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
import org.keycloak.jose.jws.Algorithm;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
@ -69,6 +74,7 @@ import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.representations.oidc.TokenMetadataRepresentation;
import org.keycloak.services.Urls;
import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.clientpolicy.ClientPolicyProvider;
import org.keycloak.services.clientpolicy.DefaultClientPolicyProviderFactory;
@ -76,11 +82,16 @@ import org.keycloak.services.clientpolicy.condition.ClientPolicyConditionProvide
import org.keycloak.services.clientpolicy.condition.ClientUpdateContextConditionFactory;
import org.keycloak.services.clientpolicy.condition.ClientRolesConditionFactory;
import org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProvider;
import org.keycloak.services.clientpolicy.executor.SecureRequestObjectExecutor;
import org.keycloak.services.clientpolicy.executor.SecureRequestObjectExecutorFactory;
import org.keycloak.services.clientpolicy.executor.SecureResponseTypeExecutorFactory;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls;
import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResource;
import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource.AuthorizationEndpointRequestObject;
import org.keycloak.testsuite.services.clientpolicy.condition.TestRaiseExeptionConditionFactory;
import org.keycloak.testsuite.services.clientpolicy.executor.TestClientAuthenticationExecutorFactory;
import org.keycloak.testsuite.services.clientpolicy.executor.TestPKCEEnforceExecutorFactory;
@ -90,6 +101,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.keycloak.testsuite.util.ServerURLs;
import org.keycloak.util.JsonSerialization;
@EnableFeature(value = Profile.Feature.CLIENT_POLICIES, skipRestart = true)
public class ClientPolicyBasicsTest extends AbstractKeycloakTest {
@ -108,6 +120,7 @@ public class ClientPolicyBasicsTest extends AbstractKeycloakTest {
static final String SECURERESPONSETYPE_EXECUTOR_NAME = "SecureResponseTypeExecutor";
static final String SECUREREQUESTOBJECT_EXECUTOR_NAME = "SecureRequestObjectExecutor";
ClientRegistration reg;
@ -656,6 +669,162 @@ public class ClientPolicyBasicsTest extends AbstractKeycloakTest {
}
}
@Test
public void testSecureRequestObjectExecutor() throws ClientRegistrationException, ClientPolicyException, URISyntaxException, IOException {
String policyName = "MyPolicy";
createPolicy(policyName, DefaultClientPolicyProviderFactory.PROVIDER_ID, null, null, null);
logger.info("... Created Policy : " + policyName);
createCondition(CLIENTROLES_CONDITION_NAME, ClientRolesConditionFactory.PROVIDER_ID, null, (ComponentRepresentation provider) -> {
setConditionClientRoles(provider, new ArrayList<>(Arrays.asList("sample-client-role")));
});
registerCondition(CLIENTROLES_CONDITION_NAME, policyName);
logger.info("... Registered Condition : " + CLIENTROLES_CONDITION_NAME);
createExecutor(SECUREREQUESTOBJECT_EXECUTOR_NAME, SecureRequestObjectExecutorFactory.PROVIDER_ID, null, (ComponentRepresentation provider) -> {
});
registerExecutor(SECUREREQUESTOBJECT_EXECUTOR_NAME, policyName);
logger.info("... Registered Executor : " + SECUREREQUESTOBJECT_EXECUTOR_NAME);
String clientId = "Zahlungs-App";
String clientSecret = "secret";
String cid = createClientByAdmin(clientId, (ClientRepresentation clientRep) -> {
String[] defaultRoles = {"sample-client-role"};
clientRep.setDefaultRoles(defaultRoles);
clientRep.setSecret(clientSecret);
});
try {
oauth.clientId(clientId);
AuthorizationEndpointRequestObject requestObject;
// check whether whether request object exists
oauth.request(null);
oauth.requestUri(null);
oauth.openLoginForm();
assertEquals(OAuthErrorException.INVALID_REQUEST, oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
assertEquals("Invalid parameter", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION));
// check whether request_uri is https scheme
// cannot test because existing AuthorizationEndpoint check and return error before executing client policy
// check whether request object can be retrieved from request_uri
// cannot test because existing AuthorizationEndpoint check and return error before executing client policy
// check whether request object can be parsed successfully
// cannot test because existing AuthorizationEndpoint check and return error before executing client policy
// check whether scope exists in both query parameter and request object
requestObject = createValidRequestObjectForSecureRequestObjectExecutor(clientId);
requestObject.setScope(null);
registerRequestObject(requestObject, clientId, Algorithm.ES256, true);
oauth.openLoginForm();
assertEquals(OAuthErrorException.INVALID_REQUEST, oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
assertEquals("Missing parameter : scope", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION));
// check whether "exp" claim exists
requestObject = createValidRequestObjectForSecureRequestObjectExecutor(clientId);
requestObject.exp(null);
registerRequestObject(requestObject, clientId, Algorithm.ES256, false);
oauth.openLoginForm();
assertEquals(SecureRequestObjectExecutor.INVALID_REQUEST_OBJECT, oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
assertEquals("Missing parameter : exp", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION));
// check whether request object not expired
requestObject = createValidRequestObjectForSecureRequestObjectExecutor(clientId);
requestObject.exp(Long.valueOf(0));
registerRequestObject(requestObject, clientId, Algorithm.ES256, true);
oauth.openLoginForm();
assertEquals(SecureRequestObjectExecutor.INVALID_REQUEST_OBJECT, oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
assertEquals("Request Expired", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION));
// check whether "aud" claim exists
requestObject = createValidRequestObjectForSecureRequestObjectExecutor(clientId);
requestObject.audience((String)null);
registerRequestObject(requestObject, clientId, Algorithm.ES256, false);
oauth.openLoginForm();
assertEquals(SecureRequestObjectExecutor.INVALID_REQUEST_OBJECT, oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
assertEquals("Missing parameter : aud", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION));
// check whether "aud" claim points to this keycloak as authz server
requestObject = createValidRequestObjectForSecureRequestObjectExecutor(clientId);
requestObject.audience(suiteContext.getAuthServerInfo().getContextRoot().toString());
registerRequestObject(requestObject, clientId, Algorithm.ES256, true);
oauth.openLoginForm();
assertEquals(SecureRequestObjectExecutor.INVALID_REQUEST_OBJECT, oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
assertEquals("Invalid parameter : aud", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION));
// confirm whether all parameters in query string are included in the request object, and have the same values
// argument "request" are parameters overridden by parameters in request object
requestObject = createValidRequestObjectForSecureRequestObjectExecutor(clientId);
requestObject.setState("notmatchstate");
registerRequestObject(requestObject, clientId, Algorithm.ES256, false);
oauth.openLoginForm();
assertEquals(OAuthErrorException.INVALID_REQUEST, oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
assertEquals("Invalid parameter", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION));
// valid request object
requestObject = createValidRequestObjectForSecureRequestObjectExecutor(clientId);
registerRequestObject(requestObject, clientId, Algorithm.ES256, true);
successfulLoginAndLogout(clientId, clientSecret);
} finally {
deleteClientByAdmin(cid);
}
}
private AuthorizationEndpointRequestObject createValidRequestObjectForSecureRequestObjectExecutor(String clientId) throws URISyntaxException {
AuthorizationEndpointRequestObject requestObject = new AuthorizationEndpointRequestObject();
requestObject.id(KeycloakModelUtils.generateId());
requestObject.iat(Long.valueOf(Time.currentTime()));
requestObject.exp(requestObject.getIat() + Long.valueOf(300));
requestObject.nbf(Long.valueOf(0));
requestObject.setClientId(clientId);
requestObject.setResponseType("code");
requestObject.setRedirectUriParam(oauth.getRedirectUri());
requestObject.setScope("openid");
String scope = KeycloakModelUtils.generateId();
oauth.stateParamHardcoded(scope);
requestObject.setState(scope);
requestObject.setMax_age(Integer.valueOf(600));
requestObject.setOtherClaims("custom_claim_ein", "rot");
requestObject.audience(Urls.realmIssuer(new URI(suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth"), REALM_NAME), "https://example.com");
return requestObject;
}
private void registerRequestObject(AuthorizationEndpointRequestObject requestObject, String clientId, Algorithm sigAlg, boolean isUseRequestUri) throws URISyntaxException, IOException {
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
// Set required signature for request_uri
// use and set jwks_url
ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm(REALM_NAME), clientId);
ClientRepresentation clientRep = clientResource.toRepresentation();
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectSignatureAlg(sigAlg);
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setUseJwksUrl(true);
String jwksUrl = TestApplicationResourceUrls.clientJwksUri();
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setJwksUrl(jwksUrl);
clientResource.update(clientRep);
oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
// generate and register client keypair
oidcClientEndpointsResource.generateKeys(sigAlg.name());
// register request object
byte[] contentBytes = JsonSerialization.writeValueAsBytes(requestObject);
String encodedRequestObject = Base64Url.encode(contentBytes);
oidcClientEndpointsResource.registerOIDCRequest(encodedRequestObject, sigAlg.name());
if (isUseRequestUri) {
oauth.request(null);
oauth.requestUri(TestApplicationResourceUrls.clientRequestUri());
} else {
oauth.requestUri(null);
oauth.request(oidcClientEndpointsResource.getOIDCRequest());
}
}
private void setupPolicyAcceptableAuthType(String policyName) {
createPolicy(policyName, DefaultClientPolicyProviderFactory.PROVIDER_ID, null, null, null);