Skip to content

Commit ed82ee0

Browse files
committed
Add demo API endpoint for finishing a U2F registration
1 parent 4d24739 commit ed82ee0

File tree

5 files changed

+186
-1
lines changed

5 files changed

+186
-1
lines changed

webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnRestResource.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ private StartRegistrationResponse(RegistrationRequest request) throws MalformedU
115115
}
116116
private final class StartRegistrationActions {
117117
public final URL finish = uriInfo.getAbsolutePathBuilder().path("finish").build().toURL();
118+
public final URL finishU2f = uriInfo.getAbsolutePathBuilder().path("finish-u2f").build().toURL();
118119
private StartRegistrationActions() throws MalformedURLException {
119120
}
120121
}
@@ -158,6 +159,19 @@ public Response finishRegistration(@NonNull String responseJson) {
158159
);
159160
}
160161

162+
@Path("register/finish-u2f")
163+
@POST
164+
public Response finishU2fRegistration(@NonNull String responseJson) {
165+
logger.trace("finishRegistration responseJson: {}", responseJson);
166+
Either<List<String>, WebAuthnServer.SuccessfulU2fRegistrationResult> result = server.insecureFinishU2fRegistration(responseJson);
167+
return finishResponse(
168+
result,
169+
"U2F registration failed; further error message(s) were unfortunately lost to an internal server error.",
170+
"finishU2fRegistration",
171+
responseJson
172+
);
173+
}
174+
161175
private final class StartAuthenticationResponse {
162176
public final boolean success = true;
163177
public final AssertionRequest request;

webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import com.yubico.webauthn.RelyingParty;
1818
import com.yubico.webauthn.StartAssertionOptions;
1919
import com.yubico.webauthn.StartRegistrationOptions;
20+
import com.yubico.webauthn.attestation.Attestation;
2021
import com.yubico.webauthn.attestation.MetadataResolver;
2122
import com.yubico.webauthn.attestation.MetadataService;
2223
import com.yubico.webauthn.attestation.StandardMetadataService;
@@ -25,7 +26,9 @@
2526
import com.yubico.webauthn.attestation.resolver.SimpleResolverWithEquality;
2627
import com.yubico.webauthn.data.AssertionResult;
2728
import com.yubico.webauthn.data.AttestationConveyancePreference;
29+
import com.yubico.webauthn.data.AttestationType;
2830
import com.yubico.webauthn.data.ByteArray;
31+
import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
2932
import com.yubico.webauthn.data.PublicKeyCredentialParameters;
3033
import com.yubico.webauthn.data.RegistrationResult;
3134
import com.yubico.webauthn.data.RelyingPartyIdentity;
@@ -39,9 +42,11 @@
3942
import demo.webauthn.data.CredentialRegistration;
4043
import demo.webauthn.data.RegistrationRequest;
4144
import demo.webauthn.data.RegistrationResponse;
45+
import demo.webauthn.data.U2fRegistrationResponse;
4246
import java.io.IOException;
4347
import java.io.InputStream;
4448
import java.io.InputStreamReader;
49+
import java.security.cert.CertificateEncodingException;
4550
import java.security.cert.CertificateException;
4651
import java.security.cert.X509Certificate;
4752
import java.time.Clock;
@@ -230,6 +235,16 @@ public SuccessfulRegistrationResult(RegistrationRequest request, RegistrationRes
230235
}
231236
}
232237

238+
@Value
239+
public class SuccessfulU2fRegistrationResult {
240+
final boolean success = true;
241+
final RegistrationRequest request;
242+
final U2fRegistrationResponse response;
243+
final CredentialRegistration registration;
244+
boolean attestationTrusted;
245+
Optional<AttestationCertInfo> attestationCert;
246+
}
247+
233248
@Value
234249
public static class AttestationCertInfo {
235250
final ByteArray der;
@@ -298,6 +313,76 @@ public Either<List<String>, SuccessfulRegistrationResult> finishRegistration(Str
298313
}
299314
}
300315

