Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updated webauthn code #1470

Open
wants to merge 1 commit into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading