Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-09-19 04:45:44 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-09-19 04:45:44 +0300
commit85dc423f7090da0a52c73eb66faf22ddb20efff9 (patch)
tree9160f299afd8c80c038f08e1545be119f5e3f1e1 /app/assets/javascripts/authentication
parent15c2c8c66dbe422588e5411eee7e68f1fa440bb8 (diff)
Add latest changes from gitlab-org/gitlab@13-4-stable-ee
Diffstat (limited to 'app/assets/javascripts/authentication')
-rw-r--r--app/assets/javascripts/authentication/mount_2fa.js19
-rw-r--r--app/assets/javascripts/authentication/u2f/authenticate.js3
-rw-r--r--app/assets/javascripts/authentication/u2f/register.js26
-rw-r--r--app/assets/javascripts/authentication/webauthn/authenticate.js69
-rw-r--r--app/assets/javascripts/authentication/webauthn/error.js28
-rw-r--r--app/assets/javascripts/authentication/webauthn/flow.js24
-rw-r--r--app/assets/javascripts/authentication/webauthn/index.js13
-rw-r--r--app/assets/javascripts/authentication/webauthn/register.js78
-rw-r--r--app/assets/javascripts/authentication/webauthn/util.js120
9 files changed, 364 insertions, 16 deletions
diff --git a/app/assets/javascripts/authentication/mount_2fa.js b/app/assets/javascripts/authentication/mount_2fa.js
index 9917151ac81..dd5a42fa5fc 100644
--- a/app/assets/javascripts/authentication/mount_2fa.js
+++ b/app/assets/javascripts/authentication/mount_2fa.js
@@ -1,14 +1,23 @@
import $ from 'jquery';
import initU2F from './u2f';
+import initWebauthn from './webauthn';
import U2FRegister from './u2f/register';
+import WebAuthnRegister from './webauthn/register';
export const mount2faAuthentication = () => {
- // Soon this will conditionally mount a webauthn app (see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26692)
- initU2F();
+ if (gon.webauthn) {
+ initWebauthn();
+ } else {
+ initU2F();
+ }
};
export const mount2faRegistration = () => {
- // Soon this will conditionally mount a webauthn app (see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26692)
- const u2fRegister = new U2FRegister($('#js-register-u2f'), gon.u2f);
- u2fRegister.start();
+ if (gon.webauthn) {
+ const webauthnRegister = new WebAuthnRegister($('#js-register-token-2fa'), gon.webauthn);
+ webauthnRegister.start();
+ } else {
+ const u2fRegister = new U2FRegister($('#js-register-token-2fa'), gon.u2f);
+ u2fRegister.start();
+ }
};
diff --git a/app/assets/javascripts/authentication/u2f/authenticate.js b/app/assets/javascripts/authentication/u2f/authenticate.js
index 201cd5c2e61..f9b5ca3e5b4 100644
--- a/app/assets/javascripts/authentication/u2f/authenticate.js
+++ b/app/assets/javascripts/authentication/u2f/authenticate.js
@@ -40,7 +40,6 @@ export default class U2FAuthenticate {
this.signRequests = u2fParams.sign_requests.map(request => omit(request, 'challenge'));
this.templates = {
- setup: '#js-authenticate-token-2fa-setup',
inProgress: '#js-authenticate-token-2fa-in-progress',
error: '#js-authenticate-token-2fa-error',
authenticated: '#js-authenticate-token-2fa-authenticated',
@@ -86,7 +85,7 @@ export default class U2FAuthenticate {
renderError(error) {
this.renderTemplate('error', {
error_message: error.message(),
- error_code: error.errorCode,
+ error_name: error.errorCode,
});
return this.container.find('#js-token-2fa-try-again').on('click', this.renderInProgress);
}
diff --git a/app/assets/javascripts/authentication/u2f/register.js b/app/assets/javascripts/authentication/u2f/register.js
index 52c0ce1fc04..9773a9185f8 100644
--- a/app/assets/javascripts/authentication/u2f/register.js
+++ b/app/assets/javascripts/authentication/u2f/register.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import { template as lodashTemplate } from 'lodash';
+import { __ } from '~/locale';
import importU2FLibrary from './util';
import U2FError from './error';
@@ -24,11 +25,10 @@ export default class U2FRegister {
this.signRequests = u2fParams.sign_requests;
this.templates = {
- notSupported: '#js-register-u2f-not-supported',
- setup: '#js-register-u2f-setup',
- inProgress: '#js-register-u2f-in-progress',
- error: '#js-register-u2f-error',
- registered: '#js-register-u2f-registered',
+ message: '#js-register-2fa-message',
+ setup: '#js-register-token-2fa-setup',
+ error: '#js-register-token-2fa-error',
+ registered: '#js-register-token-2fa-registered',
};
}
@@ -65,18 +65,22 @@ export default class U2FRegister {
renderSetup() {
this.renderTemplate('setup');
- return this.container.find('#js-setup-u2f-device').on('click', this.renderInProgress);
+ return this.container.find('#js-setup-token-2fa-device').on('click', this.renderInProgress);
}
renderInProgress() {
- this.renderTemplate('inProgress');
+ this.renderTemplate('message', {
+ message: __(
+ 'Trying to communicate with your device. Plug it in (if needed) and press the button on the device now.',
+ ),
+ });
return this.register();
}
renderError(error) {
this.renderTemplate('error', {
error_message: error.message(),
- error_code: error.errorCode,
+ error_name: error.errorCode,
});
return this.container.find('#js-token-2fa-try-again').on('click', this.renderSetup);
}
@@ -89,6 +93,10 @@ export default class U2FRegister {
}
renderNotSupported() {
- return this.renderTemplate('notSupported');
+ return this.renderTemplate('message', {
+ message: __(
+ "Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).",
+ ),
+ });
}
}
diff --git a/app/assets/javascripts/authentication/webauthn/authenticate.js b/app/assets/javascripts/authentication/webauthn/authenticate.js
new file mode 100644
index 00000000000..42c4c2b63bd
--- /dev/null
+++ b/app/assets/javascripts/authentication/webauthn/authenticate.js
@@ -0,0 +1,69 @@
+import WebAuthnError from './error';
+import WebAuthnFlow from './flow';
+import { supported, convertGetParams, convertGetResponse } from './util';
+
+// Authenticate WebAuthn devices for users to authenticate with.
+//
+// State Flow #1: setup -> in_progress -> authenticated -> POST to server
+// State Flow #2: setup -> in_progress -> error -> setup
+export default class WebAuthnAuthenticate {
+ constructor(container, form, webauthnParams, fallbackButton, fallbackUI) {
+ this.container = container;
+ this.webauthnParams = convertGetParams(JSON.parse(webauthnParams.options));
+ this.renderInProgress = this.renderInProgress.bind(this);
+
+ this.form = form;
+ this.fallbackButton = fallbackButton;
+ this.fallbackUI = fallbackUI;
+ if (this.fallbackButton) {
+ this.fallbackButton.addEventListener('click', this.switchToFallbackUI.bind(this));
+ }
+
+ this.flow = new WebAuthnFlow(container, {
+ inProgress: '#js-authenticate-token-2fa-in-progress',
+ error: '#js-authenticate-token-2fa-error',
+ authenticated: '#js-authenticate-token-2fa-authenticated',
+ });
+
+ this.container.on('click', '#js-token-2fa-try-again', this.renderInProgress);
+ }
+
+ start() {
+ if (!supported()) {
+ this.switchToFallbackUI();
+ } else {
+ this.renderInProgress();
+ }
+ }
+
+ authenticate() {
+ navigator.credentials
+ .get({ publicKey: this.webauthnParams })
+ .then(resp => {
+ const convertedResponse = convertGetResponse(resp);
+ this.renderAuthenticated(JSON.stringify(convertedResponse));
+ })
+ .catch(err => {
+ this.flow.renderError(new WebAuthnError(err, 'authenticate'));
+ });
+ }
+
+ renderInProgress() {
+ this.flow.renderTemplate('inProgress');
+ this.authenticate();
+ }
+
+ renderAuthenticated(deviceResponse) {
+ this.flow.renderTemplate('authenticated');
+ const container = this.container[0];
+ container.querySelector('#js-device-response').value = deviceResponse;
+ container.querySelector(this.form).submit();
+ this.fallbackButton.classList.add('hidden');
+ }
+
+ switchToFallbackUI() {
+ this.fallbackButton.classList.add('hidden');
+ this.container[0].classList.add('hidden');
+ this.fallbackUI.classList.remove('hidden');
+ }
+}
diff --git a/app/assets/javascripts/authentication/webauthn/error.js b/app/assets/javascripts/authentication/webauthn/error.js
new file mode 100644
index 00000000000..a1a3f861c25
--- /dev/null
+++ b/app/assets/javascripts/authentication/webauthn/error.js
@@ -0,0 +1,28 @@
+import { __ } from '~/locale';
+import { isHTTPS, FLOW_AUTHENTICATE, FLOW_REGISTER } from './util';
+
+export default class WebAuthnError {
+ constructor(error, flowType) {
+ this.error = error;
+ this.errorName = error.name || 'UnknownError';
+ this.message = this.message.bind(this);
+ this.httpsDisabled = !isHTTPS();
+ this.flowType = flowType;
+ }
+
+ message() {
+ if (this.errorName === 'NotSupportedError') {
+ return __('Your device is not compatible with GitLab. Please try another device');
+ } else if (this.errorName === 'InvalidStateError' && this.flowType === FLOW_AUTHENTICATE) {
+ return __('This device has not been registered with us.');
+ } else if (this.errorName === 'InvalidStateError' && this.flowType === FLOW_REGISTER) {
+ return __('This device has already been registered with us.');
+ } else if (this.errorName === 'SecurityError' && this.httpsDisabled) {
+ return __(
+ 'WebAuthn only works with HTTPS-enabled websites. Contact your administrator for more details.',
+ );
+ }
+
+ return __('There was a problem communicating with your device.');
+ }
+}
diff --git a/app/assets/javascripts/authentication/webauthn/flow.js b/app/assets/javascripts/authentication/webauthn/flow.js
new file mode 100644
index 00000000000..10a1debc876
--- /dev/null
+++ b/app/assets/javascripts/authentication/webauthn/flow.js
@@ -0,0 +1,24 @@
+import { template } from 'lodash';
+
+/**
+ * Generic abstraction for WebAuthnFlows, especially for register / authenticate
+ */
+export default class WebAuthnFlow {
+ constructor(container, templates) {
+ this.container = container;
+ this.templates = templates;
+ }
+
+ renderTemplate(name, params) {
+ const templateString = document.querySelector(this.templates[name]).innerHTML;
+ const compiledTemplate = template(templateString);
+ this.container.html(compiledTemplate(params));
+ }
+
+ renderError(error) {
+ this.renderTemplate('error', {
+ error_message: error.message(),
+ error_name: error.errorName,
+ });
+ }
+}
diff --git a/app/assets/javascripts/authentication/webauthn/index.js b/app/assets/javascripts/authentication/webauthn/index.js
new file mode 100644
index 00000000000..bbf694c7698
--- /dev/null
+++ b/app/assets/javascripts/authentication/webauthn/index.js
@@ -0,0 +1,13 @@
+import $ from 'jquery';
+import WebAuthnAuthenticate from './authenticate';
+
+export default () => {
+ const webauthnAuthenticate = new WebAuthnAuthenticate(
+ $('#js-authenticate-token-2fa'),
+ '#js-login-token-2fa-form',
+ gon.webauthn,
+ document.querySelector('#js-login-2fa-device'),
+ document.querySelector('.js-2fa-form'),
+ );
+ webauthnAuthenticate.start();
+};
diff --git a/app/assets/javascripts/authentication/webauthn/register.js b/app/assets/javascripts/authentication/webauthn/register.js
new file mode 100644
index 00000000000..06e4ffd6f3e
--- /dev/null
+++ b/app/assets/javascripts/authentication/webauthn/register.js
@@ -0,0 +1,78 @@
+import { __ } from '~/locale';
+import WebAuthnError from './error';
+import WebAuthnFlow from './flow';
+import { supported, isHTTPS, convertCreateParams, convertCreateResponse } from './util';
+
+// Register WebAuthn devices for users to authenticate with.
+//
+// State Flow #1: setup -> in_progress -> registered -> POST to server
+// State Flow #2: setup -> in_progress -> error -> setup
+export default class WebAuthnRegister {
+ constructor(container, webauthnParams) {
+ this.container = container;
+ this.renderInProgress = this.renderInProgress.bind(this);
+ this.webauthnOptions = convertCreateParams(webauthnParams.options);
+
+ this.flow = new WebAuthnFlow(container, {
+ message: '#js-register-2fa-message',
+ setup: '#js-register-token-2fa-setup',
+ error: '#js-register-token-2fa-error',
+ registered: '#js-register-token-2fa-registered',
+ });
+
+ this.container.on('click', '#js-token-2fa-try-again', this.renderInProgress);
+ }
+
+ start() {
+ if (!supported()) {
+ // we show a special error message when the user visits the site
+ // using a non-ssl connection as this makes WebAuthn unavailable in
+ // any case, regardless of the used browser
+ this.renderNotSupported(!isHTTPS());
+ } else {
+ this.renderSetup();
+ }
+ }
+
+ register() {
+ navigator.credentials
+ .create({
+ publicKey: this.webauthnOptions,
+ })
+ .then(cred => this.renderRegistered(JSON.stringify(convertCreateResponse(cred))))
+ .catch(err => this.flow.renderError(new WebAuthnError(err, 'register')));
+ }
+
+ renderSetup() {
+ this.flow.renderTemplate('setup');
+ this.container.find('#js-setup-token-2fa-device').on('click', this.renderInProgress);
+ }
+
+ renderInProgress() {
+ this.flow.renderTemplate('message', {
+ message: __(
+ 'Trying to communicate with your device. Plug it in (if needed) and press the button on the device now.',
+ ),
+ });
+ return this.register();
+ }
+
+ renderRegistered(deviceResponse) {
+ this.flow.renderTemplate('registered');
+ // Prefer to do this instead of interpolating using Underscore templates
+ // because of JSON escaping issues.
+ this.container.find('#js-device-response').val(deviceResponse);
+ }
+
+ renderNotSupported(noHttps) {
+ const message = noHttps
+ ? __(
+ 'WebAuthn only works with HTTPS-enabled websites. Contact your administrator for more details.',
+ )
+ : __(
+ "Your browser doesn't support WebAuthn. Please use a supported browser, e.g. Chrome (67+) or Firefox (60+).",
+ );
+
+ this.flow.renderTemplate('message', { message });
+ }
+}
diff --git a/app/assets/javascripts/authentication/webauthn/util.js b/app/assets/javascripts/authentication/webauthn/util.js
new file mode 100644
index 00000000000..5f06c000afe
--- /dev/null
+++ b/app/assets/javascripts/authentication/webauthn/util.js
@@ -0,0 +1,120 @@
+export function supported() {
+ return Boolean(
+ navigator.credentials &&
+ navigator.credentials.create &&
+ navigator.credentials.get &&
+ window.PublicKeyCredential,
+ );
+}
+
+export function isHTTPS() {
+ return window.location.protocol.startsWith('https');
+}
+
+export const FLOW_AUTHENTICATE = 'authenticate';
+export const FLOW_REGISTER = 'register';
+
+// adapted from https://stackoverflow.com/a/21797381/8204697
+function base64ToBuffer(base64) {
+ const binaryString = window.atob(base64);
+ const len = binaryString.length;
+ const bytes = new Uint8Array(len);
+ for (let i = 0; i < len; i += 1) {
+ bytes[i] = binaryString.charCodeAt(i);
+ }
+ return bytes.buffer;
+}
+
+// adapted from https://stackoverflow.com/a/9458996/8204697
+function bufferToBase64(buffer) {
+ if (typeof buffer === 'string') {
+ return buffer;
+ }
+
+ let binary = '';
+ const bytes = new Uint8Array(buffer);
+ const len = bytes.byteLength;
+ for (let i = 0; i < len; i += 1) {
+ binary += String.fromCharCode(bytes[i]);
+ }
+ return window.btoa(binary);
+}
+
+/**
+ * Returns a copy of the given object with the id property converted to buffer
+ *
+ * @param {Object} param
+ */
+function convertIdToBuffer({ id, ...rest }) {
+ return {
+ ...rest,
+ id: base64ToBuffer(id),
+ };
+}
+
+/**
+ * Returns a copy of the given array with all `id`s of the items converted to buffer
+ *
+ * @param {Array} items
+ */
+function convertIdsToBuffer(items) {
+ return items.map(convertIdToBuffer);
+}
+
+/**
+ * Returns an object with keys of the given props, and values from the given object converted to base64
+ *
+ * @param {String} obj
+ * @param {Array} props
+ */
+function convertPropertiesToBase64(obj, props) {
+ return props.reduce(
+ (acc, property) => Object.assign(acc, { [property]: bufferToBase64(obj[property]) }),
+ {},
+ );
+}
+
+export function convertGetParams({ allowCredentials, challenge, ...rest }) {
+ return {
+ ...rest,
+ ...(allowCredentials ? { allowCredentials: convertIdsToBuffer(allowCredentials) } : {}),
+ challenge: base64ToBuffer(challenge),
+ };
+}
+
+export function convertGetResponse(webauthnResponse) {
+ return {
+ type: webauthnResponse.type,
+ id: webauthnResponse.id,
+ rawId: bufferToBase64(webauthnResponse.rawId),
+ response: convertPropertiesToBase64(webauthnResponse.response, [
+ 'clientDataJSON',
+ 'authenticatorData',
+ 'signature',
+ 'userHandle',
+ ]),
+ clientExtensionResults: webauthnResponse.getClientExtensionResults(),
+ };
+}
+
+export function convertCreateParams({ challenge, user, excludeCredentials, ...rest }) {
+ return {
+ ...rest,
+ challenge: base64ToBuffer(challenge),
+ user: convertIdToBuffer(user),
+ ...(excludeCredentials ? { excludeCredentials: convertIdsToBuffer(excludeCredentials) } : {}),
+ };
+}
+
+export function convertCreateResponse(webauthnResponse) {
+ return {
+ type: webauthnResponse.type,
+ id: webauthnResponse.id,
+ rawId: bufferToBase64(webauthnResponse.rawId),
+ clientExtensionResults: webauthnResponse.getClientExtensionResults(),
+ response: convertPropertiesToBase64(webauthnResponse.response, [
+ 'clientDataJSON',
+ 'attestationObject',
+ ]),
+ };
+}