KEYCLOAK-14204 FAPI-RW Client Policy - Executor : Enforce Request Object satisfying high security level
This commit is contained in:
parent
006b98ae13
commit
6596811d5d
11 changed files with 662 additions and 10 deletions
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -1 +1,2 @@
|
|||
org.keycloak.services.clientpolicy.executor.SecureResponseTypeExecutorFactory
|
||||
org.keycloak.services.clientpolicy.executor.SecureResponseTypeExecutorFactory
|
||||
org.keycloak.services.clientpolicy.executor.SecureRequestObjectExecutorFactory
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue