BigW Consortium Gitlab

Commit 128549f1 by Timothy Andrew

Implement U2F registration.

- Move the `TwoFactorAuthsController`'s `new` action to `show`, since the page is not used to create a single "two factor auth" anymore. We can have a single 2FA authenticator app, along with any number of U2F devices, in any combination, so the page will be accessed after the first "two factor auth" is created. - Add the `u2f` javascript library, which provides an API to the browser's U2F implementation. - Add tests for the JS components
parent 1f713d52
......@@ -56,9 +56,11 @@
#= require_directory ./commit
#= require_directory ./extensions
#= require_directory ./lib
#= require_directory ./u2f
#= require_directory .
#= require fuzzaldrin-plus
#= require cropper
#= require u2f
window.slugify = (text) ->
text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
......
class @U2FError
constructor: (@errorCode) ->
@httpsDisabled = (window.location.protocol isnt 'https:')
console.error("U2F Error Code: #{@errorCode}")
message: () =>
switch
when (@errorCode is u2f.ErrorCodes.BAD_REQUEST and @httpsDisabled)
"U2F only works with HTTPS-enabled websites. Contact your administrator for more details."
when @errorCode is u2f.ErrorCodes.DEVICE_INELIGIBLE
"This device has already been registered with us."
else
"There was a problem communicating with your device."
# 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
class @U2FRegister
constructor: (@container, u2fParams) ->
@appId = u2fParams.app_id
@registerRequests = u2fParams.register_requests
@signRequests = u2fParams.sign_requests
start: () =>
if U2FUtil.isU2FSupported()
@renderSetup()
else
@renderNotSupported()
register: () =>
u2f.register(@appId, @registerRequests, @signRequests, (response) =>
if response.errorCode
error = new U2FError(response.errorCode)
@renderError(error);
else
@renderRegistered(JSON.stringify(response))
, 10)
#############
# Rendering #
#############
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'
}
renderTemplate: (name, params) =>
templateString = $(@templates[name]).html()
template = _.template(templateString)
@container.html(template(params))
renderSetup: () =>
@renderTemplate('setup')
@container.find('#js-setup-u2f-device').on('click', @renderInProgress)
renderInProgress: () =>
@renderTemplate('inProgress')
@register()
renderError: (error) =>
@renderTemplate('error', {error_message: error.message()})
@container.find('#js-u2f-try-again').on('click', @renderSetup)
renderRegistered: (deviceResponse) =>
@renderTemplate('registered')
# Prefer to do this instead of interpolating using Underscore templates
# because of JSON escaping issues.
@container.find("#js-device-response").val(deviceResponse)
renderNotSupported: () =>
@renderTemplate('notSupported')
# Helper class for U2F (universal 2nd factor) device registration and authentication.
class @U2FUtil
@isU2FSupported: ->
if @testMode
true
else
gon.u2f.browser_supports_u2f
@enableTestMode: ->
@testMode = true
<% if Rails.env.test? %>
U2FUtil.enableTestMode();
<% end %>
......@@ -342,6 +342,10 @@ class ApplicationController < ActionController::Base
session[:skip_tfa] && session[:skip_tfa] > Time.current
end
def browser_supports_u2f?
browser.chrome? && browser.version.to_i >= 41 && !browser.device.mobile?
end
def redirect_to_home_page_url?
# If user is not signed-in and tries to access root_path - redirect him to landing page
# Don't redirect to the default URL to prevent endless redirections
......@@ -355,6 +359,13 @@ class ApplicationController < ActionController::Base
current_user.nil? && root_path == request.path
end
# U2F (universal 2nd factor) devices need a unique identifier for the application
# to perform authentication.
# https://developers.yubico.com/U2F/App_ID.html
def u2f_app_id
request.base_url
end
private
def set_default_sort
......
class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
skip_before_action :check_2fa_requirement
def new
def show
unless current_user.otp_secret
current_user.otp_secret = User.generate_otp_secret(32)
end
......@@ -12,21 +12,22 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
current_user.save! if current_user.changed?
if two_factor_authentication_required?
if two_factor_authentication_required? && !current_user.two_factor_enabled?
if two_factor_grace_period_expired?
flash.now[:alert] = 'You must enable Two-factor Authentication for your account.'
flash.now[:alert] = 'You must enable Two-Factor Authentication for your account.'
else
grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
flash.now[:alert] = "You must enable Two-factor Authentication for your account before #{l(grace_period_deadline)}."
flash.now[:alert] = "You must enable Two-Factor Authentication for your account before #{l(grace_period_deadline)}."
end
end
@qr_code = build_qr_code
setup_u2f_registration
end
def create
if current_user.validate_and_consume_otp!(params[:pin_code])
current_user.two_factor_enabled = true
current_user.otp_required_for_login = true
@codes = current_user.generate_otp_backup_codes!
current_user.save!
......@@ -34,8 +35,23 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
else
@error = 'Invalid pin code'
@qr_code = build_qr_code
setup_u2f_registration
render 'show'
end
end
# A U2F (universal 2nd factor) device's information is stored after successful
# registration, which is then used while 2FA authentication is taking place.
def create_u2f
@u2f_registration = U2fRegistration.register(current_user, u2f_app_id, params[:device_response], session[:challenges])
render 'new'
if @u2f_registration.persisted?
session.delete(:challenges)
redirect_to profile_account_path, notice: "Your U2F device was registered!"
else
@qr_code = build_qr_code
setup_u2f_registration
render :show
end
end
......@@ -70,4 +86,21 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
def issuer_host
Gitlab.config.gitlab.host
end
# Setup in preparation of communication with a U2F (universal 2nd factor) device
# Actual communication is performed using a Javascript API
def setup_u2f_registration
@u2f_registration ||= U2fRegistration.new
@registration_key_handles = current_user.u2f_registrations.pluck(:key_handle)
u2f = U2F::U2F.new(u2f_app_id)
registration_requests = u2f.registration_requests
sign_requests = u2f.authentication_requests(@registration_key_handles)
session[:challenges] = registration_requests.map(&:challenge)
gon.push(u2f: { challenges: session[:challenges], app_id: u2f_app_id,
register_requests: registration_requests,
sign_requests: sign_requests,
browser_supports_u2f: browser_supports_u2f? })
end
end
......@@ -11,7 +11,7 @@
%p
Your private token is used to access application resources without authentication.
.col-lg-9
= form_for @user, url: reset_private_token_profile_path, method: :put, html: {class: "private-token"} do |f|
= form_for @user, url: reset_private_token_profile_path, method: :put, html: { class: "private-token" } do |f|
%p.cgray
- if current_user.private_token
= label_tag "token", "Private token", class: "label-light"
......@@ -29,21 +29,22 @@
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
%h4.prepend-top-0
Two-factor Authentication
Two-Factor Authentication
%p
Increase your account's security by enabling two-factor authentication (2FA).
Increase your account's security by enabling Two-Factor Authentication (2FA).
.col-lg-9
%p
Status: #{current_user.two_factor_enabled? ? 'enabled' : 'disabled'}
- if !current_user.two_factor_enabled?
%p
Download the Google Authenticator application from App Store for iOS or Google Play for Android and scan this code.
More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}.
.append-bottom-10
= link_to 'Enable two-factor authentication', new_profile_two_factor_auth_path, class: 'btn btn-success'
Status: #{current_user.two_factor_enabled? ? 'Enabled' : 'Disabled'}
- if current_user.two_factor_enabled?
= link_to 'Manage Two-Factor Authentication', profile_two_factor_auth_path, class: 'btn btn-info'
= link_to 'Disable', profile_two_factor_auth_path,
method: :delete,
data: { confirm: "Are you sure? This will invalidate your registered applications and U2F devices." },
class: 'btn btn-danger'
- else
= link_to 'Disable Two-factor Authentication', profile_two_factor_auth_path, method: :delete, class: 'btn btn-danger',
data: { confirm: 'Are you sure?' }
.append-bottom-10
= link_to 'Enable Two-Factor Authentication', profile_two_factor_auth_path, class: 'btn btn-success'
%hr
- if button_based_providers.any?
.row.prepend-top-default
......
- page_title 'Two-factor Authentication', 'Account'
- page_title 'Two-Factor Authentication', 'Account'
- header_title "Two-Factor Authentication", profile_two_factor_auth_path
.row.prepend-top-default
.col-lg-3
%h4.prepend-top-0
Two-factor Authentication (2FA)
Register Two-Factor Authentication App
%p
Increase your account's security by enabling two-factor authentication (2FA).
Use an app on your mobile device to enable two-factor authentication (2FA).
.col-lg-9
- if current_user.two_factor_otp_enabled?
= icon "check inverse", base: "circle", class: "text-success", text: "You've already enabled two-factor authentication using mobile authenticator applications. You can disable it from your account settings page."
- else
%p
Download the Google Authenticator application from App Store for iOS or Google Play for Android and scan this code.
Download the Google Authenticator application from App Store or Google Play Store and scan this code.
More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}.
.row.append-bottom-10
.col-md-3
......@@ -35,5 +39,31 @@
= label_tag :pin_code, nil, class: "label-light"
= text_field_tag :pin_code, nil, class: "form-control", required: true
.prepend-top-default
= submit_tag 'Enable two-factor authentication', class: 'btn btn-success'
= link_to 'Configure it later', skip_profile_two_factor_auth_path, :method => :patch, class: 'btn btn-cancel' if two_factor_skippable?
= submit_tag 'Register with Two-Factor App', class: 'btn btn-success'
%hr
.row.prepend-top-default
.col-lg-3
%h4.prepend-top-0
Register Universal Two-Factor (U2F) Device
%p
Use a hardware device to add the second factor of authentication.
%p
As U2F devices are only supported by a few browsers, it's recommended that you set up a
two-factor authentication app as well as a U2F device so you'll always be able to log in
using an unsupported browser.
.col-lg-9
%p
- if @registration_key_handles.present?
= icon "check inverse", base: "circle", class: "text-success", text: "You have #{pluralize(@registration_key_handles.size, 'U2F device')} registered with GitLab."
- if @u2f_registration.errors.present?
= form_errors(@u2f_registration)
= render "u2f/register"
- if two_factor_skippable?
:javascript
var button = "<a class='btn btn-xs btn-warning pull-right' data-method='patch' href='#{skip_profile_two_factor_auth_path}'>Configure it later</a>";
$(".flash-alert").append(button);
#js-register-u2f
%script#js-register-u2f-not-supported{ type: "text/template" }
%p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).
%script#js-register-u2f-setup{ type: "text/template" }
.row.append-bottom-10
.col-md-3
%a#js-setup-u2f-device.btn.btn-info{ href: 'javascript:void(0)' } Setup New U2F Device
.col-md-9
%p Your U2F device needs to be set up. Plug it in (if not already) and click the button on the left.
%script#js-register-u2f-in-progress{ type: "text/template" }
%p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.
%script#js-register-u2f-error{ type: "text/template" }
%div
%p
%span <%= error_message %>
%a.btn.btn-warning#js-u2f-try-again Try again?
%script#js-register-u2f-registered{ type: "text/template" }
%div.row.append-bottom-10
%p Your device was successfully set up! Click this button to register with the GitLab server.
= form_tag(create_u2f_profile_two_factor_auth_path, method: :post) do
= hidden_field_tag :device_response, nil, class: 'form-control', required: true, id: "js-device-response"
= submit_tag "Register U2F Device", class: "btn btn-success"
:javascript
var u2fRegister = new U2FRegister($("#js-register-u2f"), gon.u2f);
u2fRegister.start();
......@@ -343,8 +343,9 @@ Rails.application.routes.draw do
resources :keys
resources :emails, only: [:index, :create, :destroy]
resource :avatar, only: [:destroy]
resource :two_factor_auth, only: [:new, :create, :destroy] do
resource :two_factor_auth, only: [:show, :create, :destroy] do
member do
post :create_u2f
post :codes
patch :skip
end
......
......@@ -8,21 +8,21 @@ describe Profiles::TwoFactorAuthsController do
allow(subject).to receive(:current_user).and_return(user)
end
describe 'GET new' do
describe 'GET show' do
let(:user) { create(:user) }
it 'generates otp_secret for user' do
expect(User).to receive(:generate_otp_secret).with(32).and_return('secret').once
get :new
get :new # Second hit shouldn't re-generate it
get :show
get :show # Second hit shouldn't re-generate it
end
it 'assigns qr_code' do
code = double('qr code')
expect(subject).to receive(:build_qr_code).and_return(code)
get :new
get :show
expect(assigns[:qr_code]).to eq code
end
end
......@@ -40,7 +40,7 @@ describe Profiles::TwoFactorAuthsController do
expect(user).to receive(:validate_and_consume_otp!).with(pin).and_return(true)
end
it 'sets two_factor_enabled' do
it 'enables 2fa for the user' do
go
user.reload
......@@ -79,9 +79,9 @@ describe Profiles::TwoFactorAuthsController do
expect(assigns[:qr_code]).to eq code
end
it 'renders new' do
it 'renders show' do
go
expect(response).to render_template(:new)
expect(response).to render_template(:show)
end
end
end
......
= render partial: "u2f/register", locals: { create_u2f_profile_two_factor_auth_path: '/profile/two_factor_auth/create_u2f' }
class @MockU2FDevice
constructor: () ->
window.u2f ||= {}
window.u2f.register = (appId, registerRequests, signRequests, callback) =>
@registerCallback = callback
window.u2f.sign = (appId, challenges, signRequests, callback) =>
@authenticateCallback = callback
respondToRegisterRequest: (params) =>
@registerCallback(params)
respondToAuthenticateRequest: (params) =>
@authenticateCallback(params)
#= require u2f/register
#= require u2f/util
#= require u2f/error
#= require u2f
#= require ./mock_u2f_device
describe 'U2FRegister', ->
U2FUtil.enableTestMode()
fixture.load('u2f/register')
beforeEach ->
@u2fDevice = new MockU2FDevice
@container = $("#js-register-u2f")
@component = new U2FRegister(@container, $("#js-register-u2f-templates"), {}, "token")
@component.start()
it 'allows registering a U2F device', ->
setupButton = @container.find("#js-setup-u2f-device")
expect(setupButton.text()).toBe('Setup New U2F Device')
setupButton.trigger('click')
inProgressMessage = @container.children("p")
expect(inProgressMessage.text()).toContain("Trying to communicate with your device")
@u2fDevice.respondToRegisterRequest({deviceData: "this is data from the device"})
registeredMessage = @container.find('p')
deviceResponse = @container.find('#js-device-response')
expect(registeredMessage.text()).toContain("Your device was successfully set up!")
expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}')
describe "errors", ->
it "doesn't allow the same device to be registered twice (for the same user", ->
setupButton = @container.find("#js-setup-u2f-device")
setupButton.trigger('click')
@u2fDevice.respondToRegisterRequest({errorCode: 4})
errorMessage = @container.find("p")
expect(errorMessage.text()).toContain("already been registered with us")
it "displays an error message for other errors", ->
setupButton = @container.find("#js-setup-u2f-device")
setupButton.trigger('click')
@u2fDevice.respondToRegisterRequest({errorCode: "error!"})
errorMessage = @container.find("p")
expect(errorMessage.text()).toContain("There was a problem communicating with your device")
it "allows retrying registration after an error", ->
setupButton = @container.find("#js-setup-u2f-device")
setupButton.trigger('click')
@u2fDevice.respondToRegisterRequest({errorCode: "error!"})
retryButton = @container.find("#U2FTryAgain")
retryButton.trigger('click')
setupButton = @container.find("#js-setup-u2f-device")
setupButton.trigger('click')
@u2fDevice.respondToRegisterRequest({deviceData: "this is data from the device"})
registeredMessage = @container.find("p")
expect(registeredMessage.text()).toContain("Your device was successfully set up!")
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment