WIP webauthn

This commit is contained in:
Dusan Jakub
2023-09-25 16:55:29 +02:00
parent 9a0c3988e2
commit f4d2ac3899
4 changed files with 446 additions and 41 deletions

View File

@@ -0,0 +1,56 @@
package com.ysoft.geecon.webauthn;
import com.ysoft.geecon.repo.UsersRepo;
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;
import jakarta.inject.Inject;
import jakarta.ws.rs.BeanParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import java.net.URI;
@Path("webauthn")
public class LoginResource {
@Inject
WebAuthnSecurity webAuthnSecurity;
@Inject
UsersRepo usersRepo;
@Path("/login")
@POST
public Response login(@BeanParam WebAuthnLoginResponse webAuthnResponse,
RoutingContext ctx) {
// Input validation
if (!webAuthnResponse.isSet()
|| !webAuthnResponse.isValid()) {
return Response.status(Status.BAD_REQUEST).build();
}
Authenticator authenticator = this.webAuthnSecurity.login(webAuthnResponse, ctx)
.await().indefinitely();
return Response.seeOther(URI.create("/")).build();
}
@Path("/register")
@POST
public Response register(@BeanParam WebAuthnRegisterResponse webAuthnResponse,
RoutingContext ctx) {
// Input validation
if (!webAuthnResponse.isSet() || !webAuthnResponse.isValid()) {
return Response.status(Status.BAD_REQUEST).build();
}
Authenticator authenticator = this.webAuthnSecurity.register(webAuthnResponse, ctx)
.await().indefinitely();
return Response.seeOther(URI.create("/")).build();
}
}

View File

