devise-two-factor-3.1.0/0000755000004100000410000000000013526600171015106 5ustar www-datawww-datadevise-two-factor-3.1.0/.travis.yml0000644000004100000410000000211013526600171017211 0ustar www-datawww-datasudo: false language: ruby cache: bundler before_install: - gem i rubygems-update -v '<3' && update_rubygems - gem update bundler gemfile: - Gemfile - gemfiles/rails_4_1.gemfile - gemfiles/rails_4_2.gemfile - gemfiles/rails_5_0.gemfile - gemfiles/rails_5_1.gemfile - gemfiles/rails_5_2.gemfile - gemfiles/rails_6_0.gemfile rvm: - "2.1" - "2.2" - "2.3.4" - "2.4.0" - "2.4.1" - "2.5" - "2.6" matrix: exclude: - rvm: "2.1" gemfile: gemfiles/rails_5_0.gemfile - rvm: "2.2" gemfile: gemfiles/rails_5_0.gemfile - rvm: "2.1" gemfile: gemfiles/rails_5_1.gemfile - rvm: "2.2" gemfile: gemfiles/rails_5_1.gemfile - rvm: "2.1" gemfile: gemfiles/rails_5_2.gemfile - rvm: "2.2" gemfile: gemfiles/rails_5_2.gemfile - rvm: "2.1" gemfile: gemfiles/rails_6_0.gemfile - rvm: "2.2" gemfile: gemfiles/rails_6_0.gemfile - rvm: "2.3.4" gemfile: gemfiles/rails_6_0.gemfile - rvm: "2.4.0" gemfile: gemfiles/rails_6_0.gemfile - rvm: "2.4.1" gemfile: gemfiles/rails_6_0.gemfile devise-two-factor-3.1.0/.rspec0000644000004100000410000000001013526600171016212 0ustar www-datawww-data--color devise-two-factor-3.1.0/README.md0000644000004100000410000002771713526600171016403 0ustar www-datawww-data# Devise-Two-Factor Authentication By [Tinfoil Security](https://www.tinfoilsecurity.com/). Interested in [working with us](https://www.tinfoilsecurity.com/jobs)? We're hiring! [![Build Status](https://travis-ci.org/tinfoil/devise-two-factor.svg?branch=master)](https://travis-ci.org/tinfoil/devise-two-factor) Devise-Two-Factor is a minimalist extension to Devise which offers support for two-factor authentication, through the [TOTP](https://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm) scheme. It: * Allows you to incorporate two-factor authentication into your existing models * Is opinionated about security, so you don't have to be * Integrates easily with two-factor applications like [Google Authenticator](https://support.google.com/accounts/answer/1066447?hl=en) and [Authy](https://authy.com/) * Is extensible, and includes two-factor backup codes as an example of how plugins can be structured ## Contributing We welcome pull requests, bug reports, and other contributions. We're especially looking for help getting this gem fully compatible with Rails 5+ and squashing any deprecation messages. ## Example App An example Rails 4 application is provided in the `demo` directory. It showcases a minimal example of Devise-Two-Factor in action, and can act as a reference for integrating the gem into your own application. For the demo app to work, create an encryption key and store it as an environment variable. One way to do this is to create a file named `local_env.yml` in the application root. Set the value of `ENCRYPTION_KEY` in the YML file. That value will be loaded into the application environment by `application.rb`. ## Getting Started Devise-Two-Factor doesn't require much to get started, but there are a few prerequisites before you can start using it in your application. First, you'll need a Rails application setup with Devise. Visit the Devise [homepage](https://github.com/plataformatec/devise) for instructions. You can add Devise-Two-Factor to your Gemfile with: ```ruby gem 'devise-two-factor' ``` Next, since Devise-Two-Factor encrypts its secrets before storing them in the database, you'll need to generate an encryption key, and store it in an environment variable of your choice. Set the encryption key in the model that uses Devise: ```ruby devise :two_factor_authenticatable, :otp_secret_encryption_key => ENV['YOUR_ENCRYPTION_KEY_HERE'] ``` Finally, you can automate all of the required setup by simply running: ```ruby rails generate devise_two_factor MODEL ENVIRONMENT_VARIABLE ``` Where `MODEL` is the name of the model you wish to add two-factor functionality to (for example `user`), and `ENVIRONMENT_VARIABLE` is the name of the variable you're storing your encryption key in. This generator will add a few columns to the specified model: * encrypted_otp_secret * encrypted_otp_secret_iv * encrypted_otp_secret_salt * consumed_timestep * otp_required_for_login It also adds the :two_factor_authenticatable directive to your model, and sets up your encryption key. If present, it will remove :database_authenticatable from the model, as the two strategies are incompatible. Lastly, the generator will add a Warden config block to your Devise initializer, which enables the strategies required for two-factor authentication. If you're running Rails 3, or do not have strong parameters enabled, the generator will also setup the required mass-assignment security options in your model. If you're running Rails 4, you'll also need to whitelist `:otp_attempt` as a permitted parameter in Devise `:sign_in` controller. You can do this by adding the following to your `application_controller.rb`: ```ruby before_action :configure_permitted_parameters, if: :devise_controller? ... protected def configure_permitted_parameters devise_parameter_sanitizer.for(:sign_in) << :otp_attempt end ``` If you're running Devise 4.0.0 or above, you'll want to use `.permit` instead: ```ruby before_action :configure_permitted_parameters, if: :devise_controller? ... protected def configure_permitted_parameters devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt]) end ``` **After running the generator, verify that :database_authenticatable is not being loaded by your model. The generator will try to remove it, but if you have a non-standard Devise setup, this step may fail. Loading both :database_authenticatable and `:two_factor_authenticatable` in a model will allow users to bypass two-factor authenticatable due to the way Warden handles cascading strategies.** ## Designing Your Workflow Devise-Two-Factor only worries about the backend, leaving the details of the integration up to you. This means that you're responsible for building the UI that drives the gem. While there is an example Rails application included in the gem, it is important to remember that this gem is intentionally very open-ended, and you should build a user experience which fits your individual application. There are two key workflows you'll have to think about: 1. Logging in with two-factor authentication 2. Enabling two-factor authentication for a given user We chose to keep things as simple as possible, and our implementation can be found by registering at [Tinfoil Security](https://tinfoilsecurity.com/), and enabling two-factor authentication from the [security settings page](https://www.tinfoilsecurity.com/account/security). ### Logging In Logging in with two-factor authentication works extremely similarly to regular database authentication in Devise. The `TwoFactorAuthenticatable` strategy accepts three parameters: 1. email 2. password 3. otp_attempt (Their one-time password for this session) These parameters can be submitted to the standard Devise login route, and the strategy will handle the authentication of the user for you. ### Disabling Automatic Login After Password Resets If you use the Devise ```recoverable``` strategy, the default behavior after a password reset is to automatically authenticate the user and log them in. This is obviously a problem if a user has two-factor authentication enabled, as resetting the password would get around the two-factor requirement. Because of this, you need to set `sign_in_after_reset_password` to `false` (either globally in your Devise initializer or via `devise_for`). ### Enabling Two-Factor Authentication Enabling two-factor authentication for a user is easy. For example, if my user model were named User, I could do the following: ```ruby current_user.otp_required_for_login = true current_user.otp_secret = User.generate_otp_secret current_user.save! ``` Before you can do this however, you need to decide how you're going to transmit two-factor tokens to a user. Common strategies include sending an SMS, or using a mobile application such as Google Authenticator. At Tinfoil Security, we opted to use the excellent [rqrcode-rails3](https://github.com/samvincent/rqrcode-rails3) gem to generate a QR-code representing the user's secret key, which can then be scanned by any mobile two-factor authentication client. If you decide to do this you'll need to generate a URI to act as the source for the QR code. This can be done using the `User#otp_provisioning_uri` method. ```ruby issuer = 'Your App' label = "#{issuer}:#{current_user.email}" current_user.otp_provisioning_uri(label, issuer: issuer) # > "otpauth://totp/Your%20App:user@example.com?secret=[otp_secret]&issuer=Your+App" ``` If you instead to decide to send the one-time password to the user directly, such as via SMS, you'll need a mechanism for generating the one-time password on the server: ```ruby current_user.current_otp ``` The generated code will be valid for the duration specified by `otp_allowed_drift`. However you decide to handle enrollment, there are a few important considerations to be made: * Whether you'll force the use of two-factor authentication, and if so, how you'll migrate existing users to system, and what your on-boarding experience will look like * If you authenticate using SMS, you'll want to verify the user's ownership of the phone, in much the same way you're probably verifying their email address * How you'll handle device revocation in the event that a user loses access to their device, or that device is rendered temporarily unavailable (This gem includes `TwoFactorBackupable` as an example extension meant to solve this problem) It sounds like a lot of work, but most of these problems have been very elegantly solved by other people. We recommend taking a look at the excellent workflows used by Heroku and Google for inspiration. ### Filtering sensitive parameters from the logs To prevent two-factor authentication codes from leaking if your application logs get breached, you'll want to filter sensitive parameters from the Rails logs. Add the following to `config/initializers/filter_parameter_logging.rb`: ```ruby Rails.application.config.filter_parameters += [:otp_attempt] ``` ## Backup Codes Devise-Two-Factor is designed with extensibility in mind. One such extension, `TwoFactorBackupable`, is included and serves as a good example of how to extend this gem. This plugin allows you to add the ability to generate single-use backup codes for a user, which they may use to bypass two-factor authentication, in the event that they lose access to their device. To install it, you need to add the `:two_factor_backupable` directive to your model. ```ruby devise :two_factor_backupable ``` You'll also be required to enable the `:two_factor_backupable` strategy, by adding the following line to your Warden config in your Devise initializer, substituting :user for the name of your Devise scope. ```ruby manager.default_strategies(:scope => :user).unshift :two_factor_backupable ``` The final installation step is dependent on your version of Rails. If you're not running Rails 4, skip to the next section. Otherwise, create the following migration: ```ruby class AddDeviseTwoFactorBackupableToUsers < ActiveRecord::Migration def change # Change type from :string to :text if using MySQL database add_column :users, :otp_backup_codes, :string, array: true end end ``` You can then generate backup codes for a user: ```ruby codes = current_user.generate_otp_backup_codes! current_user.save! # Display codes to the user somehow! ``` The backup codes are stored in the database as bcrypt hashes, so be sure to display them to the user at this point. If all went well, the user should be able to login using each of the generated codes in place of their two-factor token. Each code is single-use, and generating a new set of backup codes for that user will invalidate all of the old ones. You can customize the length of each code, and the number of codes generated by passing the options into `:two_factor_backupable` in the Devise directive: ```ruby devise :two_factor_backupable, otp_backup_code_length: 32, otp_number_of_backup_codes: 10 ``` ### Help! I'm not using Rails 4.0! Don't worry! `TwoFactorBackupable` stores the backup codes as an array of strings in the database. In Rails 4.0 this is supported natively, but in earlier versions you can use a gem to emulate this behavior: we recommend [activerecord-postgres-array](https://github.com/tlconnor/activerecord-postgres-array). You'll then simply have to create a migration to add an array named `otp_backup_codes` to your model. If you use the above gem, this migration might look like: ```ruby class AddTwoFactorBackupCodesToUsers < ActiveRecord::Migration def change # Change type from :string_array to :text_array if using MySQL database add_column :users, :otp_backup_codes, :string_array end end ``` Now just continue with the setup in the previous section, skipping the generator step. ## Testing Devise-Two-Factor includes shared-examples for both `TwoFactorAuthenticatable` and `TwoFactorBackupable`. Adding the following two lines to the specs for your two-factor enabled models will allow you to test your models for two-factor functionality: ```ruby require 'devise_two_factor/spec_helpers' it_behaves_like "two_factor_authenticatable" it_behaves_like "two_factor_backupable" ``` devise-two-factor-3.1.0/gemfiles/0000755000004100000410000000000013526600171016701 5ustar www-datawww-datadevise-two-factor-3.1.0/gemfiles/rails_4_2.gemfile0000644000004100000410000000022313526600171022006 0ustar www-datawww-data# This file was generated by Appraisal source "https://rubygems.org" gem "railties", "~> 4.2" gem "activesupport", "~> 4.2" gemspec path: "../" devise-two-factor-3.1.0/gemfiles/rails_5_2.gemfile0000644000004100000410000000022313526600171022007 0ustar www-datawww-data# This file was generated by Appraisal source "https://rubygems.org" gem "railties", "~> 5.2" gem "activesupport", "~> 5.2" gemspec path: "../" devise-two-factor-3.1.0/gemfiles/rails_5_0.gemfile0000644000004100000410000000022313526600171022005 0ustar www-datawww-data# This file was generated by Appraisal source "https://rubygems.org" gem "railties", "~> 5.0" gem "activesupport", "~> 5.0" gemspec path: "../" devise-two-factor-3.1.0/gemfiles/rails_5_1.gemfile0000644000004100000410000000022313526600171022006 0ustar www-datawww-data# This file was generated by Appraisal source "https://rubygems.org" gem "railties", "~> 5.1" gem "activesupport", "~> 5.1" gemspec path: "../" devise-two-factor-3.1.0/gemfiles/rails_6_0.gemfile0000644000004100000410000000023713526600171022013 0ustar www-datawww-data# This file was generated by Appraisal source "https://rubygems.org" gem "railties", "~> 6.0.0.rc2" gem "activesupport", "~> 6.0.0.rc2" gemspec path: "../" devise-two-factor-3.1.0/gemfiles/rails_4_1.gemfile0000644000004100000410000000022313526600171022005 0ustar www-datawww-data# This file was generated by Appraisal source "https://rubygems.org" gem "railties", "~> 4.1" gem "activesupport", "~> 4.1" gemspec path: "../" devise-two-factor-3.1.0/data.tar.gz.sig0000444000004100000410000000100013526600171017714 0ustar www-datawww-datavIP# ޻h ~,:=6#I-D7OcdcݮLC;WPk4$&ZvsXc"O \3Ha 8\==j*^ )xPI* *!כ *49MKP쵾+;(*)`'QFV>Ns:xw>C'{;$եԠצ<Zt#^yF ueTOLHs {y]a948(nH<_LP/%'a}669=˯ZF "{kP4% 4qT->۾/J 7TQzt1 ^;5f{AƆ(Ϗ`z\`eX؛zu U&{P> CzbK >qU&7j>: Ea$2ϭ41t 'test-key'*4 attr_accessor :consumed_timestep def save(validate) # noop for testing true end end class TwoFactorAuthenticatableWithCustomizeAttrEncryptedDouble extend ::ActiveModel::Callbacks include ::ActiveModel::Validations::Callbacks # like https://github.com/tinfoil/devise-two-factor/blob/cf73e52043fbe45b74d68d02bc859522ad22fe73/UPGRADING.md#guide-to-upgrading-from-2x-to-3x extend ::AttrEncrypted attr_encrypted :otp_secret, :key => 'test-key'*8, :mode => :per_attribute_iv_and_salt, :algorithm => 'aes-256-cbc' extend ::Devise::Models define_model_callbacks :update devise :two_factor_authenticatable, :otp_secret_encryption_key => 'test-key'*4 attr_accessor :consumed_timestep def save(validate) # noop for testing true end end describe ::Devise::Models::TwoFactorAuthenticatable do context 'When included in a class' do subject { TwoFactorAuthenticatableDouble.new } it_behaves_like 'two_factor_authenticatable' end end describe ::Devise::Models::TwoFactorAuthenticatable do context 'When included in a class' do subject { TwoFactorAuthenticatableWithCustomizeAttrEncryptedDouble.new } it_behaves_like 'two_factor_authenticatable' before :each do subject.otp_secret = subject.class.generate_otp_secret subject.consumed_timestep = nil end describe 'otp_secret options' do it 'should be of the key' do expect(subject.encrypted_attributes[:otp_secret][:key]).to eq('test-key'*8) end it 'should be of the mode' do expect(subject.encrypted_attributes[:otp_secret][:mode]).to eq(:per_attribute_iv_and_salt) end it 'should be of the mode' do expect(subject.encrypted_attributes[:otp_secret][:algorithm]).to eq('aes-256-cbc') end end end end devise-two-factor-3.1.0/spec/devise/models/two_factor_backupable_spec.rb0000644000004100000410000000106713526600171026465 0ustar www-datawww-datarequire 'spec_helper' require 'active_model' class TwoFactorBackupableDouble extend ::ActiveModel::Callbacks include ::ActiveModel::Validations::Callbacks extend ::Devise::Models define_model_callbacks :update devise :two_factor_authenticatable, :two_factor_backupable, :otp_secret_encryption_key => 'test-key'*4 attr_accessor :otp_backup_codes end describe ::Devise::Models::TwoFactorBackupable do context 'When included in a class' do subject { TwoFactorBackupableDouble.new } it_behaves_like 'two_factor_backupable' end end devise-two-factor-3.1.0/spec/spec_helper.rb0000644000004100000410000000130513526600171020655 0ustar www-datawww-datarequire 'simplecov' module SimpleCov::Configuration def clean_filters @filters = [] end end SimpleCov.configure do clean_filters load_profile 'test_frameworks' end ENV["COVERAGE"] && SimpleCov.start do add_filter "/.rvm/" end $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) $LOAD_PATH.unshift(File.dirname(__FILE__)) require 'rspec' require 'faker' require 'timecop' require 'devise-two-factor' require 'devise_two_factor/spec_helpers' # Requires supporting files with custom matchers and macros, etc, # in ./support/ and its subdirectories. Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} RSpec.configure do |config| config.order = 'random' end devise-two-factor-3.1.0/CHANGELOG.md0000644000004100000410000000306613526600171016724 0ustar www-datawww-data# CHANGELOG ## Unreleased ## 3.0.2 - Add Rails 5.1 support ## 3.0.1 - Qualify call to rspec shared_examples ## 3.0.0 See `UPGRADING.md` for specific help with breaking changes from 2.x to 3.0.0. - Adds support for Devise 4. - Relax dependencies to allow attr_encrypted 3.x. - Blocks the use of attr_encrypted 2.x. There was a significant vulnerability in the encryption implementation in attr_encrypted 2.x, and that version of the gem should not be used. ## 2.2.0 - Use 192 bits, not 1024, as a secret key length. RFC 4226 recommends a minimum length of 128 bits and a recommended length of 160 bits. Google Authenticator doesn't accept 160 bit keys. ## 2.1.0 - Return false if OTP value is nil, instead of an ROTP exception. ## 2.0.1 No user-facing changes. ## 2.0.0 See `UPGRADING.md` for specific help with breaking changes from 1.x to 2.0.0. - Replace `valid_otp?` method with `validate_and_consume_otp!`. - Disallow subsequent OTPs once validated via timesteps. ## 1.1.0 - Removes runtimez activemodel dependency. - Uses `Devise::Encryptor` instead of `Devise.bcrypt`, which is deprecated. - Bump `rotp` dependency to 2.x. ## 1.0.2 - Makes Railties the only requirement for Rails generators. - Explicitly check that the `otp_attempt` param is not nil in order to avoid 'ROTP only verifies strings' exceptions. - Adding warning about recoverable devise strategy and automatic `sign_in` after a password reset. - Loosen dependency version requirements for rotp, devise, and attr_encrypted. ## 1.0.1 - Add version requirements for dependencies. ## 1.0.0 - Initial release. devise-two-factor-3.1.0/Appraisals0000644000004100000410000000101713526600171017127 0ustar www-datawww-dataappraise "rails-4-1" do gem 'railties', '~> 4.1' gem 'activesupport', '~> 4.1' end appraise "rails-4-2" do gem 'railties', '~> 4.2' gem 'activesupport', '~> 4.2' end appraise "rails-5-0" do gem 'railties', '~> 5.0' gem 'activesupport', '~> 5.0' end appraise "rails-5-1" do gem 'railties', '~> 5.1' gem 'activesupport', '~> 5.1' end appraise "rails-5-2" do gem 'railties', '~> 5.2' gem 'activesupport', '~> 5.2' end appraise "rails-6-0" do gem 'railties', '~> 6.0' gem 'activesupport', '~> 6.0' end devise-two-factor-3.1.0/certs/0000755000004100000410000000000013526600171016226 5ustar www-datawww-datadevise-two-factor-3.1.0/certs/tinfoilsecurity-gems-cert.pem0000644000004100000410000000413713526600171024056 0ustar www-datawww-data-----BEGIN CERTIFICATE----- MIIGADCCA+igAwIBAgIIBr+qGSyQe7MwDQYJKoZIhvcNAQENBQAwgZwxCzAJBgNV BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJUGFsbyBBbHRvMR8wHQYDVQQK ExZUaW5mb2lsIFNlY3VyaXR5LCBJbmMuMR8wHQYDVQQDExZUaW5mb2lsIFNlY3Vy aXR5LCBJbmMuMSowKAYJKoZIhvcNAQkBFhtzdXBwb3J0QHRpbmZvaWxzZWN1cml0 eS5jb20wHhcNMTkwODA3MTkwMjAwWhcNMjAwODA3MTkwMjAwWjCBiDELMAkGA1UE BhMCVVMxCzAJBgNVBAgTAkNBMR8wHQYDVQQKExZUaW5mb2lsIFNlY3VyaXR5LCBJ bmMuMR0wGwYDVQQDExR0aW5mb2lsc2VjdXJpdHktZ2VtczEsMCoGCSqGSIb3DQEJ ARYdZW5naW5lZXJzQHRpbmZvaWxzZWN1cml0eS5jb20wggIiMA0GCSqGSIb3DQEB AQUAA4ICDwAwggIKAoICAQDNJYNH8D+8lACLt3KzjEIPs3XVBCPaMm2eD/Xk9OOT uDV/NqgMK0icD9MRxMUtS3SCrC9QcPocXT76f2LQ3yVJuK+rBUasymEES47PIx2c zC4n4Hga0xPPuBpioO26oaRFsobyzh9RPOIbnYfpjyqtdrbm+YyM3sPR4XzFirv9 xomT4E9T4RCLgOQHTcLKL9K9m+EN7PeVdVUXV0Pa7cVs2vJUKedsd7vnr6Lzbn8T oPk/7J/4W931PbaeI5yg9ZuaRa9K2IaY1TkPI67NW4qKitBVepRlXw6Sb7TYcUnc WEQ/eC5CpnOmqUrG5tfGD8cc5aGZOkitW/VXZgVj81xgCv1hk4HjErrqq4FBNAaC SNyBfwR0TUYqg1lN1nbNjOKwfb6YRn06R2ovcFJG0tmGhsQULCr6fW8u2TfSM+U9 WFSIJx2griureY7EZPwg/MgsUiWUWMFemz3GVYXWJR3dN2pW9Uqr3rkjKZbA0bst GWahJO9HuFdDakQxoaTPYPtTQDC+kskkO6lKG1KLIoZ1iLZzB1Ks1vEeyE7lp1im WgpUq+q23PFkt1gIBi/4tGvzsLZye25QU2Y+XLzldCNm+DyRFXZ+Q+bK33IveUeU WEOv4T1qTXHAOypyzmgodVRG/PrlsSMOBfE515kG1mDMGjRcCpEtlskgxUbf7qM7 hQIDAQABo1gwVjAJBgNVHRMEAjAAMEkGA1UdHwRCMEAwPqA8oDqGOGh0dHBzOi8v d3d3LnRpbmZvaWxzZWN1cml0eS5jb20vc2VjdXJpdHkvcmV2b2NhdGlvbl9saXN0 MA0GCSqGSIb3DQEBDQUAA4ICAQAiW9gcR0TTaxsK1DaHZ9U9VxP0dhtWKyTcjDnE BHlaayjrEmmQHmEtDEFOkLYOo20LpMFaogxcSHBYto5Z3XYfU9ZPuaY6rGVFhXeq mrQn6O0iOxzNOHLSZTgEjQTc2gJg1p1CYSxapla53ivW5v01dNcS1XJE/HFHQttI gXdNb53g7XW/c1Be5MvtQKZHsYGNANE+NUUOIFMp29PI1iXBVZVm2PSYL4XlGgQi 2cg3tU2TK6+wLul5ZMMvNrOgjaWcbqgW976hUAE03OgR5GDy4M3LfYOs8OJNsktG dtpfft6WroUoiqzDCNz85fbLqhY0vwPNJxK/wFHvxTJ2MKL/BdOha8gIV2o5HPTC cBpZx2C8FyUKAZfuXwTVkMtPbhFowjPNQN4TNl5N6DRtLnb1PDn9LOXHCPd+0V2b ssxu8B1+kf9IyrKBhlqKWSoLdaPboz5FunBUj6nkbXz2VlCkqbRIAFaTtmMJjjWR I0Fgx1CAbDjnDvf/hYrjPAvwLOcQ117Tm3Wk/3MP78CwmZoFHRrBw68Ir5mcLrQy L71AbBqGd3i+azsNXMmm11ZZetYzbIy6kc9g9MFHd+3xooJpU7eRpm+tyRLKX/qu /9iBhU0HVz2daNTk8LdQtzGqff8Dj0qB4sBndVsTW1JxKRP8E3jeLBT4EBGsy9OL 5aBXTA== -----END CERTIFICATE----- devise-two-factor-3.1.0/certs/tinfoil-cacert.pem0000644000004100000410000000503513526600171021637 0ustar www-datawww-data-----BEGIN CERTIFICATE----- MIIHSjCCBTKgAwIBAgIJAK2u0LojMCNgMA0GCSqGSIb3DQEBBQUAMIGcMQswCQYD VQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVBhbG8gQWx0bzEfMB0GA1UE ChMWVGluZm9pbCBTZWN1cml0eSwgSW5jLjEfMB0GA1UEAxMWVGluZm9pbCBTZWN1 cml0eSwgSW5jLjEqMCgGCSqGSIb3DQEJARYbc3VwcG9ydEB0aW5mb2lsc2VjdXJp dHkuY29tMB4XDTExMTIyNzA1MDc0N1oXDTIxMTIyNDA1MDc0N1owgZwxCzAJBgNV BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJUGFsbyBBbHRvMR8wHQYDVQQK ExZUaW5mb2lsIFNlY3VyaXR5LCBJbmMuMR8wHQYDVQQDExZUaW5mb2lsIFNlY3Vy aXR5LCBJbmMuMSowKAYJKoZIhvcNAQkBFhtzdXBwb3J0QHRpbmZvaWxzZWN1cml0 eS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCqbHvsSj0H0FB1 0gLYoDK1BKugkSB2DZeZZHP6B1UdWRahJXJP9oT1lhfQxx8iX4cgEi7JU3NqA6NR cIRFQ50eH/qlmgs7909gaf8pDaeC0vR3wd0GeRg6qr1eDEnkzIyr/D1AMiX6H1eP Y7J3SfrdaL3gft2iPRKGkgqsXR7oBNLA3n/ShiNgPXqRDl1CCj6aMY0cn5ROFScz vT2FUB4DEwPD2l18m1p99OnXqsOLL2J65qA2+cI8FtgFmlwIi5oSf+URvIdNx+cH lInlAtVHCvAKYLY0dlQ7czMQBcRpYjp2rwPt9f2ksq9b/voMTBABYHFV+IVn8svv GZ5e1+icjtr/R7dCGmCdEdFLXVxafmZhukymG9USv9DKuv1qh7r4q8KaPIE8n7nQ m97jENFfsgnwv+nUmIJ3tzuW5ZxO7A0tIIYdwzt0UjrO3ya4R5bTFXr4bnzZ/g/s CLknWqg1BCRlPd6LnpVGPT0gNDV1pEO25wE3A3Yy0Ujxudcgay/CgUhnlU11qOAc xmar2fhNZsviUhndd/220Ad5QMV2XzcAiopJIeu0juIVGRQM7x2h19Hsp0m6sOEF jfhvbdUa4nvmIFeYFY+hr/YkTmG9ZjyBa8YaZXhwjhSmKCQ374J7mn5e0Cryuvi5 tYhwJn8rdwYZF/h2qqfEu8vaLoD09QIDAQABo4IBizCCAYcwHQYDVR0OBBYEFMmT /x412UH+5OHqgleeTjLOv6iHMIHRBgNVHSMEgckwgcaAFMmT/x412UH+5OHqglee TjLOv6iHoYGipIGfMIGcMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNV BAcTCVBhbG8gQWx0bzEfMB0GA1UEChMWVGluZm9pbCBTZWN1cml0eSwgSW5jLjEf MB0GA1UEAxMWVGluZm9pbCBTZWN1cml0eSwgSW5jLjEqMCgGCSqGSIb3DQEJARYb c3VwcG9ydEB0aW5mb2lsc2VjdXJpdHkuY29tggkAra7QuiMwI2AwDwYDVR0TAQH/ BAUwAwEB/zARBglghkgBhvhCAQEEBAMCAQYwCQYDVR0SBAIwADArBglghkgBhvhC AQ0EHhYcVGlueUNBIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAmBgNVHREEHzAdgRtz dXBwb3J0QHRpbmZvaWxzZWN1cml0eS5jb20wDgYDVR0PAQH/BAQDAgEGMA0GCSqG SIb3DQEBBQUAA4ICAQAD7nsmdg1vStFTi8/P2rgSFxlXxZT0aaVVB1bFBe/m5phb MjvKQ7VAuiFZxEp3oBNdXTi4FzT1QjhRKdlYMgKZQnU+XDLLIYuoi+atxr5qGD4B m58eCGO6ZEutVs3Z7s63UOm5rG0zJ+IEWh8VHMvxgSwiX88QyJuhOtqeiKhIeSGZ 2/qGGMWgsScnPg3J/ZVOIKUn/4ljEDlC64Gh5Zz5PZUbGSXPMhdYbSD3EknDvEGA omYW4jlPMeK3GJgwAZu9yWC8hHGFpiMca/6W0W622cg7MX+CByOd+24dvWFnOHur NHBqI+kZo/7Sjdm8x7TWEOz9Rfh5RPMeVNRTj4iq0B6GzfaecT3Yn8y7HTRRiWns IYpP+iHCFYnZhDZsFi4ccKqxKtj6BGmhLf00FuNpgkvrsU3cXrhidkCaYGYj1SME 1CMfy0PPKVDpDKeFb6y0NvLf4d57vi99dZAvSJEO18rrNEHN2VUfCKRPA/mBSMLY RxKWAby1YVT/8iC9JWix9yvgsEUtTLyOFxLGtgj3PRiQSvbNe/jK4G9WAIFe6R9E 9+HUO2owcmyFXyU3rC/z/lBfDP+2pIRFdUVRGlYCMeUqR08PXpfva5+NQz21fC69 FPRMZvXh70ntnFaWAq+j6NCss+AauC8ckECiQsTgbzJvJd6C3mJXYHkNCQODhg== -----END CERTIFICATE----- devise-two-factor-3.1.0/metadata.gz.sig0000444000004100000410000000100013526600171017776 0ustar www-datawww-data  8'{;Q1+͏6uIR߈wP  RA[Ilടҙs)P58NF#&d/&*}F%d,Ux':V tG,P`eEދV$VDZ{x ',Dt@y9GZ=Xw~ސdBŠNo? -Y:a&[0Ͼ(japl~py%v !YAdevise-two-factor-3.1.0/.gitignore0000644000004100000410000000201713526600171017076 0ustar www-datawww-data*.gem *.rbc /.config /coverage/ /InstalledFiles /pkg/ /spec/reports/ /spec/examples.txt /test/tmp/ /test/version_tmp/ /tmp/ /gemfiles/.bundle/ /gemfiles/*.gemfile.lock ## Specific to RubyMotion: .dat* .repl_history build/ *.bridgesupport build-iPhoneOS/ build-iPhoneSimulator/ ## Specific to RubyMotion (use of CocoaPods): # # We recommend against adding the Pods directory to your .gitignore. However # you should judge for yourself, the pros and cons are mentioned at: # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control # # vendor/Pods/ ## Documentation cache and generated files: /.yardoc/ /_yardoc/ /doc/ /rdoc/ ## Environment normalization: /.bundle/ /vendor/bundle /lib/bundler/man/ # for a library or gem, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: Gemfile.lock .ruby-version .ruby-gemset .tool-versions # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: .rvmrc devise-two-factor-3.1.0/LICENSE0000644000004100000410000000210113526600171016105 0ustar www-datawww-dataThe MIT License (MIT) Copyright (c) 2014 Tinfoil Security, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. devise-two-factor-3.1.0/devise-two-factor.gemspec0000644000004100000410000000310313526600171022012 0ustar www-datawww-data$:.push File.expand_path('../lib', __FILE__) require 'devise_two_factor/version' Gem::Specification.new do |s| s.name = 'devise-two-factor' s.version = DeviseTwoFactor::VERSION.dup s.platform = Gem::Platform::RUBY s.licenses = ['MIT'] s.summary = 'Barebones two-factor authentication with Devise' s.email = 'engineers@tinfoilsecurity.com' s.homepage = 'https://github.com/tinfoil/devise-two-factor' s.description = 'Barebones two-factor authentication with Devise' s.authors = ['Shane Wilton'] s.cert_chain = [ 'certs/tinfoil-cacert.pem', 'certs/tinfoilsecurity-gems-cert.pem' ] s.signing_key = File.expand_path("~/.ssh/tinfoilsecurity-gems-key.pem") if $0 =~ /gem\z/ s.rubyforge_project = 'devise-two-factor' s.files = `git ls-files`.split("\n").delete_if { |x| x.match('demo/*') } s.test_files = `git ls-files -- spec/*`.split("\n") s.require_paths = ['lib'] s.add_runtime_dependency 'railties', '< 6.1' s.add_runtime_dependency 'activesupport', '< 6.1' s.add_runtime_dependency 'attr_encrypted', '>= 1.3', '< 4', '!= 2' s.add_runtime_dependency 'devise', '~> 4.0' s.add_runtime_dependency 'rotp', '~> 2.0' s.add_development_dependency 'activemodel' s.add_development_dependency 'appraisal' s.add_development_dependency 'bundler', '> 1.0' s.add_development_dependency 'rspec', '> 3' s.add_development_dependency 'simplecov' s.add_development_dependency 'faker' s.add_development_dependency 'timecop' end devise-two-factor-3.1.0/Rakefile0000644000004100000410000000103613526600171016553 0ustar www-datawww-data# encoding: utf-8 require 'rubygems' require 'bundler' begin Bundler.setup(:default, :development) rescue Bundler::BundlerError => e $stderr.puts e.message $stderr.puts "Run `bundle install` to install missing gems" exit e.status_code end require 'rake' require 'rspec/core' require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) do |spec| spec.pattern = FileList['spec/**/*_spec.rb'] end desc "Code coverage detail" task :simplecov do ENV['COVERAGE'] = "true" Rake::Task['spec'].execute end task :default => :spec devise-two-factor-3.1.0/lib/0000755000004100000410000000000013526600171015654 5ustar www-datawww-datadevise-two-factor-3.1.0/lib/generators/0000755000004100000410000000000013526600171020025 5ustar www-datawww-datadevise-two-factor-3.1.0/lib/generators/devise_two_factor/0000755000004100000410000000000013526600171023533 5ustar www-datawww-datadevise-two-factor-3.1.0/lib/generators/devise_two_factor/devise_two_factor_generator.rb0000644000004100000410000000534013526600171031636 0ustar www-datawww-datarequire 'rails/generators' module DeviseTwoFactor module Generators class DeviseTwoFactorGenerator < Rails::Generators::NamedBase argument :encryption_key_env, :type => :string, :required => true desc 'Creates a migration to add the required attributes to NAME, and ' \ 'adds the necessary Devise directives to the model' def install_devise_two_factor create_devise_two_factor_migration inject_strategies_into_warden_config inject_devise_directives_into_model end private def create_devise_two_factor_migration migration_arguments = [ "add_devise_two_factor_to_#{plural_name}", "encrypted_otp_secret:string", "encrypted_otp_secret_iv:string", "encrypted_otp_secret_salt:string", "consumed_timestep:integer", "otp_required_for_login:boolean" ] Rails::Generators.invoke('active_record:migration', migration_arguments) end def inject_strategies_into_warden_config config_path = File.join('config', 'initializers', 'devise.rb') content = " config.warden do |manager|\n" \ " manager.default_strategies(:scope => :#{singular_table_name}).unshift :two_factor_authenticatable\n" \ " end\n\n" inject_into_file(config_path, content, after: "Devise.setup do |config|\n") end def inject_devise_directives_into_model model_path = File.join('app', 'models', "#{file_path}.rb") class_path = if namespaced? class_name.to_s.split("::") else [class_name] end indent_depth = class_path.size content = [ "devise :two_factor_authenticatable,", " :otp_secret_encryption_key => ENV['#{encryption_key_env}']\n" ] content << "attr_accessible :otp_attempt\n" if needs_attr_accessible? content = content.map { |line| " " * indent_depth + line }.join("\n") << "\n" inject_into_class(model_path, class_path.last, content) # Remove :database_authenticatable from the list of loaded models gsub_file(model_path, /(devise.*):(, )?database_authenticatable(, )?/, '\1\2') end def needs_attr_accessible? !strong_parameters_enabled? && mass_assignment_security_enabled? end def strong_parameters_enabled? defined?(ActionController::StrongParameters) end def mass_assignment_security_enabled? defined?(ActiveModel::MassAssignmentSecurity) end end end end devise-two-factor-3.1.0/lib/devise-two-factor.rb0000644000004100000410000000210313526600171021537 0ustar www-datawww-datarequire 'devise' require 'devise_two_factor/models' require 'devise_two_factor/strategies' module Devise # The length of generated OTP secrets mattr_accessor :otp_secret_length @@otp_secret_length = 24 # The number of seconds before and after the current # time for which codes will be accepted mattr_accessor :otp_allowed_drift @@otp_allowed_drift = 30 # The key used to encrypt OTP secrets in the database mattr_accessor :otp_secret_encryption_key @@otp_secret_encryption_key = nil # The length of all generated OTP backup codes mattr_accessor :otp_backup_code_length @@otp_backup_code_length = 16 # The number of backup codes generated by a call to # generate_otp_backup_codes! mattr_accessor :otp_number_of_backup_codes @@otp_number_of_backup_codes = 5 end Devise.add_module(:two_factor_authenticatable, :route => :session, :strategy => true, :controller => :sessions, :model => true) Devise.add_module(:two_factor_backupable, :route => :session, :strategy => true, :controller => :sessions, :model => true) devise-two-factor-3.1.0/lib/devise_two_factor/0000755000004100000410000000000013526600171021362 5ustar www-datawww-datadevise-two-factor-3.1.0/lib/devise_two_factor/version.rb0000644000004100000410000000006613526600171023376 0ustar www-datawww-datamodule DeviseTwoFactor VERSION = '3.1.0'.freeze end devise-two-factor-3.1.0/lib/devise_two_factor/strategies.rb0000644000004100000410000000017713526600171024066 0ustar www-datawww-datarequire 'devise_two_factor/strategies/two_factor_authenticatable' require 'devise_two_factor/strategies/two_factor_backupable' devise-two-factor-3.1.0/lib/devise_two_factor/models/0000755000004100000410000000000013526600171022645 5ustar www-datawww-datadevise-two-factor-3.1.0/lib/devise_two_factor/models/two_factor_authenticatable.rb0000644000004100000410000000474613526600171030571 0ustar www-datawww-datarequire 'attr_encrypted' require 'rotp' module Devise module Models module TwoFactorAuthenticatable extend ActiveSupport::Concern include Devise::Models::DatabaseAuthenticatable included do unless singleton_class.ancestors.include?(AttrEncrypted) extend AttrEncrypted end unless attr_encrypted?(:otp_secret) attr_encrypted :otp_secret, :key => self.otp_secret_encryption_key, :mode => :per_attribute_iv_and_salt unless self.attr_encrypted?(:otp_secret) end attr_accessor :otp_attempt end def self.required_fields(klass) [:encrypted_otp_secret, :encrypted_otp_secret_iv, :encrypted_otp_secret_salt, :consumed_timestep] end # This defaults to the model's otp_secret # If this hasn't been generated yet, pass a secret as an option def validate_and_consume_otp!(code, options = {}) otp_secret = options[:otp_secret] || self.otp_secret return false unless code.present? && otp_secret.present? totp = self.otp(otp_secret) return consume_otp! if totp.verify_with_drift(code, self.class.otp_allowed_drift) false end def otp(otp_secret = self.otp_secret) ROTP::TOTP.new(otp_secret) end def current_otp otp.at(Time.now) end # ROTP's TOTP#timecode is private, so we duplicate it here def current_otp_timestep Time.now.utc.to_i / otp.interval end def otp_provisioning_uri(account, options = {}) otp_secret = options[:otp_secret] || self.otp_secret ROTP::TOTP.new(otp_secret, options).provisioning_uri(account) end def clean_up_passwords self.otp_attempt = nil end protected # An OTP cannot be used more than once in a given timestep # Storing timestep of last valid OTP is sufficient to satisfy this requirement def consume_otp! if self.consumed_timestep != current_otp_timestep self.consumed_timestep = current_otp_timestep return save(validate: false) end false end module ClassMethods Devise::Models.config(self, :otp_secret_length, :otp_allowed_drift, :otp_secret_encryption_key) def generate_otp_secret(otp_secret_length = self.otp_secret_length) ROTP::Base32.random_base32(otp_secret_length) end end end end end devise-two-factor-3.1.0/lib/devise_two_factor/models/two_factor_backupable.rb0000644000004100000410000000333313526600171027514 0ustar www-datawww-datamodule Devise module Models # TwoFactorBackupable allows a user to generate backup codes which # provide one-time access to their account in the event that they have # lost access to their two-factor device module TwoFactorBackupable extend ActiveSupport::Concern def self.required_fields(klass) [:otp_backup_codes] end # 1) Invalidates all existing backup codes # 2) Generates otp_number_of_backup_codes backup codes # 3) Stores the hashed backup codes in the database # 4) Returns a plaintext array of the generated backup codes def generate_otp_backup_codes! codes = [] number_of_codes = self.class.otp_number_of_backup_codes code_length = self.class.otp_backup_code_length number_of_codes.times do codes << SecureRandom.hex(code_length / 2) # Hexstring has length 2*n end hashed_codes = codes.map { |code| Devise::Encryptor.digest(self.class, code) } self.otp_backup_codes = hashed_codes codes end # Returns true and invalidates the given code # iff that code is a valid backup code. def invalidate_otp_backup_code!(code) codes = self.otp_backup_codes || [] codes.each do |backup_code| next unless Devise::Encryptor.compare(self.class, backup_code, code) codes.delete(backup_code) self.otp_backup_codes = codes return true end false end protected module ClassMethods Devise::Models.config(self, :otp_backup_code_length, :otp_number_of_backup_codes, :pepper) end end end end devise-two-factor-3.1.0/lib/devise_two_factor/strategies/0000755000004100000410000000000013526600171023534 5ustar www-datawww-datadevise-two-factor-3.1.0/lib/devise_two_factor/strategies/two_factor_authenticatable.rb0000644000004100000410000000222113526600171031442 0ustar www-datawww-datamodule Devise module Strategies class TwoFactorAuthenticatable < Devise::Strategies::DatabaseAuthenticatable def authenticate! resource = mapping.to.find_for_database_authentication(authentication_hash) # We authenticate in two cases: # 1. The password and the OTP are correct # 2. The password is correct, and OTP is not required for login # We check the OTP, then defer to DatabaseAuthenticatable if validate(resource) { validate_otp(resource) } super end fail(Devise.paranoid ? :invalid : :not_found_in_database) unless resource # We want to cascade to the next strategy if this one fails, # but database authenticatable automatically halts on a bad password @halted = false if @result == :failure end def validate_otp(resource) return true unless resource.otp_required_for_login return if params[scope]['otp_attempt'].nil? resource.validate_and_consume_otp!(params[scope]['otp_attempt']) end end end end Warden::Strategies.add(:two_factor_authenticatable, Devise::Strategies::TwoFactorAuthenticatable) devise-two-factor-3.1.0/lib/devise_two_factor/strategies/two_factor_backupable.rb0000644000004100000410000000163413526600171030405 0ustar www-datawww-datamodule Devise module Strategies class TwoFactorBackupable < Devise::Strategies::DatabaseAuthenticatable def authenticate! resource = mapping.to.find_for_database_authentication(authentication_hash) if validate(resource) { resource.invalidate_otp_backup_code!(params[scope]['otp_attempt']) } # Devise fails to authenticate invalidated resources, but if we've # gotten here, the object changed (Since we deleted a recovery code) resource.save! super end fail(Devise.paranoid ? :invalid : :not_found_in_database) unless resource # We want to cascade to the next strategy if this one fails, # but database authenticatable automatically halts on a bad password @halted = false if @result == :failure end end end end Warden::Strategies.add(:two_factor_backupable, Devise::Strategies::TwoFactorBackupable) devise-two-factor-3.1.0/lib/devise_two_factor/models.rb0000644000004100000410000000016713526600171023176 0ustar www-datawww-datarequire 'devise_two_factor/models/two_factor_authenticatable' require 'devise_two_factor/models/two_factor_backupable' devise-two-factor-3.1.0/lib/devise_two_factor/spec_helpers.rb0000644000004100000410000000024313526600171024362 0ustar www-datawww-datarequire 'devise_two_factor/spec_helpers/two_factor_authenticatable_shared_examples' require 'devise_two_factor/spec_helpers/two_factor_backupable_shared_examples' devise-two-factor-3.1.0/lib/devise_two_factor/spec_helpers/0000755000004100000410000000000013526600171024036 5ustar www-datawww-datadevise-two-factor-3.1.0/lib/devise_two_factor/spec_helpers/two_factor_backupable_shared_examples.rb0000644000004100000410000000535313526600171034135 0ustar www-datawww-dataRSpec.shared_examples 'two_factor_backupable' do describe 'required_fields' do it 'has the attr_encrypted fields for otp_backup_codes' do expect(Devise::Models::TwoFactorBackupable.required_fields(subject.class)).to contain_exactly(:otp_backup_codes) end end describe '#generate_otp_backup_codes!' do context 'with no existing recovery codes' do before do @plaintext_codes = subject.generate_otp_backup_codes! end it 'generates the correct number of new recovery codes' do expect(subject.otp_backup_codes.length).to eq(subject.class.otp_number_of_backup_codes) end it 'generates recovery codes of the correct length' do @plaintext_codes.each do |code| expect(code.length).to eq(subject.class.otp_backup_code_length) end end it 'generates distinct recovery codes' do expect(@plaintext_codes.uniq).to contain_exactly(*@plaintext_codes) end it 'stores the codes as BCrypt hashes' do subject.otp_backup_codes.each do |code| # $algorithm$cost$(22 character salt + 31 character hash) expect(code).to match(/\A\$[0-9a-z]{2}\$[0-9]{2}\$[A-Za-z0-9\.\/]{53}\z/) end end end context 'with existing recovery codes' do let(:old_codes) { Faker::Lorem.words } let(:old_codes_hashed) { old_codes.map { |x| Devise::Encryptor.digest(subject.class, x) } } before do subject.otp_backup_codes = old_codes_hashed @plaintext_codes = subject.generate_otp_backup_codes! end it 'invalidates the existing recovery codes' do expect((subject.otp_backup_codes & old_codes_hashed)).to match [] end end end describe '#invalidate_otp_backup_code!' do before do @plaintext_codes = subject.generate_otp_backup_codes! end context 'given an invalid recovery code' do it 'returns false' do expect(subject.invalidate_otp_backup_code!('password')).to be false end end context 'given a valid recovery code' do it 'returns true' do @plaintext_codes.each do |code| expect(subject.invalidate_otp_backup_code!(code)).to be true end end it 'invalidates that recovery code' do code = @plaintext_codes.sample subject.invalidate_otp_backup_code!(code) expect(subject.invalidate_otp_backup_code!(code)).to be false end it 'does not invalidate the other recovery codes' do code = @plaintext_codes.sample subject.invalidate_otp_backup_code!(code) @plaintext_codes.delete(code) @plaintext_codes.each do |code| expect(subject.invalidate_otp_backup_code!(code)).to be true end end end end end ././@LongLink0000644000000000000000000000015100000000000011600 Lustar rootrootdevise-two-factor-3.1.0/lib/devise_two_factor/spec_helpers/two_factor_authenticatable_shared_examples.rbdevise-two-factor-3.1.0/lib/devise_two_factor/spec_helpers/two_factor_authenticatable_shared_example0000644000004100000410000000725213526600171034414 0ustar www-datawww-dataRSpec.shared_examples 'two_factor_authenticatable' do before :each do subject.otp_secret = subject.class.generate_otp_secret subject.consumed_timestep = nil end describe 'required_fields' do it 'should have the attr_encrypted fields for otp_secret' do expect(Devise::Models::TwoFactorAuthenticatable.required_fields(subject.class)).to contain_exactly(:encrypted_otp_secret, :encrypted_otp_secret_iv, :encrypted_otp_secret_salt, :consumed_timestep) end end describe '#otp_secret' do it 'should be of the configured length' do expect(subject.otp_secret.length).to eq(subject.class.otp_secret_length) end it 'stores the encrypted otp_secret' do expect(subject.encrypted_otp_secret).to_not be_nil end it 'stores an iv for otp_secret' do expect(subject.encrypted_otp_secret_iv).to_not be_nil end it 'stores a salt for otp_secret' do expect(subject.encrypted_otp_secret_salt).to_not be_nil end end describe '#validate_and_consume_otp!' do let(:otp_secret) { '2z6hxkdwi3uvrnpn' } before :each do Timecop.freeze(Time.current) subject.otp_secret = otp_secret end after :each do Timecop.return end context 'with a stored consumed_timestep' do context 'given a precisely correct OTP' do let(:consumed_otp) { ROTP::TOTP.new(otp_secret).at(Time.now) } before do subject.validate_and_consume_otp!(consumed_otp) end it 'fails to validate' do expect(subject.validate_and_consume_otp!(consumed_otp)).to be false end end context 'given a previously valid OTP within the allowed drift' do let(:consumed_otp) { ROTP::TOTP.new(otp_secret).at(Time.now + subject.class.otp_allowed_drift, true) } before do subject.validate_and_consume_otp!(consumed_otp) end it 'fails to validate' do expect(subject.validate_and_consume_otp!(consumed_otp)).to be false end end end it 'validates a precisely correct OTP' do otp = ROTP::TOTP.new(otp_secret).at(Time.now) expect(subject.validate_and_consume_otp!(otp)).to be true end it 'fails a nil OTP value' do otp = nil expect(subject.validate_and_consume_otp!(otp)).to be false end it 'validates an OTP within the allowed drift' do otp = ROTP::TOTP.new(otp_secret).at(Time.now + subject.class.otp_allowed_drift, true) expect(subject.validate_and_consume_otp!(otp)).to be true end it 'does not validate an OTP above the allowed drift' do otp = ROTP::TOTP.new(otp_secret).at(Time.now + subject.class.otp_allowed_drift * 2, true) expect(subject.validate_and_consume_otp!(otp)).to be false end it 'does not validate an OTP below the allowed drift' do otp = ROTP::TOTP.new(otp_secret).at(Time.now - subject.class.otp_allowed_drift * 2, true) expect(subject.validate_and_consume_otp!(otp)).to be false end end describe '#otp_provisioning_uri' do let(:otp_secret_length) { subject.class.otp_secret_length } let(:account) { Faker::Internet.email } let(:issuer) { "Tinfoil" } it "should return uri with specified account" do expect(subject.otp_provisioning_uri(account)).to match(%r{otpauth://totp/#{account}\?secret=\w{#{otp_secret_length}}}) end it 'should return uri with issuer option' do expect(subject.otp_provisioning_uri(account, issuer: issuer)).to match(%r{otpauth://totp/#{account}\?.*secret=\w{#{otp_secret_length}}(&|$)}) expect(subject.otp_provisioning_uri(account, issuer: issuer)).to match(%r{otpauth://totp/#{account}\?.*issuer=#{issuer}(&|$)}) end end end devise-two-factor-3.1.0/UPGRADING.md0000644000004100000410000000303013526600171016744 0ustar www-datawww-data# Guide to upgrading from 2.x to 3.x Pull request #76 allows for compatibility with `attr_encrypted` 3.0, which should be used due to a security vulnerability discovered in 2.0. Pull request #73 allows for compatibility with `attr_encrypted` 2.0. This version changes many of the defaults which must be taken into account to avoid corrupted OTP secrets on your model. Due to new security practices in `attr_encrypted` an encryption key with insufficient length will cause an error. If you run into this, you may set `insecure_mode: true` in the `attr_encrypted` options. You should initially add compatibility by specifying the `attr_encrypted` attribute in your model (`User` for these examples) with the old default encryption algorithm before invoking `devise :two_factor_authenticatable`: ```ruby class User < ActiveRecord::Base attr_encrypted :otp_secret, :key => self.otp_secret_encryption_key, :mode => :per_attribute_iv_and_salt, :algorithm => 'aes-256-cbc' devise :two_factor_authenticatable, :otp_secret_encryption_key => ENV['DEVISE_TWO_FACTOR_ENCRYPTION_KEY'] ``` # Guide to upgrading from 1.x to 2.x Pull request #43 added a new field to protect against "shoulder-surfing" attacks. If upgrading, you'll need to add the `:consumed_timestep` column to your `Users` model. ```ruby class AddConsumedTimestepToUsers < ActiveRecord::Migration def change add_column :users, :consumed_timestep, :integer end end ``` All uses of the `valid_otp?` method should be switched to `validate_and_consume_otp!` devise-two-factor-3.1.0/CONTRIBUTING.md0000644000004100000410000000247013526600171017342 0ustar www-datawww-dataWe love pull requests. Here's a quick guide: 1. Fork the repo. 2. Run the tests. We only take pull requests with passing tests, and it's great to know that you have a clean slate: `bundle && rake` 3. Add a test for your change. Only refactoring and documentation changes require no new tests. If you are adding functionality or fixing a bug, we need a test! 4. Make the test pass. 5. Push to your fork and submit a pull request. At this point you're waiting on us. We like to at least comment on, if not accept, pull requests within three business days (and, typically, one business day). We may suggest some changes or improvements or alternatives. Some things that will increase the chance that your pull request is accepted, taken straight from the Ruby on Rails guide: * Use Rails idioms and helpers * Include tests that fail without your code, and pass with it * Update the documentation, the surrounding one, examples elsewhere, guides, whatever is affected by your contribution Syntax: * Two spaces, no tabs. * No trailing whitespace. Blank lines should not have any space. * Prefer &&/|| over and/or. * MyClass.my_method(my_arg) not my_method( my_arg ) or my_method my_arg. * a = b and not a=b. * Follow the conventions you see used in the source already. And in case we didn't emphasize it enough: we love tests! devise-two-factor-3.1.0/Gemfile0000644000004100000410000000004613526600171016401 0ustar www-datawww-datasource 'https://rubygems.org' gemspec