KEYCLOAK-16134 Allow webauthn idless login flow (#7860)

Closes #10832
This commit is contained in:
Joaquim Fellmann 2022-03-21 11:37:33 +01:00 committed by GitHub
parent 183ad30755
commit 92c4e6d585
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 615 additions and 44 deletions

View file

@ -62,6 +62,7 @@ class AuthenticationSelectionResolver {
*/ */
static List<AuthenticationSelectionOption> createAuthenticationSelectionList(AuthenticationProcessor processor, AuthenticationExecutionModel model) { static List<AuthenticationSelectionOption> createAuthenticationSelectionList(AuthenticationProcessor processor, AuthenticationExecutionModel model) {
List<AuthenticationSelectionOption> authenticationSelectionList = new ArrayList<>(); List<AuthenticationSelectionOption> authenticationSelectionList = new ArrayList<>();
List<AuthenticationSelectionOption> userlessCredBasedAuthenticationSelectionList = new ArrayList<>();
if (processor.getAuthenticationSession() != null) { if (processor.getAuthenticationSession() != null) {
Map<String, AuthenticationExecutionModel> typeAuthExecMap = new HashMap<>(); Map<String, AuthenticationExecutionModel> typeAuthExecMap = new HashMap<>();
@ -91,11 +92,24 @@ class AuthenticationSelectionResolver {
.map(credentialType -> new AuthenticationSelectionOption(processor.getSession(), typeAuthExecMap.get(credentialType))) .map(credentialType -> new AuthenticationSelectionOption(processor.getSession(), typeAuthExecMap.get(credentialType)))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
else {
// No user associated with session. Check if this flow contains executions linked to authenticators that don't require a user
typeAuthExecMap.forEach((key, value) -> {
AuthenticatorFactory credbasedAuthenticatorFactory = (AuthenticatorFactory) processor.getSession().getKeycloakSessionFactory().getProviderFactory(Authenticator.class, value.getAuthenticator());
Authenticator credbasedAuthenticator = credbasedAuthenticatorFactory.create(processor.getSession());
if (!credbasedAuthenticator.requiresUser()) {
userlessCredBasedAuthenticationSelectionList.add(new AuthenticationSelectionOption(processor.getSession(), value));
}
});
}
//add all other authenticators //add all other authenticators
for (AuthenticationExecutionModel exec : nonCredentialExecutions) { for (AuthenticationExecutionModel exec : nonCredentialExecutions) {
authenticationSelectionList.add(new AuthenticationSelectionOption(processor.getSession(), exec)); authenticationSelectionList.add(new AuthenticationSelectionOption(processor.getSession(), exec));
} }
// Add options for userless credential based authenticators AFTER regular authenticators options
authenticationSelectionList.addAll(userlessCredBasedAuthenticationSelectionList);
} }
logger.debugf("Selections when trying execution '%s' : %s", model.getAuthenticator(), authenticationSelectionList); logger.debugf("Selections when trying execution '%s' : %s", model.getAuthenticator(), authenticationSelectionList);

View file

@ -448,6 +448,14 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
} }
} }
} }
else {
if ((authUser != null) &&
!authenticator.configuredFor(processor.getSession(), processor.getRealm(), authUser) &&
!factory.isUserSetupAllowed() &&
(authenticator instanceof CredentialValidator)) {
throw new AuthenticationFlowException("authenticator: " + factory.getId(), AuthenticationFlowError.CREDENTIAL_SETUP_REQUIRED);
}
}
logger.debugv("invoke authenticator.authenticate: {0}", factory.getId()); logger.debugv("invoke authenticator.authenticate: {0}", factory.getId());
authenticator.authenticate(context); authenticator.authenticate(context);

View file

