KEYCLOAK-19490 Add more details about 2FA to authenticate page (#9252)
Closes #9494
This commit is contained in:
parent
ab9413b48c
commit
d75d28468e
19 changed files with 594 additions and 149 deletions
|
@ -19,51 +19,51 @@ package org.keycloak;
|
|||
public interface WebAuthnConstants {
|
||||
|
||||
// Interface binded by FreeMarker template between UA and RP
|
||||
final String USER_ID = "userid";
|
||||
final String USER_NAME = "username";
|
||||
final String CHALLENGE = "challenge";
|
||||
final String ORIGIN = "origin";
|
||||
final String ERROR = "error";
|
||||
final String PUBLIC_KEY_CREDENTIAL_ID= "publicKeyCredentialId";
|
||||
final String CREDENTIAL_ID = "credentialId";
|
||||
final String CLIENT_DATA_JSON = "clientDataJSON";
|
||||
final String AUTHENTICATOR_DATA = "authenticatorData";
|
||||
final String SIGNATURE = "signature";
|
||||
final String USER_HANDLE = "userHandle";
|
||||
final String ATTESTATION_OBJECT= "attestationObject";
|
||||
final String AUTHENTICATOR_LABEL = "authenticatorLabel";
|
||||
final String RP_ENTITY_NAME = "rpEntityName";
|
||||
final String SIGNATURE_ALGORITHMS = "signatureAlgorithms";
|
||||
final String RP_ID = "rpId";
|
||||
final String ATTESTATION_CONVEYANCE_PREFERENCE = "attestationConveyancePreference";
|
||||
final String AUTHENTICATOR_ATTACHMENT = "authenticatorAttachment";
|
||||
final String REQUIRE_RESIDENT_KEY = "requireResidentKey";
|
||||
final String USER_VERIFICATION_REQUIREMENT = "userVerificationRequirement";
|
||||
final String CREATE_TIMEOUT = "createTimeout";
|
||||
final String EXCLUDE_CREDENTIAL_IDS = "excludeCredentialIds";
|
||||
final String ALLOWED_AUTHENTICATORS = "authenticators";
|
||||
final String IS_USER_IDENTIFIED = "isUserIdentified";
|
||||
final String USER_VERIFICATION = "userVerification";
|
||||
final String IS_SET_RETRY = "isSetRetry";
|
||||
|
||||
String USER_ID = "userid";
|
||||
String USER_NAME = "username";
|
||||
String CHALLENGE = "challenge";
|
||||
String ORIGIN = "origin";
|
||||
String ERROR = "error";
|
||||
String PUBLIC_KEY_CREDENTIAL_ID = "publicKeyCredentialId";
|
||||
String CREDENTIAL_ID = "credentialId";
|
||||
String CLIENT_DATA_JSON = "clientDataJSON";
|
||||
String AUTHENTICATOR_DATA = "authenticatorData";
|
||||
String SIGNATURE = "signature";
|
||||
String USER_HANDLE = "userHandle";
|
||||
String ATTESTATION_OBJECT = "attestationObject";
|
||||
String AUTHENTICATOR_LABEL = "authenticatorLabel";
|
||||
String RP_ENTITY_NAME = "rpEntityName";
|
||||
String SIGNATURE_ALGORITHMS = "signatureAlgorithms";
|
||||
String RP_ID = "rpId";
|
||||
String ATTESTATION_CONVEYANCE_PREFERENCE = "attestationConveyancePreference";
|
||||
String AUTHENTICATOR_ATTACHMENT = "authenticatorAttachment";
|
||||
String REQUIRE_RESIDENT_KEY = "requireResidentKey";
|
||||
String USER_VERIFICATION_REQUIREMENT = "userVerificationRequirement";
|
||||
String CREATE_TIMEOUT = "createTimeout";
|
||||
String EXCLUDE_CREDENTIAL_IDS = "excludeCredentialIds";
|
||||
String ALLOWED_AUTHENTICATORS = "authenticators";
|
||||
String IS_USER_IDENTIFIED = "isUserIdentified";
|
||||
String USER_VERIFICATION = "userVerification";
|
||||
String IS_SET_RETRY = "isSetRetry";
|
||||
String SHOULD_DISPLAY_AUTHENTICATORS = "shouldDisplayAuthenticators";
|
||||
|
||||
// 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
|
||||
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
|
||||
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)
|
||||
final String AUTH_CHALLENGE_NOTE = "WEBAUTH_CHALLENGE";
|
||||
String AUTH_CHALLENGE_NOTE = "WEBAUTH_CHALLENGE";
|
||||
|
||||
// option values on WebAuth API
|
||||
final String OPTION_REQUIRED = "required";
|
||||
final String OPTION_PREFERED = "preferred";
|
||||
final String OPTION_DISCOURAGED = "discouraged";
|
||||
final String OPTION_NOT_SPECIFIED = "";
|
||||
String OPTION_REQUIRED = "required";
|
||||
String OPTION_PREFERED = "preferred";
|
||||
String OPTION_DISCOURAGED = "discouraged";
|
||||
String OPTION_NOT_SPECIFIED = "";
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -104,6 +104,7 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
|
|||
// read options from policy
|
||||
String userVerificationRequirement = policy.getUserVerificationRequirement();
|
||||
form.setAttribute(WebAuthnConstants.USER_VERIFICATION, userVerificationRequirement);
|
||||
form.setAttribute(WebAuthnConstants.SHOULD_DISPLAY_AUTHENTICATORS, shouldDisplayAuthenticators(context));
|
||||
|
||||
context.challenge(form.createLoginWebAuthn());
|
||||
}
|
||||
|
@ -123,6 +124,9 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
|
|||
return WebAuthnCredentialModel.TYPE_TWOFACTOR;
|
||||
}
|
||||
|
||||
protected boolean shouldDisplayAuthenticators(AuthenticationFlowContext context) {
|
||||
return context.getUser() != null;
|
||||
}
|
||||
|
||||
public void action(AuthenticationFlowContext context) {
|
||||
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) {
|
||||
LoginFormsProvider provider = context.form().setError(errorCase);
|
||||
LoginFormsProvider provider = context.form().setError(errorCase, "");
|
||||
UserModel user = context.getUser();
|
||||
if (user != null) {
|
||||
WebAuthnAuthenticatorsBean authenticators = new WebAuthnAuthenticatorsBean(context.getSession(), context.getRealm(), user, getCredentialType());
|
||||
|
|
|
@ -55,6 +55,11 @@ public class WebAuthnPasswordlessAuthenticator extends WebAuthnAuthenticator {
|
|||
return WebAuthnCredentialModel.TYPE_PASSWORDLESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldDisplayAuthenticators(AuthenticationFlowContext context){
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
// ask the user to do required action to register webauthn authenticator
|
||||
|
|
|
@ -24,8 +24,10 @@ import org.keycloak.models.KeycloakSession;
|
|||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||
import org.keycloak.theme.DateTimeFormatterUtil;
|
||||
|
||||
public class WebAuthnAuthenticatorsBean {
|
||||
|
||||
private List<WebAuthnAuthenticatorBean> authenticators = new LinkedList<WebAuthnAuthenticatorBean>();
|
||||
|
||||
public WebAuthnAuthenticatorsBean(KeycloakSession session, RealmModel realm, UserModel user, String credentialType) {
|
||||
|
@ -35,7 +37,8 @@ public class WebAuthnAuthenticatorsBean {
|
|||
.map(webAuthnCredential -> {
|
||||
String credentialId = Base64Url.encodeBase64ToBase64Url(webAuthnCredential.getWebAuthnCredentialData().getCredentialId());
|
||||
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());
|
||||
}
|
||||
|
||||
|
@ -46,10 +49,12 @@ public class WebAuthnAuthenticatorsBean {
|
|||
public static class WebAuthnAuthenticatorBean {
|
||||
private final String credentialId;
|
||||
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.label = label;
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public String getCredentialId() {
|
||||
|
@ -59,5 +64,9 @@ public class WebAuthnAuthenticatorsBean {
|
|||
public String getLabel() {
|
||||
return this.label;
|
||||
}
|
||||
|
||||
public String getCreatedAt() {
|
||||
return this.createdAt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -97,4 +97,19 @@ public class RealmAttributeUpdater extends ServerResourceUpdater<RealmAttributeU
|
|||
rep.setBrowserFlow(browserFlow);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -29,9 +29,6 @@ public class WebAuthnErrorPage extends LanguageComboboxAwarePage {
|
|||
@FindBy(className = "alert-error")
|
||||
private WebElement errorMessage;
|
||||
|
||||
@FindBy(id = "kc-webauthn-authenticator")
|
||||
private List<WebElement> authenticators;
|
||||
|
||||
public void clickTryAgain() {
|
||||
WaitUtils.waitUntilElement(tryAgainButton).is().clickable();
|
||||
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
|
||||
public boolean isCurrent() {
|
||||
try {
|
||||
|
|
|
@ -17,11 +17,13 @@
|
|||
|
||||
package org.keycloak.testsuite.webauthn.pages;
|
||||
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.keycloak.testsuite.pages.LanguageComboboxAwarePage;
|
||||
import org.keycloak.testsuite.util.WaitUtils;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.support.FindBy;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
/**
|
||||
|
@ -32,6 +34,12 @@ public class WebAuthnLoginPage extends LanguageComboboxAwarePage {
|
|||
@FindBy(id = "authenticateWebAuthnButton")
|
||||
private WebElement authenticateButton;
|
||||
|
||||
@FindBy(id = "kc-webauthn-authenticator-label")
|
||||
private List<WebElement> authenticatorsLabels;
|
||||
|
||||
@Page
|
||||
private WebAuthnAuthenticatorsList authenticators;
|
||||
|
||||
public void clickAuthenticate() {
|
||||
WaitUtils.waitUntilElement(authenticateButton).is().clickable();
|
||||
authenticateButton.click();
|
||||
|
@ -46,6 +54,10 @@ public class WebAuthnLoginPage extends LanguageComboboxAwarePage {
|
|||
}
|
||||
}
|
||||
|
||||
public WebAuthnAuthenticatorsList getAuthenticators() {
|
||||
return authenticators;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void open() {
|
||||
throw new UnsupportedOperationException();
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
*/
|
||||
package org.keycloak.testsuite.webauthn;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Rule;
|
||||
|
@ -51,6 +52,7 @@ import org.keycloak.testsuite.pages.RegisterPage;
|
|||
import org.keycloak.testsuite.pages.SelectAuthenticatorPage;
|
||||
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
|
||||
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.WebAuthnRegisterPage;
|
||||
import org.keycloak.testsuite.webauthn.updaters.WebAuthnRealmAttributeUpdater;
|
||||
|
@ -178,6 +180,11 @@ public class WebAuthnRegisterAndLoginTest extends AbstractWebAuthnVirtualTest {
|
|||
loginPage.login(username, password);
|
||||
|
||||
webAuthnLoginPage.assertCurrent();
|
||||
|
||||
final WebAuthnAuthenticatorsList authenticators = webAuthnLoginPage.getAuthenticators();
|
||||
assertThat(authenticators.getCount(), is(1));
|
||||
assertThat(authenticators.getLabels(), Matchers.contains(authenticatorLabel));
|
||||
|
||||
webAuthnLoginPage.clickAuthenticate();
|
||||
|
||||
appPage.assertCurrent();
|
||||
|
@ -204,7 +211,7 @@ public class WebAuthnRegisterAndLoginTest extends AbstractWebAuthnVirtualTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testWebAuthnPasswordlessAlternativeWithWebAuthnAndPassword() throws IOException {
|
||||
public void webAuthnPasswordlessAlternativeWithWebAuthnAndPassword() throws IOException {
|
||||
String userId = null;
|
||||
|
||||
final String WEBAUTHN_LABEL = "webauthn";
|
||||
|
@ -280,6 +287,11 @@ public class WebAuthnRegisterAndLoginTest extends AbstractWebAuthnVirtualTest {
|
|||
passwordPage.login("password");
|
||||
|
||||
webAuthnLoginPage.assertCurrent();
|
||||
|
||||
final WebAuthnAuthenticatorsList authenticators = webAuthnLoginPage.getAuthenticators();
|
||||
assertThat(authenticators.getCount(), is(1));
|
||||
assertThat(authenticators.getLabels(), Matchers.contains(WEBAUTHN_LABEL));
|
||||
|
||||
webAuthnLoginPage.clickAuthenticate();
|
||||
|
||||
appPage.assertCurrent();
|
||||
|
@ -299,6 +311,8 @@ public class WebAuthnRegisterAndLoginTest extends AbstractWebAuthnVirtualTest {
|
|||
selectAuthenticatorPage.selectLoginMethod(SelectAuthenticatorPage.SECURITY_KEY);
|
||||
|
||||
webAuthnLoginPage.assertCurrent();
|
||||
assertThat(webAuthnLoginPage.getAuthenticators().getCount(), is(0));
|
||||
|
||||
webAuthnLoginPage.clickAuthenticate();
|
||||
|
||||
appPage.assertCurrent();
|
||||
|
@ -310,7 +324,7 @@ public class WebAuthnRegisterAndLoginTest extends AbstractWebAuthnVirtualTest {
|
|||
}
|
||||
|
||||
@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
|
||||
try (RealmAttributeUpdater rau = new RealmAttributeUpdater(testRealm()).setBrowserFlow("browser-webauthn-passwordless").update()) {
|
||||
// Login as test-user@localhost with password
|
||||
|
|
|
@ -20,11 +20,13 @@ package org.keycloak.testsuite.webauthn.account;
|
|||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory;
|
||||
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.WebAuthnRegisterFactory;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||
import org.keycloak.representations.idm.AuthenticationExecutionRepresentation;
|
||||
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.ui.account2.page.SigningInPage;
|
||||
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.authenticators.DefaultVirtualAuthOptions;
|
||||
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) {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,20 +20,16 @@ package org.keycloak.testsuite.webauthn.account;
|
|||
import org.hamcrest.Matchers;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
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.testsuite.updaters.RealmAttributeUpdater;
|
||||
import org.keycloak.testsuite.util.FlowUtil;
|
||||
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.WebAuthnLoginPage;
|
||||
import org.keycloak.testsuite.webauthn.updaters.WebAuthnRealmAttributeUpdater;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.CoreMatchers.notNullValue;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
|
@ -47,11 +43,10 @@ public class WebAuthnErrorTest extends AbstractWebAuthnAccountTest {
|
|||
protected WebAuthnErrorPage webAuthnErrorPage;
|
||||
|
||||
@Test
|
||||
public void errorPageWithPossibleAuthenticators() throws IOException {
|
||||
public void errorPageWithTimeout() throws IOException {
|
||||
final int timeoutSec = 3;
|
||||
|
||||
addWebAuthnCredential("authenticator#1");
|
||||
addWebAuthnCredential("authenticator#2");
|
||||
final String authenticatorLabel = "authenticator";
|
||||
addWebAuthnCredential(authenticatorLabel);
|
||||
|
||||
try (RealmAttributeUpdater u = new WebAuthnRealmAttributeUpdater(testRealmResource())
|
||||
.setWebAuthnPolicyCreateTimeout(timeoutSec)
|
||||
|
@ -62,7 +57,7 @@ public class WebAuthnErrorTest extends AbstractWebAuthnAccountTest {
|
|||
assertThat(realm.getWebAuthnPolicyCreateTimeout(), is(timeoutSec));
|
||||
|
||||
final int webAuthnCount = webAuthnCredentialType.getUserCredentialsCount();
|
||||
assertThat(webAuthnCount, is(2));
|
||||
assertThat(webAuthnCount, is(1));
|
||||
|
||||
getWebAuthnManager().getCurrent().getAuthenticator().removeAllCredentials();
|
||||
|
||||
|
@ -73,28 +68,18 @@ public class WebAuthnErrorTest extends AbstractWebAuthnAccountTest {
|
|||
loginToAccount();
|
||||
|
||||
webAuthnLoginPage.assertCurrent();
|
||||
|
||||
final WebAuthnAuthenticatorsList authenticators = webAuthnLoginPage.getAuthenticators();
|
||||
assertThat(authenticators.getCount(), is(1));
|
||||
assertThat(authenticators.getLabels(), Matchers.contains(authenticatorLabel));
|
||||
|
||||
webAuthnLoginPage.clickAuthenticate();
|
||||
|
||||
//Should fail after this time
|
||||
WaitUtils.pause((timeoutSec + 1) * 1000);
|
||||
|
||||
webAuthnErrorPage.assertCurrent();
|
||||
assertThat(webAuthnErrorPage.getError(), containsString("Failed to authenticate by the Security key."));
|
||||
assertThat(webAuthnErrorPage.getAuthenticatorsCount(), is(2));
|
||||
assertThat(webAuthnErrorPage.getAuthenticators(), Matchers.contains("authenticator#1", "authenticator#2"));
|
||||
assertThat(webAuthnErrorPage.getError(), is("Failed to authenticate by the Security key."));
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,23 +17,30 @@
|
|||
|
||||
package org.keycloak.testsuite.webauthn.account;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.Test;
|
||||
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.WebAuthnRegisterFactory;
|
||||
import org.keycloak.representations.idm.AuthenticationExecutionRepresentation;
|
||||
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
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.utils.SigningInPageUtils;
|
||||
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
|
||||
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.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
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.Matchers.empty;
|
||||
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.testSetUpLink;
|
||||
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 {
|
||||
|
||||
@Page
|
||||
protected WebAuthnLoginPage webAuthnLoginPage;
|
||||
|
||||
@Test
|
||||
public void categoriesTest() {
|
||||
testContext.setTestRealmReps(emptyList()); // reimport realm after this test
|
||||
|
@ -78,7 +87,7 @@ public class WebAuthnSigningInTest extends AbstractWebAuthnAccountTest implement
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testCreateWebAuthnSameUserLabel() {
|
||||
public void createWebAuthnSameUserLabel() {
|
||||
final String SAME_LABEL = "key123";
|
||||
|
||||
// Do we really allow to have several authenticators with the same user label??
|
||||
|
@ -112,7 +121,7 @@ public class WebAuthnSigningInTest extends AbstractWebAuthnAccountTest implement
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleSecurityKeys() {
|
||||
public void multipleSecurityKeys() {
|
||||
final String LABEL = "SecurityKey#";
|
||||
|
||||
List<SigningInPage.UserCredential> createdCredentials = new ArrayList<>();
|
||||
|
@ -168,16 +177,177 @@ public class WebAuthnSigningInTest extends AbstractWebAuthnAccountTest implement
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testCancelRegistration() {
|
||||
cancelRegistration(false);
|
||||
public void displayAvailableAuthenticators() {
|
||||
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
|
||||
public void testCancelPasswordlessRegistration() {
|
||||
cancelRegistration(true);
|
||||
public void notDisplayAvailableAuthenticatorsPasswordless() {
|
||||
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;
|
||||
|
||||
credentialType.clickSetUpLink();
|
||||
|
@ -239,4 +409,16 @@ public class WebAuthnSigningInTest extends AbstractWebAuthnAccountTest implement
|
|||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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-login-title=Přihlášení bezpečnostním klíčem
|
||||
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-doAuthenticate=Přihlášení bezpečnostním klíčem
|
||||
webauthn-createdAt-label=Vytvořeno
|
||||
|
||||
# WebAuthn Error
|
||||
webauthn-error-title=Chyba bezpečnostního klíče
|
||||
|
|
|
@ -416,9 +416,10 @@ webauthn-passwordless-display-name=Security Key
|
|||
webauthn-passwordless-help-text=Use your security key for passwordless sign in.
|
||||
webauthn-login-title=Security Key login
|
||||
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-doAuthenticate=Sign in with Security Key
|
||||
webauthn-createdAt-label=Created
|
||||
|
||||
# WebAuthn Error
|
||||
webauthn-error-title=Security Key Error
|
||||
|
|
|
@ -5,34 +5,63 @@
|
|||
<#elseif section = "header">
|
||||
${kcSanitize(msg("webauthn-login-title"))?no_esc}
|
||||
<#elseif section = "form">
|
||||
|
||||
<form id="webauth" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
|
||||
<div class="${properties.kcFormGroupClass!}">
|
||||
<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"/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="${properties.kcFormGroupClass!} no-bottom-margin">
|
||||
<#if authenticators??>
|
||||
<form id="authn_select" class="${properties.kcFormClass!}">
|
||||
<#list authenticators.authenticators as authenticator>
|
||||
<input type="hidden" name="authn_use_chk" value="${authenticator.credentialId}"/>
|
||||
</#list>
|
||||
</form>
|
||||
|
||||
<#if shouldDisplayAuthenticators?? && shouldDisplayAuthenticators>
|
||||
<#if authenticators.authenticators?size gt 1>
|
||||
<p class="${properties.kcSelectAuthListItemTitle!}">${kcSanitize(msg("webauthn-available-authenticators"))}</p>
|
||||
</#if>
|
||||
|
||||
<div class="${properties.kcFormClass!}">
|
||||
<#list authenticators.authenticators as authenticator>
|
||||
<div id="kc-webauthn-authenticator" class="${properties.kcSelectAuthListItemClass!}">
|
||||
<div class="${properties.kcSelectAuthListItemIconClass!}">
|
||||
<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>
|
||||
|
||||
<form class="${properties.kcFormClass!}">
|
||||
<div class="${properties.kcFormGroupClass!}">
|
||||
<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!}">
|
||||
<input id="authenticateWebAuthnButton" type="button" onclick="webAuthnAuthenticate()"
|
||||
value="${kcSanitize(msg("webauthn-doAuthenticate"))}"
|
||||
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"/>
|
||||
</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.resourcesPath}/js/base64url.js"></script>
|
||||
|
|
|
@ -18,25 +18,6 @@
|
|||
<input type="hidden" id="isSetRetry" name="isSetRetry"/>
|
||||
</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"
|
||||
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
|
||||
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">
|
||||
<button type="submit"
|
||||
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>
|
||||
</form>
|
||||
</#if>
|
||||
|
|
|
@ -262,6 +262,18 @@ div.kc-logo-text span {
|
|||
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 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
@ -620,6 +632,12 @@ ul#kc-totp-supported-apps {
|
|||
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 {
|
||||
margin: 0 auto;
|
||||
box-shadow: var(--pf-global--BoxShadow--lg);
|
||||
|
|
|
@ -106,6 +106,7 @@ kcSelectAuthListItemDescriptionClass=pf-l-stack__item select-auth-box-desc
|
|||
kcSelectAuthListItemFillClass=pf-l-split__item pf-m-fill
|
||||
kcSelectAuthListItemArrowClass=pf-l-split__item select-auth-box-arrow
|
||||
kcSelectAuthListItemArrowIconClass=fa fa-angle-right fa-lg
|
||||
kcSelectAuthListItemTitle=select-auth-box-paragraph
|
||||
|
||||
##### css classes for the authenticators
|
||||
kcAuthenticatorDefaultClass=fa list-view-pf-icon-lg
|
||||
|
|
Loading…
Reference in a new issue