mirror of
https://github.com/wahyd4/gitlabhq.git
synced 2026-08-25 04:26:08 +10:00
Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce
This commit is contained in:
@@ -23,6 +23,7 @@ v 8.9.0 (unreleased)
|
||||
- Fix issues filter when ordering by milestone
|
||||
- Todos will display target state if issuable target is 'Closed' or 'Merged'
|
||||
- Fix bug when sorting issues by milestone due date and filtering by two or more labels
|
||||
- Add support for using Yubikeys (U2F) for two-factor authentication
|
||||
- Link to blank group icon doesn't throw a 404 anymore
|
||||
- Remove 'main language' feature
|
||||
- Pipelines can be canceled only when there are running builds
|
||||
|
||||
@@ -45,9 +45,10 @@ gem 'akismet', '~> 2.0'
|
||||
gem 'devise-two-factor', '~> 3.0.0'
|
||||
gem 'rqrcode-rails3', '~> 0.1.7'
|
||||
gem 'attr_encrypted', '~> 3.0.0'
|
||||
gem 'u2f', '~> 0.2.1'
|
||||
|
||||
# Browser detection
|
||||
gem "browser", '~> 1.0.0'
|
||||
gem "browser", '~> 2.0.3'
|
||||
|
||||
# Extracting information from a git repository
|
||||
# Provide access to Gitlab::Git library
|
||||
|
||||
+5
-3
@@ -92,7 +92,7 @@ GEM
|
||||
sass (~> 3.0)
|
||||
slim (>= 1.3.6, < 4.0)
|
||||
terminal-table (~> 1.4)
|
||||
browser (1.0.1)
|
||||
browser (2.0.3)
|
||||
builder (3.2.2)
|
||||
bullet (5.0.0)
|
||||
activesupport (>= 3.0.0)
|
||||
@@ -747,6 +747,7 @@ GEM
|
||||
simple_oauth (~> 0.1.4)
|
||||
tzinfo (1.2.2)
|
||||
thread_safe (~> 0.1)
|
||||
u2f (0.2.1)
|
||||
uglifier (2.7.2)
|
||||
execjs (>= 0.3.0)
|
||||
json (>= 1.8.0)
|
||||
@@ -814,7 +815,7 @@ DEPENDENCIES
|
||||
binding_of_caller (~> 0.7.2)
|
||||
bootstrap-sass (~> 3.3.0)
|
||||
brakeman (~> 3.2.0)
|
||||
browser (~> 1.0.0)
|
||||
browser (~> 2.0.3)
|
||||
bullet
|
||||
bundler-audit
|
||||
byebug
|
||||
@@ -963,6 +964,7 @@ DEPENDENCIES
|
||||
thin (~> 1.6.1)
|
||||
tinder (~> 1.10.0)
|
||||
turbolinks (~> 2.5.0)
|
||||
u2f (~> 0.2.1)
|
||||
uglifier (~> 2.7.2)
|
||||
underscore-rails (~> 1.8.0)
|
||||
unf (~> 0.1.4)
|
||||
@@ -975,4 +977,4 @@ DEPENDENCIES
|
||||
wikicloth (= 0.8.1)
|
||||
|
||||
BUNDLED WITH
|
||||
1.12.4
|
||||
1.12.5
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# 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
|
||||
|
||||
class @U2FAuthenticate
|
||||
constructor: (@container, u2fParams) ->
|
||||
@appId = u2fParams.app_id
|
||||
@challenges = u2fParams.challenges
|
||||
@signRequests = u2fParams.sign_requests
|
||||
|
||||
start: () =>
|
||||
if U2FUtil.isU2FSupported()
|
||||
@renderSetup()
|
||||
else
|
||||
@renderNotSupported()
|
||||
|
||||
authenticate: () =>
|
||||
u2f.sign(@appId, @challenges, @signRequests, (response) =>
|
||||
if response.errorCode
|
||||
error = new U2FError(response.errorCode)
|
||||
@renderError(error);
|
||||
else
|
||||
@renderAuthenticated(JSON.stringify(response))
|
||||
, 10)
|
||||
|
||||
#############
|
||||
# Rendering #
|
||||
#############
|
||||
|
||||
templates: {
|
||||
"notSupported": "#js-authenticate-u2f-not-supported",
|
||||
"setup": '#js-authenticate-u2f-setup',
|
||||
"inProgress": '#js-authenticate-u2f-in-progress',
|
||||
"error": '#js-authenticate-u2f-error',
|
||||
"authenticated": '#js-authenticate-u2f-authenticated'
|
||||
}
|
||||
|
||||
renderTemplate: (name, params) =>
|
||||
templateString = $(@templates[name]).html()
|
||||
template = _.template(templateString)
|
||||
@container.html(template(params))
|
||||
|
||||
renderSetup: () =>
|
||||
@renderTemplate('setup')
|
||||
@container.find('#js-login-u2f-device').on('click', @renderInProgress)
|
||||
|
||||
renderInProgress: () =>
|
||||
@renderTemplate('inProgress')
|
||||
@authenticate()
|
||||
|
||||
renderError: (error) =>
|
||||
@renderTemplate('error', {error_message: error.message()})
|
||||
@container.find('#js-u2f-try-again').on('click', @renderSetup)
|
||||
|
||||
renderAuthenticated: (deviceResponse) =>
|
||||
@renderTemplate('authenticated')
|
||||
# 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')
|
||||
@@ -0,0 +1,13 @@
|
||||
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."
|
||||
@@ -0,0 +1,63 @@
|
||||
# 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')
|
||||
@@ -0,0 +1,15 @@
|
||||
# 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 %>
|
||||
@@ -182,8 +182,8 @@ class ApplicationController < ActionController::Base
|
||||
end
|
||||
|
||||
def check_2fa_requirement
|
||||
if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled && !skip_two_factor?
|
||||
redirect_to new_profile_two_factor_auth_path
|
||||
if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled? && !skip_two_factor?
|
||||
redirect_to profile_two_factor_auth_path
|
||||
end
|
||||
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
|
||||
|
||||
@@ -24,7 +24,64 @@ module AuthenticatesWithTwoFactor
|
||||
# Returns nil
|
||||
def prompt_for_two_factor(user)
|
||||
session[:otp_user_id] = user.id
|
||||
setup_u2f_authentication(user)
|
||||
render 'devise/sessions/two_factor'
|
||||
end
|
||||
|
||||
render 'devise/sessions/two_factor' and return
|
||||
def authenticate_with_two_factor
|
||||
user = self.resource = find_user
|
||||
|
||||
if user_params[:otp_attempt].present? && session[:otp_user_id]
|
||||
authenticate_with_two_factor_via_otp(user)
|
||||
elsif user_params[:device_response].present? && session[:otp_user_id]
|
||||
authenticate_with_two_factor_via_u2f(user)
|
||||
elsif user && user.valid_password?(user_params[:password])
|
||||
prompt_for_two_factor(user)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authenticate_with_two_factor_via_otp(user)
|
||||
if valid_otp_attempt?(user)
|
||||
# Remove any lingering user data from login
|
||||
session.delete(:otp_user_id)
|
||||
|
||||
remember_me(user) if user_params[:remember_me] == '1'
|
||||
sign_in(user)
|
||||
else
|
||||
flash.now[:alert] = 'Invalid two-factor code.'
|
||||
render :two_factor
|
||||
end
|
||||
end
|
||||
|
||||
# Authenticate using the response from a U2F (universal 2nd factor) device
|
||||
def authenticate_with_two_factor_via_u2f(user)
|
||||
if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenges])
|
||||
# Remove any lingering user data from login
|
||||
session.delete(:otp_user_id)
|
||||
session.delete(:challenges)
|
||||
|
||||
sign_in(user)
|
||||
else
|
||||
flash.now[:alert] = 'Authentication via U2F device failed.'
|
||||
prompt_for_two_factor(user)
|
||||
end
|
||||
end
|
||||
|
||||
# Setup in preparation of communication with a U2F (universal 2nd factor) device
|
||||
# Actual communication is performed using a Javascript API
|
||||
def setup_u2f_authentication(user)
|
||||
key_handles = user.u2f_registrations.pluck(:key_handle)
|
||||
u2f = U2F::U2F.new(u2f_app_id)
|
||||
|
||||
if key_handles.present?
|
||||
sign_requests = u2f.authentication_requests(key_handles)
|
||||
challenges = sign_requests.map(&:challenge)
|
||||
session[:challenges] = challenges
|
||||
gon.push(u2f: { challenges: challenges, app_id: u2f_app_id,
|
||||
sign_requests: sign_requests,
|
||||
browser_supports_u2f: browser_supports_u2f? })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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
|
||||
|
||||
render 'new'
|
||||
# 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])
|
||||
|
||||
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
|
||||
|
||||
@@ -30,8 +30,7 @@ class SessionsController < Devise::SessionsController
|
||||
resource.update_attributes(reset_password_token: nil,
|
||||
reset_password_sent_at: nil)
|
||||
end
|
||||
authenticated_with = user_params[:otp_attempt] ? "two-factor" : "standard"
|
||||
log_audit_event(current_user, with: authenticated_with)
|
||||
log_audit_event(current_user, with: authentication_method)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -54,7 +53,7 @@ class SessionsController < Devise::SessionsController
|
||||
end
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(:login, :password, :remember_me, :otp_attempt)
|
||||
params.require(:user).permit(:login, :password, :remember_me, :otp_attempt, :device_response)
|
||||
end
|
||||
|
||||
def find_user
|
||||
@@ -89,27 +88,6 @@ class SessionsController < Devise::SessionsController
|
||||
find_user.try(:two_factor_enabled?)
|
||||
end
|
||||
|
||||
def authenticate_with_two_factor
|
||||
user = self.resource = find_user
|
||||
|
||||
if user_params[:otp_attempt].present? && session[:otp_user_id]
|
||||
if valid_otp_attempt?(user)
|
||||
# Remove any lingering user data from login
|
||||
session.delete(:otp_user_id)
|
||||
|
||||
remember_me(user) if user_params[:remember_me] == '1'
|
||||
sign_in(user) and return
|
||||
else
|
||||
flash.now[:alert] = 'Invalid two-factor code.'
|
||||
render :two_factor and return
|
||||
end
|
||||
else
|
||||
if user && user.valid_password?(user_params[:password])
|
||||
prompt_for_two_factor(user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def auto_sign_in_with_provider
|
||||
provider = Gitlab.config.omniauth.auto_sign_in_with_provider
|
||||
return unless provider.present?
|
||||
@@ -138,4 +116,14 @@ class SessionsController < Devise::SessionsController
|
||||
def load_recaptcha
|
||||
Gitlab::Recaptcha.load_configurations!
|
||||
end
|
||||
|
||||
def authentication_method
|
||||
if user_params[:otp_attempt]
|
||||
"two-factor"
|
||||
elsif user_params[:device_response]
|
||||
"two-factor-via-u2f-device"
|
||||
else
|
||||
"standard"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -66,7 +66,7 @@ module AuthHelper
|
||||
|
||||
def two_factor_skippable?
|
||||
current_application_settings.require_two_factor_authentication &&
|
||||
!current_user.two_factor_enabled &&
|
||||
!current_user.two_factor_enabled? &&
|
||||
current_application_settings.two_factor_grace_period &&
|
||||
!two_factor_grace_period_expired?
|
||||
end
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Registration information for U2F (universal 2nd factor) devices, like Yubikeys
|
||||
|
||||
class U2fRegistration < ActiveRecord::Base
|
||||
belongs_to :user
|
||||
|
||||
def self.register(user, app_id, json_response, challenges)
|
||||
u2f = U2F::U2F.new(app_id)
|
||||
registration = self.new
|
||||
|
||||
begin
|
||||
response = U2F::RegisterResponse.load_from_json(json_response)
|
||||
registration_data = u2f.register!(challenges, response)
|
||||
registration.update(certificate: registration_data.certificate,
|
||||
key_handle: registration_data.key_handle,
|
||||
public_key: registration_data.public_key,
|
||||
counter: registration_data.counter,
|
||||
user: user)
|
||||
rescue JSON::ParserError, NoMethodError, ArgumentError
|
||||
registration.errors.add(:base, 'Your U2F device did not send a valid JSON response.')
|
||||
rescue U2F::Error => e
|
||||
registration.errors.add(:base, e.message)
|
||||
end
|
||||
|
||||
registration
|
||||
end
|
||||
|
||||
def self.authenticate(user, app_id, json_response, challenges)
|
||||
response = U2F::SignResponse.load_from_json(json_response)
|
||||
registration = user.u2f_registrations.find_by_key_handle(response.key_handle)
|
||||
u2f = U2F::U2F.new(app_id)
|
||||
|
||||
if registration
|
||||
u2f.authenticate!(challenges, response, Base64.decode64(registration.public_key), registration.counter)
|
||||
registration.update(counter: response.counter)
|
||||
true
|
||||
end
|
||||
rescue JSON::ParserError, NoMethodError, ArgumentError, U2F::Error
|
||||
false
|
||||
end
|
||||
end
|
||||
+34
-11
@@ -27,7 +27,6 @@ class User < ActiveRecord::Base
|
||||
|
||||
devise :two_factor_authenticatable,
|
||||
otp_secret_encryption_key: Gitlab::Application.config.secret_key_base
|
||||
alias_attribute :two_factor_enabled, :otp_required_for_login
|
||||
|
||||
devise :two_factor_backupable, otp_number_of_backup_codes: 10
|
||||
serialize :otp_backup_codes, JSON
|
||||
@@ -51,6 +50,7 @@ class User < ActiveRecord::Base
|
||||
has_many :keys, dependent: :destroy
|
||||
has_many :emails, dependent: :destroy
|
||||
has_many :identities, dependent: :destroy, autosave: true
|
||||
has_many :u2f_registrations, dependent: :destroy
|
||||
|
||||
# Groups
|
||||
has_many :members, dependent: :destroy
|
||||
@@ -175,8 +175,16 @@ class User < ActiveRecord::Base
|
||||
scope :active, -> { with_state(:active) }
|
||||
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
|
||||
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') }
|
||||
scope :with_two_factor, -> { where(two_factor_enabled: true) }
|
||||
scope :without_two_factor, -> { where(two_factor_enabled: false) }
|
||||
|
||||
def self.with_two_factor
|
||||
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id").
|
||||
where("u2f.id IS NOT NULL OR otp_required_for_login = ?", true).distinct(arel_table[:id])
|
||||
end
|
||||
|
||||
def self.without_two_factor
|
||||
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id").
|
||||
where("u2f.id IS NULL AND otp_required_for_login = ?", false)
|
||||
end
|
||||
|
||||
#
|
||||
# Class methods
|
||||
@@ -323,14 +331,29 @@ class User < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def disable_two_factor!
|
||||
update_attributes(
|
||||
two_factor_enabled: false,
|
||||
encrypted_otp_secret: nil,
|
||||
encrypted_otp_secret_iv: nil,
|
||||
encrypted_otp_secret_salt: nil,
|
||||
otp_grace_period_started_at: nil,
|
||||
otp_backup_codes: nil
|
||||
)
|
||||
transaction do
|
||||
update_attributes(
|
||||
otp_required_for_login: false,
|
||||
encrypted_otp_secret: nil,
|
||||
encrypted_otp_secret_iv: nil,
|
||||
encrypted_otp_secret_salt: nil,
|
||||
otp_grace_period_started_at: nil,
|
||||
otp_backup_codes: nil
|
||||
)
|
||||
self.u2f_registrations.destroy_all
|
||||
end
|
||||
end
|
||||
|
||||
def two_factor_enabled?
|
||||
two_factor_otp_enabled? || two_factor_u2f_enabled?
|
||||
end
|
||||
|
||||
def two_factor_otp_enabled?
|
||||
self.otp_required_for_login?
|
||||
end
|
||||
|
||||
def two_factor_u2f_enabled?
|
||||
self.u2f_registrations.exists?
|
||||
end
|
||||
|
||||
def namespace_uniq
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
%div
|
||||
.login-box
|
||||
.login-heading
|
||||
%h3 Two-factor Authentication
|
||||
%h3 Two-Factor Authentication
|
||||
.login-body
|
||||
= form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f|
|
||||
= f.hidden_field :remember_me, value: params[resource_name][:remember_me]
|
||||
= f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-factor Authentication code', required: true, autofocus: true, autocomplete: 'off'
|
||||
%p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes.
|
||||
.prepend-top-20
|
||||
= f.submit "Verify code", class: "btn btn-save"
|
||||
- if @user.two_factor_otp_enabled?
|
||||
%h5 Authenticate via Two-Factor App
|
||||
= form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f|
|
||||
= f.hidden_field :remember_me, value: params[resource_name][:remember_me]
|
||||
= f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-Factor Authentication code', required: true, autofocus: true, autocomplete: 'off'
|
||||
%p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes.
|
||||
.prepend-top-20
|
||||
= f.submit "Verify code", class: "btn btn-save"
|
||||
|
||||
- if @user.two_factor_u2f_enabled?
|
||||
|
||||
%hr
|
||||
= render "u2f/authenticate"
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
%td Show/hide this dialog
|
||||
%tr
|
||||
%td.shortcut
|
||||
- if browser.mac?
|
||||
- if browser.platform.mac?
|
||||
.key ⌘ shift p
|
||||
- else
|
||||
.key ctrl shift p
|
||||
|
||||
@@ -35,8 +35,6 @@
|
||||
|
||||
= csrf_meta_tags
|
||||
|
||||
= include_gon
|
||||
|
||||
- unless browser.safari?
|
||||
%meta{name: 'referrer', content: 'origin-when-cross-origin'}
|
||||
%meta{name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1'}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
%html{ lang: "en"}
|
||||
= render "layouts/head"
|
||||
%body{class: "#{user_application_theme}", 'data-page' => body_data_page}
|
||||
= Gon::Base.render_data
|
||||
|
||||
-# Ideally this would be inside the head, but turbolinks only evaluates page-specific JS in the body.
|
||||
= yield :scripts_body_top
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
%html{ lang: "en"}
|
||||
= render "layouts/head"
|
||||
%body.ui_charcoal.login-page.application.navless
|
||||
= Gon::Base.render_data
|
||||
= render "layouts/header/empty"
|
||||
= render "layouts/broadcast"
|
||||
.container.navless-container
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
%html{ lang: "en"}
|
||||
= render "layouts/head"
|
||||
%body.ui_charcoal.login-page.application.navless
|
||||
= Gon::Base.render_data
|
||||
= render "layouts/header/empty"
|
||||
= render "layouts/broadcast"
|
||||
.container.navless-container
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
%html{ lang: "en"}
|
||||
= render "layouts/head"
|
||||
%body{class: "#{user_application_theme} application navless"}
|
||||
= Gon::Base.render_data
|
||||
= render "layouts/header/empty"
|
||||
.container.navless-container
|
||||
= render "layouts/flash"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
- page_title 'Two-factor Authentication', 'Account'
|
||||
|
||||
.row.prepend-top-default
|
||||
.col-lg-3
|
||||
%h4.prepend-top-0
|
||||
Two-factor Authentication (2FA)
|
||||
%p
|
||||
Increase your account's security by enabling two-factor authentication (2FA).
|
||||
.col-lg-9
|
||||
%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'))}.
|
||||
.row.append-bottom-10
|
||||
.col-md-3
|
||||
= raw @qr_code
|
||||
.col-md-9
|
||||
.account-well
|
||||
%p.prepend-top-0.append-bottom-0
|
||||
Can't scan the code?
|
||||
%p.prepend-top-0.append-bottom-0
|
||||
To add the entry manually, provide the following details to the application on your phone.
|
||||
%p.prepend-top-0.append-bottom-0
|
||||
Account:
|
||||
= current_user.email
|
||||
%p.prepend-top-0.append-bottom-0
|
||||
Key:
|
||||
= current_user.otp_secret.scan(/.{4}/).join(' ')
|
||||
%p.two-factor-new-manual-content
|
||||
Time based: Yes
|
||||
= form_tag profile_two_factor_auth_path, method: :post do |f|
|
||||
- if @error
|
||||
.alert.alert-danger
|
||||
= @error
|
||||
.form-group
|
||||
= 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?
|
||||
@@ -0,0 +1,69 @@
|
||||
- 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
|
||||
Register Two-Factor Authentication App
|
||||
%p
|
||||
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 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
|
||||
= raw @qr_code
|
||||
.col-md-9
|
||||
.account-well
|
||||
%p.prepend-top-0.append-bottom-0
|
||||
Can't scan the code?
|
||||
%p.prepend-top-0.append-bottom-0
|
||||
To add the entry manually, provide the following details to the application on your phone.
|
||||
%p.prepend-top-0.append-bottom-0
|
||||
Account:
|
||||
= current_user.email
|
||||
%p.prepend-top-0.append-bottom-0
|
||||
Key:
|
||||
= current_user.otp_secret.scan(/.{4}/).join(' ')
|
||||
%p.two-factor-new-manual-content
|
||||
Time based: Yes
|
||||
= form_tag profile_two_factor_auth_path, method: :post do |f|
|
||||
- if @error
|
||||
.alert.alert-danger
|
||||
= @error
|
||||
.form-group
|
||||
= label_tag :pin_code, nil, class: "label-light"
|
||||
= text_field_tag :pin_code, nil, class: "form-control", required: true
|
||||
.prepend-top-default
|
||||
= 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);
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
#js-authenticate-u2f
|
||||
|
||||
%script#js-authenticate-u2f-not-supported{ type: "text/template" }
|
||||
%p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).
|
||||
|
||||
%script#js-authenticate-u2f-setup{ type: "text/template" }
|
||||
%div
|
||||
%p Insert your security key (if you haven't already), and press the button below.
|
||||
%a.btn.btn-info#js-login-u2f-device{ href: 'javascript:void(0)' } Login Via U2F Device
|
||||
|
||||
%script#js-authenticate-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-authenticate-u2f-error{ type: "text/template" }
|
||||
%div
|
||||
%p <%= error_message %>
|
||||
%a.btn.btn-warning#js-u2f-try-again Try again?
|
||||
|
||||
%script#js-authenticate-u2f-authenticated{ type: "text/template" }
|
||||
%div
|
||||
%p We heard back from your U2F device. Click this button to authenticate with the GitLab server.
|
||||
= form_tag(new_user_session_path, method: :post) do |f|
|
||||
= hidden_field_tag 'user[device_response]', nil, class: 'form-control', required: true, id: "js-device-response"
|
||||
= submit_tag "Authenticate via U2F Device", class: "btn btn-success"
|
||||
|
||||
:javascript
|
||||
var u2fAuthenticate = new U2FAuthenticate($("#js-authenticate-u2f"), gon.u2f);
|
||||
u2fAuthenticate.start();
|
||||
@@ -0,0 +1,31 @@
|
||||
#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();
|
||||
+2
-1
@@ -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
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
class CreateU2fRegistrations < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :u2f_registrations do |t|
|
||||
t.text :certificate
|
||||
t.string :key_handle, index: true
|
||||
t.string :public_key
|
||||
t.integer :counter
|
||||
t.references :user, index: true, foreign_key: true
|
||||
|
||||
t.timestamps null: false
|
||||
end
|
||||
end
|
||||
end
|
||||
+14
-1
@@ -12,7 +12,6 @@
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20160530150109) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
enable_extension "pg_trgm"
|
||||
@@ -940,6 +939,19 @@ ActiveRecord::Schema.define(version: 20160530150109) do
|
||||
add_index "todos", ["target_type", "target_id"], name: "index_todos_on_target_type_and_target_id", using: :btree
|
||||
add_index "todos", ["user_id"], name: "index_todos_on_user_id", using: :btree
|
||||
|
||||
create_table "u2f_registrations", force: :cascade do |t|
|
||||
t.text "certificate"
|
||||
t.string "key_handle"
|
||||
t.string "public_key"
|
||||
t.integer "counter"
|
||||
t.integer "user_id"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
add_index "u2f_registrations", ["key_handle"], name: "index_u2f_registrations_on_key_handle", using: :btree
|
||||
add_index "u2f_registrations", ["user_id"], name: "index_u2f_registrations_on_user_id", using: :btree
|
||||
|
||||
create_table "users", force: :cascade do |t|
|
||||
t.string "email", default: "", null: false
|
||||
t.string "encrypted_password", default: "", null: false
|
||||
@@ -1047,4 +1059,5 @@ ActiveRecord::Schema.define(version: 20160530150109) do
|
||||
add_index "web_hooks", ["created_at", "id"], name: "index_web_hooks_on_created_at_and_id", using: :btree
|
||||
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
|
||||
|
||||
add_foreign_key "u2f_registrations", "users"
|
||||
end
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
@@ -8,12 +8,27 @@ your phone.
|
||||
By enabling 2FA, the only way someone other than you can log into your account
|
||||
is to know your username and password *and* have access to your phone.
|
||||
|
||||
#### Note
|
||||
> **Note:**
|
||||
When you enable 2FA, don't forget to back up your recovery codes. For your safety, if you
|
||||
lose your codes for GitLab.com, we can't disable or recover them.
|
||||
|
||||
In addition to a phone application, GitLab supports U2F (universal 2nd factor) devices as
|
||||
the second factor of authentication. Once enabled, in addition to supplying your username and
|
||||
password to login, you'll be prompted to activate your U2F device (usually by pressing
|
||||
a button on it), and it will perform secure authentication on your behalf.
|
||||
|
||||
> **Note:** Support for U2F devices was added in version 8.8
|
||||
|
||||
The U2F workflow is only supported by Google Chrome at this point, so we _strongly_ recommend
|
||||
that you set up both methods of two-factor authentication, so you can still access your account
|
||||
from other browsers.
|
||||
|
||||
> **Note:** GitLab officially only supports [Yubikey] U2F devices.
|
||||
|
||||
## Enabling 2FA
|
||||
|
||||
### Enable 2FA via mobile application
|
||||
|
||||
**In GitLab:**
|
||||
|
||||
1. Log in to your GitLab account.
|
||||
@@ -38,9 +53,26 @@ lose your codes for GitLab.com, we can't disable or recover them.
|
||||
1. Click **Submit**.
|
||||
|
||||
If the pin you entered was correct, you'll see a message indicating that
|
||||
Two-factor Authentication has been enabled, and you'll be presented with a list
|
||||
Two-Factor Authentication has been enabled, and you'll be presented with a list
|
||||
of recovery codes.
|
||||
|
||||
### Enable 2FA via U2F device
|
||||
|
||||
**In GitLab:**
|
||||
|
||||
1. Log in to your GitLab account.
|
||||
1. Go to your **Profile Settings**.
|
||||
1. Go to **Account**.
|
||||
1. Click **Enable Two-Factor Authentication**.
|
||||
1. Plug in your U2F device.
|
||||
1. Click on **Setup New U2F Device**.
|
||||
1. A light will start blinking on your device. Activate it by pressing its button.
|
||||
|
||||
You will see a message indicating that your device was successfully set up.
|
||||
Click on **Register U2F Device** to complete the process.
|
||||
|
||||

