Add FAPI 2.0 security profile as default profile of client policies

closes #21181
This commit is contained in:
Takashi Norimatsu 2023-07-28 06:24:50 +09:00 committed by Marek Posolda
parent c2d5cc67af
commit ee998fee66
10 changed files with 978 additions and 242 deletions

View file

@ -30,6 +30,7 @@ import org.keycloak.representations.idm.ClientPolicyConditionConfigurationRepres
import org.keycloak.services.clientpolicy.ClientPolicyContext;
import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.clientpolicy.ClientPolicyVote;
import org.keycloak.services.clientpolicy.context.PreAuthorizationRequestContext;
/**
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
@ -68,6 +69,11 @@ public class ClientRolesCondition extends AbstractClientPolicyConditionProvider<
@Override
public ClientPolicyVote applyPolicy(ClientPolicyContext context) throws ClientPolicyException {
switch (context.getEvent()) {
case PRE_AUTHORIZATION_REQUEST:
PreAuthorizationRequestContext paContext = (PreAuthorizationRequestContext) context;
ClientModel client = session.getContext().getRealm().getClientByClientId(paContext.getClientId());
if (isRolesMatched(client)) return ClientPolicyVote.YES;
return ClientPolicyVote.NO;
case AUTHORIZATION_REQUEST:
case TOKEN_REQUEST:
case TOKEN_RESPONSE:

View file

@ -136,6 +136,154 @@
}
}
]
},
{
"name": "fapi-2-security-profile",
"description": "Client profile, which enforce clients to conform 'FAPI 2.0 Security Profile' 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": "holder-of-key-enforcer",
"configuration": {
"auto-configure": "true"
}
},
{
"executor": "pkce-enforcer",
"configuration": {
"auto-configure": "true"
}
},
{
"executor" : "secure-par-content",
"configuration" : {}
}
]
},
{
"name": "fapi-2-message-signing",
"description": "Client profile, which enforce clients to conform 'FAPI 2.0 Message Signing' 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": "holder-of-key-enforcer",
"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
}
}
]
}
]
}

View file

@ -1140,15 +1140,28 @@ public class OAuthClient {
}
public ParResponse doPushedAuthorizationRequest(String clientId, String clientSecret) throws IOException {
return doPushedAuthorizationRequest(clientId, clientSecret, (CloseableHttpResponse c)->{});
return doPushedAuthorizationRequest(clientId, clientSecret, (CloseableHttpResponse c)->{}, null);
}
public ParResponse doPushedAuthorizationRequest(String clientId, String clientSecret, String signedJwt) throws IOException {
return doPushedAuthorizationRequest(clientId, clientSecret, (CloseableHttpResponse c)->{}, signedJwt);
}
public ParResponse doPushedAuthorizationRequest(String clientId, String clientSecret, Consumer<CloseableHttpResponse> c) throws IOException {
try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
return doPushedAuthorizationRequest(clientId, clientSecret, c, null);
}
public ParResponse doPushedAuthorizationRequest(String clientId, String clientSecret, Consumer<CloseableHttpResponse> c, String signedJwt) throws IOException {
try (CloseableHttpClient client = httpClient.get()) {
HttpPost post = new HttpPost(getParEndpointUrl());
List<NameValuePair> parameters = new LinkedList<>();
if (signedJwt != null) {
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt));
}
if (origin != null) {
post.addHeader("Origin", origin);
}
@ -1161,6 +1174,8 @@ public class OAuthClient {
if (clientId != null && clientSecret != null) {
String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
post.setHeader("Authorization", authorization);
}
if (clientId != null) {
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, clientId));
}
if (redirectUri != null) {

View file

@ -0,0 +1,228 @@
/*
* Copyright 2023 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 static org.junit.Assert.assertEquals;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.hamcrest.Matchers;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Assume;
import org.junit.BeforeClass;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.models.AdminRoles;
import org.keycloak.models.Constants;
import org.keycloak.representations.AuthorizationResponseToken;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.client.policies.AbstractClientPoliciesTest;
import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResource;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.OAuthGrantPage;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.ServerURLs;
public abstract class AbstractFAPITest extends AbstractClientPoliciesTest {
protected final String TEST_USERNAME = "john";
protected final String TEST_USERSECRET = "password";
@Page
protected ErrorPage errorPage;
@Page
protected LoginPage loginPage;
@Page
protected OAuthGrantPage grantPage;
@Page
protected AppPage appPage;
@BeforeClass
public static void verifySSL() {
// FAPI requires SSL and does not makes sense to test it with disabled SSL
Assume.assumeTrue("The FAPI test requires SSL to be enabled.", ServerURLs.AUTH_SERVER_SSL_REQUIRED);
}
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation realm = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
List<UserRepresentation> users = realm.getUsers();
LinkedList<CredentialRepresentation> credentials = new LinkedList<>();
CredentialRepresentation password = new CredentialRepresentation();
password.setType(CredentialRepresentation.PASSWORD);
password.setValue(TEST_USERSECRET);
credentials.add(password);
UserRepresentation user = new UserRepresentation();
user.setEnabled(true);
user.setUsername(TEST_USERNAME);
user.setEmail("john@keycloak.org");
user.setFirstName("Johny");
user.setCredentials(credentials);
user.setClientRoles(Collections.singletonMap(Constants.REALM_MANAGEMENT_CLIENT_ID, Arrays.asList(AdminRoles.CREATE_CLIENT, AdminRoles.MANAGE_CLIENTS)));
users.add(user);
realm.setUsers(users);
testRealms.add(realm);
}
public static void assertScopes(String expectedScope, String receivedScope) {
Collection<String> expectedScopes = Arrays.asList(expectedScope.split(" "));
Collection<String> receivedScopes = Arrays.asList(receivedScope.split(" "));
Assert.assertTrue("Not matched. expectedScope: " + expectedScope + ", receivedScope: " + receivedScope,
expectedScopes.containsAll(receivedScopes) && receivedScopes.containsAll(expectedScopes));
}
protected String getParameterFromUrl(String paramName, boolean fragmentExpected) {
return fragmentExpected ? oauth.getCurrentFragment().get(paramName) : oauth.getCurrentQuery().get(paramName);
}
protected String loginUserAndGetCode(String clientId, boolean fragmentResponseModeExpected) {
oauth.clientId(clientId);
oauth.doLogin(TEST_USERNAME, TEST_USERSECRET);
grantPage.assertCurrent();
grantPage.assertGrants(OAuthGrantPage.PROFILE_CONSENT_TEXT, OAuthGrantPage.EMAIL_CONSENT_TEXT, OAuthGrantPage.ROLES_CONSENT_TEXT);
grantPage.accept();
String code = getParameterFromUrl(OAuth2Constants.CODE, fragmentResponseModeExpected);
Assert.assertNotNull(code);
return code;
}
protected String loginUserAndGetCodeInJwtQueryResponseMode(String clientId) {
oauth.clientId(clientId);
oauth.doLogin(TEST_USERNAME, TEST_USERSECRET);
grantPage.assertCurrent();
grantPage.assertGrants(OAuthGrantPage.PROFILE_CONSENT_TEXT, OAuthGrantPage.EMAIL_CONSENT_TEXT, OAuthGrantPage.ROLES_CONSENT_TEXT);
grantPage.accept();
System.out.println("KKKKK response = " + oauth.getCurrentQuery().get("response"));
AuthorizationResponseToken responseToken = oauth.verifyAuthorizationResponseToken(oauth.getCurrentQuery().get("response"));
String code = (String)responseToken.getOtherClaims().get("code");
Assert.assertNotNull(code);
return code;
}
protected void assertSuccessfulTokenResponse(OAuthClient.AccessTokenResponse tokenResponse) {
assertEquals(200, tokenResponse.getStatusCode());
Assert.assertThat(tokenResponse.getIdToken(), Matchers.notNullValue());
Assert.assertThat(tokenResponse.getAccessToken(), Matchers.notNullValue());
// Scope parameter must be present per FAPI
Assert.assertNotNull(tokenResponse.getScope());
assertScopes("openid profile email", tokenResponse.getScope());
// ID Token contains all the claims
IDToken idToken = oauth.verifyIDToken(tokenResponse.getIdToken());
Assert.assertNotNull(idToken.getId());
Assert.assertEquals("foo", idToken.getIssuedFor());
Assert.assertEquals("john", idToken.getPreferredUsername());
Assert.assertEquals("john@keycloak.org", idToken.getEmail());
Assert.assertEquals("Johny", idToken.getGivenName());
Assert.assertEquals(idToken.getNonce(), "123456");
}
protected void logoutUserAndRevokeConsent(String clientId, String username) {
UserResource user = ApiUtil.findUserByUsernameId(adminClient.realm(REALM_NAME), username);
user.logout();
List<Map<String, Object>> consents = user.getConsents();
org.junit.Assert.assertEquals(1, consents.size());
user.revokeConsent(clientId);
}
protected void assertRedirectedToClientWithError(String expectedError, boolean fragmentExpected, String expectedErrorDescription) {
appPage.assertCurrent();
assertEquals(expectedError, getParameterFromUrl(OAuth2Constants.ERROR, fragmentExpected));
assertEquals(expectedErrorDescription, getParameterFromUrl(OAuth2Constants.ERROR_DESCRIPTION, fragmentExpected));
}
protected void assertBrowserWithError(String expectedError) {
errorPage.assertCurrent();
Assert.assertEquals(expectedError, errorPage.getError());
}
protected OAuthClient.AccessTokenResponse doAccessTokenRequestWithClientSignedJWT(String code, String signedJwt, String codeVerifier, Supplier<CloseableHttpClient> httpClientSupplier) {
try {
List<NameValuePair> parameters = new LinkedList<>();
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.AUTHORIZATION_CODE));
parameters.add(new BasicNameValuePair(OAuth2Constants.CODE, code));
parameters.add(new BasicNameValuePair(OAuth2Constants.CODE_VERIFIER, codeVerifier));
parameters.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, oauth.getRedirectUri()));
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt));
CloseableHttpResponse response = sendRequest(oauth.getAccessTokenUrl(), parameters, httpClientSupplier);
return new OAuthClient.AccessTokenResponse(response);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
protected String createSignedRequestToken(String clientId, String algorithm) throws Exception {
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
Map<String, String> generatedKeys = oidcClientEndpointsResource.getKeysAsBase64();
KeyPair keyPair = getKeyPairFromGeneratedBase64(generatedKeys, algorithm);
PrivateKey privateKey = keyPair.getPrivate();
PublicKey publicKey = keyPair.getPublic();
return createSignedRequestToken(clientId, privateKey, publicKey, algorithm);
}
protected CloseableHttpResponse sendRequest(String requestUrl, List<NameValuePair> parameters, Supplier<CloseableHttpClient> httpClientSupplier) throws Exception {
CloseableHttpClient client = httpClientSupplier.get();
try {
HttpPost post = new HttpPost(requestUrl);
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
post.setEntity(formEntity);
return client.execute(post);
} finally {
oauth.closeClient(client);
}
}
}

View file

@ -18,22 +18,11 @@
package org.keycloak.testsuite.client;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.hamcrest.Matchers;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Assume;
import org.junit.BeforeClass;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
import org.keycloak.authentication.authenticators.client.JWTClientSecretAuthenticator;
@ -44,8 +33,6 @@ import org.keycloak.common.util.UriUtils;
import org.keycloak.constants.ServiceUrlConstants;
import org.keycloak.crypto.Algorithm;
import org.keycloak.jose.jws.crypto.HashUtils;
import org.keycloak.models.AdminRoles;
import org.keycloak.models.Constants;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
@ -54,43 +41,27 @@ import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.clientpolicy.condition.AnyClientConditionFactory;
import org.keycloak.services.clientpolicy.condition.ClientUpdaterContextConditionFactory;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls;
import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResource;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.OAuthGrantPage;
import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource;
import org.keycloak.testsuite.util.MutualTLSUtils;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.ServerURLs;
import org.keycloak.testsuite.client.policies.AbstractClientPoliciesTest;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;
import static org.junit.Assert.assertEquals;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.fail;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.ClientPoliciesBuilder;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.ClientPolicyBuilder;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createAnyClientConditionConfig;
@ -105,52 +76,7 @@ import static org.keycloak.testsuite.util.ClientPoliciesUtil.createClientUpdateC
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class FAPI1Test extends AbstractClientPoliciesTest {
@Page
protected ErrorPage errorPage;
@Page
protected LoginPage loginPage;
@Page
protected OAuthGrantPage grantPage;
@Page
protected AppPage appPage;
@BeforeClass
public static void verifySSL() {
// FAPI requires SSL and does not makes sense to test it with disabled SSL
Assume.assumeTrue("The FAPI test requires SSL to be enabled.", ServerURLs.AUTH_SERVER_SSL_REQUIRED);
}
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation realm = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
List<UserRepresentation> users = realm.getUsers();
LinkedList<CredentialRepresentation> credentials = new LinkedList<>();
CredentialRepresentation password = new CredentialRepresentation();
password.setType(CredentialRepresentation.PASSWORD);
password.setValue("password");
credentials.add(password);
UserRepresentation user = new UserRepresentation();
user.setEnabled(true);
user.setUsername("john");
user.setEmail("john@keycloak.org");
user.setFirstName("Johny");
user.setCredentials(credentials);
user.setClientRoles(Collections.singletonMap(Constants.REALM_MANAGEMENT_CLIENT_ID, Arrays.asList(AdminRoles.CREATE_CLIENT, AdminRoles.MANAGE_CLIENTS)));
users.add(user);
realm.setUsers(users);
testRealms.add(realm);
}
public class FAPI1Test extends AbstractFAPITest {
@Test
public void testFAPIBaselineClientAuthenticator() throws Exception {
@ -336,7 +262,7 @@ public class FAPI1Test extends AbstractClientPoliciesTest {
checkRedirectUriForCurrentClientDuringLogin();
// Check PKCE with S256, redirectUri and nonce/state set. Login should be successful
successfulLoginAndLogout("foo", false, (String code) -> {
successfulLoginAndLogout("foo", TEST_USERNAME, false, (String code) -> {
String signedJwt = getClientSecretSignedJWT("atleast-14chars-password", Algorithm.HS256);
return doAccessTokenRequestWithClientSignedJWT(code, signedJwt, codeVerifier, DefaultHttpClient::new);
});
@ -366,7 +292,7 @@ public class FAPI1Test extends AbstractClientPoliciesTest {
checkRedirectUriForCurrentClientDuringLogin();
// Check PKCE with S256, redirectUri and nonce/state set. Login should be successful
successfulLoginAndLogout("foo", false, (String code) -> {
successfulLoginAndLogout("foo", TEST_USERNAME, false, (String code) -> {
oauth.codeVerifier(codeVerifier);
return oauth.doAccessTokenRequest(code, null);
});
@ -458,7 +384,7 @@ public class FAPI1Test extends AbstractClientPoliciesTest {
oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256);
// Check PKCE with S256, redirectUri and nonce/state set. Login should be successful
successfulLoginAndLogout("foo", false, (String code) -> {
successfulLoginAndLogout("foo", TEST_USERNAME, false, (String code) -> {
oauth.codeVerifier(codeVerifier);
return oauth.doAccessTokenRequest(code, null);
});
@ -603,7 +529,7 @@ public class FAPI1Test extends AbstractClientPoliciesTest {
Assert.assertNotNull(accessToken.getConfirmation().getCertThumbprint());
// Logout and remove consent of the user for next logins
logoutUserAndRevokeConsent("foo");
logoutUserAndRevokeConsent("foo", TEST_USERNAME);
}
@Test
@ -657,11 +583,9 @@ public class FAPI1Test extends AbstractClientPoliciesTest {
Assert.assertNotNull(accessToken.getConfirmation().getCertThumbprint());
// Logout and remove consent of the user for next logins
logoutUserAndRevokeConsent("foo");
logoutUserAndRevokeConsent("foo", TEST_USERNAME);
}
private void checkPKCEWithS256RequiredDuringLogin(String clientId) {
// Check PKCE required - login without PKCE should fail
oauth.clientId(clientId);
@ -698,8 +622,7 @@ public class FAPI1Test extends AbstractClientPoliciesTest {
oauth.openid(true);
oauth.redirectUri(null);
oauth.openLoginForm();
errorPage.assertCurrent();
Assert.assertEquals("Invalid parameter: redirect_uri", errorPage.getError());
assertBrowserWithError("Invalid parameter: redirect_uri");
// Revert redirectUri
oauth.redirectUri(origRedirectUri);
@ -743,7 +666,7 @@ public class FAPI1Test extends AbstractClientPoliciesTest {
}
// codeToTokenExchanger is supposed to exchange "code" for the accessTokenResponse. It is supposed to send the tokenRequest including proper client authentication
private void successfulLoginAndLogout(String clientId, boolean fragmentResponseModeExpected, Function<String, OAuthClient.AccessTokenResponse> codeToTokenExchanger) throws Exception {
private void successfulLoginAndLogout(String clientId, String username, boolean fragmentResponseModeExpected, Function<String, OAuthClient.AccessTokenResponse> codeToTokenExchanger) throws Exception {
String code = loginUserAndGetCode(clientId, fragmentResponseModeExpected);
OAuthClient.AccessTokenResponse tokenResponse = codeToTokenExchanger.apply(code);
@ -751,38 +674,7 @@ public class FAPI1Test extends AbstractClientPoliciesTest {
assertSuccessfulTokenResponse(tokenResponse);
// Logout and remove consent of the user for next logins
logoutUserAndRevokeConsent(clientId);
}
private String loginUserAndGetCode(String clientId, boolean fragmentResponseModeExpected) {
oauth.clientId(clientId);
oauth.doLogin("john", "password");
grantPage.assertCurrent();
grantPage.assertGrants(OAuthGrantPage.PROFILE_CONSENT_TEXT, OAuthGrantPage.EMAIL_CONSENT_TEXT, OAuthGrantPage.ROLES_CONSENT_TEXT);
grantPage.accept();
String code = getParameterFromUrl(OAuth2Constants.CODE, fragmentResponseModeExpected);
Assert.assertNotNull(code);
return code;
}
private void assertSuccessfulTokenResponse(OAuthClient.AccessTokenResponse tokenResponse) {
assertEquals(200, tokenResponse.getStatusCode());
assertThat(tokenResponse.getIdToken(), Matchers.notNullValue());
assertThat(tokenResponse.getAccessToken(), Matchers.notNullValue());
// Scope parameter must be present per FAPI
Assert.assertNotNull(tokenResponse.getScope());
assertScopes("openid profile email", tokenResponse.getScope());
// ID Token contains all the claims
IDToken idToken = oauth.verifyIDToken(tokenResponse.getIdToken());
Assert.assertNotNull(idToken.getId());
Assert.assertEquals("foo", idToken.getIssuedFor());
Assert.assertEquals("john", idToken.getPreferredUsername());
Assert.assertEquals("john@keycloak.org", idToken.getEmail());
Assert.assertEquals("Johny", idToken.getGivenName());
Assert.assertEquals(idToken.getNonce(), "123456");
logoutUserAndRevokeConsent(clientId, username);
}
private void assertIDTokenAsDetachedSignature(String idTokenParam, String code) {
@ -800,7 +692,6 @@ public class FAPI1Test extends AbstractClientPoliciesTest {
Assert.assertEquals(idToken.getCodeHash(), HashUtils.accessTokenHash(Algorithm.PS256, code));
}
private String getClientSecretSignedJWT(String secret, String algorithm) {
JWTClientSecretCredentialsProvider jwtProvider = new JWTClientSecretCredentialsProvider();
jwtProvider.setClientSecret(secret, algorithm);
@ -811,59 +702,4 @@ public class FAPI1Test extends AbstractClientPoliciesTest {
String authServerBaseUrl = UriUtils.getOrigin(oauth.getRedirectUri()) + "/auth";
return KeycloakUriBuilder.fromUri(authServerBaseUrl).path(ServiceUrlConstants.REALM_INFO_PATH).build("test").toString();
}
private OAuthClient.AccessTokenResponse doAccessTokenRequestWithClientSignedJWT(String code, String signedJwt, String codeVerifier, Supplier<CloseableHttpClient> httpClientSupplier) {
try {
List<NameValuePair> parameters = new LinkedList<>();
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.AUTHORIZATION_CODE));
parameters.add(new BasicNameValuePair(OAuth2Constants.CODE, code));
parameters.add(new BasicNameValuePair(OAuth2Constants.CODE_VERIFIER, codeVerifier));
parameters.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, oauth.getRedirectUri()));
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt));
CloseableHttpResponse response = sendRequest(oauth.getAccessTokenUrl(), parameters, httpClientSupplier);
return new OAuthClient.AccessTokenResponse(response);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private CloseableHttpResponse sendRequest(String requestUrl, List<NameValuePair> parameters, Supplier<CloseableHttpClient> httpClientSupplier) throws Exception {
CloseableHttpClient client = httpClientSupplier.get();
try {
HttpPost post = new HttpPost(requestUrl);
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
post.setEntity(formEntity);
return client.execute(post);
} finally {
oauth.closeClient(client);
}
}
public static void assertScopes(String expectedScope, String receivedScope) {
Collection<String> expectedScopes = Arrays.asList(expectedScope.split(" "));
Collection<String> receivedScopes = Arrays.asList(receivedScope.split(" "));
Assert.assertTrue("Not matched. expectedScope: " + expectedScope + ", receivedScope: " + receivedScope,
expectedScopes.containsAll(receivedScopes) && receivedScopes.containsAll(expectedScopes));
}
private void assertRedirectedToClientWithError(String expectedError, boolean fragmentExpected, String expectedErrorDescription) {
appPage.assertCurrent();
assertEquals(expectedError, getParameterFromUrl(OAuth2Constants.ERROR, fragmentExpected));
assertEquals(expectedErrorDescription, getParameterFromUrl(OAuth2Constants.ERROR_DESCRIPTION, fragmentExpected));
}
private String getParameterFromUrl(String paramName, boolean fragmentExpected) {
return fragmentExpected ? oauth.getCurrentFragment().get(paramName) : oauth.getCurrentQuery().get(paramName);
}
private void logoutUserAndRevokeConsent(String clientId) {
UserResource user = ApiUtil.findUserByUsernameId(adminClient.realm(REALM_NAME), "john");
user.logout();
List<Map<String, Object>> consents = user.getConsents();
org.junit.Assert.assertEquals(1, consents.size());
user.revokeConsent(clientId);
}
}

View file

@ -0,0 +1,545 @@
/*
* Copyright 2023 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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createAnyClientConditionConfig;
import java.util.Collections;
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.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.protocol.oidc.utils.OIDCResponseMode;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
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.rest.resource.TestingOIDCEndpointsApplicationResource;
import org.keycloak.testsuite.util.MutualTLSUtils;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientPoliciesBuilder;
import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientPolicyBuilder;
import org.keycloak.testsuite.util.OAuthClient.ParResponse;
/**
* Test for the FAPI 2 specifications (still implementer's draft):
* - FAPI 2.0 Security Profile - https://openid.bitbucket.io/fapi/fapi-2_0-security-profile.html
* - FAPI 2.0 Message Signing - https://openid.bitbucket.io/fapi/fapi-2_0-message-signing.html
*
* Mostly tests the global FAPI policies work as expected
*
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
public class FAPI2Test extends AbstractFAPITest {
private static final String clientId = "foo";
@Test
public void testFAPI2SecurityProfileClientRegistration() throws Exception {
testFAPI2ClientRegistration(FAPI2_SECURITY_PROFILE_NAME);
}
@Test
public void testFAPI2SecurityProfileOIDCClientRegistration() throws Exception {
testFAPI2OIDCClientRegistration(FAPI2_SECURITY_PROFILE_NAME);
}
@Test
public void testFAPI2SecurityProfileSignatureAlgorithms(String profile) throws Exception {
testFAPI2SignatureAlgorithms(FAPI2_SECURITY_PROFILE_NAME);
}
@Test
public void testFAPI2SecurityProfileLoginWithPrivateKeyJWT() throws Exception {
// setup client policy
setupPolicyFAPI2ForAllClient(FAPI2_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();
assertEquals(JWTClientAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
assertEquals(Algorithm.PS256, OIDCAdvancedConfigWrapper.fromClientRepresentation(client).getTokenEndpointAuthSigningAlg());
assertEquals(false, client.isImplicitFlowEnabled());
assertEquals(OAuth2Constants.PKCE_METHOD_S256, OIDCAdvancedConfigWrapper.fromClientRepresentation(client).getPkceCodeChallengeMethod());
assertEquals(true, OIDCAdvancedConfigWrapper.fromClientRepresentation(client).isUseMtlsHokToken());
assertEquals(false, client.isFullScopeAllowed());
assertEquals(true, client.isConsentRequired());
// send a pushed authorization request
oauth.clientId(clientId);
String codeVerifier = "1234567890123456789012345678901234567890123"; // 43
String codeChallenge = generateS256CodeChallenge(codeVerifier);
TestingOIDCEndpointsApplicationResource.AuthorizationEndpointRequestObject requestObject = createValidRequestObjectForSecureRequestObjectExecutor(clientId);
requestObject.setNonce("123456");
requestObject.setCodeChallenge(codeChallenge);
requestObject.setCodeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256);
registerRequestObject(requestObject, clientId, Algorithm.PS256, false);
String signedJwt = createSignedRequestToken(clientId, Algorithm.PS256);
ParResponse pResp = oauth.doPushedAuthorizationRequest(clientId, null, signedJwt);
assertEquals(201, pResp.getStatusCode());
String requestUri = pResp.getRequestUri();
oauth.requestUri(requestUri);
oauth.request(null);
// send an authorization request
String code = loginUserAndGetCode(clientId, false);
// send a token request
signedJwt = createSignedRequestToken(clientId, Algorithm.PS256);
OAuthClient.AccessTokenResponse tokenResponse = doAccessTokenRequestWithClientSignedJWT(code, signedJwt, codeVerifier, () -> MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore());
assertSuccessfulTokenResponse(tokenResponse);
// check HoK required
AccessToken accessToken = oauth.verifyToken(tokenResponse.getAccessToken());
Assert.assertNotNull(accessToken.getConfirmation().getCertThumbprint());
// Logout and remove consent of the user for next logins
logoutUserAndRevokeConsent(clientId, TEST_USERNAME);
}
@Test
public void testFAPI2SecurityProfileLoginWithMTLS() throws Exception {
// setup client policy
setupPolicyFAPI2ForAllClient(FAPI2_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();
assertEquals(X509ClientAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
assertEquals(Algorithm.PS256, OIDCAdvancedConfigWrapper.fromClientRepresentation(client).getTokenEndpointAuthSigningAlg());
assertEquals(false, client.isImplicitFlowEnabled());
assertEquals(OAuth2Constants.PKCE_METHOD_S256, OIDCAdvancedConfigWrapper.fromClientRepresentation(client).getPkceCodeChallengeMethod());
assertEquals(true, OIDCAdvancedConfigWrapper.fromClientRepresentation(client).isUseMtlsHokToken());
assertEquals(false, client.isFullScopeAllowed());
assertEquals(true, client.isConsentRequired());
oauth.clientId(clientId);
// without PAR request - should fail
oauth.openLoginForm();
assertBrowserWithError("request_uri not included.");
String codeVerifier = "1234567890123456789012345678901234567890123"; // 43
String codeChallenge = generateS256CodeChallenge(codeVerifier);
oauth.codeChallenge(codeChallenge);
oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256);
oauth.stateParamHardcoded(null);
oauth.nonce("123456");
// requiring hybrid request - should fail
oauth.responseType(OIDCResponseType.CODE + " " + OIDCResponseType.ID_TOKEN + " " + OIDCResponseType.TOKEN);
ParResponse pResp = oauth.doPushedAuthorizationRequest(clientId, null);
assertEquals(401, pResp.getStatusCode());
assertEquals(OAuthErrorException.UNAUTHORIZED_CLIENT, pResp.getError());
// authorization request does not match PAR request - should fail
oauth.responseType(OIDCResponseType.CODE);
pResp = oauth.doPushedAuthorizationRequest(clientId, null);
assertEquals(201, pResp.getStatusCode());
String requestUri = pResp.getRequestUri();
oauth.responseType(OIDCResponseType.CODE + " " + OIDCResponseType.ID_TOKEN + " " + OIDCResponseType.TOKEN);
oauth.requestUri(requestUri);
oauth.openLoginForm();
assertRedirectedToClientWithError(OAuthErrorException.INVALID_REQUEST, false, "Parameter response_type does not match");
oauth.responseType(OIDCResponseType.CODE);
// an additional parameter in an authorization request that does not exist in a PAR request - should fail
oauth.requestUri(null);
pResp = oauth.doPushedAuthorizationRequest(clientId, null);
assertEquals(201, pResp.getStatusCode());
requestUri = pResp.getRequestUri();
oauth.stateParamRandom();
oauth.requestUri(requestUri);
oauth.openLoginForm();
assertBrowserWithError("PAR request did not include necessary parameters");
// duplicated usage of a PAR request - should fail
oauth.openLoginForm();
assertBrowserWithError("PAR not found. not issued or used multiple times.");
// send a pushed authorization request
oauth.stateParamHardcoded(null);
oauth.requestUri(null);
pResp = oauth.doPushedAuthorizationRequest(clientId, null);
assertEquals(201, pResp.getStatusCode());
requestUri = pResp.getRequestUri();
// send an authorization request
oauth.requestUri(requestUri);
String code = loginUserAndGetCode(clientId, false);
// send a token request
oauth.codeVerifier(codeVerifier);
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, null);
// check HoK required
assertSuccessfulTokenResponse(tokenResponse);
AccessToken accessToken = oauth.verifyToken(tokenResponse.getAccessToken());
Assert.assertNotNull(accessToken.getConfirmation().getCertThumbprint());
// Logout and remove consent of the user for next logins
logoutUserAndRevokeConsent(clientId, TEST_USERNAME);
}
@Test
public void testFAPI2MessageSigningClientRegistration() throws Exception {
testFAPI2ClientRegistration(FAPI2_MESSAGE_SIGNING_PROFILE_NAME);
}
@Test
public void testFAPI2MessageSigningOIDCClientRegistration() throws Exception {
testFAPI2OIDCClientRegistration(FAPI2_MESSAGE_SIGNING_PROFILE_NAME);
}
@Test
public void testFAPI2MessageSigningSignatureAlgorithms(String profile) throws Exception {
testFAPI2SignatureAlgorithms(FAPI2_MESSAGE_SIGNING_PROFILE_NAME);
}
@Test
public void testFAPI2MessageSigningLoginWithMTLS() throws Exception {
// setup client policy
setupPolicyFAPI2ForAllClient(FAPI2_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();
assertEquals(X509ClientAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
assertEquals(Algorithm.PS256, OIDCAdvancedConfigWrapper.fromClientRepresentation(client).getTokenEndpointAuthSigningAlg());
assertEquals(false, client.isImplicitFlowEnabled());
assertEquals(OAuth2Constants.PKCE_METHOD_S256, OIDCAdvancedConfigWrapper.fromClientRepresentation(client).getPkceCodeChallengeMethod());
assertEquals(true, OIDCAdvancedConfigWrapper.fromClientRepresentation(client).isUseMtlsHokToken());
assertEquals(false, client.isFullScopeAllowed());
assertEquals(true, client.isConsentRequired());
assertEquals(Algorithm.PS256, OIDCAdvancedConfigWrapper.fromClientRepresentation(client).getRequestObjectSignatureAlg());
// Set request object and correct responseType
oauth.clientId(clientId);
oauth.stateParamHardcoded(null);
String codeVerifier = "1234567890123456789012345678901234567890123"; // 43
String codeChallenge = generateS256CodeChallenge(codeVerifier);
TestingOIDCEndpointsApplicationResource.AuthorizationEndpointRequestObject requestObject = createValidRequestObjectForSecureRequestObjectExecutor(clientId);
requestObject.setNonce("123456");
requestObject.setResponseType(OIDCResponseType.CODE);
requestObject.setResponseMode(OIDCResponseMode.QUERY_JWT.value());
requestObject.setCodeChallenge(codeChallenge);
requestObject.setCodeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256);
registerRequestObject(requestObject, clientId, Algorithm.PS256, false);
// send a pushed authorization request
ParResponse pResp = oauth.doPushedAuthorizationRequest(clientId, null);
assertEquals(201, pResp.getStatusCode());
String requestUri = pResp.getRequestUri();
// send an authorization request
oauth.codeChallenge(codeChallenge);
oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256);
oauth.nonce("123456");
oauth.responseType(OIDCResponseType.CODE);
oauth.responseMode(OIDCResponseMode.QUERY_JWT.value());
oauth.requestUri(requestUri);
oauth.request(null);
String code = loginUserAndGetCodeInJwtQueryResponseMode(clientId);
// send a token request
oauth.codeVerifier(codeVerifier);
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, null);
// check HoK required
assertSuccessfulTokenResponse(tokenResponse);
AccessToken accessToken = oauth.verifyToken(tokenResponse.getAccessToken());
Assert.assertNotNull(accessToken.getConfirmation().getCertThumbprint());
// Logout and remove consent of the user for next logins
logoutUserAndRevokeConsent(clientId, TEST_USERNAME);
}
@Test
public void testFAPI2MessageSigningLoginWithPrivateKeyJWT() throws Exception {
// setup client policy
setupPolicyFAPI2ForAllClient(FAPI2_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();
assertEquals(JWTClientAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
assertEquals(Algorithm.PS256, OIDCAdvancedConfigWrapper.fromClientRepresentation(client).getTokenEndpointAuthSigningAlg());
assertEquals(Algorithm.PS256, OIDCAdvancedConfigWrapper.fromClientRepresentation(client).getRequestObjectSignatureAlg());
assertEquals(false, client.isImplicitFlowEnabled());
assertEquals(OAuth2Constants.PKCE_METHOD_S256, OIDCAdvancedConfigWrapper.fromClientRepresentation(client).getPkceCodeChallengeMethod());
assertEquals(true, OIDCAdvancedConfigWrapper.fromClientRepresentation(client).isUseMtlsHokToken());
assertEquals(false, client.isFullScopeAllowed());
assertEquals(true, client.isConsentRequired());
oauth.clientId(clientId);
oauth.stateParamHardcoded(null);
String codeVerifier = "1234567890123456789012345678901234567890123"; // 43
String codeChallenge = generateS256CodeChallenge(codeVerifier);
// without a request object - should fail
oauth.codeChallenge(codeChallenge);
oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256);
oauth.stateParamHardcoded(null);
oauth.nonce("123456");
oauth.responseType(OIDCResponseType.CODE);
TestingOIDCEndpointsApplicationResource.AuthorizationEndpointRequestObject requestObject = createValidRequestObjectForSecureRequestObjectExecutor(clientId);
registerRequestObject(requestObject, clientId, Algorithm.PS256, true);
oauth.requestUri(null);
oauth.request(null);
String signedJwt = createSignedRequestToken(clientId, Algorithm.PS256);
ParResponse pResp = oauth.doPushedAuthorizationRequest(clientId, null, signedJwt);
assertEquals(400, pResp.getStatusCode());
assertEquals(OAuthErrorException.INVALID_REQUEST_OBJECT, pResp.getError());
// Set request object and correct responseType
requestObject = createValidRequestObjectForSecureRequestObjectExecutor(clientId);
requestObject.setNonce("123456");
requestObject.setResponseType(OIDCResponseType.CODE);
requestObject.setResponseMode(OIDCResponseMode.QUERY_JWT.value());
requestObject.setCodeChallenge(codeChallenge);
requestObject.setCodeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256);
registerRequestObject(requestObject, clientId, Algorithm.PS256, false);
// send a pushed authorization request
signedJwt = createSignedRequestToken(clientId, Algorithm.PS256);
pResp = oauth.doPushedAuthorizationRequest(clientId, null, signedJwt);
assertEquals(201, pResp.getStatusCode());
String requestUri = pResp.getRequestUri();
// send an authorization request
oauth.requestUri(requestUri);
oauth.request(null);
String code = loginUserAndGetCodeInJwtQueryResponseMode(clientId);
// send a token request
signedJwt = createSignedRequestToken(clientId, Algorithm.PS256);
OAuthClient.AccessTokenResponse tokenResponse = doAccessTokenRequestWithClientSignedJWT(code, signedJwt, codeVerifier, () -> MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore());
assertSuccessfulTokenResponse(tokenResponse);
// check HoK required
assertSuccessfulTokenResponse(tokenResponse);
AccessToken accessToken = oauth.verifyToken(tokenResponse.getAccessToken());
Assert.assertNotNull(accessToken.getConfirmation().getCertThumbprint());
// Logout and remove consent of the user for next logins
logoutUserAndRevokeConsent(clientId, TEST_USERNAME);
}
private 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);
Assert.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);
Assert.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);
Assert.assertEquals(JWTClientAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
// Check the Consent is enabled, Holder-of-key is enabled, fullScopeAllowed disabled and default signature algorithm.
Assert.assertTrue(client.isConsentRequired());
OIDCAdvancedConfigWrapper clientConfig = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
Assert.assertTrue(clientConfig.isUseMtlsHokToken());
Assert.assertEquals(Algorithm.PS256, clientConfig.getIdTokenSignedResponseAlg());
Assert.assertEquals(Algorithm.PS256, clientConfig.getRequestObjectSignatureAlg());
Assert.assertFalse(client.isFullScopeAllowed());
}
private 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);
Assert.assertEquals(JWTClientAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
Assert.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);
Assert.assertEquals(X509ClientAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
// Check the Consent is enabled, PKCS set to S256
Assert.assertTrue(client.isConsentRequired());
Assert.assertEquals(OAuth2Constants.PKCE_METHOD_S256, OIDCAdvancedConfigWrapper.fromClientRepresentation(client).getPkceCodeChallengeMethod());
}
private 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);
Assert.assertEquals(Algorithm.ES256, clientConfig.getIdTokenSignedResponseAlg());
Assert.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);
Assert.assertEquals(Algorithm.PS256, clientConfig.getIdTokenSignedResponseAlg());
Assert.assertEquals(Algorithm.PS256, clientConfig.getRequestObjectSignatureAlg().toString());
Assert.assertEquals(Algorithm.PS256, clientConfig.getUserInfoSignedResponseAlg());
Assert.assertEquals(Algorithm.PS256, clientConfig.getTokenEndpointAuthSigningAlg());
Assert.assertEquals(Algorithm.PS256, client.getAttributes().get(OIDCConfigAttributes.ACCESS_TOKEN_SIGNED_RESPONSE_ALG));
}
private void setupPolicyFAPI2ForAllClient(String profile) throws Exception {
String json = (new ClientPoliciesBuilder()).addPolicy(
(new 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

@ -110,43 +110,10 @@ import org.keycloak.testsuite.client.policies.AbstractClientPoliciesTest;
*
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
public class FAPICIBATest extends AbstractClientPoliciesTest {
public class FAPICIBATest extends AbstractFAPITest {
private final String clientId = "foo";
private final String bindingMessage = "bbbbmmmm";
private final String username = "john";
@BeforeClass
public static void verifySSL() {
// FAPI requires SSL and does not makes sense to test it with disabled SSL
Assume.assumeTrue("The FAPI test requires SSL to be enabled.", ServerURLs.AUTH_SERVER_SSL_REQUIRED);
}
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation realm = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
List<UserRepresentation> users = realm.getUsers();
LinkedList<CredentialRepresentation> credentials = new LinkedList<>();
CredentialRepresentation password = new CredentialRepresentation();
password.setType(CredentialRepresentation.PASSWORD);
password.setValue("password");
credentials.add(password);
UserRepresentation user = new UserRepresentation();
user.setEnabled(true);
user.setUsername("john");
user.setEmail("john@keycloak.org");
user.setFirstName("Johny");
user.setCredentials(credentials);
user.setClientRoles(Collections.singletonMap(Constants.REALM_MANAGEMENT_CLIENT_ID, Arrays.asList(AdminRoles.CREATE_CLIENT, AdminRoles.MANAGE_CLIENTS)));
users.add(user);
realm.setUsers(users);
testRealms.add(realm);
}
@Test
public void testFAPIAdvancedClientRegistration() throws Exception {
@ -272,7 +239,7 @@ public class FAPICIBATest extends AbstractClientPoliciesTest {
assertEquals(JWTClientAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
// prepare valid signed authentication request
AuthorizationEndpointRequestObject requestObject = createFAPIValidAuthorizationEndpointRequestObject(username, bindingMessage);
AuthorizationEndpointRequestObject requestObject = createFAPIValidAuthorizationEndpointRequestObject(TEST_USERNAME, bindingMessage);
String encodedRequestObject = registerSharedAuthenticationRequest(requestObject, clientId, Algorithm.PS256);
// Get keys of client. Will be used for client authentication and signing of authentication request
@ -303,10 +270,10 @@ public class FAPICIBATest extends AbstractClientPoliciesTest {
// user Token Request
OAuthClient.AccessTokenResponse tokenRes = doBackchannelAuthenticationTokenRequestWithClientSignedJWT(
signedJwt2, response.getAuthReqId(), () -> MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore());
verifyBackchannelAuthenticationTokenRequest(tokenRes, clientId, username);
verifyBackchannelAuthenticationTokenRequest(tokenRes, clientId, TEST_USERNAME);
// Logout and remove consent of the user for next logins
logoutUserAndRevokeConsent(clientId, username);
logoutUserAndRevokeConsent(clientId, TEST_USERNAME);
}
@Test
@ -325,7 +292,7 @@ public class FAPICIBATest extends AbstractClientPoliciesTest {
assertEquals(JWTClientAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
// prepare valid signed authentication request
AuthorizationEndpointRequestObject requestObject = createFAPIValidAuthorizationEndpointRequestObject(username, bindingMessage);
AuthorizationEndpointRequestObject requestObject = createFAPIValidAuthorizationEndpointRequestObject(TEST_USERNAME, bindingMessage);
String encodedRequestObject = registerSharedAuthenticationRequest(requestObject, clientId, Algorithm.PS256);
// Get keys of client. Will be used for client authentication and signing of authentication request
@ -379,7 +346,7 @@ public class FAPICIBATest extends AbstractClientPoliciesTest {
assertEquals(X509ClientAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
// prepare valid signed authentication request
AuthorizationEndpointRequestObject requestObject = createFAPIValidAuthorizationEndpointRequestObject(username, bindingMessage);
AuthorizationEndpointRequestObject requestObject = createFAPIValidAuthorizationEndpointRequestObject(TEST_USERNAME, bindingMessage);
String encodedRequestObject = registerSharedAuthenticationRequest(requestObject, clientId, Algorithm.PS256);
// user Backchannel Authentication Request
@ -399,10 +366,10 @@ public class FAPICIBATest extends AbstractClientPoliciesTest {
// user Token Request
OAuthClient.AccessTokenResponse tokenRes = doBackchannelAuthenticationTokenRequestWithMTLS(
clientId, response.getAuthReqId(), () -> MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore());
verifyBackchannelAuthenticationTokenRequest(tokenRes, clientId, username);
verifyBackchannelAuthenticationTokenRequest(tokenRes, clientId, TEST_USERNAME);
// Logout and remove consent of the user for next logins
logoutUserAndRevokeConsent(clientId, username);
logoutUserAndRevokeConsent(clientId, TEST_USERNAME);
}
@Test
@ -423,7 +390,7 @@ public class FAPICIBATest extends AbstractClientPoliciesTest {
assertEquals(X509ClientAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
// prepare invalid signed authentication request lacking binding message
AuthorizationEndpointRequestObject requestObject = createFAPIValidAuthorizationEndpointRequestObject(username, null);
AuthorizationEndpointRequestObject requestObject = createFAPIValidAuthorizationEndpointRequestObject(TEST_USERNAME, null);
String encodedRequestObject = registerSharedAuthenticationRequest(requestObject, clientId, Algorithm.PS256);
@ -452,7 +419,7 @@ public class FAPICIBATest extends AbstractClientPoliciesTest {
ClientRepresentation client = clientResource.toRepresentation();
assertEquals(X509ClientAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType());
AuthenticationRequestAcknowledgement response = doInvalidBackchannelAuthenticationRequestWithMTLS(clientId, username, bindingMessage, () -> MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore());
AuthenticationRequestAcknowledgement response = doInvalidBackchannelAuthenticationRequestWithMTLS(clientId, TEST_USERNAME, bindingMessage, () -> MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore());
assertThat(response.getStatusCode(), is(equalTo(400)));
assertThat(response.getError(), is(equalTo(OAuthErrorException.INVALID_REQUEST)));
assertThat(response.getErrorDescription(), is(equalTo("Missing parameter: 'request' or 'request_uri'")));
@ -639,23 +606,4 @@ public class FAPICIBATest extends AbstractClientPoliciesTest {
assertThat(idToken.getAudience()[0], is(equalTo(idToken.getIssuedFor())));
}
private void logoutUserAndRevokeConsent(String clientId, String username) {
UserResource user = ApiUtil.findUserByUsernameId(adminClient.realm(REALM_NAME), username);
user.logout();
List<Map<String, Object>> consents = user.getConsents();
org.junit.Assert.assertEquals(1, consents.size());
user.revokeConsent(clientId);
}
private CloseableHttpResponse sendRequest(String requestUrl, List<NameValuePair> parameters, Supplier<CloseableHttpClient> httpClientSupplier) throws Exception {
CloseableHttpClient client = httpClientSupplier.get();
try {
HttpPost post = new HttpPost(requestUrl);
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
post.setEntity(formEntity);
return client.execute(post);
} finally {
oauth.closeClient(client);
}
}
}

View file

@ -200,6 +200,8 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest {
protected static final String FAPI1_BASELINE_PROFILE_NAME = "fapi-1-baseline";
protected static final String FAPI1_ADVANCED_PROFILE_NAME = "fapi-1-advanced";
protected static final String FAPI_CIBA_PROFILE_NAME = "fapi-ciba";
protected static final String FAPI2_SECURITY_PROFILE_NAME = "fapi-2-security-profile";
protected static final String FAPI2_MESSAGE_SIGNING_PROFILE_NAME = "fapi-2-message-signing";
protected static final String ERR_MSG_MISSING_NONCE = "Missing parameter: nonce";
protected static final String ERR_MSG_MISSING_STATE = "Missing parameter: state";
@ -334,7 +336,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), 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), 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

@ -584,10 +584,18 @@ public class ClientPoliciesExtendedEventTest extends AbstractClientPoliciesTest
).toString();
updateProfiles(json);
String clientId = generateSuffixedName(CLIENT_NAME);
String clientSecret = "secret";
String cid = createClientByAdmin(clientId, (ClientRepresentation clientRep) -> {
clientRep.setSecret(clientSecret);
});
adminClient.realm(REALM_NAME).clients().get(cid).roles().create(RoleBuilder.create().name(SAMPLE_CLIENT_ROLE).build());
// register policies
json = (new ClientPoliciesBuilder()).addPolicy(
(new ClientPolicyBuilder()).createPolicy(POLICY_NAME, "La Premiere Politique", Boolean.TRUE)
.addCondition(AnyClientConditionFactory.PROVIDER_ID, createAnyClientConditionConfig())
(new ClientPolicyBuilder()).createPolicy(POLICY_NAME, "Dei Eischt Politik", Boolean.TRUE)
.addCondition(ClientRolesConditionFactory.PROVIDER_ID,
createClientRolesConditionConfig(Arrays.asList(SAMPLE_CLIENT_ROLE)))
.addProfile(PROFILE_NAME)
.toRepresentation()
).toString();
@ -595,7 +603,7 @@ public class ClientPoliciesExtendedEventTest extends AbstractClientPoliciesTest
// Authorization Request
oauth.realm(REALM_NAME);
oauth.clientId("foo");
oauth.clientId(clientId);
oauth.openLoginForm();
assertTrue(errorPage.isCurrent());
assertEquals("Exception thrown intentionally", errorPage.getError());

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), 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), Collections.emptyList());
// each profile - fapi-1-baseline
ClientProfileRepresentation actualProfileRep = getProfileRepresentation(actualProfilesRep, FAPI1_BASELINE_PROFILE_NAME, true);