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:
parent
5e34769ee0
commit
9ea679ff35
10 changed files with 389 additions and 14 deletions
|
@ -29,6 +29,8 @@ endif::[]
|
||||||
|
|
||||||
include::topics/oidc/fapi-support.adoc[]
|
include::topics/oidc/fapi-support.adoc[]
|
||||||
|
|
||||||
|
include::topics/oidc/oauth21-support.adoc[]
|
||||||
|
|
||||||
include::topics/oidc/recommendations.adoc[]
|
include::topics/oidc/recommendations.adoc[]
|
||||||
|
|
||||||
include::topics/saml/saml-overview.adoc[]
|
include::topics/saml/saml-overview.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.
|
|
@ -73,6 +73,7 @@ include::topics/threat/ssl.adoc[]
|
||||||
include::topics/threat/csrf.adoc[]
|
include::topics/threat/csrf.adoc[]
|
||||||
include::topics/threat/redirect.adoc[]
|
include::topics/threat/redirect.adoc[]
|
||||||
include::topics/threat/fapi-compliance.adoc[]
|
include::topics/threat/fapi-compliance.adoc[]
|
||||||
|
include::topics/threat/oauth21-compliance.adoc[]
|
||||||
include::topics/threat/compromised-tokens.adoc[]
|
include::topics/threat/compromised-tokens.adoc[]
|
||||||
include::topics/threat/compromised-codes.adoc[]
|
include::topics/threat/compromised-codes.adoc[]
|
||||||
include::topics/threat/open-redirect.adoc[]
|
include::topics/threat/open-redirect.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
|
* Setting policies on what configuration a client can have
|
||||||
* Validation of client configurations
|
* 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.
|
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
|
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.
|
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::
|
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],
|
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
|
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
|
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. The administrator can configure client profiles and client policies, so that {project_name} clients can be easily made compliant with various other
|
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.
|
security profiles like SPA, Native App, Open Banking and so on.
|
||||||
|
|
||||||
== Protocol
|
== 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
|
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
|
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)::
|
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
|
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
|
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.
|
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 secure <<_client-credentials,Client Authentication method>> is used for the client
|
||||||
* Enforce <<_mtls-client-certificate-bound-tokens,Holder-of-key tokens>> are used
|
* 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]]
|
[[_client_policy_profile]]
|
||||||
=== 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.
|
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 and FAPI 2 specifications.
|
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 section of the link:{adapterguide_link}#_fapi-support[{adapterguide_name}].
|
More details exist in the FAPI and OAuth 2.1 section of the link:{adapterguide_link}#_fapi-support[{adapterguide_name}].
|
||||||
|
|
||||||
[[_client_policy_policy]]
|
[[_client_policy_policy]]
|
||||||
=== Policy
|
=== Policy
|
||||||
|
|
|
@ -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}].
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -764,8 +764,10 @@ public class OAuthClient {
|
||||||
try (CloseableHttpClient client = httpClient.get()) {
|
try (CloseableHttpClient client = httpClient.get()) {
|
||||||
HttpPost post = new HttpPost(getServiceAccountUrl());
|
HttpPost post = new HttpPost(getServiceAccountUrl());
|
||||||
|
|
||||||
|
if (clientSecret != null) {
|
||||||
String authorization = BasicAuthHelper.RFC6749.createHeader(clientId, clientSecret);
|
String authorization = BasicAuthHelper.RFC6749.createHeader(clientId, clientSecret);
|
||||||
post.setHeader("Authorization", authorization);
|
post.setHeader("Authorization", authorization);
|
||||||
|
}
|
||||||
|
|
||||||
List<NameValuePair> parameters = new LinkedList<>();
|
List<NameValuePair> parameters = new LinkedList<>();
|
||||||
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
|
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -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_SECURITY_PROFILE_NAME = "fapi-2-security-profile";
|
||||||
protected static final String FAPI2_MESSAGE_SIGNING_PROFILE_NAME = "fapi-2-message-signing";
|
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_NONCE = "Missing parameter: nonce";
|
||||||
protected static final String ERR_MSG_MISSING_STATE = "Missing parameter: state";
|
protected static final String ERR_MSG_MISSING_STATE = "Missing parameter: state";
|
||||||
protected static final String ERR_MSG_CLIENT_REG_FAIL = "Failed to send request";
|
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();
|
ClientProfilesRepresentation actualProfilesRep = getProfilesWithGlobals();
|
||||||
|
|
||||||
// same profiles
|
// 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
|
// each profile - fapi-1-baseline
|
||||||
ClientProfileRepresentation actualProfileRep = getProfileRepresentation(actualProfilesRep, FAPI1_BASELINE_PROFILE_NAME, true);
|
ClientProfileRepresentation actualProfileRep = getProfileRepresentation(actualProfilesRep, FAPI1_BASELINE_PROFILE_NAME, true);
|
||||||
|
|
|
@ -84,7 +84,7 @@ public class ClientPoliciesLoadUpdateTest extends AbstractClientPoliciesTest {
|
||||||
ClientProfilesRepresentation actualProfilesRep = getProfilesWithGlobals();
|
ClientProfilesRepresentation actualProfilesRep = getProfilesWithGlobals();
|
||||||
|
|
||||||
// same profiles
|
// 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
|
// each profile - fapi-1-baseline
|
||||||
ClientProfileRepresentation actualProfileRep = getProfileRepresentation(actualProfilesRep, FAPI1_BASELINE_PROFILE_NAME, true);
|
ClientProfileRepresentation actualProfileRep = getProfileRepresentation(actualProfilesRep, FAPI1_BASELINE_PROFILE_NAME, true);
|
||||||
|
|
Loading…
Reference in a new issue