KEYCLOAK-19490 Add more details about 2FA to authenticate page (#9252)

Closes #9494
This commit is contained in:
Martin Bartoš 2022-01-11 09:16:22 +01:00 committed by GitHub
parent ab9413b48c
commit d75d28468e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 594 additions and 149 deletions

View file

@ -19,51 +19,51 @@ package org.keycloak;
public interface WebAuthnConstants { public interface WebAuthnConstants {
// Interface binded by FreeMarker template between UA and RP // Interface binded by FreeMarker template between UA and RP
final String USER_ID = "userid"; String USER_ID = "userid";
final String USER_NAME = "username"; String USER_NAME = "username";
final String CHALLENGE = "challenge"; String CHALLENGE = "challenge";
final String ORIGIN = "origin"; String ORIGIN = "origin";
final String ERROR = "error"; String ERROR = "error";
final String PUBLIC_KEY_CREDENTIAL_ID= "publicKeyCredentialId"; String PUBLIC_KEY_CREDENTIAL_ID = "publicKeyCredentialId";
final String CREDENTIAL_ID = "credentialId"; String CREDENTIAL_ID = "credentialId";
final String CLIENT_DATA_JSON = "clientDataJSON"; String CLIENT_DATA_JSON = "clientDataJSON";
final String AUTHENTICATOR_DATA = "authenticatorData"; String AUTHENTICATOR_DATA = "authenticatorData";
final String SIGNATURE = "signature"; String SIGNATURE = "signature";
final String USER_HANDLE = "userHandle"; String USER_HANDLE = "userHandle";
final String ATTESTATION_OBJECT= "attestationObject"; String ATTESTATION_OBJECT = "attestationObject";
final String AUTHENTICATOR_LABEL = "authenticatorLabel"; String AUTHENTICATOR_LABEL = "authenticatorLabel";
final String RP_ENTITY_NAME = "rpEntityName"; String RP_ENTITY_NAME = "rpEntityName";
final String SIGNATURE_ALGORITHMS = "signatureAlgorithms"; String SIGNATURE_ALGORITHMS = "signatureAlgorithms";
final String RP_ID = "rpId"; String RP_ID = "rpId";
final String ATTESTATION_CONVEYANCE_PREFERENCE = "attestationConveyancePreference"; String ATTESTATION_CONVEYANCE_PREFERENCE = "attestationConveyancePreference";
final String AUTHENTICATOR_ATTACHMENT = "authenticatorAttachment"; String AUTHENTICATOR_ATTACHMENT = "authenticatorAttachment";
final String REQUIRE_RESIDENT_KEY = "requireResidentKey"; String REQUIRE_RESIDENT_KEY = "requireResidentKey";
final String USER_VERIFICATION_REQUIREMENT = "userVerificationRequirement"; String USER_VERIFICATION_REQUIREMENT = "userVerificationRequirement";
final String CREATE_TIMEOUT = "createTimeout"; String CREATE_TIMEOUT = "createTimeout";
final String EXCLUDE_CREDENTIAL_IDS = "excludeCredentialIds"; String EXCLUDE_CREDENTIAL_IDS = "excludeCredentialIds";
final String ALLOWED_AUTHENTICATORS = "authenticators"; String ALLOWED_AUTHENTICATORS = "authenticators";
final String IS_USER_IDENTIFIED = "isUserIdentified"; String IS_USER_IDENTIFIED = "isUserIdentified";
final String USER_VERIFICATION = "userVerification"; String USER_VERIFICATION = "userVerification";
final String IS_SET_RETRY = "isSetRetry"; String IS_SET_RETRY = "isSetRetry";
String SHOULD_DISPLAY_AUTHENTICATORS = "shouldDisplayAuthenticators";
// Event key for credential id generated by navigator.credentials.create() // Event key for credential id generated by navigator.credentials.create()
final String PUBKEY_CRED_ID_ATTR = "public_key_credential_id"; String PUBKEY_CRED_ID_ATTR = "public_key_credential_id";
// Event key for Public Key Credential's user-editable metadata // Event key for Public Key Credential's user-editable metadata
final String PUBKEY_CRED_LABEL_ATTR = "public_key_credential_label"; String PUBKEY_CRED_LABEL_ATTR = "public_key_credential_label";
// Event key for Public Key Credential's AAGUID // Event key for Public Key Credential's AAGUID
final String PUBKEY_CRED_AAGUID_ATTR = "public_key_credential_aaguid"; String PUBKEY_CRED_AAGUID_ATTR = "public_key_credential_aaguid";
// key for storing onto AuthenticationSessionModel's Attribute challenge generated by RP(keycloak) // key for storing onto AuthenticationSessionModel's Attribute challenge generated by RP(keycloak)
final String AUTH_CHALLENGE_NOTE = "WEBAUTH_CHALLENGE"; String AUTH_CHALLENGE_NOTE = "WEBAUTH_CHALLENGE";
// option values on WebAuth API // option values on WebAuth API
final String OPTION_REQUIRED = "required"; String OPTION_REQUIRED = "required";
final String OPTION_PREFERED = "preferred"; String OPTION_PREFERED = "preferred";
final String OPTION_DISCOURAGED = "discouraged"; String OPTION_DISCOURAGED = "discouraged";
final String OPTION_NOT_SPECIFIED = ""; String OPTION_NOT_SPECIFIED = "";
} }

View file

@ -104,6 +104,7 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
// read options from policy // read options from policy
String userVerificationRequirement = policy.getUserVerificationRequirement(); String userVerificationRequirement = policy.getUserVerificationRequirement();
form.setAttribute(WebAuthnConstants.USER_VERIFICATION, userVerificationRequirement); form.setAttribute(WebAuthnConstants.USER_VERIFICATION, userVerificationRequirement);
form.setAttribute(WebAuthnConstants.SHOULD_DISPLAY_AUTHENTICATORS, shouldDisplayAuthenticators(context));
context.challenge(form.createLoginWebAuthn()); context.challenge(form.createLoginWebAuthn());
} }
@ -123,6 +124,9 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
return WebAuthnCredentialModel.TYPE_TWOFACTOR; return WebAuthnCredentialModel.TYPE_TWOFACTOR;
} }
protected boolean shouldDisplayAuthenticators(AuthenticationFlowContext context) {
return context.getUser() != null;
}
public void action(AuthenticationFlowContext context) { public void action(AuthenticationFlowContext context) {
MultivaluedMap<String, String> params = context.getHttpRequest().getDecodedFormParameters(); MultivaluedMap<String, String> params = context.getHttpRequest().getDecodedFormParameters();
@ -309,7 +313,7 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
} }
private Response createErrorResponse(AuthenticationFlowContext context, final String errorCase) { private Response createErrorResponse(AuthenticationFlowContext context, final String errorCase) {
LoginFormsProvider provider = context.form().setError(errorCase); LoginFormsProvider provider = context.form().setError(errorCase, "");
UserModel user = context.getUser(); UserModel user = context.getUser();
if (user != null) { if (user != null) {
WebAuthnAuthenticatorsBean authenticators = new WebAuthnAuthenticatorsBean(context.getSession(), context.getRealm(), user, getCredentialType()); WebAuthnAuthenticatorsBean authenticators = new WebAuthnAuthenticatorsBean(context.getSession(), context.getRealm(), user, getCredentialType());

View file

@ -55,6 +55,11 @@ public class WebAuthnPasswordlessAuthenticator extends WebAuthnAuthenticator {
return WebAuthnCredentialModel.TYPE_PASSWORDLESS; return WebAuthnCredentialModel.TYPE_PASSWORDLESS;
} }
@Override
protected boolean shouldDisplayAuthenticators(AuthenticationFlowContext context){
return false;
}
@Override @Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
// ask the user to do required action to register webauthn authenticator // ask the user to do required action to register webauthn authenticator

View file

@ -24,8 +24,10 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.credential.WebAuthnCredentialModel; import org.keycloak.models.credential.WebAuthnCredentialModel;
import org.keycloak.theme.DateTimeFormatterUtil;
public class WebAuthnAuthenticatorsBean { public class WebAuthnAuthenticatorsBean {
private List<WebAuthnAuthenticatorBean> authenticators = new LinkedList<WebAuthnAuthenticatorBean>(); private List<WebAuthnAuthenticatorBean> authenticators = new LinkedList<WebAuthnAuthenticatorBean>();
public WebAuthnAuthenticatorsBean(KeycloakSession session, RealmModel realm, UserModel user, String credentialType) { public WebAuthnAuthenticatorsBean(KeycloakSession session, RealmModel realm, UserModel user, String credentialType) {
@ -34,8 +36,9 @@ public class WebAuthnAuthenticatorsBean {
.map(WebAuthnCredentialModel::createFromCredentialModel) .map(WebAuthnCredentialModel::createFromCredentialModel)
.map(webAuthnCredential -> { .map(webAuthnCredential -> {
String credentialId = Base64Url.encodeBase64ToBase64Url(webAuthnCredential.getWebAuthnCredentialData().getCredentialId()); String credentialId = Base64Url.encodeBase64ToBase64Url(webAuthnCredential.getWebAuthnCredentialData().getCredentialId());
String label = (webAuthnCredential.getUserLabel()==null || webAuthnCredential.getUserLabel().isEmpty()) ? "label missing" : webAuthnCredential.getUserLabel(); String label = (webAuthnCredential.getUserLabel() == null || webAuthnCredential.getUserLabel().isEmpty()) ? "label missing" : webAuthnCredential.getUserLabel();
return new WebAuthnAuthenticatorBean(credentialId, label); String createdAt = DateTimeFormatterUtil.getDateTimeFromMillis(webAuthnCredential.getCreatedDate(), session.getContext().resolveLocale(user));
return new WebAuthnAuthenticatorBean(credentialId, label, createdAt);
}).collect(Collectors.toList()); }).collect(Collectors.toList());
} }
@ -46,10 +49,12 @@ public class WebAuthnAuthenticatorsBean {
public static class WebAuthnAuthenticatorBean { public static class WebAuthnAuthenticatorBean {
private final String credentialId; private final String credentialId;
private final String label; private final String label;
private final String createdAt;
public WebAuthnAuthenticatorBean(String credentialId, String label) { public WebAuthnAuthenticatorBean(String credentialId, String label, String createdAt) {
this.credentialId = credentialId; this.credentialId = credentialId;
this.label = label; this.label = label;
this.createdAt = createdAt;
} }
public String getCredentialId() { public String getCredentialId() {
@ -59,5 +64,9 @@ public class WebAuthnAuthenticatorsBean {
public String getLabel() { public String getLabel() {
return this.label; return this.label;
} }
public String getCreatedAt() {
return this.createdAt;
}
} }
} }

View file

@ -0,0 +1,88 @@
/*
* Copyright 2021 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.theme;
import org.jboss.logging.Logger;
import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Optional;
/**
* Util class for localized date and time representation
*
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
*/
public class DateTimeFormatterUtil {
private static final Logger log = Logger.getLogger(DateTimeFormatterUtil.class);
public static String getDateTimeFromMillis(long millis) {
return getDateTimeFromMillis(millis, Locale.ENGLISH);
}
public static String getDateTimeFromMillis(long millis, String locale) {
return getDateTimeFromMillis(millis, getLocaleFromString(locale));
}
public static String getDateTimeFromMillis(long millis, Locale locale) {
return getDateTimeFromMillis(millis, getDefaultDateFormat(locale));
}
/**
* Get string representation of localized date and time
*
* @param millis number of milliseconds passed since January 1, 1970, 00:00:00 GMT
* @param dateFormat format of date and time. See {@link DateFormat}
* @return string representation
*/
public static String getDateTimeFromMillis(long millis, DateFormat dateFormat) {
if (dateFormat == null) return null;
return dateFormat.format(new Date(millis));
}
public static Locale getLocaleFromString(String locale) {
return getLocaleFromString(locale, Locale.ENGLISH);
}
/**
* Parse {@link Locale} from string
*
* @param locale required locale
* @param defaultValue default value if the locale parameter is invalid
* @return Locale
*/
public static Locale getLocaleFromString(String locale, Locale defaultValue) {
try {
return Optional.ofNullable(locale)
.map(Locale::new)
.orElse(defaultValue);
} catch (Exception e) {
log.debugf("Invalid locale '%s'", locale);
}
return defaultValue;
}
public static DateFormat getDefaultDateFormat() {
return getDefaultDateFormat(Locale.ENGLISH);
}
public static DateFormat getDefaultDateFormat(Locale locale) {
return DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, locale);
}
}

View file

@ -97,4 +97,19 @@ public class RealmAttributeUpdater extends ServerResourceUpdater<RealmAttributeU
rep.setBrowserFlow(browserFlow); rep.setBrowserFlow(browserFlow);
return this; return this;
} }
public RealmAttributeUpdater setDefaultLocale(String defaultLocale) {
rep.setDefaultLocale(defaultLocale);
return this;
}
public RealmAttributeUpdater addSupportedLocale(String locale) {
rep.addSupportedLocales(locale);
return this;
}
public RealmAttributeUpdater setInternationalizationEnabled(Boolean internationalizationEnabled) {
rep.setInternationalizationEnabled(internationalizationEnabled);
return this;
}
} }

View file

@ -0,0 +1,100 @@
/*
* Copyright 2021 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.pages;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.stream.Collectors;
import static org.keycloak.testsuite.util.UIUtils.getTextFromElement;
/**
* Helper class for getting available authenticators on WebAuthnLogin page
*
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
*/
public class WebAuthnAuthenticatorsList {
@FindBy(id = "kc-webauthn-authenticator")
private List<WebElement> authenticators;
public List<WebAuthnAuthenticatorItem> getItems() {
try {
List<WebAuthnAuthenticatorItem> items = new ArrayList<>();
for (WebElement auth : authenticators) {
String name = getTextFromElement(auth.findElement(By.id("kc-webauthn-authenticator-label")));
String createdAt = getTextFromElement(auth.findElement(By.id("kc-webauthn-authenticator-created")));
String createdAtLabel = getTextFromElement(auth.findElement(By.id("kc-webauthn-authenticator-created-label")));
items.add(new WebAuthnAuthenticatorItem(name, createdAt, createdAtLabel));
}
return items;
} catch (NoSuchElementException e) {
return Collections.emptyList();
}
}
public int getCount() {
try {
return authenticators.size();
} catch (NoSuchElementException e) {
return 0;
}
}
public List<String> getLabels() {
try {
return getItems().stream()
.filter(Objects::nonNull)
.map(WebAuthnAuthenticatorItem::getName)
.collect(Collectors.toList());
} catch (NoSuchElementException e) {
return Collections.emptyList();
}
}
public static class WebAuthnAuthenticatorItem {
private final String name;
private final String createdAt;
private final String createdAtLabel;
public WebAuthnAuthenticatorItem(String name, String createdAt, String createdAtLabel) {
this.name = name;
this.createdAt = createdAt;
this.createdAtLabel = createdAtLabel;
}
public String getName() {
return name;
}
public String getCreatedDate() {
return createdAt;
}
public String getCreatedLabel() {
return createdAtLabel;
}
}
}

View file

@ -29,9 +29,6 @@ public class WebAuthnErrorPage extends LanguageComboboxAwarePage {
@FindBy(className = "alert-error") @FindBy(className = "alert-error")
private WebElement errorMessage; private WebElement errorMessage;
@FindBy(id = "kc-webauthn-authenticator")
private List<WebElement> authenticators;
public void clickTryAgain() { public void clickTryAgain() {
WaitUtils.waitUntilElement(tryAgainButton).is().clickable(); WaitUtils.waitUntilElement(tryAgainButton).is().clickable();
tryAgainButton.click(); tryAgainButton.click();
@ -54,25 +51,6 @@ public class WebAuthnErrorPage extends LanguageComboboxAwarePage {
} }
} }
public int getAuthenticatorsCount() {
try {
return authenticators.size();
} catch (NoSuchElementException e) {
return 0;
}
}
public List<String> getAuthenticators() {
try {
return authenticators.stream()
.filter(Objects::nonNull)
.map(UIUtils::getTextFromElement)
.collect(Collectors.toList());
} catch (NoSuchElementException e) {
return Collections.emptyList();
}
}
@Override @Override
public boolean isCurrent() { public boolean isCurrent() {
try { try {

View file

@ -17,11 +17,13 @@
package org.keycloak.testsuite.webauthn.pages; package org.keycloak.testsuite.webauthn.pages;
import org.jboss.arquillian.graphene.page.Page;
import org.keycloak.testsuite.pages.LanguageComboboxAwarePage; import org.keycloak.testsuite.pages.LanguageComboboxAwarePage;
import org.keycloak.testsuite.util.WaitUtils; import org.keycloak.testsuite.util.WaitUtils;
import org.openqa.selenium.WebElement; import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy; import org.openqa.selenium.support.FindBy;
import java.util.List;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
/** /**
@ -32,6 +34,12 @@ public class WebAuthnLoginPage extends LanguageComboboxAwarePage {
@FindBy(id = "authenticateWebAuthnButton") @FindBy(id = "authenticateWebAuthnButton")
private WebElement authenticateButton; private WebElement authenticateButton;
@FindBy(id = "kc-webauthn-authenticator-label")
private List<WebElement> authenticatorsLabels;
@Page
private WebAuthnAuthenticatorsList authenticators;
public void clickAuthenticate() { public void clickAuthenticate() {
WaitUtils.waitUntilElement(authenticateButton).is().clickable(); WaitUtils.waitUntilElement(authenticateButton).is().clickable();
authenticateButton.click(); authenticateButton.click();
@ -46,6 +54,10 @@ public class WebAuthnLoginPage extends LanguageComboboxAwarePage {
} }
} }
public WebAuthnAuthenticatorsList getAuthenticators() {
return authenticators;
}
@Override @Override
public void open() { public void open() {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();

View file

@ -16,6 +16,7 @@
*/ */
package org.keycloak.testsuite.webauthn; package org.keycloak.testsuite.webauthn;
import org.hamcrest.Matchers;
import org.jboss.arquillian.graphene.page.Page; import org.jboss.arquillian.graphene.page.Page;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Rule; import org.junit.Rule;
@ -51,6 +52,7 @@ import org.keycloak.testsuite.pages.RegisterPage;
import org.keycloak.testsuite.pages.SelectAuthenticatorPage; import org.keycloak.testsuite.pages.SelectAuthenticatorPage;
import org.keycloak.testsuite.updaters.RealmAttributeUpdater; import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
import org.keycloak.testsuite.util.FlowUtil; import org.keycloak.testsuite.util.FlowUtil;
import org.keycloak.testsuite.webauthn.pages.WebAuthnAuthenticatorsList;
import org.keycloak.testsuite.webauthn.pages.WebAuthnLoginPage; import org.keycloak.testsuite.webauthn.pages.WebAuthnLoginPage;
import org.keycloak.testsuite.webauthn.pages.WebAuthnRegisterPage; import org.keycloak.testsuite.webauthn.pages.WebAuthnRegisterPage;
import org.keycloak.testsuite.webauthn.updaters.WebAuthnRealmAttributeUpdater; import org.keycloak.testsuite.webauthn.updaters.WebAuthnRealmAttributeUpdater;
@ -178,6 +180,11 @@ public class WebAuthnRegisterAndLoginTest extends AbstractWebAuthnVirtualTest {
loginPage.login(username, password); loginPage.login(username, password);
webAuthnLoginPage.assertCurrent(); webAuthnLoginPage.assertCurrent();
final WebAuthnAuthenticatorsList authenticators = webAuthnLoginPage.getAuthenticators();
assertThat(authenticators.getCount(), is(1));
assertThat(authenticators.getLabels(), Matchers.contains(authenticatorLabel));
webAuthnLoginPage.clickAuthenticate(); webAuthnLoginPage.clickAuthenticate();
appPage.assertCurrent(); appPage.assertCurrent();
@ -204,7 +211,7 @@ public class WebAuthnRegisterAndLoginTest extends AbstractWebAuthnVirtualTest {
} }
@Test @Test
public void testWebAuthnPasswordlessAlternativeWithWebAuthnAndPassword() throws IOException { public void webAuthnPasswordlessAlternativeWithWebAuthnAndPassword() throws IOException {
String userId = null; String userId = null;
final String WEBAUTHN_LABEL = "webauthn"; final String WEBAUTHN_LABEL = "webauthn";
@ -280,6 +287,11 @@ public class WebAuthnRegisterAndLoginTest extends AbstractWebAuthnVirtualTest {
passwordPage.login("password"); passwordPage.login("password");
webAuthnLoginPage.assertCurrent(); webAuthnLoginPage.assertCurrent();
final WebAuthnAuthenticatorsList authenticators = webAuthnLoginPage.getAuthenticators();
assertThat(authenticators.getCount(), is(1));
assertThat(authenticators.getLabels(), Matchers.contains(WEBAUTHN_LABEL));
webAuthnLoginPage.clickAuthenticate(); webAuthnLoginPage.clickAuthenticate();
appPage.assertCurrent(); appPage.assertCurrent();
@ -299,6 +311,8 @@ public class WebAuthnRegisterAndLoginTest extends AbstractWebAuthnVirtualTest {
selectAuthenticatorPage.selectLoginMethod(SelectAuthenticatorPage.SECURITY_KEY); selectAuthenticatorPage.selectLoginMethod(SelectAuthenticatorPage.SECURITY_KEY);
webAuthnLoginPage.assertCurrent(); webAuthnLoginPage.assertCurrent();
assertThat(webAuthnLoginPage.getAuthenticators().getCount(), is(0));
webAuthnLoginPage.clickAuthenticate(); webAuthnLoginPage.clickAuthenticate();
appPage.assertCurrent(); appPage.assertCurrent();
@ -310,7 +324,7 @@ public class WebAuthnRegisterAndLoginTest extends AbstractWebAuthnVirtualTest {
} }
@Test @Test
public void testWebAuthnTwoFactorAndWebAuthnPasswordlessTogether() throws IOException { public void webAuthnTwoFactorAndWebAuthnPasswordlessTogether() throws IOException {
// Change binding to browser-webauthn-passwordless. This is flow, which contains both "webauthn" and "webauthn-passwordless" authenticator // Change binding to browser-webauthn-passwordless. This is flow, which contains both "webauthn" and "webauthn-passwordless" authenticator
try (RealmAttributeUpdater rau = new RealmAttributeUpdater(testRealm()).setBrowserFlow("browser-webauthn-passwordless").update()) { try (RealmAttributeUpdater rau = new RealmAttributeUpdater(testRealm()).setBrowserFlow("browser-webauthn-passwordless").update()) {
// Login as test-user@localhost with password // Login as test-user@localhost with password

View file

@ -20,11 +20,13 @@ package org.keycloak.testsuite.webauthn.account;
import org.jboss.arquillian.graphene.page.Page; import org.jboss.arquillian.graphene.page.Page;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory;
import org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorFactory; import org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorFactory;
import org.keycloak.authentication.authenticators.browser.WebAuthnPasswordlessAuthenticatorFactory; import org.keycloak.authentication.authenticators.browser.WebAuthnPasswordlessAuthenticatorFactory;
import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory; import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory;
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory; import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
import org.keycloak.common.Profile; import org.keycloak.common.Profile;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.credential.WebAuthnCredentialModel; import org.keycloak.models.credential.WebAuthnCredentialModel;
import org.keycloak.representations.idm.AuthenticationExecutionRepresentation; import org.keycloak.representations.idm.AuthenticationExecutionRepresentation;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation; import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
@ -34,6 +36,7 @@ import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.page.AbstractPatternFlyAlert; import org.keycloak.testsuite.page.AbstractPatternFlyAlert;
import org.keycloak.testsuite.ui.account2.page.SigningInPage; import org.keycloak.testsuite.ui.account2.page.SigningInPage;
import org.keycloak.testsuite.ui.account2.page.utils.SigningInPageUtils; import org.keycloak.testsuite.ui.account2.page.utils.SigningInPageUtils;
import org.keycloak.testsuite.util.FlowUtil;
import org.keycloak.testsuite.webauthn.AbstractWebAuthnVirtualTest; import org.keycloak.testsuite.webauthn.AbstractWebAuthnVirtualTest;
import org.keycloak.testsuite.webauthn.authenticators.DefaultVirtualAuthOptions; import org.keycloak.testsuite.webauthn.authenticators.DefaultVirtualAuthOptions;
import org.keycloak.testsuite.webauthn.authenticators.UseVirtualAuthenticators; import org.keycloak.testsuite.webauthn.authenticators.UseVirtualAuthenticators;
@ -165,4 +168,23 @@ public abstract class AbstractWebAuthnAccountTest extends AbstractAuthTest imple
protected SigningInPage.UserCredential getNewestUserCredential(SigningInPage.CredentialType credentialType) { protected SigningInPage.UserCredential getNewestUserCredential(SigningInPage.CredentialType credentialType) {
return SigningInPageUtils.getNewestUserCredential(testUserResource(), credentialType); return SigningInPageUtils.getNewestUserCredential(testUserResource(), credentialType);
} }
protected void setUpWebAuthnFlow(String newFlowAlias) {
setUpWebAuthnFlow(newFlowAlias, false);
}
protected void setUpWebAuthnFlow(String newFlowAlias, boolean passwordless) {
final String providerID = passwordless ? WebAuthnPasswordlessAuthenticatorFactory.PROVIDER_ID : WebAuthnAuthenticatorFactory.PROVIDER_ID;
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(AuthenticationExecutionModel.Requirement.REQUIRED, UsernamePasswordFormFactory.PROVIDER_ID)
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, providerID)
)
.defineAsBrowserFlow() // Activate this new flow
);
}
} }

View file

@ -20,20 +20,16 @@ package org.keycloak.testsuite.webauthn.account;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
import org.jboss.arquillian.graphene.page.Page; import org.jboss.arquillian.graphene.page.Page;
import org.junit.Test; import org.junit.Test;
import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory;
import org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.updaters.RealmAttributeUpdater; import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
import org.keycloak.testsuite.util.FlowUtil;
import org.keycloak.testsuite.util.WaitUtils; import org.keycloak.testsuite.util.WaitUtils;
import org.keycloak.testsuite.webauthn.pages.WebAuthnAuthenticatorsList;
import org.keycloak.testsuite.webauthn.pages.WebAuthnErrorPage; import org.keycloak.testsuite.webauthn.pages.WebAuthnErrorPage;
import org.keycloak.testsuite.webauthn.pages.WebAuthnLoginPage; import org.keycloak.testsuite.webauthn.pages.WebAuthnLoginPage;
import org.keycloak.testsuite.webauthn.updaters.WebAuthnRealmAttributeUpdater; import org.keycloak.testsuite.webauthn.updaters.WebAuthnRealmAttributeUpdater;
import java.io.IOException; import java.io.IOException;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
@ -47,11 +43,10 @@ public class WebAuthnErrorTest extends AbstractWebAuthnAccountTest {
protected WebAuthnErrorPage webAuthnErrorPage; protected WebAuthnErrorPage webAuthnErrorPage;
@Test @Test
public void errorPageWithPossibleAuthenticators() throws IOException { public void errorPageWithTimeout() throws IOException {
final int timeoutSec = 3; final int timeoutSec = 3;
final String authenticatorLabel = "authenticator";
addWebAuthnCredential("authenticator#1"); addWebAuthnCredential(authenticatorLabel);
addWebAuthnCredential("authenticator#2");
try (RealmAttributeUpdater u = new WebAuthnRealmAttributeUpdater(testRealmResource()) try (RealmAttributeUpdater u = new WebAuthnRealmAttributeUpdater(testRealmResource())
.setWebAuthnPolicyCreateTimeout(timeoutSec) .setWebAuthnPolicyCreateTimeout(timeoutSec)
@ -62,7 +57,7 @@ public class WebAuthnErrorTest extends AbstractWebAuthnAccountTest {
assertThat(realm.getWebAuthnPolicyCreateTimeout(), is(timeoutSec)); assertThat(realm.getWebAuthnPolicyCreateTimeout(), is(timeoutSec));
final int webAuthnCount = webAuthnCredentialType.getUserCredentialsCount(); final int webAuthnCount = webAuthnCredentialType.getUserCredentialsCount();
assertThat(webAuthnCount, is(2)); assertThat(webAuthnCount, is(1));
getWebAuthnManager().getCurrent().getAuthenticator().removeAllCredentials(); getWebAuthnManager().getCurrent().getAuthenticator().removeAllCredentials();
@ -73,28 +68,18 @@ public class WebAuthnErrorTest extends AbstractWebAuthnAccountTest {
loginToAccount(); loginToAccount();
webAuthnLoginPage.assertCurrent(); webAuthnLoginPage.assertCurrent();
final WebAuthnAuthenticatorsList authenticators = webAuthnLoginPage.getAuthenticators();
assertThat(authenticators.getCount(), is(1));
assertThat(authenticators.getLabels(), Matchers.contains(authenticatorLabel));
webAuthnLoginPage.clickAuthenticate(); webAuthnLoginPage.clickAuthenticate();
//Should fail after this time //Should fail after this time
WaitUtils.pause((timeoutSec + 1) * 1000); WaitUtils.pause((timeoutSec + 1) * 1000);
webAuthnErrorPage.assertCurrent(); webAuthnErrorPage.assertCurrent();
assertThat(webAuthnErrorPage.getError(), containsString("Failed to authenticate by the Security key.")); assertThat(webAuthnErrorPage.getError(), is("Failed to authenticate by the Security key."));
assertThat(webAuthnErrorPage.getAuthenticatorsCount(), is(2));
assertThat(webAuthnErrorPage.getAuthenticators(), Matchers.contains("authenticator#1", "authenticator#2"));
} }
} }
private void setUpWebAuthnFlow(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(AuthenticationExecutionModel.Requirement.REQUIRED, UsernamePasswordFormFactory.PROVIDER_ID)
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, WebAuthnAuthenticatorFactory.PROVIDER_ID)
)
.defineAsBrowserFlow() // Activate this new flow
);
}
} }

View file

@ -17,23 +17,30 @@
package org.keycloak.testsuite.webauthn.account; package org.keycloak.testsuite.webauthn.account;
import org.hamcrest.Matchers;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Test; import org.junit.Test;
import org.keycloak.admin.client.resource.UserResource; import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorFactory;
import org.keycloak.authentication.authenticators.browser.WebAuthnPasswordlessAuthenticatorFactory;
import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory; import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory;
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory; import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
import org.keycloak.representations.idm.AuthenticationExecutionRepresentation;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.RequiredActionProviderRepresentation; import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
import org.keycloak.representations.idm.RequiredActionProviderSimpleRepresentation;
import org.keycloak.testsuite.ui.account2.page.SigningInPage; import org.keycloak.testsuite.ui.account2.page.SigningInPage;
import org.keycloak.testsuite.ui.account2.page.utils.SigningInPageUtils; import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
import org.keycloak.testsuite.webauthn.authenticators.UseVirtualAuthenticators; import org.keycloak.testsuite.webauthn.authenticators.UseVirtualAuthenticators;
import org.keycloak.testsuite.webauthn.pages.WebAuthnAuthenticatorsList;
import org.keycloak.testsuite.webauthn.pages.WebAuthnLoginPage;
import org.keycloak.theme.DateTimeFormatterUtil;
import java.io.Closeable;
import java.io.IOException;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
@ -43,7 +50,6 @@ import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.hasSize;
import static org.keycloak.models.AuthenticationExecutionModel.Requirement.REQUIRED;
import static org.keycloak.testsuite.ui.account2.page.utils.SigningInPageUtils.assertUserCredential; import static org.keycloak.testsuite.ui.account2.page.utils.SigningInPageUtils.assertUserCredential;
import static org.keycloak.testsuite.ui.account2.page.utils.SigningInPageUtils.testSetUpLink; import static org.keycloak.testsuite.ui.account2.page.utils.SigningInPageUtils.testSetUpLink;
import static org.keycloak.testsuite.util.UIUtils.refreshPageAndWaitForLoad; import static org.keycloak.testsuite.util.UIUtils.refreshPageAndWaitForLoad;
@ -51,6 +57,9 @@ import static org.keycloak.testsuite.util.WaitUtils.waitForPageToLoad;
public class WebAuthnSigningInTest extends AbstractWebAuthnAccountTest implements UseVirtualAuthenticators { public class WebAuthnSigningInTest extends AbstractWebAuthnAccountTest implements UseVirtualAuthenticators {
@Page
protected WebAuthnLoginPage webAuthnLoginPage;
@Test @Test
public void categoriesTest() { public void categoriesTest() {
testContext.setTestRealmReps(emptyList()); // reimport realm after this test testContext.setTestRealmReps(emptyList()); // reimport realm after this test
@ -78,7 +87,7 @@ public class WebAuthnSigningInTest extends AbstractWebAuthnAccountTest implement
} }
@Test @Test
public void testCreateWebAuthnSameUserLabel() { public void createWebAuthnSameUserLabel() {
final String SAME_LABEL = "key123"; final String SAME_LABEL = "key123";
// Do we really allow to have several authenticators with the same user label?? // Do we really allow to have several authenticators with the same user label??
@ -112,7 +121,7 @@ public class WebAuthnSigningInTest extends AbstractWebAuthnAccountTest implement
} }
@Test @Test
public void testMultipleSecurityKeys() { public void multipleSecurityKeys() {
final String LABEL = "SecurityKey#"; final String LABEL = "SecurityKey#";
List<SigningInPage.UserCredential> createdCredentials = new ArrayList<>(); List<SigningInPage.UserCredential> createdCredentials = new ArrayList<>();
@ -168,16 +177,177 @@ public class WebAuthnSigningInTest extends AbstractWebAuthnAccountTest implement
} }
@Test @Test
public void testCancelRegistration() { public void displayAvailableAuthenticators() {
cancelRegistration(false); addWebAuthnCredential("authenticator#1");
addWebAuthnCredential("authenticator#2");
final int webAuthnCount = webAuthnCredentialType.getUserCredentialsCount();
assertThat(webAuthnCount, is(2));
setUpWebAuthnFlow("webAuthnFlow");
logout();
signingInPage.navigateTo();
loginToAccount();
webAuthnLoginPage.assertCurrent();
WebAuthnAuthenticatorsList authenticators = webAuthnLoginPage.getAuthenticators();
assertThat(authenticators.getCount(), is(2));
assertThat(authenticators.getLabels(), Matchers.contains("authenticator#1", "authenticator#2"));
webAuthnLoginPage.clickAuthenticate();
signingInPage.assertCurrent();
} }
@Test @Test
public void testCancelPasswordlessRegistration() { public void notDisplayAvailableAuthenticatorsPasswordless() {
cancelRegistration(true); addWebAuthnCredential("authenticator#1", true);
addWebAuthnCredential("authenticator#2", true);
final int passwordlessCount = webAuthnPwdlessCredentialType.getUserCredentialsCount();
assertThat(passwordlessCount, is(2));
setUpWebAuthnFlow("passwordlessFlow", true);
logout();
signingInPage.navigateTo();
loginToAccount();
webAuthnLoginPage.assertCurrent();
assertThat(webAuthnLoginPage.getAuthenticators().getCount(), is(0));
webAuthnLoginPage.clickAuthenticate();
signingInPage.assertCurrent();
} }
private void cancelRegistration(boolean passwordless) { @Test
public void checkAuthenticatorTimeLocale() throws ParseException, IOException {
addWebAuthnCredential("authenticator#1");
final int webAuthnCount = webAuthnCredentialType.getUserCredentialsCount();
assertThat(webAuthnCount, is(1));
setUpWebAuthnFlow("webAuthnFlow");
logout();
signingInPage.navigateTo();
loginToAccount();
webAuthnLoginPage.assertCurrent();
WebAuthnAuthenticatorsList authenticators = webAuthnLoginPage.getAuthenticators();
assertThat(authenticators.getCount(), is(1));
assertThat(authenticators.getLabels(), Matchers.contains("authenticator#1"));
WebAuthnAuthenticatorsList.WebAuthnAuthenticatorItem item = authenticators.getItems().get(0);
assertThat(item, notNullValue());
assertThat(item.getName(), is("authenticator#1"));
final String dateEnglishString = item.getCreatedDate();
assertThat(dateEnglishString, notNullValue());
DateFormat format = DateTimeFormatterUtil.getDefaultDateFormat(Locale.ENGLISH);
final Date dateEnglish = format.parse(dateEnglishString);
assertThat(dateEnglish, notNullValue());
webAuthnLoginPage.clickAuthenticate();
signingInPage.assertCurrent();
logout();
try (Closeable c = setLocalesUpdater(Locale.CHINA.getLanguage()).update()) {
signingInPage.navigateTo();
loginToAccount();
webAuthnLoginPage.assertCurrent();
authenticators = webAuthnLoginPage.getAuthenticators();
assertThat(authenticators.getCount(), is(1));
item = webAuthnLoginPage.getAuthenticators().getItems().get(0);
final String dateChineseString = item.getCreatedDate();
assertThat(dateChineseString, notNullValue());
format = DateTimeFormatterUtil.getDefaultDateFormat(Locale.CHINA);
final Date dateChinese = format.parse(dateChineseString);
assertThat(dateChinese, notNullValue());
assertThat(dateEnglishString, is(not(dateChineseString)));
assertThat(dateEnglish, is(dateChinese));
webAuthnLoginPage.clickAuthenticate();
signingInPage.assertCurrent();
logout();
}
try (Closeable c = setLocalesUpdater("xx", Locale.ENGLISH.getLanguage()).update()) {
signingInPage.navigateTo();
loginToAccount();
webAuthnLoginPage.assertCurrent();
authenticators = webAuthnLoginPage.getAuthenticators();
assertThat(authenticators.getCount(), is(1));
item = webAuthnLoginPage.getAuthenticators().getItems().get(0);
final String dateInvalidString = item.getCreatedDate();
assertThat(dateInvalidString, notNullValue());
assertThat(dateInvalidString, is(dateEnglishString));
}
}
@Test
public void userAuthenticatorTimeLocale() throws IOException {
Consumer<String> checkCreatedAtLabels = (requiredLabel) ->
webAuthnLoginPage.getAuthenticators()
.getItems()
.stream()
.map(WebAuthnAuthenticatorsList.WebAuthnAuthenticatorItem::getCreatedLabel)
.forEach(f -> assertThat(f, is(requiredLabel)));
try (Closeable c = setLocalesUpdater(Locale.ENGLISH.getLanguage(), "cs").update()) {
addWebAuthnCredential("authenticator#1");
addWebAuthnCredential("authenticator#2");
final int webAuthnCount = webAuthnCredentialType.getUserCredentialsCount();
assertThat(webAuthnCount, is(2));
setUpWebAuthnFlow("webAuthnFlow");
logout();
signingInPage.navigateTo();
loginToAccount();
webAuthnLoginPage.assertCurrent();
WebAuthnAuthenticatorsList authenticators = webAuthnLoginPage.getAuthenticators();
assertThat(authenticators.getCount(), is(2));
assertThat(authenticators.getLabels(), Matchers.contains("authenticator#1", "authenticator#2"));
checkCreatedAtLabels.accept("Created");
webAuthnLoginPage.openLanguage("Čeština");
checkCreatedAtLabels.accept("Vytvořeno");
webAuthnLoginPage.clickAuthenticate();
signingInPage.assertCurrent();
}
}
@Test
public void cancelRegistration() {
checkCancelRegistration(false);
}
@Test
public void cancelPasswordlessRegistration() {
checkCancelRegistration(true);
}
private void checkCancelRegistration(boolean passwordless) {
SigningInPage.CredentialType credentialType = passwordless ? webAuthnPwdlessCredentialType : webAuthnCredentialType; SigningInPage.CredentialType credentialType = passwordless ? webAuthnPwdlessCredentialType : webAuthnCredentialType;
credentialType.clickSetUpLink(); credentialType.clickSetUpLink();
@ -239,4 +409,16 @@ public class WebAuthnSigningInTest extends AbstractWebAuthnAccountTest implement
testRemoveCredential(webAuthn1); testRemoveCredential(webAuthn1);
} }
private RealmAttributeUpdater setLocalesUpdater(String defaultLocale, String... supportedLocales) {
RealmAttributeUpdater updater = new RealmAttributeUpdater(testRealmResource())
.setDefaultLocale(defaultLocale)
.setInternationalizationEnabled(true)
.addSupportedLocale(defaultLocale);
for (String locale : supportedLocales) {
updater.addSupportedLocale(locale);
}
return updater;
}
} }

View file

@ -383,9 +383,10 @@ webauthn-passwordless-display-name=Bezpečnostní klíč
webauthn-passwordless-help-text=Použijte bezpečnostní klíč k přihlášení bez hesla. webauthn-passwordless-help-text=Použijte bezpečnostní klíč k přihlášení bez hesla.
webauthn-login-title=Přihlášení bezpečnostním klíčem webauthn-login-title=Přihlášení bezpečnostním klíčem
webauthn-registration-title=Registrace bezpečnostního klíče webauthn-registration-title=Registrace bezpečnostního klíče
webauthn-available-authenticators=Dostupné autentikátory webauthn-available-authenticators=Dostupné bezpečnostní klíče
webauthn-unsupported-browser-text=WebAuthn není v tomto prohlížeči podporováno. Zkuste jiný prohlížeč nebo kontaktujte svého administrátora. webauthn-unsupported-browser-text=WebAuthn není v tomto prohlížeči podporováno. Zkuste jiný prohlížeč nebo kontaktujte svého administrátora.
webauthn-doAuthenticate=Přihlášení bezpečnostním klíčem webauthn-doAuthenticate=Přihlášení bezpečnostním klíčem
webauthn-createdAt-label=Vytvořeno
# WebAuthn Error # WebAuthn Error
webauthn-error-title=Chyba bezpečnostního klíče webauthn-error-title=Chyba bezpečnostního klíče

View file

@ -416,9 +416,10 @@ webauthn-passwordless-display-name=Security Key
webauthn-passwordless-help-text=Use your security key for passwordless sign in. webauthn-passwordless-help-text=Use your security key for passwordless sign in.
webauthn-login-title=Security Key login webauthn-login-title=Security Key login
webauthn-registration-title=Security Key Registration webauthn-registration-title=Security Key Registration
webauthn-available-authenticators=Available authenticators webauthn-available-authenticators=Available Security Keys
webauthn-unsupported-browser-text=WebAuthn is not supported by this browser. Try another one or contact your administrator. webauthn-unsupported-browser-text=WebAuthn is not supported by this browser. Try another one or contact your administrator.
webauthn-doAuthenticate=Sign in with Security Key webauthn-doAuthenticate=Sign in with Security Key
webauthn-createdAt-label=Created
# WebAuthn Error # WebAuthn Error
webauthn-error-title=Security Key Error webauthn-error-title=Security Key Error

View file

@ -5,34 +5,63 @@
<#elseif section = "header"> <#elseif section = "header">
${kcSanitize(msg("webauthn-login-title"))?no_esc} ${kcSanitize(msg("webauthn-login-title"))?no_esc}
<#elseif section = "form"> <#elseif section = "form">
<div id="kc-form-webauthn" class="${properties.kcFormClass!}">
<form id="webauth" action="${url.loginAction}" method="post">
<input type="hidden" id="clientDataJSON" name="clientDataJSON"/>
<input type="hidden" id="authenticatorData" name="authenticatorData"/>
<input type="hidden" id="signature" name="signature"/>
<input type="hidden" id="credentialId" name="credentialId"/>
<input type="hidden" id="userHandle" name="userHandle"/>
<input type="hidden" id="error" name="error"/>
</form>
<form id="webauth" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post"> <div class="${properties.kcFormGroupClass!} no-bottom-margin">
<div class="${properties.kcFormGroupClass!}"> <#if authenticators??>
<input type="hidden" id="clientDataJSON" name="clientDataJSON"/> <form id="authn_select" class="${properties.kcFormClass!}">
<input type="hidden" id="authenticatorData" name="authenticatorData"/> <#list authenticators.authenticators as authenticator>
<input type="hidden" id="signature" name="signature"/> <input type="hidden" name="authn_use_chk" value="${authenticator.credentialId}"/>
<input type="hidden" id="credentialId" name="credentialId"/> </#list>
<input type="hidden" id="userHandle" name="userHandle"/> </form>
<input type="hidden" id="error" name="error"/>
</div>
</form>
<#if authenticators??> <#if shouldDisplayAuthenticators?? && shouldDisplayAuthenticators>
<form id="authn_select" class="${properties.kcFormClass!}"> <#if authenticators.authenticators?size gt 1>
<#list authenticators.authenticators as authenticator> <p class="${properties.kcSelectAuthListItemTitle!}">${kcSanitize(msg("webauthn-available-authenticators"))}</p>
<input type="hidden" name="authn_use_chk" value="${authenticator.credentialId}"/> </#if>
</#list>
</form>
</#if>
<form class="${properties.kcFormClass!}"> <div class="${properties.kcFormClass!}">
<div class="${properties.kcFormGroupClass!}"> <#list authenticators.authenticators as authenticator>
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}"> <div id="kc-webauthn-authenticator" class="${properties.kcSelectAuthListItemClass!}">
<input id="authenticateWebAuthnButton" type="button" onclick="webAuthnAuthenticate()" value="${kcSanitize(msg("webauthn-doAuthenticate"))}" <div class="${properties.kcSelectAuthListItemIconClass!}">
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"> <i class="${properties.kcWebAuthnKeyIcon} fa-2x"></i>
</div>
<div class="${properties.kcSelectAuthListItemBodyClass!}">
<div id="kc-webauthn-authenticator-label"
class="${properties.kcSelectAuthListItemHeadingClass!}">
${msg('${authenticator.label}')}
</div>
<div class="${properties.kcSelectAuthListItemDescriptionClass!}">
<span id="kc-webauthn-authenticator-created-label">
${msg('webauthn-createdAt-label')}
</span>
<span id="kc-webauthn-authenticator-created">
${authenticator.createdAt}
</span>
</div>
</div>
<div class="${properties.kcSelectAuthListItemFillClass!}"></div>
</div>
</#list>
</div>
</#if>
</#if>
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
<input id="authenticateWebAuthnButton" type="button" onclick="webAuthnAuthenticate()"
value="${kcSanitize(msg("webauthn-doAuthenticate"))}"
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"/>
</div>
</div> </div>
</div> </div>
</form>
<script type="text/javascript" src="${url.resourcesCommonPath}/node_modules/jquery/dist/jquery.min.js"></script> <script type="text/javascript" src="${url.resourcesCommonPath}/node_modules/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="${url.resourcesPath}/js/base64url.js"></script> <script type="text/javascript" src="${url.resourcesPath}/js/base64url.js"></script>

View file

@ -18,25 +18,6 @@
<input type="hidden" id="isSetRetry" name="isSetRetry"/> <input type="hidden" id="isSetRetry" name="isSetRetry"/>
</form> </form>
<#if authenticators??>
<table class="table">
<thead>
<tr>
<th>${kcSanitize(msg("webauthn-available-authenticators"))?no_esc}</th>
</tr>
</thead>
<tbody>
<#list authenticators.authenticators as authenticator>
<tr>
<th>
<span id="kc-webauthn-authenticator">${kcSanitize(authenticator.label)?no_esc}</span>
</th>
</tr>
</#list>
</tbody>
</table>
</#if>
<input tabindex="4" onclick="refreshPage()" type="button" <input tabindex="4" onclick="refreshPage()" type="button"
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
name="try-again" id="kc-try-again" value="${kcSanitize(msg("doTryAgain"))?no_esc}" name="try-again" id="kc-try-again" value="${kcSanitize(msg("doTryAgain"))?no_esc}"
@ -46,7 +27,7 @@
<form action="${url.loginAction}" class="${properties.kcFormClass!}" id="kc-webauthn-settings-form" method="post"> <form action="${url.loginAction}" class="${properties.kcFormClass!}" id="kc-webauthn-settings-form" method="post">
<button type="submit" <button type="submit"
class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
id="cancelWebAuthnAIA" name="cancel-aia" value="true"/>${msg("doCancel")} id="cancelWebAuthnAIA" name="cancel-aia" value="true">${msg("doCancel")}
</button> </button>
</form> </form>
</#if> </#if>

View file

@ -262,6 +262,18 @@ div.kc-logo-text span {
padding-top: 8px; padding-top: 8px;
} }
#kc-form-webauthn .select-auth-box-parent {
pointer-events: none;
}
#kc-form-webauthn .select-auth-box-desc {
color: var(--pf-global--palette--black-600);
}
#kc-form-webauthn .select-auth-box-headline {
color: var(--pf-global--Color--300);
}
#kc-content-wrapper { #kc-content-wrapper {
margin-top: 20px; margin-top: 20px;
} }
@ -620,6 +632,12 @@ ul#kc-totp-supported-apps {
font-size: var(--pf-global--FontSize--sm); font-size: var(--pf-global--FontSize--sm);
} }
.select-auth-box-paragraph {
text-align: center;
font-size: var(--pf-global--FontSize--md);
margin-bottom: 5px;
}
.card-pf { .card-pf {
margin: 0 auto; margin: 0 auto;
box-shadow: var(--pf-global--BoxShadow--lg); box-shadow: var(--pf-global--BoxShadow--lg);

View file

@ -106,6 +106,7 @@ kcSelectAuthListItemDescriptionClass=pf-l-stack__item select-auth-box-desc
kcSelectAuthListItemFillClass=pf-l-split__item pf-m-fill kcSelectAuthListItemFillClass=pf-l-split__item pf-m-fill
kcSelectAuthListItemArrowClass=pf-l-split__item select-auth-box-arrow kcSelectAuthListItemArrowClass=pf-l-split__item select-auth-box-arrow
kcSelectAuthListItemArrowIconClass=fa fa-angle-right fa-lg kcSelectAuthListItemArrowIconClass=fa fa-angle-right fa-lg
kcSelectAuthListItemTitle=select-auth-box-paragraph
##### css classes for the authenticators ##### css classes for the authenticators
kcAuthenticatorDefaultClass=fa list-view-pf-icon-lg kcAuthenticatorDefaultClass=fa list-view-pf-icon-lg