316+
/**
317+
* NOTE: This method does not validate the result. This sole purpose of
318+
* this feature is to enable testing that the "appid" extension works. This
319+
* requires a credential registered via the U2F API instead of the WebAuthn
320+
* API. Since WebAuthn is backwards compatible only with U2F
321+
* authentication, not registration, this method is used to sidestep the
322+
* WebAuthn module and just add the credential to the database of
323+
* registrations.
324+
*
325+
* This is NOT an example of good code. DO NOT base any code off this as an
326+
* example.
327+
*/
328+
public Either<List<String>, SuccessfulU2fRegistrationResult> insecureFinishU2fRegistration(String responseJson) {
329+
logger.trace("insecureFinishU2fRegistration responseJson: {}", responseJson);
330+
U2fRegistrationResponse response = null;
331+
try {
332+
response = jsonMapper.readValue(responseJson, U2fRegistrationResponse.class);
333+
} catch (IOException e) {
334+
logger.error("JSON error in insecureFinishU2fRegistration; responseJson: {}", responseJson, e);
335+
return Either.left(Arrays.asList("Registration failed!", "Failed to decode response object.", e.getMessage()));
336+
}
337+
338+
RegistrationRequest request = registerRequestStorage.getIfPresent(response.getRequestId());
339+
registerRequestStorage.invalidate(response.getRequestId());
340+
341+
if (request == null) {
342+
logger.debug("fail insecureFinishU2fRegistration responseJson: {}", responseJson);
343+
return Either.left(Arrays.asList("Registration failed!", "No such registration in progress."));
344+
} else {
345+
X509Certificate attestationCert = null;
346+
try {
347+
attestationCert = CertificateParser.parseDer(response.getCredential().getU2fResponse().getAttestationCert().getBytes());
348+
} catch (CertificateException e) {
349+
logger.error("Failed to parse attestation certificate: {}", response.getCredential().getU2fResponse().getAttestationCert(), e);
350+
}
351+
352+
Optional<Attestation> attestation = Optional.empty();
353+
try {
354+
if (attestationCert != null) {
355+
attestation = Optional.of(metadataService.getAttestation(Collections.singletonList(attestationCert)));
356+
}
357+
} catch (CertificateEncodingException e) {
358+
logger.error("Failed to resolve attestation", e);
359+
}
360+
361+
final RegistrationResult result = RegistrationResult.builder()
362+
.attestationMetadata(attestation)
363+
.attestationTrusted(attestation.map(Attestation::isTrusted).orElse(false))
364+
.attestationType(AttestationType.BASIC)
365+
.keyId(PublicKeyCredentialDescriptor.builder().id(response.getCredential().getU2fResponse().getKeyHandle()).build())
366+
.publicKeyCose(WebAuthnCodecs.rawEcdaKeyToCose(response.getCredential().getU2fResponse().getPublicKey()))
367+
.build();
368+
369+
return Either.right(
370+
new SuccessfulU2fRegistrationResult(
371+
request,
372+
response,
373+
addRegistration(
374+
request.getPublicKeyCredentialCreationOptions().getUser(),
375+
request.getCredentialNickname(),
376+
0,
377+
result
378+
),
379+
result.isAttestationTrusted(),
380+
Optional.of(new AttestationCertInfo(response.getCredential().getU2fResponse().getAttestationCert()))
381+
)
382+
);
383+
}
384+
}
385+
301386
public Either<List<String>, AssertionRequest> startAuthentication(Optional<String> username) {
302387
logger.trace("startAuthentication username: {}", username);
303388

@@ -457,13 +542,27 @@ private CredentialRegistration addRegistration(
457542
Optional<String> nickname,
458543
RegistrationResponse response,
459544
RegistrationResult registration
545+
) {
546+
return addRegistration(
547+
userIdentity,
548+
nickname,
549+
response.getCredential().getResponse().getAttestation().getAuthenticatorData().getSignatureCounter(),
550+
registration
551+
);
552+
}
553+
554+
private CredentialRegistration addRegistration(
555+
UserIdentity userIdentity,
556+
Optional<String> nickname,
557+
long signatureCount,
558+
RegistrationResult registration
460559
) {
461560
CredentialRegistration reg = CredentialRegistration.builder()
462561
.userIdentity(userIdentity)
463562
.credentialNickname(nickname)
464563
.registrationTime(clock.instant())
465564
.registration(registration)
466-
.signatureCount(response.getCredential().getResponse().getAttestation().getAuthenticatorData().getSignatureCounter())
565+
.signatureCount(signatureCount)
467566
.build();
468567

469568
logger.debug(
@@ -477,4 +576,5 @@ private CredentialRegistration addRegistration(
477576
userStorage.addRegistrationByUsername(userIdentity.getName(), reg);
478577
return reg;
479578
}
579+
480580
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package demo.webauthn.data;
2+
3+
import com.fasterxml.jackson.annotation.JsonCreator;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
import lombok.NonNull;
6+
import lombok.Value;
7+
8+
@Value
9+
public class U2fCredential {
10+
11+
private final U2fCredentialResponse u2fResponse;
12+
13+
@JsonCreator
14+
public U2fCredential(
15+
@NonNull @JsonProperty("u2fResponse") U2fCredentialResponse u2fResponse
16+
) {
17+
this.u2fResponse = u2fResponse;
18+
}
19+
20+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package demo.webauthn.data;
2+
3+
import com.fasterxml.jackson.annotation.JsonCreator;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
import com.yubico.webauthn.data.ByteArray;
6+
import lombok.NonNull;
7+
import lombok.Value;
8+
9+
@Value
10+
public class U2fCredentialResponse {
11+
12+
private final ByteArray keyHandle;
13+
private final ByteArray publicKey;
14+
private final ByteArray attestationCert;
15+
16+
@JsonCreator
17+
public U2fCredentialResponse(
18+
@NonNull @JsonProperty("keyHandle") ByteArray keyHandle,
19+
@NonNull@JsonProperty("publicKey") ByteArray publicKey,
20+
@NonNull@JsonProperty("attestationCert") ByteArray attestationCert
21+
) {
22+
this.keyHandle = keyHandle;
23+
this.publicKey = publicKey;
24+
this.attestationCert = attestationCert;
25+
}
26+
27+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package demo.webauthn.data;
2+
3+
import com.fasterxml.jackson.annotation.JsonCreator;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
import com.yubico.webauthn.data.ByteArray;
6+
import lombok.NonNull;
7+
import lombok.Value;
8+
9+
@Value
10+
public class U2fRegistrationResponse {
11+
12+
private final ByteArray requestId;
13+
private final U2fCredential credential;
14+
15+
@JsonCreator
16+
public U2fRegistrationResponse(
17+
@NonNull @JsonProperty("requestId") ByteArray requestId,
18+
@NonNull @JsonProperty("credential") U2fCredential credential
19+
) {
20+
this.requestId = requestId;
21+
this.credential = credential;
22+
}
23+
24+
}

0 commit comments

Comments
 (0)