Add FAPI 2.0 + DPoP security profile as default profile of client policies

closes #35441

Signed-off-by: Takashi Norimatsu <takashi.norimatsu.ws@hitachi.com>
This commit is contained in:
Takashi Norimatsu 2025-03-12 17:16:04 +09:00 committed by Marek Posolda
parent 788e981917
commit f00cd980c4
7 changed files with 887 additions and 2 deletions

View File

@ -74,6 +74,7 @@ test.describe("Realm settings client profiles tab tests", () => {
);
test.beforeEach(async ({ page }) => {
await searchClientProfile(page, editedProfileName);
await clickTableRowItem(page, editedProfileName);
});

View File

@ -377,6 +377,154 @@
}
]
},
{
"name": "fapi-2-dpop-security-profile",
"description": "Client profile, which enforce clients to conform 'FAPI 2.0 Security Profile' with DPoP 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-client-uris",
"configuration": {}
},
{
"executor": "secure-signature-algorithm",
"configuration": {
"default-algorithm": "PS256"
}
},
{
"executor": "secure-signature-algorithm-signed-jwt",
"configuration": {
"require-client-assertion": false
}
},
{
"executor": "consent-required",
"configuration": {
"auto-configure": true
}
},
{
"executor": "full-scope-disabled",
"configuration": {
"auto-configure": true
}
},
{
"executor": "reject-implicit-grant",
"configuration": {
"auto-configure": "true"
}
},
{
"executor": "pkce-enforcer",
"configuration": {
"auto-configure": "true"
}
},
{
"executor": "secure-par-content",
"configuration": {}
},
{
"executor": "dpop-bind-enforcer",
"configuration": {
"auto-configure": "true"
}
}
]
},
{
"name": "fapi-2-dpop-message-signing",
"description": "Client profile, which enforce clients to conform 'FAPI 2.0 Message Signing' with DPoP 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-client-uris",
"configuration": {}
},
{
"executor": "secure-signature-algorithm",
"configuration": {
"default-algorithm": "PS256"
}
},
{
"executor": "secure-signature-algorithm-signed-jwt",
"configuration": {
"require-client-assertion": false
}
},
{
"executor": "consent-required",
"configuration": {
"auto-configure": true
}
},
{
"executor": "full-scope-disabled",
"configuration": {
"auto-configure": true
}
},
{
"executor": "reject-implicit-grant",
"configuration": {
"auto-configure": "true"
}
},
{
"executor": "pkce-enforcer",
"configuration": {
"auto-configure": "true"
}
},
{
"executor" : "secure-par-content",
"configuration" : {}
},
{
"executor": "secure-request-object",
"configuration": {
"verify-nbf": true,
"available-period": "3600",
"encryption-required": false
}
},
{
"executor": "dpop-bind-enforcer",
"configuration": {
"auto-configure": "true"
}
}
]
},
{
"name": "saml-security-profile",
"description": "Client profile that enforces SAML clients to be secure.",

View File

@ -20,6 +20,13 @@ public class AccessTokenRequest extends AbstractHttpPostRequest<AccessTokenReque
return client.getEndpoints().getToken();
}
public AccessTokenRequest signedJwt(String signedJwt) {
parameter(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT);
parameter(OAuth2Constants.CLIENT_ASSERTION, signedJwt);
return this;
}
public AccessTokenRequest codeVerifier(PkceGenerator pkceGenerator) {
if (pkceGenerator != null) {
codeVerifier(pkceGenerator.getCodeVerifier());

View File

@ -0,0 +1,224 @@
/*
* 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.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.JWTClientSecretAuthenticator;
import org.keycloak.authentication.authenticators.client.X509ClientAuthenticator;
import org.keycloak.client.registration.ClientRegistrationException;
import org.keycloak.crypto.Algorithm;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
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.util.ClientPoliciesUtil;
import java.util.Collections;
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;
/**
* Test for the FAPI 2 specifications (still implementer's draft):
* <a href="https://openid.bitbucket.io/fapi/fapi-2_0-security-profile.html">FAPI 2.0 Security Profile</a>
* <a href="https://openid.bitbucket.io/fapi/fapi-2_0-message-signing.html">FAPI 2.0 Message Signing</a>
* Mostly tests the global FAPI policies work as expected
*
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
public abstract class AbstractFAPI2Test extends AbstractFAPITest {
protected static final String clientId = "foo";
protected void testFAPI2ClientRegistration(String profile) throws Exception {
setupPolicyFAPI2ForAllClient(profile);
// Register client with clientIdAndSecret - should fail
try {
createClientByAdmin("invalid", (ClientRepresentation clientRep) -> clientRep.setClientAuthenticatorType(ClientIdAndSecretAuthenticator.PROVIDER_ID));
fail();
} catch (ClientPolicyException e) {
assertEquals(OAuthErrorException.INVALID_CLIENT_METADATA, e.getMessage());
}
// Register client with signedJWT - should fail
try {
createClientByAdmin("invalid", (ClientRepresentation clientRep) -> clientRep.setClientAuthenticatorType(JWTClientSecretAuthenticator.PROVIDER_ID));
fail();
} catch (ClientPolicyException e) {
assertEquals(OAuthErrorException.INVALID_CLIENT_METADATA, e.getMessage());
}
// Register client with privateKeyJWT, but unsecured redirectUri - should fail
try {
createClientByAdmin("invalid", (ClientRepresentation clientRep) -> {
clientRep.setClientAuthenticatorType(JWTClientAuthenticator.PROVIDER_ID);
clientRep.setRedirectUris(Collections.singletonList("http://foo"));
});
fail();
} catch (ClientPolicyException e) {
assertEquals(OAuthErrorException.INVALID_CLIENT_METADATA, e.getMessage());
}
// Try to register client with "client-jwt" - should pass
String clientUUID = createClientByAdmin("client-jwt", (ClientRepresentation clientRep) -> clientRep.setClientAuthenticatorType(JWTClientAuthenticator.PROVIDER_ID));
ClientRepresentation client = getClientByAdmin(clientUUID);
assertEquals(JWTClientAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
// Try to register client with "client-x509" - should pass
clientUUID = createClientByAdmin("client-x509", (ClientRepresentation clientRep) -> clientRep.setClientAuthenticatorType(X509ClientAuthenticator.PROVIDER_ID));
client = getClientByAdmin(clientUUID);
assertEquals(X509ClientAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
// Try to register client with default authenticator - should pass. Client authenticator should be "client-jwt"
clientUUID = createClientByAdmin("client-jwt-2", (ClientRepresentation clientRep) -> {
});
client = getClientByAdmin(clientUUID);
assertEquals(JWTClientAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
// Check the Consent is enabled, Holder-of-key is enabled, fullScopeAllowed disabled and default signature algorithm.
assertTrue(client.isConsentRequired());
OIDCAdvancedConfigWrapper clientConfig = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
assertEquals(Algorithm.PS256, clientConfig.getIdTokenSignedResponseAlg());
assertEquals(Algorithm.PS256, clientConfig.getRequestObjectSignatureAlg());
assertFalse(client.isFullScopeAllowed());
switch (profile) {
case FAPI2_SECURITY_PROFILE_NAME:
case FAPI2_MESSAGE_SIGNING_PROFILE_NAME:
assertTrue(clientConfig.isUseMtlsHokToken());
assertFalse(clientConfig.isUseDPoP());
break;
case FAPI2_DPOP_SECURITY_PROFILE_NAME:
case FAPI2_DPOP_MESSAGE_SIGNING_PROFILE_NAME:
assertFalse(clientConfig.isUseMtlsHokToken());
assertTrue(clientConfig.isUseDPoP());
break;
default:
throw new IllegalStateException("Unexpected value: " + profile);
}
}
protected void testFAPI2OIDCClientRegistration(String profile) throws Exception {
setupPolicyFAPI2ForAllClient(profile);
// Try to register client with clientIdAndSecret - should fail
try {
createClientDynamically(generateSuffixedName(clientId), (OIDCClientRepresentation clientRep) -> clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.CLIENT_SECRET_BASIC));
fail();
} catch (ClientRegistrationException e) {
assertEquals(ERR_MSG_CLIENT_REG_FAIL, e.getMessage());
}
// Try to register client with "client-jwt" - should pass
String clientUUID = createClientDynamically("client-jwt", (OIDCClientRepresentation clientRep) -> {
clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.PRIVATE_KEY_JWT);
clientRep.setJwksUri("https://foo");
});
ClientRepresentation client = getClientByAdmin(clientUUID);
assertEquals(JWTClientAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
assertFalse(client.isFullScopeAllowed());
// Set new initialToken for register new clients
setInitialAccessTokenForDynamicClientRegistration();
// Try to register client with "client-x509" - should pass
clientUUID = createClientDynamically("client-x509", (OIDCClientRepresentation clientRep) -> clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.TLS_CLIENT_AUTH));
client = getClientByAdmin(clientUUID);
assertEquals(X509ClientAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
// Check the Consent is enabled, PKCS set to S256
OIDCAdvancedConfigWrapper clientConfig = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
assertTrue(client.isConsentRequired());
assertEquals(OAuth2Constants.PKCE_METHOD_S256, clientConfig.getPkceCodeChallengeMethod());
// Check Holder-of-key is enabled
switch (profile) {
case FAPI2_SECURITY_PROFILE_NAME:
case FAPI2_MESSAGE_SIGNING_PROFILE_NAME:
assertTrue(clientConfig.isUseMtlsHokToken());
assertFalse(clientConfig.isUseDPoP());
break;
case FAPI2_DPOP_SECURITY_PROFILE_NAME:
case FAPI2_DPOP_MESSAGE_SIGNING_PROFILE_NAME:
assertFalse(clientConfig.isUseMtlsHokToken());
assertTrue(clientConfig.isUseDPoP());
break;
default:
throw new IllegalStateException("Unexpected value: " + profile);
}
}
protected void testFAPI2SignatureAlgorithms(String profile) throws Exception {
setupPolicyFAPI2ForAllClient(profile);
// Test that unsecured algorithm (RS256) is not possible
try {
createClientByAdmin("invalid", (ClientRepresentation clientRep) -> {
clientRep.setClientAuthenticatorType(JWTClientAuthenticator.PROVIDER_ID);
OIDCAdvancedConfigWrapper clientConfig = OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep);
clientConfig.setIdTokenSignedResponseAlg(Algorithm.RS256);
});
fail();
} catch (ClientPolicyException e) {
assertEquals(OAuthErrorException.INVALID_REQUEST, e.getMessage());
}
// Test that secured algorithm is possible to explicitly set
String clientUUID = createClientByAdmin("client-jwt", (ClientRepresentation clientRep) -> {
clientRep.setClientAuthenticatorType(JWTClientAuthenticator.PROVIDER_ID);
OIDCAdvancedConfigWrapper clientCfg = OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep);
clientCfg.setIdTokenSignedResponseAlg(Algorithm.ES256);
});
ClientRepresentation client = getClientByAdmin(clientUUID);
OIDCAdvancedConfigWrapper clientConfig = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
assertEquals(Algorithm.ES256, clientConfig.getIdTokenSignedResponseAlg());
assertEquals(Algorithm.PS256, clientConfig.getRequestObjectSignatureAlg());
// Test default algorithms set everywhere
clientUUID = createClientByAdmin("client-jwt-default-alg", (ClientRepresentation clientRep) -> clientRep.setClientAuthenticatorType(JWTClientAuthenticator.PROVIDER_ID));
client = getClientByAdmin(clientUUID);
clientConfig = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
assertEquals(Algorithm.PS256, clientConfig.getIdTokenSignedResponseAlg());
assertEquals(Algorithm.PS256, clientConfig.getRequestObjectSignatureAlg());
assertEquals(Algorithm.PS256, clientConfig.getUserInfoSignedResponseAlg());
assertEquals(Algorithm.PS256, clientConfig.getTokenEndpointAuthSigningAlg());
assertEquals(Algorithm.PS256, client.getAttributes().get(OIDCConfigAttributes.ACCESS_TOKEN_SIGNED_RESPONSE_ALG));
}
protected void setupPolicyFAPI2ForAllClient(String profile) throws Exception {
String json = (new ClientPoliciesUtil.ClientPoliciesBuilder()).addPolicy(
(new ClientPoliciesUtil.ClientPolicyBuilder()).createPolicy("MyPolicy", "Policy for enable FAPI 2.0 Security Profile for all clients", Boolean.TRUE)
.addCondition(AnyClientConditionFactory.PROVIDER_ID,
createAnyClientConditionConfig())
.addProfile(profile)
.toRepresentation()
).toString();
updatePolicies(json);
}
}

View File

@ -0,0 +1,503 @@
/*
* 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 org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
import org.keycloak.authentication.authenticators.client.X509ClientAuthenticator;
import org.keycloak.common.Profile;
import org.keycloak.common.util.KeyUtils;
import org.keycloak.common.util.Time;
import org.keycloak.crypto.Algorithm;
import org.keycloak.jose.jwk.ECPublicJWK;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.RSAPublicJWK;
import org.keycloak.jose.jws.JWSHeader;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.RefreshToken;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls;
import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource;
import org.keycloak.testsuite.util.MutualTLSUtils;
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
import org.keycloak.testsuite.util.oauth.ParResponse;
import org.keycloak.testsuite.util.oauth.PkceGenerator;
import org.keycloak.util.JWKSUtils;
import java.security.KeyPair;
import java.util.Collections;
import java.util.Random;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createEcJwk;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createRsaJwk;
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 FAPI2DPoPTest extends AbstractFAPI2Test {
private static final String REALM_NAME = "test";
private static final String DPOP_JWT_HEADER_TYPE = "dpop+jwt";
private static final String nonce = "123456"; // need to be 123456.
@Rule
public AssertEvents events = new AssertEvents(this);
private KeyPair ecKeyPair;
private KeyPair rsaKeyPair;
private JWSHeader jwsRsaHeader;
private JWSHeader jwsEcHeader;
private String jktRsa;
private String jktEc;
@Before
public void beforeDPoPTest() throws Exception {
rsaKeyPair = KeyUtils.generateRsaKeyPair(2048);
JWK jwkRsa = createRsaJwk(rsaKeyPair.getPublic());
jwkRsa.getOtherClaims().put(RSAPublicJWK.MODULUS, ((RSAPublicJWK) jwkRsa).getModulus());
jwkRsa.getOtherClaims().put(RSAPublicJWK.PUBLIC_EXPONENT, ((RSAPublicJWK) jwkRsa).getPublicExponent());
jktRsa = JWKSUtils.computeThumbprint(jwkRsa);
jwsRsaHeader = new JWSHeader(org.keycloak.jose.jws.Algorithm.PS256, DPOP_JWT_HEADER_TYPE, jwkRsa.getKeyId(), jwkRsa);
ecKeyPair = generateEcdsaKey("secp256r1");
JWK jwkEc = createEcJwk(ecKeyPair.getPublic());
jwkEc.getOtherClaims().put(ECPublicJWK.CRV, ((ECPublicJWK) jwkEc).getCrv());
jwkEc.getOtherClaims().put(ECPublicJWK.X, ((ECPublicJWK) jwkEc).getX());
jwkEc.getOtherClaims().put(ECPublicJWK.Y, ((ECPublicJWK) jwkEc).getY());
jktEc = JWKSUtils.computeThumbprint(jwkEc);
jwsEcHeader = new JWSHeader(org.keycloak.jose.jws.Algorithm.ES256, DPOP_JWT_HEADER_TYPE, jwkEc.getKeyId(), jwkEc);
}
private final Random rand = new Random(System.currentTimeMillis());
@Test
public void testFAPI2DPoPSecurityProfileClientRegistration() throws Exception {
testFAPI2ClientRegistration(FAPI2_DPOP_SECURITY_PROFILE_NAME);
}
@Test
public void testFAPI2DPoPSecurityProfileOIDCClientRegistration() throws Exception {
testFAPI2OIDCClientRegistration(FAPI2_DPOP_SECURITY_PROFILE_NAME);
}
@Test
public void testFAPI2DPoPSecurityProfileSignatureAlgorithms() throws Exception {
testFAPI2SignatureAlgorithms(FAPI2_DPOP_SECURITY_PROFILE_NAME);
}
@Test
public void testFAPI2DPoPSecurityProfileLoginWithPrivateKeyJWT() throws Exception {
// setup client policy
setupPolicyFAPI2ForAllClient(FAPI2_DPOP_SECURITY_PROFILE_NAME);
// Register client with private-key-jwt
String clientUUID = createClientByAdmin(clientId, (ClientRepresentation clientRep) -> {
clientRep.setClientAuthenticatorType(JWTClientAuthenticator.PROVIDER_ID);
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestUris(Collections.singletonList(TestApplicationResourceUrls.clientRequestUri()));
});
ClientResource clientResource = adminClient.realm(REALM_NAME).clients().get(clientUUID);
ClientRepresentation client = clientResource.toRepresentation();
OIDCAdvancedConfigWrapper clientConfig = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
assertEquals(JWTClientAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
assertEquals(Algorithm.PS256, clientConfig.getTokenEndpointAuthSigningAlg());
assertEquals(OAuth2Constants.PKCE_METHOD_S256, clientConfig.getPkceCodeChallengeMethod());
assertFalse(client.isImplicitFlowEnabled());
assertFalse(client.isFullScopeAllowed());
assertFalse(clientConfig.isUseMtlsHokToken());
assertTrue(clientConfig.isUseDPoP());
assertTrue(client.isConsentRequired());
// send a pushed authorization request
// use EC key for DPoP proof and send dpop_jkt explicitly
int clockSkew = rand.nextInt(-10, 10); // acceptable clock skew is +-10sec
oauth.client(clientId);
pkceGenerator = PkceGenerator.s256();
String dpopProofEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.POST, oauth.getEndpoints().getPushedAuthorizationRequest(), (long) (Time.currentTime() + clockSkew), Algorithm.ES256, jwsEcHeader, ecKeyPair.getPrivate(), null);
TestingOIDCEndpointsApplicationResource.AuthorizationEndpointRequestObject requestObject = createValidRequestObjectForSecureRequestObjectExecutor(clientId);
requestObject.setNonce(nonce);
requestObject.setCodeChallenge(pkceGenerator.getCodeChallenge());
requestObject.setCodeChallengeMethod(pkceGenerator.getCodeChallengeMethod());
requestObject.setDpopJkt(jktEc);
registerRequestObject(requestObject, clientId, Algorithm.PS256, false);
String signedJwt = createSignedRequestToken(clientId, Algorithm.PS256);
ParResponse pResp = oauth
.client(clientId)
.pushedAuthorizationRequest()
.codeChallenge(pkceGenerator)
.nonce(nonce)
.dpopProof(dpopProofEncoded)
.signedJwt(signedJwt)
.send();
assertEquals(201, pResp.getStatusCode());
requestUri = pResp.getRequestUri();
request = null;
// send an authorization request
String code = loginUserAndGetCode(clientId, nonce,false);
// send a token request
dpopProofEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.POST, oauth.getEndpoints().getToken(), (long) (Time.currentTime() + clockSkew), Algorithm.ES256, jwsEcHeader, ecKeyPair.getPrivate(), null);
signedJwt = createSignedRequestToken(clientId, Algorithm.PS256);
oauth.client(clientId).httpClient().set(MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore());
AccessTokenResponse tokenResponse = oauth
.client(clientId)
.accessTokenRequest(code)
.codeVerifier(pkceGenerator.getCodeVerifier())
.dpopProof(dpopProofEncoded)
.signedJwt(signedJwt)
.send();
assertSuccessfulTokenResponse(tokenResponse);
// check HoK required
// use EC key for DPoP proof
AccessToken accessToken = oauth.verifyToken(tokenResponse.getAccessToken());
assertEquals(jktEc, accessToken.getConfirmation().getKeyThumbprint());
RefreshToken refreshToken = oauth.parseRefreshToken(tokenResponse.getRefreshToken());
assertNull(refreshToken.getConfirmation());
// Logout and remove consent of the user for next logins
logoutUserAndRevokeConsent(clientId, TEST_USERNAME);
}
@Test
public void testFAPI2DPoPSecurityProfileLoginWithMTLS() throws Exception {
// setup client policy
setupPolicyFAPI2ForAllClient(FAPI2_DPOP_SECURITY_PROFILE_NAME);
// create client with MTLS authentication
// Register client with X509
String clientUUID = 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);
});
ClientResource clientResource = adminClient.realm(REALM_NAME).clients().get(clientUUID);
ClientRepresentation client = clientResource.toRepresentation();
OIDCAdvancedConfigWrapper clientConfig = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
assertEquals(X509ClientAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
assertEquals(Algorithm.PS256, clientConfig.getTokenEndpointAuthSigningAlg());
assertEquals(OAuth2Constants.PKCE_METHOD_S256, clientConfig.getPkceCodeChallengeMethod());
assertFalse(client.isImplicitFlowEnabled());
assertFalse(client.isFullScopeAllowed());
assertFalse(clientConfig.isUseMtlsHokToken());
assertTrue(clientConfig.isUseDPoP());
assertTrue(client.isConsentRequired());
oauth.client(clientId);
// without PAR request - should fail
oauth.openLoginForm();
assertBrowserWithError("request_uri not included.");
pkceGenerator = PkceGenerator.s256();
// requiring hybrid request - should fail
ParResponse pResp = oauth
.client(clientId)
.responseType(OIDCResponseType.CODE + " " + OIDCResponseType.ID_TOKEN + " " + OIDCResponseType.TOKEN)
.pushedAuthorizationRequest()
.codeChallenge(pkceGenerator)
.state(null)
.nonce(nonce)
.send();
assertEquals(401, pResp.getStatusCode());
assertEquals(OAuthErrorException.UNAUTHORIZED_CLIENT, pResp.getError());
// an additional parameter in an authorization request that does not exist in a PAR request - should fail
pResp = oauth
.client(clientId)
.responseType(OIDCResponseType.CODE)
.pushedAuthorizationRequest()
.requestUri(null)
.codeChallenge(pkceGenerator)
.state(null)
.nonce(nonce)
.send();
assertEquals(201, pResp.getStatusCode());
oauth.loginForm().requestUri(pResp.getRequestUri()).param("custom", "value").open();
assertBrowserWithError("PAR request did not include necessary parameters");
// duplicated usage of a PAR request - should fail
oauth.loginForm().requestUri(pResp.getRequestUri()).open();
assertBrowserWithError("PAR not found. not issued or used multiple times.");
// send a pushed authorization request
// use RSA key for DPoP proof but not send dpop_jkt
int clockSkew = rand.nextInt(-10, 10); // acceptable clock skew is +-10sec
String dpopProofEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.POST, oauth.getEndpoints().getPushedAuthorizationRequest(), (long) (Time.currentTime() + clockSkew), Algorithm.PS256, jwsRsaHeader, rsaKeyPair.getPrivate(), null);
pResp = oauth
.client(clientId)
.responseType(OIDCResponseType.CODE)
.pushedAuthorizationRequest()
.requestUri(null)
.codeChallenge(pkceGenerator)
.state(null)
.nonce(nonce)
.dpopProof(dpopProofEncoded)
.send();
assertEquals(201, pResp.getStatusCode());
requestUri = pResp.getRequestUri();
// send an authorization request
String code = loginUserAndGetCode(clientId, "123456",false);
// send a token request
dpopProofEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.POST, oauth.getEndpoints().getToken(), (long) (Time.currentTime() + clockSkew), Algorithm.PS256, jwsRsaHeader, rsaKeyPair.getPrivate(), null);
AccessTokenResponse tokenResponse = oauth.
client(clientId).
accessTokenRequest(code).
codeVerifier(pkceGenerator.getCodeVerifier()).
dpopProof(dpopProofEncoded).
send();
// check HoK required
// use RSA key for DPoP proof
AccessToken accessToken = oauth.verifyToken(tokenResponse.getAccessToken());
assertEquals(jktRsa, accessToken.getConfirmation().getKeyThumbprint());
RefreshToken refreshToken = oauth.parseRefreshToken(tokenResponse.getRefreshToken());
assertNull(refreshToken.getConfirmation());
// Logout and remove consent of the user for next logins
logoutUserAndRevokeConsent(clientId, TEST_USERNAME);
}
@Test
public void testFAPI2DPoPMessageSigningClientRegistration() throws Exception {
testFAPI2ClientRegistration(FAPI2_DPOP_MESSAGE_SIGNING_PROFILE_NAME);
}
@Test
public void testFAPI2DPoPMessageSigningOIDCClientRegistration() throws Exception {
testFAPI2OIDCClientRegistration(FAPI2_DPOP_MESSAGE_SIGNING_PROFILE_NAME);
}
@Test
public void testFAPI2DPoPMessageSigningSignatureAlgorithms() throws Exception {
testFAPI2SignatureAlgorithms(FAPI2_DPOP_MESSAGE_SIGNING_PROFILE_NAME);
}
@Test
public void testFAPI2DPoPMessageSigningLoginWithMTLS() throws Exception {
// setup client policy
setupPolicyFAPI2ForAllClient(FAPI2_DPOP_MESSAGE_SIGNING_PROFILE_NAME);
// create client with MTLS authentication
// Register client with X509
String clientUUID = 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);
clientConfig.setRequestObjectRequired("request or request_uri");
clientConfig.setAuthorizationSignedResponseAlg(Algorithm.PS256);
});
ClientResource clientResource = adminClient.realm(REALM_NAME).clients().get(clientUUID);
ClientRepresentation client = clientResource.toRepresentation();
OIDCAdvancedConfigWrapper clientConfig = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
assertEquals(X509ClientAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
assertEquals(Algorithm.PS256, clientConfig.getTokenEndpointAuthSigningAlg());
assertEquals(OAuth2Constants.PKCE_METHOD_S256, clientConfig.getPkceCodeChallengeMethod());
assertEquals(Algorithm.PS256, clientConfig.getRequestObjectSignatureAlg());
assertFalse(client.isImplicitFlowEnabled());
assertFalse(client.isFullScopeAllowed());
assertFalse(clientConfig.isUseMtlsHokToken());
assertTrue(clientConfig.isUseDPoP());
assertTrue(client.isConsentRequired());
// Set request object and correct responseType
// use EC key for DPoP proof and send dpop_jkt explicitly
int clockSkew = rand.nextInt(-10, 10); // acceptable clock skew is +-10sec
String dpopProofEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.POST, oauth.getEndpoints().getPushedAuthorizationRequest(), (long) (Time.currentTime() + clockSkew), Algorithm.ES256, jwsEcHeader, ecKeyPair.getPrivate(), null);
oauth.client(clientId);
pkceGenerator = PkceGenerator.s256();
TestingOIDCEndpointsApplicationResource.AuthorizationEndpointRequestObject requestObject = createValidRequestObjectForSecureRequestObjectExecutor(clientId);
requestObject.setNonce(nonce);
requestObject.setResponseType(OIDCResponseType.CODE);
requestObject.setResponseMode(OIDCResponseMode.QUERY_JWT.value());
requestObject.setCodeChallenge(pkceGenerator.getCodeChallenge());
requestObject.setCodeChallengeMethod(pkceGenerator.getCodeChallengeMethod());
requestObject.setDpopJkt(jktEc);
registerRequestObject(requestObject, clientId, Algorithm.PS256, false);
// send a pushed authorization request
ParResponse pResp = oauth
.client(clientId)
.pushedAuthorizationRequest()
.request(request)
.requestUri(null)
.codeChallenge(pkceGenerator)
.nonce(nonce)
.dpopProof(dpopProofEncoded)
.dpopJkt(jktEc)
.send();
assertEquals(201, pResp.getStatusCode());
// send an authorization request
oauth.responseType(OIDCResponseType.CODE);
oauth.responseMode(OIDCResponseMode.QUERY_JWT.value());
requestUri = pResp.getRequestUri();
request = null;
String code = loginUserAndGetCodeInJwtQueryResponseMode(clientId, nonce);
// send a token request
dpopProofEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.POST, oauth.getEndpoints().getToken(), (long) (Time.currentTime() + clockSkew), Algorithm.ES256, jwsEcHeader, ecKeyPair.getPrivate(), null);
AccessTokenResponse tokenResponse = oauth
.client(clientId)
.accessTokenRequest(code)
.codeVerifier(pkceGenerator.getCodeVerifier())
.dpopProof(dpopProofEncoded)
.send();
// check HoK required
// use EC key for DPoP proof
AccessToken accessToken = oauth.verifyToken(tokenResponse.getAccessToken());
assertEquals(jktEc, accessToken.getConfirmation().getKeyThumbprint());
RefreshToken refreshToken = oauth.parseRefreshToken(tokenResponse.getRefreshToken());
assertNull(refreshToken.getConfirmation());
// Logout and remove consent of the user for next logins
logoutUserAndRevokeConsent(clientId, TEST_USERNAME);
}
@Test
public void testFAPI2DPoPMessageSigningLoginWithPrivateKeyJWT() throws Exception {
// setup client policy
setupPolicyFAPI2ForAllClient(FAPI2_DPOP_MESSAGE_SIGNING_PROFILE_NAME);
// create client with MTLS authentication
// Register client with X509
String clientUUID = createClientByAdmin(clientId, (ClientRepresentation clientRep) -> {
clientRep.setClientAuthenticatorType(JWTClientAuthenticator.PROVIDER_ID);
OIDCAdvancedConfigWrapper clientConfig = OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep);
clientConfig.setRequestUris(Collections.singletonList(TestApplicationResourceUrls.clientRequestUri()));
clientConfig.setRequestObjectRequired("request or request_uri");
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setAuthorizationSignedResponseAlg(Algorithm.PS256);
});
ClientResource clientResource = adminClient.realm(REALM_NAME).clients().get(clientUUID);
ClientRepresentation client = clientResource.toRepresentation();
OIDCAdvancedConfigWrapper clientConfig = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
assertEquals(JWTClientAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
assertEquals(Algorithm.PS256, clientConfig.getTokenEndpointAuthSigningAlg());
assertEquals(Algorithm.PS256, clientConfig.getRequestObjectSignatureAlg());
assertEquals(OAuth2Constants.PKCE_METHOD_S256, clientConfig.getPkceCodeChallengeMethod());
assertFalse(client.isImplicitFlowEnabled());
assertFalse(client.isFullScopeAllowed());
assertFalse(clientConfig.isUseMtlsHokToken());
assertTrue(clientConfig.isUseDPoP());
assertTrue(client.isConsentRequired());
oauth.client(clientId);
pkceGenerator = PkceGenerator.s256();
// without a request object - should fail
TestingOIDCEndpointsApplicationResource.AuthorizationEndpointRequestObject requestObject = createValidRequestObjectForSecureRequestObjectExecutor(clientId);
registerRequestObject(requestObject, clientId, Algorithm.PS256, true);
String signedJwt = createSignedRequestToken(clientId, Algorithm.PS256);
ParResponse pResp = oauth
.client(clientId)
.responseType(OIDCResponseType.CODE)
.pushedAuthorizationRequest()
.request(null)
.requestUri(null)
.codeChallenge(pkceGenerator)
.state(null)
.nonce(nonce)
.signedJwt(signedJwt)
.send();
assertEquals(400, pResp.getStatusCode());
assertEquals(OAuthErrorException.INVALID_REQUEST_OBJECT, pResp.getError());
// Set request object and correct responseType
requestObject = createValidRequestObjectForSecureRequestObjectExecutor(clientId);
requestObject.setNonce(nonce);
requestObject.setResponseType(OIDCResponseType.CODE);
requestObject.setResponseMode(OIDCResponseMode.QUERY_JWT.value());
requestObject.setCodeChallenge(pkceGenerator.getCodeChallenge());
requestObject.setCodeChallengeMethod(pkceGenerator.getCodeChallengeMethod());
registerRequestObject(requestObject, clientId, Algorithm.PS256, false);
// send a pushed authorization request
// use RSA key for DPoP proof but not send dpop_jkt
int clockSkew = rand.nextInt(-10, 10); // acceptable clock skew is +-10sec
String dpopProofEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.POST, oauth.getEndpoints().getPushedAuthorizationRequest(), (long) (Time.currentTime() + clockSkew), Algorithm.PS256, jwsRsaHeader, rsaKeyPair.getPrivate(), null);
signedJwt = createSignedRequestToken(clientId, Algorithm.PS256);
pResp = oauth
.client(clientId)
.pushedAuthorizationRequest()
.request(request)
.requestUri(null)
.codeChallenge(pkceGenerator)
.nonce(nonce)
.dpopProof(dpopProofEncoded)
.signedJwt(signedJwt)
.send();
assertEquals(201, pResp.getStatusCode());
// send an authorization request
requestUri = pResp.getRequestUri();
request = null;
String code = loginUserAndGetCodeInJwtQueryResponseMode(clientId, null);
// send a token request
// use RSA key for DPoP proof
dpopProofEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.POST, oauth.getEndpoints().getToken(), (long) (Time.currentTime() + clockSkew), Algorithm.PS256, jwsRsaHeader, rsaKeyPair.getPrivate(), null);
signedJwt = createSignedRequestToken(clientId, Algorithm.PS256);
AccessTokenResponse tokenResponse = oauth
.client(clientId)
.accessTokenRequest(code)
.codeVerifier(pkceGenerator.getCodeVerifier())
.dpopProof(dpopProofEncoded)
.signedJwt(signedJwt)
.send();
assertSuccessfulTokenResponse(tokenResponse);
// check HoK required
// use RSA key for DPoP proof
AccessToken accessToken = oauth.verifyToken(tokenResponse.getAccessToken());
assertEquals(jktRsa, accessToken.getConfirmation().getKeyThumbprint());
RefreshToken refreshToken = oauth.parseRefreshToken(tokenResponse.getRefreshToken());
assertNull(refreshToken.getConfirmation());
// Logout and remove consent of the user for next logins
logoutUserAndRevokeConsent(clientId, TEST_USERNAME);
}
}

View File

@ -209,6 +209,8 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest {
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 SAML_SECURITY_PROFILE_NAME = "saml-security-profile";
protected static final String FAPI2_DPOP_SECURITY_PROFILE_NAME = "fapi-2-dpop-security-profile";
protected static final String FAPI2_DPOP_MESSAGE_SIGNING_PROFILE_NAME = "fapi-2-dpop-message-signing";
protected static final String ERR_MSG_MISSING_NONCE = "Missing parameter: nonce";
protected static final String ERR_MSG_MISSING_STATE = "Missing parameter: state";
@ -348,7 +350,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, OAUTH2_1_PUBLIC_CLIENT_PROFILE_NAME, SAML_SECURITY_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, SAML_SECURITY_PROFILE_NAME, FAPI2_DPOP_SECURITY_PROFILE_NAME, FAPI2_DPOP_MESSAGE_SIGNING_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, OAUTH2_1_CONFIDENTIAL_CLIENT_PROFILE_NAME, OAUTH2_1_PUBLIC_CLIENT_PROFILE_NAME, SAML_SECURITY_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, SAML_SECURITY_PROFILE_NAME, FAPI2_DPOP_SECURITY_PROFILE_NAME, FAPI2_DPOP_MESSAGE_SIGNING_PROFILE_NAME), Collections.emptyList());
// each profile - fapi-1-baseline
ClientProfileRepresentation actualProfileRep = getProfileRepresentation(actualProfilesRep, FAPI1_BASELINE_PROFILE_NAME, true);