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-05-29 09:08:16 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-05-29 09:08:16 +0300
commit2958635884b1dc5c8c01b5f00f0b7b7359556c37 (patch)
treefc875db03082af0ea8861a9cbf32d7c58e98d461 /app/assets/javascripts/authentication
parentc70ed55242619ebd139c029a1f3c531d6b81a327 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/authentication')
-rw-r--r--app/assets/javascripts/authentication/mount_2fa.js14
-rw-r--r--app/assets/javascripts/authentication/u2f/authenticate.js107
-rw-r--r--app/assets/javascripts/authentication/u2f/error.js26
-rw-r--r--app/assets/javascripts/authentication/u2f/index.js17
-rw-r--r--app/assets/javascripts/authentication/u2f/register.js94
-rw-r--r--app/assets/javascripts/authentication/u2f/util.js40
6 files changed, 298 insertions, 0 deletions
diff --git a/app/assets/javascripts/authentication/mount_2fa.js b/app/assets/javascripts/authentication/mount_2fa.js
new file mode 100644
index 00000000000..9917151ac81
--- /dev/null
+++ b/app/assets/javascripts/authentication/mount_2fa.js
@@ -0,0 +1,14 @@
+import $ from 'jquery';
+import initU2F from './u2f';
+import U2FRegister from './u2f/register';
+
+export const mount2faAuthentication = () => {
+ // Soon this will conditionally mount a webauthn app (see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26692)
+ 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();
+};
diff --git a/app/assets/javascripts/authentication/u2f/authenticate.js b/app/assets/javascripts/authentication/u2f/authenticate.js
new file mode 100644
index 00000000000..6244df1180e
--- /dev/null
+++ b/app/assets/javascripts/authentication/u2f/authenticate.js
@@ -0,0 +1,107 @@
+import $ from 'jquery';
+import { template as lodashTemplate, omit } from 'lodash';
+import importU2FLibrary from './util';
+import U2FError from './error';
+
+// Authenticate U2F (universal 2nd factor) 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 U2FAuthenticate {
+ constructor(container, form, u2fParams, fallbackButton, fallbackUI) {
+ this.u2fUtils = null;
+ this.container = container;
+ this.renderAuthenticated = this.renderAuthenticated.bind(this);
+ this.renderError = this.renderError.bind(this);
+ this.renderInProgress = this.renderInProgress.bind(this);
+ this.renderTemplate = this.renderTemplate.bind(this);
+ this.authenticate = this.authenticate.bind(this);
+ this.start = this.start.bind(this);
+ this.appId = u2fParams.app_id;
+ this.challenge = u2fParams.challenge;
+ this.form = form;
+ this.fallbackButton = fallbackButton;
+ this.fallbackUI = fallbackUI;
+ if (this.fallbackButton) {
+ this.fallbackButton.addEventListener('click', this.switchToFallbackUI.bind(this));
+ }
+
+ // The U2F Javascript API v1.1 requires a single challenge, with
+ // _no challenges per-request_. The U2F Javascript API v1.0 requires a
+ // challenge per-request, which is done by copying the single challenge
+ // into every request.
+ //
+ // In either case, we don't need the per-request challenges that the server
+ // has generated, so we can remove them.
+ //
+ // Note: The server library fixes this behaviour in (unreleased) version 1.0.0.
+ // This can be removed once we upgrade.
+ // https://github.com/castle/ruby-u2f/commit/103f428071a81cd3d5f80c2e77d522d5029946a4
+ this.signRequests = u2fParams.sign_requests.map(request => omit(request, 'challenge'));
+
+ this.templates = {
+ setup: '#js-authenticate-u2f-setup',
+ inProgress: '#js-authenticate-u2f-in-progress',
+ error: '#js-authenticate-u2f-error',
+ authenticated: '#js-authenticate-u2f-authenticated',
+ };
+ }
+
+ start() {
+ return importU2FLibrary()
+ .then(utils => {
+ this.u2fUtils = utils;
+ this.renderInProgress();
+ })
+ .catch(() => this.switchToFallbackUI());
+ }
+
+ authenticate() {
+ return this.u2fUtils.sign(
+ this.appId,
+ this.challenge,
+ this.signRequests,
+ response => {
+ if (response.errorCode) {
+ const error = new U2FError(response.errorCode, 'authenticate');
+ return this.renderError(error);
+ }
+ return this.renderAuthenticated(JSON.stringify(response));
+ },
+ 10,
+ );
+ }
+
+ renderTemplate(name, params) {
+ const templateString = $(this.templates[name]).html();
+ const template = lodashTemplate(templateString);
+ return this.container.html(template(params));
+ }
+
+ renderInProgress() {
+ this.renderTemplate('inProgress');
+ return this.authenticate();
+ }
+
+ renderError(error) {
+ this.renderTemplate('error', {
+ error_message: error.message(),
+ error_code: error.errorCode,
+ });
+ return this.container.find('#js-u2f-try-again').on('click', this.renderInProgress);
+ }
+
+ renderAuthenticated(deviceResponse) {
+ this.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/u2f/error.js b/app/assets/javascripts/authentication/u2f/error.js
new file mode 100644
index 00000000000..ca0fc0700ad
--- /dev/null
+++ b/app/assets/javascripts/authentication/u2f/error.js
@@ -0,0 +1,26 @@
+import { __ } from '~/locale';
+
+export default class U2FError {
+ constructor(errorCode, u2fFlowType) {
+ this.errorCode = errorCode;
+ this.message = this.message.bind(this);
+ this.httpsDisabled = window.location.protocol !== 'https:';
+ this.u2fFlowType = u2fFlowType;
+ }
+
+ message() {
+ if (this.errorCode === window.u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled) {
+ return __(
+ 'U2F only works with HTTPS-enabled websites. Contact your administrator for more details.',
+ );
+ } else if (this.errorCode === window.u2f.ErrorCodes.DEVICE_INELIGIBLE) {
+ if (this.u2fFlowType === 'authenticate') {
+ return __('This device has not been registered with us.');
+ }
+ if (this.u2fFlowType === 'register') {
+ return __('This device has already been registered with us.');
+ }
+ }
+ return __('There was a problem communicating with your device.');
+ }
+}
diff --git a/app/assets/javascripts/authentication/u2f/index.js b/app/assets/javascripts/authentication/u2f/index.js
new file mode 100644
index 00000000000..6e0d1c308f6
--- /dev/null
+++ b/app/assets/javascripts/authentication/u2f/index.js
@@ -0,0 +1,17 @@
+import $ from 'jquery';
+import U2FAuthenticate from './authenticate';
+
+export default () => {
+ if (!gon.u2f) return;
+
+ const u2fAuthenticate = new U2FAuthenticate(
+ $('#js-authenticate-u2f'),
+ '#js-login-u2f-form',
+ gon.u2f,
+ document.querySelector('#js-login-2fa-device'),
+ document.querySelector('.js-2fa-form'),
+ );
+ u2fAuthenticate.start();
+ // needed in rspec (FakeU2fDevice)
+ gl.u2fAuthenticate = u2fAuthenticate;
+};
diff --git a/app/assets/javascripts/authentication/u2f/register.js b/app/assets/javascripts/authentication/u2f/register.js
new file mode 100644
index 00000000000..f5a422727ad
--- /dev/null
+++ b/app/assets/javascripts/authentication/u2f/register.js
@@ -0,0 +1,94 @@
+import $ from 'jquery';
+import { template as lodashTemplate } from 'lodash';
+import importU2FLibrary from './util';
+import U2FError from './error';
+
+// Register U2F (universal 2nd factor) 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 U2FRegister {
+ constructor(container, u2fParams) {
+ this.u2fUtils = null;
+ this.container = container;
+ this.renderNotSupported = this.renderNotSupported.bind(this);
+ this.renderRegistered = this.renderRegistered.bind(this);
+ this.renderError = this.renderError.bind(this);
+ this.renderInProgress = this.renderInProgress.bind(this);
+ this.renderSetup = this.renderSetup.bind(this);
+ this.renderTemplate = this.renderTemplate.bind(this);
+ this.register = this.register.bind(this);
+ this.start = this.start.bind(this);
+ this.appId = u2fParams.app_id;
+ this.registerRequests = u2fParams.register_requests;
+ 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',
+ };
+ }
+
+ start() {
+ return importU2FLibrary()
+ .then(utils => {
+ this.u2fUtils = utils;
+ this.renderSetup();
+ })
+ .catch(() => this.renderNotSupported());
+ }
+
+ register() {
+ return this.u2fUtils.register(
+ this.appId,
+ this.registerRequests,
+ this.signRequests,
+ response => {
+ if (response.errorCode) {
+ const error = new U2FError(response.errorCode, 'register');
+ return this.renderError(error);
+ }
+ return this.renderRegistered(JSON.stringify(response));
+ },
+ 10,
+ );
+ }
+
+ renderTemplate(name, params) {
+ const templateString = $(this.templates[name]).html();
+ const template = lodashTemplate(templateString);
+ return this.container.html(template(params));
+ }
+
+ renderSetup() {
+ this.renderTemplate('setup');
+ return this.container.find('#js-setup-u2f-device').on('click', this.renderInProgress);
+ }
+
+ renderInProgress() {
+ this.renderTemplate('inProgress');
+ return this.register();
+ }
+
+ renderError(error) {
+ this.renderTemplate('error', {
+ error_message: error.message(),
+ error_code: error.errorCode,
+ });
+ return this.container.find('#js-u2f-try-again').on('click', this.renderSetup);
+ }
+
+ renderRegistered(deviceResponse) {
+ this.renderTemplate('registered');
+ // Prefer to do this instead of interpolating using Underscore templates
+ // because of JSON escaping issues.
+ return this.container.find('#js-device-response').val(deviceResponse);
+ }
+
+ renderNotSupported() {
+ return this.renderTemplate('notSupported');
+ }
+}
diff --git a/app/assets/javascripts/authentication/u2f/util.js b/app/assets/javascripts/authentication/u2f/util.js
new file mode 100644
index 00000000000..b706481c02f
--- /dev/null
+++ b/app/assets/javascripts/authentication/u2f/util.js
@@ -0,0 +1,40 @@
+function isOpera(userAgent) {
+ return userAgent.indexOf('Opera') >= 0 || userAgent.indexOf('OPR') >= 0;
+}
+
+function getOperaVersion(userAgent) {
+ const match = userAgent.match(/OPR[^0-9]*([0-9]+)[^0-9]+/);
+ return match ? parseInt(match[1], 10) : false;
+}
+
+function isChrome(userAgent) {
+ return userAgent.indexOf('Chrom') >= 0 && !isOpera(userAgent);
+}
+
+function getChromeVersion(userAgent) {
+ const match = userAgent.match(/Chrom(?:e|ium)\/([0-9]+)\./);
+ return match ? parseInt(match[1], 10) : false;
+}
+
+export function canInjectU2fApi(userAgent) {
+ const isSupportedChrome = isChrome(userAgent) && getChromeVersion(userAgent) >= 41;
+ const isSupportedOpera = isOpera(userAgent) && getOperaVersion(userAgent) >= 40;
+ const isMobile =
+ userAgent.indexOf('droid') >= 0 ||
+ userAgent.indexOf('CriOS') >= 0 ||
+ /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent);
+ return (isSupportedChrome || isSupportedOpera) && !isMobile;
+}
+
+export default function importU2FLibrary() {
+ if (window.u2f) {
+ return Promise.resolve(window.u2f);
+ }
+
+ const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent : '';
+ if (canInjectU2fApi(userAgent) || (gon && gon.test_env)) {
+ return import(/* webpackMode: "eager" */ 'vendor/u2f').then(() => window.u2f);
+ }
+
+ return Promise.reject();
+}