From 1e12b15890e02fbc7b73a490b7509812fb5e38b7 Mon Sep 17 00:00:00 2001 From: Takashi Norimatsu Date: Tue, 19 Dec 2023 04:47:06 +0900 Subject: [PATCH] Supporting OAuth 2.1 for public clients closes #25316 Co-authored-by: shigeyuki kabano Signed-off-by: Takashi Norimatsu --- .../topics/oidc/oauth21-support.adoc | 4 +- .../topics/clients/client-policies.adoc | 2 +- .../topics/threat/oauth21-compliance.adoc | 2 +- .../executor/DPoPBindEnforcerExecutor.java | 7 +- .../DPoPBindEnforcerExecutorFactory.java | 7 +- .../keycloak-default-client-profiles.json | 40 ++- .../client/OAuth2_1PublicClientTest.java | 280 ++++++++++++++++++ .../policies/AbstractClientPoliciesTest.java | 4 +- .../ClientPoliciesLoadUpdateTest.java | 2 +- 9 files changed, 334 insertions(+), 14 deletions(-) create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OAuth2_1PublicClientTest.java diff --git a/docs/documentation/securing_apps/topics/oidc/oauth21-support.adoc b/docs/documentation/securing_apps/topics/oidc/oauth21-support.adoc index 77fac68451..7cbafa046c 100644 --- a/docs/documentation/securing_apps/topics/oidc/oauth21-support.adoc +++ b/docs/documentation/securing_apps/topics/oidc/oauth21-support.adoc @@ -12,6 +12,8 @@ side may need to be still done manually or through some other third-party soluti ==== 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. +and link them to the global client profiles for OAuth 2.1 support, which are automatically available in each realm. You can use either `oauth-2-1-for-confidential-client` profile for confidential clients or `oauth-2-1-for-public-client` profile for public 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. + +NOTE: When using OAuth 2.1 profile for public clients, it is recommended to use DPoP preview feature as described in the link:{adminguide_link}#_dpop-bound-tokens[{adminguide_name}] because DPoP binds an access token and a refresh token together with the public part of a client's key pair. This binding prevents an attacker from using stolen tokens. \ No newline at end of file diff --git a/docs/documentation/server_admin/topics/clients/client-policies.adoc b/docs/documentation/server_admin/topics/clients/client-policies.adoc index 2227a54301..6ace0240ed 100644 --- a/docs/documentation/server_admin/topics/clients/client-policies.adoc +++ b/docs/documentation/server_admin/topics/clients/client-policies.adoc @@ -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)and OAuth 2.1 +* 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. diff --git a/docs/documentation/server_admin/topics/threat/oauth21-compliance.adoc b/docs/documentation/server_admin/topics/threat/oauth21-compliance.adoc index 3dd25f4bec..4fd27ec6c6 100644 --- a/docs/documentation/server_admin/topics/threat/oauth21-compliance.adoc +++ b/docs/documentation/server_admin/topics/threat/oauth21-compliance.adoc @@ -2,4 +2,4 @@ === 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}]. \ No newline at end of file +for the OAuth 2.1 support. Details are described in the OAuth 2.1 section of link:{adapterguide_link}#_oauth21-support[{adapterguide_name}]. diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/executor/DPoPBindEnforcerExecutor.java b/services/src/main/java/org/keycloak/services/clientpolicy/executor/DPoPBindEnforcerExecutor.java index 4cc5522811..a3a4db5f78 100644 --- a/services/src/main/java/org/keycloak/services/clientpolicy/executor/DPoPBindEnforcerExecutor.java +++ b/services/src/main/java/org/keycloak/services/clientpolicy/executor/DPoPBindEnforcerExecutor.java @@ -85,6 +85,12 @@ public class DPoPBindEnforcerExecutor implements ClientPolicyExecutorProvider getConfigProperties() { return Collections.singletonList(AUTO_CONFIGURE_PROPERTY); } - - @Override - public boolean isSupported() { - return Profile.isFeatureEnabled(Feature.DPOP); - } } diff --git a/services/src/main/resources/keycloak-default-client-profiles.json b/services/src/main/resources/keycloak-default-client-profiles.json index 171300972f..b0fec76931 100644 --- a/services/src/main/resources/keycloak-default-client-profiles.json +++ b/services/src/main/resources/keycloak-default-client-profiles.json @@ -300,7 +300,7 @@ "client-jwt", "client-x509" ], - "default-client-authenticator": "client-jwt" + "default-client-authenticator": "client-jwt" } }, { @@ -336,6 +336,44 @@ } } ] + }, + { + "name": "oauth-2-1-for-public-client", + "description": "Client profile, which enforce public clients to conform 'OAuth 2.1' specification.", + "executors": [ + { + "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": "dpop-bind-enforcer", + "configuration": { + "auto-configure": "true" + } + }, + { + "executor": "reject-implicit-grant", + "configuration": { + "auto-configure": "true" + } + }, + { + "executor": "reject-ropc-grant", + "configuration": { + "auto-configure": "true" + } + } + ] } ] } \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OAuth2_1PublicClientTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OAuth2_1PublicClientTest.java new file mode 100644 index 0000000000..fe3863a38f --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OAuth2_1PublicClientTest.java @@ -0,0 +1,280 @@ +/* + * 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 jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.core.Response; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.keycloak.OAuth2Constants; +import org.keycloak.OAuthErrorException; +import org.keycloak.client.registration.ClientRegistrationException; +import org.keycloak.common.Profile; +import org.keycloak.common.util.SecretGenerator; +import org.keycloak.common.util.Time; +import org.keycloak.crypto.Algorithm; +import org.keycloak.jose.jwk.JWK; +import org.keycloak.jose.jws.JWSHeader; +import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; +import org.keycloak.protocol.oidc.utils.OIDCResponseType; +import org.keycloak.protocol.oidc.utils.PkceUtils; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.oidc.OIDCClientRepresentation; +import org.keycloak.representations.oidc.TokenMetadataRepresentation; +import org.keycloak.services.clientpolicy.ClientPolicyException; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.services.clientpolicy.condition.AnyClientConditionFactory; +import org.keycloak.testsuite.arquillian.annotation.EnableFeature; +import org.keycloak.testsuite.util.ClientPoliciesUtil; +import org.keycloak.testsuite.util.Matchers; +import org.keycloak.testsuite.util.OAuthClient; +import org.keycloak.util.JsonSerialization; + +import java.security.KeyPair; +import java.util.List; +import java.util.UUID; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.keycloak.testsuite.util.ClientPoliciesUtil.createAnyClientConditionConfig; +import static org.keycloak.testsuite.util.ClientPoliciesUtil.createEcJwk; +import static org.keycloak.testsuite.util.ClientPoliciesUtil.generateEcdsaKey; +import static org.keycloak.testsuite.util.ClientPoliciesUtil.generateSignedDPoPProof; + +@EnableFeature(value = Profile.Feature.DPOP, skipRestart = true) +public class OAuth2_1PublicClientTest extends AbstractFAPITest { + + private static final String OAUTH2_1_PUBLIC_CLIENT_PROFILE_NAME = "oauth-2-1-for-public-client"; + + private static final String DPOP_JWT_HEADER_TYPE = "dpop+jwt"; + + private KeyPair ecKeyPair; + + private JWK jwkEc; + + @Before + public void beforeDPoPTest() throws Exception { + ecKeyPair = generateEcdsaKey("secp256r1"); + jwkEc = createEcJwk(ecKeyPair.getPublic()); + } + + @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); + + createClientByAdmin(clientId, (ClientRepresentation clientRep) -> { + clientRep.setStandardFlowEnabled(Boolean.TRUE); + clientRep.setImplicitFlowEnabled(Boolean.TRUE); + clientRep.setPublicClient(Boolean.TRUE); + }); + + // setup profiles and policies + setupPolicyOAuth2_1PublicClientForAllClient(); + + 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); + + createClientByAdmin(clientId, (ClientRepresentation clientRep) -> { + clientRep.setStandardFlowEnabled(Boolean.TRUE); + clientRep.setPublicClient(Boolean.TRUE); + }); + + // setup profiles and policies + setupPolicyOAuth2_1PublicClientForAllClient(); + + oauth.clientId(clientId); + OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest(null, TEST_USER_NAME, TEST_USER_PASSWORD, null); + + assertEquals(400, response.getStatusCode()); + assertEquals(OAuthErrorException.INVALID_GRANT, response.getError()); + assertEquals("resource owner password credentials grant is prohibited.", response.getErrorDescription()); + } + + @Test + public void testOAuth2_1ProofKeyForCodeExchange() throws Exception { + // setup profiles and policies + setupPolicyOAuth2_1PublicClientForAllClient(); + + String clientId = generateSuffixedName(CLIENT_NAME); + String cId = createClientByAdmin(clientId, (ClientRepresentation clientRep) -> { + clientRep.setPublicClient(Boolean.TRUE); + clientRep.setRedirectUris(List.of(AssertEvents.DEFAULT_REDIRECT_URI)); + }); + assertEquals(OAuth2Constants.PKCE_METHOD_S256, OIDCAdvancedConfigWrapper.fromClientRepresentation(getClientByAdmin(cId)).getPkceCodeChallengeMethod()); + + failLoginByNotFollowingPKCE(clientId); + } + + @Test + public void testOAuth2_1RedirectUris() throws Exception { + // setup profiles and policies + setupPolicyOAuth2_1PublicClientForAllClient(); + + // registration with invalid redirect_uri - fail + try { + createClientDynamically(generateSuffixedName(CLIENT_NAME), (OIDCClientRepresentation clientRep) -> + clientRep.setRedirectUris(List.of("http://example.com/app"))); + } catch (ClientRegistrationException cre) { + assertEquals(ERR_MSG_CLIENT_REG_FAIL, cre.getMessage()); + } + + // registration with valid redirect_uri- success + String clientId = generateSuffixedName(CLIENT_NAME); + String cId = createClientByAdmin(clientId, (ClientRepresentation clientRep) -> { + clientRep.setPublicClient(Boolean.TRUE); + clientRep.setRedirectUris(List.of(AssertEvents.DEFAULT_REDIRECT_URI)); + }); + assertEquals(AssertEvents.DEFAULT_REDIRECT_URI, getClientByAdmin(cId).getRedirectUris().get(0)); + + // update with valid redirect_uri - fail + try { + createClientDynamically(clientId, (OIDCClientRepresentation clientRep) -> + clientRep.setRedirectUris(List.of("https://localhost/app"))); + } catch (ClientRegistrationException cre) { + assertEquals(ERR_MSG_CLIENT_REG_FAIL, cre.getMessage()); + } + + // authorization with invalid redirect_uri request - fail + setValidPkce(clientId); + oauth.redirectUri("https://localhost/app"); + oauth.openLoginForm(); + assertTrue(errorPage.isCurrent()); + } + + @Test + public void testOAuth2_1DPoPSenderConstrainedToken() throws Exception { + // setup profiles and policies + setupPolicyOAuth2_1PublicClientForAllClient(); + + // registration (auto-config) - success + String clientId = generateSuffixedName(CLIENT_NAME); + String cId = createClientByAdmin(clientId, (ClientRepresentation clientRep) -> { + clientRep.setPublicClient(Boolean.TRUE); + clientRep.setRedirectUris(List.of(AssertEvents.DEFAULT_REDIRECT_URI)); + }); + assertTrue(OIDCAdvancedConfigWrapper.fromClientRepresentation(getClientByAdmin(cId)).isUseDPoP()); + + // authorization request - success + setValidPkce(clientId); + oauth.clientId(clientId); + oauth.doLogin(TEST_USER_NAME, TEST_USER_PASSWORD); + + // token request with DPoP Proof - success + JWSHeader jwsEcHeader = new JWSHeader(org.keycloak.jose.jws.Algorithm.ES256, DPOP_JWT_HEADER_TYPE, jwkEc.getKeyId(), jwkEc); + String dpopProofEcEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.POST, oauth.getAccessTokenUrl(), (long) Time.currentTime(), Algorithm.ES256, jwsEcHeader, ecKeyPair.getPrivate()); + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + oauth.dpopProof(dpopProofEcEncoded); + OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, null); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatusCode()); + oauth.verifyToken(response.getAccessToken()); + + // token refresh request with DPoP Proof - success + dpopProofEcEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.POST, oauth.getAccessTokenUrl(), (long) Time.currentTime(), Algorithm.ES256, jwsEcHeader, ecKeyPair.getPrivate()); + oauth.dpopProof(dpopProofEcEncoded); + response = oauth.doRefreshTokenRequest(response.getRefreshToken(), null); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatusCode()); + + // userinfo request with DPoP Proof - success + dpopProofEcEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.GET, oauth.getUserInfoUrl(), (long) Time.currentTime(), Algorithm.ES256, jwsEcHeader, ecKeyPair.getPrivate()); + oauth.dpopProof(dpopProofEcEncoded); + OAuthClient.UserInfoResponse userInfoResponse = oauth.doUserInfoRequestByGet(response.getAccessToken()); + assertEquals(TEST_USER_NAME, userInfoResponse.getUserInfo().getPreferredUsername()); + + oauth.idTokenHint(response.getIdToken()).openLogout(); + + // revoke token with a valid DPoP proof - success + dpopProofEcEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.POST, oauth.getTokenRevocationUrl(), (long) Time.currentTime(), Algorithm.ES256, jwsEcHeader, ecKeyPair.getPrivate()); + oauth.dpopProof(dpopProofEcEncoded); + CloseableHttpResponse closableHttpResponse = oauth.doTokenRevoke(response.getAccessToken(), "access_token", null); + assertThat(closableHttpResponse, Matchers.statusCodeIsHC(Response.Status.OK)); + String introspectionResponse = oauth.introspectAccessTokenWithClientCredential(clientId, null, response.getAccessToken()); + TokenMetadataRepresentation tokenMetadataRepresentation = JsonSerialization.readValue(introspectionResponse, TokenMetadataRepresentation.class); + assertFalse(tokenMetadataRepresentation.isActive()); + + oauth.idTokenHint(response.getIdToken()).openLogout(); + } + + private void setupPolicyOAuth2_1PublicClientForAllClient() throws Exception { + String json = (new ClientPoliciesUtil.ClientPoliciesBuilder()).addPolicy( + (new ClientPoliciesUtil.ClientPolicyBuilder()).createPolicy("MyPolicy", "Policy for enable OAuth 2.1 public client profile for all clients", Boolean.TRUE) + .addCondition(AnyClientConditionFactory.PROVIDER_ID, + createAnyClientConditionConfig()) + .addProfile(OAUTH2_1_PUBLIC_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); + } +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/policies/AbstractClientPoliciesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/policies/AbstractClientPoliciesTest.java index 5299304350..7de341ac2e 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/policies/AbstractClientPoliciesTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/policies/AbstractClientPoliciesTest.java @@ -202,8 +202,8 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest { protected static final String FAPI_CIBA_PROFILE_NAME = "fapi-ciba"; 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 OAUTH2_1_PUBLIC_CLIENT_PROFILE_NAME = "oauth-2-1-for-public-client"; protected static final String ERR_MSG_MISSING_NONCE = "Missing parameter: nonce"; protected static final String ERR_MSG_MISSING_STATE = "Missing parameter: state"; @@ -338,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, OAUTH2_1_CONFIDENTIAL_CLIENT_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, OAUTH2_1_PUBLIC_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); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/policies/ClientPoliciesLoadUpdateTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/policies/ClientPoliciesLoadUpdateTest.java index fc5b985fe6..fc943142b4 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/policies/ClientPoliciesLoadUpdateTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/policies/ClientPoliciesLoadUpdateTest.java @@ -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, OAUTH2_1_CONFIDENTIAL_CLIENT_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, OAUTH2_1_PUBLIC_CLIENT_PROFILE_NAME), Collections.emptyList()); // each profile - fapi-1-baseline ClientProfileRepresentation actualProfileRep = getProfileRepresentation(actualProfilesRep, FAPI1_BASELINE_PROFILE_NAME, true);