Supporting OAuth 2.1 for confidential clients

closes #25314

Co-authored-by: shigeyuki kabano <shigeyuki.kabano.sj@hitachi.com>
Signed-off-by: Takashi Norimatsu <takashi.norimatsu.ws@hitachi.com>
This commit is contained in:
Takashi Norimatsu 2023-12-06 09:41:24 +09:00 committed by Marek Posolda
parent 5e34769ee0
commit 9ea679ff35
10 changed files with 389 additions and 14 deletions

View file

@ -29,6 +29,8 @@ endif::[]
include::topics/oidc/fapi-support.adoc[]
include::topics/oidc/oauth21-support.adoc[]
include::topics/oidc/recommendations.adoc[]
include::topics/saml/saml-overview.adoc[]

View file

@ -0,0 +1,17 @@
[[_oauth21-support]]
=== OAuth 2.1 Support
{project_name} makes it easier for administrators to make sure that their clients are compliant with these specifications:
* https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-10[The OAuth 2.1 Authorization Framework - draft specification]
This compliance means that the {project_name} server will verify the requirements
for the authorization server, which are mentioned in the specifications. {project_name} adapters do not have any specific support for the OAuth 2.1, hence the required validations on the client (application)
side may need to be still done manually or through some other third-party solutions.
==== OAuth 2.1 client profiles
To make sure that your clients are OAuth 2.1 compliant, you can configure Client Policies in your realm as described in the link:{adminguide_link}#_client_policies[{adminguide_name}]
and link them to the global client profiles for OAuth 2.1 support, which are automatically available in each realm. You can use `oauth-2-1-for-confidential-client` profile for confidential clients.
NOTE: OAuth 2.1 specification is still a draft and it may change in the future. Hence the {project_name} built-in OAuth 2.1 client profiles can change as well.

View file

@ -73,6 +73,7 @@ include::topics/threat/ssl.adoc[]
include::topics/threat/csrf.adoc[]
include::topics/threat/redirect.adoc[]
include::topics/threat/fapi-compliance.adoc[]
include::topics/threat/oauth21-compliance.adoc[]
include::topics/threat/compromised-tokens.adoc[]
include::topics/threat/compromised-codes.adoc[]
include::topics/threat/open-redirect.adoc[]

View file

