mirror of
https://github.com/ysoftdevs/oauth-playground-server.git
synced 2026-03-12 05:12:19 +01:00
676 lines
34 KiB
HTML
676 lines
34 KiB
HTML
{#include base}
|
|
{#title}Login passwordless{/title}
|
|
{#add-header}
|
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
|
<script charset="UTF-8" src="/js/cbor.js" type="text/javascript"></script>
|
|
<script charset="UTF-8" src="/js/webauthn-debug.js" type="text/javascript"></script>
|
|
<style>
|
|
body {
|
|
display: inherit;
|
|
}
|
|
|
|
.code {
|
|
white-space: pre-wrap;
|
|
word-break: break-all;
|
|
background-color: #f4f4f4;
|
|
padding: 10px;
|
|
margin: 10px 0;
|
|
font-family: monospace;
|
|
}
|
|
|
|
.step {
|
|
display: none;
|
|
}
|
|
|
|
.button-label {
|
|
padding: 20px 0 5px 0;
|
|
}
|
|
|
|
.card-header {
|
|
padding-bottom: 20px;
|
|
}
|
|
|
|
.collection {
|
|
margin: .5rem 0 1rem 0;
|
|
border: 1px solid #e0e0e0;
|
|
border-radius: 2px;
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
</style>
|
|
{/add-header}
|
|
|
|
<div class="container">
|
|
<div class="row" id="init">
|
|
<div class="col s12 m8 offset-m2">
|
|
<div class="card horizontal">
|
|
<div class="card-stacked">
|
|
<div class="card-content">
|
|
<h6 class="card-header"><b>1.</b> Create a new registration or use an existing one</h6>
|
|
<p>First the user enters their username:</p>
|
|
<input id="userName" placeholder="User name"/><br/>
|
|
<p class="button-label">New user needs to generate a new credential and register it with the application.</p>
|
|
<button class="nextBtn waves-effect waves-light btn full-width" id="register">Register</button>
|
|
<p class="button-label">Returning user must prove they own the credential.</p>
|
|
<button class="nextBtn waves-effect waves-light btn full-width" id="login">Login</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row step" id="server1-request">
|
|
<div class="col s12 m8 offset-m2">
|
|
<div class="card horizontal">
|
|
<div class="card-stacked">
|
|
<div class="card-content">
|
|
<h6 class="card-header"><b>2.</b> Request a challenge</h6>
|
|
<p>The interaction starts with an AJAX call:</p>
|
|
<div class="code">POST <span id="server1-url"></span><br><div id="server1-call"></div></div>
|
|
<p class="button-label">Basically we are just specifying the username and his display name</p>
|
|
<button class="nextBtn waves-effect waves-light btn full-width">Request challenge</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row step" id="server1-response">
|
|
<div class="col s12 m8 offset-m2">
|
|
<div class="card horizontal">
|
|
<div class="card-stacked">
|
|
<div class="card-content">
|
|
<h6 class="card-header"><b>3.</b> The challenge</h6>
|
|
<p>The server prepares a challenge for the browser to sign:</p>
|
|
<div class="code" id="server1-response-body"></div>
|
|
<p class="button-label">Let's break it down...</p>
|
|
<ul id="register-challenge-breakdown" class="collection" style="display: none;">
|
|
<li class="collection-item">
|
|
<p class="emphasis"><b>rp</b></p>
|
|
<p>
|
|
Information about the relying party. The ID of the relying party is usually the domain name of the website or service trying to authenticate the user. Name contains human-friendly name for the relying party.
|
|
</p>
|
|
</li>
|
|
<li class="collection-item">
|
|
<p class="emphasis">user</p>
|
|
<p>
|
|
Information about the user: <b>id</b> contains unique identifier in the form of base64 encoded byte array. Parameters <b>name</b> and <b>displayName</b> are taken from the initial request.
|
|
</p>
|
|
</li>
|
|
<li class="collection-item">
|
|
<p class="emphasis">challenge</p>
|
|
<p>
|
|
A random value generated by the relying party. It's used to ensure that the authentication request is fresh and not a replay of a previous one.
|
|
</p>
|
|
</li>
|
|
<li class="collection-item">
|
|
<p class="emphasis">pubKeyCredParams</p>
|
|
<p>
|
|
A list of public key credential types and cryptographic algorithm combinations supported by the relying party.
|
|
Constants of -7 and -257 denote ES256 and RS256, respectively.
|
|
</p>
|
|
</li>
|
|
<li class="collection-item">
|
|
<p class="emphasis">authenticatorSelection</p>
|
|
<p>Criteria for the authenticator:</p>
|
|
<p>
|
|
<b>requireResidentKey:</b> A resident (private) key, is a key that cannot leave your authenticator device, this means that you cannot reuse the authenticator to log into a second computer. Here, it is not required.
|
|
</p>
|
|
<p>
|
|
<b>userVerification:</b> User verification is the technical process by which the user locally authorizes themselves. User verification typically involve a touch plus pin code, password entry, or biometric (face, fingerprint, ...). User verification check is stronger than a mere user presence, in which the user just confirms the operation, e.g. by pressing a simple button.
|
|
</p>
|
|
</li>
|
|
<li class="collection-item">
|
|
<p class="emphasis">timeout</p>
|
|
<p>
|
|
Maximum time, in milliseconds, that the caller is willing to wait for the call to complete.
|
|
</p>
|
|
</li>
|
|
<li class="collection-item">
|
|
<p class="emphasis">attestation</p>
|
|
<p>
|
|
Specifies the attestation conveyance preference. "none" means the relying party is not interested in authenticator attestation.
|
|
</p>
|
|
</li>
|
|
<li class="collection-item">
|
|
<p class="emphasis">extensions</p>
|
|
<p>
|
|
Dictionary of extension identifiers with their associated data. In this case, there's an extension "txAuthSimple" with no associated data provided.
|
|
</p>
|
|
</li>
|
|
</ul>
|
|
<ul id="login-challenge-breakdown" class="collection" style="display: none;">
|
|
<li class="collection-item">
|
|
<p class="emphasis">challenge</p>
|
|
<p>
|
|
A random value generated by the relying party. It's used to ensure that the authentication request is fresh and not a replay of a previous one.
|
|
</p>
|
|
</li>
|
|
<li class="collection-item">
|
|
<p class="emphasis">timeout</p>
|
|
<p>
|
|
Specifies the time, in milliseconds, that the caller is willing to wait for the call to complete.
|
|
</p>
|
|
</li>
|
|
<li class="collection-item">
|
|
<p class="emphasis">rpId</p>
|
|
<p>
|
|
The Relying Party Identifier. It helps the client (browser) know which key to use if there's more than one for a given domain.
|
|
</p>
|
|
</li>
|
|
<li class="collection-item">
|
|
<p class="emphasis">userVerification</p>
|
|
<p>
|
|
This specifies the desired user verification method. The value "discouraged" suggests that the website doesn't require a strong verification of the user, which means if the authenticator has a user verification method (like a fingerprint scanner or PIN), it's not mandatory to use it for this authentication.
|
|
</p>
|
|
</li>
|
|
<li class="collection-item">
|
|
<p class="emphasis">extensions</p>
|
|
<p>
|
|
Dictionary of extension identifiers with their associated data. In this case, there's an extension "txAuthSimple" with no associated data provided.
|
|
</p>
|
|
</li>
|
|
<li class="collection-item">
|
|
<p class="emphasis">allowCredentials</p>
|
|
<p>
|
|
An array containing one or more credentials that are allowed for the authentication:
|
|
</p>
|
|
<ul class="collection">
|
|
<li class="collection-item">
|
|
<p class="emphasis">type</p>
|
|
<p>
|
|
Specifies the type of the public key credential.
|
|
</p>
|
|
</li>
|
|
<li class="collection-item">
|
|
<p class="emphasis">id</p>
|
|
<p>
|
|
The credential ID that identifies the public key to be used during the authentication.
|
|
</p>
|
|
</li>
|
|
<li class="collection-item">
|
|
<p class="emphasis">transports</p>
|
|
<p>
|
|
Lists the allowed transports to retrieve the credential ID. This is useful for guiding the user's client (browser) about how it can communicate with the authenticator.
|
|
</p>
|
|
</li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
<button class="nextBtn waves-effect waves-light btn full-width">Continue</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row step" id="navigator-request">
|
|
<div class="col s12 m8 offset-m2">
|
|
<div class="card horizontal">
|
|
<div class="card-stacked">
|
|
<div class="card-content">
|
|
<h6 class="card-header"><b>4.</b> WebAuthn API</h6>
|
|
<p>The challenge is passed to the Javascript call:</p>
|
|
<div class="code" id="navigator-call"></div>
|
|
<button class="nextBtn waves-effect waves-light btn full-width">Call Webauthn API</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row step" id="navigator-attestation">
|
|
<div class="col s12 m8 offset-m2">
|
|
<div class="card horizontal">
|
|
<div class="card-stacked">
|
|
<div class="card-content">
|
|
<h6 class="card-header"><b>5.</b> Attestation</h6>
|
|
<p style="padding-bottom: 5px;">Which responds:</p>
|
|
<div class="code" id="navigator-attestation-body"></div>
|
|
<p class="button-label">Let's break it down...</p>
|
|
<ul class="collection">
|
|
<li class="collection-item">
|
|
<p class="emphasis"><b>id</b></p>
|
|
<p>
|
|
A base64url encoded string representation of the rawId. It uniquely identifies the created credential.
|
|
</p>
|
|
</li>
|
|
<li class="collection-item">
|
|
<p class="emphasis"><b>rawId</b></p>
|
|
<p>
|
|
The raw identifier of the credential, typically a byte array. It's the same as the id but in its raw binary form.
|
|
</p>
|
|
</li>
|
|
<li class="collection-item">
|
|
<p class="emphasis"><b>attestationObject</b></p>
|
|
<p>
|
|
This is a base64 encoded binary representation of an attestation statement. The attestation statement is produced by the authenticator to prove to the relying party (e.g., a website) that a new public key credential has been created in the authenticator. It is <a href="https://cbor.io/">CBOR</a> encoded.
|
|
</p>
|
|
<div class="code" id="navigator-attestationObject"></div>
|
|
<p class="button-label">The authData is <a href="https://www.w3.org/TR/webauthn-2/#sctn-attested-credential-data">binary encoded</a> and contain the actual public key, but also more flags and info about the authentication:</p>
|
|
<div class="code" id="navigator-authData"></div>
|
|
</li>
|
|
<li class="collection-item">
|
|
<p class="emphasis"><b>clientDataJSON</b></p>
|
|
<p>
|
|
This is a base64 encoded JSON string that contains information about the context in which the attestation was generated. Let's break down its decoded content:
|
|
</p>
|
|
<div class="code" id="navigator-attestation-clientDataJSON"></div>
|
|
<p class="button-label">Let's break it down...</p>
|
|
<ul class="collection">
|
|
<li class="collection-item">
|
|
<p class="emphasis"><b>challenge</b></p>
|
|
<p>
|
|
This is the challenge that was sent by the relying party during the registration request. The authenticator's response must include this challenge to ensure the action was based on a recent request and not a replayed one.
|
|
</p>
|
|
</li>
|
|
<li class="collection-item">
|
|
<p class="emphasis"><b>origin</b></p>
|
|
<p>
|
|
The origin of the request, indicating where the request came from.
|
|
</p>
|
|
</li>
|
|
<li class="collection-item">
|
|
<p class="emphasis"><b>type</b></p>
|
|
<p>
|
|
This indicates the type of operation, which in this context is "webauthn.create", meaning it's a credential creation operation.
|
|
</p>
|
|
</li>
|
|
</ul>
|
|
</li>
|
|
<li class="collection-item">
|
|
<p class="emphasis"><b>type</b></p>
|
|
<p>
|
|
This specifies the type of the public key credential, and it's set to "public-key" which means it's a public key credential.
|
|
</p>
|
|
</li>
|
|
</ul>
|
|
<p class="button-label">We are now ready to submit data to the server.</p>
|
|
<p class="button-label">The server will validate the request:</p>
|
|
<ul class="browser-default">
|
|
<li>Correct format</li>
|
|
<li>Correct type, challenge and origin</li>
|
|
<li>Decode authData in attestationObject, validate flags per expectations</li>
|
|
<li>... and more</li>
|
|
</ul>
|
|
<p class="button-label">If everything matches, the new credential is stored with the user.</p>
|
|
<button class="nextBtn waves-effect waves-light btn full-width">Finish the interaction</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row step" id="navigator-assertion">
|
|
<div class="col s12 m8 offset-m2">
|
|
<div class="card horizontal">
|
|
<div class="card-stacked">
|
|
<div class="card-content">
|
|
<h6 class="card-header"><b>5.</b> Assertion</h6>
|
|
<p style="padding-bottom: 5px;">Which responds:</p>
|
|
<div class="code" id="navigator-assertion-body"></div>
|
|
<p class="button-label">Let's break it down...</p>
|
|
<ul class="collection">
|
|
<li class="collection-item">
|
|
<p class="emphasis"><b>id</b></p>
|
|
<p>
|
|
A base64url encoded string representation of the rawId. It uniquely identifies the created credential.
|
|
</p>
|
|
</li>
|
|
<li class="collection-item">
|
|
<p class="emphasis"><b>rawId</b></p>
|
|
<p>
|
|
The raw identifier of the credential, typically a byte array. It's the same as the id but in its raw binary form.
|
|
</p>
|
|
</li>
|
|
<li class="collection-item">
|
|
<p class="emphasis"><b>clientDataJSON</b></p>
|
|
<p>
|
|
This is a base64 encoded JSON string that contains information about the context in which the attestation was generated. Let's break down its decoded content:
|
|
</p>
|
|
<div class="code" id="navigator-assertion-clientDataJSON"></div>
|
|
<p class="button-label">Let's break it down...</p>
|
|
<ul class="collection">
|
|
<li class="collection-item">
|
|
<p class="emphasis"><b>challenge</b></p>
|
|
<p>
|
|
This is the challenge that was sent by the relying party during the registration request. The authenticator's response must include this challenge to ensure the action was based on a recent request and not a replayed one.
|
|
</p>
|
|
</li>
|
|
<li class="collection-item">
|
|
<p class="emphasis"><b>origin</b></p>
|
|
<p>
|
|
The origin of the request, indicating where the request came from.
|
|
</p>
|
|
</li>
|
|
<li class="collection-item">
|
|
<p class="emphasis"><b>type</b></p>
|
|
<p>
|
|
This indicates the type of operation, which in this context is "webauthn.create", meaning it's a credential creation operation.
|
|
</p>
|
|
</li>
|
|
</ul>
|
|
</li>
|
|
<li class="collection-item">
|
|
<p class="emphasis"><b>authenticatorData</b></p>
|
|
<p>
|
|
This contains information about the authentication event.
|
|
</p>
|
|
<p class="button-label">It is <a href="https://www.w3.org/TR/webauthn-2/#sctn-attested-credential-data">binary encoded</a> and this time does not contain the public key:</p>
|
|
<div class="code" id="navigator-authenticatorData"></div>
|
|
</li>
|
|
<li class="collection-item">
|
|
<p class="emphasis"><b>signature</b></p>
|
|
<p>
|
|
This is the authenticator's digital signature over the combination of the <b>authenticatorData</b> and the hash of the <b>clientDataJSON</b>. The website will use this to verify that the response came from the user's authenticator and corresponds to the challenge it issued.
|
|
</p>
|
|
</li>
|
|
<li class="collection-item">
|
|
<p class="emphasis"><b>userHandle</b></p>
|
|
<p>
|
|
An optional field which, if present, represents a unique identifier for the user, established during registration. It helps websites identify which user is trying to authenticate, especially in scenarios where the user doesn't explicitly provide a username during the login process.
|
|
</p>
|
|
</li>
|
|
<li class="collection-item">
|
|
<p class="emphasis"><b>type</b></p>
|
|
<p>
|
|
This specifies the type of the public key credential, and it's set to "public-key" which means it's a public key credential.
|
|
</p>
|
|
</li>
|
|
</ul>
|
|
<p class="button-label">We are now ready to submit data to the server.</p>
|
|
<p class="button-label">The server will validate the request:</p>
|
|
<ul class="browser-default">
|
|
<li>Correct format</li>
|
|
<li>Correct type, challenge and origin</li>
|
|
<li>Validate the signature with the public key stored on the server</li>
|
|
</ul>
|
|
<p class="button-label">If everything matches, the user is logged in.</p>
|
|
<button class="nextBtn waves-effect waves-light btn full-width">Finish the interaction</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row step" id="errors">
|
|
<div class="col s12 m8 offset-m2">
|
|
<div class="card horizontal">
|
|
<div class="card-stacked">
|
|
<div class="card-content">
|
|
<div id="result"></div>
|
|
<div id="trace"></div>
|
|
<button class="resetBtn waves-effect waves-light btn full-width">Start over</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<form action="#" method="POST">
|
|
<input name="sessionId" type="hidden" value="{sessionId}">
|
|
<div id="form-generated"></div>
|
|
</form>
|
|
</div>
|
|
|
|
<script type="text/javascript">
|
|
$.fn.extend({
|
|
showInViewport: function() {
|
|
this.show();
|
|
this.each(function () {
|
|
$([document.documentElement, document.body]).animate({
|
|
scrollTop: $(this).offset().top
|
|
}, 1000);
|
|
});
|
|
}
|
|
});
|
|
|
|
function tryDecodeBase64(str) {
|
|
try {
|
|
return atob(str);
|
|
} catch (e) {
|
|
return e + "";
|
|
}
|
|
}
|
|
|
|
function readHexString(view, start, len){
|
|
const hash = new Uint8Array(len);
|
|
|
|
for (let i = 0; i < len; i++) {
|
|
hash[i] = view.getUint8(i+start);
|
|
}
|
|
|
|
return Array.from(hash).map(byte => {
|
|
return ('0' + byte.toString(16)).slice(-2); // Ensure two-digit hex representation
|
|
}).join('');
|
|
}
|
|
|
|
function parseCredentialPubKey(array) {
|
|
console.log(array)
|
|
let view = new DataView(array.buffer, array.byteOffset, array.byteLength);
|
|
let credentialIdLength = view.getUint16(16, false);
|
|
return {
|
|
"aaguid": readHexString(view, 0, 16),
|
|
"credentialIdLength": credentialIdLength,
|
|
"credentialId": WebAuthn.bufferToBase64(array.slice(18, credentialIdLength).buffer),
|
|
"credentialPublicKey": WebAuthn.bufferToBase64(array.slice(18+credentialIdLength).buffer)
|
|
}
|
|
}
|
|
|
|
function parseAuthenticatorData(array) {
|
|
try {
|
|
console.log(array)
|
|
let view = new DataView(array.buffer, array.byteOffset, array.byteLength);
|
|
let flags = view.getInt8(32);
|
|
const hashHex = readHexString(view, 0, 32);
|
|
|
|
const result = {
|
|
"rpIdHash": hashHex,
|
|
"flags": {
|
|
"userPresent": !!(flags & 1),
|
|
"userVerified": !!(flags & (1 << 2)),
|
|
"attestedCredentialData": !!(flags & (1 << 6)),
|
|
"extensionDataIncluded": !!(flags & (1 << 7))
|
|
},
|
|
"signCount": view.getUint32(33, false),
|
|
}
|
|
|
|
try {
|
|
if (result.flags.attestedCredentialData) {
|
|
result['credentialPubKey'] = parseCredentialPubKey(array.slice(37));
|
|
}
|
|
} catch (e) {
|
|
console.log(e)
|
|
}
|
|
|
|
return result;
|
|
} catch (e) {
|
|
console.warn(e)
|
|
return "Unable to parse: " + e;
|
|
}
|
|
}
|
|
|
|
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", $(where)).show();
|
|
if (button.length) {
|
|
return new Promise((resolve, reject) => {
|
|
button.click(() => {
|
|
resolve(result);
|
|
$(button).hide();
|
|
});
|
|
});
|
|
} else {
|
|
return Promise.resolve(result);
|
|
}
|
|
}
|
|
|
|
function traceRegisterRequest(params) {
|
|
$(".step").hide();
|
|
$("#server1-request").showInViewport();
|
|
$("#server1-url").html(params.url)
|
|
$("#server1-call").html(JSON.stringify(params.body, null, 2));
|
|
return continueButton("#server1-request", params);
|
|
}
|
|
|
|
function traceRegisterResponse(params) {
|
|
$("#server1-response").showInViewport();
|
|
$("#server1-response-body").html(JSON.stringify(params, null, 2));
|
|
$("#register-challenge-breakdown").show();
|
|
|
|
return continueButton("#server1-response", params);
|
|
}
|
|
|
|
function traceLoginRequest(params) {
|
|
$(".step").hide();
|
|
$("#server1-request").showInViewport();
|
|
$("#server1-url").html(params.url)
|
|
$("#server1-call").html(JSON.stringify(params.body, null, 2));
|
|
return continueButton("#server1-request", params);
|
|
}
|
|
|
|
function traceLoginResponse(params) {
|
|
$("#server1-response").showInViewport();
|
|
$("#server1-response-body").html(JSON.stringify(params, null, 2));
|
|
$("#login-challenge-breakdown").show();
|
|
return continueButton("#server1-response", params);
|
|
}
|
|
|
|
function traceCredentialsCreateRequest(challenge) {
|
|
$("#navigator-request").showInViewport();
|
|
$("#navigator-call").html("navigator.credentials.create({ publicKey: ... });");
|
|
return continueButton("#navigator-request", challenge);
|
|
}
|
|
|
|
function traceCredentialsCreateResponse(response) {
|
|
$("#navigator-attestation").showInViewport();
|
|
$("#navigator-attestation-body").html(JSON.stringify(response, null, 2));
|
|
$("#navigator-attestation-clientDataJSON").html(JSON.stringify(JSON.parse(tryDecodeBase64(response.response.clientDataJSON)), null, 2));
|
|
let attestationObject = CBOR.decode(WebAuthn.base64ToBuffer(response.response.attestationObject));
|
|
let authDataRaw = attestationObject['authData'];
|
|
|
|
if (authDataRaw) {
|
|
let authData = parseAuthenticatorData(authDataRaw);
|
|
$("#navigator-authData").html(JSON.stringify(authData, null, 2));
|
|
attestationObject['authData'] = WebAuthn.bufferToBase64(authDataRaw)
|
|
}
|
|
$("#navigator-attestationObject").html(JSON.stringify(attestationObject, null, 2));
|
|
return continueButton("#navigator-attestation", response);
|
|
}
|
|
|
|
function traceCredentialsGetRequest(challenge) {
|
|
$("#navigator-request").showInViewport();
|
|
$("#navigator-call").html("navigator.credentials.get({ publicKey: ... });");
|
|
return continueButton("#navigator-request", challenge);
|
|
}
|
|
|
|
function traceCredentialsGetResponse(response) {
|
|
$("#navigator-assertion").showInViewport();
|
|
$("#navigator-assertion-body").html(JSON.stringify(response, null, 2));
|
|
$("#navigator-assertion-clientDataJSON").html(JSON.stringify(JSON.parse(tryDecodeBase64(response.response.clientDataJSON)), null, 2));
|
|
let authenticatorData = WebAuthn.base64ToBuffer(response.response.authenticatorData);
|
|
let authData = parseAuthenticatorData(new Uint8Array(authenticatorData));
|
|
$("#navigator-authenticatorData").html(JSON.stringify(authData, null, 2));
|
|
return continueButton("#navigator-assertion", response);
|
|
}
|
|
|
|
function traceGeneric(stage, params) {
|
|
const content = JSON.stringify(params);
|
|
const trace = $("<div class='container'></div>").attr("id", stage).html(content).appendTo("#trace");
|
|
return continueButton(trace, params);
|
|
}
|
|
|
|
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)
|
|
}
|
|
$form.submit();
|
|
}
|
|
|
|
function init() {
|
|
//$(".step").hide()
|
|
$("#init").showInViewport();
|
|
}
|
|
|
|
$(".resetBtn").click(init)
|
|
|
|
const webAuthn = new WebAuthn({
|
|
callbackPath: '/q/webauthn/callback',
|
|
registerPath: '/q/webauthn/register',
|
|
loginPath: '/q/webauthn/login',
|
|
debuggingFunction: tracer
|
|
});
|
|
|
|
const result = document.getElementById('result');
|
|
|
|
const loginButton = document.getElementById('login');
|
|
|
|
loginButton.onclick = () => {
|
|
var userName = document.getElementById('userName').value;
|
|
result.replaceChildren();
|
|
webAuthn.loginOnly({ name: userName })
|
|
.then(body => {
|
|
form("/auth/passwordless/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 => {
|
|
$("#errors").showInViewport()
|
|
$("#result").html("Login failed: " + err);
|
|
console.error(err);
|
|
});
|
|
return false;
|
|
};
|
|
|
|
const registerButton = document.getElementById('register');
|
|
|
|
registerButton.onclick = () => {
|
|
var userName = document.getElementById('userName').value;
|
|
result.replaceChildren();
|
|
|
|
webAuthn.registerOnly({ name: userName, displayName: userName})
|
|
.then(body => {
|
|
form("/auth/passwordless/register", {
|
|
'webAuthnId': body.id,
|
|
'webAuthnRawId': body.rawId,
|
|
'webAuthnResponseAttestationObject': body.response.attestationObject,
|
|
'webAuthnResponseClientDataJSON': body.response.clientDataJSON,
|
|
'webAuthnType': body.type
|
|
});
|
|
})
|
|
.catch(err => {
|
|
$("#errors").showInViewport()
|
|
$("#result").html("Registration failed: " + err);
|
|
console.error(err);
|
|
});
|
|
return false;
|
|
};
|
|
</script>
|
|
{/include}
|