@ -159,8 +159,15 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
// existing User Handle means that the authenticator used Resident Key supported public key credential // existing User Handle means that the authenticator used Resident Key supported public key credential
if (userHandle == null || userHandle.isEmpty()) { if (userHandle == null || userHandle.isEmpty()) {
// Resident Key not supported public key credential was used // Resident Key not supported public key credential was used
// so rely on the user that has already been authenticated // so rely on the user set in a previous step (if available)
userId = context.getUser().getId(); if (context.getUser() != null) {
userId = context.getUser().getId();
}
else {
setErrorResponse(context, WEBAUTHN_ERROR_USER_NOT_FOUND,
"Webauthn credential provided doesn't include user id and user id wasn't provided in a previous step");
return;
}
} else { } else {
// decode using the same charset as it has been encoded (see: WebAuthnRegister.java) // decode using the same charset as it has been encoded (see: WebAuthnRegister.java)
userId = new String(Base64Url.decode(userHandle), StandardCharsets.UTF_8); userId = new String(Base64Url.decode(userHandle), StandardCharsets.UTF_8);

View file

@ -80,4 +80,9 @@ public class WebAuthnPasswordlessAuthenticator extends WebAuthnAuthenticator {
return (WebAuthnPasswordlessCredentialProvider)session.getProvider(CredentialProvider.class, WebAuthnPasswordlessCredentialProviderFactory.PROVIDER_ID); return (WebAuthnPasswordlessCredentialProvider)session.getProvider(CredentialProvider.class, WebAuthnPasswordlessCredentialProviderFactory.PROVIDER_ID);
} }
@Override
public boolean requiresUser() {
return false;
}
} }

View file

