258 lines
7.4 KiB
JavaScript
258 lines
7.4 KiB
JavaScript
|
/*
|
||
|
* Passkey Implementation
|
||
|
*/
|
||
|
let passkey_debug = false;
|
||
|
|
||
|
/**
|
||
|
* Convert a ArrayBuffer to Base64
|
||
|
* @param {ArrayBuffer} buffer
|
||
|
* @returns {String}
|
||
|
*/
|
||
|
function arrayBufferToBase64(buffer) {
|
||
|
let binary = '';
|
||
|
let bytes = new Uint8Array(buffer);
|
||
|
let len = bytes.byteLength;
|
||
|
for (let i = 0; i < len; i++) {
|
||
|
binary += String.fromCharCode( bytes[ i ] );
|
||
|
}
|
||
|
return window.btoa(binary);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* convert RFC 1342-like base64 strings to array buffer
|
||
|
* @param {mixed} obj
|
||
|
* @returns {undefined}
|
||
|
*/
|
||
|
function recursiveBase64StrToArrayBuffer(obj) {
|
||
|
let prefix = '=?BINARY?B?';
|
||
|
let suffix = '?=';
|
||
|
if (typeof obj === 'object') {
|
||
|
for (let key in obj) {
|
||
|
if (typeof obj[key] === 'string') {
|
||
|
let str = obj[key];
|
||
|
if (str.substring(0, prefix.length) === prefix && str.substring(str.length - suffix.length) === suffix) {
|
||
|
str = str.substring(prefix.length, str.length - suffix.length);
|
||
|
|
||
|
let binary_string = window.atob(str);
|
||
|
let len = binary_string.length;
|
||
|
let bytes = new Uint8Array(len);
|
||
|
for (let i = 0; i < len; i++) {
|
||
|
bytes[i] = binary_string.charCodeAt(i);
|
||
|
}
|
||
|
obj[key] = bytes.buffer;
|
||
|
}
|
||
|
} else {
|
||
|
recursiveBase64StrToArrayBuffer(obj[key]);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function passkey_check_browser()
|
||
|
{
|
||
|
// check browser support
|
||
|
if ((! window.fetch) || (! navigator.credentials) || (! navigator.credentials.create))
|
||
|
throw new Error('Browser not supported.');
|
||
|
|
||
|
/*
|
||
|
// Availability of `window.PublicKeyCredential` means WebAuthn is usable.
|
||
|
// `isUserVerifyingPlatformAuthenticatorAvailable` means the feature detection is usable.
|
||
|
// `isConditionalMediationAvailable` means the feature detection is usable.
|
||
|
if (window.PublicKeyCredential &&
|
||
|
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
|
||
|
PublicKeyCredential.isConditionalMediationAvailable) {
|
||
|
// Check if user verifying platform authenticator is available.
|
||
|
Promise.all([
|
||
|
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),
|
||
|
PublicKeyCredential.isConditionalMediationAvailable(),
|
||
|
]).then(results => {
|
||
|
if (results.every(r => r === true)) {
|
||
|
// Display "Create a new passkey" button
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
*/
|
||
|
|
||
|
if (passkey_debug)
|
||
|
console.log('Passkey: Browser OK');
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Register/Create a passkey for a user
|
||
|
*/
|
||
|
async function passkey_register(csrf_token,icon_dom,icon,icon_shell_current,icon_shell_success,icon_shell_fail)
|
||
|
{
|
||
|
try {
|
||
|
if (! passkey_check_browser())
|
||
|
return;
|
||
|
|
||
|
// Change our icon so that it is obvious we are doing something
|
||
|
icon_dom.find('i').removeClass(icon).addClass('spinner-grow spinner-grow-sm');
|
||
|
|
||
|
// Get our arguments
|
||
|
var createArgs;
|
||
|
$.ajax({
|
||
|
url: '/passkey/register',
|
||
|
type: 'GET',
|
||
|
dataType: 'json',
|
||
|
async: false,
|
||
|
cache: false,
|
||
|
success: function(data) {
|
||
|
if (passkey_debug)
|
||
|
console.log('Passkey: Get Register Success');
|
||
|
|
||
|
recursiveBase64StrToArrayBuffer(data);
|
||
|
createArgs = data;
|
||
|
},
|
||
|
error: function(e,status,error) {
|
||
|
throw new Error(status || 'Unknown error occurred');
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// Create credentials
|
||
|
try {
|
||
|
const cred = await navigator.credentials.create(createArgs);
|
||
|
|
||
|
const authenticatorAttestationResponse = {
|
||
|
id: cred.id,
|
||
|
rawId: arrayBufferToBase64(cred.rawId),
|
||
|
transports: cred.response.getTransports ? cred.response.getTransports() : null,
|
||
|
clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
|
||
|
attestationObject: cred.response.attestationObject ? arrayBufferToBase64(cred.response.attestationObject) : null,
|
||
|
authenticatorAttachment: cred.authenticatorAttachment,
|
||
|
_token: csrf_token,
|
||
|
};
|
||
|
|
||
|
$.ajax({
|
||
|
url: '/passkey/check',
|
||
|
type: 'POST',
|
||
|
data: authenticatorAttestationResponse,
|
||
|
cache: false,
|
||
|
success: function(data) {
|
||
|
if (passkey_debug)
|
||
|
console.log('Passkey: Registration Success');
|
||
|
|
||
|
icon_dom.find('i').addClass(icon).removeClass('spinner-grow spinner-grow-sm');
|
||
|
icon_dom.removeClass(icon_shell_current).addClass(icon_shell_success);
|
||
|
},
|
||
|
error: function(e,status,error) {
|
||
|
throw new Error(status || 'Unknown error occurred');
|
||
|
}
|
||
|
});
|
||
|
|
||
|
} catch (status) {
|
||
|
if (passkey_debug)
|
||
|
console.log(status || 'Passkey: User Aborted Register');
|
||
|
|
||
|
// Restore the icon
|
||
|
icon_dom.removeClass(icon_shell_current).addClass(icon_shell_fail).find('i').addClass(icon).removeClass('spinner-grow spinner-grow-sm');
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
} catch (err) {
|
||
|
window.alert(err || 'An UNKNOWN error occurred?');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Check a passkey being presented
|
||
|
*/
|
||
|
async function passkey_check(csrf_token,redirect)
|
||
|
{
|
||
|
if (passkey_debug)
|
||
|
console.log('Passkey: Check User Passkey');
|
||
|
|
||
|
try {
|
||
|
if (! passkey_check_browser())
|
||
|
return;
|
||
|
|
||
|
// Get our arguments
|
||
|
var getArgs;
|
||
|
$.ajax({
|
||
|
url: '/passkey/get',
|
||
|
type: 'GET',
|
||
|
dataType: 'json',
|
||
|
async: false,
|
||
|
cache: false,
|
||
|
success: function(data) {
|
||
|
if (passkey_debug)
|
||
|
console.log('Passkey: Get Args Success');
|
||
|
|
||
|
recursiveBase64StrToArrayBuffer(data);
|
||
|
getArgs = data;
|
||
|
},
|
||
|
error: function(e,status,error) {
|
||
|
throw new Error(status || 'Unknown error occurred');
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// check credentials with hardware
|
||
|
const cred = await navigator.credentials.get(getArgs);
|
||
|
|
||
|
// create object for transmission to server
|
||
|
const authenticatorAttestationResponse = {
|
||
|
id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null,
|
||
|
clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
|
||
|
authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null,
|
||
|
signature: cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null,
|
||
|
userHandle: cred.response.userHandle ? arrayBufferToBase64(cred.response.userHandle) : null,
|
||
|
_token: csrf_token
|
||
|
};
|
||
|
|
||
|
$.ajax({
|
||
|
url: '/passkey/process',
|
||
|
type: 'POST',
|
||
|
data: authenticatorAttestationResponse,
|
||
|
cache: false,
|
||
|
success: function(data) {
|
||
|
if (passkey_debug)
|
||
|
console.log('Passkey: Process Success');
|
||
|
|
||
|
// Direct to the home page
|
||
|
window.location.href = (redirect !== undefined) ? redirect : '/';
|
||
|
},
|
||
|
error: function(e,status,error) {
|
||
|
throw new Error(status || 'Unknown error occurred');
|
||
|
}
|
||
|
});
|
||
|
|
||
|
} catch (err) {
|
||
|
window.alert(err || 'An UNKNOWN error occurred?');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function passkey_create(object,csrf,icon,icon_class_current,icon_class_success,icon_class_nop)
|
||
|
{
|
||
|
if (passkey_debug)
|
||
|
console.log('Passkey: Create Click');
|
||
|
|
||
|
// Availability of `window.PublicKeyCredential` means WebAuthn is usable.
|
||
|
// `isUserVerifyingPlatformAuthenticatorAvailable` means the feature detection is usable.
|
||
|
// `sConditionalMediationAvailable` means the feature detection is usable.
|
||
|
if (window.PublicKeyCredential &&
|
||
|
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
|
||
|
PublicKeyCredential.isConditionalMediationAvailable) {
|
||
|
// Check if user verifying platform authenticator is available.
|
||
|
Promise.all([
|
||
|
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),
|
||
|
PublicKeyCredential.isConditionalMediationAvailable(),
|
||
|
]).then(results => {
|
||
|
if (passkey_debug)
|
||
|
console.log('Passkey: Browser Supported');
|
||
|
|
||
|
if (results.every(r => r === true)) {
|
||
|
passkey_register(csrf,object,icon,icon_class_current,icon_class_success,icon_class_nop);
|
||
|
} else {
|
||
|
alert('It seems that passkey is NOT supported by your browse (B)');
|
||
|
}
|
||
|
});
|
||
|
|
||
|
} else {
|
||
|
alert('It seems that passkey is NOT supported by your browser (A)');
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|