mirror of
https://github.com/ysoftdevs/oauth-playground-server.git
synced 2026-01-11 22:41:28 +01:00
WIP webauthn tracer
This commit is contained in:
@@ -79,7 +79,7 @@ public class MyWebAuthnSetup implements WebAuthnUserProvider {
|
||||
}
|
||||
// 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"));
|
||||
return Uni.createFrom().failure(new Throwable("Duplicate user: " + authenticator.getUserName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,41 +15,10 @@
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
|
||||
<script charset="UTF-8" src="/js/webauthn-debug.js" type="text/javascript"></script>
|
||||
<style>
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto auto;
|
||||
}
|
||||
|
||||
button, input {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.item {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
nav > ul {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
nav > ul > li {
|
||||
float: left;
|
||||
}
|
||||
|
||||
nav > ul > li > a {
|
||||
display: block;
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 14px 16px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
nav > ul > li > a:hover {
|
||||
background-color: #111;
|
||||
.code {
|
||||
white-space: pre;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -63,49 +32,36 @@
|
||||
|
||||
<div id="trace"></div>
|
||||
|
||||
<div class="container" id="server1">
|
||||
<div id="server1-call"></div>
|
||||
<div id="server1-response"></div>
|
||||
<div class="container step" id="server1">
|
||||
The interaction starts with an AJAX call.
|
||||
<div class="code">POST <span id="server1-url"></span>
|
||||
<div id="server1-call"></div>
|
||||
</div>
|
||||
The server prepares a challenge for the browser to sign.
|
||||
<div class="code" id="server1-response"></div>
|
||||
</div>
|
||||
|
||||
<div class="container" id="navigator">
|
||||
<div id="navigator-call"></div>
|
||||
<div id="navigator-response"></div>
|
||||
<div class="container step" id="navigator">
|
||||
The challenge is passed to the browser call:
|
||||
<div class="code" id="navigator-call">navigator.credentials.create(...);</div>
|
||||
Which responds:
|
||||
<div class="code" id="navigator-response"></div>
|
||||
The <strong>response.clientDataJSON</strong> are base64 encoded:
|
||||
<div class="code" id="navigator-clientDataJSON"></div>
|
||||
</div>
|
||||
|
||||
<div class="container" id="server2">
|
||||
<div id="server2-call"></div>
|
||||
<div id="server2-response"></div>
|
||||
<div class="container step" id="server2">
|
||||
|
||||
<div class="code" id="server2-call"></div>
|
||||
</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>
|
||||
<input name="sessionId" type="hidden" value="somesessionid">
|
||||
<div id="form-generated"></div>
|
||||
<button type="submit">Finish</button>
|
||||
</form>
|
||||
|
||||
<script type="text/javascript">
|
||||
@@ -129,6 +85,8 @@
|
||||
}
|
||||
|
||||
function fillOrHideJsonField(id, value) {
|
||||
$("#" + id).remove();
|
||||
$
|
||||
let el = document.getElementById(id);
|
||||
if (!el) throw "No element #" + id;
|
||||
if (value !== undefined) {
|
||||
@@ -141,6 +99,90 @@
|
||||
|
||||
function tracer(stage, params) {
|
||||
console.log(stage, params)
|
||||
|
||||
switch (stage) {
|
||||
case "register-request":
|
||||
return traceRegisterRequest(params);
|
||||
case "register-response":
|
||||
return traceRegisterResponse(params);
|
||||
case "credentials-create-request":
|
||||
return traceCredentialsCreateRequest(params);
|
||||
case "credentials-create-response":
|
||||
return traceCredentialsCreateResponse(params);
|
||||
case "login-request":
|
||||
return traceLoginRequest(params);
|
||||
case "login-response":
|
||||
return traceLoginResponse(params);
|
||||
case "credentials-get-request":
|
||||
return traceCredentialsGetRequest(params);
|
||||
case "credentials-get-response":
|
||||
return traceCredentialsGetResponse(params);
|
||||
default:
|
||||
return traceGeneric(stage, params);
|
||||
}
|
||||
}
|
||||
|
||||
function continueButton(where, result) {
|
||||
const button = $("<button>Continue</button>").appendTo(where)
|
||||
return new Promise((resolve, reject) => {
|
||||
button.click(() => {
|
||||
resolve(result);
|
||||
$(button).remove();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function traceRegisterRequest(params) {
|
||||
$(".step").hide();
|
||||
$("#server1").show();
|
||||
$("#server1-url").html(params.url)
|
||||
$("#server1-call").html(JSON.stringify(params.body, null, 2));
|
||||
return continueButton("#server1-call", params);
|
||||
}
|
||||
|
||||
function traceRegisterResponse(params) {
|
||||
$("#server1-response").html(JSON.stringify(params, null, 2));
|
||||
return continueButton("#server1-response", params);
|
||||
}
|
||||
|
||||
function traceLoginRequest(params) {
|
||||
$(".step").hide();
|
||||
$("#server1").show();
|
||||
$("#server1-url").html(params.url)
|
||||
$("#server1-call").html(JSON.stringify(params.body, null, 2));
|
||||
return continueButton("#server1-call", params);
|
||||
}
|
||||
|
||||
function traceLoginResponse(params) {
|
||||
$("#server1-response").html(JSON.stringify(params, null, 2));
|
||||
return continueButton("#server1-response", params);
|
||||
}
|
||||
|
||||
function traceCredentialsCreateRequest(challenge) {
|
||||
$("#navigator").show();
|
||||
//$("#navigator-call").html(JSON.stringify(challenge, null, 2));
|
||||
return continueButton("#navigator-call", challenge);
|
||||
}
|
||||
|
||||
function traceCredentialsCreateResponse(response) {
|
||||
$("#navigator-response").html(JSON.stringify(response, null, 2));
|
||||
$("#navigator-clientDataJSON").html(JSON.stringify(JSON.parse(tryDecodeBase64(response.response.clientDataJSON)), null, 2));
|
||||
return continueButton("#navigator-response", response);
|
||||
}
|
||||
|
||||
function traceCredentialsGetRequest(challenge) {
|
||||
$("#navigator").show();
|
||||
//$("#navigator-call").html(JSON.stringify(challenge, null, 2));
|
||||
return continueButton("#navigator-call", challenge);
|
||||
}
|
||||
|
||||
function traceCredentialsGetResponse(response) {
|
||||
$("#navigator-response").html(JSON.stringify(response, null, 2));
|
||||
$("#navigator-clientDataJSON").html(JSON.stringify(JSON.parse(tryDecodeBase64(response.response.clientDataJSON)), null, 2));
|
||||
return continueButton("#navigator-response", response);
|
||||
}
|
||||
|
||||
function traceGeneric(stage, params) {
|
||||
const content = JSON.stringify(params);
|
||||
const trace = $("<div class='container'></div>").attr("id", stage).html(content).appendTo("#trace");
|
||||
const button = $("<button>Continue</button>").appendTo(trace)
|
||||
@@ -152,6 +194,15 @@
|
||||
});
|
||||
}
|
||||
|
||||
function form(action, fields) {
|
||||
const $form = $("form").attr("action", action);
|
||||
const $fields = $("#form-generated", $form).empty()
|
||||
for ([key, value] of Object.entries(fields)) {
|
||||
console.log(key);
|
||||
$("<input type='hidden'>").attr("name", key).val(value).appendTo($fields)
|
||||
}
|
||||
}
|
||||
|
||||
const webAuthn = new WebAuthn({
|
||||
callbackPath: '/q/webauthn/callback',
|
||||
registerPath: '/q/webauthn/register',
|
||||
@@ -170,24 +221,15 @@
|
||||
result.replaceChildren();
|
||||
webAuthn.loginOnly({name: userName})
|
||||
.then(body => {
|
||||
// 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;
|
||||
form("/webauthn/login", {
|
||||
'webAuthnId': body.id,
|
||||
'webAuthnRawId': body.rawId,
|
||||
'webAuthnResponseClientDataJSON': body.response.clientDataJSON,
|
||||
'webAuthnResponseAuthenticatorData': body.response.authenticatorData,
|
||||
'webAuthnResponseSignature': body.response.signature,
|
||||
'webAuthnResponseUserHandle': body.response.userHandle,
|
||||
'webAuthnType': body.type
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
result.append("Login failed: " + err);
|
||||
@@ -207,14 +249,13 @@
|
||||
|
||||
webAuthn.registerOnly({name: userName, displayName: userName /*firstName + " " + lastName*/})
|
||||
.then(body => {
|
||||
// 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);
|
||||
|
||||
form("/webauthn/register", {
|
||||
'webAuthnId': body.id,
|
||||
'webAuthnRawId': body.rawId,
|
||||
'webAuthnResponseAttestationObject': body.response.attestationObject,
|
||||
'webAuthnResponseClientDataJSON': body.response.clientDataJSON,
|
||||
'webAuthnType': body.type
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
result.append("Registration failed: " + err);
|
||||
|
||||
@@ -112,9 +112,23 @@
|
||||
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))
|
||||
this._debugPostJson = function (stage, url, body) {
|
||||
return this.debuggingFunction(stage + "-request", {url: url, body: body})
|
||||
.then(params => fetch(params.url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(params.body)
|
||||
}))
|
||||
.then(res => {
|
||||
if (res.status >= 200 && res.status < 300) {
|
||||
return res;
|
||||
}
|
||||
throw new Error(res.statusText);
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(resp => this.debuggingFunction(stage + "-response", resp));
|
||||
}
|
||||
|
||||
@@ -134,21 +148,7 @@
|
||||
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())
|
||||
return self._debugPostJson("register", self.registerPath, user || {})
|
||||
.then(res => {
|
||||
res.challenge = base64ToBuffer(res.challenge);
|
||||
res.user.id = base64ToBuffer(res.user.id);
|
||||
@@ -159,8 +159,7 @@
|
||||
}
|
||||
return res;
|
||||
})
|
||||
.then(res => self._debugAround("credentials-create", x => navigator.credentials.create(x), {publicKey: res}))
|
||||
.then(credential => {
|
||||
.then(res => self._debugAround("credentials-create", x => navigator.credentials.create(x).then(credential => {
|
||||
return {
|
||||
id: credential.id,
|
||||
rawId: bufferToBase64(credential.rawId),
|
||||
@@ -170,49 +169,22 @@
|
||||
},
|
||||
type: credential.type
|
||||
};
|
||||
});
|
||||
}), {publicKey: res}));
|
||||
|
||||
};
|
||||
|
||||
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);
|
||||
return self._debugPostJson("register-callback", self.registerCallbackPath, body)
|
||||
});
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
.then(body => self._debugPostJson("login-callback", self.loginCallbackPath, body))
|
||||
};
|
||||
|
||||
WebAuthn.prototype.loginOnly = function (user) {
|
||||
@@ -220,21 +192,7 @@
|
||||
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())
|
||||
return self._debugPostJson("login", self.loginPath, user)
|
||||
.then(res => {
|
||||
res.challenge = base64ToBuffer(res.challenge);
|
||||
if (res.allowCredentials) {
|
||||
@@ -244,8 +202,7 @@
|
||||
}
|
||||
return res;
|
||||
})
|
||||
.then(res => self._debugAround("credentials-get", x => navigator.credentials.get(x), {publicKey: res}))
|
||||
.then(credential => {
|
||||
.then(res => self._debugAround("credentials-get", x => navigator.credentials.get(x).then(credential => {
|
||||
return {
|
||||
id: credential.id,
|
||||
rawId: bufferToBase64(credential.rawId),
|
||||
@@ -257,7 +214,8 @@
|
||||
},
|
||||
type: credential.type
|
||||
};
|
||||
})
|
||||
}), {publicKey: res}))
|
||||
|
||||
};
|
||||
|
||||
return WebAuthn;
|
||||
|
||||
Reference in New Issue
Block a user