@ -6,7 +6,7 @@ To make it easy to secure client applications, it is beneficial to realize the f
* Setting policies on what configuration a client can have
* Validation of client configurations
* Conformance to a required security standards and profiles such as Financial-grade API (FAPI)
* Conformance to a required security standards and profiles such as Financial-grade API (FAPI)and OAuth 2.1
To realize these points in a unified way, _Client Policies_ concept is introduced.
@ -28,11 +28,11 @@ Validation of client configurations::
Client Policies can do these validation of client configurations mentioned just above and they can also be used to autoconfigure some client configuration switches to meet
the advanced security requirements. In the future, individual client configuration settings may be replaced by Client Policies directly performing required validations.
Conformance to a required security standards and profiles such as FAPI::
The _Global client profiles_ are client profiles pre-configured in {project_name} by default. They are pre-configured to be compliant with standard security profiles like link:{adapterguide_link}#_fapi-support[FAPI],
Conformance to a required security standards and profiles such as FAPI and OAuth 2.1::
The _Global client profiles_ are client profiles pre-configured in {project_name} by default. They are pre-configured to be compliant with standard security profiles like link:{adapterguide_link}#_fapi-support[FAPI] and link:{adapterguide_link}#_oauth21-support[OAuth 2.1],
which makes it easy for the administrator to secure their client application to be compliant with the particular security profile. At this moment, {project_name} has global
profiles for the support of FAPI specifications. The administrator will just need to configure the client policies to specify which clients should
be compliant with the FAPI. The administrator can configure client profiles and client policies, so that {project_name} clients can be easily made compliant with various other
profiles for the support of FAPI and OAuth 2.1 specifications. The administrator will just need to configure the client policies to specify which clients should
be compliant with the FAPI and OAuth 2.1. The administrator can configure client profiles and client policies, so that {project_name} clients can be easily made compliant with various other
security profiles like SPA, Native App, Open Banking and so on.
== Protocol
@ -61,7 +61,7 @@ The way of creating/updating a client::
So for example when creating a client, a condition can be configured to evaluate to true when this client is created by OIDC Dynamic Client Registration without initial
access token (Anonymous Dynamic Client Registration). So this condition can be used for example to ensure that all clients registered through OIDC Dynamic Client Registration
are FAPI compliant.
are FAPI or OAuth 2.1 compliant.
Author of a client (Checked by presence to the particular role or group)::
On OpenID Connect dynamic client registration, an author of a client is the end user who was authenticated to get an access token for generating a new client, not Service
@ -111,7 +111,7 @@ on the OIDC authorization request). Events are:
On each event, an executor can work in multiple phases. For example, on creating/updating a client, the executor can modify the client configuration by autoconfigure specific client
settings. After that, the executor validates this configuration in validation phase.
One of several purposes for this executor is to realize the security requirements of client conformance profiles like FAPI. To do so, the following executors are needed:
One of several purposes for this executor is to realize the security requirements of client conformance profiles like FAPI and OAuth 2.1. To do so, the following executors are needed:
* Enforce secure <<_client-credentials,Client Authentication method>> is used for the client
* Enforce <<_mtls-client-certificate-bound-tokens,Holder-of-key tokens>> are used
@ -136,9 +136,9 @@ One of several purposes for this executor is to realize the security requirement
[[_client_policy_profile]]
=== Profile
A profile consists of several executors, which can realize a security profile like FAPI. Profile can be configured by the Admin REST API (Admin Console) together with its executors.
Three _global profiles_ exist and they are configured in {project_name} by default with pre-configured executors compliant with the FAPI 1 Baseline, FAPI 1 Advanced, FAPI CIBA and FAPI 2 specifications.
More details exist in the FAPI section of the link:{adapterguide_link}#_fapi-support[{adapterguide_name}].
A profile consists of several executors, which can realize a security profile like FAPI and OAuth 2.1. Profile can be configured by the Admin REST API (Admin Console) together with its executors.
Three _global profiles_ exist and they are configured in {project_name} by default with pre-configured executors compliant with the FAPI 1 Baseline, FAPI 1 Advanced, FAPI CIBA, FAPI 2 and OAuth 2.1 specifications.
More details exist in the FAPI and OAuth 2.1 section of the link:{adapterguide_link}#_fapi-support[{adapterguide_name}].
[[_client_policy_policy]]
=== Policy

View file

@ -0,0 +1,5 @@
=== OAuth 2.1 compliance
To make sure that {project_name} server will validate your client to be more secure and OAuth 2.1 compliant, you can configure client policies
for the OAuth 2.1 support. Details are described in the OAuth 2.1 section of link:{adapterguide_link}#_oauth21-support[{adapterguide_name}].

View file

@ -284,6 +284,58 @@
}
}
]
},
{
"name": "oauth-2-1-for-confidential-client",
"description": "Client profile, which enforce confidential clients to conform 'OAuth 2.1' specification.",
"executors": [
{
"executor": "confidential-client",
"configuration": {}
},
{
"executor": "secure-client-authenticator",
"configuration": {
"allowed-client-authenticators": [
"client-jwt",
"client-x509"
],
"default-client-authenticator": "client-jwt"
}
},
{
"executor": "secure-redirect-uris-enforcer",
"configuration": {
"allow-ipv4-loopback-address": "true",
"allow-ipv6-loopback-address": "true",
"allow-private-use-uri-scheme": "true"
}
},
{
"executor": "pkce-enforcer",
"configuration": {
"auto-configure": "true"
}
},
{
"executor": "holder-of-key-enforcer",
"configuration": {
"auto-configure": "true"
}
},
{
"executor": "reject-implicit-grant",
"configuration": {
"auto-configure": "true"
}
},
{
"executor": "reject-ropc-grant",
"configuration": {
"auto-configure": "true"
}
}
]
}
]
}