@ -19,6 +19,12 @@ public class SelectAuthenticatorPage extends LanguageComboboxAwarePage {
// Corresponds to the PasswordForm // Corresponds to the PasswordForm
public static final String PASSWORD = "Password"; public static final String PASSWORD = "Password";
// Corresponds to the UsernameForm
public static final String USERNAME = "Username";
// Corresponds to the UsernamePasswordForm
public static final String USERNAMEPASSWORD = "Username and password";
// Corresponds to the OTPFormAuthenticator // Corresponds to the OTPFormAuthenticator
public static final String AUTHENTICATOR_APPLICATION = "Authenticator Application"; public static final String AUTHENTICATOR_APPLICATION = "Authenticator Application";

View file

@ -0,0 +1,529 @@
/*
* Copyright 2022 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.webauthn;
import org.jboss.arquillian.graphene.page.Page;
import org.jboss.logging.Logger;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.WebAuthnConstants;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.authentication.authenticators.browser.*;
import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory;
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
import org.keycloak.common.util.SecretGenerator;
import org.keycloak.events.Details;
import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.credential.WebAuthnCredentialModel;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.AbstractAdminTest;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.pages.*;
import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.util.FlowUtil;
import org.keycloak.testsuite.webauthn.pages.WebAuthnLoginPage;
import org.keycloak.testsuite.webauthn.pages.WebAuthnRegisterPage;
import org.openqa.selenium.virtualauthenticator.Credential;
import java.io.IOException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.keycloak.WebAuthnConstants.OPTION_DISCOURAGED;
import static org.keycloak.WebAuthnConstants.OPTION_REQUIRED;
import static org.keycloak.models.AuthenticationExecutionModel.Requirement.ALTERNATIVE;
import static org.keycloak.models.AuthenticationExecutionModel.Requirement.REQUIRED;
import static org.keycloak.testsuite.webauthn.utils.PropertyRequirement.NO;
import static org.keycloak.testsuite.webauthn.utils.PropertyRequirement.YES;
import static org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions.Protocol;
import static org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions.Transport;
public class WebAuthnIdlessTest extends AbstractWebAuthnVirtualTest {
@Rule
public AssertEvents events = new AssertEvents(this);
@Page
protected AppPage appPage;
@Page
protected LoginPage loginPage;
@Page
protected ErrorPage errorPage;
@Page
protected WebAuthnLoginPage webAuthnLoginPage;
@Page
protected WebAuthnRegisterPage webAuthnRegisterPage;
@Page
protected LoginUsernameOnlyPage loginUsernamePage;
@Page
protected SelectAuthenticatorPage selectAuthenticatorPage;
private static final Logger logger = Logger.getLogger(WebAuthnIdlessTest.class);
protected final static String username = "test-user@localhost";
protected final static String password = "password";
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
}
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation realmRepresentation = AbstractAdminTest.loadJson(getClass().getResourceAsStream("/webauthn/testrealm-webauthn.json"), RealmRepresentation.class);
testRealms.add(realmRepresentation);
}
// Register webauthn-passwordless credential (resident key)
// Authenticate IDLess (resident key)
@Test
public void testWebAuthnIDLessLogin() throws IOException {
configureUser(username, false, true, true);
initializeAuthenticator(true, true, true, true);
setWebAuthnRealmSettings(false, false, true, true);
// Trigger webauthn-passwordless setup (resident key)
setUpUsernamePasswordFlow("username-password-flow");
String credentialId = usernamePasswordAuthWithAuthSetup(username, true, true);
setUpIDLessOnlyFlow("idless-only-flow");
idlessAuthentication(username, credentialId, false, true);
}
// Register webauthn-passwordless credential (non-resident key)
// Authenticate IDLess (non-resident key): should fail
@Test
public void testWebAuthnIDLessWithNonResidentCredentialLogin() throws IOException {
configureUser(username, false, true, true);
initializeAuthenticator(false, true, true, true);
setWebAuthnRealmSettings(false, false, false, true);
// Trigger webauthn-passwordless (non resident key setup)
setUpUsernamePasswordFlow("username-password-flow");
String credentialId = usernamePasswordAuthWithAuthSetup(username, true, false);
setUpIDLessOnlyFlow("idless-only-flow");
idlessAuthentication(username, credentialId, false, false);
}
// Authenticate IDLess with no webauthn-passwordless credential registered: should fail
@Test
public void testWebAuthnIDLessWithNoWebAuthnPasswordlessCredentialLogin() throws IOException {
configureUser(username, true, true, true);
initializeAuthenticator(false, true, true, true);
setWebAuthnRealmSettings(false, false, true, true);
setUpIDLessOnlyFlow("idless-only-flow");
idlessAuthentication(username, null, false, false);
}
// Register webauthn-passwordless credential (resident key)
// Register webauthn credential (non resident key)
// Assert 'Try another way' with security key on first step (before any form input)
// Authenticate UsernamePassword + WebAuthn (non resident key)
// Authenticate Username + WebAuthnPasswordless (resident key)
// Authenticate IDLess (resident key)
@Test
public void testWebAuthnIDLessAndWebAuthnAndWebAuthnPasswordlessLogin() throws IOException {
initializeAuthenticator(true, true, true, true);
setWebAuthnRealmSettings(false, false, true, true);
// Trigger webauthn-passwordless (resident key) setup
configureUser(username, false, true, true);
setUpUsernamePasswordFlow("username-password-flow");
String webAuthnPasswordlessCredId = usernamePasswordAuthWithAuthSetup(username, true, true);
// Trigger webauthn (non resident key) setup
configureUser(username, true, false, false);
setUpUsernamePasswordFlow("username-password-flow");
String webAuthnCredId = usernamePasswordAuthWithAuthSetup(username, false, false);
setUpIDLessAndWebAuthnAndPasswordlessFlow("webauthn-webauthnpasswordless-idless");
// Check tryAnotherWay link on first step page
checkTryAnotherWay();
// UsernamePasswordForm + WebAuthn
usernamePasswordAndWebAuthnAuthentication(username, webAuthnCredId);
// UsernameForm + WebAuthnPasswordless
usernameAndWebAuthnPasswordlessAuthentication(username, webAuthnPasswordlessCredId);
// WebAuthnIDLess
idlessAuthentication(username, webAuthnPasswordlessCredId, true, true);
}
protected String usernamePasswordAuthWithAuthSetup(String username, boolean isPasswordless, boolean withResidentKey) {
String raProviderID = isPasswordless ? WebAuthnPasswordlessRegisterFactory.PROVIDER_ID :
WebAuthnRegisterFactory.PROVIDER_ID;
String credType = isPasswordless ? WebAuthnCredentialModel.TYPE_PASSWORDLESS: WebAuthnCredentialModel.TYPE_TWOFACTOR;
String userId = getUserRepresentation(username).getId();
UserResource userRes = testRealm().users().get(userId);
assertThat(userRes.credentials().stream().filter(cred ->
cred.getType().equals(credType)).collect(Collectors.toList()).size(), is(0));
assertThat(getVirtualAuthManager().getCurrent().getAuthenticator().getCredentials().stream().filter(cred ->
cred.isResidentCredential() == isPasswordless).collect(Collectors.toList()).size(), is(0));
loginPage.open();
loginPage.assertCurrent();
loginPage.login(username, password);
webAuthnRegisterPage.assertCurrent();
webAuthnRegisterPage.clickRegister();
String labelPrefix = isPasswordless ? "wapl-" : "wa-";
String authenticatorLabel = labelPrefix + SecretGenerator.getInstance().randomString(24);
webAuthnRegisterPage.registerWebAuthnCredential(authenticatorLabel);
appPage.assertCurrent();
assertThat(appPage.getRequestType(), is(RequestType.AUTH_RESPONSE));
EventRepresentation eventRep = events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION)
.user(userId)
.detail(Details.CUSTOM_REQUIRED_ACTION, raProviderID)
.detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, authenticatorLabel)
.detail(WebAuthnConstants.PUBKEY_CRED_AAGUID_ATTR, ALL_ZERO_AAGUID)
.assertEvent();
String credentialId = eventRep.getDetails().get(WebAuthnConstants.PUBKEY_CRED_ID_ATTR);
assertThat(userRes.credentials().stream()
.filter(cred -> cred.getType().equals(credType))
.filter(cred -> cred.getUserLabel().equals(authenticatorLabel))
.collect(Collectors.toList()).size(), is(1));
assertThat(getVirtualAuthManager().getCurrent().getAuthenticator().getCredentials().stream()
.filter(cred -> cred.isResidentCredential() == withResidentKey)
.collect(Collectors.toList()).size(), is(1));
if (withResidentKey) {
assertThat(getVirtualAuthManager().getCurrent().getAuthenticator().getCredentials().stream()
.filter(cred -> cred.isResidentCredential())
.filter(cred -> (new String(cred.getUserHandle())).equals(userId))
.collect(Collectors.toList()).size(), is(1));
}
String sessionId = events.expectLogin()
.user(userId)
.assertEvent().getSessionId();
events.clear();
appPage.logout();
events.expectLogout(sessionId)
.user(userId)
.assertEvent();
return credentialId;
}
protected void checkTryAnotherWay() {
loginPage.open();
loginPage.assertCurrent();
loginPage.assertTryAnotherWayLinkAvailability(true);
loginPage.clickTryAnotherWayLink();
selectAuthenticatorPage.assertCurrent();
assertThat(selectAuthenticatorPage.getLoginMethodHelpText(SelectAuthenticatorPage.USERNAMEPASSWORD),
is("Sign in by entering your username and password."));
assertThat(selectAuthenticatorPage.getLoginMethodHelpText(SelectAuthenticatorPage.USERNAME),
is("Start sign in by entering your username"));
assertThat(selectAuthenticatorPage.getLoginMethodHelpText(SelectAuthenticatorPage.SECURITY_KEY),
is("Use your security key for passwordless sign in."));
selectAuthenticatorPage.selectLoginMethod(SelectAuthenticatorPage.USERNAMEPASSWORD);
loginPage.assertCurrent();
loginPage.clickTryAnotherWayLink();
selectAuthenticatorPage.assertCurrent();
selectAuthenticatorPage.selectLoginMethod(SelectAuthenticatorPage.USERNAME);
loginUsernamePage.assertCurrent();
loginUsernamePage.clickTryAnotherWayLink();
selectAuthenticatorPage.assertCurrent();
}
protected void usernamePasswordAndWebAuthnAuthentication(String username, String credentialId) {
String userId = getUserRepresentation(username).getId();
loginPage.open();
loginPage.assertCurrent();
loginPage.assertTryAnotherWayLinkAvailability(true);
loginPage.clickTryAnotherWayLink();
selectAuthenticatorPage.assertCurrent();
selectAuthenticatorPage.selectLoginMethod(SelectAuthenticatorPage.USERNAMEPASSWORD);
loginPage.assertCurrent();
loginPage.login(username, password);
webAuthnLoginPage.assertCurrent();
webAuthnLoginPage.clickAuthenticate();
appPage.assertCurrent();
String sessionId = events.expectLogin()
.user(userId)
.detail(WebAuthnConstants.PUBKEY_CRED_ID_ATTR, credentialId)
.detail("web_authn_authenticator_user_verification_checked", Boolean.FALSE.toString())
.assertEvent().getSessionId();
events.clear();
appPage.logout();
events.expectLogout(sessionId)
.user(userId)
.assertEvent();
}
protected void usernameAndWebAuthnPasswordlessAuthentication(String username, String credentialId) {
String userId = getUserRepresentation(username).getId();
loginPage.open();
loginPage.assertCurrent();
loginPage.assertTryAnotherWayLinkAvailability(true);
loginPage.clickTryAnotherWayLink();
selectAuthenticatorPage.assertCurrent();
selectAuthenticatorPage.selectLoginMethod(SelectAuthenticatorPage.USERNAME);
loginUsernamePage.assertCurrent();
loginUsernamePage.login(username);
webAuthnLoginPage.assertCurrent();
webAuthnLoginPage.clickAuthenticate();
appPage.assertCurrent();
String sessionId = events.expectLogin()
.user(userId)
.detail(WebAuthnConstants.PUBKEY_CRED_ID_ATTR, credentialId)
.detail("web_authn_authenticator_user_verification_checked", Boolean.TRUE.toString())
.assertEvent().getSessionId();
events.clear();
appPage.logout();
events.expectLogout(sessionId)
.user(userId)
.assertEvent();
}
protected void idlessAuthentication(String username, String credentialId, boolean tryAnotherMethod, boolean shouldSuccess) {
String userId = getUserRepresentation(username).getId();
loginPage.open();
loginPage.assertCurrent();
if (tryAnotherMethod) {
loginPage.assertTryAnotherWayLinkAvailability(true);
loginPage.clickTryAnotherWayLink();
selectAuthenticatorPage.assertCurrent();
selectAuthenticatorPage.selectLoginMethod(SelectAuthenticatorPage.SECURITY_KEY);
}
webAuthnLoginPage.assertCurrent();
webAuthnLoginPage.clickAuthenticate();
if (shouldSuccess) {
appPage.assertCurrent();
String sessionId = events.expectLogin()
.user(userId)
.detail(WebAuthnConstants.PUBKEY_CRED_ID_ATTR, credentialId)
.detail("web_authn_authenticator_user_verification_checked", Boolean.TRUE.toString())
.assertEvent().getSessionId();
events.clear();
appPage.logout();
events.expectLogout(sessionId)
.user(userId)
.assertEvent();
}
else {
loginPage.assertCurrent();
assertThat(loginPage.getError(), containsString("Failed to authenticate by the Security key."));
}
}
protected void setWebAuthnRealmSettings(boolean waRequireRK, boolean waRequireUV, boolean waplRequireRK, boolean waplRequireUV ) {
String waRequireRKString = waRequireRK ? YES.getValue() : NO.getValue();
String waRequireUVString = waRequireUV ? OPTION_REQUIRED : OPTION_DISCOURAGED;
String waplRequireRKString = waplRequireRK ? YES.getValue() : NO.getValue();
String waplRequireUVString = waplRequireUV ? OPTION_REQUIRED : OPTION_DISCOURAGED;
RealmRepresentation realmRep = testRealm().toRepresentation();
realmRep.setWebAuthnPolicyPasswordlessRequireResidentKey(waplRequireRKString);
realmRep.setWebAuthnPolicyPasswordlessUserVerificationRequirement(waplRequireUVString);
realmRep.setWebAuthnPolicyPasswordlessRpEntityName("localhost");
realmRep.setWebAuthnPolicyPasswordlessRpId("localhost");
realmRep.setWebAuthnPolicyRequireResidentKey(waRequireRKString);
realmRep.setWebAuthnPolicyUserVerificationRequirement(waRequireUVString);
realmRep.setWebAuthnPolicyRpEntityName("localhost");
realmRep.setWebAuthnPolicyRpId("localhost");
testRealm().update(realmRep);
realmRep = testRealm().toRepresentation();
assertThat(realmRep.getWebAuthnPolicyPasswordlessRequireResidentKey(), containsString(waplRequireRKString));
assertThat(realmRep.getWebAuthnPolicyPasswordlessUserVerificationRequirement(), containsString(waplRequireUVString));
assertThat(realmRep.getWebAuthnPolicyPasswordlessRpEntityName(), containsString("localhost"));
assertThat(realmRep.getWebAuthnPolicyPasswordlessRpId(), containsString("localhost"));
assertThat(realmRep.getWebAuthnPolicyRequireResidentKey(), containsString(waRequireRKString));
assertThat(realmRep.getWebAuthnPolicyUserVerificationRequirement(), containsString(waRequireUVString));
assertThat(realmRep.getWebAuthnPolicyRpEntityName(), containsString("localhost"));
assertThat(realmRep.getWebAuthnPolicyRpId(), containsString("localhost"));
}
protected void initializeAuthenticator(boolean hasRK, boolean hasUV, boolean isVerified, boolean isConsenting) {
getVirtualAuthManager().removeAuthenticator();
getVirtualAuthManager().useAuthenticator(getDefaultAuthenticatorOptions()
.setHasResidentKey(hasRK)
.setHasUserVerification(hasUV)
.setIsUserVerified(isVerified)
.setIsUserConsenting(isConsenting)
.setTransport(Transport.USB)
.setProtocol(Protocol.CTAP2));
getVirtualAuthManager().getCurrent().getAuthenticator().removeAllCredentials();
assertThat(getVirtualAuthManager().getCurrent().getOptions().hasResidentKey(), is(hasRK));
assertThat(getVirtualAuthManager().getCurrent().getOptions().isUserConsenting(), is(isConsenting));
assertThat(getVirtualAuthManager().getCurrent().getOptions().isUserVerified(), is(isVerified));
assertThat(getVirtualAuthManager().getCurrent().getOptions().hasUserVerification(), is(hasUV));
assertThat(getVirtualAuthManager().getCurrent().getOptions().getProtocol(), is(Protocol.CTAP2));
assertThat(getVirtualAuthManager().getCurrent().getOptions().getTransport(), is(Transport.USB));
assertThat(getVirtualAuthManager().getCurrent().getAuthenticator().getCredentials().size(), is(0));
}
protected UserRepresentation getUserRepresentation(String username)
{
if (username != null)
return ApiUtil.findUserByUsername(testRealm(), username);
else
return null;
}
protected void configureUser(String username, boolean registerWA, boolean registerWAPL, boolean resetCred) {
UserRepresentation user = getUserRepresentation(username);
assertThat(user, notNullValue());
// Clear existing required actions
user.getRequiredActions().clear();
if (registerWAPL) {
user.getRequiredActions().add(WebAuthnPasswordlessRegisterFactory.PROVIDER_ID);
}
if (registerWA) {
user.getRequiredActions().add(WebAuthnRegisterFactory.PROVIDER_ID);
}
UserResource userResource = testRealm().users().get(user.getId());
assertThat(userResource, notNullValue());
userResource.update(user);
if (resetCred) {
// Remove existing webauthn credentials
Predicate<CredentialRepresentation> isWebAuthnPasswordless = item -> item.getType().equals(WebAuthnCredentialModel.TYPE_PASSWORDLESS);
Predicate<CredentialRepresentation> isWebAuthn = item -> item.getType().equals(WebAuthnCredentialModel.TYPE_TWOFACTOR);
userResource.credentials().stream()
.filter(isWebAuthnPasswordless.or(isWebAuthn))
.forEach(item -> userResource.removeCredential(item.getId()));
// User should only have password credential set at this stage
assertThat(userResource.credentials().size(), is(1));
assertThat(userResource.credentials().get(0).getType(), is(CredentialRepresentation.PASSWORD));
}
user = userResource.toRepresentation();
assertThat(user, notNullValue());
if (registerWA)
assertThat(user.getRequiredActions(), hasItem(WebAuthnRegisterFactory.PROVIDER_ID));
if (registerWAPL)
assertThat(user.getRequiredActions(), hasItem(WebAuthnPasswordlessRegisterFactory.PROVIDER_ID));
}
/* Set auth flow to:
UsernamePasswordForm + WebAuthn (ALTERNATIVE)
UsernameForm + WebAuthnPasswordless (ALTERNATIVE)
IDLess (ALTERNATIVE)
*/
private void setUpIDLessAndWebAuthnAndPasswordlessFlow(String newFlowAlias) {
testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow(newFlowAlias));
testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session)
.selectFlow(newFlowAlias)
.inForms(forms -> forms
.clear()
.addSubFlowExecution(ALTERNATIVE, subFlow -> subFlow
.addAuthenticatorExecution(REQUIRED, UsernamePasswordFormFactory.PROVIDER_ID)
.addAuthenticatorExecution(REQUIRED, WebAuthnAuthenticatorFactory.PROVIDER_ID))
.addSubFlowExecution(ALTERNATIVE, subFlow -> subFlow
.addAuthenticatorExecution(REQUIRED, UsernameFormFactory.PROVIDER_ID)
.addAuthenticatorExecution(REQUIRED, WebAuthnPasswordlessAuthenticatorFactory.PROVIDER_ID))
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.ALTERNATIVE, WebAuthnPasswordlessAuthenticatorFactory.PROVIDER_ID)
)
.defineAsBrowserFlow() // Activate this new flow
);
}
private void setUpIDLessOnlyFlow(String newFlowAlias) {
testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow(newFlowAlias));
testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session)
.selectFlow(newFlowAlias)
.inForms(forms -> forms
.clear()
.addAuthenticatorExecution(REQUIRED, WebAuthnPasswordlessAuthenticatorFactory.PROVIDER_ID)
)
.defineAsBrowserFlow() // Activate this new flow
);
}
private void setUpUsernamePasswordFlow(String newFlowAlias) {
testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow(newFlowAlias));
testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session)
.selectFlow(newFlowAlias)
.inForms(forms -> forms
.clear()
.addAuthenticatorExecution(REQUIRED, UsernamePasswordFormFactory.PROVIDER_ID)
)
.defineAsBrowserFlow() // Activate this new flow
);
}
}

