mirror of
https://github.com/ysoftdevs/oauth-playground-server.git
synced 2026-01-18 09:38:09 +01:00
WIP webauthn
This commit is contained in:
56
src/main/java/com/ysoft/geecon/webauthn/LoginResource.java
Normal file
56
src/main/java/com/ysoft/geecon/webauthn/LoginResource.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
264
src/main/resources/META-INF/resources/js/webauthn-debug.js
Normal file
264
src/main/resources/META-INF/resources/js/webauthn-debug.js
Normal 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;
|
||||
}));
|
||||
Reference in New Issue
Block a user