Skip to content

Commit

Permalink
Optional two-factor authentication using Google Authenticator and QR …
Browse files Browse the repository at this point in the history
…code automatic setup
  • Loading branch information
David FRANCOIS committed Jun 25, 2011
1 parent 36fe317 commit ad02f26
Show file tree
Hide file tree
Showing 32 changed files with 502 additions and 199 deletions.
4 changes: 4 additions & 0 deletions Gemfile
Expand Up @@ -33,6 +33,10 @@ gem 'transitions',

gem 'will_paginate'

gem 'rotp', '~> 1.3.0'

gem 'rqrcode'

group :test do
gem 'mocha', :require => false
gem 'factory_girl_rails'
Expand Down
4 changes: 4 additions & 0 deletions Gemfile.lock
Expand Up @@ -105,6 +105,8 @@ GEM
recaptcha (0.3.1)
render_component_vho (3.0.3)
railties (~> 3.0.0)
rotp (1.3.0)
rqrcode (0.3.4)
thor (0.14.6)
transitions (0.0.9)
treetop (1.4.9)
Expand Down Expand Up @@ -138,6 +140,8 @@ DEPENDENCIES
rails (= 3.0.0)
rake (~> 0.8.7)
recaptcha
rotp (~> 1.3.0)
rqrcode
transitions
whenever
will_paginate
7 changes: 7 additions & 0 deletions app/controllers/users_controller.rb
Expand Up @@ -37,4 +37,11 @@ def update
render :action => :edit
end
end

def reset_otp_secret
current_user.generate_otp_secret && current_user.save!

redirect_to otp_configuration_user_path,
:notice => t("users.otp_configuration.reset")
end
end
16 changes: 9 additions & 7 deletions app/models/user.rb
Expand Up @@ -2,15 +2,17 @@ class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :token_authenticatable, :confirmable, :lockable and :timeoutable
devise :database_authenticatable,
:otp_checkable,
:registerable,
:confirmable,
:recoverable,
:rememberable,
:trackable,
:validatable
:validatable,
:lockable

# Setup accessible (or protected) attributes for your model
attr_accessible :password, :password_confirmation, :remember_me, :time_zone, :merchant
attr_accessible :password, :password_confirmation, :remember_me, :time_zone,
:merchant, :require_otp

attr_accessor :captcha,
:skip_captcha,
Expand Down Expand Up @@ -74,10 +76,6 @@ def balance(currency, options = {} )
transfers.with_currency(currency).with_confirmations(options[:unconfirmed]).map(&:amount).sum
end

def generate_account_id
self.account = "BC-U#{"%06d" % (rand * 10 ** 6).to_i}"
end

def confirm!
super
UserMailer.registration_confirmation(self).deliver
Expand All @@ -94,4 +92,8 @@ def self.find_for_database_authentication(warden_conditions)
account = conditions.delete(:account)
where(conditions).where(["account = :value OR email = :value", { :value => account }]).first
end

def generate_account_id
self.account = "BC-U#{"%06d" % (rand * 10 ** 6).to_i}"
end
end
29 changes: 0 additions & 29 deletions app/views/devise/sessions/new.html.erb

This file was deleted.

25 changes: 25 additions & 0 deletions app/views/devise/sessions/new.html.haml
@@ -0,0 +1,25 @@
%h1= t(:sign_in)

= form_for(resource, :as => resource_name, :url => session_path(resource_name)) do |f|
.form-field
= f.label :account
= f.text_field :account
.explanation
= t(:account_description)

.form-field
= f.label :password
= f.password_field :password
.explanation
= link_to t(".password_explanation"), new_password_path(resource_name)

.form-field
= f.label :otp
= f.text_field :otp, :class => "otp"
.explanation
= t(".otp_explanation")

.form-field
= f.submit t(:sign_in), :class => "submit"

= render :partial => "devise/shared/links"
@@ -1,8 +1,4 @@
<ul id="devise-actions">
<%- if devise_mapping.recoverable? && controller_name != 'passwords' %>
<li><%= link_to t(:password_forgotten), new_password_path(resource_name) %></li>
<% end -%>
<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
<li><%= link_to t(:didnt_receive_confirmation), new_confirmation_path(resource_name) %></li>
<% end -%>
Expand Down
12 changes: 0 additions & 12 deletions app/views/devise/unlocks/new.html.erb

