WIP webauthn tracer

This commit is contained in:
Dusan Jakub
2023-09-26 12:28:34 +02:00
parent ebb18f5e9a
commit 99f62423a9
3 changed files with 161 additions and 162 deletions

View File

@@ -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()));
}
}
}

View File

@@ -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);

View File

@@ -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;