mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-10 15:32:05 -03:30
Adding ConditionalClientScopeAuthenticator (#36020)
closes #36081 Signed-off-by: mposolda <mposolda@gmail.com> Co-authored-by: andymunro <48995441+andymunro@users.noreply.github.com> Signed-off-by: Marek Posolda <mposolda@gmail.com>
This commit is contained in:
parent
0a6099f295
commit
a3fd076960
@ -56,9 +56,11 @@ by the LDAP provider.
|
||||
|
||||
As OpenShift v3 reached end-of-life a while back, support for identity brokering with OpenShift v3 has been removed from Keycloak.
|
||||
|
||||
= New conditional authenticator `Condition - sub-flow executed`
|
||||
= New conditional authenticators `Condition - sub-flow executed` and `Condition - client scope`
|
||||
|
||||
The `Condition - sub-flow executed` is a new conditional authenticator in {project_name}. The condition checks if a previous sub-flow was executed (or not executed) successfully during the authentication flow execution. For more details, see link:{adminguide_link}#conditions-in-conditional-flows[Conditions in conditional flows].
|
||||
The `Condition - sub-flow executed` and `Condition - client scope` are new conditional authenticators in {project_name}. The condition `Condition - sub-flow executed` checks if a previous sub-flow was
|
||||
executed (or not executed) successfully during the authentication flow execution. The condition `Condition - client scope` checks if a configured client scope is present as a client scope of the
|
||||
client requesting authentication. For more details, see link:{adminguide_link}#conditions-in-conditional-flows[Conditions in conditional flows].
|
||||
|
||||
= Defining dependencies between provider factories
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
BIN
docs/documentation/server_admin/images/post-login-flow-otp.png
Normal file
BIN
docs/documentation/server_admin/images/post-login-flow-otp.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@ -50,6 +50,7 @@ include::topics/identity-broker/suggested.adoc[]
|
||||
include::topics/identity-broker/mappers.adoc[]
|
||||
include::topics/identity-broker/session-data.adoc[]
|
||||
include::topics/identity-broker/first-login-flow.adoc[]
|
||||
include::topics/identity-broker/post-login-flow.adoc[]
|
||||
include::topics/identity-broker/tokens.adoc[]
|
||||
include::topics/identity-broker/logout.adoc[]
|
||||
include::topics/sso-protocols.adoc[]
|
||||
|
||||
@ -54,6 +54,15 @@ The sub-flow name to check if it was executed or not executed. Required.
|
||||
Check result:::
|
||||
When the condition evaluates to true. If `executed` returns true when the configured sub-flow was executed with output success, false otherwise. If `not-executed` returns false when the sub-flow was executed with output success, true otherwise (the negation of the previous option).
|
||||
|
||||
`Condition - client scope`::
|
||||
The condition to evaluate if a configured client scope is present as a client scope of the client requesting authentication. These configuration fields exist:
|
||||
|
||||
Client scope name:::
|
||||
The name of the client scope, which should be present as a client scope of the client, which is requesting authentication. If requested client scope is default client scope of the client requesting login, the condition will be evaluated to true. If requested client scope is optional client scope of the client requesting login, condition will be evaluated to true if client scope is sent by the client in the login request (for example, by the `scope` parameter in case of OIDC/OAuth2 client login). Required.
|
||||
|
||||
Negate output:::
|
||||
Apply a NOT to the check result. When this is true, then the condition will evaluate to true just if configured client scope is not present.
|
||||
|
||||
|
||||
==== Explicitly deny/allow access in conditional flows
|
||||
|
||||
@ -124,6 +133,7 @@ image:images/2fa-example2-config.png[Configuration for the sub-flow executed]
|
||||
|
||||
The step `Deny access` denies the authentication if not executed.
|
||||
|
||||
[[_conditional-2fa-otp-default]]
|
||||
===== Conditional 2FA sub-flow with OTP default
|
||||
|
||||
The last example is very similar to the previous one. Instead of denying the access, step `OTP Form` is configured as required.
|
||||
@ -131,4 +141,4 @@ The last example is very similar to the previous one. Instead of denying the acc
|
||||
.2FA all alternative with OTP default
|
||||
image:images/2fa-example3.png[2FA all alternative with OTP default]
|
||||
|
||||
With this flow, if the user has none of the 2FA methods configured, the OTP setup will be enforced to continue the login.
|
||||
With this flow, if the user has none of the 2FA methods configured, the OTP setup will be enforced to continue the login.
|
||||
|
||||
@ -14,3 +14,6 @@
|
||||
If {project_name} does not find the configured default identity provider, the login form is displayed.
|
||||
|
||||
This authenticator is responsible for processing the `kc_idp_hint` query parameter. See the <<_client_suggested_idp, client suggested identity provider>> section for more information.
|
||||
|
||||
NOTE: The authenticator will redirect to the identity provider and authentication is delegated to the identity provider. The `browser` authentication flow will not continue after the login with the identity provider
|
||||
is successfully finished. If you want to perform additional steps after the identity provider login (for example 2-factor authentication), it may be needed to configure <<_identity_broker_post_login_flow, Post login flow>>.
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
[[_identity_broker_post_login_flow]]
|
||||
|
||||
=== Post login flow
|
||||
|
||||
Post login flow is useful for the situations when you want to trigger some additional authentication actions after every login with the particular identity provider.
|
||||
For example, you may want to trigger 2-factor authentication after every login of {project_name} to `Facebook` because `Facebook` does not provide 2-factor authentication during its login.
|
||||
|
||||
Once you setup the authentication flow with the needed steps, set it as `Post login flow` when configuring the identity provider.
|
||||
|
||||
==== Post login flow examples
|
||||
|
||||
===== Requesting 2-factor authentication after identity provider login
|
||||
|
||||
The easiest way is to enforce authentication with one particular 2-factor method. For example, when requesting OTP, the flow can look like this with only a single authenticator configured.
|
||||
This type of flow asks the user to configure the OTP during the first login with the identity provide when the user does not have OTP set on the account.
|
||||
|
||||
.2FA post login flow with OTP
|
||||
image:images/post-login-flow-otp.png[Post login OTP]
|
||||
|
||||
The more complex setup can include multiple 2-factor authentication methods configured as `ALTERNATIVE`. In this case, make sure that the user is requested to setup one of
|
||||
the methods if that user does not yet have any 2-factor authentication configured on the account. This could be done as follows:
|
||||
|
||||
* Make sure that one of the 2-factor methods is configured as `REQUIRED` in the <<_identity_broker_first_login, First login flow>>. This method can works if you expect all your users to be registered by
|
||||
the identity provider login.
|
||||
|
||||
* Wrap the 2-factor methods as `ALTERNATIVE` into a conditional subflow such as one called `2FA` and create another conditional subflow such as one called `OTP if no 2FA`,
|
||||
which will be triggered only if the previous subflow was not executed and will ask user to add one of the 2-factor methods (for example, OTP). The example of a similar flow configuration is provided
|
||||
in the <<_conditional-2fa-otp-default, Conditions section of the Authentication flows chapter>>.
|
||||
|
||||
==== Requesting additional authentication steps for the dedicated clients
|
||||
|
||||
In some cases, a client or group of clients may need to perform some additional steps after identity provider login.
|
||||
The following is an example of a flow that prescribes that when the client scope `foo` is requested, the user is required to authenticate with the OTP after identity provider login.
|
||||
|
||||
.2FA post login flow with client scope and OTP
|
||||
image:images/post-login-flow-client-scope.png[Post login with client scope and OTP]
|
||||
|
||||
This is an example of configuring the `Condition - client scope` for requesting the specified client scope.
|
||||
|
||||
.2FA post login flow client scope configuration
|
||||
image:images/post-login-flow-client-scope-config.png[Post login flow client scope configuration]
|
||||
|
||||
The requested clients need to have this client scope set on them either
|
||||
as default or as optional. In the latter case, the flow is executed only if the client scope is requested by the client (for example, by the `scope` parameter in the case of OIDC/OAuth2 client logins).
|
||||
@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
*
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.authentication.authenticators.conditional;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.models.AuthenticatorConfigModel;
|
||||
import org.keycloak.models.ClientScopeModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.protocol.oidc.TokenManager;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
|
||||
/**
|
||||
* Conditional authenticator to check if specified client-scope is present in the authentication request
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class ConditionalClientScopeAuthenticator implements ConditionalAuthenticator {
|
||||
|
||||
protected static final ConditionalClientScopeAuthenticator SINGLETON = new ConditionalClientScopeAuthenticator();
|
||||
|
||||
private static final Logger logger = Logger.getLogger(ConditionalClientScopeAuthenticator.class);
|
||||
|
||||
@Override
|
||||
public boolean matchCondition(AuthenticationFlowContext context) {
|
||||
final AuthenticatorConfigModel configModel = context.getAuthenticatorConfig();
|
||||
if (configModel == null || configModel.getConfig() == null) {
|
||||
logger.warnf("No configuration defined for the conditional client scope authenticator.");
|
||||
return false;
|
||||
}
|
||||
|
||||
final String clientScopeName = configModel.getConfig().get(ConditionalClientScopeAuthenticatorFactory.CLIENT_SCOPE);
|
||||
boolean negateOutput = Boolean.parseBoolean(configModel.getConfig().get(ConditionalClientScopeAuthenticatorFactory.CONF_NEGATE));
|
||||
if (clientScopeName == null) {
|
||||
logger.warnf("No client scope configured in the option '%s' of the configuration '%s'.", ConditionalClientScopeAuthenticatorFactory.CLIENT_SCOPE, configModel.getAlias());
|
||||
return false;
|
||||
}
|
||||
|
||||
final RealmModel realm = context.getRealm();
|
||||
ClientScopeModel targetClientScope = KeycloakModelUtils.getClientScopeByName(context.getRealm(), clientScopeName);
|
||||
if (targetClientScope == null) {
|
||||
logger.warnf("No client scope '%s' defined in the realm '%s'.", clientScopeName, realm.getName());
|
||||
return false;
|
||||
}
|
||||
|
||||
AuthenticationSessionModel authSession = context.getAuthenticationSession();
|
||||
boolean clientScopePresent = TokenManager.getRequestedClientScopes(context.getSession(), authSession.getClientNote(OAuth2Constants.SCOPE), authSession.getClient(), authSession.getAuthenticatedUser())
|
||||
.anyMatch(clientScope -> targetClientScope.getId().equals(clientScope.getId()));
|
||||
|
||||
return negateOutput != clientScopePresent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void action(AuthenticationFlowContext context) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresUser() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
*
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.authentication.authenticators.conditional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class ConditionalClientScopeAuthenticatorFactory implements ConditionalAuthenticatorFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "conditional-client-scope";
|
||||
public static final String CLIENT_SCOPE = "client_scope";
|
||||
public static final String CONF_NEGATE = "negate";
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Condition - client scope";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConfigurable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
|
||||
return new AuthenticationExecutionModel.Requirement[]{AuthenticationExecutionModel.Requirement.REQUIRED, AuthenticationExecutionModel.Requirement.DISABLED};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUserSetupAllowed() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Condition to evaluate if a configured client scope is present as a client scope of the client requesting authentication";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return ProviderConfigurationBuilder.create()
|
||||
.property()
|
||||
.name(CLIENT_SCOPE)
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.label("Client scope name")
|
||||
.helpText("The name of the client scope, which should be present as a client scope of the client, which is requesting authentication. If requested client scope is default client scope of the client requesting login, the condition will be evaluated to true. If requested client scope is optional client scope of the client requesting login, condition will be evaluated to true if client scope is sent by the client in the login request (EG. by the 'scope' parameter in case of OIDC/OAuth2 client login)")
|
||||
.required(true)
|
||||
.add()
|
||||
.property()
|
||||
.name(CONF_NEGATE)
|
||||
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||
.label("Negate output")
|
||||
.helpText(
|
||||
"Apply a NOT to the check result. When this is true, then the condition will evaluate to true just if configured client scope is not present"
|
||||
)
|
||||
.required(true)
|
||||
.add()
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConditionalAuthenticator getSingleton() {
|
||||
return ConditionalClientScopeAuthenticator.SINGLETON;
|
||||
}
|
||||
}
|
||||
@ -23,6 +23,7 @@ org.keycloak.authentication.authenticators.browser.OTPFormAuthenticatorFactory
|
||||
org.keycloak.authentication.authenticators.browser.SpnegoAuthenticatorFactory
|
||||
org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticatorFactory
|
||||
org.keycloak.authentication.authenticators.conditional.ConditionalSubFlowExecutedAuthenticatorFactory
|
||||
org.keycloak.authentication.authenticators.conditional.ConditionalClientScopeAuthenticatorFactory
|
||||
org.keycloak.authentication.authenticators.conditional.ConditionalRoleAuthenticatorFactory
|
||||
org.keycloak.authentication.authenticators.conditional.ConditionalUserConfiguredAuthenticatorFactory
|
||||
org.keycloak.authentication.authenticators.conditional.ConditionalLoaAuthenticatorFactory
|
||||
|
||||
@ -235,6 +235,7 @@ public class ProvidersTest extends AbstractAuthenticationTest {
|
||||
addProviderInfo(result, "idp-add-organization-member", "Organization Member Onboard", "Adds a federated user as a member of an organization");
|
||||
addProviderInfo(result, "organization", "Organization Identity-First Login", "If organizations are enabled, automatically redirects users to the corresponding identity provider.");
|
||||
addProviderInfo(result, "conditional-sub-flow-executed", "Condition - sub-flow executed", "Condition to evaluate if a sub-flow was executed successfully during the authentication process");
|
||||
addProviderInfo(result, "conditional-client-scope", "Condition - client scope", "Condition to evaluate if a configured client scope is present as a client scope of the client requesting authentication");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -47,7 +47,6 @@ import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.testsuite.Assert;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.broker.util.SimpleHttpDefault;
|
||||
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
|
||||
import org.keycloak.testsuite.util.AccountHelper;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
import org.keycloak.testsuite.util.WaitUtils;
|
||||
@ -68,9 +67,6 @@ import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.keycloak.models.utils.TimeBasedOTP.DEFAULT_INTERVAL_SECONDS;
|
||||
import static org.keycloak.testsuite.admin.ApiUtil.removeUserByUsername;
|
||||
import static org.keycloak.testsuite.broker.BrokerRunOnServerUtil.configurePostBrokerLoginWithOTP;
|
||||
import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_ALIAS;
|
||||
import static org.keycloak.testsuite.broker.BrokerTestConstants.REALM_PROV_NAME;
|
||||
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
|
||||
@ -279,165 +275,6 @@ public final class KcOidcBrokerTest extends AbstractAdvancedBrokerTest {
|
||||
Assert.assertEquals("hard-coded", user.getAttributes().get("hard-coded").get(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Refers to in old test suite: PostBrokerFlowTest#testBrokerReauthentication_samlBrokerWithOTPRequired
|
||||
*/
|
||||
@Test
|
||||
public void testReauthenticationSamlBrokerWithOTPRequired() throws Exception {
|
||||
KcSamlBrokerConfiguration samlBrokerConfig = KcSamlBrokerConfiguration.INSTANCE;
|
||||
ClientRepresentation samlClient = samlBrokerConfig.createProviderClients().get(0);
|
||||
IdentityProviderRepresentation samlBroker = samlBrokerConfig.setUpIdentityProvider();
|
||||
RealmResource consumerRealm = adminClient.realm(bc.consumerRealmName());
|
||||
|
||||
try {
|
||||
updateExecutions(AbstractBrokerTest::disableUpdateProfileOnFirstLogin);
|
||||
adminClient.realm(bc.providerRealmName()).clients().create(samlClient);
|
||||
consumerRealm.identityProviders().create(samlBroker);
|
||||
|
||||
oauth.clientId("broker-app");
|
||||
loginPage.open(bc.consumerRealmName());
|
||||
|
||||
testingClient.server(bc.consumerRealmName()).run(configurePostBrokerLoginWithOTP(samlBrokerConfig.getIDPAlias()));
|
||||
logInWithBroker(samlBrokerConfig);
|
||||
|
||||
totpPage.assertCurrent();
|
||||
String totpSecret = totpPage.getTotpSecret();
|
||||
totpPage.configure(totp.generateTOTP(totpSecret));
|
||||
|
||||
AccountHelper.logout(adminClient.realm(bc.consumerRealmName()), bc.getUserLogin());
|
||||
AccountHelper.logout(adminClient.realm(bc.providerRealmName()), bc.getUserLogin());
|
||||
|
||||
setOtpTimeOffset(DEFAULT_INTERVAL_SECONDS, totp);
|
||||
|
||||
oauth.clientId("broker-app");
|
||||
loginPage.open(bc.consumerRealmName());
|
||||
|
||||
logInWithBroker(bc);
|
||||
|
||||
waitForPage(driver, "account already exists", false);
|
||||
idpConfirmLinkPage.assertCurrent();
|
||||
idpConfirmLinkPage.clickLinkAccount();
|
||||
|
||||
loginPage.clickSocial(samlBrokerConfig.getIDPAlias());
|
||||
waitForPage(driver, "sign in to", true);
|
||||
log.debug("Logging in");
|
||||
loginTotpPage.login(totp.generateTOTP(totpSecret));
|
||||
|
||||
assertNumFederatedIdentities(consumerRealm.users().search(samlBrokerConfig.getUserLogin()).get(0).getId(), 2);
|
||||
} finally {
|
||||
updateExecutions(AbstractBrokerTest::setUpMissingUpdateProfileOnFirstLogin);
|
||||
removeUserByUsername(consumerRealm, "consumer");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refers to in old test suite: PostBrokerFlowTest#testBrokerReauthentication_oidcBrokerWithOTPRequired
|
||||
*/
|
||||
@Test
|
||||
public void testReauthenticationOIDCBrokerWithOTPRequired() throws Exception {
|
||||
KcSamlBrokerConfiguration samlBrokerConfig = KcSamlBrokerConfiguration.INSTANCE;
|
||||
ClientRepresentation samlClient = samlBrokerConfig.createProviderClients().get(0);
|
||||
IdentityProviderRepresentation samlBroker = samlBrokerConfig.setUpIdentityProvider();
|
||||
RealmResource consumerRealm = adminClient.realm(bc.consumerRealmName());
|
||||
|
||||
try {
|
||||
updateExecutions(AbstractBrokerTest::disableUpdateProfileOnFirstLogin);
|
||||
adminClient.realm(bc.providerRealmName()).clients().create(samlClient);
|
||||
consumerRealm.identityProviders().create(samlBroker);
|
||||
|
||||
oauth.clientId("broker-app");
|
||||
loginPage.open(bc.consumerRealmName());
|
||||
|
||||
logInWithBroker(samlBrokerConfig);
|
||||
AccountHelper.logout(adminClient.realm(bc.consumerRealmName()), bc.getUserLogin());
|
||||
AccountHelper.logout(adminClient.realm(bc.providerRealmName()), bc.getUserLogin());
|
||||
|
||||
testingClient.server(bc.consumerRealmName()).run(configurePostBrokerLoginWithOTP(bc.getIDPAlias()));
|
||||
|
||||
oauth.clientId("broker-app");
|
||||
loginPage.open(bc.consumerRealmName());
|
||||
|
||||
logInWithBroker(bc);
|
||||
|
||||
waitForPage(driver, "account already exists", false);
|
||||
idpConfirmLinkPage.assertCurrent();
|
||||
idpConfirmLinkPage.clickLinkAccount();
|
||||
loginPage.clickSocial(samlBrokerConfig.getIDPAlias());
|
||||
|
||||
totpPage.assertCurrent();
|
||||
String totpSecret = totpPage.getTotpSecret();
|
||||
totpPage.configure(totp.generateTOTP(totpSecret));
|
||||
logoutFromRealm(getConsumerRoot(), bc.consumerRealmName());
|
||||
|
||||
assertNumFederatedIdentities(consumerRealm.users().search(samlBrokerConfig.getUserLogin()).get(0).getId(), 2);
|
||||
} finally {
|
||||
updateExecutions(AbstractBrokerTest::setUpMissingUpdateProfileOnFirstLogin);
|
||||
removeUserByUsername(consumerRealm, "consumer");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refers to in old test suite: PostBrokerFlowTest#testBrokerReauthentication_bothBrokerWithOTPRequired
|
||||
*/
|
||||
@Test
|
||||
public void testReauthenticationBothBrokersWithOTPRequired() throws Exception {
|
||||
final RealmResource consumerRealm = adminClient.realm(bc.consumerRealmName());
|
||||
final RealmResource providerRealm = adminClient.realm(bc.providerRealmName());
|
||||
|
||||
try (RealmAttributeUpdater rauConsumer = new RealmAttributeUpdater(consumerRealm).setOtpPolicyCodeReusable(true).update();
|
||||
RealmAttributeUpdater rauProvider = new RealmAttributeUpdater(providerRealm).setOtpPolicyCodeReusable(true).update()) {
|
||||
|
||||
KcSamlBrokerConfiguration samlBrokerConfig = KcSamlBrokerConfiguration.INSTANCE;
|
||||
ClientRepresentation samlClient = samlBrokerConfig.createProviderClients().get(0);
|
||||
IdentityProviderRepresentation samlBroker = samlBrokerConfig.setUpIdentityProvider();
|
||||
|
||||
try {
|
||||
updateExecutions(AbstractBrokerTest::disableUpdateProfileOnFirstLogin);
|
||||
providerRealm.clients().create(samlClient);
|
||||
consumerRealm.identityProviders().create(samlBroker);
|
||||
|
||||
oauth.clientId("broker-app");
|
||||
loginPage.open(bc.consumerRealmName());
|
||||
|
||||
testingClient.server(bc.consumerRealmName()).run(configurePostBrokerLoginWithOTP(samlBrokerConfig.getIDPAlias()));
|
||||
logInWithBroker(samlBrokerConfig);
|
||||
totpPage.assertCurrent();
|
||||
String totpSecret = totpPage.getTotpSecret();
|
||||
totpPage.configure(totp.generateTOTP(totpSecret));
|
||||
AccountHelper.logout(adminClient.realm(bc.consumerRealmName()), bc.getUserLogin());
|
||||
AccountHelper.logout(adminClient.realm(bc.providerRealmName()), bc.getUserLogin());
|
||||
|
||||
testingClient.server(bc.consumerRealmName()).run(configurePostBrokerLoginWithOTP(bc.getIDPAlias()));
|
||||
oauth.clientId("broker-app");
|
||||
loginPage.open(bc.consumerRealmName());
|
||||
|
||||
logInWithBroker(bc);
|
||||
|
||||
waitForPage(driver, "account already exists", false);
|
||||
idpConfirmLinkPage.assertCurrent();
|
||||
idpConfirmLinkPage.clickLinkAccount();
|
||||
loginPage.clickSocial(samlBrokerConfig.getIDPAlias());
|
||||
|
||||
loginTotpPage.assertCurrent();
|
||||
loginTotpPage.login(totp.generateTOTP(totpSecret));
|
||||
AccountHelper.logout(adminClient.realm(bc.consumerRealmName()), bc.getUserLogin());
|
||||
AccountHelper.logout(adminClient.realm(bc.providerRealmName()), bc.getUserLogin());
|
||||
|
||||
oauth.clientId("broker-app");
|
||||
loginPage.open(bc.consumerRealmName());
|
||||
|
||||
logInWithBroker(bc);
|
||||
|
||||
loginTotpPage.assertCurrent();
|
||||
loginTotpPage.login(totp.generateTOTP(totpSecret));
|
||||
|
||||
assertNumFederatedIdentities(consumerRealm.users().search(samlBrokerConfig.getUserLogin()).get(0).getId(), 2);
|
||||
} finally {
|
||||
updateExecutions(AbstractBrokerTest::setUpMissingUpdateProfileOnFirstLogin);
|
||||
removeUserByUsername(consumerRealm, "consumer");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidIssuedFor() {
|
||||
|
||||
@ -0,0 +1,314 @@
|
||||
/*
|
||||
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
*
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.testsuite.broker;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.authentication.AuthenticationFlow;
|
||||
import org.keycloak.authentication.authenticators.access.AllowAccessAuthenticatorFactory;
|
||||
import org.keycloak.authentication.authenticators.browser.OTPFormAuthenticatorFactory;
|
||||
import org.keycloak.authentication.authenticators.conditional.ConditionalClientScopeAuthenticatorFactory;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.AuthenticationFlowModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.utils.TimeBasedOTP;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
import org.keycloak.testsuite.client.KeycloakTestingClient;
|
||||
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
|
||||
import org.keycloak.testsuite.util.AccountHelper;
|
||||
import org.keycloak.testsuite.util.FlowUtil;
|
||||
import static org.keycloak.models.utils.TimeBasedOTP.DEFAULT_INTERVAL_SECONDS;
|
||||
import static org.keycloak.testsuite.admin.ApiUtil.removeUserByUsername;
|
||||
import static org.keycloak.testsuite.broker.BrokerRunOnServerUtil.configurePostBrokerLoginWithOTP;
|
||||
import static org.keycloak.testsuite.broker.BrokerTestTools.getConsumerRoot;
|
||||
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class KcOidcPostBrokerLoginTest extends AbstractInitializedBaseBrokerTest {
|
||||
|
||||
private static final KcOidcBrokerConfiguration BROKER_CONFIG_INSTANCE = new KcOidcBrokerConfiguration();
|
||||
|
||||
@Rule
|
||||
public AssertEvents events = new AssertEvents(this);
|
||||
|
||||
@Override
|
||||
protected BrokerConfiguration getBrokerConfiguration() {
|
||||
return BROKER_CONFIG_INSTANCE;
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUpTotp() {
|
||||
totp = new TimeBasedOTP();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testReauthenticationSamlBrokerWithOTPRequired() throws Exception {
|
||||
KcSamlBrokerConfiguration samlBrokerConfig = KcSamlBrokerConfiguration.INSTANCE;
|
||||
ClientRepresentation samlClient = samlBrokerConfig.createProviderClients().get(0);
|
||||
IdentityProviderRepresentation samlBroker = samlBrokerConfig.setUpIdentityProvider();
|
||||
RealmResource consumerRealm = adminClient.realm(bc.consumerRealmName());
|
||||
|
||||
try {
|
||||
updateExecutions(AbstractBrokerTest::disableUpdateProfileOnFirstLogin);
|
||||
adminClient.realm(bc.providerRealmName()).clients().create(samlClient);
|
||||
consumerRealm.identityProviders().create(samlBroker);
|
||||
|
||||
oauth.clientId("broker-app");
|
||||
loginPage.open(bc.consumerRealmName());
|
||||
|
||||
testingClient.server(bc.consumerRealmName()).run(configurePostBrokerLoginWithOTP(samlBrokerConfig.getIDPAlias()));
|
||||
logInWithBroker(samlBrokerConfig);
|
||||
|
||||
totpPage.assertCurrent();
|
||||
String totpSecret = totpPage.getTotpSecret();
|
||||
totpPage.configure(totp.generateTOTP(totpSecret));
|
||||
|
||||
AccountHelper.logout(adminClient.realm(bc.consumerRealmName()), bc.getUserLogin());
|
||||
AccountHelper.logout(adminClient.realm(bc.providerRealmName()), bc.getUserLogin());
|
||||
|
||||
setOtpTimeOffset(DEFAULT_INTERVAL_SECONDS, totp);
|
||||
|
||||
oauth.clientId("broker-app");
|
||||
loginPage.open(bc.consumerRealmName());
|
||||
|
||||
logInWithBroker(bc);
|
||||
|
||||
waitForPage(driver, "account already exists", false);
|
||||
idpConfirmLinkPage.assertCurrent();
|
||||
idpConfirmLinkPage.clickLinkAccount();
|
||||
|
||||
loginPage.clickSocial(samlBrokerConfig.getIDPAlias());
|
||||
waitForPage(driver, "sign in to", true);
|
||||
log.debug("Logging in");
|
||||
loginTotpPage.login(totp.generateTOTP(totpSecret));
|
||||
|
||||
assertNumFederatedIdentities(consumerRealm.users().search(samlBrokerConfig.getUserLogin()).get(0).getId(), 2);
|
||||
} finally {
|
||||
updateExecutions(AbstractBrokerTest::setUpMissingUpdateProfileOnFirstLogin);
|
||||
removeUserByUsername(consumerRealm, "consumer");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testReauthenticationOIDCBrokerWithOTPRequired() throws Exception {
|
||||
KcSamlBrokerConfiguration samlBrokerConfig = KcSamlBrokerConfiguration.INSTANCE;
|
||||
ClientRepresentation samlClient = samlBrokerConfig.createProviderClients().get(0);
|
||||
IdentityProviderRepresentation samlBroker = samlBrokerConfig.setUpIdentityProvider();
|
||||
RealmResource consumerRealm = adminClient.realm(bc.consumerRealmName());
|
||||
|
||||
try {
|
||||
updateExecutions(AbstractBrokerTest::disableUpdateProfileOnFirstLogin);
|
||||
adminClient.realm(bc.providerRealmName()).clients().create(samlClient);
|
||||
consumerRealm.identityProviders().create(samlBroker);
|
||||
|
||||
oauth.clientId("broker-app");
|
||||
loginPage.open(bc.consumerRealmName());
|
||||
|
||||
logInWithBroker(samlBrokerConfig);
|
||||
AccountHelper.logout(adminClient.realm(bc.consumerRealmName()), bc.getUserLogin());
|
||||
AccountHelper.logout(adminClient.realm(bc.providerRealmName()), bc.getUserLogin());
|
||||
|
||||
testingClient.server(bc.consumerRealmName()).run(configurePostBrokerLoginWithOTP(bc.getIDPAlias()));
|
||||
|
||||
oauth.clientId("broker-app");
|
||||
loginPage.open(bc.consumerRealmName());
|
||||
|
||||
logInWithBroker(bc);
|
||||
|
||||
waitForPage(driver, "account already exists", false);
|
||||
idpConfirmLinkPage.assertCurrent();
|
||||
idpConfirmLinkPage.clickLinkAccount();
|
||||
loginPage.clickSocial(samlBrokerConfig.getIDPAlias());
|
||||
|
||||
totpPage.assertCurrent();
|
||||
String totpSecret = totpPage.getTotpSecret();
|
||||
totpPage.configure(totp.generateTOTP(totpSecret));
|
||||
logoutFromRealm(getConsumerRoot(), bc.consumerRealmName());
|
||||
|
||||
assertNumFederatedIdentities(consumerRealm.users().search(samlBrokerConfig.getUserLogin()).get(0).getId(), 2);
|
||||
} finally {
|
||||
updateExecutions(AbstractBrokerTest::setUpMissingUpdateProfileOnFirstLogin);
|
||||
removeUserByUsername(consumerRealm, "consumer");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testReauthenticationBothBrokersWithOTPRequired() throws Exception {
|
||||
final RealmResource consumerRealm = adminClient.realm(bc.consumerRealmName());
|
||||
final RealmResource providerRealm = adminClient.realm(bc.providerRealmName());
|
||||
|
||||
try (RealmAttributeUpdater rauConsumer = new RealmAttributeUpdater(consumerRealm).setOtpPolicyCodeReusable(true).update();
|
||||
RealmAttributeUpdater rauProvider = new RealmAttributeUpdater(providerRealm).setOtpPolicyCodeReusable(true).update()) {
|
||||
|
||||
KcSamlBrokerConfiguration samlBrokerConfig = KcSamlBrokerConfiguration.INSTANCE;
|
||||
ClientRepresentation samlClient = samlBrokerConfig.createProviderClients().get(0);
|
||||
IdentityProviderRepresentation samlBroker = samlBrokerConfig.setUpIdentityProvider();
|
||||
|
||||
try {
|
||||
updateExecutions(AbstractBrokerTest::disableUpdateProfileOnFirstLogin);
|
||||
providerRealm.clients().create(samlClient);
|
||||
consumerRealm.identityProviders().create(samlBroker);
|
||||
|
||||
oauth.clientId("broker-app");
|
||||
loginPage.open(bc.consumerRealmName());
|
||||
|
||||
testingClient.server(bc.consumerRealmName()).run(configurePostBrokerLoginWithOTP(samlBrokerConfig.getIDPAlias()));
|
||||
logInWithBroker(samlBrokerConfig);
|
||||
totpPage.assertCurrent();
|
||||
String totpSecret = totpPage.getTotpSecret();
|
||||
totpPage.configure(totp.generateTOTP(totpSecret));
|
||||
AccountHelper.logout(adminClient.realm(bc.consumerRealmName()), bc.getUserLogin());
|
||||
AccountHelper.logout(adminClient.realm(bc.providerRealmName()), bc.getUserLogin());
|
||||
|
||||
testingClient.server(bc.consumerRealmName()).run(configurePostBrokerLoginWithOTP(bc.getIDPAlias()));
|
||||
oauth.clientId("broker-app");
|
||||
loginPage.open(bc.consumerRealmName());
|
||||
|
||||
logInWithBroker(bc);
|
||||
|
||||
waitForPage(driver, "account already exists", false);
|
||||
idpConfirmLinkPage.assertCurrent();
|
||||
idpConfirmLinkPage.clickLinkAccount();
|
||||
loginPage.clickSocial(samlBrokerConfig.getIDPAlias());
|
||||
|
||||
loginTotpPage.assertCurrent();
|
||||
loginTotpPage.login(totp.generateTOTP(totpSecret));
|
||||
AccountHelper.logout(adminClient.realm(bc.consumerRealmName()), bc.getUserLogin());
|
||||
AccountHelper.logout(adminClient.realm(bc.providerRealmName()), bc.getUserLogin());
|
||||
|
||||
oauth.clientId("broker-app");
|
||||
loginPage.open(bc.consumerRealmName());
|
||||
|
||||
logInWithBroker(bc);
|
||||
|
||||
loginTotpPage.assertCurrent();
|
||||
loginTotpPage.login(totp.generateTOTP(totpSecret));
|
||||
|
||||
assertNumFederatedIdentities(consumerRealm.users().search(samlBrokerConfig.getUserLogin()).get(0).getId(), 2);
|
||||
} finally {
|
||||
updateExecutions(AbstractBrokerTest::setUpMissingUpdateProfileOnFirstLogin);
|
||||
removeUserByUsername(consumerRealm, "consumer");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testPostBrokerLoginFlowWithOTP() {
|
||||
updateExecutions(AbstractBrokerTest::disableUpdateProfileOnFirstLogin);
|
||||
|
||||
// Setup with default client scope - OTP required
|
||||
configurePostBrokerLoginWithClientScopeConditionAndOTP(testingClient, bc.consumerRealmName(), bc.getIDPAlias(), OAuth2Constants.SCOPE_PROFILE, false);
|
||||
|
||||
oauth.clientId("broker-app");
|
||||
loginPage.open(bc.consumerRealmName());
|
||||
logInWithBroker(bc);
|
||||
|
||||
totpPage.assertCurrent();
|
||||
String totpSecret = totpPage.getTotpSecret();
|
||||
totpPage.configure(totp.generateTOTP(totpSecret));
|
||||
|
||||
RealmResource realm = adminClient.realm(bc.consumerRealmName());
|
||||
assertNumFederatedIdentities(realm.users().search(bc.getUserLogin()).get(0).getId(), 1);
|
||||
|
||||
appPage.assertCurrent();
|
||||
AccountHelper.logout(adminClient.realm(bc.consumerRealmName()), bc.getUserLogin());
|
||||
AccountHelper.logout(adminClient.realm(bc.providerRealmName()), bc.getUserLogin());
|
||||
|
||||
// Setup with optional client scope - scope not present in scope parameter, OTP not required
|
||||
configurePostBrokerLoginWithClientScopeConditionAndOTP(testingClient, bc.consumerRealmName(), bc.getIDPAlias(), OAuth2Constants.SCOPE_PHONE, false);
|
||||
setOtpTimeOffset(DEFAULT_INTERVAL_SECONDS, totp);
|
||||
|
||||
loginPage.open(bc.consumerRealmName());
|
||||
logInWithBroker(bc);
|
||||
|
||||
appPage.assertCurrent();
|
||||
AccountHelper.logout(adminClient.realm(bc.consumerRealmName()), bc.getUserLogin());
|
||||
AccountHelper.logout(adminClient.realm(bc.providerRealmName()), bc.getUserLogin());
|
||||
|
||||
// Setup with optional client scope - scope parameter present, OTP required
|
||||
oauth.scope("openid phone");
|
||||
|
||||
loginPage.open(bc.consumerRealmName());
|
||||
logInWithBroker(bc);
|
||||
|
||||
loginTotpPage.assertCurrent();
|
||||
loginTotpPage.login(totp.generateTOTP(totpSecret));
|
||||
|
||||
appPage.assertCurrent();
|
||||
AccountHelper.logout(adminClient.realm(bc.consumerRealmName()), bc.getUserLogin());
|
||||
AccountHelper.logout(adminClient.realm(bc.providerRealmName()), bc.getUserLogin());
|
||||
|
||||
// Setup with optional client scope with negate - scope parameter present, OTP not required
|
||||
configurePostBrokerLoginWithClientScopeConditionAndOTP(testingClient, bc.consumerRealmName(), bc.getIDPAlias(), OAuth2Constants.SCOPE_PHONE, true);
|
||||
|
||||
oauth.scope("openid phone");
|
||||
|
||||
loginPage.open(bc.consumerRealmName());
|
||||
logInWithBroker(bc);
|
||||
|
||||
appPage.assertCurrent();
|
||||
AccountHelper.logout(adminClient.realm(bc.consumerRealmName()), bc.getUserLogin());
|
||||
AccountHelper.logout(adminClient.realm(bc.providerRealmName()), bc.getUserLogin());
|
||||
}
|
||||
|
||||
static void configurePostBrokerLoginWithClientScopeConditionAndOTP(KeycloakTestingClient testingClient, String consumerRealmName, String idpAlias, String clientScopeName, boolean negate) {
|
||||
testingClient.server(consumerRealmName).run(session -> {
|
||||
AuthenticationFlowModel flowModel = session.getContext().getRealm().getFlowByAlias("post-broker");
|
||||
if (flowModel == null) {
|
||||
flowModel = FlowUtil.createFlowModel("post-broker", "basic-flow", "post-broker flow with client-scope condition and OTP", true, false);
|
||||
session.getContext().getRealm().addAuthenticationFlow(flowModel);
|
||||
}
|
||||
|
||||
FlowUtil.inCurrentRealm(session)
|
||||
// Select new flow
|
||||
.selectFlow("post-broker")
|
||||
.clear()
|
||||
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, AllowAccessAuthenticatorFactory.PROVIDER_ID)
|
||||
.addSubFlowExecution("OTP requested when client scope", AuthenticationFlow.BASIC_FLOW, AuthenticationExecutionModel.Requirement.CONDITIONAL, (flowUtil) -> {
|
||||
flowUtil.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, ConditionalClientScopeAuthenticatorFactory.PROVIDER_ID, (configModel) -> {
|
||||
Map<String, String> config = new HashMap<>();
|
||||
config.put(ConditionalClientScopeAuthenticatorFactory.CLIENT_SCOPE, clientScopeName);
|
||||
config.put(ConditionalClientScopeAuthenticatorFactory.CONF_NEGATE, String.valueOf(negate));
|
||||
configModel.setConfig(config);
|
||||
configModel.setAlias("condition - client scope");
|
||||
})
|
||||
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, OTPFormAuthenticatorFactory.PROVIDER_ID);
|
||||
});
|
||||
IdentityProviderModel idp = session.identityProviders().getByAlias(idpAlias);
|
||||
idp.setPostBrokerLoginFlowId(flowModel.getId());
|
||||
session.identityProviders().update(idp);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user