Skip to content

Commit

Permalink
Add support for Web Authentication API
Browse files Browse the repository at this point in the history
Adds a two factor authentication plugin that supports FIDO2/WebAuthn
security keys.

- Fixes phpmyadmin#17229

Signed-off-by: Maurício Meneghini Fauth <mauricio@fauth.dev>
  • Loading branch information
MauricioFauth committed Dec 17, 2022
1 parent c8150fd commit fb62602
Show file tree
Hide file tree
Showing 12 changed files with 468 additions and 4 deletions.
4 changes: 4 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
"plugin:compat/recommended"
],
"plugins": ["no-jquery"],
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "script"
},
"env": {
"browser": true,
"es6": true,
Expand Down
12 changes: 9 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@
"conflict": {
"bacon/bacon-qr-code": "<2.0",
"pragmarx/google2fa-qrcode": "<2.1",
"tecnickcom/tcpdf": "<6.4.4"
"tecnickcom/tcpdf": "<6.4.4",
"web-auth/webauthn-lib": "<3.3"
},
"suggest": {
"ext-curl": "Updates checking",
Expand All @@ -93,7 +94,11 @@
"tecnickcom/tcpdf": "For PDF support",
"pragmarx/google2fa-qrcode": "^2.1 - For 2FA authentication",
"bacon/bacon-qr-code": "^2.0 - For 2FA authentication",
"code-lts/u2f-php-server": "For FIDO U2F authentication"
"code-lts/u2f-php-server": "For FIDO U2F authentication",
"web-auth/webauthn-lib": "^3.3 - For FIDO2/WebAuthn authentication",
"ext-gmp": "For FIDO2/WebAuthn authentication",
"ext-bcmath": "For FIDO2/WebAuthn authentication",
"phpseclib/bcmath_compat": "For FIDO2/WebAuthn authentication"
},
"require-dev": {
"bacon/bacon-qr-code": "^2.0",
Expand All @@ -111,7 +116,8 @@
"squizlabs/php_codesniffer": "~3.6.0",
"symfony/console": "^5.2.3",
"tecnickcom/tcpdf": "^6.4.4",
"vimeo/psalm": "^4.22"
"vimeo/psalm": "^4.22",
"web-auth/webauthn-lib": "^3.3"
},
"extra": {
"branch-alias": {
Expand Down
156 changes: 156 additions & 0 deletions js/src/webauthn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
const base64UrlDecode = (input) => {
// eslint-disable-next-line no-param-reassign
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');
}
// eslint-disable-next-line no-param-reassign
input += new Array(5 - pad).join('=');
}

return window.atob(input);
};

const arrayToBase64String = (a) => window.btoa(String.fromCharCode(...a));

const preparePublicKeyOptions = publicKey => {
// Convert challenge from Base64Url string to Uint8Array
publicKey.challenge = Uint8Array.from(
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(
base64UrlDecode(data.id),
c => c.charCodeAt(0)
),
};
}
);
}

if (publicKey.allowCredentials !== undefined) {
publicKey.allowCredentials = publicKey.allowCredentials.map(
data => {
return {
...data,
id: Uint8Array.from(
base64UrlDecode(data.id),
c => c.charCodeAt(0)
),
};
}
);
}

return publicKey;
};

const preparePublicKeyCredentials = data => {
const publicKeyCredential = {
id: data.id,
type: data.type,
rawId: arrayToBase64String(new Uint8Array(data.rawId)),
response: {
clientDataJSON: arrayToBase64String(
new Uint8Array(data.response.clientDataJSON)
),
},
};

if (data.response.attestationObject !== undefined) {
publicKeyCredential.response.attestationObject = arrayToBase64String(
new Uint8Array(data.response.attestationObject)
);
}

if (data.response.authenticatorData !== undefined) {
publicKeyCredential.response.authenticatorData = arrayToBase64String(
new Uint8Array(data.response.authenticatorData)
);
}

if (data.response.signature !== undefined) {
publicKeyCredential.response.signature = arrayToBase64String(
new Uint8Array(data.response.signature)
);
}

if (data.response.userHandle !== undefined) {
publicKeyCredential.response.userHandle = arrayToBase64String(
new Uint8Array(data.response.userHandle)
);
}

return publicKeyCredential;
};

const createPublicKeyCredential = async function (publicKey) {
// eslint-disable-next-line compat/compat
const credentials = await navigator.credentials.create({ publicKey: publicKey });

return preparePublicKeyCredentials(credentials);
};

const getPublicKeyCredential = async function (publicKey) {
// eslint-disable-next-line compat/compat
const credentials = await navigator.credentials.get({ publicKey: publicKey });

return preparePublicKeyCredentials(credentials);
};

AJAX.registerOnload('webauthn.js', function () {
const $inputReg = $('#webauthn_registration_response');
if ($inputReg.length > 0) {
const $formReg = $inputReg.parents('form');
$formReg.find('input[type=submit]').hide();

const webauthnOptionsJson = $inputReg.attr('data-webauthn-options');
const webauthnOptions = JSON.parse(webauthnOptionsJson);
const publicKey = preparePublicKeyOptions(webauthnOptions);
const publicKeyCredential = createPublicKeyCredential(publicKey);
publicKeyCredential
.then((data) => {
$inputReg.val(JSON.stringify(data));
$formReg.trigger('submit');
})
.catch((error) => Functions.ajaxShowMessage(error, false, 'error'));
}

const $inputAuth = $('#webauthn_authentication_response');
if ($inputAuth.length > 0) {
const $formAuth = $inputAuth.parents('form');
$formAuth.find('input[type=submit]').hide();

const webauthnRequestJson = $inputAuth.attr('data-webauthn-request');
const webauthnRequest = JSON.parse(webauthnRequestJson);
const publicKey = preparePublicKeyOptions(webauthnRequest);
const publicKeyCredential = getPublicKeyCredential(publicKey);
publicKeyCredential
.then((data) => {
$inputAuth.val(JSON.stringify(data));
$formAuth.trigger('submit');
})
.catch((error) => Functions.ajaxShowMessage(error, false, 'error'));
}
});
2 changes: 1 addition & 1 deletion libraries/classes/Plugins/TwoFactor/Key.php
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,6 @@ public static function getName()
*/
public static function getDescription()
{
return __('Provides authentication using hardware security tokens supporting FIDO U2F, such as a Yubikey.');
return __('Provides authentication using hardware security tokens supporting FIDO U2F, such as a YubiKey.');
}
}
Loading

0 comments on commit fb62602

Please sign in to comment.