View file

@ -55,33 +55,33 @@
</div> </div>
</div> </div>
<#if realm.password && social.providers??>
<div id="kc-social-providers" class="${properties.kcFormSocialAccountSectionClass!}">
<hr/>
<h4>${msg("identity-provider-login-label")}</h4>
<ul class="${properties.kcFormSocialAccountListClass!} <#if social.providers?size gt 3>${properties.kcFormSocialAccountListGridClass!}</#if>">
<#list social.providers as p>
<a id="social-${p.alias}" class="${properties.kcFormSocialAccountListButtonClass!} <#if social.providers?size gt 3>${properties.kcFormSocialAccountGridItem!}</#if>"
type="button" href="${p.loginUrl}">
<#if p.iconClasses?has_content>
<i class="${properties.kcCommonLogoIdP!} ${p.iconClasses!}" aria-hidden="true"></i>
<span class="${properties.kcFormSocialAccountNameClass!} kc-social-icon-text">${p.displayName}</span>
<#else>
<span class="${properties.kcFormSocialAccountNameClass!}">${p.displayName}</span>
</#if>
</a>
</#list>
</ul>
</div>
</#if>
<#elseif section = "info" > <#elseif section = "info" >
<#if realm.password && realm.registrationAllowed && !registrationDisabled??> <#if realm.password && realm.registrationAllowed && !registrationDisabled??>
<div id="kc-registration"> <div id="kc-registration">
<span>${msg("noAccount")} <a tabindex="6" href="${url.registrationUrl}">${msg("doRegister")}</a></span> <span>${msg("noAccount")} <a tabindex="6" href="${url.registrationUrl}">${msg("doRegister")}</a></span>
</div> </div>
</#if> </#if>
<#elseif section = "socialProviders" >
<#if realm.password && social.providers??>
<div id="kc-social-providers" class="${properties.kcFormSocialAccountSectionClass!}">
<hr/>
<h4>${msg("identity-provider-login-label")}</h4>
<ul class="${properties.kcFormSocialAccountListClass!} <#if social.providers?size gt 3>${properties.kcFormSocialAccountListGridClass!}</#if>">
<#list social.providers as p>
<a id="social-${p.alias}" class="${properties.kcFormSocialAccountListButtonClass!} <#if social.providers?size gt 3>${properties.kcFormSocialAccountGridItem!}</#if>"
type="button" href="${p.loginUrl}">
<#if p.iconClasses?has_content>
<i class="${properties.kcCommonLogoIdP!} ${p.iconClasses!}" aria-hidden="true"></i>
<span class="${properties.kcFormSocialAccountNameClass!} kc-social-icon-text">${p.displayName!}</span>
<#else>
<span class="${properties.kcFormSocialAccountNameClass!}">${p.displayName!}</span>
</#if>
</a>
</#list>
</ul>
</div>
</#if>
</#if> </#if>
</@layout.registrationLayout> </@layout.registrationLayout>