View file

@ -764,8 +764,10 @@ public class OAuthClient {
try (CloseableHttpClient client = httpClient.get()) {
HttpPost post = new HttpPost(getServiceAccountUrl());
if (clientSecret != null) {
String authorization = BasicAuthHelper.RFC6749.createHeader(clientId, clientSecret);
post.setHeader("Authorization", authorization);
}
List<NameValuePair> parameters = new LinkedList<>();
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));

View file

@ -0,0 +1,294 @@
/*
* Copyright 2024 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 org.junit.After;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
import org.keycloak.authentication.authenticators.client.X509ClientAuthenticator;
import org.keycloak.client.registration.ClientRegistrationException;
import org.keycloak.common.util.SecretGenerator;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.protocol.oidc.utils.PkceUtils;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.clientpolicy.condition.AnyClientConditionFactory;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls;
import org.keycloak.testsuite.util.ClientPoliciesUtil;
import org.keycloak.testsuite.util.MutualTLSUtils;
import org.keycloak.testsuite.util.OAuthClient;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createAnyClientConditionConfig;
public class OAuth2_1ConfidentialClientTest extends AbstractFAPITest {
private static final String OAUTH2_1_CONFIDENTIAL_CLIENT_PROFILE_NAME = "oauth-2-1-for-confidential-client";
@After
public void revertPolicies() throws ClientPolicyException {
oauth.openid(true);
oauth.responseType(OIDCResponseType.CODE);
oauth.nonce(null);
oauth.codeChallenge(null);
oauth.codeChallengeMethod(null);
oauth.dpopProof(null);
updatePolicies("{}");
}
@Test
public void testOAuth2_1NotAllowImplicitGrant() throws Exception {
String clientId = generateSuffixedName(CLIENT_NAME);
String cId = createClientByAdmin(clientId, (ClientRepresentation clientRep) -> {
clientRep.setStandardFlowEnabled(Boolean.TRUE);
clientRep.setImplicitFlowEnabled(Boolean.TRUE);
clientRep.setClientAuthenticatorType(JWTClientAuthenticator.PROVIDER_ID);
});
assertEquals(JWTClientAuthenticator.PROVIDER_ID, getClientByAdmin(cId).getClientAuthenticatorType());
// setup profiles and policies
setupPolicyOAuth2_1ConfidentialClientForAllClient();
setValidPkce(clientId);
// implicit grant
testProhibitedImplicitOrHybridFlow(false, OIDCResponseType.TOKEN, generateNonce());
// hybrid grant
testProhibitedImplicitOrHybridFlow(true, OIDCResponseType.TOKEN + " " + OIDCResponseType.ID_TOKEN,
generateNonce());
// hybrid grant
testProhibitedImplicitOrHybridFlow(true, OIDCResponseType.TOKEN + " " + OIDCResponseType.CODE,
generateNonce());
// hybrid grant
testProhibitedImplicitOrHybridFlow(true, OIDCResponseType.TOKEN + " " + OIDCResponseType.CODE + " " + OIDCResponseType.ID_TOKEN,
generateNonce());
}
@Test
public void testOAuth2_1NotAllowResourceOwnerPasswordCredentialsGrant() throws Exception {
String clientId = generateSuffixedName(CLIENT_NAME);
String cId = createClientByAdmin(clientId, (ClientRepresentation clientRep) -> {
clientRep.setClientAuthenticatorType(X509ClientAuthenticator.PROVIDER_ID);
OIDCAdvancedConfigWrapper clientConfig = OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep);
clientConfig.setRequestUris(Collections.singletonList(TestApplicationResourceUrls.clientRequestUri()));
clientConfig.setTlsClientAuthSubjectDn(MutualTLSUtils.DEFAULT_KEYSTORE_SUBJECT_DN);
clientConfig.setAllowRegexPatternComparison(false);
clientRep.setDirectAccessGrantsEnabled(true);
});
// setup profiles and policies
setupPolicyOAuth2_1ConfidentialClientForAllClient();
// resource owner password credentials grant - fail
oauth.clientId(clientId);
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest(null, TEST_USERNAME, TEST_USERSECRET);
assertEquals(400, response.getStatusCode());
assertEquals(OAuthErrorException.INVALID_GRANT, response.getError());
assertEquals("resource owner password credentials grant is prohibited.", response.getErrorDescription());
}
@Test
public void testOAuth2_1ClientAuthentication() throws Exception {
// setup profiles and policies
setupPolicyOAuth2_1ConfidentialClientForAllClient();
// register client with clientIdAndSecret - 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 x509 - success
String clientId = generateSuffixedName(CLIENT_NAME);
String cId = createClientByAdmin(clientId, (ClientRepresentation clientRep) -> {
clientRep.setClientAuthenticatorType(X509ClientAuthenticator.PROVIDER_ID);
OIDCAdvancedConfigWrapper clientConfig = OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep);
clientConfig.setRequestUris(Collections.singletonList(TestApplicationResourceUrls.clientRequestUri()));
clientConfig.setTlsClientAuthSubjectDn(MutualTLSUtils.DEFAULT_KEYSTORE_SUBJECT_DN);
clientConfig.setAllowRegexPatternComparison(false);
});
verifyClientSettings(getClientByAdmin(cId), X509ClientAuthenticator.PROVIDER_ID);
}
@Test
public void testOAuth2_1ProofKeyForCodeExchange() throws Exception {
// setup profiles and policies
setupPolicyOAuth2_1ConfidentialClientForAllClient();
String clientId = generateSuffixedName(CLIENT_NAME);
String cId = createClientByAdmin(clientId, (ClientRepresentation clientRep) -> {
clientRep.setStandardFlowEnabled(Boolean.TRUE);
clientRep.setClientAuthenticatorType(JWTClientAuthenticator.PROVIDER_ID);
});
verifyClientSettings(getClientByAdmin(cId), JWTClientAuthenticator.PROVIDER_ID);
failLoginByNotFollowingPKCE(clientId);
}
@Test
public void testOAuth2_1RedirectUris() throws Exception {
// setup profiles and policies
setupPolicyOAuth2_1ConfidentialClientForAllClient();
String clientId = generateSuffixedName(CLIENT_NAME);
String cId = createClientByAdmin(clientId, (ClientRepresentation clientRep) -> {
clientRep.setClientAuthenticatorType(X509ClientAuthenticator.PROVIDER_ID);
OIDCAdvancedConfigWrapper clientConfig = OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep);
clientConfig.setRequestUris(Collections.singletonList(TestApplicationResourceUrls.clientRequestUri()));
clientConfig.setTlsClientAuthSubjectDn(MutualTLSUtils.DEFAULT_KEYSTORE_SUBJECT_DN);
clientConfig.setAllowRegexPatternComparison(false);
});
verifyClientSettings(getClientByAdmin(cId), X509ClientAuthenticator.PROVIDER_ID);
faiilUpdateRedirectUrisDynamically(clientId, List.of("https://dev.example.com:8443/*"));
successUpdateRedirectUrisByAdmin(cId,
List.of("https://dev.example.com:8443/callback", "https://[::1]/auth/admin",
"com.example.app:/oauth2redirect/example-provider", "https://127.0.0.1/auth/admin"));
failAuthorizationRequest(clientId, TestApplicationResourceUrls.clientRequestUri());
}
@Test
public void testOAuth2_1OAuthMtlsSenderConstrainedToken() throws Exception {
// setup profiles and policies
setupPolicyOAuth2_1ConfidentialClientForAllClient();
String clientId = generateSuffixedName(CLIENT_NAME);
String cId = createClientByAdmin(clientId, (ClientRepresentation clientRep) -> {
clientRep.setClientAuthenticatorType(X509ClientAuthenticator.PROVIDER_ID);
OIDCAdvancedConfigWrapper clientConfig = OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep);
clientConfig.setRequestUris(Collections.singletonList(TestApplicationResourceUrls.clientRequestUri()));
clientConfig.setTlsClientAuthSubjectDn(MutualTLSUtils.DEFAULT_KEYSTORE_SUBJECT_DN);
clientConfig.setAllowRegexPatternComparison(false);
});
verifyClientSettings(getClientByAdmin(cId), X509ClientAuthenticator.PROVIDER_ID);
oauth.clientId(clientId);
setValidPkce(clientId);
OAuthClient.AuthorizationEndpointResponse res = oauth.doLogin(TEST_USERNAME, TEST_USERSECRET);
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(res.getCode(), null);
AccessToken accessToken = oauth.verifyToken(tokenResponse.getAccessToken());
Assert.assertNotNull(accessToken.getConfirmation().getCertThumbprint());
oauth.idTokenHint(tokenResponse.getIdToken()).openLogout();
}
private void setupPolicyOAuth2_1ConfidentialClientForAllClient() throws Exception {
String json = (new ClientPoliciesUtil.ClientPoliciesBuilder()).addPolicy(
(new ClientPoliciesUtil.ClientPolicyBuilder()).createPolicy("MyPolicy", "Policy for enable OAuth 2.1 confidential client profile for all clients", Boolean.TRUE)
.addCondition(AnyClientConditionFactory.PROVIDER_ID,
createAnyClientConditionConfig())
.addProfile(OAUTH2_1_CONFIDENTIAL_CLIENT_PROFILE_NAME)
.toRepresentation()
).toString();
updatePolicies(json);
}
private void testProhibitedImplicitOrHybridFlow(boolean isOpenid, String responseType, String nonce) {
oauth.openid(isOpenid);
oauth.responseType(responseType);
oauth.nonce(nonce);
oauth.openLoginForm();
assertEquals(OAuthErrorException.INVALID_REQUEST, oauth.getCurrentFragment().get(OAuth2Constants.ERROR));
assertEquals("Implicit/Hybrid flow is prohibited.", oauth.getCurrentFragment().get(OAuth2Constants.ERROR_DESCRIPTION));
}
private void setValidPkce(String clientId) throws Exception {
oauth.clientId(clientId);
String codeVerifier = PkceUtils.generateCodeVerifier();
String codeChallenge = generateS256CodeChallenge(codeVerifier);
oauth.codeChallenge(codeChallenge);
oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256);
oauth.codeVerifier(codeVerifier);
}
private String generateNonce() {
return SecretGenerator.getInstance().randomString(16);
}
private void verifyClientSettings(ClientRepresentation clientRep, String clientAuthenticatorType) {
assertFalse(clientRep.isBearerOnly());
assertFalse(clientRep.isPublicClient());
assertEquals(clientAuthenticatorType, clientRep.getClientAuthenticatorType());
assertEquals(OAuth2Constants.PKCE_METHOD_S256, OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).getPkceCodeChallengeMethod());
assertTrue(OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).isUseMtlsHokToken());
assertFalse(clientRep.isImplicitFlowEnabled());
assertFalse(clientRep.isDirectAccessGrantsEnabled());
}
private void faiilUpdateRedirectUrisDynamically(String clientId, List<String> redirectUrisList) {
try {
updateClientDynamically(clientId, (OIDCClientRepresentation clientRep) ->
clientRep.setRedirectUris(redirectUrisList));
fail();
} catch (ClientRegistrationException e) {
assertEquals(ERR_MSG_CLIENT_REG_FAIL, e.getMessage());
}
}
private void successUpdateRedirectUrisByAdmin(String cId, List<String> redirectUrisList) {
try {
updateClientByAdmin(cId, (ClientRepresentation clientRep) -> {
clientRep.setAttributes(new HashMap<>());
clientRep.setRedirectUris(redirectUrisList);
});
ClientRepresentation cRep = getClientByAdmin(cId);
assertEquals(new HashSet<>(redirectUrisList), new HashSet<>(cRep.getRedirectUris()));
} catch (ClientPolicyException cpe) {
fail();
}
}
private void failAuthorizationRequest(String clientId, String redirectUri) {
oauth.clientId(clientId);
oauth.redirectUri(redirectUri);
oauth.openLoginForm();
assertTrue(errorPage.isCurrent());
}
}

View file

@ -203,6 +203,8 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest {
protected static final String FAPI2_SECURITY_PROFILE_NAME = "fapi-2-security-profile";
protected static final String FAPI2_MESSAGE_SIGNING_PROFILE_NAME = "fapi-2-message-signing";
protected static final String OAUTH2_1_CONFIDENTIAL_CLIENT_PROFILE_NAME = "oauth-2-1-for-confidential-client";
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";
@ -336,7 +338,7 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest {
ClientProfilesRepresentation actualProfilesRep = getProfilesWithGlobals();
// same profiles
assertExpectedProfiles(actualProfilesRep, Arrays.asList(FAPI1_BASELINE_PROFILE_NAME, FAPI1_ADVANCED_PROFILE_NAME, FAPI_CIBA_PROFILE_NAME, FAPI2_SECURITY_PROFILE_NAME, FAPI2_MESSAGE_SIGNING_PROFILE_NAME), Arrays.asList("ordinal-test-profile", "lack-of-builtin-field-test-profile"));
assertExpectedProfiles(actualProfilesRep, Arrays.asList(FAPI1_BASELINE_PROFILE_NAME, FAPI1_ADVANCED_PROFILE_NAME, FAPI_CIBA_PROFILE_NAME, FAPI2_SECURITY_PROFILE_NAME, FAPI2_MESSAGE_SIGNING_PROFILE_NAME, OAUTH2_1_CONFIDENTIAL_CLIENT_PROFILE_NAME), Arrays.asList("ordinal-test-profile", "lack-of-builtin-field-test-profile"));
// each profile - fapi-1-baseline
ClientProfileRepresentation actualProfileRep = getProfileRepresentation(actualProfilesRep, FAPI1_BASELINE_PROFILE_NAME, true);

View file

@ -84,7 +84,7 @@ public class ClientPoliciesLoadUpdateTest extends AbstractClientPoliciesTest {
ClientProfilesRepresentation actualProfilesRep = getProfilesWithGlobals();
// same profiles
assertExpectedProfiles(actualProfilesRep, Arrays.asList(FAPI1_BASELINE_PROFILE_NAME, FAPI1_ADVANCED_PROFILE_NAME, FAPI_CIBA_PROFILE_NAME, FAPI2_SECURITY_PROFILE_NAME, FAPI2_MESSAGE_SIGNING_PROFILE_NAME), Collections.emptyList());
assertExpectedProfiles(actualProfilesRep, Arrays.asList(FAPI1_BASELINE_PROFILE_NAME, FAPI1_ADVANCED_PROFILE_NAME, FAPI_CIBA_PROFILE_NAME, FAPI2_SECURITY_PROFILE_NAME, FAPI2_MESSAGE_SIGNING_PROFILE_NAME, OAUTH2_1_CONFIDENTIAL_CLIENT_PROFILE_NAME), Collections.emptyList());
// each profile - fapi-1-baseline
ClientProfileRepresentation actualProfileRep = getProfileRepresentation(actualProfilesRep, FAPI1_BASELINE_PROFILE_NAME, true);