|
||||
|
||||
## Recovery Codes
|
||||
|
||||
Should you ever lose access to your phone, you can use one of the ten provided
|
||||
@@ -51,21 +83,39 @@ account.
|
||||
If you lose the recovery codes or just want to generate new ones, you can do so
|
||||
from the **Profile Settings** > **Account** page where you first enabled 2FA.
|
||||
|
||||
> **Note:** Recovery codes are not generated for U2F devices.
|
||||
|
||||
## Logging in with 2FA Enabled
|
||||
|
||||
Logging in with 2FA enabled is only slightly different than a normal login.
|
||||
Enter your username and password credentials as you normally would, and you'll
|
||||
be presented with a second prompt for an authentication code. Enter the pin from
|
||||
your phone's application or a recovery code to log in.
|
||||
be presented with a second prompt, depending on which type of 2FA you've enabled.
|
||||
|
||||

|
||||
### Log in via mobile application
|
||||
|
||||
Enter the pin from your phone's application or a recovery code to log in.
|
||||
|
||||

|
||||
|
||||
### Log in via U2F device
|
||||
|
||||
1. Click **Login via U2F Device**
|
||||
1. A light will start blinking on your device. Activate it by pressing its button.
|
||||
|
||||
You will see a message indicating that your device responded to the authentication request.
|
||||
Click on **Authenticate via U2F Device** to complete the process.
|
||||
|
||||

