diff --git a/1_Getting_Started/README.md b/1_Getting_Started/README.md
index 6601d9d..fa70d70 100644
--- a/1_Getting_Started/README.md
+++ b/1_Getting_Started/README.md
@@ -4,11 +4,10 @@ This section outlines the prerequisites to complete the workshop and instruction
## Prerequisites
In order to complete this workshop you will need a development environment with the following:
* [Git](https://git-scm.com/)
-* [JDK 1.8](https://www.oracle.com/technetwork/java/javase/downloads/index.html) or later
+* [JDK 17](https://www.oracle.com/technetwork/java/javase/downloads/index.html) or later
* [Maven 3.2](https://maven.apache.org/download.cgi)+
* FIDO2 compatible platform / browser
- * MacOS: [Safari Technical Preview](https://developer.apple.com/safari/technology-preview/) version 71+
- * Windows 10 Version 1809+: Edge
+ * Chrome 67+, Firefox 60+, Safari 14+, or Edge (all modern browsers support WebAuthn natively)
* A favorite text editor or IDE
* A security key
diff --git a/1_Getting_Started/complete/Dockerfile b/1_Getting_Started/complete/Dockerfile
index a68b8cd..6812d02 100644
--- a/1_Getting_Started/complete/Dockerfile
+++ b/1_Getting_Started/complete/Dockerfile
@@ -2,12 +2,12 @@ FROM alpine/git as clone
WORKDIR /app
RUN git clone https://github.com/YubicoLabs/java-webauthn-passwordless-workshop.git
-FROM maven:3.5-jdk-8-alpine as build
+FROM maven:3.9-eclipse-temurin-17-alpine as build
WORKDIR /app
COPY --from=clone /app/java-webauthn-passwordless-workshop/initial /app
RUN mvn clean package
-FROM openjdk:8-jre-alpine
+FROM eclipse-temurin:17-jre-alpine
VOLUME /tmp
WORKDIR /app
COPY --from=build /app/target/demo-0.0.1-SNAPSHOT.jar /app
diff --git a/2_Credential_Repository/README.md b/2_Credential_Repository/README.md
index d967a78..1dde3a8 100644
--- a/2_Credential_Repository/README.md
+++ b/2_Credential_Repository/README.md
@@ -50,25 +50,24 @@ Windows 10 example of multiple credentials on a security key. The top choice is
### Dependency configuration
-Open the ```pom.xml``` and add the webauthn-server-core and webauthn-server-attestation dependencies. The workshop is known to work with version 1.2.0 of the java-webauthn-server.
+Open the ```pom.xml``` and add the webauthn-server-core dependency. The workshop is known to work with version 2.9.0 of the java-webauthn-server.
```xml
ch.qos.logbacklogback-classic
- 1.2.3
+ 1.2.13com.yubicowebauthn-server-core
- 1.2.0
+ 2.9.0compile
- com.yubico
- webauthn-server-attestation
-
- 1.2.0
+ com.google.guava
+ guava
+ 32.1.3-jrecompile
```
@@ -88,7 +87,7 @@ We will be using some resources from the webauthn-server-demo in our project.
2. Open `getLibs.sh` or `getLibs.ps1` in your editor take a look at what it does
1. First it clones the java-webauthn-server repo
2. Next the webauthn-server-demo data and yubico packages are copied to the project
- 3. Then the AuthenticatedAction, Config, InMemoryRegistrationStorage, and RegistrationStorage, preview metadata, lib, and js content are copied to the project
+ 3. Then the AuthenticatedAction, Config, InMemoryRegistrationStorage, and RegistrationStorage, lib, and js content are copied to the project
4. Finally, the webauthn-server-demo package names and renamed to match our project structure. Some of the java files that were copied over had an incorrect package name. For each file we replace 'demo.webauthn' with 'com.example.demo'.
3. Run the getLibs script
@@ -144,11 +143,6 @@ the server will report. Example: `YUBICO_WEBAUTHN_RP_ID=demo.yubico.com`
the server will report. Example: `YUBICO_WEBAUTHN_RP_ID='Yubico Web
Authentication demo'`
-- `YUBICO_WEBAUTHN_RP_ICON`: An optional URL to an
-[icon](https://www.w3.org/TR/webauthn/#dom-publickeycredentialentity-icon) to
-represent this Relying Party. Example:
-`YUBICO_WEBAUTHN_RP_ICON='https://www.yubico.com/wp-content/uploads/2014/09/favicon.ico'`
-
**InMemoryRegistrationStorage.java**
The `InMemoryRegistrationStorage` implements the `RegistrationStorage` and `CredentialRepository` interfaces and stores credential registrations in memory.
@@ -173,7 +167,7 @@ The `js` and `lib` folders contain javascript libraries which help process the W
**preview-metadata.json**
-The `preview-metadata.json` stores metadata for known/trusted authenticators.
+The `preview-metadata.json` file is included for legacy compatibility but is no longer used by java-webauthn-server 2.x. Attestation metadata features were removed in version 2.0.
diff --git a/2_Credential_Repository/complete/Dockerfile b/2_Credential_Repository/complete/Dockerfile
index 5890ab7..00df046 100644
--- a/2_Credential_Repository/complete/Dockerfile
+++ b/2_Credential_Repository/complete/Dockerfile
@@ -2,12 +2,12 @@ FROM alpine/git as clone
WORKDIR /app
RUN git clone https://github.com/YubicoLabs/java-webauthn-passwordless-workshop.git
-FROM maven:3.5-jdk-8-alpine as build
+FROM maven:3.9-eclipse-temurin-17-alpine as build
WORKDIR /app
COPY --from=clone /app/java-webauthn-passwordless-workshop/2_Credential_Repository/complete /app
RUN mvn clean package
-FROM openjdk:8-jre-alpine
+FROM eclipse-temurin:17-jre-alpine
VOLUME /tmp
WORKDIR /app
COPY --from=build /app/target/demo-0.0.1-SNAPSHOT.jar /app
diff --git a/2_Credential_Repository/complete/getLibs.sh b/2_Credential_Repository/complete/getLibs.sh
index 154e048..fb19e2d 100755
--- a/2_Credential_Repository/complete/getLibs.sh
+++ b/2_Credential_Repository/complete/getLibs.sh
@@ -7,7 +7,7 @@ cd tmp
git clone https://github.com/Yubico/java-webauthn-server.git
git fetch https://github.com/Yubico/java-webauthn-server.git
-git -C java-webauthn-server checkout tags/1.2.0
+git -C java-webauthn-server checkout tags/2.9.0
# Copy the webauthn-server-demo data package to the project
cp -r java-webauthn-server/webauthn-server-demo/src/main/java/demo/webauthn/data ../src/main/java/com/example/demo
diff --git a/2_Credential_Repository/complete/pom.xml b/2_Credential_Repository/complete/pom.xml
index cc945c9..cee8b0a 100644
--- a/2_Credential_Repository/complete/pom.xml
+++ b/2_Credential_Repository/complete/pom.xml
@@ -5,7 +5,7 @@
org.springframework.bootspring-boot-starter-parent
- 2.1.4.RELEASE
+ 2.7.18com.example
@@ -15,7 +15,7 @@
Demo project for Spring Boot
- 1.8
+ 17
@@ -55,21 +55,19 @@
ch.qos.logbacklogback-classic
- 1.2.3
+ 1.2.13com.yubicowebauthn-server-core
- 1.2.0
+ 2.9.0compile
- com.yubico
- webauthn-server-attestation
-
- 1.2.0
- compile
+ com.google.guava
+ guava
+ 32.1.3-jre
@@ -120,7 +118,7 @@
${REGION}
- jre8
+ java17
diff --git a/2_Credential_Repository/complete/src/main/java/com/example/demo/Config.java b/2_Credential_Repository/complete/src/main/java/com/example/demo/Config.java
index 875bfe6..0dc811a 100644
--- a/2_Credential_Repository/complete/src/main/java/com/example/demo/Config.java
+++ b/2_Credential_Repository/complete/src/main/java/com/example/demo/Config.java
@@ -24,7 +24,6 @@
package com.example.demo;
-import com.yubico.internal.util.CollectionUtil;
import com.yubico.webauthn.data.RelyingPartyIdentity;
import com.yubico.webauthn.extension.appid.AppId;
import com.yubico.webauthn.extension.appid.InvalidAppIdException;
@@ -53,7 +52,7 @@ public class Config {
private final Optional appId;
private Config(Set origins, int port, RelyingPartyIdentity rpIdentity, Optional appId) {
- this.origins = CollectionUtil.immutableSet(origins);
+ this.origins = Collections.unmodifiableSet(new HashSet<>(origins));
this.port = port;
this.rpIdentity = rpIdentity;
this.appId = appId;
@@ -140,15 +139,9 @@ private static RelyingPartyIdentity computeRpIdentity() throws MalformedURLExcep
resultBuilder.id(id);
}
- if (icon == null) {
- logger.debug("RP icon not given - using none.");
- } else {
- try {
- resultBuilder.icon(Optional.of(new URL(icon)));
- } catch (MalformedURLException e) {
- logger.error("Invalid icon URL: {}", icon, e);
- throw e;
- }
+ // Note: icon property was removed in webauthn-server-core 2.x
+ if (icon != null) {
+ logger.warn("RP icon property is no longer supported in webauthn-server-core 2.x and will be ignored");
}
final RelyingPartyIdentity result = resultBuilder.build();
diff --git a/2_Credential_Repository/complete/src/main/java/com/example/demo/InMemoryRegistrationStorage.java b/2_Credential_Repository/complete/src/main/java/com/example/demo/InMemoryRegistrationStorage.java
index 17c5460..1a3efe5 100644
--- a/2_Credential_Repository/complete/src/main/java/com/example/demo/InMemoryRegistrationStorage.java
+++ b/2_Credential_Repository/complete/src/main/java/com/example/demo/InMemoryRegistrationStorage.java
@@ -26,7 +26,6 @@
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
-import com.yubico.internal.util.CollectionUtil;
import com.yubico.webauthn.AssertionResult;
import com.yubico.webauthn.CredentialRepository;
import com.yubico.webauthn.RegisteredCredential;
@@ -34,6 +33,7 @@
import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
import com.example.demo.data.CredentialRegistration;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashSet;
import java.util.NoSuchElementException;
import java.util.Optional;
@@ -171,7 +171,7 @@ public Optional lookup(ByteArray credentialId, ByteArray u
@Override
public Set lookupAll(ByteArray credentialId) {
- return CollectionUtil.immutableSet(
+ return Collections.unmodifiableSet(
storage.asMap().values().stream()
.flatMap(Collection::stream)
.filter(reg -> reg.getCredential().getCredentialId().equals(credentialId))
diff --git a/2_Credential_Repository/complete/src/main/java/com/example/demo/WebAuthnServer.java b/2_Credential_Repository/complete/src/main/java/com/example/demo/WebAuthnServer.java
index 1be3ea0..030c1d5 100644
--- a/2_Credential_Repository/complete/src/main/java/com/example/demo/WebAuthnServer.java
+++ b/2_Credential_Repository/complete/src/main/java/com/example/demo/WebAuthnServer.java
@@ -26,12 +26,9 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
-import com.google.common.io.Closeables;
-import com.yubico.internal.util.CertificateParser;
-import com.yubico.internal.util.ExceptionUtil;
-import com.yubico.internal.util.WebAuthnCodecs;
import com.yubico.util.Either;
import com.yubico.webauthn.AssertionResult;
import com.yubico.webauthn.FinishAssertionOptions;
@@ -42,16 +39,6 @@
import com.yubico.webauthn.StartAssertionOptions;
import com.yubico.webauthn.StartRegistrationOptions;
import com.yubico.webauthn.U2fVerifier;
-import com.yubico.webauthn.attestation.Attestation;
-import com.yubico.webauthn.attestation.AttestationResolver;
-import com.yubico.webauthn.attestation.MetadataObject;
-import com.yubico.webauthn.attestation.MetadataService;
-import com.yubico.webauthn.attestation.StandardMetadataService;
-import com.yubico.webauthn.attestation.TrustResolver;
-import com.yubico.webauthn.attestation.resolver.CompositeAttestationResolver;
-import com.yubico.webauthn.attestation.resolver.CompositeTrustResolver;
-import com.yubico.webauthn.attestation.resolver.SimpleAttestationResolver;
-import com.yubico.webauthn.attestation.resolver.SimpleTrustResolverWithEquality;
import com.yubico.webauthn.data.AttestationConveyancePreference;
import com.yubico.webauthn.data.AuthenticatorSelectionCriteria;
import com.yubico.webauthn.data.ByteArray;
@@ -70,11 +57,11 @@
import com.example.demo.data.U2fRegistrationResponse;
import com.example.demo.data.U2fRegistrationResult;
import java.io.IOException;
-import java.io.InputStream;
import java.security.SecureRandom;
-import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
+import java.io.ByteArrayInputStream;
import java.time.Clock;
import java.util.Arrays;
import java.util.Collection;
@@ -96,28 +83,13 @@ public class WebAuthnServer {
private static final Logger logger = LoggerFactory.getLogger(WebAuthnServer.class);
private static final SecureRandom random = new SecureRandom();
- private static final String PREVIEW_METADATA_PATH = "/preview-metadata.json";
-
private final Cache assertRequestStorage;
private final Cache registerRequestStorage;
private final RegistrationStorage userStorage;
private final Cache authenticatedActions = newCache();
-
- private final TrustResolver trustResolver = new CompositeTrustResolver(Arrays.asList(
- StandardMetadataService.createDefaultTrustResolver(),
- createExtraTrustResolver()
- ));
-
- private final MetadataService metadataService = new StandardMetadataService(
- new CompositeAttestationResolver(Arrays.asList(
- StandardMetadataService.createDefaultAttestationResolver(trustResolver),
- createExtraMetadataResolver(trustResolver)
- ))
- );
-
private final Clock clock = Clock.systemDefaultZone();
- private final ObjectMapper jsonMapper = WebAuthnCodecs.json();
+ private final ObjectMapper jsonMapper = new ObjectMapper().registerModule(new Jdk8Module());
private final RelyingParty rp;
@@ -135,8 +107,6 @@ public WebAuthnServer(RegistrationStorage userStorage, Cache startRegistration(
.build()
)
.authenticatorSelection(AuthenticatorSelectionCriteria.builder()
- .requireResidentKey(requireResidentKey)
+ .residentKey(requireResidentKey ? com.yubico.webauthn.data.ResidentKeyRequirement.REQUIRED : com.yubico.webauthn.data.ResidentKeyRequirement.DISCOURAGED)
.build()
)
.build()
@@ -254,7 +246,7 @@ public Either, AssertionRequestWrapper> startAddCredential(
StartRegistrationOptions.builder()
.user(existingUser)
.authenticatorSelection(AuthenticatorSelectionCriteria.builder()
- .requireResidentKey(requireResidentKey)
+ .residentKey(requireResidentKey ? com.yubico.webauthn.data.ResidentKeyRequirement.REQUIRED : com.yubico.webauthn.data.ResidentKeyRequirement.DISCOURAGED)
.build()
)
.build()
@@ -316,7 +308,8 @@ public AttestationCertInfo(ByteArray certDer) {
der = certDer;
X509Certificate cert = null;
try {
- cert = CertificateParser.parseDer(certDer.getBytes());
+ CertificateFactory cf = CertificateFactory.getInstance("X.509");
+ cert = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certDer.getBytes()));
} catch (CertificateException e) {
logger.error("Failed to parse attestation certificate");
}
@@ -395,36 +388,19 @@ public Either, SuccessfulU2fRegistrationResult> finishU2fRegistrati
} else {
try {
- ExceptionUtil.assure(
- U2fVerifier.verify(rp.getAppId().get(), request, response),
- "Failed to verify signature."
- );
+ if (!U2fVerifier.verify(rp.getAppId().get(), request, response)) {
+ throw new RuntimeException("Failed to verify signature.");
+ }
} catch (Exception e) {
logger.debug("Failed to verify U2F signature.", e);
return Either.left(Arrays.asList("Failed to verify signature.", e.getMessage()));
}
- X509Certificate attestationCert = null;
- try {
- attestationCert = CertificateParser.parseDer(response.getCredential().getU2fResponse().getAttestationCertAndSignature().getBytes());
- } catch (CertificateException e) {
- logger.error("Failed to parse attestation certificate: {}", response.getCredential().getU2fResponse().getAttestationCertAndSignature(), e);
- }
-
- Optional attestation = Optional.empty();
- try {
- if (attestationCert != null) {
- attestation = Optional.of(metadataService.getAttestation(Collections.singletonList(attestationCert)));
- }
- } catch (CertificateEncodingException e) {
- logger.error("Failed to resolve attestation", e);
- }
-
final U2fRegistrationResult result = U2fRegistrationResult.builder()
.keyId(PublicKeyCredentialDescriptor.builder().id(response.getCredential().getU2fResponse().getKeyHandle()).build())
- .attestationTrusted(attestation.map(Attestation::isTrusted).orElse(false))
- .publicKeyCose(WebAuthnCodecs.rawEcdaKeyToCose(response.getCredential().getU2fResponse().getPublicKey()))
- .attestationMetadata(attestation)
+ .attestationTrusted(false)
+ .publicKeyCose(rawEcdaKeyToCose(response.getCredential().getU2fResponse().getPublicKey()))
+ .attestationMetadata(Optional.empty())
.build();
return Either.right(
@@ -471,7 +447,6 @@ public static class SuccessfulAuthenticationResult {
AssertionRequestWrapper request;
AssertionResponse response;
Collection registrations;
- List warnings;
}
public Either, SuccessfulAuthenticationResult> finishAuthentication(String responseJson) {
@@ -515,8 +490,7 @@ public Either, SuccessfulAuthenticationResult> finishAuthentication
new SuccessfulAuthenticationResult(
request,
response,
- userStorage.getRegistrationsByUsername(result.getUsername()),
- result.getWarnings()
+ userStorage.getRegistrationsByUsername(result.getUsername())
)
);
} else {
@@ -613,8 +587,7 @@ private CredentialRegistration addRegistration(
.userHandle(userIdentity.getId())
.publicKeyCose(result.getPublicKeyCose())
.signatureCount(response.getCredential().getResponse().getParsedAuthenticatorData().getSignatureCounter())
- .build(),
- result.getAttestationMetadata()
+ .build()
);
}
@@ -633,8 +606,7 @@ private CredentialRegistration addRegistration(
.userHandle(userIdentity.getId())
.publicKeyCose(result.getPublicKeyCose())
.signatureCount(signatureCount)
- .build(),
- result.getAttestationMetadata()
+ .build()
);
}
@@ -642,8 +614,7 @@ private CredentialRegistration addRegistration(
UserIdentity userIdentity,
Optional nickname,
long signatureCount,
- RegisteredCredential credential,
- Optional attestationMetadata
+ RegisteredCredential credential
) {
CredentialRegistration reg = CredentialRegistration.builder()
.userIdentity(userIdentity)
@@ -651,7 +622,7 @@ private CredentialRegistration addRegistration(
.registrationTime(clock.instant())
.credential(credential)
.signatureCount(signatureCount)
- .attestationMetadata(attestationMetadata)
+ .attestationMetadata(Optional.empty())
.build();
logger.debug(
diff --git a/2_Credential_Repository/complete/src/main/java/com/example/demo/data/CredentialRegistration.java b/2_Credential_Repository/complete/src/main/java/com/example/demo/data/CredentialRegistration.java
index 276e0d7..b439356 100644
--- a/2_Credential_Repository/complete/src/main/java/com/example/demo/data/CredentialRegistration.java
+++ b/2_Credential_Repository/complete/src/main/java/com/example/demo/data/CredentialRegistration.java
@@ -27,7 +27,6 @@
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.yubico.webauthn.RegisteredCredential;
-import com.yubico.webauthn.attestation.Attestation;
import com.yubico.webauthn.data.UserIdentity;
import java.time.Instant;
import java.util.Optional;
@@ -49,7 +48,7 @@ public class CredentialRegistration {
Instant registrationTime;
RegisteredCredential credential;
- Optional attestationMetadata;
+ Optional
diff --git a/3_Registration/complete/src/main/java/com/example/demo/Config.java b/3_Registration/complete/src/main/java/com/example/demo/Config.java
index 875bfe6..0dc811a 100644
--- a/3_Registration/complete/src/main/java/com/example/demo/Config.java
+++ b/3_Registration/complete/src/main/java/com/example/demo/Config.java
@@ -24,7 +24,6 @@
package com.example.demo;
-import com.yubico.internal.util.CollectionUtil;
import com.yubico.webauthn.data.RelyingPartyIdentity;
import com.yubico.webauthn.extension.appid.AppId;
import com.yubico.webauthn.extension.appid.InvalidAppIdException;
@@ -53,7 +52,7 @@ public class Config {
private final Optional appId;
private Config(Set origins, int port, RelyingPartyIdentity rpIdentity, Optional appId) {
- this.origins = CollectionUtil.immutableSet(origins);
+ this.origins = Collections.unmodifiableSet(new HashSet<>(origins));
this.port = port;
this.rpIdentity = rpIdentity;
this.appId = appId;
@@ -140,15 +139,9 @@ private static RelyingPartyIdentity computeRpIdentity() throws MalformedURLExcep
resultBuilder.id(id);
}
- if (icon == null) {
- logger.debug("RP icon not given - using none.");
- } else {
- try {
- resultBuilder.icon(Optional.of(new URL(icon)));
- } catch (MalformedURLException e) {
- logger.error("Invalid icon URL: {}", icon, e);
- throw e;
- }
+ // Note: icon property was removed in webauthn-server-core 2.x
+ if (icon != null) {
+ logger.warn("RP icon property is no longer supported in webauthn-server-core 2.x and will be ignored");
}
final RelyingPartyIdentity result = resultBuilder.build();
diff --git a/3_Registration/complete/src/main/java/com/example/demo/InMemoryRegistrationStorage.java b/3_Registration/complete/src/main/java/com/example/demo/InMemoryRegistrationStorage.java
index 17c5460..1a3efe5 100644
--- a/3_Registration/complete/src/main/java/com/example/demo/InMemoryRegistrationStorage.java
+++ b/3_Registration/complete/src/main/java/com/example/demo/InMemoryRegistrationStorage.java
@@ -26,7 +26,6 @@
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
-import com.yubico.internal.util.CollectionUtil;
import com.yubico.webauthn.AssertionResult;
import com.yubico.webauthn.CredentialRepository;
import com.yubico.webauthn.RegisteredCredential;
@@ -34,6 +33,7 @@
import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
import com.example.demo.data.CredentialRegistration;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashSet;
import java.util.NoSuchElementException;
import java.util.Optional;
@@ -171,7 +171,7 @@ public Optional lookup(ByteArray credentialId, ByteArray u
@Override
public Set lookupAll(ByteArray credentialId) {
- return CollectionUtil.immutableSet(
+ return Collections.unmodifiableSet(
storage.asMap().values().stream()
.flatMap(Collection::stream)
.filter(reg -> reg.getCredential().getCredentialId().equals(credentialId))
diff --git a/3_Registration/complete/src/main/java/com/example/demo/WebAuthnServer.java b/3_Registration/complete/src/main/java/com/example/demo/WebAuthnServer.java
index 2525ad2..030c1d5 100644
--- a/3_Registration/complete/src/main/java/com/example/demo/WebAuthnServer.java
+++ b/3_Registration/complete/src/main/java/com/example/demo/WebAuthnServer.java
@@ -26,12 +26,9 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
-import com.google.common.io.Closeables;
-import com.yubico.internal.util.CertificateParser;
-import com.yubico.internal.util.ExceptionUtil;
-import com.yubico.internal.util.WebAuthnCodecs;
import com.yubico.util.Either;
import com.yubico.webauthn.AssertionResult;
import com.yubico.webauthn.FinishAssertionOptions;
@@ -42,16 +39,6 @@
import com.yubico.webauthn.StartAssertionOptions;
import com.yubico.webauthn.StartRegistrationOptions;
import com.yubico.webauthn.U2fVerifier;
-import com.yubico.webauthn.attestation.Attestation;
-import com.yubico.webauthn.attestation.AttestationResolver;
-import com.yubico.webauthn.attestation.MetadataObject;
-import com.yubico.webauthn.attestation.MetadataService;
-import com.yubico.webauthn.attestation.StandardMetadataService;
-import com.yubico.webauthn.attestation.TrustResolver;
-import com.yubico.webauthn.attestation.resolver.CompositeAttestationResolver;
-import com.yubico.webauthn.attestation.resolver.CompositeTrustResolver;
-import com.yubico.webauthn.attestation.resolver.SimpleAttestationResolver;
-import com.yubico.webauthn.attestation.resolver.SimpleTrustResolverWithEquality;
import com.yubico.webauthn.data.AttestationConveyancePreference;
import com.yubico.webauthn.data.AuthenticatorSelectionCriteria;
import com.yubico.webauthn.data.ByteArray;
@@ -70,11 +57,11 @@
import com.example.demo.data.U2fRegistrationResponse;
import com.example.demo.data.U2fRegistrationResult;
import java.io.IOException;
-import java.io.InputStream;
import java.security.SecureRandom;
-import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
+import java.io.ByteArrayInputStream;
import java.time.Clock;
import java.util.Arrays;
import java.util.Collection;
@@ -89,16 +76,6 @@
import lombok.Value;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-
-import com.yubico.webauthn.data.AuthenticatorAttachment;
-
-import org.springframework.context.annotation.Bean;
-import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
-import com.fasterxml.jackson.annotation.PropertyAccessor;
-import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
-import com.fasterxml.jackson.databind.SerializationFeature;
-import static com.fasterxml.jackson.annotation.JsonInclude.Include;
-
import org.springframework.stereotype.Service;
@Service
@@ -106,28 +83,13 @@ public class WebAuthnServer {
private static final Logger logger = LoggerFactory.getLogger(WebAuthnServer.class);
private static final SecureRandom random = new SecureRandom();
- private static final String PREVIEW_METADATA_PATH = "/preview-metadata.json";
-
private final Cache assertRequestStorage;
private final Cache registerRequestStorage;
private final RegistrationStorage userStorage;
private final Cache authenticatedActions = newCache();
-
- private final TrustResolver trustResolver = new CompositeTrustResolver(Arrays.asList(
- StandardMetadataService.createDefaultTrustResolver(),
- createExtraTrustResolver()
- ));
-
- private final MetadataService metadataService = new StandardMetadataService(
- new CompositeAttestationResolver(Arrays.asList(
- StandardMetadataService.createDefaultAttestationResolver(trustResolver),
- createExtraMetadataResolver(trustResolver)
- ))
- );
-
private final Clock clock = Clock.systemDefaultZone();
- private final ObjectMapper jsonMapper = WebAuthnCodecs.json();
+ private final ObjectMapper jsonMapper = new ObjectMapper().registerModule(new Jdk8Module());
private final RelyingParty rp;
@@ -145,63 +107,72 @@ public WebAuthnServer(RegistrationStorage userStorage, Cache Cache newCache() {
.build();
}
- public Either startRegistration(@NonNull String username, @NonNull String displayName,
- Optional credentialNickname, boolean requireResidentKey) {
+ public Either startRegistration(
+ @NonNull String username,
+ @NonNull String displayName,
+ Optional credentialNickname,
+ boolean requireResidentKey
+ ) {
logger.trace("startRegistration username: {}, credentialNickname: {}", username, credentialNickname);
- if (username == null || username.isEmpty()) {
- return Either.left("username must not be empty.");
- }
-
- Collection registrations = userStorage.getRegistrationsByUsername(username);
-
- UserIdentity user;
-
- if (registrations.isEmpty()) {
- user = UserIdentity.builder().name(username).displayName(displayName).id(generateRandom(32)).build();
+ if (userStorage.getRegistrationsByUsername(username).isEmpty()) {
+ RegistrationRequest request = new RegistrationRequest(
+ username,
+ credentialNickname,
+ generateRandom(32),
+ rp.startRegistration(
+ StartRegistrationOptions.builder()
+ .user(UserIdentity.builder()
+ .name(username)
+ .displayName(displayName)
+ .id(generateRandom(32))
+ .build()
+ )
+ .authenticatorSelection(AuthenticatorSelectionCriteria.builder()
+ .residentKey(requireResidentKey ? com.yubico.webauthn.data.ResidentKeyRequirement.REQUIRED : com.yubico.webauthn.data.ResidentKeyRequirement.DISCOURAGED)
+ .build()
+ )
+ .build()
+ )
+ );
+ registerRequestStorage.put(request.getRequestId(), request);
+ return Either.right(request);
} else {
- user = registrations.stream().findAny().get().getUserIdentity();
+ return Either.left("The username \"" + username + "\" is already registered.");
}
-
- RegistrationRequest request = new RegistrationRequest(username, credentialNickname, generateRandom(32),
- rp.startRegistration(StartRegistrationOptions.builder().user(user)
- .authenticatorSelection(Optional
- .of(AuthenticatorSelectionCriteria.builder().requireResidentKey(requireResidentKey)
- .authenticatorAttachment(AuthenticatorAttachment.CROSS_PLATFORM) // Default to roaming security keys (CROSS_PLATFORM). Comment out this line to enable either PLATFORM or CROSS_PLATFORM authenticators
- .build()))
- .build()));
-
- registerRequestStorage.put(request.getRequestId(), request);
-
- return Either.right(request);
}
public Either, AssertionRequestWrapper> startAddCredential(
@@ -271,7 +246,7 @@ public Either, AssertionRequestWrapper> startAddCredential(
StartRegistrationOptions.builder()
.user(existingUser)
.authenticatorSelection(AuthenticatorSelectionCriteria.builder()
- .requireResidentKey(requireResidentKey)
+ .residentKey(requireResidentKey ? com.yubico.webauthn.data.ResidentKeyRequirement.REQUIRED : com.yubico.webauthn.data.ResidentKeyRequirement.DISCOURAGED)
.build()
)
.build()
@@ -333,7 +308,8 @@ public AttestationCertInfo(ByteArray certDer) {
der = certDer;
X509Certificate cert = null;
try {
- cert = CertificateParser.parseDer(certDer.getBytes());
+ CertificateFactory cf = CertificateFactory.getInstance("X.509");
+ cert = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certDer.getBytes()));
} catch (CertificateException e) {
logger.error("Failed to parse attestation certificate");
}
@@ -412,36 +388,19 @@ public Either, SuccessfulU2fRegistrationResult> finishU2fRegistrati
} else {
try {
- ExceptionUtil.assure(
- U2fVerifier.verify(rp.getAppId().get(), request, response),
- "Failed to verify signature."
- );
+ if (!U2fVerifier.verify(rp.getAppId().get(), request, response)) {
+ throw new RuntimeException("Failed to verify signature.");
+ }
} catch (Exception e) {
logger.debug("Failed to verify U2F signature.", e);
return Either.left(Arrays.asList("Failed to verify signature.", e.getMessage()));
}
- X509Certificate attestationCert = null;
- try {
- attestationCert = CertificateParser.parseDer(response.getCredential().getU2fResponse().getAttestationCertAndSignature().getBytes());
- } catch (CertificateException e) {
- logger.error("Failed to parse attestation certificate: {}", response.getCredential().getU2fResponse().getAttestationCertAndSignature(), e);
- }
-
- Optional attestation = Optional.empty();
- try {
- if (attestationCert != null) {
- attestation = Optional.of(metadataService.getAttestation(Collections.singletonList(attestationCert)));
- }
- } catch (CertificateEncodingException e) {
- logger.error("Failed to resolve attestation", e);
- }
-
final U2fRegistrationResult result = U2fRegistrationResult.builder()
.keyId(PublicKeyCredentialDescriptor.builder().id(response.getCredential().getU2fResponse().getKeyHandle()).build())
- .attestationTrusted(attestation.map(Attestation::isTrusted).orElse(false))
- .publicKeyCose(WebAuthnCodecs.rawEcdaKeyToCose(response.getCredential().getU2fResponse().getPublicKey()))
- .attestationMetadata(attestation)
+ .attestationTrusted(false)
+ .publicKeyCose(rawEcdaKeyToCose(response.getCredential().getU2fResponse().getPublicKey()))
+ .attestationMetadata(Optional.empty())
.build();
return Either.right(
@@ -488,7 +447,6 @@ public static class SuccessfulAuthenticationResult {
AssertionRequestWrapper request;
AssertionResponse response;
Collection registrations;
- List warnings;
}
public Either, SuccessfulAuthenticationResult> finishAuthentication(String responseJson) {
@@ -532,8 +490,7 @@ public Either, SuccessfulAuthenticationResult> finishAuthentication
new SuccessfulAuthenticationResult(
request,
response,
- userStorage.getRegistrationsByUsername(result.getUsername()),
- result.getWarnings()
+ userStorage.getRegistrationsByUsername(result.getUsername())
)
);
} else {
@@ -630,8 +587,7 @@ private CredentialRegistration addRegistration(
.userHandle(userIdentity.getId())
.publicKeyCose(result.getPublicKeyCose())
.signatureCount(response.getCredential().getResponse().getParsedAuthenticatorData().getSignatureCounter())
- .build(),
- result.getAttestationMetadata()
+ .build()
);
}
@@ -650,8 +606,7 @@ private CredentialRegistration addRegistration(
.userHandle(userIdentity.getId())
.publicKeyCose(result.getPublicKeyCose())
.signatureCount(signatureCount)
- .build(),
- result.getAttestationMetadata()
+ .build()
);
}
@@ -659,8 +614,7 @@ private CredentialRegistration addRegistration(
UserIdentity userIdentity,
Optional nickname,
long signatureCount,
- RegisteredCredential credential,
- Optional attestationMetadata
+ RegisteredCredential credential
) {
CredentialRegistration reg = CredentialRegistration.builder()
.userIdentity(userIdentity)
@@ -668,7 +622,7 @@ private CredentialRegistration addRegistration(
.registrationTime(clock.instant())
.credential(credential)
.signatureCount(signatureCount)
- .attestationMetadata(attestationMetadata)
+ .attestationMetadata(Optional.empty())
.build();
logger.debug(
diff --git a/3_Registration/complete/src/main/java/com/example/demo/data/CredentialRegistration.java b/3_Registration/complete/src/main/java/com/example/demo/data/CredentialRegistration.java
index 276e0d7..b439356 100644
--- a/3_Registration/complete/src/main/java/com/example/demo/data/CredentialRegistration.java
+++ b/3_Registration/complete/src/main/java/com/example/demo/data/CredentialRegistration.java
@@ -27,7 +27,6 @@
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.yubico.webauthn.RegisteredCredential;
-import com.yubico.webauthn.attestation.Attestation;
import com.yubico.webauthn.data.UserIdentity;
import java.time.Instant;
import java.util.Optional;
@@ -49,7 +48,7 @@ public class CredentialRegistration {
Instant registrationTime;
RegisteredCredential credential;
- Optional attestationMetadata;
+ Optional attestationMetadata;
@JsonProperty("registrationTime")
public String getRegistrationTimestamp() {
diff --git a/3_Registration/complete/src/main/java/com/example/demo/data/RegistrationResult.java b/3_Registration/complete/src/main/java/com/example/demo/data/RegistrationResult.java
index d377e6a..a34c1a4 100644
--- a/3_Registration/complete/src/main/java/com/example/demo/data/RegistrationResult.java
+++ b/3_Registration/complete/src/main/java/com/example/demo/data/RegistrationResult.java
@@ -1,6 +1,5 @@
package com.example.demo.data;
-import com.yubico.webauthn.attestation.Attestation;
import com.yubico.webauthn.data.AttestationType;
import com.yubico.webauthn.data.ByteArray;
import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
@@ -26,22 +25,12 @@ public class RegistrationResult {
@NonNull
private final ByteArray publicKeyCose;
- @NonNull
- @Builder.Default
- private final List warnings = Collections.emptyList();
-
- @NonNull
- @Builder.Default
- private final Optional attestationMetadata = Optional.empty();
-
public static RegistrationResult fromLibraryType(com.yubico.webauthn.RegistrationResult result) {
return builder()
.keyId(result.getKeyId())
.attestationTrusted(result.isAttestationTrusted())
.attestationType(result.getAttestationType())
.publicKeyCose(result.getPublicKeyCose())
- .warnings(result.getWarnings())
- .attestationMetadata(result.getAttestationMetadata())
.build();
}
diff --git a/3_Registration/complete/src/main/java/com/example/demo/data/U2fRegistrationResult.java b/3_Registration/complete/src/main/java/com/example/demo/data/U2fRegistrationResult.java
index 31c9b51..ff4a473 100644
--- a/3_Registration/complete/src/main/java/com/example/demo/data/U2fRegistrationResult.java
+++ b/3_Registration/complete/src/main/java/com/example/demo/data/U2fRegistrationResult.java
@@ -1,10 +1,7 @@
package com.example.demo.data;
-import com.yubico.webauthn.attestation.Attestation;
import com.yubico.webauthn.data.ByteArray;
import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
-import java.util.Collections;
-import java.util.List;
import java.util.Optional;
import lombok.Builder;
import lombok.NonNull;
@@ -24,9 +21,5 @@ public class U2fRegistrationResult {
@NonNull
@Builder.Default
- private final List warnings = Collections.emptyList();
-
- @NonNull
- @Builder.Default
- private final Optional attestationMetadata = Optional.empty();
+ private final Optional attestationMetadata = Optional.empty();
}
diff --git a/3_Registration/complete/src/main/java/com/yubico/webauthn/U2fVerifier.java b/3_Registration/complete/src/main/java/com/yubico/webauthn/U2fVerifier.java
index f543411..74917bb 100644
--- a/3_Registration/complete/src/main/java/com/yubico/webauthn/U2fVerifier.java
+++ b/3_Registration/complete/src/main/java/com/yubico/webauthn/U2fVerifier.java
@@ -25,9 +25,8 @@
package com.yubico.webauthn;
import com.fasterxml.jackson.databind.JsonNode;
-import com.yubico.internal.util.CertificateParser;
-import com.yubico.internal.util.ExceptionUtil;
-import com.yubico.internal.util.WebAuthnCodecs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.yubico.webauthn.data.ByteArray;
import com.yubico.webauthn.data.exception.Base64UrlException;
import com.yubico.webauthn.extension.appid.AppId;
@@ -36,28 +35,49 @@
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
public class U2fVerifier {
- private static final BouncyCastleCrypto crypto = new BouncyCastleCrypto();
+ private static final ObjectMapper jsonMapper = new ObjectMapper().registerModule(new Jdk8Module());
+
+ private static ByteArray hash(String data) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ return new ByteArray(digest.digest(data.getBytes()));
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static ByteArray hash(ByteArray data) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ return new ByteArray(digest.digest(data.getBytes()));
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ }
public static boolean verify(AppId appId, RegistrationRequest request, U2fRegistrationResponse response) throws CertificateException, IOException, Base64UrlException {
- final ByteArray appIdHash = crypto.hash(appId.getId());
- final ByteArray clientDataHash = crypto.hash(response.getCredential().getU2fResponse().getClientDataJSON());
+ final ByteArray appIdHash = hash(appId.getId());
+ final ByteArray clientDataHash = hash(response.getCredential().getU2fResponse().getClientDataJSON());
- final JsonNode clientData = WebAuthnCodecs.json().readTree(response.getCredential().getU2fResponse().getClientDataJSON().getBytes());
+ final JsonNode clientData = jsonMapper.readTree(response.getCredential().getU2fResponse().getClientDataJSON().getBytes());
final String challengeBase64 = clientData.get("challenge").textValue();
- ExceptionUtil.assure(
- request.getPublicKeyCredentialCreationOptions().getChallenge().equals(ByteArray.fromBase64Url(challengeBase64)),
- "Wrong challenge."
- );
+ if (!request.getPublicKeyCredentialCreationOptions().getChallenge().equals(ByteArray.fromBase64Url(challengeBase64))) {
+ throw new IllegalArgumentException("Wrong challenge.");
+ }
InputStream attestationCertAndSignatureStream = new ByteArrayInputStream(response.getCredential().getU2fResponse().getAttestationCertAndSignature().getBytes());
- final X509Certificate attestationCert = CertificateParser.parseDer(attestationCertAndSignatureStream);
+ CertificateFactory cf = CertificateFactory.getInstance("X.509");
+ final X509Certificate attestationCert = (X509Certificate) cf.generateCertificate(attestationCertAndSignatureStream);
byte[] signatureBytes = new byte[attestationCertAndSignatureStream.available()];
attestationCertAndSignatureStream.read(signatureBytes);
diff --git a/3_Registration/complete/src/main/java/com/yubico/webauthn/attestation/resolver/SimpleTrustResolverWithEquality.java b/3_Registration/complete/src/main/java/com/yubico/webauthn/attestation/resolver/SimpleTrustResolverWithEquality.java
deleted file mode 100644
index 8bb5d8d..0000000
--- a/3_Registration/complete/src/main/java/com/yubico/webauthn/attestation/resolver/SimpleTrustResolverWithEquality.java
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright (c) 2018, Yubico AB
-// All rights reserved.
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are met:
-//
-// 1. Redistributions of source code must retain the above copyright notice, this
-// list of conditions and the following disclaimer.
-//
-// 2. Redistributions in binary form must reproduce the above copyright notice,
-// this list of conditions and the following disclaimer in the documentation
-// and/or other materials provided with the distribution.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-package com.yubico.webauthn.attestation.resolver;
-
-import com.google.common.collect.ArrayListMultimap;
-import com.google.common.collect.Multimap;
-import com.yubico.webauthn.attestation.TrustResolver;
-import java.security.cert.X509Certificate;
-import java.util.Collection;
-import java.util.List;
-import java.util.Optional;
-
-/**
- * Resolves a metadata object whose associated certificate has signed the
- * argument certificate, or is equal to the argument certificate.
- */
-public class SimpleTrustResolverWithEquality implements TrustResolver {
-
- private final SimpleTrustResolver subresolver;
- private final Multimap trustedCerts = ArrayListMultimap.create();
-
- public SimpleTrustResolverWithEquality(Collection trustedCertificates) {
- subresolver = new SimpleTrustResolver(trustedCertificates);
-
- for (X509Certificate cert : trustedCertificates) {
- trustedCerts.put(cert.getSubjectDN().getName(), cert);
- }
- }
-
- @Override
- public Optional resolveTrustAnchor(X509Certificate attestationCertificate, List caCertificateChain) {
- Optional subResult = subresolver.resolveTrustAnchor(attestationCertificate, caCertificateChain);
-
- if (subResult.isPresent()) {
- return subResult;
- } else {
- for (X509Certificate cert : trustedCerts.get(attestationCertificate.getSubjectDN().getName())) {
- if (cert.equals(attestationCertificate)) {
- return Optional.of(cert);
- }
- }
-
- return Optional.empty();
- }
- }
-
-}
diff --git a/3_Registration/complete/src/test/java/com/example/demo/DemoApplicationTests.java b/3_Registration/complete/src/test/java/com/example/demo/DemoApplicationTests.java
index b76e7f2..1db698f 100644
--- a/3_Registration/complete/src/test/java/com/example/demo/DemoApplicationTests.java
+++ b/3_Registration/complete/src/test/java/com/example/demo/DemoApplicationTests.java
@@ -1,11 +1,8 @@
package com.example.demo;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.test.context.junit4.SpringRunner;
-@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
diff --git a/4_Authentication/README.md b/4_Authentication/README.md
index d439a08..9477d02 100644
--- a/4_Authentication/README.md
+++ b/4_Authentication/README.md
@@ -142,6 +142,8 @@ Let's expose two REST endpoints to start and finish the WebAuthn authentication
.anyRequest().authenticated()
```
+
+
### Integrate WebAuthn API into application
1. Open the `./src/main/resources/templates/login.html` template
2. Add the following code in the header section
diff --git a/4_Authentication/complete/Dockerfile b/4_Authentication/complete/Dockerfile
index da23b73..0ddc511 100644
--- a/4_Authentication/complete/Dockerfile
+++ b/4_Authentication/complete/Dockerfile
@@ -2,12 +2,12 @@ FROM alpine/git as clone
WORKDIR /app
RUN git clone https://github.com/YubicoLabs/java-webauthn-passwordless-workshop.git
-FROM maven:3.5-jdk-8-alpine as build
+FROM maven:3.9-eclipse-temurin-17-alpine as build
WORKDIR /app
COPY --from=clone /app/java-webauthn-passwordless-workshop/4_Authentication/complete /app
RUN mvn clean package
-FROM openjdk:8-jre-alpine
+FROM eclipse-temurin:17-jre-alpine
VOLUME /tmp
WORKDIR /app
COPY --from=build /app/target/demo-0.0.1-SNAPSHOT.jar /app
diff --git a/4_Authentication/complete/getLibs.sh b/4_Authentication/complete/getLibs.sh
index 154e048..fb19e2d 100755
--- a/4_Authentication/complete/getLibs.sh
+++ b/4_Authentication/complete/getLibs.sh
@@ -7,7 +7,7 @@ cd tmp
git clone https://github.com/Yubico/java-webauthn-server.git
git fetch https://github.com/Yubico/java-webauthn-server.git
-git -C java-webauthn-server checkout tags/1.2.0
+git -C java-webauthn-server checkout tags/2.9.0
# Copy the webauthn-server-demo data package to the project
cp -r java-webauthn-server/webauthn-server-demo/src/main/java/demo/webauthn/data ../src/main/java/com/example/demo
diff --git a/4_Authentication/complete/pom.xml b/4_Authentication/complete/pom.xml
index 4343b8b..45bf12b 100644
--- a/4_Authentication/complete/pom.xml
+++ b/4_Authentication/complete/pom.xml
@@ -5,7 +5,7 @@
org.springframework.bootspring-boot-starter-parent
- 2.1.4.RELEASE
+ 2.7.18com.example
@@ -15,7 +15,7 @@
Demo project for Spring Boot
- 1.8
+ 17
@@ -55,21 +55,19 @@
ch.qos.logbacklogback-classic
- 1.2.3
+ 1.2.13com.yubicowebauthn-server-core
- 1.2.0
+ 2.9.0compile
- com.yubico
- webauthn-server-attestation
-
- 1.2.0
- compile
+ com.google.guava
+ guava
+ 32.1.3-jre
@@ -120,7 +118,7 @@
${REGION}
- jre8
+ java17
diff --git a/4_Authentication/complete/src/main/java/com/example/demo/Config.java b/4_Authentication/complete/src/main/java/com/example/demo/Config.java
index 875bfe6..0dc811a 100644
--- a/4_Authentication/complete/src/main/java/com/example/demo/Config.java
+++ b/4_Authentication/complete/src/main/java/com/example/demo/Config.java
@@ -24,7 +24,6 @@
package com.example.demo;
-import com.yubico.internal.util.CollectionUtil;
import com.yubico.webauthn.data.RelyingPartyIdentity;
import com.yubico.webauthn.extension.appid.AppId;
import com.yubico.webauthn.extension.appid.InvalidAppIdException;
@@ -53,7 +52,7 @@ public class Config {
private final Optional appId;
private Config(Set origins, int port, RelyingPartyIdentity rpIdentity, Optional appId) {
- this.origins = CollectionUtil.immutableSet(origins);
+ this.origins = Collections.unmodifiableSet(new HashSet<>(origins));
this.port = port;
this.rpIdentity = rpIdentity;
this.appId = appId;
@@ -140,15 +139,9 @@ private static RelyingPartyIdentity computeRpIdentity() throws MalformedURLExcep
resultBuilder.id(id);
}
- if (icon == null) {
- logger.debug("RP icon not given - using none.");
- } else {
- try {
- resultBuilder.icon(Optional.of(new URL(icon)));
- } catch (MalformedURLException e) {
- logger.error("Invalid icon URL: {}", icon, e);
- throw e;
- }
+ // Note: icon property was removed in webauthn-server-core 2.x
+ if (icon != null) {
+ logger.warn("RP icon property is no longer supported in webauthn-server-core 2.x and will be ignored");
}
final RelyingPartyIdentity result = resultBuilder.build();
diff --git a/4_Authentication/complete/src/main/java/com/example/demo/InMemoryRegistrationStorage.java b/4_Authentication/complete/src/main/java/com/example/demo/InMemoryRegistrationStorage.java
index 17c5460..1a3efe5 100644
--- a/4_Authentication/complete/src/main/java/com/example/demo/InMemoryRegistrationStorage.java
+++ b/4_Authentication/complete/src/main/java/com/example/demo/InMemoryRegistrationStorage.java
@@ -26,7 +26,6 @@
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
-import com.yubico.internal.util.CollectionUtil;
import com.yubico.webauthn.AssertionResult;
import com.yubico.webauthn.CredentialRepository;
import com.yubico.webauthn.RegisteredCredential;
@@ -34,6 +33,7 @@
import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
import com.example.demo.data.CredentialRegistration;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashSet;
import java.util.NoSuchElementException;
import java.util.Optional;
@@ -171,7 +171,7 @@ public Optional lookup(ByteArray credentialId, ByteArray u
@Override
public Set lookupAll(ByteArray credentialId) {
- return CollectionUtil.immutableSet(
+ return Collections.unmodifiableSet(
storage.asMap().values().stream()
.flatMap(Collection::stream)
.filter(reg -> reg.getCredential().getCredentialId().equals(credentialId))
diff --git a/4_Authentication/complete/src/main/java/com/example/demo/WebAuthnServer.java b/4_Authentication/complete/src/main/java/com/example/demo/WebAuthnServer.java
index 9f4e337..030c1d5 100644
--- a/4_Authentication/complete/src/main/java/com/example/demo/WebAuthnServer.java
+++ b/4_Authentication/complete/src/main/java/com/example/demo/WebAuthnServer.java
@@ -26,12 +26,9 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
-import com.google.common.io.Closeables;
-import com.yubico.internal.util.CertificateParser;
-import com.yubico.internal.util.ExceptionUtil;
-import com.yubico.internal.util.WebAuthnCodecs;
import com.yubico.util.Either;
import com.yubico.webauthn.AssertionResult;
import com.yubico.webauthn.FinishAssertionOptions;
@@ -42,18 +39,7 @@
import com.yubico.webauthn.StartAssertionOptions;
import com.yubico.webauthn.StartRegistrationOptions;
import com.yubico.webauthn.U2fVerifier;
-import com.yubico.webauthn.attestation.Attestation;
-import com.yubico.webauthn.attestation.AttestationResolver;
-import com.yubico.webauthn.attestation.MetadataObject;
-import com.yubico.webauthn.attestation.MetadataService;
-import com.yubico.webauthn.attestation.StandardMetadataService;
-import com.yubico.webauthn.attestation.TrustResolver;
-import com.yubico.webauthn.attestation.resolver.CompositeAttestationResolver;
-import com.yubico.webauthn.attestation.resolver.CompositeTrustResolver;
-import com.yubico.webauthn.attestation.resolver.SimpleAttestationResolver;
-import com.yubico.webauthn.attestation.resolver.SimpleTrustResolverWithEquality;
import com.yubico.webauthn.data.AttestationConveyancePreference;
-import com.yubico.webauthn.data.AuthenticatorAttachment;
import com.yubico.webauthn.data.AuthenticatorSelectionCriteria;
import com.yubico.webauthn.data.ByteArray;
import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
@@ -71,11 +57,11 @@
import com.example.demo.data.U2fRegistrationResponse;
import com.example.demo.data.U2fRegistrationResult;
import java.io.IOException;
-import java.io.InputStream;
import java.security.SecureRandom;
-import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
+import java.io.ByteArrayInputStream;
import java.time.Clock;
import java.util.Arrays;
import java.util.Collection;
@@ -90,14 +76,6 @@
import lombok.Value;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-
-import org.springframework.context.annotation.Bean;
-import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
-import com.fasterxml.jackson.annotation.PropertyAccessor;
-import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
-import com.fasterxml.jackson.databind.SerializationFeature;
-import static com.fasterxml.jackson.annotation.JsonInclude.Include;
-
import org.springframework.stereotype.Service;
@Service
@@ -105,28 +83,13 @@ public class WebAuthnServer {
private static final Logger logger = LoggerFactory.getLogger(WebAuthnServer.class);
private static final SecureRandom random = new SecureRandom();
- private static final String PREVIEW_METADATA_PATH = "/preview-metadata.json";
-
private final Cache assertRequestStorage;
private final Cache registerRequestStorage;
private final RegistrationStorage userStorage;
private final Cache authenticatedActions = newCache();
-
- private final TrustResolver trustResolver = new CompositeTrustResolver(Arrays.asList(
- StandardMetadataService.createDefaultTrustResolver(),
- createExtraTrustResolver()
- ));
-
- private final MetadataService metadataService = new StandardMetadataService(
- new CompositeAttestationResolver(Arrays.asList(
- StandardMetadataService.createDefaultAttestationResolver(trustResolver),
- createExtraMetadataResolver(trustResolver)
- ))
- );
-
private final Clock clock = Clock.systemDefaultZone();
- private final ObjectMapper jsonMapper = WebAuthnCodecs.json();
+ private final ObjectMapper jsonMapper = new ObjectMapper().registerModule(new Jdk8Module());
private final RelyingParty rp;
@@ -144,63 +107,72 @@ public WebAuthnServer(RegistrationStorage userStorage, Cache Cache newCache() {
}
public Either startRegistration(
- @NonNull String username,
- @NonNull String displayName,
- Optional credentialNickname,
- boolean requireResidentKey
- ) {
- logger.trace("startRegistration username: {}, credentialNickname: {}", username, credentialNickname);
-
- if (username == null || username.isEmpty()) {
- return Either.left("username must not be empty.");
- }
-
- Collection registrations = userStorage.getRegistrationsByUsername(username);
-
- UserIdentity user;
-
- if (registrations.isEmpty()) {
- user = UserIdentity.builder()
- .name(username)
- .displayName(displayName)
- .id(generateRandom(32))
- .build();
- } else {
- user = registrations.stream().findAny().get().getUserIdentity();
- }
+ @NonNull String username,
+ @NonNull String displayName,
+ Optional credentialNickname,
+ boolean requireResidentKey
+ ) {
+ logger.trace("startRegistration username: {}, credentialNickname: {}", username, credentialNickname);
+ if (userStorage.getRegistrationsByUsername(username).isEmpty()) {
RegistrationRequest request = new RegistrationRequest(
username,
credentialNickname,
generateRandom(32),
rp.startRegistration(
StartRegistrationOptions.builder()
- .user(user)
- .authenticatorSelection(Optional.of(AuthenticatorSelectionCriteria.builder()
- .requireResidentKey(requireResidentKey)
- .authenticatorAttachment(AuthenticatorAttachment.CROSS_PLATFORM) // Default to roaming security keys (CROSS_PLATFORM). Comment out this line to enable either PLATFORM or CROSS_PLATFORM authenticators
+ .user(UserIdentity.builder()
+ .name(username)
+ .displayName(displayName)
+ .id(generateRandom(32))
.build()
- ))
+ )
+ .authenticatorSelection(AuthenticatorSelectionCriteria.builder()
+ .residentKey(requireResidentKey ? com.yubico.webauthn.data.ResidentKeyRequirement.REQUIRED : com.yubico.webauthn.data.ResidentKeyRequirement.DISCOURAGED)
+ .build()
+ )
.build()
)
);
-
registerRequestStorage.put(request.getRequestId(), request);
-
return Either.right(request);
+ } else {
+ return Either.left("The username \"" + username + "\" is already registered.");
}
+ }
public Either, AssertionRequestWrapper> startAddCredential(
@NonNull String username,
@@ -286,7 +246,7 @@ public Either, AssertionRequestWrapper> startAddCredential(
StartRegistrationOptions.builder()
.user(existingUser)
.authenticatorSelection(AuthenticatorSelectionCriteria.builder()
- .requireResidentKey(requireResidentKey)
+ .residentKey(requireResidentKey ? com.yubico.webauthn.data.ResidentKeyRequirement.REQUIRED : com.yubico.webauthn.data.ResidentKeyRequirement.DISCOURAGED)
.build()
)
.build()
@@ -348,7 +308,8 @@ public AttestationCertInfo(ByteArray certDer) {
der = certDer;
X509Certificate cert = null;
try {
- cert = CertificateParser.parseDer(certDer.getBytes());
+ CertificateFactory cf = CertificateFactory.getInstance("X.509");
+ cert = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certDer.getBytes()));
} catch (CertificateException e) {
logger.error("Failed to parse attestation certificate");
}
@@ -427,36 +388,19 @@ public Either, SuccessfulU2fRegistrationResult> finishU2fRegistrati
} else {
try {
- ExceptionUtil.assure(
- U2fVerifier.verify(rp.getAppId().get(), request, response),
- "Failed to verify signature."
- );
+ if (!U2fVerifier.verify(rp.getAppId().get(), request, response)) {
+ throw new RuntimeException("Failed to verify signature.");
+ }
} catch (Exception e) {
logger.debug("Failed to verify U2F signature.", e);
return Either.left(Arrays.asList("Failed to verify signature.", e.getMessage()));
}
- X509Certificate attestationCert = null;
- try {
- attestationCert = CertificateParser.parseDer(response.getCredential().getU2fResponse().getAttestationCertAndSignature().getBytes());
- } catch (CertificateException e) {
- logger.error("Failed to parse attestation certificate: {}", response.getCredential().getU2fResponse().getAttestationCertAndSignature(), e);
- }
-
- Optional attestation = Optional.empty();
- try {
- if (attestationCert != null) {
- attestation = Optional.of(metadataService.getAttestation(Collections.singletonList(attestationCert)));
- }
- } catch (CertificateEncodingException e) {
- logger.error("Failed to resolve attestation", e);
- }
-
final U2fRegistrationResult result = U2fRegistrationResult.builder()
.keyId(PublicKeyCredentialDescriptor.builder().id(response.getCredential().getU2fResponse().getKeyHandle()).build())
- .attestationTrusted(attestation.map(Attestation::isTrusted).orElse(false))
- .publicKeyCose(WebAuthnCodecs.rawEcdaKeyToCose(response.getCredential().getU2fResponse().getPublicKey()))
- .attestationMetadata(attestation)
+ .attestationTrusted(false)
+ .publicKeyCose(rawEcdaKeyToCose(response.getCredential().getU2fResponse().getPublicKey()))
+ .attestationMetadata(Optional.empty())
.build();
return Either.right(
@@ -503,7 +447,6 @@ public static class SuccessfulAuthenticationResult {
AssertionRequestWrapper request;
AssertionResponse response;
Collection registrations;
- List warnings;
}
public Either, SuccessfulAuthenticationResult> finishAuthentication(String responseJson) {
@@ -547,8 +490,7 @@ public Either, SuccessfulAuthenticationResult> finishAuthentication
new SuccessfulAuthenticationResult(
request,
response,
- userStorage.getRegistrationsByUsername(result.getUsername()),
- result.getWarnings()
+ userStorage.getRegistrationsByUsername(result.getUsername())
)
);
} else {
@@ -645,8 +587,7 @@ private CredentialRegistration addRegistration(
.userHandle(userIdentity.getId())
.publicKeyCose(result.getPublicKeyCose())
.signatureCount(response.getCredential().getResponse().getParsedAuthenticatorData().getSignatureCounter())
- .build(),
- result.getAttestationMetadata()
+ .build()
);
}
@@ -665,8 +606,7 @@ private CredentialRegistration addRegistration(
.userHandle(userIdentity.getId())
.publicKeyCose(result.getPublicKeyCose())
.signatureCount(signatureCount)
- .build(),
- result.getAttestationMetadata()
+ .build()
);
}
@@ -674,8 +614,7 @@ private CredentialRegistration addRegistration(
UserIdentity userIdentity,
Optional nickname,
long signatureCount,
- RegisteredCredential credential,
- Optional attestationMetadata
+ RegisteredCredential credential
) {
CredentialRegistration reg = CredentialRegistration.builder()
.userIdentity(userIdentity)
@@ -683,7 +622,7 @@ private CredentialRegistration addRegistration(
.registrationTime(clock.instant())
.credential(credential)
.signatureCount(signatureCount)
- .attestationMetadata(attestationMetadata)
+ .attestationMetadata(Optional.empty())
.build();
logger.debug(
diff --git a/4_Authentication/complete/src/main/java/com/example/demo/data/CredentialRegistration.java b/4_Authentication/complete/src/main/java/com/example/demo/data/CredentialRegistration.java
index 276e0d7..b439356 100644
--- a/4_Authentication/complete/src/main/java/com/example/demo/data/CredentialRegistration.java
+++ b/4_Authentication/complete/src/main/java/com/example/demo/data/CredentialRegistration.java
@@ -27,7 +27,6 @@
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.yubico.webauthn.RegisteredCredential;
-import com.yubico.webauthn.attestation.Attestation;
import com.yubico.webauthn.data.UserIdentity;
import java.time.Instant;
import java.util.Optional;
@@ -49,7 +48,7 @@ public class CredentialRegistration {
Instant registrationTime;
RegisteredCredential credential;
- Optional attestationMetadata;
+ Optional attestationMetadata;
@JsonProperty("registrationTime")
public String getRegistrationTimestamp() {
diff --git a/4_Authentication/complete/src/main/java/com/example/demo/data/RegistrationResult.java b/4_Authentication/complete/src/main/java/com/example/demo/data/RegistrationResult.java
index d377e6a..a34c1a4 100644
--- a/4_Authentication/complete/src/main/java/com/example/demo/data/RegistrationResult.java
+++ b/4_Authentication/complete/src/main/java/com/example/demo/data/RegistrationResult.java
@@ -1,6 +1,5 @@
package com.example.demo.data;
-import com.yubico.webauthn.attestation.Attestation;
import com.yubico.webauthn.data.AttestationType;
import com.yubico.webauthn.data.ByteArray;
import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
@@ -26,22 +25,12 @@ public class RegistrationResult {
@NonNull
private final ByteArray publicKeyCose;
- @NonNull
- @Builder.Default
- private final List warnings = Collections.emptyList();
-
- @NonNull
- @Builder.Default
- private final Optional attestationMetadata = Optional.empty();
-
public static RegistrationResult fromLibraryType(com.yubico.webauthn.RegistrationResult result) {
return builder()
.keyId(result.getKeyId())
.attestationTrusted(result.isAttestationTrusted())
.attestationType(result.getAttestationType())
.publicKeyCose(result.getPublicKeyCose())
- .warnings(result.getWarnings())
- .attestationMetadata(result.getAttestationMetadata())
.build();
}
diff --git a/4_Authentication/complete/src/main/java/com/example/demo/data/U2fRegistrationResult.java b/4_Authentication/complete/src/main/java/com/example/demo/data/U2fRegistrationResult.java
index 31c9b51..ff4a473 100644
--- a/4_Authentication/complete/src/main/java/com/example/demo/data/U2fRegistrationResult.java
+++ b/4_Authentication/complete/src/main/java/com/example/demo/data/U2fRegistrationResult.java
@@ -1,10 +1,7 @@
package com.example.demo.data;
-import com.yubico.webauthn.attestation.Attestation;
import com.yubico.webauthn.data.ByteArray;
import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
-import java.util.Collections;
-import java.util.List;
import java.util.Optional;
import lombok.Builder;
import lombok.NonNull;
@@ -24,9 +21,5 @@ public class U2fRegistrationResult {
@NonNull
@Builder.Default
- private final List warnings = Collections.emptyList();
-
- @NonNull
- @Builder.Default
- private final Optional attestationMetadata = Optional.empty();
+ private final Optional attestationMetadata = Optional.empty();
}
diff --git a/4_Authentication/complete/src/main/java/com/yubico/webauthn/U2fVerifier.java b/4_Authentication/complete/src/main/java/com/yubico/webauthn/U2fVerifier.java
index f543411..74917bb 100644
--- a/4_Authentication/complete/src/main/java/com/yubico/webauthn/U2fVerifier.java
+++ b/4_Authentication/complete/src/main/java/com/yubico/webauthn/U2fVerifier.java
@@ -25,9 +25,8 @@
package com.yubico.webauthn;
import com.fasterxml.jackson.databind.JsonNode;
-import com.yubico.internal.util.CertificateParser;
-import com.yubico.internal.util.ExceptionUtil;
-import com.yubico.internal.util.WebAuthnCodecs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.yubico.webauthn.data.ByteArray;
import com.yubico.webauthn.data.exception.Base64UrlException;
import com.yubico.webauthn.extension.appid.AppId;
@@ -36,28 +35,49 @@
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
public class U2fVerifier {
- private static final BouncyCastleCrypto crypto = new BouncyCastleCrypto();
+ private static final ObjectMapper jsonMapper = new ObjectMapper().registerModule(new Jdk8Module());
+
+ private static ByteArray hash(String data) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ return new ByteArray(digest.digest(data.getBytes()));
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static ByteArray hash(ByteArray data) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ return new ByteArray(digest.digest(data.getBytes()));
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ }
public static boolean verify(AppId appId, RegistrationRequest request, U2fRegistrationResponse response) throws CertificateException, IOException, Base64UrlException {
- final ByteArray appIdHash = crypto.hash(appId.getId());
- final ByteArray clientDataHash = crypto.hash(response.getCredential().getU2fResponse().getClientDataJSON());
+ final ByteArray appIdHash = hash(appId.getId());
+ final ByteArray clientDataHash = hash(response.getCredential().getU2fResponse().getClientDataJSON());
- final JsonNode clientData = WebAuthnCodecs.json().readTree(response.getCredential().getU2fResponse().getClientDataJSON().getBytes());
+ final JsonNode clientData = jsonMapper.readTree(response.getCredential().getU2fResponse().getClientDataJSON().getBytes());
final String challengeBase64 = clientData.get("challenge").textValue();
- ExceptionUtil.assure(
- request.getPublicKeyCredentialCreationOptions().getChallenge().equals(ByteArray.fromBase64Url(challengeBase64)),
- "Wrong challenge."
- );
+ if (!request.getPublicKeyCredentialCreationOptions().getChallenge().equals(ByteArray.fromBase64Url(challengeBase64))) {
+ throw new IllegalArgumentException("Wrong challenge.");
+ }
InputStream attestationCertAndSignatureStream = new ByteArrayInputStream(response.getCredential().getU2fResponse().getAttestationCertAndSignature().getBytes());
- final X509Certificate attestationCert = CertificateParser.parseDer(attestationCertAndSignatureStream);
+ CertificateFactory cf = CertificateFactory.getInstance("X.509");
+ final X509Certificate attestationCert = (X509Certificate) cf.generateCertificate(attestationCertAndSignatureStream);
byte[] signatureBytes = new byte[attestationCertAndSignatureStream.available()];
attestationCertAndSignatureStream.read(signatureBytes);
diff --git a/4_Authentication/complete/src/main/java/com/yubico/webauthn/attestation/resolver/SimpleTrustResolverWithEquality.java b/4_Authentication/complete/src/main/java/com/yubico/webauthn/attestation/resolver/SimpleTrustResolverWithEquality.java
deleted file mode 100644
index 8bb5d8d..0000000
--- a/4_Authentication/complete/src/main/java/com/yubico/webauthn/attestation/resolver/SimpleTrustResolverWithEquality.java
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright (c) 2018, Yubico AB
-// All rights reserved.
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are met:
-//
-// 1. Redistributions of source code must retain the above copyright notice, this
-// list of conditions and the following disclaimer.
-//
-// 2. Redistributions in binary form must reproduce the above copyright notice,
-// this list of conditions and the following disclaimer in the documentation
-// and/or other materials provided with the distribution.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-package com.yubico.webauthn.attestation.resolver;
-
-import com.google.common.collect.ArrayListMultimap;
-import com.google.common.collect.Multimap;
-import com.yubico.webauthn.attestation.TrustResolver;
-import java.security.cert.X509Certificate;
-import java.util.Collection;
-import java.util.List;
-import java.util.Optional;
-
-/**
- * Resolves a metadata object whose associated certificate has signed the
- * argument certificate, or is equal to the argument certificate.
- */
-public class SimpleTrustResolverWithEquality implements TrustResolver {
-
- private final SimpleTrustResolver subresolver;
- private final Multimap trustedCerts = ArrayListMultimap.create();
-
- public SimpleTrustResolverWithEquality(Collection trustedCertificates) {
- subresolver = new SimpleTrustResolver(trustedCertificates);
-
- for (X509Certificate cert : trustedCertificates) {
- trustedCerts.put(cert.getSubjectDN().getName(), cert);
- }
- }
-
- @Override
- public Optional resolveTrustAnchor(X509Certificate attestationCertificate, List caCertificateChain) {
- Optional subResult = subresolver.resolveTrustAnchor(attestationCertificate, caCertificateChain);
-
- if (subResult.isPresent()) {
- return subResult;
- } else {
- for (X509Certificate cert : trustedCerts.get(attestationCertificate.getSubjectDN().getName())) {
- if (cert.equals(attestationCertificate)) {
- return Optional.of(cert);
- }
- }
-
- return Optional.empty();
- }
- }
-
-}
diff --git a/4_Authentication/complete/src/test/java/com/example/demo/DemoApplicationTests.java b/4_Authentication/complete/src/test/java/com/example/demo/DemoApplicationTests.java
index b76e7f2..1db698f 100644
--- a/4_Authentication/complete/src/test/java/com/example/demo/DemoApplicationTests.java
+++ b/4_Authentication/complete/src/test/java/com/example/demo/DemoApplicationTests.java
@@ -1,11 +1,8 @@
package com.example.demo;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.test.context.junit4.SpringRunner;
-@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..a0dac3f
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,26 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
+
+## [Unreleased]
+
+### Changed
+- Updated `java-webauthn-server` dependency from 1.x to 2.9.0
+- Migrated to java-webauthn-server 2.x API following official migration guide
+- Updated minimum Java requirement from JDK 1.8 to JDK 17
+- Updated Spring Boot to 2.7.18 for Java 17 compatibility
+- Replaced deprecated `RelyingParty` builder patterns with 2.x equivalents
+- Removed deprecated `icon` property from `RelyingPartyIdentity` (no longer supported in 2.x)
+- Simplified attestation and assertion verification using new 2.x APIs
+- Updated `CredentialRegistration` to use `RegisteredCredential.builder()` pattern
+- Removed deprecated `SimpleTrustResolverWithEquality` class (trust anchors handled differently in 2.x)
+- Fixed README typo: "also know" → "also known"
+- Updated README developer video links to working YouTube playlist
+- Removed outdated trust store references from README (aligns with 2.x architecture changes)
+
+### Fixed
+- Fixed compilation errors from deprecated 1.x APIs
+- Fixed test configuration for Spring Boot 2.7.x
+- Fixed Dockerfile configurations to use correct Java version
diff --git a/README.md b/README.md
index 30b7743..d565fc7 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@ You'll start with a java web application that secures access to a page with a lo
## What you'll need:
* [Git](https://git-scm.com/)
* [Docker](https://docs.docker.com/install/)
-* [JDK 1.8](https://www.oracle.com/technetwork/java/javase/downloads/index.html) or later
+* [JDK 17](https://www.oracle.com/technetwork/java/javase/downloads/index.html) or later
* [Maven 3.2](https://maven.apache.org/download.cgi)+
* FIDO2 Compatible browser
* MacOS: [Safari Technical Preview](https://developer.apple.com/safari/technology-preview/) version 71+
@@ -51,7 +51,7 @@ The authenticator makes credentials, generates cryptographic proof of user authe
The client is the bridge between the authenticator and the server. It implements the FIDO2 Client to Authenticator Protocol (CTAP) and the WebAuthn API. The client could be a browser exposing the WebAuthn API to web applications, or an OS subsystem exposing a platform-specific FIDO2 API to native applications such as mobile or desktop apps.
**Server:**
-The server, also know as Relying Party or RP, consists conceptually of at least a web server and the server-side portions of a web application, plus a WebAuthn server. The WebAuthn server has a trust store, containing the (public) trust anchors for the attestation of FIDO2 Authenticators. Note: a trust store is needed only if RP cares about attestation metadata.
+The server, also known as Relying Party or RP, consists conceptually of at least a web server and the server-side portions of a web application, plus a WebAuthn server. The WebAuthn server provides core WebAuthn registration and authentication functionality.
See the diagram below for a depiction of the server architecture

@@ -60,7 +60,7 @@ See the diagram below for a depiction of the server architecture
If you need more resources to understand WebAuthn and FIDO2 then
* Read the [WebAuthn Developer Guide](https://developers.yubico.com/WebAuthn/WebAuthn_Developer_Guide/)
* Review the [Java WebAuthn Server Library Code](https://github.com/Yubico/java-webauthn-server)
-* Watch the [Developer Videos](https://www.yubico.com/why-yubico/for-developers/developer-videos/)
+* Watch the [Developer Videos](https://www.youtube.com/playlist?list=PL1n2DPbVwGA3mS23rC5jm5jGTnbU4E60L)
# Modules
This workshop is split into multiple modules. Each module builds upon the previous module as you expand the application. You must complete each module before proceeding to the next.
diff --git a/initial/getLibs.sh b/initial/getLibs.sh
index 154e048..fb19e2d 100755
--- a/initial/getLibs.sh
+++ b/initial/getLibs.sh
@@ -7,7 +7,7 @@ cd tmp
git clone https://github.com/Yubico/java-webauthn-server.git
git fetch https://github.com/Yubico/java-webauthn-server.git
-git -C java-webauthn-server checkout tags/1.2.0
+git -C java-webauthn-server checkout tags/2.9.0
# Copy the webauthn-server-demo data package to the project
cp -r java-webauthn-server/webauthn-server-demo/src/main/java/demo/webauthn/data ../src/main/java/com/example/demo
diff --git a/initial/pom.xml b/initial/pom.xml
index 72c1c17..f78fb08 100644
--- a/initial/pom.xml
+++ b/initial/pom.xml
@@ -4,7 +4,7 @@
org.springframework.bootspring-boot-starter-parent
- 2.1.4.RELEASE
+ 2.7.18com.example
@@ -14,7 +14,7 @@
Demo project for Spring Boot
- 1.8
+ 17
@@ -80,7 +80,7 @@
${REGION}
- jre8
+ java17
diff --git a/initial/src/test/java/com/example/demo/DemoApplicationTests.java b/initial/src/test/java/com/example/demo/DemoApplicationTests.java
index b76e7f2..1db698f 100644
--- a/initial/src/test/java/com/example/demo/DemoApplicationTests.java
+++ b/initial/src/test/java/com/example/demo/DemoApplicationTests.java
@@ -1,11 +1,8 @@
package com.example.demo;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.test.context.junit4.SpringRunner;
-@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {