From 9ea679ff35f90c36712b37a9b9eec6db93eaa9fe Mon Sep 17 00:00:00 2001 From: Takashi Norimatsu Date: Wed, 6 Dec 2023 09:41:24 +0900 Subject: [PATCH] Supporting OAuth 2.1 for confidential clients closes #25314 Co-authored-by: shigeyuki kabano Signed-off-by: Takashi Norimatsu --- docs/documentation/securing_apps/topics.adoc | 2 + .../topics/oidc/oauth21-support.adoc | 17 + docs/documentation/server_admin/topics.adoc | 1 + .../topics/clients/client-policies.adoc | 20 +- .../topics/threat/oauth21-compliance.adoc | 5 + .../keycloak-default-client-profiles.json | 52 ++++ .../keycloak/testsuite/util/OAuthClient.java | 6 +- .../OAuth2_1ConfidentialClientTest.java | 294 ++++++++++++++++++ .../policies/AbstractClientPoliciesTest.java | 4 +- .../ClientPoliciesLoadUpdateTest.java | 2 +- 10 files changed, 389 insertions(+), 14 deletions(-) create mode 100644 docs/documentation/securing_apps/topics/oidc/oauth21-support.adoc create mode 100644 docs/documentation/server_admin/topics/threat/oauth21-compliance.adoc create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OAuth2_1ConfidentialClientTest.java diff --git a/docs/documentation/securing_apps/topics.adoc b/docs/documentation/securing_apps/topics.adoc index 19b68ac414..925698df02 100644 --- a/docs/documentation/securing_apps/topics.adoc +++ b/docs/documentation/securing_apps/topics.adoc @@ -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[] diff --git a/docs/documentation/securing_apps/topics/oidc/oauth21-support.adoc b/docs/documentation/securing_apps/topics/oidc/oauth21-support.adoc new file mode 100644 index 0000000000..77fac68451 --- /dev/null +++ b/docs/documentation/securing_apps/topics/oidc/oauth21-support.adoc @@ -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. diff --git a/docs/documentation/server_admin/topics.adoc b/docs/documentation/server_admin/topics.adoc index 6995a567fb..fcef802a05 100644 --- a/docs/documentation/server_admin/topics.adoc +++ b/docs/documentation/server_admin/topics.adoc @@ -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[] diff --git a/docs/documentation/server_admin/topics/clients/client-policies.adoc b/docs/documentation/server_admin/topics/clients/client-policies.adoc index 0624aa350a..2227a54301 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) +* 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 diff --git a/docs/documentation/server_admin/topics/threat/oauth21-compliance.adoc b/docs/documentation/server_admin/topics/threat/oauth21-compliance.adoc new file mode 100644 index 0000000000..3dd25f4bec --- /dev/null +++ b/docs/documentation/server_admin/topics/threat/oauth21-compliance.adoc @@ -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}]. \ No newline at end of file diff --git a/services/src/main/resources/keycloak-default-client-profiles.json b/services/src/main/resources/keycloak-default-client-profiles.json index 23cfbd6218..171300972f 100644 --- a/services/src/main/resources/keycloak-default-client-profiles.json +++ b/services/src/main/resources/keycloak-default-client-profiles.json @@ -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" + } + } + ] } ] } \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java index bc8a73a838..501cd2414f 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java @@ -764,8 +764,10 @@ public class OAuthClient { try (CloseableHttpClient client = httpClient.get()) { HttpPost post = new HttpPost(getServiceAccountUrl()); - String authorization = BasicAuthHelper.RFC6749.createHeader(clientId, clientSecret); - post.setHeader("Authorization", authorization); + if (clientSecret != null) { + String authorization = BasicAuthHelper.RFC6749.createHeader(clientId, clientSecret); + post.setHeader("Authorization", authorization); + } List parameters = new LinkedList<>(); parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OAuth2_1ConfidentialClientTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OAuth2_1ConfidentialClientTest.java new file mode 100644 index 0000000000..bf4978054f --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OAuth2_1ConfidentialClientTest.java @@ -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 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 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()); + } + + +} 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 6364e16659..5299304350 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 @@ -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); 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 e47dcc5543..fc5b985fe6 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), 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);