diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee
index 18c1aa0d4e282c9b1164fedcaf9db6eab022df4d..a76b111bf036b8534cf2a8df217870b666aa9d40 100644
--- a/app/assets/javascripts/application.js.coffee
+++ b/app/assets/javascripts/application.js.coffee
@@ -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()
diff --git a/app/assets/javascripts/u2f/error.js.coffee b/app/assets/javascripts/u2f/error.js.coffee
new file mode 100644
index 0000000000000000000000000000000000000000..1a2fc3e757f4183f28d297022781273ca6da7139
--- /dev/null
+++ b/app/assets/javascripts/u2f/error.js.coffee
@@ -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."
diff --git a/app/assets/javascripts/u2f/register.js.coffee b/app/assets/javascripts/u2f/register.js.coffee
new file mode 100644
index 0000000000000000000000000000000000000000..74472cfa1208724727fb463b4fc3c2125ec9df37
--- /dev/null
+++ b/app/assets/javascripts/u2f/register.js.coffee
@@ -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')
diff --git a/app/assets/javascripts/u2f/util.js.coffee.erb b/app/assets/javascripts/u2f/util.js.coffee.erb
new file mode 100644
index 0000000000000000000000000000000000000000..d59341c38b917ef39f699d120cbe96ed5af668ad
--- /dev/null
+++ b/app/assets/javascripts/u2f/util.js.coffee.erb
@@ -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 %>
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index e73b2d085518378de74fef4f50738aea6ad6e01b..62f63701799af446f4ec2ae45d8ec91832d45075 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -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
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index 8f83fdd02bc71c256564ac40e6731415021e85ce..6a358fdcc0583abeba2ae418817c49b848d5a038 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -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
+
+  # A U2F (universal 2nd factor) device's information is stored after successful
+  # registration, which is then used while 2FA authentication is taking place.
+  def create_u2f
+    @u2f_registration = U2fRegistration.register(current_user, u2f_app_id, params[:device_response], session[:challenges])
 
-      render 'new'
+    if @u2f_registration.persisted?
+      session.delete(:challenges)
+      redirect_to profile_account_path, notice: "Your U2F device was registered!"
+    else
+      @qr_code = build_qr_code
+      setup_u2f_registration
+      render :show
     end
   end
 
@@ -70,4 +86,21 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
   def issuer_host
     Gitlab.config.gitlab.host
   end
+
+  # Setup in preparation of communication with a U2F (universal 2nd factor) device
+  # Actual communication is performed using a Javascript API
+  def setup_u2f_registration
+    @u2f_registration ||= U2fRegistration.new
+    @registration_key_handles = current_user.u2f_registrations.pluck(:key_handle)
+    u2f = U2F::U2F.new(u2f_app_id)
+
+    registration_requests = u2f.registration_requests
+    sign_requests = u2f.authentication_requests(@registration_key_handles)
+    session[:challenges] = registration_requests.map(&:challenge)
+
+    gon.push(u2f: { challenges: session[:challenges], app_id: u2f_app_id,
+                    register_requests: registration_requests,
+                    sign_requests: sign_requests,
+                    browser_supports_u2f: browser_supports_u2f? })
+  end
 end
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 01ac8161945d2b802e83933a5614eceac717a5a1..3d2a245ecbdf85279bf10ac51f4ba087d845be16 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -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
diff --git a/app/views/profiles/two_factor_auths/new.html.haml b/app/views/profiles/two_factor_auths/new.html.haml
deleted file mode 100644
index 69fc81cb45c66327b7d54b400ad9fc2ae3659f1a..0000000000000000000000000000000000000000
--- a/app/views/profiles/two_factor_auths/new.html.haml
+++ /dev/null
@@ -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?
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..ce76cb73c9c82ec214f3bca70e5bf7572cb07500
--- /dev/null
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -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);
+
diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..46af591fc4315c517c09883ae278c157f8e999ef
--- /dev/null
+++ b/app/views/u2f/_register.html.haml
@@ -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();
diff --git a/config/routes.rb b/config/routes.rb
index 1fc7985136bc35263af35825ad653f183ebb850e..27ab79d68f5d360442276b2013f82d31495cd977 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -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
diff --git a/spec/controllers/profiles/two_factor_auths_controller_spec.rb b/spec/controllers/profiles/two_factor_auths_controller_spec.rb
index 4fb1473c2d264ffc651487a2a8217c9d2879c65b..d08d0018b359a40bcf192afa6f490e2b0c84b9f0 100644
--- a/spec/controllers/profiles/two_factor_auths_controller_spec.rb
+++ b/spec/controllers/profiles/two_factor_auths_controller_spec.rb
@@ -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
diff --git a/spec/javascripts/fixtures/u2f/register.html.haml b/spec/javascripts/fixtures/u2f/register.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..393c0613fd300c9ccd3c76c0bd1ded662efb6d86
--- /dev/null
+++ b/spec/javascripts/fixtures/u2f/register.html.haml
@@ -0,0 +1 @@
+= render partial: "u2f/register", locals: { create_u2f_profile_two_factor_auth_path: '/profile/two_factor_auth/create_u2f' }
diff --git a/spec/javascripts/u2f/mock_u2f_device.js.coffee b/spec/javascripts/u2f/mock_u2f_device.js.coffee
new file mode 100644
index 0000000000000000000000000000000000000000..97ed0e83a0e3287cd3539070779348b081e4a66c
--- /dev/null
+++ b/spec/javascripts/u2f/mock_u2f_device.js.coffee
@@ -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)
diff --git a/spec/javascripts/u2f/register_spec.js.coffee b/spec/javascripts/u2f/register_spec.js.coffee
new file mode 100644
index 0000000000000000000000000000000000000000..0858abeca1aab86b940d15714be479dcf64c2459
--- /dev/null
+++ b/spec/javascripts/u2f/register_spec.js.coffee
@@ -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!")
diff --git a/vendor/assets/javascripts/u2f.js b/vendor/assets/javascripts/u2f.js
new file mode 100644
index 0000000000000000000000000000000000000000..e666b1360514dabcf4573ab0ec3fb6339d15c649
--- /dev/null
+++ b/vendor/assets/javascripts/u2f.js
@@ -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);
+    });
+};
\ No newline at end of file