Two factor authentication (#7751)

This commit is contained in:
lislis 2019-04-28 19:06:48 +02:00 committed by Jonne Haß
parent 3f74a759b3
commit 9d5b981809
36 changed files with 731 additions and 16 deletions

1
.gitignore vendored
View file

@ -11,6 +11,7 @@ app/assets/images/custom/
# Configuration files
config/diaspora.yml
config/initializers/secret_token.rb
config/initializers/twofa_encryption_key.rb
.bundle
vendor/bundle/
vendor/cache/

View file

@ -5,8 +5,9 @@
## Bug fixes
## Features
* Add a manifest.json file as a first step to make diaspora* a Progressive Web App [#7998](https://github.com/diaspora/diaspora/pull/7998)
* Add a manifest.json file as a first step to make diaspora\* a Progressive Web App [#7998](https://github.com/diaspora/diaspora/pull/7998)
* Allow `web+diaspora://` links to link to a profile with only the diaspora ID [#8000](https://github.com/diaspora/diaspora/pull/8000)
* Support TOTP two factor authentication [#7751](https://github.com/diaspora/diaspora/pull/7751)
# 0.7.10.0

View file

@ -27,7 +27,9 @@ gem "json-schema", "2.8.1"
# Authentication
gem "devise", "4.6.1"
gem "devise-two-factor", "3.0.3"
gem "devise_lastseenable", "0.0.6"
gem "rqrcode", "0.10.1"
# Captcha

View file

@ -60,6 +60,8 @@ GEM
mime-types (>= 2.99)
unf
ast (2.4.0)
attr_encrypted (3.1.0)
encryptor (~> 3.0.0)
attr_required (1.0.1)
autoprefixer-rails (8.6.5)
execjs
@ -169,6 +171,12 @@ GEM
railties (>= 4.1.0, < 6.0)
responders
warden (~> 1.2.3)
devise-two-factor (3.0.3)
activesupport (< 5.3)
attr_encrypted (>= 1.3, < 4, != 2)
devise (~> 4.0)
railties (< 5.3)
rotp (~> 2.0)
devise_lastseenable (0.0.6)
devise
rails (>= 3.0.4)
@ -191,6 +199,7 @@ GEM
docile (1.3.1)
domain_name (0.5.20180417)
unf (>= 0.0.5, < 1.0.0)
encryptor (3.0.0)
entypo-rails (3.0.0)
railties (>= 4.1, < 6)
equalizer (0.0.11)
@ -596,6 +605,9 @@ GEM
responders (2.4.1)
actionpack (>= 4.2.0, < 6.0)
railties (>= 4.2.0, < 6.0)
rotp (2.1.2)
rqrcode (0.10.1)
chunky_png (~> 1.0)
rspec (3.8.0)
rspec-core (~> 3.8.0)
rspec-expectations (~> 3.8.0)
@ -785,6 +797,7 @@ DEPENDENCIES
cucumber-rails (= 1.6.0)
database_cleaner (= 1.7.0)
devise (= 4.6.1)
devise-two-factor (= 3.0.3)
devise_lastseenable (= 0.0.6)
diaspora-prosody-config (= 0.0.7)
diaspora_federation-json_schema (= 0.2.6)
@ -876,6 +889,7 @@ DEPENDENCIES
redcarpet (= 3.4.0)
redis (= 3.3.5)
responders (= 2.4.1)
rqrcode (= 0.10.1)
rspec-json_expectations (~> 2.1)
rspec-rails (= 3.8.2)
rubocop (= 0.66.0)

View file

@ -1,4 +1,5 @@
.page-sessions.action-new,
.page-sessions.action-create,
.page-passwords.action-new,
.page-passwords.action-edit {
padding-top: 25px;

View file

@ -27,6 +27,7 @@ class ApplicationController < ActionController::Base
before_action :gon_set_current_user
before_action :gon_set_appconfig
before_action :gon_set_preloads
before_action :configure_permitted_parameters, if: :devise_controller?
inflection_method grammatical_gender: :gender
@ -182,4 +183,10 @@ class ApplicationController < ActionController::Base
return unless gon.preloads.nil?
gon.preloads = {}
end
protected
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt])
end
end

View file

@ -5,10 +5,54 @@
# the COPYRIGHT file.
class SessionsController < Devise::SessionsController
after_action :reset_authentication_token, only: [:create]
before_action :reset_authentication_token, only: [:destroy]
# rubocop:disable Rails/LexicallyScopedActionFilter
before_action :authenticate_with_2fa, only: :create
after_action :reset_authentication_token, only: :create
before_action :reset_authentication_token, only: :destroy
# rubocop:enable Rails/LexicallyScopedActionFilter
def find_user
return User.find(session[:otp_user_id]) if session[:otp_user_id]
User.find_for_authentication(username: params[:user][:username]) if params[:user][:username]
end
def authenticate_with_2fa
self.resource = find_user
u = find_user
return true unless u&.otp_required_for_login?
if params[:user][:otp_attempt].present? && session[:otp_user_id]
authenticate_with_two_factor_via_otp(u)
elsif u&.valid_password?(params[:user][:password])
prompt_for_two_factor(u)
end
end
def valid_otp_attempt?(user)
user.validate_and_consume_otp!(params[:user][:otp_attempt]) ||
user.invalidate_otp_backup_code!(params[:user][:otp_attempt])
rescue OpenSSL::Cipher::CipherError => _error
false
end
def authenticate_with_two_factor_via_otp(user)
if valid_otp_attempt?(user)
session.delete(:otp_user_id)
sign_in(user)
else
flash.now[:alert] = "Invalid token"
prompt_for_two_factor(user)
end
end
def prompt_for_two_factor(user)
session[:otp_user_id] = user.id
render :two_factor
end
def reset_authentication_token
current_user.reset_authentication_token! unless current_user.nil?
current_user&.reset_authentication_token!
end
end

View file

@ -0,0 +1,60 @@
# frozen_string_literal: true
class TwoFactorAuthenticationsController < ApplicationController
before_action :authenticate_user!
before_action :verify_otp_required, only: [:create]
def show
@user = current_user
end
def create
current_user.otp_secret = User.generate_otp_secret(32)
current_user.save!
redirect_to confirm_two_factor_authentication_path
end
def confirm_2fa
redirect_to two_factor_authentication_path if current_user.otp_required_for_login?
end
def confirm_and_activate_2fa
if current_user.validate_and_consume_otp!(params[:user][:code])
current_user.otp_required_for_login = true
current_user.save!
flash[:notice] = t("two_factor_auth.flash.success_activation")
redirect_to recovery_codes_two_factor_authentication_path
else
flash[:alert] = t("two_factor_auth.flash.error_token")
redirect_to confirm_two_factor_authentication_path
end
end
def recovery_codes
@recovery_codes = current_user.generate_otp_backup_codes!
current_user.save!
end
def destroy
if acceptable_code?
current_user.otp_required_for_login = false
current_user.save!
flash[:notice] = t("two_factor_auth.flash.success_deactivation")
else
flash.now[:alert] = t("two_factor_auth.flash.error_token")
end
redirect_to two_factor_authentication_path
end
private
def verify_otp_required
redirect_to two_factor_authentication_path if current_user.otp_required_for_login?
end
def acceptable_code?
current_user.validate_and_consume_otp!(params[:two_factor_authentication][:code]) ||
current_user.invalidate_otp_backup_code!(params[:two_factor_authentication][:code])
end
end

View file

@ -152,6 +152,8 @@ class UsersController < ApplicationController
:auto_follow_back_aspect_id,
:getting_started,
:post_default_public,
:otp_required_for_login,
:otp_secret,
email_preferences: UserPreference::VALID_EMAIL_TYPES.map(&:to_sym)
)
end

View file

@ -72,4 +72,9 @@ module ApplicationHelper
buf << [nonced_javascript_tag("$.fx.off = true;")] if Rails.env.test?
buf.join("\n").html_safe
end
def qrcode_uri
label = current_user.username
current_user.otp_provisioning_uri(label, issuer: AppConfig.environment.url)
end
end

View file

@ -19,7 +19,15 @@ class User < ApplicationRecord
scope :halfyear_actives, ->(time = Time.now) { logged_in_since(time - 6.month) }
scope :active, -> { joins(:person).where(people: {closed_account: false}) }
devise :database_authenticatable, :registerable,
attribute :otp_secret
devise :two_factor_authenticatable,
:two_factor_backupable,
otp_secret_encryption_key: AppConfig.twofa_encryption_key,
otp_backup_code_length: 16,
otp_number_of_backup_codes: 10
devise :registerable,
:recoverable, :rememberable, :trackable, :validatable,
:lockable, :lastseenable, :lock_strategy => :none, :unlock_strategy => :none
@ -42,6 +50,7 @@ class User < ApplicationRecord
validate :no_person_with_same_username
serialize :hidden_shareables, Hash
serialize :otp_backup_codes, Array
has_one :person, inverse_of: :owner, foreign_key: :owner_id
has_one :profile, through: :person

View file

@ -0,0 +1,39 @@
- content_for :page_title do
= AppConfig.settings.pod_name + " - " + t("two_factor_auth.title")
.container#twofa
.text-center
.logos-asterisk
%h1
= t("two_factor_auth.title")
= form_for resource, as: resource_name,
url: session_path(resource_name),
html: {class: "block-form"},
method: :post do |f|
%fieldset
%label.sr-only#otp-label{for: "otp_attempt"}
= t("two_factor_auth.input_token.label")
= f.text_field :otp_attempt,
type: :text,
placeholder: t("two_factor_auth.input_token.placeholder"),
required: true,
autofocus: true,
class: "input-block-level form-control"
%p= t "two_factor_auth.recovery.reminder"
.actions
= f.button t("devise.sessions.new.sign_in"),
type: :submit,
class: "btn btn-large btn-block btn-primary"
.text-center
- if display_password_reset_link?
= link_to t("devise.shared.links.forgot_your_password"),
new_password_path(resource_name), id: "forgot_password_link"
%br
- if display_registration_link?
= link_to t("devise.shared.links.sign_up"), new_registration_path(resource_name)

View file

@ -9,6 +9,8 @@
class: request.path == edit_user_path ? "list-group-item active" : "list-group-item"
= link_to t("privacy"), privacy_settings_path,
class: current_page?(privacy_settings_path) ? "list-group-item active" : "list-group-item"
= link_to t("two_factor_auth.title"), two_factor_authentication_path,
class: current_page?(two_factor_authentication_path) ? "list-group-item active" : "list-group-item"
= link_to t("_services"), services_path,
class: current_page?(services_path) ? "list-group-item active" : "list-group-item"
= link_to t("_applications"), api_openid_connect_user_applications_path,

View file

@ -0,0 +1,11 @@
.col-md-12
.row
.col-md-12
%h3= t("two_factor_auth.title")
%p= t("two_factor_auth.explanation")
.well= t("two_factor_auth.deactivated.status")
= form_for "user", url: two_factor_authentication_path, html: {method: :post} do |f|
= f.hidden_field :otp_required_for_login, value: true
.clearfix.form-group= f.submit t("two_factor_auth.deactivated.change_button"),
class: "btn btn-primary pull-right"

View file

@ -0,0 +1,33 @@
.col-md-12
.row
.col-md-12
%h3= t("two_factor_auth.confirm.title")
%p= t("two_factor_auth.confirm.status")
.small-horizontal-spacer
%h4= t("two_factor_auth.confirm.scan_title")
.row
.col-md-6
%p= t("two_factor_auth.confirm.scan_explanation")
.two-factor-qr
!= RQRCode::QRCode.new(qrcode_uri).as_svg(offset: 10, fill: "ffffff", module_size: 5)
.col-md-6
%p= t("two_factor_auth.confirm.manual_explanation")
%p!= t("two_factor_auth.confirm.manual_explanation_cont")
%pre.well= current_user.otp_secret.scan(/.{4}/).join(" ")
.row
.col-md-12
.small-horizontal-spacer
%h4= t("two_factor_auth.confirm.input_title")
= t("two_factor_auth.confirm.input_explanation")
= form_for "user", url: confirm_two_factor_authentication_path,
html: {method: :post, class: "form-horizontal"} do |f|
.form-group
= f.label :code, t("two_factor_auth.input_token.label"), class: "control-label col-sm-6"
.col-sm-6
= f.text_field :code, placeholder: t("two_factor_auth.input_token.placeholder"), class: "form-control"
.form-group
.col-sm-12
= link_to t("cancel"), two_factor_authentication_path, class: "btn btn-default"
= f.submit t("two_factor_auth.confirm.activate_button"), class: "btn btn-primary pull-right"

View file

@ -0,0 +1,28 @@
.col-md-12
.row
.col-md-12
%h3= t("two_factor_auth.title")
%p= t("two_factor_auth.explanation")
.well= t("two_factor_auth.activated.status")
%hr
%h4= t("two_factor_auth.activated.change_button")
%p= t("two_factor_auth.activated.change_label")
= form_for "two_factor_authentication", url: two_factor_authentication_path,
html: {method: :delete, class: "form-horizontal"} do |f|
.form-group
= f.label :code, t("two_factor_auth.input_token.label"), class: "control-label col-sm-6"
.col-sm-6
= f.text_field :code, placeholder: t("two_factor_auth.input_token.placeholder"), class: "form-control"
= t("two_factor_auth.recovery.reminder")
.clearfix= f.submit t("two_factor_auth.activated.change_button"), class: "btn btn-primary pull-right"
%hr
%h4= t("two_factor_auth.recovery.title")
%p= t("two_factor_auth.recovery.explanation_short")
%p= t("two_factor_auth.recovery.invalidation_notice")
%p= link_to t("two_factor_auth.recovery.button"),
recovery_codes_two_factor_authentication_path, class: "btn btn-default"

View file

@ -0,0 +1,18 @@
.col-md-12
.row
.col-md-12
%h3= t("two_factor_auth.title")
.well= t("two_factor_auth.activated.status")
%hr
%h3= t("two_factor_auth.recovery.title")
%p= t("two_factor_auth.recovery.explanation")
%ol.recovery-codes
- @recovery_codes.each do |code|
%li<
%samp= code
.form-group.submit_block.clearfix
= link_to t("ok"), two_factor_authentication_path, class: "btn btn-primary pull-right"

View file

@ -0,0 +1,4 @@
.form-group
= f.label :code, t("two_factor_auth.input_token.label"), class: "control-label col-sm-6"
.col-sm-6
= f.text_field :code, placeholder: t("two_factor_auth.input_token.placeholder"), class: "form-control"

View file

@ -0,0 +1,11 @@
- content_for :page_title do
= t("two_factor_auth.confirm.title")
.container-fluid
.row
.col-md-3
.sidebar
= render "shared/settings_nav"
.col-md-9
.framed-content.clearfix
= render "confirm"

View file

@ -0,0 +1,12 @@
- content_for :page_title do
= t("two_factor_auth.title")
= t("two_factor_auth.recovery.title")
.container-fluid
.row
.col-md-3
.sidebar
= render "shared/settings_nav"
.col-md-9
.framed-content.clearfix
= render "recovery"

View file

@ -0,0 +1,15 @@
- content_for :page_title do
= t("two_factor_auth.title")
.container-fluid
.row
.col-md-3
.sidebar
= render "shared/settings_nav"
.col-md-9
.framed-content.clearfix
- if @user.otp_required_for_login
= render "deactivate"
- else
= render "activate"

View file

@ -15,6 +15,11 @@ end
# Use this hook to configure devise mailer, warden hooks and so forth.
# Many of these configuration options can be set straight in your model.
Devise.setup do |config|
config.warden do |manager|
manager.default_strategies(scope: :user).unshift :two_factor_authenticatable
manager.default_strategies(scope: :user).unshift :two_factor_backupable
end
# The secret key used by Devise. Devise uses this key to generate
# random tokens. Changing this key will render invalid all existing
# confirmation, reset password and unlock tokens in the database.
@ -270,4 +275,8 @@ Devise.setup do |config|
# When using omniauth, Devise cannot automatically set Omniauth path,
# so you need to do it manually. For the users scope, it would be:
# config.omniauth_path_prefix = '/my_engine/users/auth'
# if a user enables 2fa this would log them in without requiring them
# to enter a token
config.sign_in_after_reset_password = false
end

View file

@ -4,3 +4,4 @@
# Configure sensitive parameters which will be filtered from the log file.
Rails.application.config.filter_parameters += %i[password message text bio]
Rails.application.config.filter_parameters += [:otp_attempt]

View file

@ -1311,6 +1311,42 @@ en:
email_confirmed: "Email %{email} activated"
email_not_confirmed: "Email could not be activated. Wrong link?"
two_factor_auth:
title: "Two-factor authentication"
explanation: "Two-factor authentication is a powerful way to ensure you are the only one able to sign in to your account. When signing in, you will enter a 6-digit code along with your password to prove your identity. Be careful though: if you lose your phone and the recovery codes created when you activate this feature, access to your diaspora* account will be blocked forever."
activated:
status: "Two-factor authentication activated"
change_label: "Deactivate two-factor authentication by entering a TOTP token."
change_button: "Deactivate"
deactivated:
status: "Two-factor authentication not activated"
change_label: "Activate two-factor authentication"
change_button: "Activate"
confirm:
title: "Confirm activation"
status: "Two-factor authentication is not fully activated yet, you need to confirm activation with a TOTP token"
scan_title: "Scan the QR code"
scan_explanation: "Please scan the QR code with a TOTP capable app, such as andOTP (Android), FreeOTP (iOS), SailOTP (SailfishOS)."
manual_explanation: "In case you cant scan the QR code automatically you can manually enter the secret in your app."
manual_explanation_cont: "We are using time-based one-time passwords (TOTP) with six-digit tokens. In case your app prompts you for a time interval and algorithm enter 30 seconds and sha1 respectively. <br /> The spaces are just for readability, please enter the code without them."
input_title: "Confim with TOTP token"
input_explanation: "After scanning or entering the secret, enter the six-digit code you see and confirm the setup."
activate_button: "Confirm and activate"
input_token:
label: "Two-factor token"
placeholder: "six-digit two-factor token"
recovery:
title: "Recovery codes"
reminder: "Alternatively, you can use one of the recovery codes."
explanation: "If you ever lose access to your phone, you can use one of the recovery codes below to regain access to your account. Keep the recovery codes safe. For example, you may print them and store them with other important documents."
explanation_short: "Recovery codes allow you to regain access to your account if you lose your phone. Note that you can use each recovery code only once."
invalidation_notice: "If you've lost your recovery codes, you can regenerate them here. Your old recovery codes will be invalidated."
button: "Generate new recovery codes"
flash:
success_activation: "Successfully activated two-factor authentication"
success_deactivation: "Successfully deactivated two-factor authentication"
error_token: "Token was incorrect or invalid"
will_paginate:
previous_label: "&laquo; previous"
next_label: "next &raquo;"

View file

@ -119,6 +119,12 @@ Rails.application.routes.draw do
get "getting_started_completed" => :getting_started_completed
end
resource :two_factor_authentication, only: %i[show create destroy] do
get :confirm, action: :confirm_2fa
post :confirm, action: :confirm_and_activate_2fa
get :recovery_codes
end
devise_for :users, controllers: {sessions: :sessions}, skip: :registration
devise_scope :user do
get "/users/sign_up" => "registrations#new", :as => :new_user_registration

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
class AddDeviseTwoFactorToUsers < ActiveRecord::Migration[5.1]
def change
add_column :users, :encrypted_otp_secret, :string
add_column :users, :encrypted_otp_secret_iv, :string
add_column :users, :encrypted_otp_secret_salt, :string
add_column :users, :consumed_timestep, :integer
add_column :users, :otp_required_for_login, :boolean
end
end

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddTwoFactorBackupableToUser < ActiveRecord::Migration[5.1]
def change
add_column :users, :otp_backup_codes, :text
end
end

View file

@ -29,8 +29,7 @@ Feature: Change password
When I follow the "Change my password" link from the last sent email
When I fill out the password reset form with "supersecret" and "supersecret"
And I submit the password reset form
Then I should be on the stream page
And I sign out manually
Then I should be on the new user session page
And I sign in manually as "georges_abitbol" with password "supersecret"
Then I should be on the stream page

View file

@ -0,0 +1,90 @@
# frozen_string_literal: true
@javascript
Feature: Two-factor autentication
Scenario: Activate 2fa
Given a user with email "alice@test.com"
When I sign in as "alice@test.com"
When I go to the two-factor authentication page
And I press "Activate"
Then I should see "Confirm activation"
When I scan the QR code and fill in a valid TOTP token for "alice@test.com"
And I press "Confirm and activate"
Then I should see "Two-factor authentication activated"
And I should see "Recovery codes"
When I confirm activation
Then I should see "Two-factor authentication activated"
And I should see "Deactivate"
Scenario: Signing in with 2fa activated and correct token
Given a user with username "alice" and password "secret"
And 2fa is activated for "alice"
When I go to the login page
And I fill in username "alice" and password "secret"
And press "Sign in"
Then I should see "Two-factor authentication"
When I fill in a valid TOTP token for "alice"
And I press "Sign in"
Then I should be on the stream page
Scenario: Trying to sign in with 2fa activated and incorrect token
Given a user with username "alice" and password "secret"
And 2fa is activated for "alice"
When I go to the login page
And I fill in username "alice" and password "secret"
And press "Sign in"
Then I should see "Two-factor authentication"
When I fill in an invalid TOTP token
And I press "Sign in"
Then I should see "Two-factor authentication"
Scenario: Signing in with 2fa activated and a recovery code
Given a user with username "alice" and password "secret"
And 2fa is activated for "alice"
When I go to the login page
And I fill in username "alice" and password "secret"
And press "Sign in"
Then I should see "Two-factor authentication"
When I fill in a recovery code from "alice"
And I press "Sign in"
Then I should be on the stream page
Scenario: Regenerating recovery codes
Given a user with email "alice@test.com"
When I sign in as "alice@test.com"
And 2fa is activated for "alice@test.com"
When I go to the two-factor authentication page
Then I should see "Generate new recovery codes"
When I press the recovery code generate button
Then I should see a list of recovery codes
Scenario: Deactivating 2fa with correct token
Given a user with email "alice@test.com"
When I sign in as "alice@test.com"
And 2fa is activated for "alice@test.com"
When I go to the two-factor authentication page
Then I should see "Deactivate"
When I fill in a valid TOTP token to deactivate for "alice@test.com"
And I press "Deactivate"
Then I should see "Two-factor authentication not activated"
Scenario: Deactivating 2fa with recovery token
Given a user with email "alice@test.com"
When I sign in as "alice@test.com"
And 2fa is activated for "alice@test.com"
When I go to the two-factor authentication page
Then I should see "Deactivate"
When I fill in a recovery code to deactivate from "alice@test.com"
And I press "Deactivate"
Then I should see "Two-factor authentication not activated"
Scenario: Trying to deactivate with incorrect token
Given a user with email "alice@test.com"
When I sign in as "alice@test.com"
And 2fa is activated for "alice@test.com"
When I go to the two-factor authentication page
Then I should see "Deactivate"
When I fill in an invalid TOTP token to deactivate
And I press "Deactivate"
Then I should see "Two-factor authentication activated"
And I should see "Deactivate"

View file

@ -31,9 +31,8 @@ Feature: Change password
When I follow the "Change my password" link from the last sent email
And I fill out the password reset form with "supersecret" and "supersecret"
And I submit the password reset form
Then I should be on the stream page
When I sign out
And I go to the login page
Then I should be on the new user session page
When I go to the login page
And I sign in manually as "georges_abitbol" with password "supersecret" on the mobile website
Then I should be on the stream page

View file

@ -0,0 +1,67 @@
# frozen_string_literal: true
When /^I scan the QR code and fill in a valid TOTP token for "([^"]*)"$/ do |email|
@me = find_user email
fill_in "user_code", with: @me.current_otp
end
When /^I fill in a valid TOTP token for "([^"]*)"$/ do |username|
@me = find_user username
fill_in "user_otp_attempt", with: @me.current_otp
end
When /^I fill in an invalid TOTP token$/ do
fill_in "user_otp_attempt", with: "c0ffee"
end
When /^I fill in a valid TOTP token to deactivate for "([^"]*)"$/ do |username|
@me = find_user username
fill_in "two_factor_authentication_code", with: @me.current_otp
end
When /^I fill in an invalid TOTP token to deactivate$/ do
fill_in "two_factor_authentication_code", with: "c0ffee"
end
When /^I fill in a recovery code from "([^"]*)"$/ do |username|
@me = find_user username
@codes = @me.generate_otp_backup_codes!
@me.save!
fill_in "user_otp_attempt", with: @codes.first
end
When /^I fill in a recovery code to deactivate from "([^"]*)"$/ do |username|
@me = find_user username
@codes = @me.generate_otp_backup_codes!
@me.save!
fill_in "two_factor_authentication_code", with: @codes.first
end
When /^I confirm activation$/ do
find(".btn-primary", match: :first).click
end
When /^2fa is activated for "([^"]*)"$/ do |username|
@me = find_user username
@me.otp_secret = User.generate_otp_secret(32)
@me.otp_required_for_login = true
@me.save!
end
When /^I fill in username "([^"]*)" and password "([^"]*)"$/ do |username, password|
fill_in "user_username", with: username
fill_in "user_password", with: password
end
Then /^I should see a list of recovery codes$/ do
find(".recovery-codes", match: :first)
find(".recovery-codes li samp", match: :first)
end
When /^I press the recovery code generate button$/ do
find(".btn-default", match: :first).click
end
def find_user(username)
User.find_by(username: username) || User.find_by(email: username)
end

View file

@ -40,6 +40,8 @@ module NavigationHelpers
edit_user_path
when /^forgot password page$/
new_user_password_path
when /^the two-factor authentication page$/
two_factor_authentication_path
when %r{^"(/.*)"}
Regexp.last_match(1)
else

View file

@ -52,25 +52,43 @@ module Configuration
def secret_token
if heroku?
return ENV['SECRET_TOKEN'] if ENV['SECRET_TOKEN']
return ENV["SECRET_TOKEN"] if ENV["SECRET_TOKEN"]
warn "FATAL: Running on Heroku with SECRET_TOKEN unset"
warn " Run heroku config:add SECRET_TOKEN=#{SecureRandom.hex(40)}"
Process.exit(1)
abort
else
token_file = File.expand_path(
'../config/initializers/secret_token.rb',
"../config/initializers/secret_token.rb",
File.dirname(__FILE__)
)
unless File.exist? token_file
`DISABLE_SPRING=1 bin/rake generate:secret_token`
end
system "DISABLE_SPRING=1 bin/rake generate:secret_token" unless File.exist? token_file
require token_file
Diaspora::Application.config.secret_key_base
end
end
def twofa_encryption_key
if heroku?
return ENV["TWOFA_ENCRYPTION_KEY"] if ENV["TWOFA_ENCRYPTION_KEY"]
warn "FATAL: Running on Heroku with TWOFA_ENCRYPTION_KEY unset"
warn " Run heroku config:add TWOFA_ENCRYPTION_KEY=#{SecureRandom.hex(32)}"
abort
else
key_file = File.expand_path(
"../config/initializers/twofa_encryption_key.rb",
File.dirname(__FILE__)
)
system "DISABLE_SPRING=1 bin/rake generate:twofa_key" unless File.exist? key_file
require key_file
Diaspora::Application.config.twofa_encryption_key
end
end
def version_string
return @version_string unless @version_string.nil?
@version_string = version.number.to_s
@version_string = "#{@version_string}-p#{git_revision[0..7]}" if git_available?
@version_string

View file

@ -0,0 +1,24 @@
# frozen_string_literal: true
namespace :generate do
desc "Generates a key for encrypting 2fa tokens"
task :twofa_key do
path = Rails.root.join("config", "initializers", "twofa_encryption_key.rb")
key = SecureRandom.hex(32)
File.open(path, "w") do |f|
f.write <<~CONF
# frozen_string_literal: true
# The 2fa encryption key is used to encrypt users' OTP tokens in the database.
# You can regenerate this key by running `rake generate:twofa_key`
# If you change this key after a user has set up 2fa
# the users' tokens cannot be recovered
# and they will not be able to log in again!
Diaspora::Application.config.twofa_encryption_key = "#{key}"
CONF
end
end
end

View file

@ -0,0 +1,120 @@
# frozen_string_literal: true
describe TwoFactorAuthenticationsController, type: :controller do
before do
@user = FactoryGirl.create :user
sign_in @user
end
describe "#show" do
it "shows the deactivated state of 2fa" do
get :show
expect(response.body).to match I18n.t("two_factor_auth.title")
expect(response.body).to match I18n.t("two_factor_auth.deactivated.status")
expect(@user).to have_attributes(otp_required_for_login: nil)
end
it "shows the activated state of 2fa" do
activate_2fa
get :show
expect(response.body).to match I18n.t("two_factor_auth.title")
expect(response.body).to match I18n.t("two_factor_auth.activated.status")
expect(response.body).to match I18n.t("two_factor_auth.input_token.label")
expect(response.body).to match I18n.t("two_factor_auth.recovery.button")
expect(@user).to have_attributes(otp_required_for_login: true)
end
end
describe "#create" do
it "sets the otp_secret flag" do
post :create, params: {user: {otp_required_for_login: "true"}}
expect(response).to be_redirect
expect(response.location).to match confirm_two_factor_authentication_path
end
end
describe "#confirm_2fa" do
context "2fa is not yet activated" do
before do
create_otp_token
end
it "shows the QR verification code" do
get :confirm_2fa
expect(response.body).to match I18n.t("two_factor_auth.confirm.title")
expect(response.body).to include("svg")
expect(response.body).to match(/#{@user.otp_secret.scan(/.{4}/).join(" ")}/)
expect(response.body).to match I18n.t("two_factor_auth.input_token.label")
end
end
context "2fa is already activated" do
before do
activate_2fa
end
it "redirects back" do
get :confirm_2fa
expect(response).to be_redirect
expect(response.location).to match two_factor_authentication_path
end
end
end
describe "#confirm_and_activate_2fa" do
before do
create_otp_token
end
it "redirects back to confirm when token was wrong" do
post :confirm_and_activate_2fa, params: {user: {code: "not valid token"}}
expect(response.location).to match confirm_two_factor_authentication_path
expect(flash[:alert]).to match I18n.t("two_factor_auth.flash.error_token")
end
it "redirects to #recovery_codes when token was correct" do
post :confirm_and_activate_2fa, params: {user: {code: @user.current_otp}}
expect(response.location).to match recovery_codes_two_factor_authentication_path
expect(flash[:notice]).to match I18n.t("two_factor_auth.flash.success_activation")
end
end
describe "#recovery_codes" do
before do
activate_2fa
end
it "shows recovery codes page" do
get :recovery_codes
expect(response.body).to match I18n.t("two_factor_auth.recovery.title")
expect(@user).to have_attributes(otp_required_for_login: true)
end
end
describe "#destroy" do
before do
activate_2fa
end
it "deactivates 2fa if token is correct" do
delete :destroy, params: {two_factor_authentication: {code: @user.current_otp}}
expect(response).to be_redirect
expect(flash[:notice]).to match I18n.t("two_factor_auth.flash.success_deactivation")
end
it "does nothing if token is wrong" do
delete :destroy, params: {two_factor_authentication: {code: "a wrong code"}}
expect(response).to be_redirect
expect(flash[:alert]).to match I18n.t("two_factor_auth.flash.error_token")
end
end
def create_otp_token
@user.otp_secret = User.generate_otp_secret(32)
@user.save!
end
def confirm_activation
@user.otp_required_for_login = true
@user.save!
end
def activate_2fa
create_otp_token
confirm_activation
end
end

View file

@ -983,6 +983,13 @@ describe User, :type => :model do
post_default_public
exported_at
exported_photos_at
consumed_timestep
encrypted_otp_secret
encrypted_otp_secret_iv
encrypted_otp_secret_salt
otp_backup_codes
otp_required_for_login
otp_secret
)
)
end