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.models.credential.dto.WebAuthnSecretData;
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @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,
|
public static WebAuthnCredentialModel create(String credentialType, String userLabel, String aaguid, String credentialId,
|
||||||
String attestationStatement, String credentialPublicKey, long counter, String attestationStatementFormat) {
|
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();
|
WebAuthnSecretData secretData = new WebAuthnSecretData();
|
||||||
|
|
||||||
WebAuthnCredentialModel credentialModel = new WebAuthnCredentialModel(credentialType, credentialData, secretData);
|
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.JsonCreator;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
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>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
*/
|
*/
|
||||||
|
@ -32,6 +36,7 @@ public class WebAuthnCredentialData {
|
||||||
private String attestationStatement;
|
private String attestationStatement;
|
||||||
private String credentialPublicKey;
|
private String credentialPublicKey;
|
||||||
private String attestationStatementFormat;
|
private String attestationStatementFormat;
|
||||||
|
private Set<String> transports;
|
||||||
|
|
||||||
@JsonCreator
|
@JsonCreator
|
||||||
public WebAuthnCredentialData(@JsonProperty("aaguid") String aaguid,
|
public WebAuthnCredentialData(@JsonProperty("aaguid") String aaguid,
|
||||||
|
@ -39,13 +44,15 @@ public class WebAuthnCredentialData {
|
||||||
@JsonProperty("counter") long counter,
|
@JsonProperty("counter") long counter,
|
||||||
@JsonProperty("attestationStatement") String attestationStatement,
|
@JsonProperty("attestationStatement") String attestationStatement,
|
||||||
@JsonProperty("credentialPublicKey") String credentialPublicKey,
|
@JsonProperty("credentialPublicKey") String credentialPublicKey,
|
||||||
@JsonProperty("attestationStatementFormat") String attestationStatementFormat) {
|
@JsonProperty("attestationStatementFormat") String attestationStatementFormat,
|
||||||
|
@JsonProperty("transports") Set<String> transports) {
|
||||||
this.aaguid = aaguid;
|
this.aaguid = aaguid;
|
||||||
this.credentialId = credentialId;
|
this.credentialId = credentialId;
|
||||||
this.counter = counter;
|
this.counter = counter;
|
||||||
this.attestationStatement = attestationStatement;
|
this.attestationStatement = attestationStatement;
|
||||||
this.credentialPublicKey = credentialPublicKey;
|
this.credentialPublicKey = credentialPublicKey;
|
||||||
this.attestationStatementFormat = attestationStatementFormat;
|
this.attestationStatementFormat = attestationStatementFormat;
|
||||||
|
this.transports = transports;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAaguid() {
|
public String getAaguid() {
|
||||||
|
@ -80,6 +87,14 @@ public class WebAuthnCredentialData {
|
||||||
this.attestationStatementFormat = attestationStatementFormat;
|
this.attestationStatementFormat = attestationStatementFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Set<String> getTransports() {
|
||||||
|
return transports != null ? transports : Collections.emptySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTransports(Set<String> transports) {
|
||||||
|
this.transports = transports;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "WebAuthnCredentialData { " +
|
return "WebAuthnCredentialData { " +
|
||||||
|
@ -90,6 +105,7 @@ public class WebAuthnCredentialData {
|
||||||
", attestationStatement='" + attestationStatement + '\'' +
|
", attestationStatement='" + attestationStatement + '\'' +
|
||||||
", credentialPublicKey='" + credentialPublicKey + '\'' +
|
", credentialPublicKey='" + credentialPublicKey + '\'' +
|
||||||
", attestationStatementFormat='" + attestationStatementFormat + '\'' +
|
", attestationStatementFormat='" + attestationStatementFormat + '\'' +
|
||||||
|
", transports=" + Arrays.toString(getTransports().toArray()) +
|
||||||
" }";
|
" }";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,8 @@ public interface WebAuthnConstants {
|
||||||
String ALLOWED_AUTHENTICATORS = "authenticators";
|
String ALLOWED_AUTHENTICATORS = "authenticators";
|
||||||
String IS_USER_IDENTIFIED = "isUserIdentified";
|
String IS_USER_IDENTIFIED = "isUserIdentified";
|
||||||
String USER_VERIFICATION = "userVerification";
|
String USER_VERIFICATION = "userVerification";
|
||||||
|
String TRANSPORTS = "transports";
|
||||||
|
|
||||||
String IS_SET_RETRY = "isSetRetry";
|
String IS_SET_RETRY = "isSetRetry";
|
||||||
String SHOULD_DISPLAY_AUTHENTICATORS = "shouldDisplayAuthenticators";
|
String SHOULD_DISPLAY_AUTHENTICATORS = "shouldDisplayAuthenticators";
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,9 @@ import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
|
@ -28,6 +30,8 @@ import javax.ws.rs.core.MultivaluedMap;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
import com.webauthn4j.WebAuthnRegistrationManager;
|
import com.webauthn4j.WebAuthnRegistrationManager;
|
||||||
|
import com.webauthn4j.data.AuthenticatorTransport;
|
||||||
|
import org.apache.commons.collections4.CollectionUtils;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.jboss.resteasy.spi.HttpRequest;
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
import org.keycloak.WebAuthnConstants;
|
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.certpath.CertPathTrustworthinessValidator;
|
||||||
import com.webauthn4j.validator.attestation.trustworthiness.self.DefaultSelfAttestationTrustworthinessValidator;
|
import com.webauthn4j.validator.attestation.trustworthiness.self.DefaultSelfAttestationTrustworthinessValidator;
|
||||||
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
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_DETAIL_LABEL;
|
||||||
import static org.keycloak.WebAuthnConstants.REG_ERR_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
|
// check User Verification by considering a malicious user might modify the result of calling WebAuthn API
|
||||||
boolean isUserVerificationRequired = policy.getUserVerificationRequirement().equals(WebAuthnConstants.OPTION_REQUIRED);
|
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);
|
RegistrationParameters registrationParameters = new RegistrationParameters(serverProperty, isUserVerificationRequired);
|
||||||
|
|
||||||
WebAuthnRegistrationManager webAuthnRegistrationManager = createWebAuthnRegistrationManager();
|
WebAuthnRegistrationManager webAuthnRegistrationManager = createWebAuthnRegistrationManager();
|
||||||
|
@ -224,6 +239,7 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
|
||||||
credential.setAttestedCredentialData(registrationData.getAttestationObject().getAuthenticatorData().getAttestedCredentialData());
|
credential.setAttestedCredentialData(registrationData.getAttestationObject().getAuthenticatorData().getAttestedCredentialData());
|
||||||
credential.setCount(registrationData.getAttestationObject().getAuthenticatorData().getSignCount());
|
credential.setCount(registrationData.getAttestationObject().getAuthenticatorData().getSignCount());
|
||||||
credential.setAttestationStatementFormat(registrationData.getAttestationObject().getFormat());
|
credential.setAttestationStatementFormat(registrationData.getAttestationObject().getFormat());
|
||||||
|
credential.setTransports(registrationData.getTransports());
|
||||||
|
|
||||||
// Save new webAuthn credential
|
// Save new webAuthn credential
|
||||||
WebAuthnCredentialProvider webAuthnCredProvider = (WebAuthnCredentialProvider) this.session.getProvider(CredentialProvider.class, getCredentialProviderId());
|
WebAuthnCredentialProvider webAuthnCredProvider = (WebAuthnCredentialProvider) this.session.getProvider(CredentialProvider.class, getCredentialProviderId());
|
||||||
|
@ -305,9 +321,17 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
|
||||||
private void showInfoAfterWebAuthnApiCreate(RegistrationData response) {
|
private void showInfoAfterWebAuthnApiCreate(RegistrationData response) {
|
||||||
AttestedCredentialData attestedCredentialData = response.getAttestationObject().getAuthenticatorData().getAttestedCredentialData();
|
AttestedCredentialData attestedCredentialData = response.getAttestationObject().getAuthenticatorData().getAttestedCredentialData();
|
||||||
AttestationStatement attestationStatement = response.getAttestationObject().getAttestationStatement();
|
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("createad key's algorithm = {0}", String.valueOf(attestedCredentialData.getCOSEKey().getAlgorithm().getValue()));
|
||||||
logger.debugv("aaguid = {0}", attestedCredentialData.getAaguid().toString());
|
logger.debugv("aaguid = {0}", attestedCredentialData.getAaguid().toString());
|
||||||
logger.debugv("attestation format = {0}", attestationStatement.getFormat());
|
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 {
|
private void checkAcceptedAuthenticator(RegistrationData response, WebAuthnPolicy policy) throws Exception {
|
||||||
|
|
|
@ -16,14 +16,19 @@
|
||||||
|
|
||||||
package org.keycloak.credential;
|
package org.keycloak.credential;
|
||||||
|
|
||||||
|
import org.apache.commons.collections4.CollectionUtils;
|
||||||
import org.keycloak.common.util.Base64;
|
import org.keycloak.common.util.Base64;
|
||||||
|
|
||||||
import com.webauthn4j.data.AuthenticationParameters;
|
import com.webauthn4j.data.AuthenticationParameters;
|
||||||
import com.webauthn4j.data.AuthenticationRequest;
|
import com.webauthn4j.data.AuthenticationRequest;
|
||||||
|
import com.webauthn4j.data.AuthenticatorTransport;
|
||||||
import com.webauthn4j.data.attestation.authenticator.AttestedCredentialData;
|
import com.webauthn4j.data.attestation.authenticator.AttestedCredentialData;
|
||||||
import com.webauthn4j.data.attestation.authenticator.COSEKey;
|
import com.webauthn4j.data.attestation.authenticator.COSEKey;
|
||||||
import com.webauthn4j.data.attestation.statement.AttestationStatement;
|
import com.webauthn4j.data.attestation.statement.AttestationStatement;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class WebAuthnCredentialModelInput implements CredentialInput {
|
public class WebAuthnCredentialModelInput implements CredentialInput {
|
||||||
|
|
||||||
private AttestedCredentialData attestedCredentialData;
|
private AttestedCredentialData attestedCredentialData;
|
||||||
|
@ -34,6 +39,7 @@ public class WebAuthnCredentialModelInput implements CredentialInput {
|
||||||
private String credentialDBId;
|
private String credentialDBId;
|
||||||
private final String credentialType;
|
private final String credentialType;
|
||||||
private String attestationStatementFormat;
|
private String attestationStatementFormat;
|
||||||
|
private Set<AuthenticatorTransport> transports;
|
||||||
|
|
||||||
public WebAuthnCredentialModelInput(String credentialType) {
|
public WebAuthnCredentialModelInput(String credentialType) {
|
||||||
this.credentialType = credentialType;
|
this.credentialType = credentialType;
|
||||||
|
@ -115,6 +121,14 @@ public class WebAuthnCredentialModelInput implements CredentialInput {
|
||||||
this.attestationStatementFormat = attestationStatementFormat;
|
this.attestationStatementFormat = attestationStatementFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Set<AuthenticatorTransport> getTransports() {
|
||||||
|
return transports;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTransports(Set<AuthenticatorTransport> transports) {
|
||||||
|
this.transports = transports;
|
||||||
|
}
|
||||||
|
|
||||||
public String toString() {
|
public String toString() {
|
||||||
StringBuilder sb = new StringBuilder("Credential Type = " + credentialType + ",");
|
StringBuilder sb = new StringBuilder("Credential Type = " + credentialType + ",");
|
||||||
if (credentialDBId != null)
|
if (credentialDBId != null)
|
||||||
|
@ -156,6 +170,15 @@ public class WebAuthnCredentialModelInput implements CredentialInput {
|
||||||
.append(Base64.encodeBytes(authenticationRequest.getCredentialId()))
|
.append(Base64.encodeBytes(authenticationRequest.getCredentialId()))
|
||||||
.append(",");
|
.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)
|
if (sb.length() > 0)
|
||||||
sb.deleteCharAt(sb.lastIndexOf(","));
|
sb.deleteCharAt(sb.lastIndexOf(","));
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
|
|
|
@ -19,10 +19,12 @@ package org.keycloak.credential;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import com.webauthn4j.WebAuthnAuthenticationManager;
|
import com.webauthn4j.WebAuthnAuthenticationManager;
|
||||||
import com.webauthn4j.converter.util.ObjectConverter;
|
import com.webauthn4j.converter.util.ObjectConverter;
|
||||||
|
import com.webauthn4j.data.AuthenticatorTransport;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
|
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
|
||||||
import org.keycloak.common.util.Base64;
|
import org.keycloak.common.util.Base64;
|
||||||
|
@ -105,7 +107,22 @@ public class WebAuthnCredentialProvider implements CredentialProvider<WebAuthnCr
|
||||||
long counter = webAuthnModel.getCount();
|
long counter = webAuthnModel.getCount();
|
||||||
String attestationStatementFormat = webAuthnModel.getAttestationStatementFormat();
|
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());
|
model.setId(webAuthnModel.getCredentialDBId());
|
||||||
|
|
||||||
|
|
|
@ -15,20 +15,26 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.forms.login.freemarker.model;
|
package org.keycloak.forms.login.freemarker.model;
|
||||||
|
|
||||||
import java.util.LinkedList;
|
import com.webauthn4j.data.AuthenticatorTransport;
|
||||||
import java.util.List;
|
import org.apache.commons.collections4.CollectionUtils;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.keycloak.common.util.Base64Url;
|
import org.keycloak.common.util.Base64Url;
|
||||||
import org.keycloak.models.KeycloakSession;
|
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;
|
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 {
|
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) {
|
public WebAuthnAuthenticatorsBean(KeycloakSession session, RealmModel realm, UserModel user, String credentialType) {
|
||||||
// should consider multiple credentials in the future, but only single credential supported now.
|
// 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 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();
|
||||||
String createdAt = DateTimeFormatterUtil.getDateTimeFromMillis(webAuthnCredential.getCreatedDate(), session.getContext().resolveLocale(user));
|
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());
|
}).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,14 +55,18 @@ public class WebAuthnAuthenticatorsBean {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class WebAuthnAuthenticatorBean {
|
public static class WebAuthnAuthenticatorBean {
|
||||||
|
public static final String DEFAULT_ICON = "kcWebAuthnDefaultIcon";
|
||||||
|
|
||||||
private final String credentialId;
|
private final String credentialId;
|
||||||
private final String label;
|
private final String label;
|
||||||
private final String createdAt;
|
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.credentialId = credentialId;
|
||||||
this.label = label;
|
this.label = label;
|
||||||
this.createdAt = createdAt;
|
this.createdAt = createdAt;
|
||||||
|
this.transports = TransportsBean.convertFromSet(transports);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getCredentialId() {
|
public String getCredentialId() {
|
||||||
|
@ -68,5 +80,118 @@ public class WebAuthnAuthenticatorsBean {
|
||||||
public String getCreatedAt() {
|
public String getCreatedAt() {
|
||||||
return this.createdAt;
|
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 name = getTextFromElement(auth.findElement(By.id("kc-webauthn-authenticator-label")));
|
||||||
String createdAt = getTextFromElement(auth.findElement(By.id("kc-webauthn-authenticator-created")));
|
String createdAt = getTextFromElement(auth.findElement(By.id("kc-webauthn-authenticator-created")));
|
||||||
String createdAtLabel = getTextFromElement(auth.findElement(By.id("kc-webauthn-authenticator-created-label")));
|
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;
|
return items;
|
||||||
} catch (NoSuchElementException e) {
|
} catch (NoSuchElementException e) {
|
||||||
|
@ -78,11 +79,13 @@ public class WebAuthnAuthenticatorsList {
|
||||||
private final String name;
|
private final String name;
|
||||||
private final String createdAt;
|
private final String createdAt;
|
||||||
private final String createdAtLabel;
|
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.name = name;
|
||||||
this.createdAt = createdAt;
|
this.createdAt = createdAt;
|
||||||
this.createdAtLabel = createdAtLabel;
|
this.createdAtLabel = createdAtLabel;
|
||||||
|
this.transport = transport;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
|
@ -96,5 +99,9 @@ public class WebAuthnAuthenticatorsList {
|
||||||
public String getCreatedLabel() {
|
public String getCreatedLabel() {
|
||||||
return createdAtLabel;
|
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.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.updaters.RealmAttributeUpdater;
|
||||||
import org.keycloak.testsuite.util.FlowUtil;
|
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;
|
||||||
import org.keycloak.testsuite.webauthn.authenticators.VirtualAuthenticatorManager;
|
import org.keycloak.testsuite.webauthn.authenticators.VirtualAuthenticatorManager;
|
||||||
|
import org.keycloak.testsuite.webauthn.pages.WebAuthnLoginPage;
|
||||||
import org.keycloak.testsuite.webauthn.pages.WebAuthnRegisterPage;
|
import org.keycloak.testsuite.webauthn.pages.WebAuthnRegisterPage;
|
||||||
import org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions;
|
import org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions;
|
||||||
|
|
||||||
|
@ -56,6 +58,9 @@ public abstract class AbstractWebAuthnAccountTest extends AbstractAuthTest imple
|
||||||
@Page
|
@Page
|
||||||
protected WebAuthnRegisterPage webAuthnRegisterPage;
|
protected WebAuthnRegisterPage webAuthnRegisterPage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected WebAuthnLoginPage webAuthnLoginPage;
|
||||||
|
|
||||||
private VirtualAuthenticatorManager webAuthnManager;
|
private VirtualAuthenticatorManager webAuthnManager;
|
||||||
protected SigningInPage.CredentialType webAuthnCredentialType;
|
protected SigningInPage.CredentialType webAuthnCredentialType;
|
||||||
protected SigningInPage.CredentialType webAuthnPwdlessCredentialType;
|
protected SigningInPage.CredentialType webAuthnPwdlessCredentialType;
|
||||||
|
@ -187,4 +192,16 @@ public abstract class AbstractWebAuthnAccountTest extends AbstractAuthTest imple
|
||||||
.defineAsBrowserFlow() // Activate this new flow
|
.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.CredentialRepresentation;
|
||||||
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
|
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
|
||||||
import org.keycloak.testsuite.ui.account2.page.SigningInPage;
|
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.WebAuthnAuthenticatorsList;
|
||||||
import org.keycloak.testsuite.webauthn.pages.WebAuthnLoginPage;
|
import org.keycloak.testsuite.webauthn.pages.WebAuthnLoginPage;
|
||||||
import org.keycloak.theme.DateTimeFormatterUtil;
|
import org.keycloak.theme.DateTimeFormatterUtil;
|
||||||
|
@ -58,9 +57,6 @@ import static org.keycloak.testsuite.util.WaitUtils.waitForPageToLoad;
|
||||||
|
|
||||||
public class WebAuthnSigningInTest extends AbstractWebAuthnAccountTest {
|
public class WebAuthnSigningInTest extends AbstractWebAuthnAccountTest {
|
||||||
|
|
||||||
@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
|
||||||
|
@ -451,16 +447,4 @@ public class WebAuthnSigningInTest extends AbstractWebAuthnAccountTest {
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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ý
|
displayUnsupported=Požadovaný typ zobrazení není podporovaný
|
||||||
browserRequired=Pro přihlášení je vyžadován prohlížeč
|
browserRequired=Pro přihlášení je vyžadován prohlížeč
|
||||||
browserContinue=Pro dokončení 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
|
termsTitle=Smluvní podmínky
|
||||||
termsTitleHtml=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]:
|
browserContinuePrompt=Open browser and continue login? [y/n]:
|
||||||
browserContinueAnswer=y
|
browserContinueAnswer=y
|
||||||
|
|
||||||
|
# Transports
|
||||||
|
usb=USB
|
||||||
|
nfc=NFC
|
||||||
|
bluetooth=Bluetooth
|
||||||
|
internal=Internal
|
||||||
|
unknown=Unknown
|
||||||
|
|
||||||
termsTitle=Terms and Conditions
|
termsTitle=Terms and Conditions
|
||||||
termsText=<p>Terms and conditions to be defined</p>
|
termsText=<p>Terms and conditions to be defined</p>
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
<div class="${properties.kcSelectAuthListItemClass!}" onclick="fillAndSubmit('${authenticationSelection.authExecId}')">
|
<div class="${properties.kcSelectAuthListItemClass!}" onclick="fillAndSubmit('${authenticationSelection.authExecId}')">
|
||||||
|
|
||||||
<div class="${properties.kcSelectAuthListItemIconClass!}">
|
<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>
|
||||||
<div class="${properties.kcSelectAuthListItemBodyClass!}">
|
<div class="${properties.kcSelectAuthListItemBodyClass!}">
|
||||||
<div class="${properties.kcSelectAuthListItemHeadingClass!}">
|
<div class="${properties.kcSelectAuthListItemHeadingClass!}">
|
||||||
|
|
|
@ -25,26 +25,38 @@
|
||||||
|
|
||||||
<#if shouldDisplayAuthenticators?? && shouldDisplayAuthenticators>
|
<#if shouldDisplayAuthenticators?? && shouldDisplayAuthenticators>
|
||||||
<#if authenticators.authenticators?size gt 1>
|
<#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>
|
</#if>
|
||||||
|
|
||||||
<div class="${properties.kcFormClass!}">
|
<div class="${properties.kcFormClass!}">
|
||||||
<#list authenticators.authenticators as authenticator>
|
<#list authenticators.authenticators as authenticator>
|
||||||
<div id="kc-webauthn-authenticator" class="${properties.kcSelectAuthListItemClass!}">
|
<div id="kc-webauthn-authenticator" class="${properties.kcSelectAuthListItemClass!}">
|
||||||
<div class="${properties.kcSelectAuthListItemIconClass!}">
|
<div class="${properties.kcSelectAuthListItemIconClass!}">
|
||||||
<i class="${properties.kcWebAuthnKeyIcon} fa-2x"></i>
|
<i class="${(properties['${authenticator.transports.iconClass}'])!'${properties.kcWebAuthnDefaultIcon!}'} ${properties.kcSelectAuthListItemIconPropertyClass!}"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="${properties.kcSelectAuthListItemBodyClass!}">
|
<div class="${properties.kcSelectAuthListItemBodyClass!}">
|
||||||
<div id="kc-webauthn-authenticator-label"
|
<div id="kc-webauthn-authenticator-label"
|
||||||
class="${properties.kcSelectAuthListItemHeadingClass!}">
|
class="${properties.kcSelectAuthListItemHeadingClass!}">
|
||||||
${msg('${authenticator.label}')}
|
${kcSanitize(msg('${authenticator.label}'))?no_esc}
|
||||||
</div>
|
</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!}">
|
<div class="${properties.kcSelectAuthListItemDescriptionClass!}">
|
||||||
<span id="kc-webauthn-authenticator-created-label">
|
<span id="kc-webauthn-authenticator-created-label">
|
||||||
${msg('webauthn-createdAt-label')}
|
${kcSanitize(msg('webauthn-createdAt-label'))?no_esc}
|
||||||
</span>
|
</span>
|
||||||
<span id="kc-webauthn-authenticator-created">
|
<span id="kc-webauthn-authenticator-created">
|
||||||
${authenticator.createdAt}
|
${kcSanitize(authenticator.createdAt)?no_esc}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
<input type="hidden" id="attestationObject" name="attestationObject"/>
|
<input type="hidden" id="attestationObject" name="attestationObject"/>
|
||||||
<input type="hidden" id="publicKeyCredentialId" name="publicKeyCredentialId"/>
|
<input type="hidden" id="publicKeyCredentialId" name="publicKeyCredentialId"/>
|
||||||
<input type="hidden" id="authenticatorLabel" name="authenticatorLabel"/>
|
<input type="hidden" id="authenticatorLabel" name="authenticatorLabel"/>
|
||||||
|
<input type="hidden" id="transports" name="transports"/>
|
||||||
<input type="hidden" id="error" name="error"/>
|
<input type="hidden" id="error" name="error"/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -103,6 +104,12 @@
|
||||||
$("#attestationObject").val(base64url.encode(new Uint8Array(attestationObject), {pad: false}));
|
$("#attestationObject").val(base64url.encode(new Uint8Array(attestationObject), {pad: false}));
|
||||||
$("#publicKeyCredentialId").val(base64url.encode(new Uint8Array(publicKeyCredentialId), {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 initLabel = "WebAuthn Authenticator (Default Label)";
|
||||||
let labelResult = window.prompt("Please input your registered authenticator's label", initLabel);
|
let labelResult = window.prompt("Please input your registered authenticator's label", initLabel);
|
||||||
if (labelResult === null) labelResult = initLabel;
|
if (labelResult === null) labelResult = initLabel;
|
||||||
|
@ -150,6 +157,18 @@
|
||||||
}
|
}
|
||||||
return excludeCredentials;
|
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>
|
</script>
|
||||||
|
|
||||||
<input type="submit"
|
<input type="submit"
|
||||||
|
|
|
@ -274,6 +274,19 @@ div.kc-logo-text span {
|
||||||
color: var(--pf-global--Color--300);
|
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 {
|
#kc-content-wrapper {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,14 @@ kcFeedbackSuccessIcon=fa fa-fw fa-check-circle
|
||||||
kcFeedbackInfoIcon=fa fa-fw fa-info-circle
|
kcFeedbackInfoIcon=fa fa-fw fa-info-circle
|
||||||
|
|
||||||
kcResetFlowIcon=pficon pficon-arrow fa
|
kcResetFlowIcon=pficon pficon-arrow fa
|
||||||
|
|
||||||
|
# WebAuthn icons
|
||||||
kcWebAuthnKeyIcon=pficon pficon-key
|
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
|
kcFormClass=form-horizontal
|
||||||
kcFormGroupClass=form-group
|
kcFormGroupClass=form-group
|
||||||
|
@ -100,6 +107,7 @@ kcSrOnlyClass=sr-only
|
||||||
kcSelectAuthListClass=pf-l-stack select-auth-container
|
kcSelectAuthListClass=pf-l-stack select-auth-container
|
||||||
kcSelectAuthListItemClass=pf-l-stack__item select-auth-box-parent pf-l-split
|
kcSelectAuthListItemClass=pf-l-stack__item select-auth-box-parent pf-l-split
|
||||||
kcSelectAuthListItemIconClass=pf-l-split__item select-auth-box-icon
|
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
|
kcSelectAuthListItemBodyClass=pf-l-split__item pf-l-stack
|
||||||
kcSelectAuthListItemHeadingClass=pf-l-stack__item select-auth-box-headline pf-c-title
|
kcSelectAuthListItemHeadingClass=pf-l-stack__item select-auth-box-headline pf-c-title
|
||||||
kcSelectAuthListItemDescriptionClass=pf-l-stack__item select-auth-box-desc
|
kcSelectAuthListItemDescriptionClass=pf-l-stack__item select-auth-box-desc
|
||||||
|
|
Loading…
Reference in a new issue