/*
* Enterprise JSF project.
*
* Copyright 2023 e-Contract.be BV. All rights reserved.
* e-Contract.be BV proprietary/confidential. Use is subject to license terms.
*/
package be.e_contract.demo;
import be.e_contract.ejsf.component.webauthn.WebAuthnAuthenticatedEvent;
import be.e_contract.ejsf.component.webauthn.WebAuthnAuthenticationError;
import be.e_contract.ejsf.component.webauthn.WebAuthnErrorEvent;
import be.e_contract.ejsf.component.webauthn.WebAuthnRegisteredEvent;
import be.e_contract.ejsf.component.webauthn.WebAuthnRegistrationError;
import com.yubico.fido.metadata.AAGUID;
import com.yubico.fido.metadata.FidoMetadataDownloader;
import com.yubico.fido.metadata.FidoMetadataService;
import com.yubico.fido.metadata.MetadataBLOB;
import com.yubico.webauthn.AssertionResult;
import com.yubico.webauthn.RegisteredCredential;
import com.yubico.webauthn.attestation.AttestationTrustSource;
import com.yubico.webauthn.data.AttestationObject;
import com.yubico.webauthn.data.AttestedCredentialData;
import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
import com.yubico.webauthn.data.AuthenticatorData;
import com.yubico.webauthn.data.AuthenticatorTransport;
import com.yubico.webauthn.data.ByteArray;
import com.yubico.webauthn.data.UserIdentity;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.Optional;
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
import javax.faces.view.ViewScoped;
import javax.inject.Inject;
import javax.inject.Named;
import java.util.Set;
import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Produces;
import javax.faces.context.ExternalContext;
import org.primefaces.PrimeFaces;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Named("webAuthnController")
@ViewScoped
public class WebAuthnController implements Serializable {
private static final Logger LOGGER = LoggerFactory.getLogger(WebAuthnController.class);
@Inject
private WebAuthnCredentialRepository credentialRepository;
@Inject
private AAGUIDRepository aaguidRepository;
private String username;
private String userVerification;
private String authenticatorAttachment;
private String residentKey;
private String attestationConveyance;
private boolean allowUntrustedAttestation;
public String getUsername() {
return this.username;
}
public void setUsername(String username) {
this.username = username;
}
public String getUserVerification() {
return this.userVerification;
}
public void setUserVerification(String userVerification) {
this.userVerification = userVerification;
}
public String getAuthenticatorAttachment() {
return this.authenticatorAttachment;
}
public void setAuthenticatorAttachment(String authenticatorAttachment) {
this.authenticatorAttachment = authenticatorAttachment;
}
public String getResidentKey() {
return this.residentKey;
}
public void setResidentKey(String residentKey) {
this.residentKey = residentKey;
}
public String getAttestationConveyance() {
return this.attestationConveyance;
}
public void setAttestationConveyance(String attestationConveyance) {
this.attestationConveyance = attestationConveyance;
}
public boolean isAllowUntrustedAttestation() {
return this.allowUntrustedAttestation;
}
public void setAllowUntrustedAttestation(boolean allowUntrustedAttestation) {
this.allowUntrustedAttestation = allowUntrustedAttestation;
}
@PostConstruct
public void postConstruct() {
this.allowUntrustedAttestation = true;
this.userVerification = "preferred";
this.authenticatorAttachment = "cross-platform";
this.residentKey = "preferred";
this.attestationConveyance = "direct";
}
public void registeredListener(WebAuthnRegisteredEvent event) {
RegisteredCredential registeredCredential = event.getRegisteredCredential();
String username = event.getUsername();
Set<AuthenticatorTransport> authenticatorTransports = event.getAuthenticatorTransports();
UserIdentity userIdentity = event.getUserIdentity();
FacesContext facesContext = FacesContext.getCurrentInstance();
facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO,
"Registered: " + username, null));
facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO,
"User ID: " + userIdentity.getId().getHex(), null));
facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO,
"Credential ID: " + registeredCredential.getCredentialId().getHex(), null));
AuthenticatorAttestationResponse authenticatorAttestationResponse = event.getAuthenticatorAttestationResponse();
AttestationObject attestationObject = authenticatorAttestationResponse.getAttestation();
AuthenticatorData authenticatorData = attestationObject.getAuthenticatorData();
Optional<AttestedCredentialData> attestedCredentialDataOptional = authenticatorData.getAttestedCredentialData();
if (attestedCredentialDataOptional.isPresent()) {
AttestedCredentialData attestedCredentialData = attestedCredentialDataOptional.get();
ByteArray aaguidByteArray = attestedCredentialData.getAaguid();
AAGUID aaguid = new AAGUID(aaguidByteArray);
facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO,
"AAGUID: " + aaguid.asGuidString(), null));
String authenticatorName = this.aaguidRepository.findName(aaguid.asGuidString());
if (null != authenticatorName) {
facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO,
"Authenticator: " + authenticatorName, null));
}
}
if (null != event.getResidentKey()) {
facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO,
"Resident key: " + event.getResidentKey(), null));
}
facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO,
"Attestation trusted: " + event.isAttestationTrusted(), null));
if (null != event.getAttestationCertificate()) {
X509Certificate attestationCertificate = event.getAttestationCertificate();
facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO,
"Attestation certificate: " + attestationCertificate.getSubjectX500Principal(), null));
}
if (null != event.getPrf()) {
facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO,
"PRF enabled: " + event.getPrf(), null));
} else {
facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO,
"PRF not supported", null));
}
this.credentialRepository.addRegistration(username, registeredCredential, authenticatorTransports, userIdentity);
}
public void authenticatedListener(WebAuthnAuthenticatedEvent event) {
AssertionResult assertionResult = event.getAssertionResult();
FacesContext facesContext = FacesContext.getCurrentInstance();
facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO,
"Authenticated: " + assertionResult.getUsername(), null));
facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO,
"User ID: " + assertionResult.getCredential().getUserHandle().getHex(), null));
facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO,
"Credential ID: " + assertionResult.getCredential().getCredentialId().getHex(), null));
ByteArray prf = event.getPrf();
if (null != prf) {
facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO,
"PRF: " + prf.getHex(), null));
}
this.credentialRepository.updateSignatureCount(assertionResult);
}
public void errorListener(WebAuthnErrorEvent event) {
String errorMessage = event.getMessage();
FacesContext facesContext = FacesContext.getCurrentInstance();
String message = "WebAuthn error: " + errorMessage;
FacesMessage facesMessage = new FacesMessage(FacesMessage.SEVERITY_ERROR, message, null);
facesContext.addMessage(null, facesMessage);
}
public void registrationErrorListener(WebAuthnRegistrationError error) {
String message = "WebAuthn error: " + error.getErrorMessage();
FacesMessage facesMessage = new FacesMessage(FacesMessage.SEVERITY_ERROR, message, null);
FacesContext facesContext = FacesContext.getCurrentInstance();
facesContext.addMessage(null, facesMessage);
PrimeFaces primeFaces = PrimeFaces.current();
primeFaces.ajax().update(":messages");
}
public void authenticationErrorListener(WebAuthnAuthenticationError error) {
String message = "WebAuthn error: " + error.getErrorMessage();
FacesMessage facesMessage = new FacesMessage(FacesMessage.SEVERITY_ERROR, message, null);
FacesContext facesContext = FacesContext.getCurrentInstance();
facesContext.addMessage(null, facesMessage);
PrimeFaces primeFaces = PrimeFaces.current();
primeFaces.ajax().update(":messages");
}
public ByteArray prfListener(ByteArray credentialId) {
LOGGER.debug("PRF listener: {}", credentialId.getHex());
// of course you don't do this in production
// just want to verify a deterministic PRF result here
MessageDigest messageDigest;
try {
messageDigest = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException ex) {
LOGGER.error("algo error: " + ex.getMessage(), ex);
return credentialId;
}
// make sure we return at least 32 bytes
byte[] salt = messageDigest.digest(credentialId.getBytes());
return new ByteArray(salt);
}
public String getRelyingPartyId() {
FacesContext facesContext = FacesContext.getCurrentInstance();
ExternalContext externalContext = facesContext.getExternalContext();
String requestServerName = externalContext.getRequestServerName();
// production configuration should always return the same value
if ("localhost".equals(requestServerName)) {
return "localhost";
}
return "demo.e-contract.be";
}
public byte[] getUserId() {
LOGGER.debug("getUserId");
byte[] userId = new byte[32];
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(userId);
return userId;
}
@Produces
@ApplicationScoped
@Named("webAuthnAttestationTrustSource")
public AttestationTrustSource createAttestationTrustSource() {
LOGGER.debug("creating AttestationTrustSource");
System.setProperty("com.sun.security.enableCRLDP", "true");
File trustRootCacheFile;
File blobCacheFile;
try {
trustRootCacheFile = File.createTempFile("fido-mds-trust-root-", ".bin");
blobCacheFile = File.createTempFile("fido-mds-blob-", ".bin");
} catch (IOException ex) {
LOGGER.error("I/O error: " + ex.getMessage(), ex);
return null;
}
FidoMetadataDownloader downloader = FidoMetadataDownloader.builder()
.expectLegalHeader("Retrieval and use of this BLOB indicates acceptance of the appropriate agreement located at https://fidoalliance.org/metadata/metadata-legal-terms/")
.useDefaultTrustRoot()
.useTrustRootCacheFile(trustRootCacheFile)
.useDefaultBlob()
.useBlobCacheFile(blobCacheFile)
.verifyDownloadsOnly(true)
.build();
FidoMetadataService mds;
try {
MetadataBLOB metadataBlob = downloader.loadCachedBlob();
mds = FidoMetadataService.builder()
.useBlob(metadataBlob)
.build();
} catch (Exception ex) {
LOGGER.error("error: " + ex.getMessage(), ex);
return null;
}
LOGGER.debug("trust root cache file: {}", trustRootCacheFile.getAbsolutePath());
LOGGER.debug("blob cache file: {}", blobCacheFile.getAbsolutePath());
return mds;
}
public String registrationMessageInterceptor(String request, String response) {
LOGGER.debug("registration request: {}", request);
LOGGER.debug("registration response: {}", response);
return response;
}
public String authenticationMessageInterceptor(String request, String response) {
LOGGER.debug("authentication request: {}", request);
LOGGER.debug("authentication response: {}", response);
return response;
}
}
/*
* Enterprise JSF project.
*
* Copyright 2023 e-Contract.be BV. All rights reserved.
* e-Contract.be BV proprietary/confidential. Use is subject to license terms.
*/
package be.e_contract.demo;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.Base64Variants;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.yubico.webauthn.AssertionResult;
import com.yubico.webauthn.CredentialRepository;
import com.yubico.webauthn.RegisteredCredential;
import com.yubico.webauthn.data.AuthenticatorTransport;
import com.yubico.webauthn.data.ByteArray;
import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
import com.yubico.webauthn.data.UserIdentity;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Named;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ApplicationScoped
@Named("webAuthnCredentialRepository")
public class WebAuthnCredentialRepository implements CredentialRepository {
private static final Logger LOGGER = LoggerFactory.getLogger(WebAuthnCredentialRepository.class);
// username, List of JSON encoded Credentials
private final Map<String, Set<String>> usernameCredentials = new HashMap<>();
private static class Credential {
@JsonProperty("registeredCredential")
private RegisteredCredential registeredCredential;
@JsonProperty("authenticatorTransports")
private Set<AuthenticatorTransport> authenticatorTransports;
@JsonProperty("userIdentity")
private UserIdentity userIdentity;
public Credential() {
super();
}
public Credential(RegisteredCredential registeredCredential, Set<AuthenticatorTransport> authenticatorTransports,
UserIdentity userIdentity) {
this.registeredCredential = registeredCredential;
this.authenticatorTransports = authenticatorTransports;
this.userIdentity = userIdentity;
}
private static ObjectMapper createObjectMapper() {
ObjectMapper objectMapper = JsonMapper.builder()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true)
.serializationInclusion(JsonInclude.Include.NON_ABSENT)
.defaultBase64Variant(Base64Variants.MODIFIED_FOR_URL)
.addModule(new Jdk8Module())
.addModule(new JavaTimeModule())
.build();
return objectMapper;
}
public String toJSON() {
ObjectMapper objectMapper = createObjectMapper();
StringWriter stringWriter = new StringWriter();
try {
objectMapper.writeValue(stringWriter, this);
} catch (IOException ex) {
LOGGER.error("I/O error: " + ex.getMessage(), ex);
}
LOGGER.debug("JSON credential: {}", stringWriter);
LOGGER.debug("JSON credential size: {} bytes", stringWriter.toString().length());
return stringWriter.toString();
}
public static Credential fromJSON(String json) {
ObjectMapper objectMapper = createObjectMapper();
try {
return objectMapper.readValue(json, Credential.class);
} catch (JsonProcessingException ex) {
LOGGER.error("JSON error: " + ex.getMessage(), ex);
}
return null;
}
public static Set<Credential> fromJSON(Set<String> jsonCredentials) {
Set<Credential> credentials = new HashSet<>();
for (String jsonCredential : jsonCredentials) {
Credential credential = Credential.fromJSON(jsonCredential);
credentials.add(credential);
}
return credentials;
}
}
@Override
public Set<PublicKeyCredentialDescriptor> getCredentialIdsForUsername(String username) {
LOGGER.debug("getCredentialIdsForUsername: {}", username);
Set<String> jsonCredentials = usernameCredentials.get(username);
if (null == jsonCredentials) {
return Collections.EMPTY_SET;
}
Set<Credential> credentials = Credential.fromJSON(jsonCredentials);
Set<PublicKeyCredentialDescriptor> result = new HashSet<>();
for (Credential credential : credentials) {
RegisteredCredential registeredCredential = credential.registeredCredential;
ByteArray credentialId = registeredCredential.getCredentialId();
LOGGER.debug("credential id: {}", credentialId.getHex());
Set<AuthenticatorTransport> authenticatorTransports = credential.authenticatorTransports;
PublicKeyCredentialDescriptor descriptor = PublicKeyCredentialDescriptor.builder()
.id(credentialId)
.transports(authenticatorTransports)
.build();
result.add(descriptor);
}
return result;
}
@Override
public Optional<ByteArray> getUserHandleForUsername(String username) {
LOGGER.debug("getUserHandleForUsername: {}", username);
Set<String> jsonCredentials = this.usernameCredentials.get(username);
if (null == jsonCredentials) {
return Optional.empty();
}
Set<Credential> credentials = Credential.fromJSON(jsonCredentials);
for (Credential credential : credentials) {
ByteArray userHandle = credential.userIdentity.getId();
LOGGER.debug("user handle: {}", userHandle.getHex());
return Optional.of(userHandle);
}
return Optional.empty();
}
@Override
public Optional<String> getUsernameForUserHandle(ByteArray userHandle) {
LOGGER.debug("getUsernameForUserHandle: {}", userHandle.getHex());
Collection<Set<String>> allJsonCredentials = this.usernameCredentials.values();
for (Set<String> jsonCredentials : allJsonCredentials) {
Set<Credential> credentials = Credential.fromJSON(jsonCredentials);
for (Credential credential : credentials) {
if (credential.userIdentity.getId().equals(userHandle)) {
String username = credential.userIdentity.getName();
LOGGER.debug("username: {}", username);
return Optional.of(username);
}
}
}
return Optional.empty();
}
@Override
public Optional<RegisteredCredential> lookup(ByteArray credentialId, ByteArray userHandle) {
LOGGER.debug("lookup");
LOGGER.debug("credential id: {}", credentialId.getHex());
LOGGER.debug("user handle: {}", userHandle.getHex());
Collection<Set<String>> allJsonCredentials = this.usernameCredentials.values();
for (Set<String> jsonCredentials : allJsonCredentials) {
Set<Credential> credentials = Credential.fromJSON(jsonCredentials);
for (Credential credential : credentials) {
if (credential.registeredCredential.getCredentialId().equals(credentialId)) {
if (credential.registeredCredential.getUserHandle().equals(userHandle)) {
RegisteredCredential registeredCredential
= RegisteredCredential.builder()
.credentialId(credential.registeredCredential.getCredentialId())
.userHandle(credential.userIdentity.getId())
.publicKeyCose(credential.registeredCredential.getPublicKeyCose())
.signatureCount(credential.registeredCredential.getSignatureCount())
.build();
return Optional.of(registeredCredential);
}
}
}
}
return Optional.empty();
}
@Override
public Set<RegisteredCredential> lookupAll(ByteArray credentialId) {
LOGGER.debug("lookupAll: {}", credentialId.getHex());
Set<RegisteredCredential> result = new HashSet<>();
Collection<Set<String>> allJsonCredentials = this.usernameCredentials.values();
for (Set<String> jsonCredentials : allJsonCredentials) {
Set<Credential> credentials = Credential.fromJSON(jsonCredentials);
for (Credential credential : credentials) {
if (credential.registeredCredential.getCredentialId().equals(credentialId)) {
RegisteredCredential registeredCredential = RegisteredCredential.builder()
.credentialId(credential.registeredCredential.getCredentialId())
.userHandle(credential.userIdentity.getId())
.publicKeyCose(credential.registeredCredential.getPublicKeyCose())
.signatureCount(credential.registeredCredential.getSignatureCount())
.build();
result.add(registeredCredential);
}
}
}
return result;
}
public void addRegistration(String username, RegisteredCredential registeredCredential, Set<AuthenticatorTransport> authenticatorTransports,
UserIdentity userIdentity) {
LOGGER.debug("add registration for user {}", username);
LOGGER.debug("user id: {}", userIdentity.getId().getHex());
LOGGER.debug("user handle: {}", registeredCredential.getUserHandle().getHex());
LOGGER.debug("credential id: {}", registeredCredential.getCredentialId().getHex());
Set<String> jsoncredentials = this.usernameCredentials.get(username);
if (null == jsoncredentials) {
jsoncredentials = new HashSet<>();
this.usernameCredentials.put(username, jsoncredentials);
}
Credential credential = new Credential(registeredCredential, authenticatorTransports, userIdentity);
jsoncredentials.add(credential.toJSON());
}
public void updateSignatureCount(AssertionResult result) {
String username = result.getUsername();
ByteArray credentialId = result.getCredential().getCredentialId();
Set<String> jsonCredentials = this.usernameCredentials.get(username);
if (null == jsonCredentials) {
LOGGER.warn("unknown username: {}", username);
return;
}
Credential foundCredential = null;
Set<Credential> credentials = Credential.fromJSON(jsonCredentials);
for (Credential credential : credentials) {
RegisteredCredential registeredCredential = credential.registeredCredential;
if (registeredCredential.getCredentialId().equals(credentialId)) {
foundCredential = credential;
break;
}
}
if (null == foundCredential) {
LOGGER.warn("unknown credential {} for user {}", credentialId.getHex(), username);
return;
}
foundCredential.registeredCredential = foundCredential.registeredCredential
.toBuilder()
.signatureCount(result.getSignatureCount())
.build();
}
public Set<String> getUsers() {
return this.usernameCredentials.keySet();
}
public void removeAll() {
this.usernameCredentials.clear();
}
public void remove(String username) {
this.usernameCredentials.remove(username);
}
}