|
||||
|
||||
## Disabling 2FA
|
||||
|
||||
1. Log in to your GitLab account.
|
||||
1. Go to your **Profile Settings**.
|
||||
1. Go to **Account**.
|
||||
1. Click **Disable Two-factor Authentication**.
|
||||
1. Click **Disable**, under **Two-Factor Authentication**.
|
||||
|
||||
This will clear all your two-factor authentication registrations, including mobile
|
||||
applications and U2F devices.
|
||||
|
||||
## Note to GitLab administrators
|
||||
|
||||
@@ -74,3 +124,4 @@ You need to take special care to that 2FA keeps working after
|
||||
|
||||
[Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en
|
||||
[FreeOTP]: https://fedorahosted.org/freeotp/
|
||||
[YubiKey]: https://www.yubico.com/products/yubikey-hardware/
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ module API
|
||||
expose :identities, using: Entities::Identity
|
||||
expose :can_create_group?, as: :can_create_group
|
||||
expose :can_create_project?, as: :can_create_project
|
||||
expose :two_factor_enabled
|
||||
expose :two_factor_enabled?, as: :two_factor_enabled
|
||||
expose :external
|
||||
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
|
||||
|
||||
@@ -25,10 +25,15 @@ describe SessionsController do
|
||||
expect(response).to set_flash.to /Signed in successfully/
|
||||
expect(subject.current_user). to eq user
|
||||
end
|
||||
|
||||
it "creates an audit log record" do
|
||||
expect { post(:create, user: { login: user.username, password: user.password }) }.to change { SecurityEvent.count }.by(1)
|
||||
expect(SecurityEvent.last.details[:with]).to eq("standard")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when using two-factor authentication' do
|
||||
context 'when using two-factor authentication via OTP' do
|
||||
let(:user) { create(:user, :two_factor) }
|
||||
|
||||
def authenticate_2fa(user_params)
|
||||
@@ -117,6 +122,25 @@ describe SessionsController do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "creates an audit log record" do
|
||||
expect { authenticate_2fa(login: user.username, otp_attempt: user.current_otp) }.to change { SecurityEvent.count }.by(1)
|
||||
expect(SecurityEvent.last.details[:with]).to eq("two-factor")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when using two-factor authentication via U2F device' do
|
||||
let(:user) { create(:user, :two_factor) }
|
||||
|
||||
def authenticate_2fa_u2f(user_params)
|
||||
post(:create, { user: user_params }, { otp_user_id: user.id })
|
||||
end
|
||||
|
||||
it "creates an audit log record" do
|
||||
allow(U2fRegistration).to receive(:authenticate).and_return(true)
|
||||
expect { authenticate_2fa_u2f(login: user.username, device_response: "{}") }.to change { SecurityEvent.count }.by(1)
|
||||
expect(SecurityEvent.last.details[:with]).to eq("two-factor-via-u2f-device")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
FactoryGirl.define do
|
||||
factory :u2f_registration do
|
||||
certificate { FFaker::BaconIpsum.characters(728) }
|
||||
key_handle { FFaker::BaconIpsum.characters(86) }
|
||||
public_key { FFaker::BaconIpsum.characters(88) }
|
||||
counter 0
|
||||
end
|
||||
end
|
||||
+13
-1
@@ -15,14 +15,26 @@ FactoryGirl.define do
|
||||
end
|
||||
|
||||
trait :two_factor do
|
||||
two_factor_via_otp
|
||||
end
|
||||
|
||||
trait :two_factor_via_otp do
|
||||
before(:create) do |user|
|
||||
user.two_factor_enabled = true
|
||||
user.otp_required_for_login = true
|
||||
user.otp_secret = User.generate_otp_secret(32)
|
||||
user.otp_grace_period_started_at = Time.now
|
||||
user.generate_otp_backup_codes!
|
||||
end
|
||||
end
|
||||
|
||||
trait :two_factor_via_u2f do
|
||||
transient { registrations_count 5 }
|
||||
|
||||
after(:create) do |user, evaluator|
|
||||
create_list(:u2f_registration, evaluator.registrations_count, user: user)
|
||||
end
|
||||
end
|
||||
|
||||
factory :omniauth_user do
|
||||
transient do
|
||||
extern_uid '123456'
|
||||
|
||||
@@ -19,7 +19,7 @@ describe "Admin::Users", feature: true do
|
||||
|
||||
describe 'Two-factor Authentication filters' do
|
||||
it 'counts users who have enabled 2FA' do
|
||||
create(:user, two_factor_enabled: true)
|
||||
create(:user, :two_factor)
|
||||
|
||||
visit admin_users_path
|
||||
|
||||
@@ -29,7 +29,7 @@ describe "Admin::Users", feature: true do
|
||||
end
|
||||
|
||||
it 'filters by users who have enabled 2FA' do
|
||||
user = create(:user, two_factor_enabled: true)
|
||||
user = create(:user, :two_factor)
|
||||
|
||||
visit admin_users_path
|
||||
click_link '2FA Enabled'
|
||||
@@ -38,7 +38,7 @@ describe "Admin::Users", feature: true do
|
||||
end
|
||||
|
||||
it 'counts users who have not enabled 2FA' do
|
||||
create(:user, two_factor_enabled: false)
|
||||
create(:user)
|
||||
|
||||
visit admin_users_path
|
||||
|
||||
@@ -48,7 +48,7 @@ describe "Admin::Users", feature: true do
|
||||
end
|
||||
|
||||
it 'filters by users who have not enabled 2FA' do
|
||||
user = create(:user, two_factor_enabled: false)
|
||||
user = create(:user)
|
||||
|
||||
visit admin_users_path
|
||||
click_link '2FA Disabled'
|
||||
@@ -173,7 +173,7 @@ describe "Admin::Users", feature: true do
|
||||
|
||||
describe 'Two-factor Authentication status' do
|
||||
it 'shows when enabled' do
|
||||
@user.update_attribute(:two_factor_enabled, true)
|
||||
@user.update_attribute(:otp_required_for_login, true)
|
||||
|
||||
visit admin_user_path(@user)
|
||||
|
||||
|
||||
+13
-13
@@ -33,11 +33,11 @@ feature 'Login', feature: true do
|
||||
|
||||
before do
|
||||
login_with(user, remember: true)
|
||||
expect(page).to have_content('Two-factor Authentication')
|
||||
expect(page).to have_content('Two-Factor Authentication')
|
||||
end
|
||||
|
||||
def enter_code(code)
|
||||
fill_in 'Two-factor Authentication code', with: code
|
||||
fill_in 'Two-Factor Authentication code', with: code
|
||||
click_button 'Verify code'
|
||||
end
|
||||
|
||||
@@ -143,12 +143,12 @@ feature 'Login', feature: true do
|
||||
|
||||
context 'within the grace period' do
|
||||
it 'redirects to two-factor configuration page' do
|
||||
expect(current_path).to eq new_profile_two_factor_auth_path
|
||||
expect(page).to have_content('You must enable Two-factor Authentication for your account before')
|
||||
expect(current_path).to eq profile_two_factor_auth_path
|
||||
expect(page).to have_content('You must enable Two-Factor Authentication for your account before')
|
||||
end
|
||||
|
||||
it 'disallows skipping two-factor configuration' do
|
||||
expect(current_path).to eq new_profile_two_factor_auth_path
|
||||
it 'allows skipping two-factor configuration', js: true do
|
||||
expect(current_path).to eq profile_two_factor_auth_path
|
||||
|
||||
click_link 'Configure it later'
|
||||
expect(current_path).to eq root_path
|
||||
@@ -159,26 +159,26 @@ feature 'Login', feature: true do
|
||||
let(:user) { create(:user, otp_grace_period_started_at: 9999.hours.ago) }
|
||||
|
||||
it 'redirects to two-factor configuration page' do
|
||||
expect(current_path).to eq new_profile_two_factor_auth_path
|
||||
expect(page).to have_content('You must enable Two-factor Authentication for your account.')
|
||||
expect(current_path).to eq profile_two_factor_auth_path
|
||||
expect(page).to have_content('You must enable Two-Factor Authentication for your account.')
|
||||
end
|
||||
|
||||
it 'disallows skipping two-factor configuration' do
|
||||
expect(current_path).to eq new_profile_two_factor_auth_path
|
||||
it 'disallows skipping two-factor configuration', js: true do
|
||||
expect(current_path).to eq profile_two_factor_auth_path
|
||||
expect(page).not_to have_link('Configure it later')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'without grace pariod defined' do
|
||||
context 'without grace period defined' do
|
||||
before(:each) do
|
||||
stub_application_setting(two_factor_grace_period: 0)
|
||||
login_with(user)
|
||||
end
|
||||
|
||||
it 'redirects to two-factor configuration page' do
|
||||
expect(current_path).to eq new_profile_two_factor_auth_path
|
||||
expect(page).to have_content('You must enable Two-factor Authentication for your account.')
|
||||
expect(current_path).to eq profile_two_factor_auth_path
|
||||
expect(page).to have_content('You must enable Two-Factor Authentication for your account.')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
require 'spec_helper'
|
||||
|
||||
feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: true, js: true do
|
||||
def register_u2f_device(u2f_device = nil)
|
||||
u2f_device ||= FakeU2fDevice.new(page)
|
||||
u2f_device.respond_to_u2f_registration
|
||||
click_on 'Setup New U2F Device'
|
||||
expect(page).to have_content('Your device was successfully set up')
|
||||
click_on 'Register U2F Device'
|
||||
u2f_device
|
||||
end
|
||||
|
||||
describe "registration" do
|
||||
let(:user) { create(:user) }
|
||||
before { login_as(user) }
|
||||
|
||||
describe 'when 2FA via OTP is disabled' do
|
||||
it 'allows registering a new device' do
|
||||
visit profile_account_path
|
||||
click_on 'Enable Two-Factor Authentication'
|
||||
|
||||
register_u2f_device
|
||||
|
||||
expect(page.body).to match('Your U2F device was registered')
|
||||
end
|
||||
|
||||
it 'allows registering more than one device' do
|
||||
visit profile_account_path
|
||||
|
||||
# First device
|
||||
click_on 'Enable Two-Factor Authentication'
|
||||
register_u2f_device
|
||||
expect(page.body).to match('Your U2F device was registered')
|
||||
|
||||
# Second device
|
||||
click_on 'Manage Two-Factor Authentication'
|
||||
register_u2f_device
|
||||
expect(page.body).to match('Your U2F device was registered')
|
||||
click_on 'Manage Two-Factor Authentication'
|
||||
|
||||
expect(page.body).to match('You have 2 U2F devices registered')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when 2FA via OTP is enabled' do
|
||||
before { user.update_attributes(otp_required_for_login: true) }
|
||||
|
||||
it 'allows registering a new device' do
|
||||
visit profile_account_path
|
||||
click_on 'Manage Two-Factor Authentication'
|
||||
expect(page.body).to match("You've already enabled two-factor authentication using mobile")
|
||||
|
||||
register_u2f_device
|
||||
|
||||
expect(page.body).to match('Your U2F device was registered')
|
||||
end
|
||||
|
||||
it 'allows registering more than one device' do
|
||||
visit profile_account_path
|
||||
|
||||
# First device
|
||||
click_on 'Manage Two-Factor Authentication'
|
||||
register_u2f_device
|
||||
expect(page.body).to match('Your U2F device was registered')
|
||||
|
||||
# Second device
|
||||
click_on 'Manage Two-Factor Authentication'
|
||||
register_u2f_device
|
||||
expect(page.body).to match('Your U2F device was registered')
|
||||
|
||||
click_on 'Manage Two-Factor Authentication'
|
||||
expect(page.body).to match('You have 2 U2F devices registered')
|
||||
end
|
||||
end
|
||||
|
||||
it 'allows the same device to be registered for multiple users' do
|
||||
# First user
|
||||
visit profile_account_path
|
||||
click_on 'Enable Two-Factor Authentication'
|
||||
u2f_device = register_u2f_device
|
||||
expect(page.body).to match('Your U2F device was registered')
|
||||
logout
|
||||
|
||||
# Second user
|
||||
login_as(:user)
|
||||
visit profile_account_path
|
||||
click_on 'Enable Two-Factor Authentication'
|
||||
register_u2f_device(u2f_device)
|
||||
expect(page.body).to match('Your U2F device was registered')
|
||||
|
||||
expect(U2fRegistration.count).to eq(2)
|
||||
end
|
||||
|
||||
context "when there are form errors" do
|
||||
it "doesn't register the device if there are errors" do
|
||||
visit profile_account_path
|
||||
click_on 'Enable Two-Factor Authentication'
|
||||
|
||||
# Have the "u2f device" respond with bad data
|
||||
page.execute_script("u2f.register = function(_,_,_,callback) { callback('bad response'); };")
|
||||
click_on 'Setup New U2F Device'
|
||||
expect(page).to have_content('Your device was successfully set up')
|
||||
click_on 'Register U2F Device'
|
||||
|
||||
expect(U2fRegistration.count).to eq(0)
|
||||
expect(page.body).to match("The form contains the following error")
|
||||
expect(page.body).to match("did not send a valid JSON response")
|
||||
end
|
||||
|
||||
it "allows retrying registration" do
|
||||
visit profile_account_path
|
||||
click_on 'Enable Two-Factor Authentication'
|
||||
|
||||
# Failed registration
|
||||
page.execute_script("u2f.register = function(_,_,_,callback) { callback('bad response'); };")
|
||||
click_on 'Setup New U2F Device'
|
||||
expect(page).to have_content('Your device was successfully set up')
|
||||
click_on 'Register U2F Device'
|
||||
expect(page.body).to match("The form contains the following error")
|
||||
|
||||
# Successful registration
|
||||
register_u2f_device
|
||||
|
||||
expect(page.body).to match('Your U2F device was registered')
|
||||
expect(U2fRegistration.count).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "authentication" do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
# Register and logout
|
||||
login_as(user)
|
||||
visit profile_account_path
|
||||
click_on 'Enable Two-Factor Authentication'
|
||||
@u2f_device = register_u2f_device
|
||||
logout
|
||||
end
|
||||
|
||||
describe "when 2FA via OTP is disabled" do
|
||||
it "allows logging in with the U2F device" do
|
||||
login_with(user)
|
||||
|
||||
@u2f_device.respond_to_u2f_authentication
|
||||
click_on "Login Via U2F Device"
|
||||
expect(page.body).to match('We heard back from your U2F device')
|
||||
click_on "Authenticate via U2F Device"
|
||||
|
||||
expect(page.body).to match('Signed in successfully')
|
||||
end
|
||||
end
|
||||
|
||||
describe "when 2FA via OTP is enabled" do
|
||||
it "allows logging in with the U2F device" do
|
||||
user.update_attributes(otp_required_for_login: true)
|
||||
login_with(user)
|
||||
|
||||
@u2f_device.respond_to_u2f_authentication
|
||||
click_on "Login Via U2F Device"
|
||||
expect(page.body).to match('We heard back from your U2F device')
|
||||
click_on "Authenticate via U2F Device"
|
||||
|
||||
expect(page.body).to match('Signed in successfully')
|
||||
end
|
||||
end
|
||||
|
||||
describe "when a given U2F device has already been registered by another user" do
|
||||
describe "but not the current user" do
|
||||
it "does not allow logging in with that particular device" do
|
||||
# Register current user with the different U2F device
|
||||
current_user = login_as(:user)
|
||||
visit profile_account_path
|
||||
click_on 'Enable Two-Factor Authentication'
|
||||
register_u2f_device
|
||||
logout
|
||||
|
||||
# Try authenticating user with the old U2F device
|
||||
login_as(current_user)
|
||||
@u2f_device.respond_to_u2f_authentication
|
||||
click_on "Login Via U2F Device"
|
||||
expect(page.body).to match('We heard back from your U2F device')
|
||||
click_on "Authenticate via U2F Device"
|
||||
|
||||
expect(page.body).to match('Authentication via U2F device failed')
|
||||
end
|
||||
end
|
||||
|
||||
describe "and also the current user" do
|
||||
it "allows logging in with that particular device" do
|
||||
# Register current user with the same U2F device
|
||||
current_user = login_as(:user)
|
||||
visit profile_account_path
|
||||
click_on 'Enable Two-Factor Authentication'
|
||||
register_u2f_device(@u2f_device)
|
||||
logout
|
||||
|
||||
# Try authenticating user with the same U2F device
|
||||
login_as(current_user)
|
||||
@u2f_device.respond_to_u2f_authentication
|
||||
click_on "Login Via U2F Device"
|
||||
expect(page.body).to match('We heard back from your U2F device')
|
||||
click_on "Authenticate via U2F Device"
|
||||
|
||||
expect(page.body).to match('Signed in successfully')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "when a given U2F device has not been registered" do
|
||||
it "does not allow logging in with that particular device" do
|
||||
unregistered_device = FakeU2fDevice.new(page)
|
||||
login_as(user)
|
||||
unregistered_device.respond_to_u2f_authentication
|
||||
click_on "Login Via U2F Device"
|
||||
expect(page.body).to match('We heard back from your U2F device')
|
||||
click_on "Authenticate via U2F Device"
|
||||
|
||||
expect(page.body).to match('Authentication via U2F device failed')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "when two-factor authentication is disabled" do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
login_as(user)
|
||||
visit profile_account_path
|
||||
click_on 'Enable Two-Factor Authentication'
|
||||
register_u2f_device
|
||||
end
|
||||
|
||||
it "deletes u2f registrations" do
|
||||
expect { click_on "Disable" }.to change { U2fRegistration.count }.from(1).to(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1 @@
|
||||
= render partial: "u2f/authenticate", locals: { new_user_session_path: "/users/sign_in" }
|
||||
@@ -0,0 +1 @@
|
||||
= render partial: "u2f/register", locals: { create_u2f_profile_two_factor_auth_path: '/profile/two_factor_auth/create_u2f' }
|
||||
@@ -0,0 +1,52 @@
|
||||
#= require u2f/authenticate
|
||||
#= require u2f/util
|
||||
#= require u2f/error
|
||||
#= require u2f
|
||||
#= require ./mock_u2f_device
|
||||
|
||||
describe 'U2FAuthenticate', ->
|
||||
U2FUtil.enableTestMode()
|
||||
fixture.load('u2f/authenticate')
|
||||
|
||||
beforeEach ->
|
||||
@u2fDevice = new MockU2FDevice
|
||||
@container = $("#js-authenticate-u2f")
|
||||
@component = new U2FAuthenticate(@container, {}, "token")
|
||||
@component.start()
|
||||
|
||||
it 'allows authenticating via a U2F device', ->
|
||||
setupButton = @container.find("#js-login-u2f-device")
|
||||
setupMessage = @container.find("p")
|
||||
expect(setupMessage.text()).toContain('Insert your security key')
|
||||
expect(setupButton.text()).toBe('Login Via U2F Device')
|
||||
setupButton.trigger('click')
|
||||
|
||||
inProgressMessage = @container.find("p")
|
||||
expect(inProgressMessage.text()).toContain("Trying to communicate with your device")
|
||||
|
||||
@u2fDevice.respondToAuthenticateRequest({deviceData: "this is data from the device"})
|
||||
authenticatedMessage = @container.find("p")
|
||||
deviceResponse = @container.find('#js-device-response')
|
||||
expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server")
|
||||
expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}')
|
||||
|
||||
describe "errors", ->
|
||||
it "displays an error message", ->
|
||||
setupButton = @container.find("#js-login-u2f-device")
|
||||
setupButton.trigger('click')
|
||||
@u2fDevice.respondToAuthenticateRequest({errorCode: "error!"})
|
||||
errorMessage = @container.find("p")
|
||||
expect(errorMessage.text()).toContain("There was a problem communicating with your device")
|
||||
|
||||
it "allows retrying authentication after an error", ->
|
||||
setupButton = @container.find("#js-login-u2f-device")
|
||||
setupButton.trigger('click')
|
||||
@u2fDevice.respondToAuthenticateRequest({errorCode: "error!"})
|
||||
retryButton = @container.find("#js-u2f-try-again")
|
||||
retryButton.trigger('click')
|
||||
|
||||
setupButton = @container.find("#js-login-u2f-device")
|
||||
setupButton.trigger('click')
|
||||
@u2fDevice.respondToAuthenticateRequest({deviceData: "this is data from the device"})
|
||||
authenticatedMessage = @container.find("p")
|
||||
expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server")
|
||||
@@ -0,0 +1,15 @@
|
||||
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)
|
||||
@@ -0,0 +1,57 @@
|
||||
#= 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!")
|
||||
@@ -121,6 +121,66 @@ describe User, models: true do
|
||||
end
|
||||
end
|
||||
|
||||
describe "scopes" do
|
||||
describe ".with_two_factor" do
|
||||
it "returns users with 2fa enabled via OTP" do
|
||||
user_with_2fa = create(:user, :two_factor_via_otp)
|
||||
user_without_2fa = create(:user)
|
||||
users_with_two_factor = User.with_two_factor.pluck(:id)
|
||||
|
||||
expect(users_with_two_factor).to include(user_with_2fa.id)
|
||||
expect(users_with_two_factor).not_to include(user_without_2fa.id)
|
||||
end
|
||||
|
||||
it "returns users with 2fa enabled via U2F" do
|
||||
user_with_2fa = create(:user, :two_factor_via_u2f)
|
||||
user_without_2fa = create(:user)
|
||||
users_with_two_factor = User.with_two_factor.pluck(:id)
|
||||
|
||||
expect(users_with_two_factor).to include(user_with_2fa.id)
|
||||
expect(users_with_two_factor).not_to include(user_without_2fa.id)
|
||||
end
|
||||
|
||||
it "returns users with 2fa enabled via OTP and U2F" do
|
||||
user_with_2fa = create(:user, :two_factor_via_otp, :two_factor_via_u2f)
|
||||
user_without_2fa = create(:user)
|
||||
users_with_two_factor = User.with_two_factor.pluck(:id)
|
||||
|
||||
expect(users_with_two_factor).to eq([user_with_2fa.id])
|
||||
expect(users_with_two_factor).not_to include(user_without_2fa.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe ".without_two_factor" do
|
||||
it "excludes users with 2fa enabled via OTP" do
|
||||
user_with_2fa = create(:user, :two_factor_via_otp)
|
||||
user_without_2fa = create(:user)
|
||||
users_without_two_factor = User.without_two_factor.pluck(:id)
|
||||
|
||||
expect(users_without_two_factor).to include(user_without_2fa.id)
|
||||
expect(users_without_two_factor).not_to include(user_with_2fa.id)
|
||||
end
|
||||
|
||||
it "excludes users with 2fa enabled via U2F" do
|
||||
user_with_2fa = create(:user, :two_factor_via_u2f)
|
||||
user_without_2fa = create(:user)
|
||||
users_without_two_factor = User.without_two_factor.pluck(:id)
|
||||
|
||||
expect(users_without_two_factor).to include(user_without_2fa.id)
|
||||
expect(users_without_two_factor).not_to include(user_with_2fa.id)
|
||||
end
|
||||
|
||||
it "excludes users with 2fa enabled via OTP and U2F" do
|
||||
user_with_2fa = create(:user, :two_factor_via_otp, :two_factor_via_u2f)
|
||||
user_without_2fa = create(:user)
|
||||
users_without_two_factor = User.without_two_factor.pluck(:id)
|
||||
|
||||
expect(users_without_two_factor).to include(user_without_2fa.id)
|
||||
expect(users_without_two_factor).not_to include(user_with_2fa.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "Respond to" do
|
||||
it { is_expected.to respond_to(:is_admin?) }
|
||||
it { is_expected.to respond_to(:name) }
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
class FakeU2fDevice
|
||||
def initialize(page)
|
||||
@page = page
|
||||
end
|
||||
|
||||
def respond_to_u2f_registration
|
||||
app_id = @page.evaluate_script('gon.u2f.app_id')
|
||||
challenges = @page.evaluate_script('gon.u2f.challenges')
|
||||
|
||||
json_response = u2f_device(app_id).register_response(challenges[0])
|
||||
|
||||
@page.execute_script("
|
||||
u2f.register = function(appId, registerRequests, signRequests, callback) {
|
||||
callback(#{json_response});
|
||||
};
|
||||
")
|
||||
end
|
||||
|
||||
def respond_to_u2f_authentication
|
||||
app_id = @page.evaluate_script('gon.u2f.app_id')
|
||||
challenges = @page.evaluate_script('gon.u2f.challenges')
|
||||
json_response = u2f_device(app_id).sign_response(challenges[0])
|
||||
|
||||
@page.execute_script("
|
||||
u2f.sign = function(appId, challenges, signRequests, callback) {
|
||||
callback(#{json_response});
|
||||
};
|
||||
")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def u2f_device(app_id)
|
||||
@u2f_device ||= U2F::FakeU2F.new(app_id)
|
||||
end
|
||||
end
|
||||
Vendored
+748
@@ -0,0 +1,748 @@
|
||||
//Copyright 2014-2015 Google Inc. All rights reserved.
|
||||
|
||||
//Use of this source code is governed by a BSD-style
|
||||
//license that can be found in the LICENSE file or at
|
||||
//https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
/**
|
||||
* @fileoverview The U2F api.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
|
||||
/**
|
||||
* Namespace for the U2F api.
|
||||
* @type {Object}
|
||||
*/
|
||||
var u2f = u2f || {};
|
||||
|
||||
/**
|
||||
* FIDO U2F Javascript API Version
|
||||
* @number
|
||||
*/
|
||||
var js_api_version;
|
||||
|
||||
/**
|
||||
* The U2F extension id
|
||||
* @const {string}
|
||||
*/
|
||||
// The Chrome packaged app extension ID.
|
||||
// Uncomment this if you want to deploy a server instance that uses
|
||||
// the package Chrome app and does not require installing the U2F Chrome extension.
|
||||
u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd';
|
||||
// The U2F Chrome extension ID.
|
||||
// Uncomment this if you want to deploy a server instance that uses
|
||||
// the U2F Chrome extension to authenticate.
|
||||
// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne';
|
||||
|
||||
|
||||
/**
|
||||
* Message types for messsages to/from the extension
|
||||
* @const
|
||||
* @enum {string}
|
||||
*/
|
||||
u2f.MessageTypes = {
|
||||
'U2F_REGISTER_REQUEST': 'u2f_register_request',
|
||||
'U2F_REGISTER_RESPONSE': 'u2f_register_response',
|
||||
'U2F_SIGN_REQUEST': 'u2f_sign_request',
|
||||
'U2F_SIGN_RESPONSE': 'u2f_sign_response',
|
||||
'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request',
|
||||
'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response'
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Response status codes
|
||||
* @const
|
||||
* @enum {number}
|
||||
*/
|
||||
u2f.ErrorCodes = {
|
||||
'OK': 0,
|
||||
'OTHER_ERROR': 1,
|
||||
'BAD_REQUEST': 2,
|
||||
'CONFIGURATION_UNSUPPORTED': 3,
|
||||
'DEVICE_INELIGIBLE': 4,
|
||||
'TIMEOUT': 5
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* A message for registration requests
|
||||
* @typedef {{
|
||||
* type: u2f.MessageTypes,
|
||||
* appId: ?string,
|
||||
* timeoutSeconds: ?number,
|
||||
* requestId: ?number
|
||||
* }}
|
||||
*/
|
||||
u2f.U2fRequest;
|
||||
|
||||
|
||||
/**
|
||||
* A message for registration responses
|
||||
* @typedef {{
|
||||
* type: u2f.MessageTypes,
|
||||
* responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse),
|
||||
* requestId: ?number
|
||||
* }}
|
||||
*/
|
||||
u2f.U2fResponse;
|
||||
|
||||
|
||||
/**
|
||||
* An error object for responses
|
||||
* @typedef {{
|
||||
* errorCode: u2f.ErrorCodes,
|
||||
* errorMessage: ?string
|
||||
* }}
|
||||
*/
|
||||
u2f.Error;
|
||||
|
||||
/**
|
||||
* Data object for a single sign request.
|
||||
* @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}}
|
||||
*/
|
||||
u2f.Transport;
|
||||
|
||||
|
||||
/**
|
||||
* Data object for a single sign request.
|
||||
* @typedef {Array<u2f.Transport>}
|
||||
*/
|
||||
u2f.Transports;
|
||||
|
||||
/**
|
||||
* Data object for a single sign request.
|
||||
* @typedef {{
|
||||
* version: string,
|
||||
* challenge: string,
|
||||
* keyHandle: string,
|
||||
* appId: string
|
||||
* }}
|
||||
*/
|
||||
u2f.SignRequest;
|
||||
|
||||
|
||||
/**
|
||||
* Data object for a sign response.
|
||||
* @typedef {{
|
||||
* keyHandle: string,
|
||||
* signatureData: string,
|
||||
* clientData: string
|
||||
* }}
|
||||
*/
|
||||
u2f.SignResponse;
|
||||
|
||||
|
||||
/**
|
||||
* Data object for a registration request.
|
||||
* @typedef {{
|
||||
* version: string,
|
||||
* challenge: string
|
||||
* }}
|
||||
*/
|
||||
u2f.RegisterRequest;
|
||||
|
||||
|
||||
/**
|
||||
* Data object for a registration response.
|
||||
* @typedef {{
|
||||
* version: string,
|
||||
* keyHandle: string,
|
||||
* transports: Transports,
|
||||
* appId: string
|
||||
* }}
|
||||
*/
|
||||
u2f.RegisterResponse;
|
||||
|
||||
|
||||
/**
|
||||
* Data object for a registered key.
|
||||
* @typedef {{
|
||||
* version: string,
|
||||
* keyHandle: string,
|
||||
* transports: ?Transports,
|
||||
* appId: ?string
|
||||
* }}
|
||||
*/
|
||||
u2f.RegisteredKey;
|
||||
|
||||
|
||||
/**
|
||||
* Data object for a get API register response.
|
||||
* @typedef {{
|
||||
* js_api_version: number
|
||||
* }}
|
||||
*/
|
||||
u2f.GetJsApiVersionResponse;
|
||||
|
||||
|
||||
//Low level MessagePort API support
|
||||
|
||||
/**
|
||||
* Sets up a MessagePort to the U2F extension using the
|
||||
* available mechanisms.
|
||||
* @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
|
||||
*/
|
||||
u2f.getMessagePort = function(callback) {
|
||||
if (typeof chrome != 'undefined' && chrome.runtime) {
|
||||
// The actual message here does not matter, but we need to get a reply
|
||||
// for the callback to run. Thus, send an empty signature request
|
||||
// in order to get a failure response.
|
||||
var msg = {
|
||||
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
|
||||
signRequests: []
|
||||
};
|
||||
chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() {
|
||||
if (!chrome.runtime.lastError) {
|
||||
// We are on a whitelisted origin and can talk directly
|
||||
// with the extension.
|
||||
u2f.getChromeRuntimePort_(callback);
|
||||
} else {
|
||||
// chrome.runtime was available, but we couldn't message
|
||||
// the extension directly, use iframe
|
||||
u2f.getIframePort_(callback);
|
||||
}
|
||||
});
|
||||
} else if (u2f.isAndroidChrome_()) {
|
||||
u2f.getAuthenticatorPort_(callback);
|
||||
} else if (u2f.isIosChrome_()) {
|
||||
u2f.getIosPort_(callback);
|
||||
} else {
|
||||
// chrome.runtime was not available at all, which is normal
|
||||
// when this origin doesn't have access to any extensions.
|
||||
u2f.getIframePort_(callback);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Detect chrome running on android based on the browser's useragent.
|
||||
* @private
|
||||
*/
|
||||
u2f.isAndroidChrome_ = function() {
|
||||
var userAgent = navigator.userAgent;
|
||||
return userAgent.indexOf('Chrome') != -1 &&
|
||||
userAgent.indexOf('Android') != -1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Detect chrome running on iOS based on the browser's platform.
|
||||
* @private
|
||||
*/
|
||||
u2f.isIosChrome_ = function() {
|
||||
return $.inArray(navigator.platform, ["iPhone", "iPad", "iPod"]) > -1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Connects directly to the extension via chrome.runtime.connect.
|
||||
* @param {function(u2f.WrappedChromeRuntimePort_)} callback
|
||||
* @private
|
||||
*/
|
||||
u2f.getChromeRuntimePort_ = function(callback) {
|
||||
var port = chrome.runtime.connect(u2f.EXTENSION_ID,
|
||||
{'includeTlsChannelId': true});
|
||||
setTimeout(function() {
|
||||
callback(new u2f.WrappedChromeRuntimePort_(port));
|
||||
}, 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a 'port' abstraction to the Authenticator app.
|
||||
* @param {function(u2f.WrappedAuthenticatorPort_)} callback
|
||||
* @private
|
||||
*/
|
||||
u2f.getAuthenticatorPort_ = function(callback) {
|
||||
setTimeout(function() {
|
||||
callback(new u2f.WrappedAuthenticatorPort_());
|
||||
}, 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a 'port' abstraction to the iOS client app.
|
||||
* @param {function(u2f.WrappedIosPort_)} callback
|
||||
* @private
|
||||
*/
|
||||
u2f.getIosPort_ = function(callback) {
|
||||
setTimeout(function() {
|
||||
callback(new u2f.WrappedIosPort_());
|
||||
}, 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* A wrapper for chrome.runtime.Port that is compatible with MessagePort.
|
||||
* @param {Port} port
|
||||
* @constructor
|
||||
* @private
|
||||
*/
|
||||
u2f.WrappedChromeRuntimePort_ = function(port) {
|
||||
this.port_ = port;
|
||||
};
|
||||
|
||||
/**
|
||||
* Format and return a sign request compliant with the JS API version supported by the extension.
|
||||
* @param {Array<u2f.SignRequest>} signRequests
|
||||
* @param {number} timeoutSeconds
|
||||
* @param {number} reqId
|
||||
* @return {Object}
|
||||
*/
|
||||
u2f.formatSignRequest_ =
|
||||
function(appId, challenge, registeredKeys, timeoutSeconds, reqId) {
|
||||
if (js_api_version === undefined || js_api_version < 1.1) {
|
||||
// Adapt request to the 1.0 JS API
|
||||
var signRequests = [];
|
||||
for (var i = 0; i < registeredKeys.length; i++) {
|
||||
signRequests[i] = {
|
||||
version: registeredKeys[i].version,
|
||||
challenge: challenge,
|
||||
keyHandle: registeredKeys[i].keyHandle,
|
||||
appId: appId
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
|
||||
signRequests: signRequests,
|
||||
timeoutSeconds: timeoutSeconds,
|
||||
requestId: reqId
|
||||
};
|
||||
}
|
||||
// JS 1.1 API
|
||||
return {
|
||||
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
|
||||
appId: appId,
|
||||
challenge: challenge,
|
||||
registeredKeys: registeredKeys,
|
||||
timeoutSeconds: timeoutSeconds,
|
||||
requestId: reqId
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Format and return a register request compliant with the JS API version supported by the extension..
|
||||
* @param {Array<u2f.SignRequest>} signRequests
|
||||
* @param {Array<u2f.RegisterRequest>} signRequests
|
||||
* @param {number} timeoutSeconds
|
||||
* @param {number} reqId
|
||||
* @return {Object}
|
||||
*/
|
||||
u2f.formatRegisterRequest_ =
|
||||
function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) {
|
||||
if (js_api_version === undefined || js_api_version < 1.1) {
|
||||
// Adapt request to the 1.0 JS API
|
||||
for (var i = 0; i < registerRequests.length; i++) {
|
||||
registerRequests[i].appId = appId;
|
||||
}
|
||||
var signRequests = [];
|
||||
for (var i = 0; i < registeredKeys.length; i++) {
|
||||
signRequests[i] = {
|
||||
version: registeredKeys[i].version,
|
||||
challenge: registerRequests[0],
|
||||
keyHandle: registeredKeys[i].keyHandle,
|
||||
appId: appId
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
|
||||
signRequests: signRequests,
|
||||
registerRequests: registerRequests,
|
||||
timeoutSeconds: timeoutSeconds,
|
||||
requestId: reqId
|
||||
};
|
||||
}
|
||||
// JS 1.1 API
|
||||
return {
|
||||
type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
|
||||
appId: appId,
|
||||
registerRequests: registerRequests,
|
||||
registeredKeys: registeredKeys,
|
||||
timeoutSeconds: timeoutSeconds,
|
||||
requestId: reqId
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Posts a message on the underlying channel.
|
||||
* @param {Object} message
|
||||
*/
|
||||
u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) {
|
||||
this.port_.postMessage(message);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Emulates the HTML 5 addEventListener interface. Works only for the
|
||||
* onmessage event, which is hooked up to the chrome.runtime.Port.onMessage.
|
||||
* @param {string} eventName
|
||||
* @param {function({data: Object})} handler
|
||||
*/
|
||||
u2f.WrappedChromeRuntimePort_.prototype.addEventListener =
|
||||
function(eventName, handler) {
|
||||
var name = eventName.toLowerCase();
|
||||
if (name == 'message' || name == 'onmessage') {
|
||||
this.port_.onMessage.addListener(function(message) {
|
||||
// Emulate a minimal MessageEvent object
|
||||
handler({'data': message});
|
||||
});
|
||||
} else {
|
||||
console.error('WrappedChromeRuntimePort only supports onMessage');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrap the Authenticator app with a MessagePort interface.
|
||||
* @constructor
|
||||
* @private
|
||||
*/
|
||||
u2f.WrappedAuthenticatorPort_ = function() {
|
||||
this.requestId_ = -1;
|
||||
this.requestObject_ = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch the Authenticator intent.
|
||||
* @param {Object} message
|
||||
*/
|
||||
u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) {
|
||||
var intentUrl =
|
||||
u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
|
||||
';S.request=' + encodeURIComponent(JSON.stringify(message)) +
|
||||
';end';
|
||||
document.location = intentUrl;
|
||||
};
|
||||
|
||||
/**
|
||||
* Tells what type of port this is.
|
||||
* @return {String} port type
|
||||
*/
|
||||
u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() {
|
||||
return "WrappedAuthenticatorPort_";
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Emulates the HTML 5 addEventListener interface.
|
||||
* @param {string} eventName
|
||||
* @param {function({data: Object})} handler
|
||||
*/
|
||||
u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) {
|
||||
var name = eventName.toLowerCase();
|
||||
if (name == 'message') {
|
||||
var self = this;
|
||||
/* Register a callback to that executes when
|
||||
* chrome injects the response. */
|
||||
window.addEventListener(
|
||||
'message', self.onRequestUpdate_.bind(self, handler), false);
|
||||
} else {
|
||||
console.error('WrappedAuthenticatorPort only supports message');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback invoked when a response is received from the Authenticator.
|
||||
* @param function({data: Object}) callback
|
||||
* @param {Object} message message Object
|
||||
*/
|
||||
u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ =
|
||||
function(callback, message) {
|
||||
var messageObject = JSON.parse(message.data);
|
||||
var intentUrl = messageObject['intentURL'];
|
||||
|
||||
var errorCode = messageObject['errorCode'];
|
||||
var responseObject = null;
|
||||
if (messageObject.hasOwnProperty('data')) {
|
||||
responseObject = /** @type {Object} */ (
|
||||
JSON.parse(messageObject['data']));
|
||||
}
|
||||
|
||||
callback({'data': responseObject});
|
||||
};
|
||||
|
||||
/**
|
||||
* Base URL for intents to Authenticator.
|
||||
* @const
|
||||
* @private
|
||||
*/
|
||||
u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ =
|
||||
'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE';
|
||||
|
||||
/**
|
||||
* Wrap the iOS client app with a MessagePort interface.
|
||||
* @constructor
|
||||
* @private
|
||||
*/
|
||||
u2f.WrappedIosPort_ = function() {};
|
||||
|
||||
/**
|
||||
* Launch the iOS client app request
|
||||
* @param {Object} message
|
||||
*/
|
||||
u2f.WrappedIosPort_.prototype.postMessage = function(message) {
|
||||
var str = JSON.stringify(message);
|
||||
var url = "u2f://auth?" + encodeURI(str);
|
||||
location.replace(url);
|
||||
};
|
||||
|
||||
/**
|
||||
* Tells what type of port this is.
|
||||
* @return {String} port type
|
||||
*/
|
||||
u2f.WrappedIosPort_.prototype.getPortType = function() {
|
||||
return "WrappedIosPort_";
|
||||
};
|
||||
|
||||
/**
|
||||
* Emulates the HTML 5 addEventListener interface.
|
||||
* @param {string} eventName
|
||||
* @param {function({data: Object})} handler
|
||||
*/
|
||||
u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) {
|
||||
var name = eventName.toLowerCase();
|
||||
if (name !== 'message') {
|
||||
console.error('WrappedIosPort only supports message');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets up an embedded trampoline iframe, sourced from the extension.
|
||||
* @param {function(MessagePort)} callback
|
||||
* @private
|
||||
*/
|
||||
u2f.getIframePort_ = function(callback) {
|
||||
// Create the iframe
|
||||
var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID;
|
||||
var iframe = document.createElement('iframe');
|
||||
iframe.src = iframeOrigin + '/u2f-comms.html';
|
||||
iframe.setAttribute('style', 'display:none');
|
||||
document.body.appendChild(iframe);
|
||||
|
||||
var channel = new MessageChannel();
|
||||
var ready = function(message) {
|
||||
if (message.data == 'ready') {
|
||||
channel.port1.removeEventListener('message', ready);
|
||||
callback(channel.port1);
|
||||
} else {
|
||||
console.error('First event on iframe port was not "ready"');
|
||||
}
|
||||
};
|
||||
channel.port1.addEventListener('message', ready);
|
||||
channel.port1.start();
|
||||
|
||||
iframe.addEventListener('load', function() {
|
||||
// Deliver the port to the iframe and initialize
|
||||
iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
//High-level JS API
|
||||
|
||||
/**
|
||||
* Default extension response timeout in seconds.
|
||||
* @const
|
||||
*/
|
||||
u2f.EXTENSION_TIMEOUT_SEC = 30;
|
||||
|
||||
/**
|
||||
* A singleton instance for a MessagePort to the extension.
|
||||
* @type {MessagePort|u2f.WrappedChromeRuntimePort_}
|
||||
* @private
|
||||
*/
|
||||
u2f.port_ = null;
|
||||
|
||||
/**
|
||||
* Callbacks waiting for a port
|
||||
* @type {Array<function((MessagePort|u2f.WrappedChromeRuntimePort_))>}
|
||||
* @private
|
||||
*/
|
||||
u2f.waitingForPort_ = [];
|
||||
|
||||
/**
|
||||
* A counter for requestIds.
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
u2f.reqCounter_ = 0;
|
||||
|
||||
/**
|
||||
* A map from requestIds to client callbacks
|
||||
* @type {Object.<number,(function((u2f.Error|u2f.RegisterResponse))
|
||||
* |function((u2f.Error|u2f.SignResponse)))>}
|
||||
* @private
|
||||
*/
|
||||
u2f.callbackMap_ = {};
|
||||
|
||||
/**
|
||||
* Creates or retrieves the MessagePort singleton to use.
|
||||
* @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
|
||||
* @private
|
||||
*/
|
||||
u2f.getPortSingleton_ = function(callback) {
|
||||
if (u2f.port_) {
|
||||
callback(u2f.port_);
|
||||
} else {
|
||||
if (u2f.waitingForPort_.length == 0) {
|
||||
u2f.getMessagePort(function(port) {
|
||||
u2f.port_ = port;
|
||||
u2f.port_.addEventListener('message',
|
||||
/** @type {function(Event)} */ (u2f.responseHandler_));
|
||||
|
||||
// Careful, here be async callbacks. Maybe.
|
||||
while (u2f.waitingForPort_.length)
|
||||
u2f.waitingForPort_.shift()(u2f.port_);
|
||||
});
|
||||
}
|
||||
u2f.waitingForPort_.push(callback);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles response messages from the extension.
|
||||
* @param {MessageEvent.<u2f.Response>} message
|
||||
* @private
|
||||
*/
|
||||
u2f.responseHandler_ = function(message) {
|
||||
var response = message.data;
|
||||
var reqId = response['requestId'];
|
||||
if (!reqId || !u2f.callbackMap_[reqId]) {
|
||||
console.error('Unknown or missing requestId in response.');
|
||||
return;
|
||||
}
|
||||
var cb = u2f.callbackMap_[reqId];
|
||||
delete u2f.callbackMap_[reqId];
|
||||
cb(response['responseData']);
|
||||
};
|
||||
|
||||
/**
|
||||
* Dispatches an array of sign requests to available U2F tokens.
|
||||
* If the JS API version supported by the extension is unknown, it first sends a
|
||||
* message to the extension to find out the supported API version and then it sends
|
||||
* the sign request.
|
||||
* @param {string=} appId
|
||||
* @param {string=} challenge
|
||||
* @param {Array<u2f.RegisteredKey>} registeredKeys
|
||||
* @param {function((u2f.Error|u2f.SignResponse))} callback
|
||||
* @param {number=} opt_timeoutSeconds
|
||||
*/
|
||||
u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
|
||||
if (js_api_version === undefined) {
|
||||
// Send a message to get the extension to JS API version, then send the actual sign request.
|
||||
u2f.getApiVersion(
|
||||
function (response) {
|
||||
js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version'];
|
||||
console.log("Extension JS API Version: ", js_api_version);
|
||||
u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
|
||||
});
|
||||
} else {
|
||||
// We know the JS API version. Send the actual sign request in the supported API version.
|
||||
u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Dispatches an array of sign requests to available U2F tokens.
|
||||
* @param {string=} appId
|
||||
* @param {string=} challenge
|
||||
* @param {Array<u2f.RegisteredKey>} registeredKeys
|
||||
* @param {function((u2f.Error|u2f.SignResponse))} callback
|
||||
* @param {number=} opt_timeoutSeconds
|
||||
*/
|
||||
u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
|
||||
u2f.getPortSingleton_(function(port) {
|
||||
var reqId = ++u2f.reqCounter_;
|
||||
u2f.callbackMap_[reqId] = callback;
|
||||
var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
|
||||
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
|
||||
var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId);
|
||||
port.postMessage(req);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Dispatches register requests to available U2F tokens. An array of sign
|
||||
* requests identifies already registered tokens.
|
||||
* If the JS API version supported by the extension is unknown, it first sends a
|
||||
* message to the extension to find out the supported API version and then it sends
|
||||
* the register request.
|
||||
* @param {string=} appId
|
||||
* @param {Array<u2f.RegisterRequest>} registerRequests
|
||||
* @param {Array<u2f.RegisteredKey>} registeredKeys
|
||||
* @param {function((u2f.Error|u2f.RegisterResponse))} callback
|
||||
* @param {number=} opt_timeoutSeconds
|
||||
*/
|
||||
u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
|
||||
if (js_api_version === undefined) {
|
||||
// Send a message to get the extension to JS API version, then send the actual register request.
|
||||
u2f.getApiVersion(
|
||||
function (response) {
|
||||
js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version'];
|
||||
console.log("Extension JS API Version: ", js_api_version);
|
||||
u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
|
||||
callback, opt_timeoutSeconds);
|
||||
});
|
||||
} else {
|
||||
// We know the JS API version. Send the actual register request in the supported API version.
|
||||
u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
|
||||
callback, opt_timeoutSeconds);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Dispatches register requests to available U2F tokens. An array of sign
|
||||
* requests identifies already registered tokens.
|
||||
* @param {string=} appId
|
||||
* @param {Array<u2f.RegisterRequest>} registerRequests
|
||||
* @param {Array<u2f.RegisteredKey>} registeredKeys
|
||||
* @param {function((u2f.Error|u2f.RegisterResponse))} callback
|
||||
* @param {number=} opt_timeoutSeconds
|
||||
*/
|
||||
u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
|
||||
u2f.getPortSingleton_(function(port) {
|
||||
var reqId = ++u2f.reqCounter_;
|
||||
u2f.callbackMap_[reqId] = callback;
|
||||
var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
|
||||
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
|
||||
var req = u2f.formatRegisterRequest_(
|
||||
appId, registeredKeys, registerRequests, timeoutSeconds, reqId);
|
||||
port.postMessage(req);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Dispatches a message to the extension to find out the supported
|
||||
* JS API version.
|
||||
* If the user is on a mobile phone and is thus using Google Authenticator instead
|
||||
* of the Chrome extension, don't send the request and simply return 0.
|
||||
* @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback
|
||||
* @param {number=} opt_timeoutSeconds
|
||||
*/
|
||||
u2f.getApiVersion = function(callback, opt_timeoutSeconds) {
|
||||
u2f.getPortSingleton_(function(port) {
|
||||
// If we are using Android Google Authenticator or iOS client app,
|
||||
// do not fire an intent to ask which JS API version to use.
|
||||
if (port.getPortType) {
|
||||
var apiVersion;
|
||||
switch (port.getPortType()) {
|
||||
case 'WrappedIosPort_':
|
||||
case 'WrappedAuthenticatorPort_':
|
||||
apiVersion = 1.1;
|
||||
break;
|
||||
|
||||
default:
|
||||
apiVersion = 0;
|
||||
break;
|
||||
}
|
||||
callback({ 'js_api_version': apiVersion });
|
||||
return;
|
||||
}
|
||||
var reqId = ++u2f.reqCounter_;
|
||||
u2f.callbackMap_[reqId] = callback;
|
||||
var req = {
|
||||
type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST,
|
||||
timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ?
|
||||
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC),
|
||||
requestId: reqId
|
||||
};
|
||||
port.postMessage(req);
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user