Skip to content

Commit 8b41647

Browse files
author
GitLab Bot
committed
Add latest changes from gitlab-org/gitlab@master
1 parent 742d4b0 commit 8b41647

File tree

18 files changed

+676
-66
lines changed

18 files changed

+676
-66
lines changed

app/assets/javascripts/authentication/mount_2fa.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import $ from 'jquery';
22
import initU2F from './u2f';
33
import U2FRegister from './u2f/register';
4-
import initWebauthn from './webauthn';
4+
import initWebauthnAuthentication from './webauthn';
55
import WebAuthnRegister from './webauthn/register';
66

77
export const mount2faAuthentication = () => {
88
if (gon.webauthn) {
9-
initWebauthn();
9+
initWebauthnAuthentication();
1010
} else {
1111
initU2F();
1212
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
<script>
2+
import {
3+
GlAlert,
4+
GlButton,
5+
GlForm,
6+
GlFormInput,
7+
GlFormGroup,
8+
GlLink,
9+
GlLoadingIcon,
10+
GlSprintf,
11+
} from '@gitlab/ui';
12+
import {
13+
I18N_BUTTON_REGISTER,
14+
I18N_BUTTON_SETUP,
15+
I18N_BUTTON_TRY_AGAIN,
16+
I18N_DEVICE_NAME,
17+
I18N_DEVICE_NAME_DESCRIPTION,
18+
I18N_DEVICE_NAME_PLACEHOLDER,
19+
I18N_ERROR_HTTP,
20+
I18N_ERROR_UNSUPPORTED_BROWSER,
21+
I18N_INFO_TEXT,
22+
I18N_NOTICE,
23+
I18N_PASSWORD,
24+
I18N_PASSWORD_DESCRIPTION,
25+
I18N_STATUS_SUCCESS,
26+
I18N_STATUS_WAITING,
27+
STATE_ERROR,
28+
STATE_READY,
29+
STATE_SUCCESS,
30+
STATE_UNSUPPORTED,
31+
STATE_WAITING,
32+
WEBAUTHN_DOCUMENTATION_PATH,
33+
} from '~/authentication/webauthn/constants';
34+
import WebAuthnError from '~/authentication/webauthn/error';
35+
import {
36+
FLOW_REGISTER,
37+
convertCreateParams,
38+
convertCreateResponse,
39+
isHTTPS,
40+
supported,
41+
} from '~/authentication/webauthn/util';
42+
import csrf from '~/lib/utils/csrf';
43+
44+
export default {
45+
name: 'WebAuthnRegistration',
46+
components: {
47+
GlAlert,
48+
GlButton,
49+
GlForm,
50+
GlFormInput,
51+
GlFormGroup,
52+
GlLink,
53+
GlLoadingIcon,
54+
GlSprintf,
55+
},
56+
I18N_BUTTON_REGISTER,
57+
I18N_BUTTON_SETUP,
58+
I18N_BUTTON_TRY_AGAIN,
59+
I18N_DEVICE_NAME,
60+
I18N_DEVICE_NAME_DESCRIPTION,
61+
I18N_DEVICE_NAME_PLACEHOLDER,
62+
I18N_ERROR_HTTP,
63+
I18N_ERROR_UNSUPPORTED_BROWSER,
64+
I18N_INFO_TEXT,
65+
I18N_NOTICE,
66+
I18N_PASSWORD,
67+
I18N_PASSWORD_DESCRIPTION,
68+
I18N_STATUS_SUCCESS,
69+
I18N_STATUS_WAITING,
70+
STATE_ERROR,
71+
STATE_READY,
72+
STATE_SUCCESS,
73+
STATE_UNSUPPORTED,
74+
STATE_WAITING,
75+
WEBAUTHN_DOCUMENTATION_PATH,
76+
inject: ['initialError', 'passwordRequired', 'targetPath'],
77+
data() {
78+
return {
79+
csrfToken: csrf.token,
80+
form: { deviceName: '', password: '' },
81+
state: STATE_UNSUPPORTED,
82+
errorMessage: this.initialError,
83+
credentials: null,
84+
};
85+
},
86+
computed: {
87+
disabled() {
88+
const isEmptyDeviceName = this.form.deviceName.trim() === '';
89+
const isEmptyPassword = this.form.password.trim() === '';
90+
91+
if (this.passwordRequired === false) {
92+
return isEmptyDeviceName;
93+
}
94+
95+
return isEmptyDeviceName || isEmptyPassword;
96+
},
97+
},
98+
created() {
99+
if (this.errorMessage) {
100+
this.state = STATE_ERROR;
101+
return;
102+
}
103+
104+
if (isHTTPS() && supported()) {
105+
this.state = STATE_READY;
106+
return;
107+
}
108+
109+
this.errorMessage = isHTTPS() ? I18N_ERROR_UNSUPPORTED_BROWSER : I18N_ERROR_HTTP;
110+
},
111+
methods: {
112+
isCurrentState(state) {
113+
return this.state === state;
114+
},
115+
async onRegister() {
116+
this.state = STATE_WAITING;
117+
118+
try {
119+
const credentials = await navigator.credentials.create({
120+
publicKey: convertCreateParams(gon.webauthn.options),
121+
});
122+
123+
this.credentials = JSON.stringify(convertCreateResponse(credentials));
124+
this.state = STATE_SUCCESS;
125+
} catch (error) {
126+
this.errorMessage = new WebAuthnError(error, FLOW_REGISTER).message();
127+
this.state = STATE_ERROR;
128+
}
129+
},
130+
},
131+
};
132+
</script>
133+
134+
<template>
135+
<div>
136+
<template v-if="isCurrentState($options.STATE_UNSUPPORTED)">
137+
<gl-alert variant="danger" :dismissible="false">{{ errorMessage }}</gl-alert>
138+
</template>
139+
140+
<template v-else-if="isCurrentState($options.STATE_READY)">
141+
<div class="row">
142+
<div class="col-md-5">
143+
<gl-button variant="confirm" @click="onRegister">{{
144+
$options.I18N_BUTTON_SETUP
145+
}}</gl-button>
146+
</div>
147+
<div class="col-md-7">
148+
<p>{{ $options.I18N_INFO_TEXT }}</p>
149+
</div>
150+
</div>
151+
</template>
152+
153+
<template v-else-if="isCurrentState($options.STATE_WAITING)">
154+
<gl-alert :dismissible="false">
155+
{{ $options.I18N_STATUS_WAITING }}
156+
<gl-loading-icon />
157+
</gl-alert>
158+
</template>
159+
160+
<template v-else-if="isCurrentState($options.STATE_SUCCESS)">
161+
<p>{{ $options.I18N_STATUS_SUCCESS }}</p>
162+
<gl-alert :dismissible="false" class="gl-mb-5">
163+
<gl-sprintf :message="$options.I18N_NOTICE">
164+
<template #link="{ content }">
165+
<gl-link :href="$options.WEBAUTHN_DOCUMENTATION_PATH" target="_blank">{{
166+
content
167+
}}</gl-link>
168+
</template>
169+
</gl-sprintf>
170+
</gl-alert>
171+
172+
<div class="row">
173+
<gl-form method="post" :action="targetPath" class="col-md-9" data-testid="create-webauthn">
174+
<gl-form-group
175+
v-if="passwordRequired"
176+
:description="$options.I18N_PASSWORD_DESCRIPTION"
177+
:label="$options.I18N_PASSWORD"
178+
label-for="webauthn-registration-current-password"
179+
>
180+
<gl-form-input
181+
id="webauthn-registration-current-password"
182+
v-model="form.password"
183+
name="current_password"
184+
type="password"
185+
autocomplete="current-password"
186+
data-testid="current-password-input"
187+
/>
188+
</gl-form-group>
189+
190+
<gl-form-group
191+
:description="$options.I18N_DEVICE_NAME_DESCRIPTION"
192+
:label="$options.I18N_DEVICE_NAME"
193+
label-for="device-name"
194+
>
195+
<gl-form-input
196+
id="device-name"
197+
v-model="form.deviceName"
198+
name="device_registration[name]"
199+
:placeholder="$options.I18N_DEVICE_NAME_PLACEHOLDER"
200+
data-testid="device-name-input"
201+
/>
202+
</gl-form-group>
203+
204+
<input type="hidden" name="device_registration[device_response]" :value="credentials" />
205+
<input :value="csrfToken" type="hidden" name="authenticity_token" />
206+
207+
<gl-button type="submit" :disabled="disabled" variant="confirm">{{
208+
$options.I18N_BUTTON_REGISTER
209+
}}</gl-button>
210+
</gl-form>
211+
</div>
212+
</template>
213+
214+
<template v-else-if="isCurrentState($options.STATE_ERROR)">
215+
<gl-alert
216+
variant="danger"
217+
:dismissible="false"
218+
class="gl-mb-5"
219+
:secondary-button-text="$options.I18N_BUTTON_TRY_AGAIN"
220+
@secondaryAction="onRegister"
221+
>
222+
{{ errorMessage }}
223+
</gl-alert>
224+
</template>
225+
</div>
226+
</template>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { __ } from '~/locale';
2+
import { helpPagePath } from '~/helpers/help_page_helper';
3+
4+
export const I18N_BUTTON_REGISTER = __('Register device');
5+
export const I18N_BUTTON_SETUP = __('Set up new device');
6+
export const I18N_BUTTON_TRY_AGAIN = __('Try again?');
7+
export const I18N_DEVICE_NAME = __('Device name');
8+
export const I18N_DEVICE_NAME_DESCRIPTION = __(
9+
'Excluding USB security keys, you should include the browser name together with the device name.',
10+
);
11+
export const I18N_DEVICE_NAME_PLACEHOLDER = __('Macbook Touch ID on Edge');
12+
export const I18N_ERROR_HTTP = __(
13+
'WebAuthn only works with HTTPS-enabled websites. Contact your administrator for more details.',
14+
);
15+
export const I18N_ERROR_UNSUPPORTED_BROWSER = __(
16+
"Your browser doesn't support WebAuthn. Please use a supported browser, e.g. Chrome (67+) or Firefox (60+).",
17+
);
18+
export const I18N_INFO_TEXT = __(
19+
'Your device needs to be set up. Plug it in (if needed) and click the button on the left.',
20+
);
21+
export const I18N_NOTICE = __(
22+
'You must save your recovery codes after you first register a two-factor authenticator, so you do not lose access to your account. %{linkStart}See the documentation on managing your WebAuthn device for more information.%{linkEnd}',
23+
);
24+
export const I18N_PASSWORD = __('Current password');
25+
export const I18N_PASSWORD_DESCRIPTION = __(
26+
'Your current password is required to register a new device.',
27+
);
28+
export const I18N_STATUS_SUCCESS = __(
29+
'Your device was successfully set up! Give it a name and register it with the GitLab server.',
30+
);
31+
export const I18N_STATUS_WAITING = __(
32+
'Trying to communicate with your device. Plug it in (if needed) and press the button on the device now.',
33+
);
34+
35+
export const STATE_ERROR = 'error';
36+
export const STATE_READY = 'ready';
37+
export const STATE_SUCCESS = 'success';
38+
export const STATE_UNSUPPORTED = 'unsupported';
39+
export const STATE_WAITING = 'waiting';
40+
41+
export const WEBAUTHN_DOCUMENTATION_PATH = helpPagePath(
42+
'user/profile/account/two_factor_authentication',
43+
{ anchor: 'set-up-a-webauthn-device' },
44+
);

app/assets/javascripts/authentication/webauthn/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import $ from 'jquery';
2-
import WebAuthnAuthenticate from './authenticate';
2+
import WebAuthnAuthenticate from '~/authentication/webauthn/authenticate';
33

44
export default () => {
55
const webauthnAuthenticate = new WebAuthnAuthenticate(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import Vue from 'vue';
2+
import WebAuthnRegistration from '~/authentication/webauthn/components/registration.vue';
3+
import { parseBoolean } from '~/lib/utils/common_utils';
4+
5+
export const initWebAuthnRegistration = () => {
6+
const el = document.querySelector('#js-device-registration');
7+
8+
if (!el) {
9+
return null;
10+
}
11+
12+
const { initialError, passwordRequired, targetPath } = el.dataset;
13+
14+
return new Vue({
15+
el,
16+
name: 'WebAuthnRegistrationRoot',
17+
provide: { initialError, passwordRequired: parseBoolean(passwordRequired), targetPath },
18+
render(h) {
19+
return h(WebAuthnRegistration);
20+
},
21+
});
22+
};

app/assets/javascripts/pages/profiles/two_factor_auths/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { mount2faRegistration } from '~/authentication/mount_2fa';
2+
import { initWebAuthnRegistration } from '~/authentication/webauthn/registration';
23
import { initRecoveryCodes, initManageTwoFactorForm } from '~/authentication/two_factor_auth';
34
import { parseBoolean } from '~/lib/utils/common_utils';
45

@@ -15,6 +16,7 @@ if (skippable) {
1516
}
1617

1718
mount2faRegistration();
19+
initWebAuthnRegistration();
1820

1921
initRecoveryCodes();
2022

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# frozen_string_literal: true
2+
3+
module DeviceRegistrationHelper
4+
def device_registration_data(current_password_required:, target_path:, webauthn_error:)
5+
{
6+
initial_error: webauthn_error && webauthn_error[:message],
7+
target_path: target_path,
8+
password_required: current_password_required.to_s
9+
}
10+
end
11+
end

app/models/oauth_access_token.rb

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ class OauthAccessToken < Doorkeeper::AccessToken
44
belongs_to :resource_owner, class_name: 'User'
55
belongs_to :application, class_name: 'Doorkeeper::Application'
66

7+
validates :expires_in, presence: true
8+
79
alias_attribute :user, :resource_owner
810

911
scope :latest_per_application, -> { select('distinct on(application_id) *').order(application_id: :desc, created_at: :desc) }

0 commit comments

Comments
 (0)