parent
183ad30755
commit
92c4e6d585
9 changed files with 615 additions and 44 deletions
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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!}">
|
||||||
|
|
Loading…
Reference in a new issue