Skip to content

Commit

Permalink
Updated webauthn code
Browse files Browse the repository at this point in the history
  • Loading branch information
FroMage committed Nov 14, 2024
1 parent 9be2239 commit a4da57a
Show file tree
Hide file tree
Showing 17 changed files with 262 additions and 401 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
package org.acme.security.webauthn;

import org.jboss.resteasy.reactive.RestForm;

import io.quarkus.security.webauthn.WebAuthnCredentialRecord;
import io.quarkus.security.webauthn.WebAuthnLoginResponse;
import io.quarkus.security.webauthn.WebAuthnRegisterResponse;
import io.quarkus.security.webauthn.WebAuthnSecurity;
import io.vertx.ext.web.RoutingContext;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.BeanParam;
Expand All @@ -8,14 +15,6 @@
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;

import org.jboss.resteasy.reactive.RestForm;

import io.quarkus.security.webauthn.WebAuthnLoginResponse;
import io.quarkus.security.webauthn.WebAuthnRegisterResponse;
import io.quarkus.security.webauthn.WebAuthnSecurity;
import io.vertx.ext.auth.webauthn.Authenticator;
import io.vertx.ext.web.RoutingContext;

@Path("")
public class LoginResource {

Expand All @@ -25,25 +24,24 @@ public class LoginResource {
@Path("/login")
@POST
@Transactional
public Response login(@RestForm String userName,
@BeanParam WebAuthnLoginResponse webAuthnResponse,
public Response login(@BeanParam WebAuthnLoginResponse webAuthnResponse,
RoutingContext ctx) {
// Input validation
if(userName == null || userName.isEmpty() || !webAuthnResponse.isSet() || !webAuthnResponse.isValid()) {
if(!webAuthnResponse.isSet() || !webAuthnResponse.isValid()) {
return Response.status(Status.BAD_REQUEST).build();
}

User user = User.findByUserName(userName);
if(user == null) {
// Invalid user
return Response.status(Status.BAD_REQUEST).build();
}
try {
Authenticator authenticator = this.webAuthnSecurity.login(webAuthnResponse, ctx).await().indefinitely();
WebAuthnCredentialRecord credentialRecord = this.webAuthnSecurity.login(webAuthnResponse, ctx).await().indefinitely();
User user = User.findByUserName(credentialRecord.getUserName());
if(user == null) {
// Invalid user
return Response.status(Status.BAD_REQUEST).build();
}
// bump the auth counter
user.webAuthnCredential.counter = authenticator.getCounter();
user.webAuthnCredential.counter = credentialRecord.getCounter();
// make a login cookie
this.webAuthnSecurity.rememberUser(authenticator.getUserName(), ctx);
this.webAuthnSecurity.rememberUser(credentialRecord.getUserName(), ctx);
return Response.ok().build();
} catch (Exception exception) {
// handle login failure - make a proper error response
Expand All @@ -69,10 +67,10 @@ public Response register(@RestForm String userName,
}
try {
// store the user
Authenticator authenticator = this.webAuthnSecurity.register(webAuthnResponse, ctx).await().indefinitely();
WebAuthnCredentialRecord credentialRecord = this.webAuthnSecurity.register(webAuthnResponse, ctx).await().indefinitely();
User newUser = new User();
newUser.userName = authenticator.getUserName();
WebAuthnCredential credential = new WebAuthnCredential(authenticator, newUser);
newUser.userName = credentialRecord.getUserName();
WebAuthnCredential credential = new WebAuthnCredential(credentialRecord, newUser);
credential.persist();
newUser.persist();
// make a login cookie
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,76 +3,52 @@
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import io.smallrye.common.annotation.Blocking;
import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.security.webauthn.WebAuthnCredentialRecord;
import io.quarkus.security.webauthn.WebAuthnUserProvider;
import io.smallrye.common.annotation.Blocking;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.auth.webauthn.AttestationCertificates;
import io.vertx.ext.auth.webauthn.Authenticator;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.transaction.Transactional;

import static org.acme.security.webauthn.WebAuthnCredential.findByCredID;
import static org.acme.security.webauthn.WebAuthnCredential.findByUserName;

@Blocking
@ApplicationScoped
public class MyWebAuthnSetup implements WebAuthnUserProvider {

@Transactional
@Override
public Uni<List<Authenticator>> findWebAuthnCredentialsByUserName(String userName) {
return Uni.createFrom().item(toAuthenticators(findByUserName(userName)));
public Uni<List<WebAuthnCredentialRecord>> findByUserName(String userId) {
return Uni.createFrom().item(WebAuthnCredential.findByUserName(userId).stream().map(WebAuthnCredential::toWebAuthnCredentialRecord).toList());
}

@Transactional
@Override
public Uni<List<Authenticator>> findWebAuthnCredentialsByCredID(String credID) {
return Uni.createFrom().item(toAuthenticators(findByCredID(credID)));
public Uni<WebAuthnCredentialRecord> findByCredentialId(String credId) {
WebAuthnCredential creds = WebAuthnCredential.findByCredentialId(credId);
if(creds == null)
return Uni.createFrom().failure(new RuntimeException("No such credential ID"));
return Uni.createFrom().item(creds.toWebAuthnCredentialRecord());
}

@Transactional
@Override
public Uni<Void> updateOrStoreWebAuthnCredentials(Authenticator authenticator) {
// leave the scooby user to the manual endpoint, because if we do it here it will be created/updated twice
if(!authenticator.getUserName().equals("scooby")) {
User user = User.findByUserName(authenticator.getUserName());
if(user == null) {
// new user
User newUser = new User();
newUser.userName = authenticator.getUserName();
WebAuthnCredential credential = new WebAuthnCredential(authenticator, newUser);
credential.persist();
newUser.persist();
} else {
// existing user
user.webAuthnCredential.counter = authenticator.getCounter();
}
}
return Uni.createFrom().nullItem();
public Uni<Void> store(WebAuthnCredentialRecord credentialRecord) {
User newUser = new User();
newUser.userName = credentialRecord.getUserName();
WebAuthnCredential credential = new WebAuthnCredential(credentialRecord, newUser);
credential.persist();
newUser.persist();
return Uni.createFrom().voidItem();
}

private static List<Authenticator> toAuthenticators(List<WebAuthnCredential> dbs) {
return dbs.stream().map(MyWebAuthnSetup::toAuthenticator).collect(Collectors.toList());
@Transactional
@Override
public Uni<Void> update(String credentialId, long counter) {
WebAuthnCredential credential = WebAuthnCredential.findByCredentialId(credentialId);
credential.counter = counter;
return Uni.createFrom().voidItem();
}

private static Authenticator toAuthenticator(WebAuthnCredential credential) {
Authenticator ret = new Authenticator();
ret.setAaguid(credential.aaguid);
AttestationCertificates attestationCertificates = new AttestationCertificates();
attestationCertificates.setAlg(credential.alg);
ret.setAttestationCertificates(attestationCertificates);
ret.setCounter(credential.counter);
ret.setCredID(credential.credID);
ret.setFmt(credential.fmt);
ret.setPublicKey(credential.publicKey);
ret.setType(credential.type);
ret.setUserName(credential.userName);
return ret;
}

@Override
public Set<String> getRoles(String userId) {
if(userId.equals("admin")) {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,72 +1,25 @@
package org.acme.security.webauthn;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
import io.quarkus.security.webauthn.WebAuthnCredentialRecord;
import io.quarkus.security.webauthn.WebAuthnCredentialRecord.RequiredPersistedData;
import jakarta.persistence.Entity;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Id;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;

import io.quarkus.hibernate.orm.panache.PanacheEntity;
import io.vertx.ext.auth.webauthn.Authenticator;
import io.vertx.ext.auth.webauthn.PublicKeyCredential;

@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"userName", "credID"}))
@Entity
public class WebAuthnCredential extends PanacheEntity {
public class WebAuthnCredential extends PanacheEntityBase {

/**
* The username linked to this authenticator
*/
public String userName;

/**
* The type of key (must be "public-key")
*/
public String type = "public-key";

/**
* The non user identifiable id for the authenticator
*/
public String credID;
@Id
public String credentialId;

/**
* The public key associated with this authenticator
*/
public String publicKey;

/**
* The signature counter of the authenticator to prevent replay attacks
*/
public byte[] publicKey;
public long publicKeyAlgorithm;
public long counter;

public String aaguid;

/**
* The Authenticator attestation certificates object, a JSON like:
* <pre>{@code
* {
* "alg": "string",
* "x5c": [
* "base64"
* ]
* }
* }</pre>
*/
/**
* The algorithm used for the public credential
*/
public PublicKeyCredential alg;

/**
* The list of X509 certificates encoded as base64url.
*/
@OneToMany(mappedBy = "webAuthnCredential")
public List<WebAuthnCertificate> x5c = new ArrayList<>();

public String fmt;
public UUID aaguid;

// owning side
@OneToOne
Expand All @@ -75,34 +28,28 @@ public class WebAuthnCredential extends PanacheEntity {
public WebAuthnCredential() {
}

public WebAuthnCredential(Authenticator authenticator, User user) {
aaguid = authenticator.getAaguid();
if(authenticator.getAttestationCertificates() != null)
alg = authenticator.getAttestationCertificates().getAlg();
counter = authenticator.getCounter();
credID = authenticator.getCredID();
fmt = authenticator.getFmt();
publicKey = authenticator.getPublicKey();
type = authenticator.getType();
userName = authenticator.getUserName();
if(authenticator.getAttestationCertificates() != null
&& authenticator.getAttestationCertificates().getX5c() != null) {
for (String x5c : authenticator.getAttestationCertificates().getX5c()) {
WebAuthnCertificate cert = new WebAuthnCertificate();
cert.x5c = x5c;
cert.webAuthnCredential = this;
this.x5c.add(cert);
}
}
public WebAuthnCredential(WebAuthnCredentialRecord credentialRecord, User user) {
RequiredPersistedData requiredPersistedData = credentialRecord.getRequiredPersistedData();
aaguid = requiredPersistedData.aaguid();
counter = requiredPersistedData.counter();
credentialId = requiredPersistedData.credentialId();
publicKey = requiredPersistedData.publicKey();
publicKeyAlgorithm = requiredPersistedData.publicKeyAlgorithm();
this.user = user;
user.webAuthnCredential = this;
}

public WebAuthnCredentialRecord toWebAuthnCredentialRecord() {
return WebAuthnCredentialRecord
.fromRequiredPersistedData(
new RequiredPersistedData(user.userName, credentialId, aaguid, publicKey, publicKeyAlgorithm, counter));
}

public static List<WebAuthnCredential> findByUserName(String userName) {
return list("userName", userName);
return list("user.userName", userName);
}

public static List<WebAuthnCredential> findByCredID(String credID) {
return list("credID", credID);
public static WebAuthnCredential findByCredentialId(String credentialId) {
return findById(credentialId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ <h1>Status</h1>
<div class="item">
<h1>Login</h1>
<p>
<input id="userNameLogin" placeholder="User name"/><br/>
<button id="login">Login</button>
</p>
</div>
Expand All @@ -73,11 +72,7 @@ <h1>Register</h1>
</div>
</div>
<script type="text/javascript">
const webAuthn = new WebAuthn({
callbackPath: '/q/webauthn/callback',
registerPath: '/q/webauthn/register',
loginPath: '/q/webauthn/login'
});
const webAuthn = new WebAuthn();

const result = document.getElementById('result');

Expand All @@ -88,10 +83,11 @@ <h1>Register</h1>
const loginButton = document.getElementById('login');

loginButton.addEventListener("click", (e) => {
var userName = document.getElementById('userNameLogin').value;
result.replaceChildren();
webAuthn.login({ name: userName })
.then(body => {
webAuthn.login()
.then(x => fetch('/api/public/me'))
.then(response => response.text())
.then(userName => {
result.append("User: "+userName);
})
.catch(err => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@

quarkus.hibernate-orm.database.generation=drop-and-create

quarkus.webauthn.login-page=/
quarkus.webauthn.login-page=/
quarkus.webauthn.enable-login-endpoint=true
quarkus.webauthn.enable-registration-endpoint=true
Loading

0 comments on commit a4da57a

Please sign in to comment.