@@ -6,6 +6,7 @@ import io.quarkus.security.webauthn.WebAuthnUserProvider;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.auth.webauthn.AttestationCertificates;
import io.vertx.ext.auth.webauthn.Authenticator;
import io.vertx.ext.web.RoutingContext;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@@ -13,9 +14,13 @@ import java.util.List;
@ApplicationScoped
public class MyWebAuthnSetup implements WebAuthnUserProvider {
public static final String AUTHORIZED_USER = MyWebAuthnSetup.class.getPackageName() + "#AUTHORIZED_USER";
@Inject
UsersRepo usersRepo;
@Inject
RoutingContext routingContext;
private static List<Authenticator> toAuthenticators(List<WebAuthnCredential> dbs) {
return dbs.stream().map(MyWebAuthnSetup::toAuthenticator).toList();
}
@@ -69,6 +74,9 @@ public class MyWebAuthnSetup implements WebAuthnUserProvider {
usersRepo.register(new User(authenticator.getUserName(), null, List.of(credential1)));
return Uni.createFrom().nullItem();
} else {
if (routingContext.get(AUTHORIZED_USER) != null) {
return Uni.createFrom().nullItem();
}
// returning (or duplicate) user with new credential -> reject,
// as we do not provide a means to register additional credentials yet
return Uni.createFrom().failure(new Throwable("Duplicate user"));

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<title>Login</title>
<script charset="UTF-8" src="/q/webauthn/webauthn.js" type="text/javascript"></script>
<script charset="UTF-8" src="/js/webauthn-debug.js" type="text/javascript"></script>
<style>
.container {
display: grid;
@@ -45,59 +45,126 @@
</head>
<body>
<nav>
<ul>
<li><a href="/api/public">Public API</a></li>
<li><a href="/api/users/me">User API</a></li>
<li><a href="/api/admin">Admin API</a></li>
<li><a href="/q/webauthn/logout">Logout</a></li>
</nav>
<div class="container">
<div class="item">
<h1>Status</h1>
<div id="result"></div>
</div>
<div class="item">
<h1>Login</h1>
<p>
<input id="userNameLogin" placeholder="User name"/><br/>
<button id="login">Login</button>
</p>
</div>
<div class="item">
<h1>Register</h1>
<p>
<input id="userNameRegister" placeholder="User name"/><br/>
<input id="firstName" placeholder="First name"/><br/>
<input id="lastName" placeholder="Last name"/><br/>
<button id="register">Register</button>
</p>
</div>
<input id="userName" placeholder="User name"/><br/>
<button id="login">Login</button>
<button id="register">Register</button>
</div>
<div class="container" id="server1">
<div id="server1-call"></div>
<div id="server1-response"></div>
</div>
<div class="container" id="navigator">
<div id="navigator-call"></div>
<div id="navigator-response"></div>
</div>
<div class="container" id="server2">
<div id="server2-call"></div>
<div id="server2-response"></div>
</div>
<div id="result"></div>
<form action="#" method="POST">
<input id="webAuthnId" name="webAuthnId" placeholder="webAuthnId"/><br/>
<input id="webAuthnRawId" name="webAuthnRawId" placeholder="webAuthnRawId"/><br/>
<input id="webAuthnResponseAttestationObject" name="webAuthnResponseAttestationObject"
placeholder="webAuthnResponseAttestationObject"/><br/>
<input id="webAuthnResponseClientDataJSON" name="webAuthnResponseClientDataJSON"
placeholder="webAuthnResponseClientDataJSON"/><br/>
<div id="webAuthnResponseClientDataJSONJson"></div>
<br/>
<input id="webAuthnResponseAuthenticatorData" name="webAuthnResponseAuthenticatorData"
placeholder="webAuthnResponseAuthenticatorData"/><br/>
<div id="webAuthnResponseAuthenticatorDataJson"></div>
<br/>
<input id="webAuthnResponseSignature" name="webAuthnResponseSignature"
placeholder="webAuthnResponseSignature"/><br/>
<div id="webAuthnResponseSignatureJson"></div>
<br/>
<input id="webAuthnResponseUserHandle" name="webAuthnResponseUserHandle"
placeholder="webAuthnResponseUserHandle"/><br/>
<div id="webAuthnResponseUserHandleJson"></div>
<br/>
<input id="webAuthnType" name="webAuthnType" placeholder="webAuthnType"/><br/>
<button formaction="/webauthn/register" type="submit">Finish registration</button>
<button formaction="/webauthn/login" type="submit">Finish login</button>
</form>
<script type="text/javascript">
function tryDecodeBase64(str) {
try {
return atob(str);
} catch (e) {
return e + "";
}
}
function fillOrHideFormField(id, value) {
let el = document.getElementById(id);
if (!el) throw "No element #" + id;
el.value = value;
if (value !== undefined) {
el.style.display = "";
} else {
//el.style.display = "none";
}
}
function fillOrHideJsonField(id, value) {
let el = document.getElementById(id);
if (!el) throw "No element #" + id;
if (value !== undefined) {
el.style.display = "";
el.innerHTML = tryDecodeBase64(value);
} else {
el.style.display = "none";
}
}
const webAuthn = new WebAuthn({
callbackPath: '/q/webauthn/callback',
registerPath: '/q/webauthn/register',
loginPath: '/q/webauthn/login'
loginPath: '/q/webauthn/login',
loginCallbackPath: '/webauthn/login',
registerCallbackPath: '/webauthn/register',
});
const result = document.getElementById('result');
fetch('/api/public/me')
.then(response => response.text())
.then(name => result.append("User: " + name));
const loginButton = document.getElementById('login');
loginButton.onclick = () => {
var userName = document.getElementById('userNameLogin').value;
var userName = document.getElementById('userName').value;
result.replaceChildren();
webAuthn.login({name: userName})
webAuthn.loginOnly({name: userName})
.then(body => {
result.append("User: " + userName);
// store the registration JSON in form elements
fillOrHideFormField('webAuthnId', body.id);
fillOrHideFormField('webAuthnRawId', body.rawId);
fillOrHideFormField('webAuthnResponseClientDataJSON', body.response.clientDataJSON);
fillOrHideJsonField('webAuthnResponseClientDataJSONJson', body.response.clientDataJSON);
fillOrHideFormField('webAuthnResponseAuthenticatorData', body.response.authenticatorData);
fillOrHideFormField('webAuthnResponseSignature', body.response.signature);
fillOrHideFormField('webAuthnResponseUserHandle', body.response.userHandle);
fillOrHideFormField('webAuthnType', body.type);
// document.getElementById('webAuthnId').value = body.id;
// document.getElementById('webAuthnRawId').value = body.rawId;
// document.getElementById('webAuthnResponseClientDataJSON').value = body.response.clientDataJSON;
// document.getElementById('webAuthnResponseAuthenticatorData').value = body.response.authenticatorData;
// document.getElementById('webAuthnResponseSignature').value = body.response.signature;
// document.getElementById('webAuthnResponseUserHandle').value = body.response.userHandle;
// document.getElementById('webAuthnType').value = body.type;
})
.catch(err => {
result.append("Login failed: " + err);
console.error(err);
});
return false;
};
@@ -105,16 +172,26 @@
const registerButton = document.getElementById('register');
registerButton.onclick = () => {
var userName = document.getElementById('userNameRegister').value;
var firstName = document.getElementById('firstName').value;
var lastName = document.getElementById('lastName').value;
var userName = document.getElementById('userName').value;
// var firstName = document.getElementById('firstName').value;
// var lastName = document.getElementById('lastName').value;
result.replaceChildren();
webAuthn.register({name: userName, displayName: firstName + " " + lastName})
webAuthn.registerOnly({name: userName, displayName: userName /*firstName + " " + lastName*/})
.then(body => {
result.append("User: " + userName);
// store the registration JSON in form elements
fillOrHideFormField('webAuthnId', body.id);
fillOrHideFormField('webAuthnRawId', body.rawId);
fillOrHideFormField('webAuthnResponseAttestationObject', body.response.attestationObject);
fillOrHideFormField('webAuthnResponseClientDataJSON', body.response.clientDataJSON);
fillOrHideJsonField('webAuthnResponseClientDataJSONJson', body.response.clientDataJSON);
fillOrHideFormField('webAuthnType', body.type);
})
.catch(err => {
result.append("Registration failed: " + err);
console.error(err);
});
return false;
};

View File

@@ -0,0 +1,264 @@
"use strict";
/* Universial Module (UMD) design pattern
* https://github.com/umdjs/umd/blob/master/templates/returnExports.js
*/
(function (root, factory) {
if (typeof define === "function" && define.amd) {
// register as an AMD anonymous module
define([], factory);
} else if (typeof module === "object" && module.exports) {
// use a node.js style export
module.exports = factory();
} else {
// if this isn't running under Node or AMD, just set a global variable
root.WebAuthn = factory();
}
// the return value of this function is what becomes the AMD / CommonJS / Global export
}(this, function () { // eslint-disable-line no-invalid-this
/*
* Base64URL-ArrayBuffer
* https://github.com/herrjemand/Base64URL-ArrayBuffer
*
* Copyright (c) 2017 Yuriy Ackermann <ackermann.yuriy@gmail.com>
* Copyright (c) 2012 Niklas von Hertzen
* Licensed under the MIT license.
*/
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
// Use a lookup table to find the index.
const lookup = new Uint8Array(256);
for (let i = 0; i < chars.length; i++) {
lookup[chars.charCodeAt(i)] = i;
}
const bufferToBase64 = function (arraybuffer) {
const bytes = new Uint8Array(arraybuffer);
let i;
let len = bytes.length;
let base64url = '';
for (i = 0; i < len; i += 3) {
base64url += chars[bytes[i] >> 2];
base64url += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];
base64url += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];
base64url += chars[bytes[i + 2] & 63];
}
if ((len % 3) === 2) {
base64url = base64url.substring(0, base64url.length - 1);
} else if (len % 3 === 1) {
base64url = base64url.substring(0, base64url.length - 2);
}
return base64url;
}
const base64ToBuffer = function (base64string) {
if (base64string) {
let bufferLength = base64string.length * 0.75;
let len = base64string.length;
let i;
let p = 0;
let encoded1;
let encoded2;
let encoded3;
let encoded4;
let bytes = new Uint8Array(bufferLength);
for (i = 0; i < len; i += 4) {
encoded1 = lookup[base64string.charCodeAt(i)];
encoded2 = lookup[base64string.charCodeAt(i + 1)];
encoded3 = lookup[base64string.charCodeAt(i + 2)];
encoded4 = lookup[base64string.charCodeAt(i + 3)];
bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
}
return bytes.buffer;
}
}
/*
* WebAuthn
*
* Copyright (c) 2020 Paulo Lopes <pmlopes@gmail.com>
* Licensed under the Apache 2 license.
*
*
*/
function WebAuthn(options) {
this.registerPath = options.registerPath;
this.loginPath = options.loginPath;
this.callbackPath = options.callbackPath;
// validation
if (!this.callbackPath) {
throw new Error('Callback path is missing!');
}
this.loginCallbackPath = options.loginCallbackPath || this.callbackPath;
this.registerCallbackPath = options.registerCallbackPath || this.callbackPath;
this.debuggingFunction = options.debuggingFunction || function (stage, params) {
console.log(stage, params);
return Promise.resolve(params);
};
this._debugFetch = function (stage, input, init) {
return this.debuggingFunction(stage + "-request", {input: input, init: init})
.then(params => fetch(params.input, params.init))
.then(resp => this.debuggingFunction(stage + "-response", resp));
}
this._debugAround = function (stage, func, params) {
return this.debuggingFunction(stage + "-request", params)
.then(params => func(params))
.then(result => this.debuggingFunction(stage + "-response", result));
}
}
WebAuthn.constructor = WebAuthn;
WebAuthn.prototype.registerOnly = function (user) {
const self = this;
if (!self.registerPath) {
return Promise.reject('Register path missing form the initial configuration!');
}
return self._debugFetch("register", self.registerPath, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(user || {})
})
.then(res => {
if (res.status === 200) {
return res;
}
throw new Error(res.statusText);
})
.then(res => res.json())
.then(res => {
res.challenge = base64ToBuffer(res.challenge);
res.user.id = base64ToBuffer(res.user.id);
if (res.excludeCredentials) {
for (let i = 0; i < res.excludeCredentials.length; i++) {
res.excludeCredentials[i].id = base64ToBuffer(res.excludeCredentials[i].id);
}
}
return res;
})
.then(res => self._debugAround("credentials-create", x => navigator.credentials.create(x), {publicKey: res}))
.then(credential => {
return {
id: credential.id,
rawId: bufferToBase64(credential.rawId),
response: {
attestationObject: bufferToBase64(credential.response.attestationObject),
clientDataJSON: bufferToBase64(credential.response.clientDataJSON)
},
type: credential.type
};
});
};
WebAuthn.prototype.register = function (user) {
const self = this;
return self.registerOnly(user)
.then(body => {
return self._debugFetch("register-callback", self.registerCallbackPath, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
})
})
.then(res => {
if (res.status >= 200 && res.status < 300) {
return res;
}
throw new Error(res.statusText);
});
};
WebAuthn.prototype.login = function (user) {
const self = this;
return self.loginOnly(user)
.then(body => {
return self._debugFetch("login-callback", self.loginCallbackPath, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(body),
})
})
.then(res => {
if (res.status >= 200 && res.status < 300) {
return res;
}
throw new Error(res.statusText);
});
};
WebAuthn.prototype.loginOnly = function (user) {
const self = this;
if (!self.loginPath) {
return Promise.reject('Login path missing from the initial configuration!');
}
return self._debugFetch("login", self.loginPath, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(user)
})
.then(res => {
if (res.status === 200) {
return res;
}
throw new Error(res.statusText);
})
.then(res => res.json())
.then(res => {
res.challenge = base64ToBuffer(res.challenge);
if (res.allowCredentials) {
for (let i = 0; i < res.allowCredentials.length; i++) {
res.allowCredentials[i].id = base64ToBuffer(res.allowCredentials[i].id);
}
}
return res;
})
.then(res => self._debugAround("credentials-get", x => navigator.credentials.get(x), {publicKey: res}))
.then(credential => {
return {
id: credential.id,
rawId: bufferToBase64(credential.rawId),
response: {
clientDataJSON: bufferToBase64(credential.response.clientDataJSON),
authenticatorData: bufferToBase64(credential.response.authenticatorData),
signature: bufferToBase64(credential.response.signature),
userHandle: bufferToBase64(credential.response.userHandle),
},
type: credential.type
};
})
};
return WebAuthn;
}));