Skip to content

Commit

Permalink
First commit
Browse files Browse the repository at this point in the history
  • Loading branch information
sjinks committed Jan 23, 2022
0 parents commit d91fbb6
Show file tree
Hide file tree
Showing 52 changed files with 20,476 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*.js
/node_modules/
/vendor/
3 changes: 3 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": [ "plugin:@wordpress/eslint-plugin/recommended" ]
}
15 changes: 15 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
*.sh text eol=lf
*.xml.dist export-ignore
/.* export-ignore
/assets/*.ts export-ignore
/assets/*.map export-ignore
/bin/ export-ignore
/lang/*.po export-ignore
/lang/*.pot export-ignore
/lang/Makefile export-ignore
/stubs/ export-ignore
/docker-compose.yml export-ignore
/rollup.config.mjs export-ignore
/package-lock.json export-ignore
/package.json export-ignore
/tsconfig.json export-ignore
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/node_modules
/vendor
/.wp-env.json
7 changes: 7 additions & 0 deletions .prettierrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
semi: true,
trailingComma: 'all',
singleQuote: true,
printWidth: 120,
tabWidth: 4,
};
153 changes: 153 additions & 0 deletions assets/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import {
L_NOT_ALLOWED_ERROR,
L_SECURITY_ERROR,
L_NOT_SUPPORTED_ERROR,
L_ABORT_ERROR,
L_UNKNOWN_KEY,
L_KEY_ALREADY_REGISTERED,
} from './lang';
interface PublicKeyCredentialUserEntityPlain extends PublicKeyCredentialEntity {
displayName: string;
id: string;
}

interface PublicKeyCredentialDescriptorPlain {
id: string;
transports?: AuthenticatorTransport[];
type: PublicKeyCredentialType;
}

export interface PublicKeyCredentialCreationOptionsPlain {
attestation?: AttestationConveyancePreference;
authenticatorSelection?: AuthenticatorSelectionCriteria;
challenge: string;
excludeCredentials?: PublicKeyCredentialDescriptorPlain[];
extensions?: AuthenticationExtensionsClientInputs;
pubKeyCredParams: PublicKeyCredentialParameters[];
rp: PublicKeyCredentialRpEntity;
timeout?: number;
user: PublicKeyCredentialUserEntityPlain;
}

export interface PublicKeyCredentialRequestOptionsPlain {
allowCredentials?: PublicKeyCredentialDescriptorPlain[];
challenge: string;
extensions?: AuthenticationExtensionsClientInputs;
rpId?: string;
timeout?: number;
userVerification?: UserVerificationRequirement;
}

interface AuthenticatorResponsePlain {
clientDataJSON: string;
}

interface AuthenticatorAttestationResponsePlain extends AuthenticatorResponsePlain {
attestationObject: string;
}

interface AuthenticatorAssertionResponsePlain extends AuthenticatorResponsePlain {
authenticatorData: string;
signature: string;
userHandle: string | null | undefined;
}

export interface PublicKeyCredentialPlain extends Credential {
rawId: string;
response: Partial<AuthenticatorAttestationResponsePlain & AuthenticatorAssertionResponsePlain>;
clientExtensionResults: AuthenticationExtensionsClientOutputs;
}

export function arrayToBase64String(a: Uint8Array): string {
return window.btoa(String.fromCharCode(...a));
}

export function base64UrlDecode(input: string): string {
return window.atob(input.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat(3 - ((3 + input.length) % 4)));
}

export function stringToBufferMapper(c: string): number {
return c.charCodeAt(0);
}

export function preparePublicKeyCreationOptions(
publicKey: PublicKeyCredentialCreationOptionsPlain,
): PublicKeyCredentialCreationOptions {
return {
...publicKey,
user: {
...publicKey.user,
id: Uint8Array.from(base64UrlDecode(publicKey.user.id), stringToBufferMapper),
},
challenge: Uint8Array.from(base64UrlDecode(publicKey.challenge), stringToBufferMapper),
excludeCredentials: publicKey.excludeCredentials?.map(
(data: PublicKeyCredentialDescriptorPlain): PublicKeyCredentialDescriptor => ({
...data,
id: Uint8Array.from(base64UrlDecode(data.id), stringToBufferMapper),
}),
),
};
}

export function preparePublicKeyCredentialRequestOptions(
publicKey: PublicKeyCredentialRequestOptionsPlain,
): PublicKeyCredentialRequestOptions {
return {
...publicKey,
challenge: Uint8Array.from(base64UrlDecode(publicKey.challenge), stringToBufferMapper).buffer,
allowCredentials: publicKey.allowCredentials?.map(
(data: PublicKeyCredentialDescriptorPlain): PublicKeyCredentialDescriptor => ({
...data,
id: Uint8Array.from(base64UrlDecode(data.id), stringToBufferMapper).buffer,
}),
),
};
}

export function preparePublicKeyCredential(data: PublicKeyCredential): PublicKeyCredentialPlain {
const response = data.response as AuthenticatorAssertionResponse | AuthenticatorAttestationResponse;
return {
id: data.id,
type: data.type,
rawId: arrayToBase64String(new Uint8Array(data.rawId)),
clientExtensionResults: data.getClientExtensionResults(),
response: {
attestationObject:
'attestationObject' in response
? arrayToBase64String(new Uint8Array(response.attestationObject))
: undefined,
authenticatorData:
'authenticatorData' in response
? arrayToBase64String(new Uint8Array(response.authenticatorData))
: undefined,
signature: 'signature' in response ? arrayToBase64String(new Uint8Array(response.signature)) : undefined,
userHandle:
'userHandle' in response && response.userHandle
? arrayToBase64String(new Uint8Array(response.userHandle))
: undefined,
clientDataJSON: arrayToBase64String(new Uint8Array(data.response.clientDataJSON)),
},
};
}

export function decodeDOMException(e: DOMException, isAuth: boolean): string {
switch (e.name) {
case 'NotAllowedError':
return L_NOT_ALLOWED_ERROR;

case 'SecurityError':
return L_SECURITY_ERROR;

case 'NotSupportedError':
return L_NOT_SUPPORTED_ERROR;

case 'AbortError':
return L_ABORT_ERROR;

case 'InvalidStateError':
return isAuth ? L_UNKNOWN_KEY : L_KEY_ALREADY_REGISTERED;

default:
return e.message;
}
}
19 changes: 19 additions & 0 deletions assets/lang.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { __ } from '@wordpress/i18n';

export const L_WEBAUTHN_NOT_SUPPORTED = __('WebAuthn is not supported by the browser.', '2fa-wa-js');
export const L_UNABLE_TO_GET_PK_CREDENTIAL = __('Unable to get a public key credential.', '2fa-wa-js');
export const L_NOT_ALLOWED_ERROR = __('The request is not allowed.', '2fa-wa-js');
export const L_SECURITY_ERROR = __('The operation is insecure.', '2fa-wa-js');
export const L_NOT_SUPPORTED_ERROR = __('The operation is not supported.', '2fa-wa-js');
export const L_ABORT_ERROR = __('The operation was canceled.', '2fa-wa-js');
export const L_UNKNOWN_KEY = __('You cannot use this key to log in.', '2fa-wa-js');
export const L_KEY_ALREADY_REGISTERED = __('This key is already registered.', '2fa-wa-js');
export const L_UNKNOWN_ERROR = __('This key is already registered.', '2fa-wa-js');
export const L_FETCHING_REG_INFO = __('Fetching registration information…', '2fa-wa-js');
export const L_GENERATING_CREDENTIALS = __('Generating credentials…', '2fa-wa-js');
export const L_REGISTERING_CREDENTIALS = __('Registering credentials…', '2fa-wa-js');
export const L_FAILED_TO_CREATE_CREDENTIALS = __('Unable to create public key credentials', '2fa-wa-js');
export const L_KEY_REGISTERED = __('The key has been registered.', '2fa-wa-js');
export const L_SENDING_REQUEST = __('Sending request…', '2fa-wa-js');
export const L_KEY_REVOKED = __('The key has been revoked.', '2fa-wa-js');
export const L_KEY_RENAMED = __('The key has been renamed.', '2fa-wa-js');
1 change: 1 addition & 0 deletions assets/login.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions assets/login.min.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

69 changes: 69 additions & 0 deletions assets/login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {
PublicKeyCredentialRequestOptionsPlain,
preparePublicKeyCredentialRequestOptions,
preparePublicKeyCredential,
decodeDOMException,
} from './common';
import { L_WEBAUTHN_NOT_SUPPORTED, L_UNABLE_TO_GET_PK_CREDENTIAL } from './lang';

// eslint-disable-next-line camelcase
declare let tfa_webauthn: {
options: PublicKeyCredentialRequestOptionsPlain;
};

function showError(error: string): void {
const el = document.getElementById('login_error');
if (el && el.parentNode) {
el.parentNode.removeChild(el);
}

const form = document.getElementById('loginform');
if (form) {
form.insertAdjacentHTML('beforebegin', '<div id="login_error" role="alert"><p>' + error + '</p></div>');
}
}

function startAuthentication(): void {
const loginForm = document.getElementById('loginform') as HTMLFormElement;
const publicKey = preparePublicKeyCredentialRequestOptions(tfa_webauthn.options);
(
navigator.credentials.get({
publicKey,
}) as Promise<PublicKeyCredential | null>
)
.then((credential) => {
if (credential) {
(document.getElementById('webauthn_response') as HTMLInputElement).value = JSON.stringify(
preparePublicKeyCredential(credential),
);
loginForm.submit();
} else {
throw new Error(L_UNABLE_TO_GET_PK_CREDENTIAL);
}
})
.catch((e: Error) => {
const message = e instanceof DOMException ? decodeDOMException(e, true) : e.message;
showError(message);
(document.getElementById('webauthn-retry') as HTMLDivElement).removeAttribute('hidden');
});
}

const callback = (): void => {
const retryButton = document.querySelector('#webauthn-retry .button') as HTMLButtonElement;
retryButton.addEventListener('click', (e) => {
(document.getElementById('webauthn-retry') as HTMLDivElement).setAttribute('hidden', 'hidden');
startAuthentication();
});

if ('credentials' in navigator) {
startAuthentication();
} else {
showError(L_WEBAUTHN_NOT_SUPPORTED);
}
};

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', callback);
} else {
callback();
}
Loading

0 comments on commit d91fbb6

Please sign in to comment.