This file was deleted.

13 changes: 13 additions & 0 deletions app/views/devise/unlocks/new.html.haml
@@ -0,0 +1,13 @@
%h2= t(".resend")

= form_for(resource, :as => resource_name, :url => unlock_path(resource_name), :html => { :method => :post }) do |f|
= devise_error_messages!

.form-field
= f.label :email
= f.text_field :email

.form-field
= f.submit t(:submit), :class => "submit"

= render :partial => "devise/shared/links"
2 changes: 1 addition & 1 deletion app/views/invoices/show.html.haml
Expand Up @@ -2,7 +2,7 @@

%h1= t(".details")

%table.default.invoice-details
%table.default.details
%tr
%th= t(".payee")
%td.fixed= @invoice.user.account
Expand Down
8 changes: 8 additions & 0 deletions app/views/users/_form.html.haml
Expand Up @@ -22,11 +22,19 @@
.explanation
= t ".merchant_explanation"

- unless @user.new_record?
.form-field
= f.label :require_otp
= f.check_box :require_otp, :class => "checkbox"
.explanation
= link_to t(".otp_config"), otp_configuration_user_path

- if @user.new_record?
.recaptcha-field
%label{ :for => "recaptcha_response_field" } t(".turing_test")
= f.text_field :account, :disabled => true
.recaptcha-input= recaptcha_tags :ssl => true


.form-field
= f.submit t(".submit"), :class => 'submit'
5 changes: 5 additions & 0 deletions app/views/users/_qrcode.html.haml
@@ -0,0 +1,5 @@
%table.qrcode
- qrcode.modules.each_index do |x|
%tr
- qrcode.modules.each_index do |y|
%td{ :class => (qrcode.dark?(x,y) && "black") }
55 changes: 55 additions & 0 deletions app/views/users/otp_configuration.en.html.haml
@@ -0,0 +1,55 @@
%h1 Required steps for TOTP authentication
%h2 Why use one-time-password authentication ?
%p
TOTP authentication adds a security layer on top of the login/password authentication
mechanism. For your account to be compromised, an attacker must not only know your
password, but also know your shared-secret or compromise your mobile device.e.
%p
If you activate the "Require TOTP" option in your account settings you won't be
able to sign in without providing a TOTP token. You can use the same token multiple
times, but the token is time based and is only valid for 30 seconds. You need a
Blackebbery, Android or iPhone for the token generation.

%h2 Reset your shared-secret
%p
If you think your mobile device has been compromised or if you think someone
got a hold of your shared-secret you <strong>must</strong> reset it.
%p
You will need to update your mobile device configuration.
%br
%p
= form_tag reset_otp_secret_user_path, :method => :post do
= submit_tag "Reset shared-secret", :class => "submit"

%h2 Configuration
%p
%ul
%li Install the #{link_to "Google Authenticator", "http://www.google.com/support/accounts/bin/answer.py?hl=en&answer=1066447", :target => "_blank"} app on your mobile device
%li
Configure the shared secret, either:
%ul
%li manuelly, or
%li automatically by scanning the QR code below.

%h3 Automatic configuration

%p Scan the QR code to automatically configure your mobile device.

= render :partial => 'users/qrcode', :locals => { :qrcode => RQRCode::QRCode.new(current_user.provisioning_uri, :size => 6) }

%h3 Manual configuration

%p Use these parameters to configure your device
%br
%table.default.details
%tr
%th Account
%td= current_user.account
%tr
%th Token type
%td Time-based
%tr
%th Shared secret
%td.fixed= current_user.otp_secret


54 changes: 54 additions & 0 deletions app/views/users/otp_configuration.fr.html.haml
@@ -0,0 +1,54 @@
%h1 Configuration de l'authentification avec jeton TOTP
%h2 Objectif
%p
L'authentification avec jeton TOTP a pour but de rajouter une couche de sécurité
supplémentaire à l'authentification utilisant un idenfiant et un mot de passe.
%p
Si vous activez l'option "Demander jeton TOTP" au niveau des paramètres de votre
compte vous devrez générer un code de connexion différent à chaque connexion. Vous
aurez besoin d'un téléphone Blackberry, Android ou iPhone pour la génération du
code de connexion.

