KEYCLOAK-14208 Default client profiles for FAPI
This commit is contained in:
parent
ab13e3e4fe
commit
73a38997d8
11 changed files with 964 additions and 50 deletions
|
@ -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
|
||||
|
|
|
@ -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.");
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -446,7 +446,7 @@ public class TestingOIDCEndpointsApplicationResource {
|
|||
return nonce;
|
||||
}
|
||||
|
||||
public void getNonce(String nonce) {
|
||||
public void setNonce(String nonce) {
|
||||
this.nonce = nonce;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -349,7 +349,7 @@ module.config([ '$routeProvider', function($routeProvider) {
|
|||
return RealmLoader();
|
||||
},
|
||||
clientProfiles : function(ClientPoliciesProfilesLoader) {
|
||||
return ClientPoliciesProfilesLoader.loadClientProfiles('false');
|
||||
return ClientPoliciesProfilesLoader.loadClientProfiles('true');
|
||||
}
|
||||
},
|
||||
controller : 'ClientPoliciesProfilesJsonCtrl'
|
||||
|
|
Loading…
Reference in a new issue