[KEYCLOAK-18741] - Review error messages when validating PAR requests

This commit is contained in:
Pedro Igor 2021-07-14 11:49:51 -03:00
parent 7f34af4016
commit 54a0e84070
8 changed files with 231 additions and 44 deletions

View file

@ -30,6 +30,8 @@ public class OAuthErrorException extends Exception {
public static final String UNSUPPORTED_RESPONSE_TYPE = "unsupported_response_type"; public static final String UNSUPPORTED_RESPONSE_TYPE = "unsupported_response_type";
public static final String SERVER_ERROR = "server_error"; public static final String SERVER_ERROR = "server_error";
public static final String TEMPORARILY_UNAVAILABLE = "temporarily_unavailable"; public static final String TEMPORARILY_UNAVAILABLE = "temporarily_unavailable";
public static final String INVALID_REQUEST_URI = "invalid_request_uri";
public static final String INVALID_REQUEST_OBJECT = "invalid_request_object";
// OpenID Connect 1 // OpenID Connect 1
public static final String INTERACTION_REQUIRED = "interaction_required"; public static final String INTERACTION_REQUIRED = "interaction_required";

View file

@ -31,6 +31,7 @@ import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
/** /**
* Parse the parameters from OIDC "request" object * Parse the parameters from OIDC "request" object
@ -58,6 +59,10 @@ public class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser
throw new RuntimeException("The client_id in the request object is not the same as the authorizing client"); throw new RuntimeException("The client_id in the request object is not the same as the authorizing client");
} }
if (requestParams.has(OIDCLoginProtocol.REQUEST_URI_PARAM)) {
throw new RuntimeException("The request_uri claim should not be set in the request object");
}
session.setAttribute(AuthzEndpointRequestParser.AUTHZ_REQUEST_OBJECT, requestParams); session.setAttribute(AuthzEndpointRequestParser.AUTHZ_REQUEST_OBJECT, requestParams);
} }

View file

@ -96,7 +96,7 @@ public class ParEndpoint extends AbstractParEndpoint {
try { try {
authorizationRequest = AuthorizationEndpointRequestParserProcessor.parseRequest(event, session, client, httpRequest.getDecodedFormParameters()); authorizationRequest = AuthorizationEndpointRequestParserProcessor.parseRequest(event, session, client, httpRequest.getDecodedFormParameters());
} catch (Exception e) { } catch (Exception e) {
throw throwErrorResponseException(OAuthErrorException.INVALID_REQUEST, e.getMessage(), Response.Status.BAD_REQUEST); throw throwErrorResponseException(OAuthErrorException.INVALID_REQUEST_OBJECT, e.getMessage(), Response.Status.BAD_REQUEST);
} }
AuthorizationEndpointChecker checker = new AuthorizationEndpointChecker() AuthorizationEndpointChecker checker = new AuthorizationEndpointChecker()

View file

@ -19,6 +19,7 @@ package org.keycloak.services.clientpolicy.context;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest; import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
import org.keycloak.protocol.oidc.utils.OIDCResponseType; import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.services.clientpolicy.ClientPolicyContext; import org.keycloak.services.clientpolicy.ClientPolicyContext;
@ -64,4 +65,8 @@ public class AuthorizationRequestContext implements ClientPolicyContext {
public MultivaluedMap<String, String> getRequestParameters() { public MultivaluedMap<String, String> getRequestParameters() {
return requestParameters; return requestParameters;
} }
public boolean isParRequest() {
return requestParameters.containsKey(OIDCLoginProtocol.REQUEST_URI_PARAM);
}
} }

View file

@ -17,6 +17,8 @@
package org.keycloak.services.clientpolicy.executor; package org.keycloak.services.clientpolicy.executor;
import static org.keycloak.OAuthErrorException.INVALID_REQUEST_OBJECT;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -28,9 +30,7 @@ import org.keycloak.OAuthErrorException;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; 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.endpoints.request.AuthzEndpointRequestParser;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.idm.ClientPolicyExecutorConfigurationRepresentation; import org.keycloak.representations.idm.ClientPolicyExecutorConfigurationRepresentation;
import org.keycloak.services.Urls; import org.keycloak.services.Urls;
import org.keycloak.services.clientpolicy.ClientPolicyContext; import org.keycloak.services.clientpolicy.ClientPolicyContext;
@ -47,7 +47,6 @@ public class SecureRequestObjectExecutor implements ClientPolicyExecutorProvider
private static final Logger logger = Logger.getLogger(SecureRequestObjectExecutor.class); private static final Logger logger = Logger.getLogger(SecureRequestObjectExecutor.class);
public static final String INVALID_REQUEST_OBJECT = "invalid_request_object";
public static final Integer DEFAULT_AVAILABLE_PERIOD = Integer.valueOf(3600); // (sec) from FAPI 1.0 Advanced requirement public static final Integer DEFAULT_AVAILABLE_PERIOD = Integer.valueOf(3600); // (sec) from FAPI 1.0 Advanced requirement
private final KeycloakSession session; private final KeycloakSession session;
@ -126,26 +125,21 @@ public class SecureRequestObjectExecutor implements ClientPolicyExecutorProvider
switch (context.getEvent()) { switch (context.getEvent()) {
case AUTHORIZATION_REQUEST: case AUTHORIZATION_REQUEST:
AuthorizationRequestContext authorizationRequestContext = (AuthorizationRequestContext)context; AuthorizationRequestContext authorizationRequestContext = (AuthorizationRequestContext)context;
executeOnAuthorizationRequest(authorizationRequestContext.getparsedResponseType(), executeOnAuthorizationRequest(authorizationRequestContext);
authorizationRequestContext.getAuthorizationEndpointRequest(),
authorizationRequestContext.getRedirectUri(),
authorizationRequestContext.getRequestParameters());
break; break;
default: default:
return; return;
} }
} }
private void executeOnAuthorizationRequest( private void executeOnAuthorizationRequest(AuthorizationRequestContext context) throws ClientPolicyException {
OIDCResponseType parsedResponseType,
AuthorizationEndpointRequest request,
String redirectUri,
MultivaluedMap<String, String> params) throws ClientPolicyException {
logger.trace("Authz Endpoint - authz request"); logger.trace("Authz Endpoint - authz request");
MultivaluedMap<String, String> params = context.getRequestParameters();
if (params == null) { if (params == null) {
logger.trace("request parameter not exist."); logger.trace("request parameter not exist.");
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Missing parameters"); throwClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Missing parameters", context);
} }
String requestParam = params.getFirst(OIDCLoginProtocol.REQUEST_PARAM); String requestParam = params.getFirst(OIDCLoginProtocol.REQUEST_PARAM);
@ -154,7 +148,8 @@ public class SecureRequestObjectExecutor implements ClientPolicyExecutorProvider
// check whether whether request object exists // check whether whether request object exists
if (requestParam == null && requestUriParam == null) { if (requestParam == null && requestUriParam == null) {
logger.trace("request object not exist."); logger.trace("request object not exist.");
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Missing parameter: 'request' or 'request_uri'"); throwClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Missing parameter: 'request' or 'request_uri'",
context);
} }
JsonNode requestObject = (JsonNode)session.getAttribute(AuthzEndpointRequestParser.AUTHZ_REQUEST_OBJECT); JsonNode requestObject = (JsonNode)session.getAttribute(AuthzEndpointRequestParser.AUTHZ_REQUEST_OBJECT);
@ -162,19 +157,21 @@ public class SecureRequestObjectExecutor implements ClientPolicyExecutorProvider
// check whether request object exists // check whether request object exists
if (requestObject == null || requestObject.isEmpty()) { if (requestObject == null || requestObject.isEmpty()) {
logger.trace("request object not exist."); logger.trace("request object not exist.");
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Invalid parameter: : 'request' or 'request_uri'"); throwClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Invalid parameter: : 'request' or 'request_uri'",
context);
} }
// check whether scope exists in both query parameter and request object // check whether scope exists in both query parameter and request object
if (params.getFirst(OIDCLoginProtocol.SCOPE_PARAM) == null && requestObject.get(OIDCLoginProtocol.SCOPE_PARAM) == null) { if (params.getFirst(OIDCLoginProtocol.SCOPE_PARAM) == null && requestObject.get(OIDCLoginProtocol.SCOPE_PARAM) == null) {
logger.trace("scope object not exist."); logger.trace("scope object not exist.");
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Parameter 'scope' missing in the request parameters or in 'request' object"); throwClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Parameter 'scope' missing in the request parameters or in 'request' object",
context);
} }
// check whether "exp" claim exists // check whether "exp" claim exists
if (requestObject.get("exp") == null) { if (requestObject.get("exp") == null) {
logger.trace("exp claim not incuded."); logger.trace("exp claim not incuded.");
throw new ClientPolicyException(INVALID_REQUEST_OBJECT, "Missing parameter in the 'request' object: exp"); throwClientPolicyException(INVALID_REQUEST_OBJECT, "Missing parameter in the 'request' object: exp", context);
} }
// check whether request object not expired // check whether request object not expired
@ -190,21 +187,21 @@ public class SecureRequestObjectExecutor implements ClientPolicyExecutorProvider
// check whether "nbf" claim exists // check whether "nbf" claim exists
if (requestObject.get("nbf") == null) { if (requestObject.get("nbf") == null) {
logger.trace("nbf claim not incuded."); logger.trace("nbf claim not incuded.");
throw new ClientPolicyException(INVALID_REQUEST_OBJECT, "Missing parameter in the 'request' object: nbf"); throwClientPolicyException(INVALID_REQUEST_OBJECT, "Missing parameter in the 'request' object: nbf", context);
} }
// check whether request object not yet being processed // check whether request object not yet being processed
long nbf = requestObject.get("nbf").asLong(); long nbf = requestObject.get("nbf").asLong();
if (Time.currentTime() < nbf) { // TODO: Time.currentTime() is int while nbf is long... if (Time.currentTime() < nbf) { // TODO: Time.currentTime() is int while nbf is long...
logger.trace("request object not yet being processed."); logger.trace("request object not yet being processed.");
throw new ClientPolicyException(INVALID_REQUEST_OBJECT, "Request not yet being processed"); throwClientPolicyException(INVALID_REQUEST_OBJECT, "Request not yet being processed", context);
} }
// check whether request object's available period is short // check whether request object's available period is short
int availablePeriod = Optional.ofNullable(configuration.getAvailablePeriod()).orElse(DEFAULT_AVAILABLE_PERIOD).intValue(); int availablePeriod = Optional.ofNullable(configuration.getAvailablePeriod()).orElse(DEFAULT_AVAILABLE_PERIOD).intValue();
if (exp - nbf > availablePeriod) { if (exp - nbf > availablePeriod) {
logger.trace("request object's available period is long."); logger.trace("request object's available period is long.");
throw new ClientPolicyException(INVALID_REQUEST_OBJECT, "Request's available period is long"); throwClientPolicyException(INVALID_REQUEST_OBJECT, "Request's available period is long", context);
} }
} }
@ -213,7 +210,7 @@ public class SecureRequestObjectExecutor implements ClientPolicyExecutorProvider
JsonNode audience = requestObject.get("aud"); JsonNode audience = requestObject.get("aud");
if (audience == null) { if (audience == null) {
logger.trace("aud claim not incuded."); logger.trace("aud claim not incuded.");
throw new ClientPolicyException(INVALID_REQUEST_OBJECT, "Missing parameter in the 'request' object: aud"); throwClientPolicyException(INVALID_REQUEST_OBJECT, "Missing parameter in the 'request' object: aud", context);
} }
if (audience.isArray()) { if (audience.isArray()) {
for (JsonNode node : audience) aud.add(node.asText()); for (JsonNode node : audience) aud.add(node.asText());
@ -222,14 +219,14 @@ public class SecureRequestObjectExecutor implements ClientPolicyExecutorProvider
} }
if (aud.isEmpty()) { if (aud.isEmpty()) {
logger.trace("aud claim not incuded."); logger.trace("aud claim not incuded.");
throw new ClientPolicyException(INVALID_REQUEST_OBJECT, "Missing parameter value in the 'request' object: aud"); throwClientPolicyException(INVALID_REQUEST_OBJECT, "Missing parameter value in the 'request' object: aud", context);
} }
// check whether "aud" claim points to this keycloak as authz server // check whether "aud" claim points to this keycloak as authz server
String iss = Urls.realmIssuer(session.getContext().getUri().getBaseUri(), session.getContext().getRealm().getName()); String iss = Urls.realmIssuer(session.getContext().getUri().getBaseUri(), session.getContext().getRealm().getName());
if (!aud.contains(iss)) { if (!aud.contains(iss)) {
logger.trace("aud not points to the intended realm."); logger.trace("aud not points to the intended realm.");
throw new ClientPolicyException(INVALID_REQUEST_OBJECT, "Invalid parameter in the 'request' object: aud"); throwClientPolicyException(INVALID_REQUEST_OBJECT, "Invalid parameter in the 'request' object: aud", context);
} }
// confirm whether all parameters in query string are included in the request object, and have the same values // confirm whether all parameters in query string are included in the request object, and have the same values
@ -240,7 +237,8 @@ public class SecureRequestObjectExecutor implements ClientPolicyExecutorProvider
.findFirst(); .findFirst();
if (incorrectParam.isPresent()) { if (incorrectParam.isPresent()) {
logger.warnf("Parameter '%s' does not have same value in 'request' object and in request parameters", incorrectParam.get()); logger.warnf("Parameter '%s' does not have same value in 'request' object and in request parameters", incorrectParam.get());
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Invalid parameter. Parameters in 'request' object not matching with request parameters"); throwClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Invalid parameter. Parameters in 'request' object not matching with request parameters",
context);
} }
Boolean encryptionRequired = Optional.ofNullable(configuration.isEncryptionRequired()).orElse(Boolean.FALSE); Boolean encryptionRequired = Optional.ofNullable(configuration.isEncryptionRequired()).orElse(Boolean.FALSE);
@ -258,4 +256,12 @@ public class SecureRequestObjectExecutor implements ClientPolicyExecutorProvider
return false; return false;
} }
private void throwClientPolicyException(String error, String message,
AuthorizationRequestContext context) throws ClientPolicyException {
if (context.isParRequest() && INVALID_REQUEST_OBJECT.equals(error)) {
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST_URI, message);
}
throw new ClientPolicyException(error, message);
}
} }

View file

@ -35,6 +35,8 @@ import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
import org.keycloak.authentication.authenticators.client.JWTClientSecretAuthenticator; import org.keycloak.authentication.authenticators.client.JWTClientSecretAuthenticator;
import org.keycloak.authentication.authenticators.client.X509ClientAuthenticator; import org.keycloak.authentication.authenticators.client.X509ClientAuthenticator;
import org.keycloak.client.registration.ClientRegistrationException; import org.keycloak.client.registration.ClientRegistrationException;
import org.keycloak.common.Profile;
import org.keycloak.common.util.Base64Url;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
@ -85,6 +87,7 @@ import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls; 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.rest.resource.TestingOIDCEndpointsApplicationResource.AuthorizationEndpointRequestObject;
import org.keycloak.testsuite.services.clientpolicy.condition.TestRaiseExeptionConditionFactory; import org.keycloak.testsuite.services.clientpolicy.condition.TestRaiseExeptionConditionFactory;
import org.keycloak.testsuite.services.clientpolicy.executor.TestRaiseExeptionExecutorFactory; import org.keycloak.testsuite.services.clientpolicy.executor.TestRaiseExeptionExecutorFactory;
@ -108,16 +111,11 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import static org.keycloak.protocol.oidc.grants.ciba.channel.AuthenticationChannelResponse.Status.SUCCEED;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson; import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername; import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername;
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.QUARKUS;
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.ClientPoliciesBuilder; import static org.keycloak.testsuite.util.ClientPoliciesUtil.ClientPoliciesBuilder;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.ClientPolicyBuilder; import static org.keycloak.testsuite.util.ClientPoliciesUtil.ClientPolicyBuilder;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.ClientProfileBuilder; import static org.keycloak.testsuite.util.ClientPoliciesUtil.ClientProfileBuilder;
@ -1237,7 +1235,7 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
requestObject.exp(null); requestObject.exp(null);
registerRequestObject(requestObject, clientId, Algorithm.ES256, false); registerRequestObject(requestObject, clientId, Algorithm.ES256, false);
oauth.openLoginForm(); oauth.openLoginForm();
assertEquals(SecureRequestObjectExecutor.INVALID_REQUEST_OBJECT, oauth.getCurrentQuery().get(OAuth2Constants.ERROR)); assertEquals(OAuthErrorException.INVALID_REQUEST_OBJECT, oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
assertEquals("Missing parameter in the 'request' object: exp", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION)); assertEquals("Missing parameter in the 'request' object: exp", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION));
// check whether request object not expired // check whether request object not expired
@ -1245,7 +1243,7 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
requestObject.exp(Long.valueOf(0)); requestObject.exp(Long.valueOf(0));
registerRequestObject(requestObject, clientId, Algorithm.ES256, true); registerRequestObject(requestObject, clientId, Algorithm.ES256, true);
oauth.openLoginForm(); oauth.openLoginForm();
assertEquals(SecureRequestObjectExecutor.INVALID_REQUEST_OBJECT, oauth.getCurrentQuery().get(OAuth2Constants.ERROR)); assertEquals(OAuthErrorException.INVALID_REQUEST_OBJECT, oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
assertEquals("Request Expired", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION)); assertEquals("Request Expired", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION));
// check whether "nbf" claim exists // check whether "nbf" claim exists
@ -1253,7 +1251,7 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
requestObject.nbf(null); requestObject.nbf(null);
registerRequestObject(requestObject, clientId, Algorithm.ES256, false); registerRequestObject(requestObject, clientId, Algorithm.ES256, false);
oauth.openLoginForm(); oauth.openLoginForm();
assertEquals(SecureRequestObjectExecutor.INVALID_REQUEST_OBJECT, oauth.getCurrentQuery().get(OAuth2Constants.ERROR)); assertEquals(OAuthErrorException.INVALID_REQUEST_OBJECT, oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
assertEquals("Missing parameter in the 'request' object: nbf", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION)); assertEquals("Missing parameter in the 'request' object: nbf", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION));
// check whether request object not yet being processed // check whether request object not yet being processed
@ -1261,7 +1259,7 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
requestObject.nbf(requestObject.getNbf() + 600); requestObject.nbf(requestObject.getNbf() + 600);
registerRequestObject(requestObject, clientId, Algorithm.ES256, false); registerRequestObject(requestObject, clientId, Algorithm.ES256, false);
oauth.openLoginForm(); oauth.openLoginForm();
assertEquals(SecureRequestObjectExecutor.INVALID_REQUEST_OBJECT, oauth.getCurrentQuery().get(OAuth2Constants.ERROR)); assertEquals(OAuthErrorException.INVALID_REQUEST_OBJECT, oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
assertEquals("Request not yet being processed", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION)); assertEquals("Request not yet being processed", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION));
// check whether request object's available period is short // check whether request object's available period is short
@ -1269,7 +1267,7 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
requestObject.exp(requestObject.getNbf() + availablePeriod.intValue() + 1); requestObject.exp(requestObject.getNbf() + availablePeriod.intValue() + 1);
registerRequestObject(requestObject, clientId, Algorithm.ES256, false); registerRequestObject(requestObject, clientId, Algorithm.ES256, false);
oauth.openLoginForm(); oauth.openLoginForm();
assertEquals(SecureRequestObjectExecutor.INVALID_REQUEST_OBJECT, oauth.getCurrentQuery().get(OAuth2Constants.ERROR)); assertEquals(OAuthErrorException.INVALID_REQUEST_OBJECT, oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
assertEquals("Request's available period is long", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION)); assertEquals("Request's available period is long", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION));
// check whether "aud" claim exists // check whether "aud" claim exists
@ -1277,7 +1275,7 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
requestObject.audience((String)null); requestObject.audience((String)null);
registerRequestObject(requestObject, clientId, Algorithm.ES256, false); registerRequestObject(requestObject, clientId, Algorithm.ES256, false);
oauth.openLoginForm(); oauth.openLoginForm();
assertEquals(SecureRequestObjectExecutor.INVALID_REQUEST_OBJECT, oauth.getCurrentQuery().get(OAuth2Constants.ERROR)); assertEquals(OAuthErrorException.INVALID_REQUEST_OBJECT, oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
assertEquals("Missing parameter in the 'request' object: aud", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION)); assertEquals("Missing parameter in the 'request' object: aud", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION));
// check whether "aud" claim points to this keycloak as authz server // check whether "aud" claim points to this keycloak as authz server
@ -1285,7 +1283,7 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
requestObject.audience(suiteContext.getAuthServerInfo().getContextRoot().toString()); requestObject.audience(suiteContext.getAuthServerInfo().getContextRoot().toString());
registerRequestObject(requestObject, clientId, Algorithm.ES256, true); registerRequestObject(requestObject, clientId, Algorithm.ES256, true);
oauth.openLoginForm(); oauth.openLoginForm();
assertEquals(SecureRequestObjectExecutor.INVALID_REQUEST_OBJECT, oauth.getCurrentQuery().get(OAuth2Constants.ERROR)); assertEquals(OAuthErrorException.INVALID_REQUEST_URI, oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
assertEquals("Invalid parameter in the 'request' object: aud", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION)); assertEquals("Invalid parameter in the 'request' object: 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 // confirm whether all parameters in query string are included in the request object, and have the same values
@ -1316,7 +1314,7 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
requestObject.nbf(null); requestObject.nbf(null);
registerRequestObject(requestObject, clientId, Algorithm.ES256, false); registerRequestObject(requestObject, clientId, Algorithm.ES256, false);
oauth.openLoginForm(); oauth.openLoginForm();
assertEquals(SecureRequestObjectExecutor.INVALID_REQUEST_OBJECT, oauth.getCurrentQuery().get(OAuth2Constants.ERROR)); assertEquals(OAuthErrorException.INVALID_REQUEST_OBJECT, oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
assertEquals("Missing parameter in the 'request' object: nbf", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION)); assertEquals("Missing parameter in the 'request' object: nbf", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION));
// check whether request object not yet being processed // check whether request object not yet being processed
@ -1324,7 +1322,7 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
requestObject.nbf(requestObject.getNbf() + 600); requestObject.nbf(requestObject.getNbf() + 600);
registerRequestObject(requestObject, clientId, Algorithm.ES256, false); registerRequestObject(requestObject, clientId, Algorithm.ES256, false);
oauth.openLoginForm(); oauth.openLoginForm();
assertEquals(SecureRequestObjectExecutor.INVALID_REQUEST_OBJECT, oauth.getCurrentQuery().get(OAuth2Constants.ERROR)); assertEquals(OAuthErrorException.INVALID_REQUEST_OBJECT, oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
assertEquals("Request not yet being processed", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION)); assertEquals("Request not yet being processed", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION));
// check whether request object's available period is short // check whether request object's available period is short
@ -1332,7 +1330,7 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
requestObject.exp(requestObject.getNbf() + SecureRequestObjectExecutor.DEFAULT_AVAILABLE_PERIOD + 1); requestObject.exp(requestObject.getNbf() + SecureRequestObjectExecutor.DEFAULT_AVAILABLE_PERIOD + 1);
registerRequestObject(requestObject, clientId, Algorithm.ES256, false); registerRequestObject(requestObject, clientId, Algorithm.ES256, false);
oauth.openLoginForm(); oauth.openLoginForm();
assertEquals(SecureRequestObjectExecutor.INVALID_REQUEST_OBJECT, oauth.getCurrentQuery().get(OAuth2Constants.ERROR)); assertEquals(OAuthErrorException.INVALID_REQUEST_OBJECT, oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
assertEquals("Request's available period is long", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION)); assertEquals("Request's available period is long", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION));
// update profile : not check "nbf" // update profile : not check "nbf"
@ -1373,10 +1371,118 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
requestObject = createValidRequestObjectForSecureRequestObjectExecutor(clientId); requestObject = createValidRequestObjectForSecureRequestObjectExecutor(clientId);
registerRequestObject(requestObject, clientId, Algorithm.ES256, false); registerRequestObject(requestObject, clientId, Algorithm.ES256, false);
oauth.openLoginForm(); oauth.openLoginForm();
assertEquals(SecureRequestObjectExecutor.INVALID_REQUEST_OBJECT, oauth.getCurrentQuery().get(OAuth2Constants.ERROR)); assertEquals(OAuthErrorException.INVALID_REQUEST_OBJECT, oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
assertEquals("Request object not encrypted", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION)); assertEquals("Request object not encrypted", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION));
} }
@Test
public void testParSecureRequestObjectExecutor() throws Exception {
Integer availablePeriod = Integer.valueOf(SecureRequestObjectExecutor.DEFAULT_AVAILABLE_PERIOD + 400);
// register profiles
String json = (new ClientProfilesBuilder()).addProfile(
(new ClientProfileBuilder()).createProfile(PROFILE_NAME, "Prvy Profil")
.addExecutor(SecureRequestObjectExecutorFactory.PROVIDER_ID,
createSecureRequestObjectExecutorConfig(availablePeriod, true))
.toRepresentation()
).toString();
updateProfiles(json);
// register policies
json = (new ClientPoliciesBuilder()).addPolicy(
(new ClientPolicyBuilder()).createPolicy(POLICY_NAME, "Prva Politika", Boolean.TRUE)
.addCondition(ClientRolesConditionFactory.PROVIDER_ID,
createClientRolesConditionConfig(Arrays.asList(SAMPLE_CLIENT_ROLE)))
.addProfile(PROFILE_NAME)
.toRepresentation()
).toString();
updatePolicies(json);
String clientId = generateSuffixedName(CLIENT_NAME);
String clientSecret = "secret";
String cid = createClientByAdmin(clientId, (ClientRepresentation clientRep) -> {
clientRep.setSecret(clientSecret);
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestUris(Arrays.asList(TestApplicationResourceUrls.clientRequestUri()));
});
oauth.realm(REALM_NAME);
oauth.clientId(clientId);
adminClient.realm(REALM_NAME).clients().get(cid).roles().create(RoleBuilder.create().name(SAMPLE_CLIENT_ROLE).build());
AuthorizationEndpointRequestObject requestObject = createValidRequestObjectForSecureRequestObjectExecutor(clientId);
oauth.request(signRequestObject(requestObject));
OAuthClient.ParResponse pResp = oauth.doPushedAuthorizationRequest(clientId, clientSecret);
assertEquals(201, pResp.getStatusCode());
String requestUri = pResp.getRequestUri();
oauth.scope(null);
oauth.responseType(null);
oauth.request(null);
oauth.requestUri(requestUri);
OAuthClient.AuthorizationEndpointResponse loginResponse = oauth.doLogin(TEST_USER_NAME, TEST_USER_PASSWORD);
assertNotNull(loginResponse.getCode());
oauth.openLogout();
requestObject.exp(null);
oauth.requestUri(null);
oauth.request(signRequestObject(requestObject));
pResp = oauth.doPushedAuthorizationRequest(clientId, clientSecret);
requestUri = pResp.getRequestUri();
oauth.request(null);
oauth.requestUri(requestUri);
oauth.openLoginForm();
assertEquals(OAuthErrorException.INVALID_REQUEST_URI, oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
requestObject = createValidRequestObjectForSecureRequestObjectExecutor(clientId);
requestObject.nbf(null);
oauth.requestUri(null);
oauth.request(signRequestObject(requestObject));
pResp = oauth.doPushedAuthorizationRequest(clientId, clientSecret);
requestUri = pResp.getRequestUri();
oauth.request(null);
oauth.requestUri(requestUri);
oauth.openLoginForm();
assertEquals(OAuthErrorException.INVALID_REQUEST_URI, oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
requestObject = createValidRequestObjectForSecureRequestObjectExecutor(clientId);
requestObject.audience("https://www.other1.example.com/");
oauth.request(signRequestObject(requestObject));
oauth.requestUri(null);
pResp = oauth.doPushedAuthorizationRequest(clientId, clientSecret);
requestUri = pResp.getRequestUri();
oauth.request(null);
oauth.requestUri(requestUri);
oauth.openLoginForm();
assertEquals(OAuthErrorException.INVALID_REQUEST_URI, oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
requestObject = createValidRequestObjectForSecureRequestObjectExecutor(clientId);
requestObject.setOtherClaims(OIDCLoginProtocol.REQUEST_URI_PARAM, "foo");
oauth.request(signRequestObject(requestObject));
oauth.requestUri(null);
pResp = oauth.doPushedAuthorizationRequest(clientId, clientSecret);
assertEquals(OAuthErrorException.INVALID_REQUEST_OBJECT, pResp.getError());
}
private String signRequestObject(AuthorizationEndpointRequestObject requestObject) throws IOException {
byte[] contentBytes = JsonSerialization.writeValueAsBytes(requestObject);
String encodedRequestObject = Base64Url.encode(contentBytes);
TestOIDCEndpointsApplicationResource client = testingClient.testApp().oidcClientEndpoints();
// use and set jwks_url
ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm(oauth.getRealm()), oauth.getClientId());
ClientRepresentation clientRep = clientResource.toRepresentation();
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setUseJwksUrl(true);
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setJwksUrl(TestApplicationResourceUrls.clientJwksUri());
clientResource.update(clientRep);
client.generateKeys(org.keycloak.crypto.Algorithm.PS256);
client.registerOIDCRequest(encodedRequestObject, org.keycloak.crypto.Algorithm.PS256);
// do not send any other parameter but the request request parameter
String oidcRequest = client.getOIDCRequest();
return oidcRequest;
}
@Test @Test
public void testSecureSessionEnforceExecutor() throws Exception { public void testSecureSessionEnforceExecutor() throws Exception {
// register profiles // register profiles

View file

@ -514,7 +514,7 @@ public class FAPI1Test extends AbstractClientPoliciesTest {
requestObject.nbf(null); requestObject.nbf(null);
registerRequestObject(requestObject, "foo", org.keycloak.jose.jws.Algorithm.PS256, true); registerRequestObject(requestObject, "foo", org.keycloak.jose.jws.Algorithm.PS256, true);
oauth.openLoginForm(); oauth.openLoginForm();
assertRedirectedToClientWithError(SecureRequestObjectExecutor.INVALID_REQUEST_OBJECT,false, "Missing parameter in the 'request' object: nbf"); assertRedirectedToClientWithError(OAuthErrorException.INVALID_REQUEST_URI,false, "Missing parameter in the 'request' object: nbf");
// Create valid request object - more extensive testing of 'request' object is in ClientPoliciesTest.testSecureRequestObjectExecutor() // Create valid request object - more extensive testing of 'request' object is in ClientPoliciesTest.testSecureRequestObjectExecutor()
requestObject = createValidRequestObjectForSecureRequestObjectExecutor("foo"); requestObject = createValidRequestObjectForSecureRequestObjectExecutor("foo");

View file

@ -44,6 +44,7 @@ import org.keycloak.OAuthErrorException;
import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.common.util.Base64Url; import org.keycloak.common.util.Base64Url;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.crypto.Algorithm;
import org.keycloak.models.AdminRoles; import org.keycloak.models.AdminRoles;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.models.ParConfig; import org.keycloak.models.ParConfig;
@ -209,6 +210,68 @@ public class ParTest extends AbstractClientPoliciesTest {
} }
} }
@Test
public void testWrongSigningAlgorithmForRequestObject() throws Exception {
try {
// setup PAR realm settings
int requestUriLifespan = 45;
setParRealmSettings(requestUriLifespan);
// create client dynamically
String clientId = createClientDynamically(generateSuffixedName(CLIENT_NAME),
(OIDCClientRepresentation clientRep) -> {
clientRep.setRequirePushedAuthorizationRequests(Boolean.TRUE);
clientRep.setRedirectUris(new ArrayList<>(Arrays.asList(CLIENT_REDIRECT_URI)));
clientRep.setRequestObjectSigningAlg(Algorithm.PS256);
});
oauth.clientId(clientId);
OIDCClientRepresentation oidcCRep = getClientDynamically(clientId);
String clientSecret = oidcCRep.getClientSecret();
assertEquals(Boolean.TRUE, oidcCRep.getRequirePushedAuthorizationRequests());
assertTrue(oidcCRep.getRedirectUris().contains(CLIENT_REDIRECT_URI));
assertEquals(OIDCLoginProtocol.CLIENT_SECRET_BASIC, oidcCRep.getTokenEndpointAuthMethod());
TestingOIDCEndpointsApplicationResource.AuthorizationEndpointRequestObject requestObject = new TestingOIDCEndpointsApplicationResource.AuthorizationEndpointRequestObject();
requestObject.id(KeycloakModelUtils.generateId());
requestObject.iat(Long.valueOf(Time.currentTime()));
requestObject.exp(requestObject.getIat() + Long.valueOf(300));
requestObject.nbf(requestObject.getIat());
requestObject.setClientId(oauth.getClientId());
requestObject.setResponseType("code");
requestObject.setRedirectUriParam(CLIENT_REDIRECT_URI);
requestObject.setScope("openid");
requestObject.setNonce(KeycloakModelUtils.generateId());
byte[] contentBytes = JsonSerialization.writeValueAsBytes(requestObject);
String encodedRequestObject = Base64Url.encode(contentBytes);
TestOIDCEndpointsApplicationResource client = testingClient.testApp().oidcClientEndpoints();
// use and set jwks_url
ClientResource clientResource = ApiUtil
.findClientByClientId(adminClient.realm(oauth.getRealm()), oauth.getClientId());
ClientRepresentation clientRep = clientResource.toRepresentation();
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setUseJwksUrl(true);
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep)
.setJwksUrl(TestApplicationResourceUrls.clientJwksUri());
clientResource.update(clientRep);
client.generateKeys(org.keycloak.crypto.Algorithm.RS256);
client.registerOIDCRequest(encodedRequestObject, org.keycloak.crypto.Algorithm.RS256);
// do not send any other parameter but the request request parameter
oauth.request(client.getOIDCRequest());
oauth.responseType(null);
oauth.redirectUri(null);
oauth.scope(null);
ParResponse pResp = oauth.doPushedAuthorizationRequest(clientId, clientSecret);
assertEquals(400, pResp.getStatusCode());
assertEquals(OAuthErrorException.INVALID_REQUEST_OBJECT, pResp.getError());
} finally {
restoreParRealmSettings();
}
}
@Test @Test
public void testSuccessfulUsingRequestParameter() throws Exception { public void testSuccessfulUsingRequestParameter() throws Exception {
try { try {
@ -903,7 +966,7 @@ public class ParTest extends AbstractClientPoliciesTest {
oauth.redirectUri(CLIENT_REDIRECT_URI); oauth.redirectUri(CLIENT_REDIRECT_URI);
ParResponse pResp = oauth.doPushedAuthorizationRequest(clientId, clientSecret); ParResponse pResp = oauth.doPushedAuthorizationRequest(clientId, clientSecret);
assertEquals(400, pResp.getStatusCode()); assertEquals(400, pResp.getStatusCode());
assertEquals(OAuthErrorException.INVALID_REQUEST, pResp.getError()); assertEquals(OAuthErrorException.INVALID_REQUEST_OBJECT, pResp.getError());
} }
// PAR including invalid redirect_uri // PAR including invalid redirect_uri