devise-two-factor-4.0.0/0000755000004100000410000000000014042055576015115 5ustar www-datawww-datadevise-two-factor-4.0.0/.travis.yml0000644000004100000410000000211014042055576017220 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-4.0.0/.rspec0000644000004100000410000000001014042055576016221 0ustar www-datawww-data--color devise-two-factor-4.0.0/README.md0000644000004100000410000003040014042055576016371 0ustar www-datawww-data# Devise-Two-Factor Authentication By [Tinfoil Security](https://www.tinfoilsecurity.com/) (acq. [Synopsys](https://www.synopsys.com/) 2020). Interested in [working with us](https://www.synopsys.com/careers.html)? 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) 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 Remember to apply the new migration. ```ruby bundle exec rake db:migrate ``` 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://www.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`. This value can be modified by adding a config in `config/initializers/devise.rb`. ```ruby Devise.otp_allowed_drift = 240 # value in seconds Devise.setup do |config| ... end ``` 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-4.0.0/gemfiles/0000755000004100000410000000000014042055576016710 5ustar www-datawww-datadevise-two-factor-4.0.0/gemfiles/rails_4_2.gemfile0000644000004100000410000000022314042055576022015 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-4.0.0/gemfiles/rails_5_2.gemfile0000644000004100000410000000022314042055576022016 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-4.0.0/gemfiles/rails_5_0.gemfile0000644000004100000410000000022314042055576022014 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-4.0.0/gemfiles/rails_5_1.gemfile0000644000004100000410000000022314042055576022015 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-4.0.0/gemfiles/rails_6_0.gemfile0000644000004100000410000000023714042055576022022 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-4.0.0/gemfiles/rails_4_1.gemfile0000644000004100000410000000022314042055576022014 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-4.0.0/data.tar.gz.sig0000444000004100000410000000100014042055576017723 0ustar www-datawww-dataCMI턗]B &hoŌ:cYiN.+p]dz 8_gmge.enʍJ.kIa$msh8p禑 g.=ÇJ$澟^!mBY;2It xũj}땇[d~GKߵ2r~ƘÕIc+==ET=N'6_S"@宋·Q\laBK6;ٔl}+w^#s1A~/Ȁ&ns n=9~ח]8K\2ndevise-two-factor-4.0.0/spec/0000755000004100000410000000000014042055576016047 5ustar www-datawww-datadevise-two-factor-4.0.0/spec/devise/0000755000004100000410000000000014042055576017326 5ustar www-datawww-datadevise-two-factor-4.0.0/spec/devise/models/0000755000004100000410000000000014042055576020611 5ustar www-datawww-datadevise-two-factor-4.0.0/spec/devise/models/two_factor_authenticatable_spec.rb0000644000004100000410000000524414042055576027541 0ustar www-datawww-datarequire 'spec_helper' require 'active_model' class TwoFactorAuthenticatableDouble extend ::ActiveModel::Callbacks include ::ActiveModel::Validations::Callbacks 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 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 describe ::Devise::Models::TwoFactorAuthenticatable do context 'When clean_up_passwords is called ' do subject { TwoFactorAuthenticatableDouble.new } before :each do subject.otp_attempt = 'foo' subject.password_confirmation = 'foo' end it 'otp_attempt should be nill' do subject.clean_up_passwords expect(subject.otp_attempt).to be_nil end it 'password_confirmation should be nill' do subject.clean_up_passwords expect(subject.password_confirmation).to be_nil end end end devise-two-factor-4.0.0/spec/devise/models/two_factor_backupable_spec.rb0000644000004100000410000000106714042055576026474 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-4.0.0/spec/spec_helper.rb0000644000004100000410000000142614042055576020670 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 'devise-two-factor' require 'devise_two_factor/spec_helpers' require 'active_support/testing/time_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.include ActiveSupport::Testing::TimeHelpers config.order = 'random' end devise-two-factor-4.0.0/CHANGELOG.md0000644000004100000410000000350214042055576016726 0ustar www-datawww-data# CHANGELOG ## Unreleased ## 4.0.0 - Update ROTP - Add Rails 6.1 support - Remove timecop dependency - Clarify changes in project ownership - Bugfixes & cleanup ## 3.1.0 - Add Rails 6.0 support - New gem signing certificate - Fix paranoid-mode being ignored ## 3.0.3 - Add Rails 5.2 support ## 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-4.0.0/Appraisals0000644000004100000410000000101714042055576017136 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-4.0.0/certs/0000755000004100000410000000000014042055576016235 5ustar www-datawww-datadevise-two-factor-4.0.0/certs/tinfoilsecurity-gems-cert.pem0000644000004100000410000000413714042055576024065 0ustar www-datawww-data-----BEGIN CERTIFICATE----- MIIGADCCA+igAwIBAgIIP4wV6YA6CO0wDQYJKoZIhvcNAQENBQAwgZwxCzAJBgNV BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJUGFsbyBBbHRvMR8wHQYDVQQK ExZUaW5mb2lsIFNlY3VyaXR5LCBJbmMuMR8wHQYDVQQDExZUaW5mb2lsIFNlY3Vy aXR5LCBJbmMuMSowKAYJKoZIhvcNAQkBFhtzdXBwb3J0QHRpbmZvaWxzZWN1cml0 eS5jb20wHhcNMjEwNDA4MTUxODAwWhcNMjExMjI0MDUwNzAwWjCBiDELMAkGA1UE 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 MA0GCSqGSIb3DQEBDQUAA4ICAQB4p1yL6e/38Dmf5HdZoSJzQ7AcM+jrD0LdMC1V H0Y107JzZWvIB2aWH4tw4+SKGTr52OvyGFLpBv5jsWUUFssuAV971T1x41kWJSYt tnljNguSrH6ah/pDravLxi+JGQXMBRXhkdvQKbFOfutSe9HEuZLiWUYNDYM17XJq WmG+QhNgXliXgu4AQg+8vb1rDbu/G491GEuxbwaLyyKG8X+P5mYTBbMQbTgJkNfX elpmFtqivFaEHs3evVGEEZRQhe8i5V0Ak2c4Or1ap/pZQf3hUIkZbw7HumyZYNWi VJDMpObUyucv6++TNW8bAI5Oip8DGeYKibPsJ0IfYxMmRC3BmY1E3IIvAdsUHTcq WapfQlX732+mfx/gSBpuZhdwEqjWj0xPj6l9DjQrGuhUEijfucKqyY3F280OYM1b 2zG6cwVmh5IeR9nVv0i2KNkoc2zC8tcGpjfBBuDdXZCpow54DRJU4qQ6S0lH5ojs aQHEEIQ9/STv9TKuc4KlMUey8W6L0Zw+xFWnkLeygaMps1PhPokSbrABQsB4C10Q QSG/Dvvw438W/2sb9aR+skGh1oNAwJiFhLNaNALfkSXRtU16gLMPBJCi2Xqyco7V Wh4SFQHrAbuglSi0nYgFm2SxYf/r6JRKxhVkwo8wxRiV8rDZj7WmzQoZK4GHj1u6 LXXw3g== -----END CERTIFICATE----- devise-two-factor-4.0.0/certs/tinfoil-cacert.pem0000644000004100000410000000503514042055576021646 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-4.0.0/metadata.gz.sig0000444000004100000410000000100014042055576020005 0ustar www-datawww-data8h4,7cV32gmAUtZ&*Օ")}z5_0HָD/NQ3y/?Le{J-h71BL'])C `k:'-0+6\y1Bv=Wtɥ8}Ov&%*xF]ψ%.Knzw =ZDqm֜)^jޛ@p#$ xN0g>ōC T@\}6=dd5qm;' ~_ϥE[buC ΌXe@7ƻQta܂̵&k*X[o #AV*I3p^i\4Dy]8χ!Zu?ZfA84 $Aޜw5q+zx<&: iQXBٔЛ;?!>+1 }V M.5@vYjaG6rCՃƂg8NT;{Pzd xdevise-two-factor-4.0.0/checksums.yaml.gz.sig0000444000004100000410000000100014042055576021153 0ustar www-datawww-data8r^||';L$N8,IOaaRk=!W w]f`4bwj2n>R9nkXGwoC3۽<\ ܣ# FNlJp6æBhZ@Z*iyByu ֶX vNFc,imeK?1ѱ=Ysם/ݑQ -^ t߯$ b^` i83-RM36Bf݆aKs9F %zf#+bRsŭq:=5 EM&my>4z!lTQͿ=W0r+ImՅV[FI&o#1ѕze/p<LTZ\ZtGw>`1q2q[{f?8k.:%zKpQsGef Qu7.pɹ9XkH\Re:=-devise-two-factor-4.0.0/.gitignore0000644000004100000410000000201714042055576017105 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-4.0.0/LICENSE0000644000004100000410000000207114042055576016122 0ustar www-datawww-dataThe MIT License (MIT) Copyright (c) 2014 Synopsys, 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-4.0.0/devise-two-factor.gemspec0000644000004100000410000000275414042055576022034 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.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.2' s.add_runtime_dependency 'activesupport', '< 6.2' s.add_runtime_dependency 'attr_encrypted', '>= 1.3', '< 4', '!= 2' s.add_runtime_dependency 'devise', '~> 4.0' s.add_runtime_dependency 'rotp', '~> 6.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' end devise-two-factor-4.0.0/Rakefile0000644000004100000410000000103614042055576016562 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-4.0.0/lib/0000755000004100000410000000000014042055576015663 5ustar www-datawww-datadevise-two-factor-4.0.0/lib/generators/0000755000004100000410000000000014042055576020034 5ustar www-datawww-datadevise-two-factor-4.0.0/lib/generators/devise_two_factor/0000755000004100000410000000000014042055576023542 5ustar www-datawww-datadevise-two-factor-4.0.0/lib/generators/devise_two_factor/devise_two_factor_generator.rb0000644000004100000410000000534014042055576031645 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-4.0.0/lib/devise-two-factor.rb0000644000004100000410000000210314042055576021546 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-4.0.0/lib/devise_two_factor/0000755000004100000410000000000014042055576021371 5ustar www-datawww-datadevise-two-factor-4.0.0/lib/devise_two_factor/version.rb0000644000004100000410000000006614042055576023405 0ustar www-datawww-datamodule DeviseTwoFactor VERSION = '4.0.0'.freeze end devise-two-factor-4.0.0/lib/devise_two_factor/strategies.rb0000644000004100000410000000017714042055576024075 0ustar www-datawww-datarequire 'devise_two_factor/strategies/two_factor_authenticatable' require 'devise_two_factor/strategies/two_factor_backupable' devise-two-factor-4.0.0/lib/devise_two_factor/models/0000755000004100000410000000000014042055576022654 5ustar www-datawww-datadevise-two-factor-4.0.0/lib/devise_two_factor/models/two_factor_authenticatable.rb0000644000004100000410000000525214042055576030571 0ustar www-datawww-datarequire 'rotp' module Devise module Models module TwoFactorAuthenticatable extend ActiveSupport::Concern include Devise::Models::DatabaseAuthenticatable included do unless %i[otp_secret otp_secret=].all? { |attr| method_defined?(attr) } require 'attr_encrypted' 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 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 = otp(otp_secret) if totp.verify(code, drift_behind: self.class.otp_allowed_drift, drift_ahead: self.class.otp_allowed_drift) return consume_otp! end 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 super 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-4.0.0/lib/devise_two_factor/models/two_factor_backupable.rb0000644000004100000410000000333314042055576027523 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-4.0.0/lib/devise_two_factor/strategies/0000755000004100000410000000000014042055576023543 5ustar www-datawww-datadevise-two-factor-4.0.0/lib/devise_two_factor/strategies/two_factor_authenticatable.rb0000644000004100000410000000222114042055576031451 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-4.0.0/lib/devise_two_factor/strategies/two_factor_backupable.rb0000644000004100000410000000163414042055576030414 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-4.0.0/lib/devise_two_factor/models.rb0000644000004100000410000000016714042055576023205 0ustar www-datawww-datarequire 'devise_two_factor/models/two_factor_authenticatable' require 'devise_two_factor/models/two_factor_backupable' devise-two-factor-4.0.0/lib/devise_two_factor/spec_helpers.rb0000644000004100000410000000024314042055576024371 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-4.0.0/lib/devise_two_factor/spec_helpers/0000755000004100000410000000000014042055576024045 5ustar www-datawww-datadevise-two-factor-4.0.0/lib/devise_two_factor/spec_helpers/two_factor_backupable_shared_examples.rb0000644000004100000410000000535314042055576034144 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-4.0.0/lib/devise_two_factor/spec_helpers/two_factor_authenticatable_shared_examples.rbdevise-two-factor-4.0.0/lib/devise_two_factor/spec_helpers/two_factor_authenticatable_shared_example0000644000004100000410000000731514042055576034423 0ustar www-datawww-datarequire 'cgi' RSpec.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 travel_to(Time.now) subject.otp_secret = otp_secret end after :each do travel_back 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) } 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) 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) 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) 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/#{CGI.escape(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/#{issuer}:#{CGI.escape(account)}\?.*secret=\w{#{otp_secret_length}}(&|$)}) expect(subject.otp_provisioning_uri(account, issuer: issuer)).to match(%r{otpauth://totp/#{issuer}:#{CGI.escape(account)}\?.*issuer=#{issuer}(&|$)}) end end end devise-two-factor-4.0.0/UPGRADING.md0000644000004100000410000000303014042055576016753 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-4.0.0/CONTRIBUTING.md0000644000004100000410000000247014042055576017351 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-4.0.0/Gemfile0000644000004100000410000000004614042055576016410 0ustar www-datawww-datasource 'https://rubygems.org' gemspec