Store information about transport media of WebAuthn authenticator
Closes #9800
This commit is contained in:
parent
07d43f31f3
commit
d82122b982
19 changed files with 510 additions and 36 deletions
|
@ -26,6 +26,9 @@ import org.keycloak.models.credential.dto.WebAuthnCredentialData;
|
|||
import org.keycloak.models.credential.dto.WebAuthnSecretData;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
|
@ -49,7 +52,12 @@ public class WebAuthnCredentialModel extends CredentialModel {
|
|||
|
||||
public static WebAuthnCredentialModel create(String credentialType, String userLabel, String aaguid, String credentialId,
|
||||
String attestationStatement, String credentialPublicKey, long counter, String attestationStatementFormat) {
|
||||
WebAuthnCredentialData credentialData = new WebAuthnCredentialData(aaguid, credentialId, counter, attestationStatement, credentialPublicKey, attestationStatementFormat);
|
||||
return create(credentialType, userLabel, aaguid, credentialId, attestationStatement, credentialPublicKey, counter, attestationStatementFormat, Collections.emptySet());
|
||||
}
|
||||
|
||||
public static WebAuthnCredentialModel create(String credentialType, String userLabel, String aaguid, String credentialId,
|
||||
String attestationStatement, String credentialPublicKey, long counter, String attestationStatementFormat, Set<String> transports) {
|
||||
WebAuthnCredentialData credentialData = new WebAuthnCredentialData(aaguid, credentialId, counter, attestationStatement, credentialPublicKey, attestationStatementFormat, transports);
|
||||
WebAuthnSecretData secretData = new WebAuthnSecretData();
|
||||
|
||||
WebAuthnCredentialModel credentialModel = new WebAuthnCredentialModel(credentialType, credentialData, secretData);
|
||||
|
|
|
@ -21,6 +21,10 @@ package org.keycloak.models.credential.dto;
|
|||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
|
@ -32,6 +36,7 @@ public class WebAuthnCredentialData {
|
|||
private String attestationStatement;
|
||||
private String credentialPublicKey;
|
||||
private String attestationStatementFormat;
|
||||
private Set<String> transports;
|
||||
|
||||
@JsonCreator
|
||||
public WebAuthnCredentialData(@JsonProperty("aaguid") String aaguid,
|
||||
|
@ -39,13 +44,15 @@ public class WebAuthnCredentialData {
|
|||
@JsonProperty("counter") long counter,
|
||||
@JsonProperty("attestationStatement") String attestationStatement,
|
||||
@JsonProperty("credentialPublicKey") String credentialPublicKey,
|
||||
@JsonProperty("attestationStatementFormat") String attestationStatementFormat) {
|
||||
@JsonProperty("attestationStatementFormat") String attestationStatementFormat,
|
||||
@JsonProperty("transports") Set<String> transports) {
|
||||
this.aaguid = aaguid;
|
||||
this.credentialId = credentialId;
|
||||
this.counter = counter;
|
||||
this.attestationStatement = attestationStatement;
|
||||
this.credentialPublicKey = credentialPublicKey;
|
||||
this.attestationStatementFormat = attestationStatementFormat;
|
||||
this.transports = transports;
|
||||
}
|
||||
|
||||
public String getAaguid() {
|
||||
|
@ -80,6 +87,14 @@ public class WebAuthnCredentialData {
|
|||
this.attestationStatementFormat = attestationStatementFormat;
|
||||
}
|
||||
|
||||
public Set<String> getTransports() {
|
||||
return transports != null ? transports : Collections.emptySet();
|
||||
}
|
||||
|
||||
public void setTransports(Set<String> transports) {
|
||||
this.transports = transports;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "WebAuthnCredentialData { " +
|
||||
|
@ -90,6 +105,7 @@ public class WebAuthnCredentialData {
|
|||
", attestationStatement='" + attestationStatement + '\'' +
|
||||
", credentialPublicKey='" + credentialPublicKey + '\'' +
|
||||
", attestationStatementFormat='" + attestationStatementFormat + '\'' +
|
||||
", transports=" + Arrays.toString(getTransports().toArray()) +
|
||||
" }";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,6 +44,8 @@ public interface WebAuthnConstants {
|
|||
String ALLOWED_AUTHENTICATORS = "authenticators";
|
||||
String IS_USER_IDENTIFIED = "isUserIdentified";
|
||||
String USER_VERIFICATION = "userVerification";
|
||||
String TRANSPORTS = "transports";
|
||||
|
||||
String IS_SET_RETRY = "isSetRetry";
|
||||
String SHOULD_DISPLAY_AUTHENTICATORS = "shouldDisplayAuthenticators";
|
||||
|
||||
|
|
|
@ -20,7 +20,9 @@ import java.nio.charset.StandardCharsets;
|
|||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
@ -28,6 +30,8 @@ import javax.ws.rs.core.MultivaluedMap;
|
|||
import javax.ws.rs.core.Response;
|
||||
|
||||
import com.webauthn4j.WebAuthnRegistrationManager;
|
||||
import com.webauthn4j.data.AuthenticatorTransport;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.WebAuthnConstants;
|
||||
|
@ -69,6 +73,7 @@ import com.webauthn4j.validator.attestation.statement.u2f.FIDOU2FAttestationStat
|
|||
import com.webauthn4j.validator.attestation.trustworthiness.certpath.CertPathTrustworthinessValidator;
|
||||
import com.webauthn4j.validator.attestation.trustworthiness.self.DefaultSelfAttestationTrustworthinessValidator;
|
||||
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||
import org.keycloak.utils.StringUtil;
|
||||
|
||||
import static org.keycloak.WebAuthnConstants.REG_ERR_DETAIL_LABEL;
|
||||
import static org.keycloak.WebAuthnConstants.REG_ERR_LABEL;
|
||||
|
@ -205,7 +210,17 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
|
|||
// check User Verification by considering a malicious user might modify the result of calling WebAuthn API
|
||||
boolean isUserVerificationRequired = policy.getUserVerificationRequirement().equals(WebAuthnConstants.OPTION_REQUIRED);
|
||||
|
||||
RegistrationRequest registrationRequest = new RegistrationRequest(attestationObject, clientDataJSON);
|
||||
final String transportsParam = params.getFirst(WebAuthnConstants.TRANSPORTS);
|
||||
|
||||
RegistrationRequest registrationRequest;
|
||||
|
||||
if (StringUtil.isNotBlank(transportsParam)) {
|
||||
final Set<String> transports = new HashSet<>(Arrays.asList(transportsParam.split(",")));
|
||||
registrationRequest = new RegistrationRequest(attestationObject, clientDataJSON, transports);
|
||||
} else {
|
||||
registrationRequest = new RegistrationRequest(attestationObject, clientDataJSON);
|
||||
}
|
||||
|
||||
RegistrationParameters registrationParameters = new RegistrationParameters(serverProperty, isUserVerificationRequired);
|
||||
|
||||
WebAuthnRegistrationManager webAuthnRegistrationManager = createWebAuthnRegistrationManager();
|
||||
|
@ -224,6 +239,7 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
|
|||
credential.setAttestedCredentialData(registrationData.getAttestationObject().getAuthenticatorData().getAttestedCredentialData());
|
||||
credential.setCount(registrationData.getAttestationObject().getAuthenticatorData().getSignCount());
|
||||
credential.setAttestationStatementFormat(registrationData.getAttestationObject().getFormat());
|
||||
credential.setTransports(registrationData.getTransports());
|
||||
|
||||
// Save new webAuthn credential
|
||||
WebAuthnCredentialProvider webAuthnCredProvider = (WebAuthnCredentialProvider) this.session.getProvider(CredentialProvider.class, getCredentialProviderId());
|
||||
|
@ -305,9 +321,17 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
|
|||
private void showInfoAfterWebAuthnApiCreate(RegistrationData response) {
|
||||
AttestedCredentialData attestedCredentialData = response.getAttestationObject().getAuthenticatorData().getAttestedCredentialData();
|
||||
AttestationStatement attestationStatement = response.getAttestationObject().getAttestationStatement();
|
||||
Set<AuthenticatorTransport> transports = response.getTransports();
|
||||
|
||||
logger.debugv("createad key's algorithm = {0}", String.valueOf(attestedCredentialData.getCOSEKey().getAlgorithm().getValue()));
|
||||
logger.debugv("aaguid = {0}", attestedCredentialData.getAaguid().toString());
|
||||
logger.debugv("attestation format = {0}", attestationStatement.getFormat());
|
||||
|
||||
if (CollectionUtils.isNotEmpty(transports)) {
|
||||
logger.debugv("transports = [{0}]", transports.stream()
|
||||
.map(AuthenticatorTransport::getValue)
|
||||
.collect(Collectors.joining(",")));
|
||||
}
|
||||
}
|
||||
|
||||
private void checkAcceptedAuthenticator(RegistrationData response, WebAuthnPolicy policy) throws Exception {
|
||||
|
|
|
@ -16,14 +16,19 @@
|
|||
|
||||
package org.keycloak.credential;
|
||||
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.keycloak.common.util.Base64;
|
||||
|
||||
import com.webauthn4j.data.AuthenticationParameters;
|
||||
import com.webauthn4j.data.AuthenticationRequest;
|
||||
import com.webauthn4j.data.AuthenticatorTransport;
|
||||
import com.webauthn4j.data.attestation.authenticator.AttestedCredentialData;
|
||||
import com.webauthn4j.data.attestation.authenticator.COSEKey;
|
||||
import com.webauthn4j.data.attestation.statement.AttestationStatement;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class WebAuthnCredentialModelInput implements CredentialInput {
|
||||
|
||||
private AttestedCredentialData attestedCredentialData;
|
||||
|
@ -34,6 +39,7 @@ public class WebAuthnCredentialModelInput implements CredentialInput {
|
|||
private String credentialDBId;
|
||||
private final String credentialType;
|
||||
private String attestationStatementFormat;
|
||||
private Set<AuthenticatorTransport> transports;
|
||||
|
||||
public WebAuthnCredentialModelInput(String credentialType) {
|
||||
this.credentialType = credentialType;
|
||||
|
@ -115,6 +121,14 @@ public class WebAuthnCredentialModelInput implements CredentialInput {
|
|||
this.attestationStatementFormat = attestationStatementFormat;
|
||||
}
|
||||
|
||||
public Set<AuthenticatorTransport> getTransports() {
|
||||
return transports;
|
||||
}
|
||||
|
||||
public void setTransports(Set<AuthenticatorTransport> transports) {
|
||||
this.transports = transports;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder("Credential Type = " + credentialType + ",");
|
||||
if (credentialDBId != null)
|
||||
|
@ -156,6 +170,15 @@ public class WebAuthnCredentialModelInput implements CredentialInput {
|
|||
.append(Base64.encodeBytes(authenticationRequest.getCredentialId()))
|
||||
.append(",");
|
||||
}
|
||||
if (CollectionUtils.isNotEmpty(transports)) {
|
||||
final String transportsString = transports.stream()
|
||||
.map(AuthenticatorTransport::getValue)
|
||||
.collect(Collectors.joining(","));
|
||||
|
||||
sb.append("Transports = [")
|
||||
.append(transportsString)
|
||||
.append("],");
|
||||
}
|
||||
if (sb.length() > 0)
|
||||
sb.deleteCharAt(sb.lastIndexOf(","));
|
||||
return sb.toString();
|
||||
|
|
|
@ -19,10 +19,12 @@ package org.keycloak.credential;
|
|||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.webauthn4j.WebAuthnAuthenticationManager;
|
||||
import com.webauthn4j.converter.util.ObjectConverter;
|
||||
import com.webauthn4j.data.AuthenticatorTransport;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
|
||||
import org.keycloak.common.util.Base64;
|
||||
|
@ -105,7 +107,22 @@ public class WebAuthnCredentialProvider implements CredentialProvider<WebAuthnCr
|
|||
long counter = webAuthnModel.getCount();
|
||||
String attestationStatementFormat = webAuthnModel.getAttestationStatementFormat();
|
||||
|
||||
WebAuthnCredentialModel model = WebAuthnCredentialModel.create(getType(), userLabel, aaguid, credentialId, null, credentialPublicKey, counter, attestationStatementFormat);
|
||||
final Set<String> transports = webAuthnModel.getTransports()
|
||||
.stream()
|
||||
.map(AuthenticatorTransport::getValue)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
WebAuthnCredentialModel model = WebAuthnCredentialModel.create(
|
||||
getType(),
|
||||
userLabel,
|
||||
aaguid,
|
||||
credentialId,
|
||||
null,
|
||||
credentialPublicKey,
|
||||
counter,
|
||||
attestationStatementFormat,
|
||||
transports
|
||||
);
|
||||
|
||||
model.setId(webAuthnModel.getCredentialDBId());
|
||||
|
||||
|
|
|
@ -15,20 +15,26 @@
|
|||
*/
|
||||
package org.keycloak.forms.login.freemarker.model;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.webauthn4j.data.AuthenticatorTransport;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.keycloak.common.util.Base64Url;
|
||||
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;
|
||||
import org.keycloak.utils.StringUtil;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class WebAuthnAuthenticatorsBean {
|
||||
|
||||
private List<WebAuthnAuthenticatorBean> authenticators = new LinkedList<WebAuthnAuthenticatorBean>();
|
||||
private final List<WebAuthnAuthenticatorBean> authenticators;
|
||||
|
||||
public WebAuthnAuthenticatorsBean(KeycloakSession session, RealmModel realm, UserModel user, String credentialType) {
|
||||
// should consider multiple credentials in the future, but only single credential supported now.
|
||||
|
@ -38,7 +44,9 @@ public class WebAuthnAuthenticatorsBean {
|
|||
String credentialId = Base64Url.encodeBase64ToBase64Url(webAuthnCredential.getWebAuthnCredentialData().getCredentialId());
|
||||
String label = (webAuthnCredential.getUserLabel() == null || webAuthnCredential.getUserLabel().isEmpty()) ? "label missing" : webAuthnCredential.getUserLabel();
|
||||
String createdAt = DateTimeFormatterUtil.getDateTimeFromMillis(webAuthnCredential.getCreatedDate(), session.getContext().resolveLocale(user));
|
||||
return new WebAuthnAuthenticatorBean(credentialId, label, createdAt);
|
||||
final Set<String> transports = webAuthnCredential.getWebAuthnCredentialData().getTransports();
|
||||
|
||||
return new WebAuthnAuthenticatorBean(credentialId, label, createdAt, transports);
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
@ -47,14 +55,18 @@ public class WebAuthnAuthenticatorsBean {
|
|||
}
|
||||
|
||||
public static class WebAuthnAuthenticatorBean {
|
||||
public static final String DEFAULT_ICON = "kcWebAuthnDefaultIcon";
|
||||
|
||||
private final String credentialId;
|
||||
private final String label;
|
||||
private final String createdAt;
|
||||
private final TransportsBean transports;
|
||||
|
||||
public WebAuthnAuthenticatorBean(String credentialId, String label, String createdAt) {
|
||||
public WebAuthnAuthenticatorBean(String credentialId, String label, String createdAt, Set<String> transports) {
|
||||
this.credentialId = credentialId;
|
||||
this.label = label;
|
||||
this.createdAt = createdAt;
|
||||
this.transports = TransportsBean.convertFromSet(transports);
|
||||
}
|
||||
|
||||
public String getCredentialId() {
|
||||
|
@ -68,5 +80,118 @@ public class WebAuthnAuthenticatorsBean {
|
|||
public String getCreatedAt() {
|
||||
return this.createdAt;
|
||||
}
|
||||
|
||||
public TransportsBean getTransports() {
|
||||
return transports;
|
||||
}
|
||||
|
||||
public static class TransportsBean {
|
||||
private final Set<String> displayNameProperties;
|
||||
private final String iconClass;
|
||||
|
||||
public TransportsBean(Set<String> displayNameProperties, String iconClass) {
|
||||
this.displayNameProperties = displayNameProperties;
|
||||
this.iconClass = iconClass;
|
||||
}
|
||||
|
||||
public TransportsBean(String displayNameProperty, String iconClass) {
|
||||
this(Collections.singleton(displayNameProperty), iconClass);
|
||||
}
|
||||
|
||||
public TransportsBean(Transport transport) {
|
||||
this(transport.getDisplayNameProperty(), transport.getIconClass());
|
||||
}
|
||||
|
||||
public Set<String> getDisplayNameProperties() {
|
||||
return displayNameProperties;
|
||||
}
|
||||
|
||||
public String getIconClass() {
|
||||
return iconClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts set of available transport media to TransportsBean
|
||||
*
|
||||
* @param transports set of available transport media
|
||||
* @return TransportBean
|
||||
*/
|
||||
public static TransportsBean convertFromSet(Set<String> transports) {
|
||||
if (CollectionUtils.isEmpty(transports)) {
|
||||
return new TransportsBean(Transport.UNKNOWN);
|
||||
}
|
||||
|
||||
final Set<Transport> trans = transports.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(Transport::getByMapperName)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
if (trans.size() <= 1) {
|
||||
final Transport transport = trans.stream()
|
||||
.findFirst()
|
||||
.orElse(Transport.UNKNOWN);
|
||||
|
||||
return new TransportsBean(transport);
|
||||
} else {
|
||||
final Set<String> displayNameProperties = trans.stream()
|
||||
.map(Transport::getDisplayNameProperty)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
return new TransportsBean(displayNameProperties, DEFAULT_ICON);
|
||||
}
|
||||
}
|
||||
|
||||
protected enum Transport {
|
||||
USB("usb", AuthenticatorTransport.USB.getValue(), "kcWebAuthnUSB"),
|
||||
NFC("nfc", AuthenticatorTransport.NFC.getValue(), "kcWebAuthnNFC"),
|
||||
BLE("bluetooth", AuthenticatorTransport.BLE.getValue(), "kcWebAuthnBLE"),
|
||||
INTERNAL("internal", AuthenticatorTransport.INTERNAL.getValue(), "kcWebAuthnInternal"),
|
||||
UNKNOWN("unknown", null, DEFAULT_ICON);
|
||||
|
||||
private final String displayNameProperty;
|
||||
private final String mapperName;
|
||||
private final String iconClass;
|
||||
|
||||
/**
|
||||
* @param displayNameProperty Message property - defined in messages_xx.properties
|
||||
* @param mapperName used for mapping transport media name
|
||||
* @param iconClass icon class for particular transport media - defined in theme.properties
|
||||
*/
|
||||
Transport(String displayNameProperty, String mapperName, String iconClass) {
|
||||
this.displayNameProperty = displayNameProperty;
|
||||
this.mapperName = mapperName;
|
||||
this.iconClass = iconClass;
|
||||
}
|
||||
|
||||
public String getDisplayNameProperty() {
|
||||
return displayNameProperty;
|
||||
}
|
||||
|
||||
public String getMapperName() {
|
||||
return mapperName;
|
||||
}
|
||||
|
||||
public String getIconClass() {
|
||||
return iconClass;
|
||||
}
|
||||
|
||||
public static Transport getByDisplayNameProperty(String property) {
|
||||
return Arrays.stream(Transport.values())
|
||||
.filter(f -> f.getDisplayNameProperty().equals(property))
|
||||
.findFirst()
|
||||
.orElse(UNKNOWN);
|
||||
}
|
||||
|
||||
public static Transport getByMapperName(String mapperName) {
|
||||
if (StringUtil.isBlank(mapperName)) return UNKNOWN;
|
||||
|
||||
return Arrays.stream(Transport.values())
|
||||
.filter(f -> Objects.nonNull(f.getMapperName()))
|
||||
.filter(f -> f.getMapperName().equals(mapperName))
|
||||
.findFirst()
|
||||
.orElse(UNKNOWN);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,7 +47,8 @@ public class WebAuthnAuthenticatorsList {
|
|||
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));
|
||||
String transport = getTextFromElement(auth.findElement(By.id("kc-webauthn-authenticator-transport")));
|
||||
items.add(new WebAuthnAuthenticatorItem(name, createdAt, createdAtLabel, transport));
|
||||
}
|
||||
return items;
|
||||
} catch (NoSuchElementException e) {
|
||||
|
@ -78,11 +79,13 @@ public class WebAuthnAuthenticatorsList {
|
|||
private final String name;
|
||||
private final String createdAt;
|
||||
private final String createdAtLabel;
|
||||
private final String transport;
|
||||
|
||||
public WebAuthnAuthenticatorItem(String name, String createdAt, String createdAtLabel) {
|
||||
public WebAuthnAuthenticatorItem(String name, String createdAt, String createdAtLabel, String transport) {
|
||||
this.name = name;
|
||||
this.createdAt = createdAt;
|
||||
this.createdAtLabel = createdAtLabel;
|
||||
this.transport = transport;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
|
@ -96,5 +99,9 @@ public class WebAuthnAuthenticatorsList {
|
|||
public String getCreatedLabel() {
|
||||
return createdAtLabel;
|
||||
}
|
||||
|
||||
public String getTransport() {
|
||||
return transport;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright 2022 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.testsuite.webauthn;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.keycloak.testsuite.webauthn.pages.WebAuthnAuthenticatorsList;
|
||||
import org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.CoreMatchers.notNullValue;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.keycloak.testsuite.webauthn.authenticators.DefaultVirtualAuthOptions.DEFAULT_BLE;
|
||||
import static org.keycloak.testsuite.webauthn.authenticators.DefaultVirtualAuthOptions.DEFAULT_INTERNAL;
|
||||
import static org.keycloak.testsuite.webauthn.authenticators.DefaultVirtualAuthOptions.DEFAULT_NFC;
|
||||
import static org.keycloak.testsuite.webauthn.authenticators.DefaultVirtualAuthOptions.DEFAULT_USB;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
|
||||
*/
|
||||
public class WebAuthnTransportsTest extends AbstractWebAuthnVirtualTest {
|
||||
|
||||
@Test
|
||||
public void usbTransport() {
|
||||
assertTransport(DEFAULT_USB.getOptions(), "USB");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nfcTransport() {
|
||||
assertTransport(DEFAULT_NFC.getOptions(), "NFC");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bluetoothTransport() {
|
||||
assertTransport(DEFAULT_BLE.getOptions(), "Bluetooth");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void internalTransport() {
|
||||
assertTransport(DEFAULT_INTERNAL.getOptions(), "Internal");
|
||||
}
|
||||
|
||||
private void assertTransport(VirtualAuthenticatorOptions authenticator, String transportName) {
|
||||
getVirtualAuthManager().useAuthenticator(authenticator);
|
||||
registerDefaultUser();
|
||||
logout();
|
||||
|
||||
loginPage.open();
|
||||
loginPage.assertCurrent();
|
||||
loginPage.login(USERNAME, PASSWORD);
|
||||
|
||||
webAuthnLoginPage.assertCurrent();
|
||||
|
||||
WebAuthnAuthenticatorsList authenticatorsList = webAuthnLoginPage.getAuthenticators();
|
||||
assertThat(authenticatorsList, notNullValue());
|
||||
|
||||
List<WebAuthnAuthenticatorsList.WebAuthnAuthenticatorItem> items = authenticatorsList.getItems();
|
||||
assertThat(items, notNullValue());
|
||||
assertThat(items.size(), is(1));
|
||||
assertThat(items.get(0).getTransport(), is(transportName));
|
||||
}
|
||||
|
||||
}
|
|
@ -36,11 +36,13 @@ 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.updaters.RealmAttributeUpdater;
|
||||
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;
|
||||
import org.keycloak.testsuite.webauthn.authenticators.VirtualAuthenticatorManager;
|
||||
import org.keycloak.testsuite.webauthn.pages.WebAuthnLoginPage;
|
||||
import org.keycloak.testsuite.webauthn.pages.WebAuthnRegisterPage;
|
||||
import org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions;
|
||||
|
||||
|
@ -56,6 +58,9 @@ public abstract class AbstractWebAuthnAccountTest extends AbstractAuthTest imple
|
|||
@Page
|
||||
protected WebAuthnRegisterPage webAuthnRegisterPage;
|
||||
|
||||
@Page
|
||||
protected WebAuthnLoginPage webAuthnLoginPage;
|
||||
|
||||
private VirtualAuthenticatorManager webAuthnManager;
|
||||
protected SigningInPage.CredentialType webAuthnCredentialType;
|
||||
protected SigningInPage.CredentialType webAuthnPwdlessCredentialType;
|
||||
|
@ -187,4 +192,16 @@ public abstract class AbstractWebAuthnAccountTest extends AbstractAuthTest imple
|
|||
.defineAsBrowserFlow() // Activate this new flow
|
||||
);
|
||||
}
|
||||
|
||||
protected 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,6 @@ import org.keycloak.models.credential.WebAuthnCredentialModel;
|
|||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
|
||||
import org.keycloak.testsuite.ui.account2.page.SigningInPage;
|
||||
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
|
||||
import org.keycloak.testsuite.webauthn.pages.WebAuthnAuthenticatorsList;
|
||||
import org.keycloak.testsuite.webauthn.pages.WebAuthnLoginPage;
|
||||
import org.keycloak.theme.DateTimeFormatterUtil;
|
||||
|
@ -58,9 +57,6 @@ import static org.keycloak.testsuite.util.WaitUtils.waitForPageToLoad;
|
|||
|
||||
public class WebAuthnSigningInTest extends AbstractWebAuthnAccountTest {
|
||||
|
||||
@Page
|
||||
protected WebAuthnLoginPage webAuthnLoginPage;
|
||||
|
||||
@Test
|
||||
public void categoriesTest() {
|
||||
testContext.setTestRealmReps(emptyList()); // reimport realm after this test
|
||||
|
@ -451,16 +447,4 @@ public class WebAuthnSigningInTest extends AbstractWebAuthnAccountTest {
|
|||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Copyright 2022 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.testsuite.webauthn.account;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.testsuite.webauthn.pages.WebAuthnAuthenticatorsList;
|
||||
import org.keycloak.testsuite.webauthn.pages.WebAuthnLoginPage;
|
||||
import org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.CoreMatchers.notNullValue;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.keycloak.testsuite.webauthn.authenticators.DefaultVirtualAuthOptions.DEFAULT_BLE;
|
||||
import static org.keycloak.testsuite.webauthn.authenticators.DefaultVirtualAuthOptions.DEFAULT_INTERNAL;
|
||||
import static org.keycloak.testsuite.webauthn.authenticators.DefaultVirtualAuthOptions.DEFAULT_NFC;
|
||||
import static org.keycloak.testsuite.webauthn.authenticators.DefaultVirtualAuthOptions.DEFAULT_USB;
|
||||
|
||||
/**
|
||||
* Test for checking localization for authenticator transport media name
|
||||
*
|
||||
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
|
||||
*/
|
||||
public class WebAuthnTransportLocaleTest extends AbstractWebAuthnAccountTest {
|
||||
|
||||
@Test
|
||||
public void localizationTransportUSB() {
|
||||
assertLocalizationIndividual(DEFAULT_USB.getOptions(), "USB", "USB");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void localizationTransportNFC() {
|
||||
assertLocalizationIndividual(DEFAULT_NFC.getOptions(), "NFC", "NFC");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void localizationTransportBluetooth() {
|
||||
assertLocalizationIndividual(DEFAULT_BLE.getOptions(), "Bluetooth", "Bluetooth");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void localizationTransportInternal() {
|
||||
assertLocalizationIndividual(DEFAULT_INTERNAL.getOptions(), "Internal", "Interní");
|
||||
}
|
||||
|
||||
private void assertLocalizationIndividual(VirtualAuthenticatorOptions options, String originalName, String localizedText) {
|
||||
final Consumer<String> checkTransportName = (requiredName) -> {
|
||||
WebAuthnAuthenticatorsList authenticators = webAuthnLoginPage.getAuthenticators();
|
||||
assertThat(authenticators, notNullValue());
|
||||
assertThat(authenticators.getCount(), is(1));
|
||||
assertThat(authenticators.getLabels(), Matchers.contains("authenticator#1"));
|
||||
|
||||
List<WebAuthnAuthenticatorsList.WebAuthnAuthenticatorItem> items = authenticators.getItems();
|
||||
assertThat(items, notNullValue());
|
||||
assertThat(items.size(), is(1));
|
||||
|
||||
WebAuthnAuthenticatorsList.WebAuthnAuthenticatorItem item = items.get(0);
|
||||
assertThat(item, notNullValue());
|
||||
assertThat(item.getTransport(), is(requiredName));
|
||||
};
|
||||
|
||||
try (Closeable c = setLocalesUpdater(Locale.ENGLISH.getLanguage(), "cs").update()) {
|
||||
|
||||
getWebAuthnManager().useAuthenticator(options);
|
||||
addWebAuthnCredential("authenticator#1");
|
||||
|
||||
final int webAuthnCount = webAuthnCredentialType.getUserCredentialsCount();
|
||||
assertThat(webAuthnCount, is(1));
|
||||
|
||||
setUpWebAuthnFlow("webAuthnFlow");
|
||||
logout();
|
||||
|
||||
signingInPage.navigateTo();
|
||||
loginToAccount();
|
||||
|
||||
webAuthnLoginPage.assertCurrent();
|
||||
|
||||
checkTransportName.accept(originalName);
|
||||
|
||||
webAuthnLoginPage.openLanguage("Čeština");
|
||||
|
||||
checkTransportName.accept(localizedText);
|
||||
|
||||
webAuthnLoginPage.clickAuthenticate();
|
||||
signingInPage.assertCurrent();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Cannot update locale.", e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -46,6 +46,8 @@ codeErrorTitle=Kód chyby\: {0}
|
|||
displayUnsupported=Požadovaný typ zobrazení není podporovaný
|
||||
browserRequired=Pro přihlášení je vyžadován prohlížeč
|
||||
browserContinue=Pro dokončení přihlášení je vyžadován prohlížeč
|
||||
internal=Interní
|
||||
unknown=Neznámé
|
||||
|
||||
termsTitle=Smluvní podmínky
|
||||
termsTitleHtml=Smluvní podmínky
|
||||
|
|
|
@ -52,6 +52,12 @@ browserContinue=Browser required to complete login
|
|||
browserContinuePrompt=Open browser and continue login? [y/n]:
|
||||
browserContinueAnswer=y
|
||||
|
||||
# Transports
|
||||
usb=USB
|
||||
nfc=NFC
|
||||
bluetooth=Bluetooth
|
||||
internal=Internal
|
||||
unknown=Unknown
|
||||
|
||||
termsTitle=Terms and Conditions
|
||||
termsText=<p>Terms and conditions to be defined</p>
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<div class="${properties.kcSelectAuthListItemClass!}" onclick="fillAndSubmit('${authenticationSelection.authExecId}')">
|
||||
|
||||
<div class="${properties.kcSelectAuthListItemIconClass!}">
|
||||
<i class="${properties['${authenticationSelection.iconCssClass}']!authenticationSelection.iconCssClass} fa-2x"></i>
|
||||
<i class="${properties['${authenticationSelection.iconCssClass}']!authenticationSelection.iconCssClass} ${properties.kcSelectAuthListItemIconPropertyClass!}"></i>
|
||||
</div>
|
||||
<div class="${properties.kcSelectAuthListItemBodyClass!}">
|
||||
<div class="${properties.kcSelectAuthListItemHeadingClass!}">
|
||||
|
|
|
@ -25,26 +25,38 @@
|
|||
|
||||
<#if shouldDisplayAuthenticators?? && shouldDisplayAuthenticators>
|
||||
<#if authenticators.authenticators?size gt 1>
|
||||
<p class="${properties.kcSelectAuthListItemTitle!}">${kcSanitize(msg("webauthn-available-authenticators"))}</p>
|
||||
<p class="${properties.kcSelectAuthListItemTitle!}">${kcSanitize(msg("webauthn-available-authenticators"))?no_esc}</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>
|
||||
<i class="${(properties['${authenticator.transports.iconClass}'])!'${properties.kcWebAuthnDefaultIcon!}'} ${properties.kcSelectAuthListItemIconPropertyClass!}"></i>
|
||||
</div>
|
||||
<div class="${properties.kcSelectAuthListItemBodyClass!}">
|
||||
<div id="kc-webauthn-authenticator-label"
|
||||
class="${properties.kcSelectAuthListItemHeadingClass!}">
|
||||
${msg('${authenticator.label}')}
|
||||
${kcSanitize(msg('${authenticator.label}'))?no_esc}
|
||||
</div>
|
||||
|
||||
<#if authenticator.transports??>
|
||||
<div id="kc-webauthn-authenticator-transport"
|
||||
class="${properties.kcSelectAuthListItemDescriptionClass!}">
|
||||
<#list authenticator.transports.displayNameProperties as nameProperty>
|
||||
<span>${kcSanitize(msg('${nameProperty!}'))?no_esc}</span>
|
||||
<#if nameProperty?has_next>
|
||||
<span>, </span>
|
||||
</#if>
|
||||
</#list>
|
||||
</div>
|
||||
</#if>
|
||||
<div class="${properties.kcSelectAuthListItemDescriptionClass!}">
|
||||
<span id="kc-webauthn-authenticator-created-label">
|
||||
${msg('webauthn-createdAt-label')}
|
||||
${kcSanitize(msg('webauthn-createdAt-label'))?no_esc}
|
||||
</span>
|
||||
<span id="kc-webauthn-authenticator-created">
|
||||
${authenticator.createdAt}
|
||||
${kcSanitize(authenticator.createdAt)?no_esc}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
<input type="hidden" id="attestationObject" name="attestationObject"/>
|
||||
<input type="hidden" id="publicKeyCredentialId" name="publicKeyCredentialId"/>
|
||||
<input type="hidden" id="authenticatorLabel" name="authenticatorLabel"/>
|
||||
<input type="hidden" id="transports" name="transports"/>
|
||||
<input type="hidden" id="error" name="error"/>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -103,6 +104,12 @@
|
|||
$("#attestationObject").val(base64url.encode(new Uint8Array(attestationObject), {pad: false}));
|
||||
$("#publicKeyCredentialId").val(base64url.encode(new Uint8Array(publicKeyCredentialId), {pad: false}));
|
||||
|
||||
|
||||
let transports = result.response.getTransports();
|
||||
if (transports) {
|
||||
$("#transports").val(getTransportsAsString(transports));
|
||||
}
|
||||
|
||||
let initLabel = "WebAuthn Authenticator (Default Label)";
|
||||
let labelResult = window.prompt("Please input your registered authenticator's label", initLabel);
|
||||
if (labelResult === null) labelResult = initLabel;
|
||||
|
@ -150,6 +157,18 @@
|
|||
}
|
||||
return excludeCredentials;
|
||||
}
|
||||
|
||||
function getTransportsAsString(transportsList) {
|
||||
if (transportsList === '' || transportsList.constructor !== Array) return "";
|
||||
|
||||
let transportsString = "";
|
||||
|
||||
for (let i = 0; i < transportsList.length; i++) {
|
||||
transportsString += transportsList[i] + ",";
|
||||
}
|
||||
|
||||
return transportsString.slice(0, -1);
|
||||
}
|
||||
</script>
|
||||
|
||||
<input type="submit"
|
||||
|
|
|
@ -274,6 +274,19 @@ div.kc-logo-text span {
|
|||
color: var(--pf-global--Color--300);
|
||||
}
|
||||
|
||||
#kc-form-webauthn .select-auth-box-icon {
|
||||
flex: 0 0 3em;
|
||||
}
|
||||
|
||||
#kc-form-webauthn .select-auth-box-icon-properties {
|
||||
margin-top: 10px;
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
#kc-form-webauthn .pf-l-stack__item {
|
||||
margin: -1px 0;
|
||||
}
|
||||
|
||||
#kc-content-wrapper {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
|
|
@ -49,7 +49,14 @@ kcFeedbackSuccessIcon=fa fa-fw fa-check-circle
|
|||
kcFeedbackInfoIcon=fa fa-fw fa-info-circle
|
||||
|
||||
kcResetFlowIcon=pficon pficon-arrow fa
|
||||
|
||||
# WebAuthn icons
|
||||
kcWebAuthnKeyIcon=pficon pficon-key
|
||||
kcWebAuthnDefaultIcon=pficon pficon-key
|
||||
kcWebAuthnUSB=fa fa-usb
|
||||
kcWebAuthnNFC=fa fa-wifi
|
||||
kcWebAuthnBLE=fa fa-bluetooth-b
|
||||
kcWebAuthnInternal=pficon pficon-key
|
||||
|
||||
kcFormClass=form-horizontal
|
||||
kcFormGroupClass=form-group
|
||||
|
@ -100,6 +107,7 @@ kcSrOnlyClass=sr-only
|
|||
kcSelectAuthListClass=pf-l-stack select-auth-container
|
||||
kcSelectAuthListItemClass=pf-l-stack__item select-auth-box-parent pf-l-split
|
||||
kcSelectAuthListItemIconClass=pf-l-split__item select-auth-box-icon
|
||||
kcSelectAuthListItemIconPropertyClass=fa-2x select-auth-box-icon-properties
|
||||
kcSelectAuthListItemBodyClass=pf-l-split__item pf-l-stack
|
||||
kcSelectAuthListItemHeadingClass=pf-l-stack__item select-auth-box-headline pf-c-title
|
||||
kcSelectAuthListItemDescriptionClass=pf-l-stack__item select-auth-box-desc
|
||||
|
|
Loading…
Reference in a new issue