%h2 Ré-initialiser votre secret partagé
%p
Si vous pensez que votre téléphone a été compromis, ou si vous pensez que quelqu'un
a pris connaissance de votre secret partagé vous <strong>devez</strong> le ré-initialiser.
%p
Vous devrez mettre à jour la configuration de votre téléphone.
%br
%p
= form_tag reset_otp_secret_user_path, :method => :post do
= submit_tag "Ré-initialiser", :class => "submit"

%h2 Configuration
%p
%ul
%li Installez l'application #{link_to "Google Authenticator", "http://www.google.com/support/accounts/bin/answer.py?hl=en&answer=1066447", :target => "_blank"}
%li
Configurez le secret partagé qui permettra de générer des codes d'authentification, soit :
%ul
%li manuellement, soit
%li automatiquement en scannant le code QR ci-dessous.

%h3 Configuration automatique

%p Scannez le code QR afin de configurer automatiquement votre générateur de codes d'authentification.

= render :partial => 'users/qrcode', :locals => { :qrcode => RQRCode::QRCode.new(current_user.provisioning_uri, :size => 6) }

%h3 Configuration manuelle

%p Utilisez les paramètres suivants pour configurer votre générateur de codes d'authentification
%br
%table.default.details
%tr
%th Compte
%td= current_user.account
%tr
%th Type de code
%td Temporel
%tr
%th Secret partagé
%td.fixed= current_user.otp_secret


2 changes: 0 additions & 2 deletions config/application.rb
Expand Up @@ -8,8 +8,6 @@ module BitcoinBank
class Application < Rails::Application
I18n.const_set :Locales, {
:en => "English",
# :de => "Deutsch",
# :it => "Italiano",
:fr => "Français"
}

Expand Down
6 changes: 6 additions & 0 deletions config/initializers/devise.rb
Expand Up @@ -139,4 +139,10 @@
# end
# manager.default_strategies(:scope => :user).unshift :twitter_oauth
# end

require 'devise/strategies/otp_checkable'

config.warden do |manager|
manager.default_strategies(:scope => :user).unshift :otp_checkable
end
end
1 change: 1 addition & 0 deletions config/locales/en/devise.yml
Expand Up @@ -14,6 +14,7 @@ en:
invalid_token: 'Invalid authentication token'
timeout: 'Your session expired, please sign in again to continue'
inactive: 'Your account was not activated yet'
invalid_otp: Invalid or expired OTP
sessions:
signed_in: 'Signed in successfully'
signed_out: 'Signed out successfully'
Expand Down
2 changes: 2 additions & 0 deletions config/locales/en/models.yml
Expand Up @@ -73,6 +73,8 @@ en:
current_password: Current password
new_password: New password
merchant: Merchant API
require_otp: Require TOTP
otp: One-time password
trade:
type: Type
amount: Amount
Expand Down
15 changes: 14 additions & 1 deletion config/locales/en/views.yml
Expand Up @@ -40,4 +40,17 @@ en:
form:
submit: Save user
merchant_explanation: Activate merchant tools

otp_explanation: Require one-time-password to authenticate
otp_config: Help and setup instructions
otp_configuration:
reset: The shared-secret has been correctly reset, you'll need to update your mobile device settings

devise:
sessions:
new:
otp_explanation: Required only if you enabled OTP authentication
password_explanation: Password forgotten ?

unlocks:
new:
resend: Send unlock instructions
1 change: 1 addition & 0 deletions config/locales/fr/devise.yml
Expand Up @@ -14,6 +14,7 @@ fr:
invalid_token: 'Invalid authentication token'
timeout: 'Your session expired, please sign in again to continue'
inactive: 'Your account was not activated yet'
invalid_otp: Code d'authentification invalide ou expiré
sessions:
signed_in: 'Signed in successfully'
signed_out: 'Signed out successfully'
Expand Down
2 changes: 2 additions & 0 deletions config/locales/fr/models.yml
Expand Up @@ -74,6 +74,8 @@ fr:
current_password: Mot de passe actuel
new_password: Nouveau mot de passe
merchant: API E-commerce
require_otp: Demander jeton TOTP
otp: Code d'authentification
trade:
type: Type
amount: Montant
Expand Down

0 comments on commit ad02f26

Please sign in to comment.