Add FAPI 2.0 security profile as default profile of client policies
closes #21181
This commit is contained in:
parent
c2d5cc67af
commit
ee998fee66
10 changed files with 978 additions and 242 deletions
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue