diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
index e67a5a9926..2f82266312 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
@@ -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());
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java
index 66441b066e..7f5befabba 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java
@@ -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);
}
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java
index 8b0ce56bdd..cdb0d11f1c 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java
@@ -57,9 +57,10 @@ class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser {
} else {
this.requestParams = session.tokens().decodeClientJWT(requestObject, client, JsonNode.class);
if (this.requestParams == null) {
- throw new RuntimeException("Failed to verify signature on 'request' object");
+ throw new RuntimeException("Failed to verify signature on 'request' object");
}
}
+ session.setAttribute(AuthzEndpointRequestParser.AUTHZ_REQUEST_OBJECT, requestParams);
}
@Override
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java
index 92b61e7c08..0ae53658e5 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java
@@ -30,7 +30,7 @@ import java.util.Set;
/**
* @author Marek Posolda
*/
-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 KNOWN_REQ_PARAMS = new HashSet<>();
+ public static final Set KNOWN_REQ_PARAMS = new HashSet<>();
static {
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.CLIENT_ID_PARAM);
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/AuthorizationRequestContext.java b/services/src/main/java/org/keycloak/services/clientpolicy/AuthorizationRequestContext.java
index 8986515857..75e46d3647 100644
--- a/services/src/main/java/org/keycloak/services/clientpolicy/AuthorizationRequestContext.java
+++ b/services/src/main/java/org/keycloak/services/clientpolicy/AuthorizationRequestContext.java
@@ -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 requestParameters;
public AuthorizationRequestContext(OIDCResponseType parsedResponseType,
AuthorizationEndpointRequest request,
- String redirectUri) {
+ String redirectUri,
+ MultivaluedMap 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 getRequestParameters() {
+ return requestParameters;
+ }
}
diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/executor/SecureRequestObjectExecutor.java b/services/src/main/java/org/keycloak/services/clientpolicy/executor/SecureRequestObjectExecutor.java
new file mode 100644
index 0000000000..e6360d4530
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/clientpolicy/executor/SecureRequestObjectExecutor.java
@@ -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 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 aud = new ArrayList();
+ 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();
+ }
+
+}
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/executor/SecureRequestObjectExecutorFactory.java b/services/src/main/java/org/keycloak/services/clientpolicy/executor/SecureRequestObjectExecutorFactory.java
new file mode 100644
index 0000000000..616ef46aaf
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/clientpolicy/executor/SecureRequestObjectExecutorFactory.java
@@ -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 getConfigProperties() {
+ return Collections.emptyList();
+ }
+
+}
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProviderFactory
index c2e6d38b25..58147e7d4b 100644
--- a/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProviderFactory
+++ b/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProviderFactory
@@ -1 +1,2 @@
-org.keycloak.services.clientpolicy.executor.SecureResponseTypeExecutorFactory
\ No newline at end of file
+org.keycloak.services.clientpolicy.executor.SecureResponseTypeExecutorFactory
+org.keycloak.services.clientpolicy.executor.SecureRequestObjectExecutorFactory
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java
index 1049fb6597..b3cd9888fd 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java
@@ -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 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;
+ }
+
+ }
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java
index abca35c1a9..e0f4dcbfa2 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java
@@ -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)
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPolicyBasicsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPolicyBasicsTest.java
index 90b8a47979..1a67800340 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPolicyBasicsTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPolicyBasicsTest.java
@@ -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 {
@@ -107,8 +119,9 @@ public class ClientPolicyBasicsTest extends AbstractKeycloakTest {
static final String CLIENTROLES_CONDITION_BETA_NAME = "ClientRolesCondition-beta";
static final String SECURERESPONSETYPE_EXECUTOR_NAME = "SecureResponseTypeExecutor";
-
-
+
+ static final String SECUREREQUESTOBJECT_EXECUTOR_NAME = "SecureRequestObjectExecutor";
+
ClientRegistration reg;
@Rule
@@ -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);