mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-06-21 01:25:55 +02:00
Use jbtronics/2fa-webauthn for u2f two factor authentication
This commit is contained in:
parent
03aaff3c79
commit
068daeda75
18 changed files with 1389 additions and 604 deletions
|
@ -1,134 +0,0 @@
|
|||
import u2fApi from 'u2f-api'
|
||||
|
||||
'use strict'
|
||||
|
||||
window.u2fauth = window.u2fauth || {}
|
||||
|
||||
u2fauth.formId = 'u2fForm'
|
||||
u2fauth.authCodeId = '_auth_code'
|
||||
u2fauth.keynameId = 'u2fkeyname'
|
||||
u2fauth.pressButtonId = 'u2fpressbutton'
|
||||
u2fauth.errorId = 'u2fError'
|
||||
u2fauth.timeout = 30
|
||||
u2fauth.errorTranslation = {
|
||||
1: 'Unknown Error',
|
||||
2: 'Bad Request',
|
||||
3: 'Client configuration not supported',
|
||||
4: 'Device already registered or ineligible',
|
||||
5: 'Timeout. Click to retry'
|
||||
}
|
||||
|
||||
u2fauth.ready = function (fn) {
|
||||
if (document.readyState !== 'loading') {
|
||||
fn()
|
||||
} else if (document.addEventListener) {
|
||||
document.addEventListener('DOMContentLoaded', fn)
|
||||
} else {
|
||||
document.attachEvent('onreadystatechange', function () {
|
||||
if (document.readyState !== 'loading') { fn() }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
u2fauth.authenticate = function () {
|
||||
u2fauth.clearError()
|
||||
u2fauth.showPressButton()
|
||||
|
||||
var form = document.getElementById(u2fauth.formId)
|
||||
var request = JSON.parse(form.dataset.request)
|
||||
|
||||
u2fApi.isSupported()
|
||||
.then(function (supported) {
|
||||
if (supported) {
|
||||
return u2fApi.sign(request, u2fauth.timeout)
|
||||
.then(response => {
|
||||
u2fauth.hidePressButton()
|
||||
u2fauth.submit(form, response)
|
||||
})
|
||||
} else {
|
||||
alert('Browser not supported')
|
||||
}
|
||||
})
|
||||
.catch(data => {
|
||||
u2fauth.hidePressButton()
|
||||
u2fauth.showError(data.metaData.code, u2fauth.authenticate)
|
||||
})
|
||||
}
|
||||
|
||||
u2fauth.register = function () {
|
||||
u2fauth.clearError()
|
||||
u2fauth.hideKeyname()
|
||||
u2fauth.showPressButton()
|
||||
|
||||
var form = document.getElementById(u2fauth.formId)
|
||||
var request = JSON.parse(form.dataset.request)
|
||||
|
||||
u2fApi.isSupported()
|
||||
.then(function (supported) {
|
||||
if (supported) {
|
||||
return u2fApi.register(request[0], request[1], u2fauth.timeout)
|
||||
.then(response => {
|
||||
u2fauth.hidePressButton()
|
||||
u2fauth.submit(form, response)
|
||||
})
|
||||
} else {
|
||||
alert('Browser not supported')
|
||||
}
|
||||
})
|
||||
.catch(data => {
|
||||
console.info(data)
|
||||
u2fauth.hidePressButton()
|
||||
u2fauth.showError(data.metaData.code, u2fauth.register)
|
||||
})
|
||||
}
|
||||
|
||||
u2fauth.submit = function (form, data) {
|
||||
var codeField = document.getElementById(u2fauth.authCodeId)
|
||||
codeField.value = JSON.stringify(data)
|
||||
form.submit()
|
||||
}
|
||||
|
||||
u2fauth.hideKeyname = function () {
|
||||
var keyname = document.getElementById(u2fauth.keynameId)
|
||||
keyname.style.display = 'none'
|
||||
}
|
||||
|
||||
u2fauth.hidePressButton = function () {
|
||||
var pressButton = document.getElementById(u2fauth.pressButtonId)
|
||||
pressButton.style.display = 'none'
|
||||
}
|
||||
|
||||
u2fauth.showPressButton = function () {
|
||||
var pressButton = document.getElementById(u2fauth.pressButtonId)
|
||||
pressButton.style.display = 'block'
|
||||
}
|
||||
|
||||
u2fauth.clearError = function () {
|
||||
var errorDisplay = document.getElementById(u2fauth.errorId)
|
||||
errorDisplay.style.display = 'none'
|
||||
errorDisplay.innerText = ''
|
||||
}
|
||||
|
||||
u2fauth.showError = function (error, callback) {
|
||||
var errorDisplay = document.getElementById(u2fauth.errorId)
|
||||
errorDisplay.style.display = 'block'
|
||||
errorDisplay.innerText = u2fauth.errorTranslation[error]
|
||||
errorDisplay.onclick = callback
|
||||
}
|
||||
|
||||
u2fauth.ready(function () {
|
||||
const form = document.getElementById('u2fForm')
|
||||
if (!form) {
|
||||
return
|
||||
}
|
||||
const type = form.dataset.action
|
||||
|
||||
if (type === 'auth') {
|
||||
u2fauth.authenticate()
|
||||
} else if (type === 'reg' && form.addEventListener) {
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault()
|
||||
u2fauth.register()
|
||||
}, false)
|
||||
}
|
||||
})
|
218
assets/js/webauthn_tfa.js
Normal file
218
assets/js/webauthn_tfa.js
Normal file
|
@ -0,0 +1,218 @@
|
|||
'use strict'
|
||||
|
||||
class WebauthnTFA {
|
||||
|
||||
// Decodes a Base64Url string
|
||||
_base64UrlDecode = (input) => {
|
||||
input = input
|
||||
.replace(/-/g, '+')
|
||||
.replace(/_/g, '/');
|
||||
|
||||
const pad = input.length % 4;
|
||||
if (pad) {
|
||||
if (pad === 1) {
|
||||
throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding');
|
||||
}
|
||||
input += new Array(5-pad).join('=');
|
||||
}
|
||||
|
||||
return window.atob(input);
|
||||
};
|
||||
|
||||
// Converts an array of bytes into a Base64Url string
|
||||
_arrayToBase64String = (a) => btoa(String.fromCharCode(...a));
|
||||
|
||||
// Prepares the public key options object returned by the Webauthn Framework
|
||||
_preparePublicKeyOptions = publicKey => {
|
||||
//Convert challenge from Base64Url string to Uint8Array
|
||||
publicKey.challenge = Uint8Array.from(
|
||||
this._base64UrlDecode(publicKey.challenge),
|
||||
c => c.charCodeAt(0)
|
||||
);
|
||||
|
||||
//Convert the user ID from Base64 string to Uint8Array
|
||||
if (publicKey.user !== undefined) {
|
||||
publicKey.user = {
|
||||
...publicKey.user,
|
||||
id: Uint8Array.from(
|
||||
window.atob(publicKey.user.id),
|
||||
c => c.charCodeAt(0)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
//If excludeCredentials is defined, we convert all IDs to Uint8Array
|
||||
if (publicKey.excludeCredentials !== undefined) {
|
||||
publicKey.excludeCredentials = publicKey.excludeCredentials.map(
|
||||
data => {
|
||||
return {
|
||||
...data,
|
||||
id: Uint8Array.from(
|
||||
this._base64UrlDecode(data.id),
|
||||
c => c.charCodeAt(0)
|
||||
),
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (publicKey.allowCredentials !== undefined) {
|
||||
publicKey.allowCredentials = publicKey.allowCredentials.map(
|
||||
data => {
|
||||
return {
|
||||
...data,
|
||||
id: Uint8Array.from(
|
||||
this._base64UrlDecode(data.id),
|
||||
c => c.charCodeAt(0)
|
||||
),
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return publicKey;
|
||||
};
|
||||
|
||||
// Prepares the public key credentials object returned by the authenticator
|
||||
_preparePublicKeyCredentials = data => {
|
||||
const publicKeyCredential = {
|
||||
id: data.id,
|
||||
type: data.type,
|
||||
rawId: this._arrayToBase64String(new Uint8Array(data.rawId)),
|
||||
response: {
|
||||
clientDataJSON: this._arrayToBase64String(
|
||||
new Uint8Array(data.response.clientDataJSON)
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
if (data.response.attestationObject !== undefined) {
|
||||
publicKeyCredential.response.attestationObject = this._arrayToBase64String(
|
||||
new Uint8Array(data.response.attestationObject)
|
||||
);
|
||||
}
|
||||
|
||||
if (data.response.authenticatorData !== undefined) {
|
||||
publicKeyCredential.response.authenticatorData = this._arrayToBase64String(
|
||||
new Uint8Array(data.response.authenticatorData)
|
||||
);
|
||||
}
|
||||
|
||||
if (data.response.signature !== undefined) {
|
||||
publicKeyCredential.response.signature = this._arrayToBase64String(
|
||||
new Uint8Array(data.response.signature)
|
||||
);
|
||||
}
|
||||
|
||||
if (data.response.userHandle !== undefined) {
|
||||
publicKeyCredential.response.userHandle = this._arrayToBase64String(
|
||||
new Uint8Array(data.response.userHandle)
|
||||
);
|
||||
}
|
||||
|
||||
return publicKeyCredential;
|
||||
};
|
||||
|
||||
|
||||
constructor()
|
||||
{
|
||||
const register_dom_ready = (fn) => {
|
||||
if (document.readyState !== 'loading') {
|
||||
fn();
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', fn);
|
||||
}
|
||||
}
|
||||
|
||||
register_dom_ready(() => {
|
||||
this.registerForms();
|
||||
});
|
||||
}
|
||||
|
||||
registerForms()
|
||||
{
|
||||
//Find all forms which have an data-webauthn-tfa-action attribute
|
||||
const forms = document.querySelectorAll('form[data-webauthn-tfa-action]');
|
||||
|
||||
forms.forEach((form) => {
|
||||
console.debug('Found webauthn TFA form with action: ' + form.getAttribute('data-webauthn-tfa-action'), form);
|
||||
//Ensure that the form has webauthn data
|
||||
|
||||
const dataString = form.getAttribute('data-webauthn-tfa-data')
|
||||
const action = form.getAttribute('data-webauthn-tfa-action');
|
||||
|
||||
if (!dataString) {
|
||||
console.error('Form does not have webauthn data, can not continue!', form);
|
||||
return;
|
||||
}
|
||||
|
||||
//Convert dataString to the needed dataObject
|
||||
const dataObject = JSON.parse(dataString);
|
||||
const options = this._preparePublicKeyOptions(dataObject);
|
||||
|
||||
|
||||
if(action === 'authenticate'){
|
||||
this.authenticate(form, {publicKey: options});
|
||||
}
|
||||
|
||||
if(action === 'register'){
|
||||
//Register submit action, so we can do the registration on submit
|
||||
form.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
this.register(form, {publicKey: options});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
//Catch submit event and do webauthn stuff
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the form with the given result data
|
||||
* @param form
|
||||
* @param data
|
||||
* @private
|
||||
*/
|
||||
_submit(form, data)
|
||||
{
|
||||
const resultField = document.getElementById('_auth_code');
|
||||
resultField.value = JSON.stringify(data)
|
||||
form.submit();
|
||||
}
|
||||
|
||||
authenticate(form, authData)
|
||||
{
|
||||
navigator.credentials.get(authData)
|
||||
.then((credential) => {
|
||||
//Convert our credential to a form which can be JSON encoded
|
||||
let data = this._preparePublicKeyCredentials(credential);
|
||||
|
||||
this._submit(form, data)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("WebAuthn Authentication error: ", error);
|
||||
alert("Error: " + error)
|
||||
});
|
||||
}
|
||||
|
||||
register(form, authData)
|
||||
{
|
||||
navigator.credentials.create(authData)
|
||||
.then((credential) => {
|
||||
//Convert our credential to a form which can be JSON encoded
|
||||
let data = this._preparePublicKeyCredentials(credential);
|
||||
|
||||
this._submit(form, data)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("WebAuthn Registration error: ", error);
|
||||
alert("Error: " + error)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.webauthnTFA = new WebauthnTFA();
|
Loading…
Add table
Add a link
Reference in a new issue