View file

@ -69,6 +69,17 @@
</#if> </#if>
</div> </div>
</div>
<#elseif section = "info" >
<#if realm.password && realm.registrationAllowed && !registrationDisabled??>
<div id="kc-registration-container">
<div id="kc-registration">
<span>${msg("noAccount")} <a tabindex="6"
href="${url.registrationUrl}">${msg("doRegister")}</a></span>
</div>
</div>
</#if>
<#elseif section = "socialProviders" >
<#if realm.password && social.providers??> <#if realm.password && social.providers??>
<div id="kc-social-providers" class="${properties.kcFormSocialAccountSectionClass!}"> <div id="kc-social-providers" class="${properties.kcFormSocialAccountSectionClass!}">
<hr/> <hr/>
@ -89,17 +100,6 @@
</ul> </ul>
</div> </div>
</#if> </#if>
</div>
<#elseif section = "info" >
<#if realm.password && realm.registrationAllowed && !registrationDisabled??>
<div id="kc-registration-container">
<div id="kc-registration">
<span>${msg("noAccount")} <a tabindex="6"
href="${url.registrationUrl}">${msg("doRegister")}</a></span>
</div>
</div>
</#if>
</#if> </#if>
</@layout.registrationLayout> </@layout.registrationLayout>

View file

@ -1,4 +1,4 @@
<#macro registrationLayout bodyClass="" displayInfo=false displayMessage=true displayRequiredFields=false showAnotherWayIfPresent=true> <#macro registrationLayout bodyClass="" displayInfo=false displayMessage=true displayRequiredFields=false>
<!DOCTYPE html> <!DOCTYPE html>
<html class="${properties.kcHtmlClass!}"> <html class="${properties.kcHtmlClass!}">
@ -125,15 +125,17 @@
<#nested "form"> <#nested "form">
<#if auth?has_content && auth.showTryAnotherWayLink() && showAnotherWayIfPresent> <#if auth?has_content && auth.showTryAnotherWayLink()>
<form id="kc-select-try-another-way-form" action="${url.loginAction}" method="post"> <form id="kc-select-try-another-way-form" action="${url.loginAction}" method="post">
<div class="${properties.kcFormGroupClass!}"> <div class="${properties.kcFormGroupClass!}">
<input type="hidden" name="tryAnotherWay" value="on"/> <input type="hidden" name="tryAnotherWay" value="on"/>
<a href="#" id="try-another-way" <a href="#" id="try-another-way"
onclick="document.forms['kc-select-try-another-way-form'].submit();return false;">${msg("doTryAnotherWay")}</a> onclick="document.forms['kc-select-try-another-way-form'].submit();return false;">${msg("doTryAnotherWay")}</a>
</div> </div>
</form> </form>
</#if> </#if>
<#nested "socialProviders">
<#if displayInfo> <#if displayInfo>
<div id="kc-info" class="${properties.kcSignUpClass!}"> <div id="kc-info" class="${properties.kcSignUpClass!}">