KEYCLOAK-14208 Default client profiles for FAPI

This commit is contained in:
mposolda 2021-05-13 16:11:52 +02:00 committed by Marek Posolda
parent ab13e3e4fe
commit 73a38997d8
11 changed files with 964 additions and 50 deletions

View file

@ -35,7 +35,7 @@ public class PKCEEnforcerExecutorFactory implements ClientPolicyExecutorProvider
public static final String AUTO_CONFIGURE = "auto-configure";
private static final ProviderConfigProperty AUTO_CONFIGURE_PROPERTY = new ProviderConfigProperty(
AUTO_CONFIGURE, "Auto-configure", "If On, then the during client creation or update, the configuration of the client will be auto-configured to enforce usage of PKCE", ProviderConfigProperty.BOOLEAN_TYPE, false);
AUTO_CONFIGURE, "Auto-configure", "If On, then the during client creation or update, the configuration of the client will be auto-configured to enforce usage of PKCE with secure algorithm S256", ProviderConfigProperty.BOOLEAN_TYPE, false);
@Override
public ClientPolicyExecutorProvider create(KeycloakSession session) {
@ -61,7 +61,7 @@ public class PKCEEnforcerExecutorFactory implements ClientPolicyExecutorProvider
@Override
public String getHelpText() {
return "It makes the client enforce Proof Key for Code Exchange operation.";
return "It makes the client enforce Proof Key for Code Exchange operation with secure algorithm like S256.";
}
@Override

View file

@ -140,7 +140,7 @@ public class SecureRequestObjectExecutor implements ClientPolicyExecutorProvider
// check whether whether request object exists
if (requestParam == null && requestUriParam == null) {
logger.trace("request object not exist.");
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Invalid parameter");
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Missing parameter: 'request' or 'request_uri'");
}
JsonNode requestObject = (JsonNode)session.getAttribute(AuthzEndpointRequestParser.AUTHZ_REQUEST_OBJECT);
@ -148,19 +148,19 @@ public class SecureRequestObjectExecutor implements ClientPolicyExecutorProvider
// check whether request object exists
if (requestObject == null || requestObject.isEmpty()) {
logger.trace("request object not exist.");
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Invalid parameter");
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Invalid parameter: : 'request' or 'request_uri'");
}
// check whether scope exists in both query parameter and request object
if (params.getFirst(OIDCLoginProtocol.SCOPE_PARAM) == null || requestObject.get(OIDCLoginProtocol.SCOPE_PARAM) == null) {
logger.trace("scope object not exist.");
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Missing parameter : scope");
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Parameter 'scope' missing in the request parameters or in 'request' object");
}
// check whether "exp" claim exists
if (requestObject.get("exp") == null) {
logger.trace("exp claim not incuded.");
throw new ClientPolicyException(INVALID_REQUEST_OBJECT, "Missing parameter : exp");
throw new ClientPolicyException(INVALID_REQUEST_OBJECT, "Missing parameter in the 'request' object: exp");
}
// check whether request object not expired
@ -176,7 +176,7 @@ public class SecureRequestObjectExecutor implements ClientPolicyExecutorProvider
// check whether "nbf" claim exists
if (requestObject.get("nbf") == null) {
logger.trace("nbf claim not incuded.");
throw new ClientPolicyException(INVALID_REQUEST_OBJECT, "Missing parameter : nbf");
throw new ClientPolicyException(INVALID_REQUEST_OBJECT, "Missing parameter in the 'request' object: nbf");
}
// check whether request object not yet being processed
@ -199,7 +199,7 @@ public class SecureRequestObjectExecutor implements ClientPolicyExecutorProvider
JsonNode audience = requestObject.get("aud");
if (audience == null) {
logger.trace("aud claim not incuded.");
throw new ClientPolicyException(INVALID_REQUEST_OBJECT, "Missing parameter : aud");
throw new ClientPolicyException(INVALID_REQUEST_OBJECT, "Missing parameter in the 'request' object: aud");
}
if (audience.isArray()) {
for (JsonNode node : audience) aud.add(node.asText());
@ -208,21 +208,25 @@ public class SecureRequestObjectExecutor implements ClientPolicyExecutorProvider
}
if (aud.isEmpty()) {
logger.trace("aud claim not incuded.");
throw new ClientPolicyException(INVALID_REQUEST_OBJECT, "Missing parameter : aud");
throw new ClientPolicyException(INVALID_REQUEST_OBJECT, "Missing parameter value in the 'request' object: 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)) {
logger.trace("aud not points to the intended realm.");
throw new ClientPolicyException(INVALID_REQUEST_OBJECT, "Invalid parameter : aud");
throw new ClientPolicyException(INVALID_REQUEST_OBJECT, "Invalid parameter in the 'request' object: 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))) {
logger.trace("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");
Optional<String> incorrectParam = AuthzEndpointRequestParser.KNOWN_REQ_PARAMS.stream()
.filter(param -> params.containsKey(param))
.filter(param -> !isSameParameterIncluded(param, params.getFirst(param), requestObject))
.findFirst();
if (incorrectParam.isPresent()) {
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");
}
logger.trace("Passed.");

View file

@ -33,7 +33,9 @@ public class SecureSigningAlgorithmForSignedJwtExecutorFactory implements Client
public static final String REQUIRE_CLIENT_ASSERTION = "require-client-assertion";
private static final ProviderConfigProperty REQUIRE_CLIENT_ASSERTION_PROPERTY = new ProviderConfigProperty(
REQUIRE_CLIENT_ASSERTION, "Require Client Assertion", "If this is ON, then parameter 'client_assertion' will be required and request will fail if it is not present. If false, then parameter 'client_assertion' is not required. When 'client_assertion' parameter is present, then the algorithm on the JWT from specified client assertion is always checked regardless of the value of this switch", ProviderConfigProperty.BOOLEAN_TYPE, false);
REQUIRE_CLIENT_ASSERTION, "Require Client Assertion", "If this is ON, then parameter 'client_assertion' will be required in the requests and request will fail if it is not present. " +
"If false, then parameter 'client_assertion' is not required in the requests, which is convenient for example for clients authenticating with MTLS. When 'client_assertion' parameter is present in the request, " +
"then the algorithm on the JWT from specified client assertion is always checked regardless of the value of this switch", ProviderConfigProperty.BOOLEAN_TYPE, false);
@Override
public ClientPolicyExecutorProvider create(KeycloakSession session) {

View file

@ -1,12 +1,101 @@
{
"profiles": [
{
"name": "global-default-profile",
"description": "The global default profile for enforcing basic security level to clients.",
"name": "fapi-1-baseline",
"description": "Client profile, which enforce clients to conform 'Financial-grade API Security Profile 1.0 - Part 1: Baseline' specification.",
"executors": [
{
"executor": "secure-session",
"configuration": {}
},
{
"executor": "pkce-enforcer",
"configuration": {
"auto-configure": true
}
},
{
"executor": "secure-client-authenticator",
"configuration": {
"allowed-client-authenticators": [
"client-jwt",
"client-secret-jwt",
"client-x509"
],
"default-client-authenticator": "client-jwt"
}
},
{
"executor": "secure-client-uris",
"configuration": {}
},
{
"executor": "consent-required",
"configuration": {}
}
]
},
{
"name": "fapi-1-advanced",
"description": "Client profile, which enforce clients to conform 'Financial-grade API Security Profile 1.0 - Part 2: Advanced' specification.",
"executors": [
{
"executor": "secure-session",
"configuration": {}
},
{
"executor": "confidential-client",
"configuration": {}
},
{
"executor": "secure-client-authenticator",
"configuration": {
"allowed-client-authenticators": [
"client-jwt",
"client-x509"
],
"default-client-authenticator": "client-jwt"
}
},
{
"executor": "secure-client-uris",
"configuration": {}
},
{
"executor": "secure-request-object",
"configuration": {
"available-period": "3600",
"verify-nbf": true
}
},
{
"executor": "secure-response-type",
"configuration": {
"auto-configure": true,
"allow-token-response-type": false
}
},
{
"executor": "secure-signature-algorithm",
"configuration": {
"default-algorithm": "PS256"
}
},
{
"executor": "secure-signature-algorithm-signed-jwt",
"configuration": {
"require-client-assertion": false
}
},
{
"executor": "consent-required",
"configuration": {}
},
{
"executor": "holder-of-key-enforcer",
"configuration": {
"auto-configure": true
}
}
]
}

View file

@ -446,7 +446,7 @@ public class TestingOIDCEndpointsApplicationResource {
return nonce;
}
public void getNonce(String nonce) {
public void setNonce(String nonce) {
this.nonce = nonce;
}

View file

@ -26,7 +26,6 @@ import static org.junit.Assert.fail;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.MessageDigest;
@ -124,6 +123,7 @@ import org.keycloak.services.clientpolicy.condition.ClientUpdaterSourceHostsCond
import org.keycloak.services.clientpolicy.condition.ClientUpdaterSourceHostsConditionFactory;
import org.keycloak.services.clientpolicy.condition.ClientUpdaterSourceRolesCondition;
import org.keycloak.services.clientpolicy.condition.ClientUpdaterSourceRolesConditionFactory;
import org.keycloak.services.clientpolicy.executor.ConsentRequiredExecutorFactory;
import org.keycloak.services.clientpolicy.executor.HolderOfKeyEnforcerExecutor;
import org.keycloak.services.clientpolicy.executor.HolderOfKeyEnforcerExecutorFactory;
import org.keycloak.services.clientpolicy.executor.PKCEEnforcerExecutor;
@ -172,6 +172,9 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest {
protected static final String PROFILE_NAME = "MyProfile";
protected static final String SAMPLE_CLIENT_ROLE = "sample-client-role";
protected static final String FAPI1_BASELINE_PROFILE_NAME = "fapi-1-baseline";
protected static final String FAPI1_ADVANCED_PROFILE_NAME = "fapi-1-advanced";
protected static final String ERR_MSG_MISSING_NONCE = "Missing parameter: nonce";
protected static final String ERR_MSG_MISSING_STATE = "Missing parameter: state";
protected static final String ERR_MSG_CLIENT_REG_FAIL = "Failed to send request";
@ -190,11 +193,14 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest {
@Before
public void before() throws Exception {
setInitialAccessTokenForDynamicClientRegistration();
}
protected void setInitialAccessTokenForDynamicClientRegistration() {
// get initial access token for Dynamic Client Registration with authentication
reg = ClientRegistration.create().url(suiteContext.getAuthServerInfo().getContextRoot() + "/auth", REALM_NAME).build();
ClientInitialAccessPresentation token = adminClient.realm(REALM_NAME).clientInitialAccess().create(new ClientInitialAccessCreatePresentation(0, 10));
reg.auth(Auth.token(token));
}
@After
@ -246,7 +252,6 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest {
createClientRolesConditionConfig(Arrays.asList(SAMPLE_CLIENT_ROLE)))
.addCondition(ClientScopesConditionFactory.PROVIDER_ID,
createClientScopesConditionConfig(ClientScopesConditionFactory.OPTIONAL, Arrays.asList(SAMPLE_CLIENT_ROLE)))
.addProfile("global-default-profile")
.addProfile("ordinal-test-profile")
.addProfile("lack-of-builtin-field-test-profile")
.addProfile("ordinal-test-profile")
@ -284,14 +289,15 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest {
ClientProfilesRepresentation actualProfilesRep = getProfilesWithGlobals();
// same profiles
assertExpectedProfiles(actualProfilesRep, Arrays.asList("global-default-profile"), Arrays.asList("ordinal-test-profile", "lack-of-builtin-field-test-profile"));
assertExpectedProfiles(actualProfilesRep, Arrays.asList(FAPI1_BASELINE_PROFILE_NAME, FAPI1_ADVANCED_PROFILE_NAME), Arrays.asList("ordinal-test-profile", "lack-of-builtin-field-test-profile"));
// each profile - global-default-profile
ClientProfileRepresentation actualProfileRep = getProfileRepresentation(actualProfilesRep, "global-default-profile", true);
assertExpectedProfile(actualProfileRep, "global-default-profile", "The global default profile for enforcing basic security level to clients.");
// each profile - fapi-1-baseline
ClientProfileRepresentation actualProfileRep = getProfileRepresentation(actualProfilesRep, FAPI1_BASELINE_PROFILE_NAME, true);
assertExpectedProfile(actualProfileRep, FAPI1_BASELINE_PROFILE_NAME, "Client profile, which enforce clients to conform 'Financial-grade API Security Profile 1.0 - Part 1: Baseline' specification.");
// each executor
assertExpectedExecutors(Arrays.asList(SecureSessionEnforceExecutorFactory.PROVIDER_ID), actualProfileRep);
assertExpectedExecutors(Arrays.asList(SecureSessionEnforceExecutorFactory.PROVIDER_ID, PKCEEnforcerExecutorFactory.PROVIDER_ID, SecureClientAuthenticatorExecutorFactory.PROVIDER_ID,
SecureClientUrisExecutorFactory.PROVIDER_ID, ConsentRequiredExecutorFactory.PROVIDER_ID), actualProfileRep);
assertExpectedSecureSessionEnforceExecutor(actualProfileRep);
// each profile - ordinal-test-profile - updated
@ -382,7 +388,7 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest {
return keyPair;
}
private KeyPair getKeyPairFromGeneratedBase64(Map<String, String> generatedKeys, String algorithm) throws Exception {
protected KeyPair getKeyPairFromGeneratedBase64(Map<String, String> generatedKeys, String algorithm) throws Exception {
// It seems that PemUtils.decodePrivateKey, decodePublicKey can only treat RSA type keys, not EC type keys. Therefore, these are not used.
String privateKeyBase64 = generatedKeys.get(TestingOIDCEndpointsApplicationResource.PRIVATE_KEY);
String publicKeyBase64 = generatedKeys.get(TestingOIDCEndpointsApplicationResource.PUBLIC_KEY);
@ -429,7 +435,7 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest {
// Signed JWT for client authentication utility
protected String createSignedRequestToken(String clientId, PrivateKey privateKey, PublicKey publicKey, String algorithm) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException {
protected String createSignedRequestToken(String clientId, PrivateKey privateKey, PublicKey publicKey, String algorithm) {
JsonWebToken jwt = createRequestToken(clientId, getRealmInfoUrl());
String kid = KeyUtils.createKeyId(publicKey);
SignatureSignerContext signer = oauth.createSigner(privateKey, kid, algorithm);

View file

@ -94,7 +94,7 @@ public class ClientPoliciesImportExportTest extends AbstractClientPoliciesTest {
assertExpectedLoadedPolicies((ClientPoliciesRepresentation reps)->{
ClientPolicyRepresentation rep = getPolicyRepresentation(reps, "new-policy");
assertExpectedPolicy("new-policy", "duplicated profiles are ignored.", true, Arrays.asList("global-default-profile", "ordinal-test-profile", "lack-of-builtin-field-test-profile"),
assertExpectedPolicy("new-policy", "duplicated profiles are ignored.", true, Arrays.asList("ordinal-test-profile", "lack-of-builtin-field-test-profile"),
rep);
});
}

View file

@ -34,7 +34,6 @@ import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthen
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
import org.keycloak.authentication.authenticators.client.JWTClientSecretAuthenticator;
import org.keycloak.authentication.authenticators.client.X509ClientAuthenticator;
import org.keycloak.common.Profile;
import org.keycloak.representations.idm.ClientPoliciesRepresentation;
import org.keycloak.representations.idm.ClientPolicyRepresentation;
import org.keycloak.representations.idm.ClientProfileRepresentation;
@ -46,6 +45,8 @@ import org.keycloak.services.clientpolicy.condition.ClientAccessTypeConditionFac
import org.keycloak.services.clientpolicy.condition.ClientRolesConditionFactory;
import org.keycloak.services.clientpolicy.executor.PKCEEnforcerExecutorFactory;
import org.keycloak.services.clientpolicy.executor.SecureClientAuthenticatorExecutorFactory;
import org.keycloak.services.clientpolicy.executor.ConsentRequiredExecutorFactory;
import org.keycloak.services.clientpolicy.executor.SecureClientUrisExecutorFactory;
import org.keycloak.services.clientpolicy.executor.SecureSessionEnforceExecutorFactory;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
@ -75,14 +76,16 @@ public class ClientPoliciesLoadUpdateTest extends AbstractClientPoliciesTest {
ClientProfilesRepresentation actualProfilesRep = getProfilesWithGlobals();
// same profiles
assertExpectedProfiles(actualProfilesRep, Arrays.asList("global-default-profile"), Collections.emptyList());
assertExpectedProfiles(actualProfilesRep, Arrays.asList(FAPI1_BASELINE_PROFILE_NAME, FAPI1_ADVANCED_PROFILE_NAME), Collections.emptyList());
// each profile
ClientProfileRepresentation actualProfileRep = getProfileRepresentation(actualProfilesRep, "global-default-profile", true);
assertExpectedProfile(actualProfileRep, "global-default-profile", "The global default profile for enforcing basic security level to clients.");
// each profile - fapi-1-baseline
ClientProfileRepresentation actualProfileRep = getProfileRepresentation(actualProfilesRep, FAPI1_BASELINE_PROFILE_NAME, true);
assertExpectedProfile(actualProfileRep, FAPI1_BASELINE_PROFILE_NAME, "Client profile, which enforce clients to conform 'Financial-grade API Security Profile 1.0 - Part 1: Baseline' specification.");
// each executor
assertExpectedExecutors(Arrays.asList(SecureSessionEnforceExecutorFactory.PROVIDER_ID), actualProfileRep);
// Test some executor
assertExpectedExecutors(Arrays.asList(SecureSessionEnforceExecutorFactory.PROVIDER_ID, PKCEEnforcerExecutorFactory.PROVIDER_ID, SecureClientAuthenticatorExecutorFactory.PROVIDER_ID,
SecureClientUrisExecutorFactory.PROVIDER_ID, ConsentRequiredExecutorFactory.PROVIDER_ID), actualProfileRep);
assertExpectedSecureSessionEnforceExecutor(actualProfileRep);
// Check the "get" request without globals. Assert nothing loaded
actualProfilesRep = getProfilesWithoutGlobals();
@ -108,7 +111,7 @@ public class ClientPoliciesLoadUpdateTest extends AbstractClientPoliciesTest {
assertExpectedLoadedPolicies((ClientPoliciesRepresentation reps)->{
ClientPolicyRepresentation rep = getPolicyRepresentation(reps, "new-policy");
assertExpectedPolicy("new-policy", "duplicated profiles are ignored.", true, Arrays.asList("global-default-profile", "ordinal-test-profile", "lack-of-builtin-field-test-profile"),
assertExpectedPolicy("new-policy", "duplicated profiles are ignored.", true, Arrays.asList("ordinal-test-profile", "lack-of-builtin-field-test-profile"),
rep);
});
@ -146,7 +149,7 @@ public class ClientPoliciesLoadUpdateTest extends AbstractClientPoliciesTest {
assertExpectedLoadedPolicies((ClientPoliciesRepresentation reps)->{
ClientPolicyRepresentation rep = getPolicyRepresentation(reps, "new-policy");
assertExpectedPolicy("new-policy", modifiedPolicyDescription, false, Arrays.asList("global-default-profile", "ordinal-test-profile", "lack-of-builtin-field-test-profile"),
assertExpectedPolicy("new-policy", modifiedPolicyDescription, false, Arrays.asList("ordinal-test-profile", "lack-of-builtin-field-test-profile"),
rep);
});
@ -193,14 +196,15 @@ public class ClientPoliciesLoadUpdateTest extends AbstractClientPoliciesTest {
@Test
public void testOverwriteBuiltinProfileNotAllowed() throws Exception {
// register profiles
String json = (new ClientProfilesBuilder()).addProfile(
(new ClientProfileBuilder()).createProfile("global-default-profile", "Pershyy Profil")
String json =
(new ClientProfilesBuilder()).addProfile(
(new ClientProfileBuilder()).createProfile(FAPI1_BASELINE_PROFILE_NAME, "Pershyy Profil")
.addExecutor(SecureClientAuthenticatorExecutorFactory.PROVIDER_ID,
createSecureClientAuthenticatorExecutorConfig(
Arrays.asList(JWTClientAuthenticator.PROVIDER_ID, JWTClientSecretAuthenticator.PROVIDER_ID, X509ClientAuthenticator.PROVIDER_ID),
X509ClientAuthenticator.PROVIDER_ID))
.toRepresentation()
).toString();
).toRepresentation().toString();
try {
updateProfiles(json);
fail();
@ -301,7 +305,7 @@ public class ClientPoliciesLoadUpdateTest extends AbstractClientPoliciesTest {
Boolean.TRUE)
.addCondition(ClientRolesConditionFactory.PROVIDER_ID,
createClientRolesConditionConfig(Arrays.asList(SAMPLE_CLIENT_ROLE)))
.addProfile("global-default-profile")
.addProfile(FAPI1_BASELINE_PROFILE_NAME)
.toRepresentation();
ClientPolicyRepresentation loadedPolicyRep =

View file

@ -1148,7 +1148,7 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
oauth.requestUri(null);
oauth.openLoginForm();
assertEquals(OAuthErrorException.INVALID_REQUEST, oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
assertEquals("Invalid parameter", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION));
assertEquals("Missing parameter: 'request' or 'request_uri'", 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
@ -1165,7 +1165,7 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
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));
assertEquals("Parameter 'scope' missing in the request parameters or in 'request' object", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION));
// check whether "exp" claim exists
requestObject = createValidRequestObjectForSecureRequestObjectExecutor(clientId);
@ -1173,7 +1173,7 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
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));
assertEquals("Missing parameter in the 'request' object: exp", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION));
// check whether request object not expired
requestObject = createValidRequestObjectForSecureRequestObjectExecutor(clientId);
@ -1189,7 +1189,7 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
registerRequestObject(requestObject, clientId, Algorithm.ES256, false);
oauth.openLoginForm();
assertEquals(SecureRequestObjectExecutor.INVALID_REQUEST_OBJECT, oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
assertEquals("Missing parameter : 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
requestObject = createValidRequestObjectForSecureRequestObjectExecutor(clientId);
@ -1213,7 +1213,7 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
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));
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
requestObject = createValidRequestObjectForSecureRequestObjectExecutor(clientId);
@ -1221,7 +1221,7 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
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));
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
// argument "request" are parameters overridden by parameters in request object
@ -1230,7 +1230,7 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
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));
assertEquals("Invalid parameter. Parameters in 'request' object not matching with request parameters", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION));
// valid request object
requestObject = createValidRequestObjectForSecureRequestObjectExecutor(clientId);
@ -1252,7 +1252,7 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
registerRequestObject(requestObject, clientId, Algorithm.ES256, false);
oauth.openLoginForm();
assertEquals(SecureRequestObjectExecutor.INVALID_REQUEST_OBJECT, oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
assertEquals("Missing parameter : 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
requestObject = createValidRequestObjectForSecureRequestObjectExecutor(clientId);

View file

@ -0,0 +1,809 @@
/*
* Copyright 2021 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.testsuite.client;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.hamcrest.Matchers;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.adapters.authentication.JWTClientSecretCredentialsProvider;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
import org.keycloak.authentication.authenticators.client.JWTClientSecretAuthenticator;
import org.keycloak.authentication.authenticators.client.X509ClientAuthenticator;
import org.keycloak.client.registration.ClientRegistrationException;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.common.util.UriUtils;
import org.keycloak.constants.ServiceUrlConstants;
import org.keycloak.crypto.Algorithm;
import org.keycloak.jose.jws.crypto.HashUtils;
import org.keycloak.models.AdminRoles;
import org.keycloak.models.Constants;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.clientpolicy.condition.AnyClientConditionFactory;
import org.keycloak.services.clientpolicy.executor.SecureRequestObjectExecutor;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls;
import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResource;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.OAuthGrantPage;
import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource;
import org.keycloak.testsuite.util.MutualTLSUtils;
import org.keycloak.testsuite.util.OAuthClient;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
/**
* Test for the FAPI 1 specifications:
* - Financial-grade API Security Profile 1.0 - Part 1: Baseline - https://openid.net/specs/openid-financial-api-part-1-1_0.html#authorization-server
* - Financial-grade API Security Profile 1.0 - Part 2: Advanced - https://openid.net/specs/openid-financial-api-part-2-1_0.html
*
* Mostly tests the global FAPI policies work as expected
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class FAPI1Test extends AbstractClientPoliciesTest {
@Page
protected ErrorPage errorPage;
@Page
protected LoginPage loginPage;
@Page
protected OAuthGrantPage grantPage;
@Page
protected AppPage appPage;
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation realm = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
List<UserRepresentation> users = realm.getUsers();
LinkedList<CredentialRepresentation> credentials = new LinkedList<>();
CredentialRepresentation password = new CredentialRepresentation();
password.setType(CredentialRepresentation.PASSWORD);
password.setValue("password");
credentials.add(password);
UserRepresentation user = new UserRepresentation();
user.setEnabled(true);
user.setUsername("john");
user.setEmail("john@keycloak.org");
user.setFirstName("Johny");
user.setCredentials(credentials);
user.setClientRoles(Collections.singletonMap(Constants.REALM_MANAGEMENT_CLIENT_ID, Arrays.asList(AdminRoles.CREATE_CLIENT, AdminRoles.MANAGE_CLIENTS)));
users.add(user);
realm.setUsers(users);
testRealms.add(realm);
}
@Test
public void testFAPIBaselineClientAuthenticator() throws Exception {
setupPolicyFAPIBaselineForAllClient();
// Try to register client with clientIdAndSecret - should fail
try {
createClientByAdmin("invalid", (ClientRepresentation clientRep) -> {
clientRep.setClientAuthenticatorType(ClientIdAndSecretAuthenticator.PROVIDER_ID);
});
fail();
} catch (ClientPolicyException e) {
assertEquals(OAuthErrorException.INVALID_CLIENT_METADATA, e.getMessage());
}
// Try to register client with "client-jwt" - should pass
String clientUUID = createClientByAdmin("client-jwt", (ClientRepresentation clientRep) -> {
clientRep.setClientAuthenticatorType(JWTClientAuthenticator.PROVIDER_ID);
});
ClientRepresentation client = getClientByAdmin(clientUUID);
Assert.assertEquals(JWTClientAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
// Try to register client with "client-secret-jwt" - should pass
clientUUID = createClientByAdmin("client-secret-jwt", (ClientRepresentation clientRep) -> {
clientRep.setClientAuthenticatorType(JWTClientSecretAuthenticator.PROVIDER_ID);
});
client = getClientByAdmin(clientUUID);
Assert.assertEquals(JWTClientSecretAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
// Try to register client with "client-x509" - should pass
clientUUID = createClientByAdmin("client-x509", (ClientRepresentation clientRep) -> {
clientRep.setClientAuthenticatorType(X509ClientAuthenticator.PROVIDER_ID);
});
client = getClientByAdmin(clientUUID);
Assert.assertEquals(X509ClientAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
// Try to register client with default authenticator - should pass. Client authenticator should be "client-jwt"
clientUUID = createClientByAdmin("client-jwt-2", (ClientRepresentation clientRep) -> {
});
client = getClientByAdmin(clientUUID);
Assert.assertEquals(JWTClientAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
// Check the Consent is enabled, PKCS set to S256
Assert.assertTrue(client.isConsentRequired());
Assert.assertEquals(OAuth2Constants.PKCE_METHOD_S256, OIDCAdvancedConfigWrapper.fromClientRepresentation(client).getPkceCodeChallengeMethod());
}
@Test
public void testFAPIBaselineOIDCClientRegistration() throws Exception {
setupPolicyFAPIBaselineForAllClient();
// Try to register client with clientIdAndSecret - should fail
try {
createClientDynamically(generateSuffixedName("foo"), (OIDCClientRepresentation clientRep) -> {
clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.CLIENT_SECRET_BASIC);
});
fail();
} catch (ClientRegistrationException e) {
assertEquals(ERR_MSG_CLIENT_REG_FAIL, e.getMessage());
}
// Try to register client with "client-jwt" - should pass
String clientUUID = createClientDynamically("client-jwt", (OIDCClientRepresentation clientRep) -> {
clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.PRIVATE_KEY_JWT);
clientRep.setJwksUri("https://foo");
});
ClientRepresentation client = getClientByAdmin(clientUUID);
Assert.assertEquals(JWTClientAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
// Set new initialToken for register new clients
setInitialAccessTokenForDynamicClientRegistration();
// Try to register client with "client-secret-jwt" - should pass
clientUUID = createClientDynamically("client-secret-jwt", (OIDCClientRepresentation clientRep) -> {
clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.CLIENT_SECRET_JWT);
});
client = getClientByAdmin(clientUUID);
Assert.assertEquals(JWTClientSecretAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
// Set new initialToken for register new clients
setInitialAccessTokenForDynamicClientRegistration();
// Try to register client with "client-x509" - should pass
clientUUID = createClientDynamically("client-x509", (OIDCClientRepresentation clientRep) -> {
clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.TLS_CLIENT_AUTH);
});
client = getClientByAdmin(clientUUID);
Assert.assertEquals(X509ClientAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
// Check the Consent is enabled, PKCS set to S256
Assert.assertTrue(client.isConsentRequired());
Assert.assertEquals(OAuth2Constants.PKCE_METHOD_S256, OIDCAdvancedConfigWrapper.fromClientRepresentation(client).getPkceCodeChallengeMethod());
}
@Test
public void testFAPIBaselineRedirectUri() throws Exception {
setupPolicyFAPIBaselineForAllClient();
// Try to register redirect_uri like "http://hostname.com" - should fail
try {
String clientUUID = createClientByAdmin("invalid", (ClientRepresentation clientRep) -> {
clientRep.setRedirectUris(Collections.singletonList("http://hostname.com"));
});
fail();
} catch (ClientPolicyException e) {
assertEquals(OAuthErrorException.INVALID_CLIENT_METADATA, e.getMessage());
}
// Try to register redirect_uri like "https://hostname.com/foo/*" - should fail due the wildcard
try {
createClientByAdmin("invalid", (ClientRepresentation clientRep) -> {
clientRep.setRedirectUris(Collections.singletonList("https://hostname.com/foo/*"));
});
fail();
} catch (ClientPolicyException e) {
assertEquals(OAuthErrorException.INVALID_CLIENT_METADATA, e.getMessage());
}
// Try to register redirect_uri like "https://hostname.com" - should pass
String clientUUID = createClientByAdmin("invalid", (ClientRepresentation clientRep) -> {
clientRep.setRedirectUris(Collections.singletonList("https://hostname.com"));
});
ClientRepresentation client = getClientByAdmin(clientUUID);
Assert.assertNames(client.getRedirectUris(), "https://hostname.com");
}
@Test
public void testFAPIBaselineConfidentialClientLogin() throws Exception {
setupPolicyFAPIBaselineForAllClient();
// Register client (default authenticator)
String clientUUID = createClientByAdmin("foo", (ClientRepresentation clientRep) -> {
clientRep.setClientAuthenticatorType(JWTClientSecretAuthenticator.PROVIDER_ID);
clientRep.setSecret("secret");
});
ClientRepresentation client = getClientByAdmin(clientUUID);
Assert.assertFalse(client.isPublicClient());
Assert.assertEquals(JWTClientSecretAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
checkPKCEWithS256RequiredDuringLogin("foo");
// Setup PKCE
String codeVerifier = "1234567890123456789012345678901234567890123"; // 43
String codeChallenge = generateS256CodeChallenge(codeVerifier);
oauth.codeChallenge(codeChallenge);
oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256);
checkNonceAndStateForCurrentClientDuringLogin();
checkRedirectUriForCurrentClientDuringLogin();
// Check PKCE with S256, redirectUri and nonce/state set. Login should be successful
successfulLoginAndLogout("foo", false, (String code) -> {
String signedJwt = getClientSecretSignedJWT("secret", Algorithm.HS256);
return doAccessTokenRequestWithClientSignedJWT(code, signedJwt, codeVerifier, DefaultHttpClient::new);
});
}
@Test
public void testFAPIBaselinePublicClientLogin() throws Exception {
setupPolicyFAPIBaselineForAllClient();
// Register client as public client
String clientUUID = createClientByAdmin("foo", (ClientRepresentation clientRep) -> {
clientRep.setPublicClient(true);
});
ClientRepresentation client = getClientByAdmin(clientUUID);
Assert.assertTrue(client.isPublicClient());
checkPKCEWithS256RequiredDuringLogin("foo");
// Setup PKCE
String codeVerifier = "1234567890123456789012345678901234567890123"; // 43
String codeChallenge = generateS256CodeChallenge(codeVerifier);
oauth.codeChallenge(codeChallenge);
oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256);
checkNonceAndStateForCurrentClientDuringLogin();
checkRedirectUriForCurrentClientDuringLogin();
// Check PKCE with S256, redirectUri and nonce/state set. Login should be successful
successfulLoginAndLogout("foo", false, (String code) -> {
oauth.codeVerifier(codeVerifier);
return oauth.doAccessTokenRequest(code, null);
});
}
@Test
public void testFAPIAdvancedClientRegistration() throws Exception {
// Set "advanced" policy
setupPolicyFAPIAdvancedForAllClient();
// Register client with clientIdAndSecret - should fail
try {
createClientByAdmin("invalid", (ClientRepresentation clientRep) -> {
clientRep.setClientAuthenticatorType(ClientIdAndSecretAuthenticator.PROVIDER_ID);
});
fail();
} catch (ClientPolicyException e) {
assertEquals(OAuthErrorException.INVALID_CLIENT_METADATA, e.getMessage());
}
// Register client with signedJWT - should fail
try {
createClientByAdmin("invalid", (ClientRepresentation clientRep) -> {
clientRep.setClientAuthenticatorType(JWTClientSecretAuthenticator.PROVIDER_ID);
});
fail();
} catch (ClientPolicyException e) {
assertEquals(OAuthErrorException.INVALID_CLIENT_METADATA, e.getMessage());
}
// Register client with privateKeyJWT, but unsecured redirectUri - should fail
try {
createClientByAdmin("invalid", (ClientRepresentation clientRep) -> {
clientRep.setClientAuthenticatorType(JWTClientAuthenticator.PROVIDER_ID);
clientRep.setRedirectUris(Collections.singletonList("http://foo"));
});
fail();
} catch (ClientPolicyException e) {
assertEquals(OAuthErrorException.INVALID_CLIENT_METADATA, e.getMessage());
}
// Try to register client with "client-jwt" - should pass
String clientUUID = createClientByAdmin("client-jwt", (ClientRepresentation clientRep) -> {
clientRep.setClientAuthenticatorType(JWTClientAuthenticator.PROVIDER_ID);
});
ClientRepresentation client = getClientByAdmin(clientUUID);
Assert.assertEquals(JWTClientAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
// Try to register client with "client-x509" - should pass
clientUUID = createClientByAdmin("client-x509", (ClientRepresentation clientRep) -> {
clientRep.setClientAuthenticatorType(X509ClientAuthenticator.PROVIDER_ID);
});
client = getClientByAdmin(clientUUID);
Assert.assertEquals(X509ClientAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
// Try to register client with default authenticator - should pass. Client authenticator should be "client-jwt"
clientUUID = createClientByAdmin("client-jwt-2", (ClientRepresentation clientRep) -> {
});
client = getClientByAdmin(clientUUID);
Assert.assertEquals(JWTClientAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
// Check the Consent is enabled, Holder-of-key is enabled and default signature algorithm
Assert.assertTrue(client.isConsentRequired());
OIDCAdvancedConfigWrapper clientConfig = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
Assert.assertTrue(clientConfig.isUseMtlsHokToken());
Assert.assertEquals(Algorithm.PS256, clientConfig.getIdTokenSignedResponseAlg());
Assert.assertEquals(Algorithm.PS256, clientConfig.getRequestObjectSignatureAlg().toString());
}
@Test
public void testFAPIAdvancedPublicClientLoginNotPossible() throws Exception {
setupPolicyFAPIBaselineForAllClient();
// Register client as public client
String clientUUID = createClientByAdmin("foo", (ClientRepresentation clientRep) -> {
clientRep.setPublicClient(true);
});
ClientRepresentation client = getClientByAdmin(clientUUID);
Assert.assertTrue(client.isPublicClient());
// Setup PKCE and nonce
oauth.nonce("123456");
String codeVerifier = "1234567890123456789012345678901234567890123"; // 43
String codeChallenge = generateS256CodeChallenge(codeVerifier);
oauth.codeChallenge(codeChallenge);
oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256);
// Check PKCE with S256, redirectUri and nonce/state set. Login should be successful
successfulLoginAndLogout("foo", false, (String code) -> {
oauth.codeVerifier(codeVerifier);
return oauth.doAccessTokenRequest(code, null);
});
// Set "advanced" policy
setupPolicyFAPIAdvancedForAllClient();
// Should not be possible to login anymore with public client
oauth.openLoginForm();
assertRedirectedToClientWithError(OAuthErrorException.INVALID_CLIENT, false,"invalid client access type");
}
@Test
public void testFAPIAdvancedSignatureAlgorithms() throws Exception {
// Set "advanced" policy
setupPolicyFAPIAdvancedForAllClient();
// Test that unsecured algorithm (RS256) is not possible
try {
createClientByAdmin("invalid", (ClientRepresentation clientRep) -> {
clientRep.setClientAuthenticatorType(JWTClientAuthenticator.PROVIDER_ID);
OIDCAdvancedConfigWrapper clientConfig = OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep);
clientConfig.setIdTokenSignedResponseAlg(Algorithm.RS256);
});
fail();
} catch (ClientPolicyException e) {
assertEquals(OAuthErrorException.INVALID_REQUEST, e.getMessage());
}
// Test that secured algorithm is possible to explicitly set
String clientUUID = createClientByAdmin("client-jwt", (ClientRepresentation clientRep) -> {
clientRep.setClientAuthenticatorType(JWTClientAuthenticator.PROVIDER_ID);
OIDCAdvancedConfigWrapper clientCfg = OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep);
clientCfg.setIdTokenSignedResponseAlg(Algorithm.ES256);
});
ClientRepresentation client = getClientByAdmin(clientUUID);
OIDCAdvancedConfigWrapper clientConfig = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
Assert.assertEquals(Algorithm.ES256, clientConfig.getIdTokenSignedResponseAlg());
Assert.assertEquals(Algorithm.PS256, clientConfig.getRequestObjectSignatureAlg().toString());
// Test default algorithms set everywhere
clientUUID = createClientByAdmin("client-jwt-default-alg", (ClientRepresentation clientRep) -> {
clientRep.setClientAuthenticatorType(JWTClientAuthenticator.PROVIDER_ID);
});
client = getClientByAdmin(clientUUID);
clientConfig = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
Assert.assertEquals(Algorithm.PS256, clientConfig.getIdTokenSignedResponseAlg());
Assert.assertEquals(Algorithm.PS256, clientConfig.getRequestObjectSignatureAlg().toString());
Assert.assertEquals(Algorithm.PS256, clientConfig.getUserInfoSignedResponseAlg().toString());
Assert.assertEquals(Algorithm.PS256, clientConfig.getTokenEndpointAuthSigningAlg());
Assert.assertEquals(Algorithm.PS256, client.getAttributes().get(OIDCConfigAttributes.ACCESS_TOKEN_SIGNED_RESPONSE_ALG));
}
@Test
public void testFAPIAdvancedLoginWithPrivateKeyJWT() throws Exception {
// Set "advanced" policy
setupPolicyFAPIAdvancedForAllClient();
// Register client with private-key-jwt
String clientUUID = createClientByAdmin("foo", (ClientRepresentation clientRep) -> {
clientRep.setClientAuthenticatorType(JWTClientAuthenticator.PROVIDER_ID);
clientRep.setImplicitFlowEnabled(true);
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestUris(Collections.singletonList(TestApplicationResourceUrls.clientRequestUri()));
});
ClientResource clientResource = adminClient.realm(REALM_NAME).clients().get(clientUUID);
ClientRepresentation client = clientResource.toRepresentation();
assertEquals(JWTClientAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
// Check nonce and redirectUri
oauth.clientId("foo");
checkNonceAndStateForCurrentClientDuringLogin();
checkRedirectUriForCurrentClientDuringLogin();
// Check login request object required
oauth.openLoginForm();
assertRedirectedToClientWithError(OAuthErrorException.INVALID_REQUEST,false, "Missing parameter: 'request' or 'request_uri'");
// Create request without 'nbf' . Should fail in FAPI1 advanced client policy
TestingOIDCEndpointsApplicationResource.AuthorizationEndpointRequestObject requestObject = createValidRequestObjectForSecureRequestObjectExecutor("foo");
requestObject.nbf(null);
registerRequestObject(requestObject, "foo", org.keycloak.jose.jws.Algorithm.PS256, true);
oauth.openLoginForm();
assertRedirectedToClientWithError(SecureRequestObjectExecutor.INVALID_REQUEST_OBJECT,false, "Missing parameter in the 'request' object: nbf");
// Create valid request object - more extensive testing of 'request' object is in ClientPoliciesTest.testSecureRequestObjectExecutor()
requestObject = createValidRequestObjectForSecureRequestObjectExecutor("foo");
requestObject.setNonce("123456"); // Nonce from method "checkNonceAndStateForCurrentClientDuringLogin()"
registerRequestObject(requestObject, "foo", org.keycloak.jose.jws.Algorithm.PS256, true);
// Check response type
oauth.openLoginForm();
assertRedirectedToClientWithError(OAuthErrorException.INVALID_REQUEST,false, "invalid response_type");
// Add the response_Type including token. Should fail
oauth.responseType(OIDCResponseType.CODE + " " + OIDCResponseType.ID_TOKEN + " " + OIDCResponseType.TOKEN);
requestObject.setResponseType(OIDCResponseType.CODE + " " + OIDCResponseType.ID_TOKEN + " " + OIDCResponseType.TOKEN);
registerRequestObject(requestObject, "foo", org.keycloak.jose.jws.Algorithm.PS256, true);
oauth.openLoginForm();
assertRedirectedToClientWithError(OAuthErrorException.INVALID_REQUEST,true, "invalid response_type");
// Set correct response_type for FAPI 1 Advanced
oauth.responseType(OIDCResponseType.CODE + " " + OIDCResponseType.ID_TOKEN);
requestObject.setResponseType(OIDCResponseType.CODE + " " + OIDCResponseType.ID_TOKEN);
registerRequestObject(requestObject, "foo", org.keycloak.jose.jws.Algorithm.PS256, true);
oauth.openLoginForm();
loginPage.assertCurrent();
// Get keys of client. Will be used for client authentication and signing of request object
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
Map<String, String> generatedKeys = oidcClientEndpointsResource.getKeysAsBase64();
KeyPair keyPair = getKeyPairFromGeneratedBase64(generatedKeys, Algorithm.PS256);
PrivateKey privateKey = keyPair.getPrivate();
PublicKey publicKey = keyPair.getPublic();
String code = loginUserAndGetCode("foo", true);
// Check token not present in the AuthorizationResponse. Check ID Token present, but used as detached signature
Assert.assertNull(getParameterFromUrl(OAuth2Constants.ACCESS_TOKEN, true));
String idTokenParam = getParameterFromUrl(OAuth2Constants.ID_TOKEN, true);
assertIDTokenAsDetachedSignature(idTokenParam, code);
// Check HoK required
String signedJwt = createSignedRequestToken("foo", privateKey, publicKey, org.keycloak.crypto.Algorithm.PS256);
OAuthClient.AccessTokenResponse tokenResponse = doAccessTokenRequestWithClientSignedJWT(code, signedJwt, null, DefaultHttpClient::new);
Assert.assertEquals(OAuthErrorException.INVALID_GRANT,tokenResponse.getError());
Assert.assertEquals("Client Certification missing for MTLS HoK Token Binding", tokenResponse.getErrorDescription());
// Login with private-key-jwt client authentication and MTLS added to HttpClient. TokenRequest should be successful now
oauth.openLoginForm();
code = oauth.getCurrentFragment().get(OAuth2Constants.CODE);
Assert.assertNotNull(code);
String signedJwt2 = createSignedRequestToken("foo", privateKey, publicKey, org.keycloak.crypto.Algorithm.PS256);
tokenResponse = doAccessTokenRequestWithClientSignedJWT(code, signedJwt2, null, () -> MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore());
assertSuccessfulTokenResponse(tokenResponse);
AccessToken accessToken = oauth.verifyToken(tokenResponse.getAccessToken());
Assert.assertNotNull(accessToken.getCertConf().getCertThumbprint());
// Logout and remove consent of the user for next logins
logoutUserAndRevokeConsent("foo");
}
@Test
public void testFAPIAdvancedLoginWithMTLS() throws Exception {
// Set "advanced" policy
setupPolicyFAPIAdvancedForAllClient();
// Register client with X509
String clientUUID = createClientByAdmin("foo", (ClientRepresentation clientRep) -> {
clientRep.setClientAuthenticatorType(X509ClientAuthenticator.PROVIDER_ID);
clientRep.setImplicitFlowEnabled(true);
OIDCAdvancedConfigWrapper clientConfig = OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep);
clientConfig.setRequestUris(Collections.singletonList(TestApplicationResourceUrls.clientRequestUri()));
clientConfig.setTlsClientAuthSubjectDn("EMAILADDRESS=contact@keycloak.org, CN=Keycloak Intermediate CA, OU=Keycloak, O=Red Hat, ST=MA, C=US");
});
ClientResource clientResource = adminClient.realm(REALM_NAME).clients().get(clientUUID);
ClientRepresentation client = clientResource.toRepresentation();
assertEquals(X509ClientAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
// Check nonce and redirectUri
oauth.clientId("foo");
checkNonceAndStateForCurrentClientDuringLogin();
checkRedirectUriForCurrentClientDuringLogin();
// Check login request object required
oauth.openLoginForm();
assertRedirectedToClientWithError(OAuthErrorException.INVALID_REQUEST,false, "Missing parameter: 'request' or 'request_uri'");
// Set request object and correct responseType
TestingOIDCEndpointsApplicationResource.AuthorizationEndpointRequestObject requestObject = createValidRequestObjectForSecureRequestObjectExecutor("foo");
requestObject.setNonce("123456"); // Nonce from method "checkNonceAndStateForCurrentClientDuringLogin()"
oauth.responseType(OIDCResponseType.CODE + " " + OIDCResponseType.ID_TOKEN);
requestObject.setResponseType(OIDCResponseType.CODE + " " + OIDCResponseType.ID_TOKEN);
registerRequestObject(requestObject, "foo", org.keycloak.jose.jws.Algorithm.PS256, true);
oauth.openLoginForm();
loginPage.assertCurrent();
String code = loginUserAndGetCode("foo", true);
// Check token not present in the AuthorizationResponse. Check ID Token present, but used as detached signature
Assert.assertNull(getParameterFromUrl(OAuth2Constants.ACCESS_TOKEN, true));
String idTokenParam = getParameterFromUrl(OAuth2Constants.ID_TOKEN, true);
assertIDTokenAsDetachedSignature(idTokenParam, code);
// Check HoK required
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, null);
assertSuccessfulTokenResponse(tokenResponse);
AccessToken accessToken = oauth.verifyToken(tokenResponse.getAccessToken());
Assert.assertNotNull(accessToken.getCertConf().getCertThumbprint());
// Logout and remove consent of the user for next logins
logoutUserAndRevokeConsent("foo");
}
private void checkPKCEWithS256RequiredDuringLogin(String clientId) {
// Check PKCE required - login without PKCE should fail
oauth.clientId(clientId);
oauth.openLoginForm();
assertRedirectedToClientWithError(OAuthErrorException.INVALID_REQUEST,false, "Missing parameter: code_challenge_method");
// Check PKCE required - login with "plain" PKCE should fail
oauth.codeChallenge("234567890_234567890123");
oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_PLAIN);
oauth.openLoginForm();
assertRedirectedToClientWithError(OAuthErrorException.INVALID_REQUEST,false, "Invalid parameter: code challenge method is not configured one");
}
// Assumption is that clientId is already set in "oauth" client when this method is called. Also assumption is that PKCE parameters are properly set (in case PKCE required for the client)
private void checkNonceAndStateForCurrentClientDuringLogin() {
oauth.openLoginForm();
assertRedirectedToClientWithError(OAuthErrorException.INVALID_REQUEST,false, "Missing parameter: nonce");
// Check "state" required in non-OIDC request
oauth.nonce("123456");
oauth.stateParamHardcoded(null);
oauth.openid(false);
oauth.openLoginForm();
assertRedirectedToClientWithError(OAuthErrorException.INVALID_REQUEST,false, "Missing parameter: state");
// Revert to default "state" parameter generator
oauth.stateParamRandom();
}
private void checkRedirectUriForCurrentClientDuringLogin() {
String origRedirectUri = oauth.getRedirectUri();
// Check redirect_uri required
oauth.openid(true);
oauth.redirectUri(null);
oauth.openLoginForm();
errorPage.assertCurrent();
Assert.assertEquals("Invalid parameter: redirect_uri", errorPage.getError());
// Revert redirectUri
oauth.redirectUri(origRedirectUri);
}
private void setupPolicyFAPIBaselineForAllClient() throws Exception {
String json = (new ClientPoliciesBuilder()).addPolicy(
(new ClientPolicyBuilder()).createPolicy("MyPolicy", "Policy for enable FAPI Baseline for all clients", Boolean.TRUE)
.addCondition(AnyClientConditionFactory.PROVIDER_ID,
createAnyClientConditionConfig())
.addProfile(FAPI1_BASELINE_PROFILE_NAME)
.toRepresentation()
).toString();
updatePolicies(json);
}
private void setupPolicyFAPIAdvancedForAllClient() throws Exception {
String json = (new ClientPoliciesBuilder()).addPolicy(
(new ClientPolicyBuilder()).createPolicy("MyPolicy", "Policy for enable FAPI Advanced for all clients", Boolean.TRUE)
.addCondition(AnyClientConditionFactory.PROVIDER_ID,
createAnyClientConditionConfig())
.addProfile(FAPI1_ADVANCED_PROFILE_NAME)
.toRepresentation()
).toString();
updatePolicies(json);
}
// codeToTokenExchanger is supposed to exchange "code" for the accessTokenResponse. It is supposed to send the tokenRequest including proper client authentication
private void successfulLoginAndLogout(String clientId, boolean fragmentResponseModeExpected, Function<String, OAuthClient.AccessTokenResponse> codeToTokenExchanger) throws Exception {
String code = loginUserAndGetCode(clientId, fragmentResponseModeExpected);
OAuthClient.AccessTokenResponse tokenResponse = codeToTokenExchanger.apply(code);
assertSuccessfulTokenResponse(tokenResponse);
// Logout and remove consent of the user for next logins
logoutUserAndRevokeConsent(clientId);
}
private String loginUserAndGetCode(String clientId, boolean fragmentResponseModeExpected) {
oauth.clientId(clientId);
oauth.doLogin("john", "password");
grantPage.assertCurrent();
grantPage.assertGrants(OAuthGrantPage.PROFILE_CONSENT_TEXT, OAuthGrantPage.EMAIL_CONSENT_TEXT, OAuthGrantPage.ROLES_CONSENT_TEXT);
grantPage.accept();
String code = getParameterFromUrl(OAuth2Constants.CODE, fragmentResponseModeExpected);
Assert.assertNotNull(code);
return code;
}
private void assertSuccessfulTokenResponse(OAuthClient.AccessTokenResponse tokenResponse) {
assertEquals(200, tokenResponse.getStatusCode());
Assert.assertThat(tokenResponse.getIdToken(), Matchers.notNullValue());
Assert.assertThat(tokenResponse.getAccessToken(), Matchers.notNullValue());
// Scope parameter must be present per FAPI
Assert.assertNotNull(tokenResponse.getScope());
assertScopes("openid profile email", tokenResponse.getScope());
// ID Token contains all the claims
IDToken idToken = oauth.verifyIDToken(tokenResponse.getIdToken());
Assert.assertNotNull(idToken.getId());
Assert.assertEquals("foo", idToken.getIssuedFor());
Assert.assertEquals("john", idToken.getPreferredUsername());
Assert.assertEquals("john@keycloak.org", idToken.getEmail());
Assert.assertEquals("Johny", idToken.getGivenName());
Assert.assertEquals(idToken.getNonce(), "123456");
}
private void assertIDTokenAsDetachedSignature(String idTokenParam, String code) {
Assert.assertNotNull(idTokenParam);
IDToken idToken = oauth.verifyIDToken(idTokenParam);
Assert.assertNotNull(idToken.getId());
Assert.assertEquals("foo", idToken.getIssuedFor());
Assert.assertNull(idToken.getPreferredUsername());
Assert.assertNull(idToken.getEmail());
Assert.assertNull(idToken.getGivenName());
Assert.assertNull(idToken.getAccessTokenHash());
Assert.assertEquals(idToken.getNonce(), "123456");
String state = getParameterFromUrl(OAuth2Constants.STATE, true);
Assert.assertEquals(idToken.getStateHash(), HashUtils.oidcHash(Algorithm.PS256, state));
Assert.assertEquals(idToken.getCodeHash(), HashUtils.oidcHash(Algorithm.PS256, code));
}
private String getClientSecretSignedJWT(String secret, String algorithm) {
JWTClientSecretCredentialsProvider jwtProvider = new JWTClientSecretCredentialsProvider();
jwtProvider.setClientSecret(secret, algorithm);
return jwtProvider.createSignedRequestToken(oauth.getClientId(), getRealmInfoUrl(), algorithm);
}
private String getRealmInfoUrl() {
String authServerBaseUrl = UriUtils.getOrigin(oauth.getRedirectUri()) + "/auth";
return KeycloakUriBuilder.fromUri(authServerBaseUrl).path(ServiceUrlConstants.REALM_INFO_PATH).build("test").toString();
}
private OAuthClient.AccessTokenResponse doAccessTokenRequestWithClientSignedJWT(String code, String signedJwt, String codeVerifier, Supplier<CloseableHttpClient> httpClientSupplier) {
try {
List<NameValuePair> parameters = new LinkedList<>();
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.AUTHORIZATION_CODE));
parameters.add(new BasicNameValuePair(OAuth2Constants.CODE, code));
parameters.add(new BasicNameValuePair(OAuth2Constants.CODE_VERIFIER, codeVerifier));
parameters.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, oauth.getRedirectUri()));
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt));
CloseableHttpResponse response = sendRequest(oauth.getAccessTokenUrl(), parameters, httpClientSupplier);
return new OAuthClient.AccessTokenResponse(response);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private CloseableHttpResponse sendRequest(String requestUrl, List<NameValuePair> parameters, Supplier<CloseableHttpClient> httpClientSupplier) throws Exception {
CloseableHttpClient client = httpClientSupplier.get();
try {
HttpPost post = new HttpPost(requestUrl);
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
post.setEntity(formEntity);
return client.execute(post);
} finally {
oauth.closeClient(client);
}
}
public static void assertScopes(String expectedScope, String receivedScope) {
Collection<String> expectedScopes = Arrays.asList(expectedScope.split(" "));
Collection<String> receivedScopes = Arrays.asList(receivedScope.split(" "));
Assert.assertTrue("Not matched. expectedScope: " + expectedScope + ", receivedScope: " + receivedScope,
expectedScopes.containsAll(receivedScopes) && receivedScopes.containsAll(expectedScopes));
}
private void assertRedirectedToClientWithError(String expectedError, boolean fragmentExpected, String expectedErrorDescription) {
appPage.assertCurrent();
assertEquals(expectedError, getParameterFromUrl(OAuth2Constants.ERROR, fragmentExpected));
assertEquals(expectedErrorDescription, getParameterFromUrl(OAuth2Constants.ERROR_DESCRIPTION, fragmentExpected));
}
private String getParameterFromUrl(String paramName, boolean fragmentExpected) {
return fragmentExpected ? oauth.getCurrentFragment().get(paramName) : oauth.getCurrentQuery().get(paramName);
}
private void logoutUserAndRevokeConsent(String clientId) {
UserResource user = ApiUtil.findUserByUsernameId(adminClient.realm(REALM_NAME), "john");
user.logout();
List<Map<String, Object>> consents = user.getConsents();
org.junit.Assert.assertEquals(1, consents.size());
user.revokeConsent(clientId);
}
}

View file

@ -349,7 +349,7 @@ module.config([ '$routeProvider', function($routeProvider) {
return RealmLoader();
},
clientProfiles : function(ClientPoliciesProfilesLoader) {
return ClientPoliciesProfilesLoader.loadClientProfiles('false');
return ClientPoliciesProfilesLoader.loadClientProfiles('true');
}
},
controller : 'ClientPoliciesProfilesJsonCtrl'