├── .document ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .rspec ├── Appraisals ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── app ├── assets │ ├── javascripts │ │ └── devise_authy.js │ └── stylesheets │ │ ├── devise_authy.css │ │ └── devise_authy.sass ├── controllers │ ├── devise │ │ └── devise_authy_controller.rb │ └── devise_authy │ │ └── passwords_controller.rb └── views │ └── devise │ ├── enable_authy.html.erb │ ├── enable_authy.html.haml │ ├── verify_authy.html.erb │ ├── verify_authy.html.haml │ ├── verify_authy_installation.html.erb │ └── verify_authy_installation.html.haml ├── config.ru ├── config └── locales │ └── en.yml ├── devise-authy.gemspec ├── gemfiles ├── .bundle │ └── config ├── rails_5_2.gemfile └── rails_6.gemfile ├── lib ├── devise-authy.rb ├── devise-authy │ ├── controllers │ │ ├── helpers.rb │ │ └── view_helpers.rb │ ├── hooks │ │ └── authy_authenticatable.rb │ ├── mapping.rb │ ├── models │ │ ├── authy_authenticatable.rb │ │ └── authy_lockable.rb │ ├── rails.rb │ ├── routes.rb │ └── version.rb └── generators │ ├── active_record │ ├── devise_authy_generator.rb │ └── templates │ │ └── migration.rb │ └── devise_authy │ ├── devise_authy_generator.rb │ └── install_generator.rb └── spec ├── config └── routes_spec.rb ├── controllers ├── devise_authy_controller_spec.rb ├── devise_sessions_controller_spec.rb └── passwords_controller_spec.rb ├── devise-authy └── version_spec.rb ├── factories.rb ├── generators ├── active_record │ └── devise_authy_generator_spec.rb └── devise_authy │ ├── devise_authy_generator_spec.rb │ └── install_generator_spec.rb ├── helpers └── view_helpers_spec.rb ├── internal ├── app │ ├── assets │ │ └── config │ │ │ └── manifest.js │ ├── controllers │ │ ├── application_controller.rb │ │ └── home_controller.rb │ ├── helpers │ │ └── application_helper.rb │ └── models │ │ ├── lockable_user.rb │ │ └── user.rb ├── config │ ├── database.yml │ ├── initializers │ │ └── devise.rb │ └── routes.rb ├── db │ └── schema.rb ├── log │ └── .gitignore └── public │ └── favicon.ico ├── models ├── lockable_user_spec.rb └── user_spec.rb └── spec_helper.rb /.document: -------------------------------------------------------------------------------- 1 | lib/**/*.rb 2 | bin/* 3 | - 4 | features/**/*.feature 5 | LICENSE.txt 6 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | ruby: [2.5, 2.6, 2.7, "3.0", 3.1, head] 12 | gemfile: [rails_5_2, rails_6] 13 | exclude: 14 | - ruby: "3.0" 15 | gemfile: rails_5_2 16 | - ruby: 3.1 17 | gemfile: rails_5_2 18 | - ruby: head 19 | gemfile: rails_5_2 20 | continue-on-error: ${{ endsWith(matrix.ruby, 'head') }} 21 | env: 22 | BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Set up Ruby ${{ matrix.ruby }} 26 | uses: ruby/setup-ruby@v1 27 | with: 28 | ruby-version: ${{ matrix.ruby }} 29 | - name: Install dependencies 30 | run: bundle install 31 | - name: Run tests 32 | run: bundle exec rspec 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /spec/examples.txt 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | ## Specific to RubyMotion: 14 | .dat* 15 | .repl_history 16 | build/ 17 | 18 | ## Documentation cache and generated files: 19 | /.yardoc/ 20 | /_yardoc/ 21 | /doc/ 22 | /rdoc/ 23 | 24 | ## Environment normalization: 25 | /.bundle/ 26 | /vendor/bundle 27 | /lib/bundler/man/ 28 | 29 | # for a library or gem, you might want to ignore these files since the code is 30 | # intended to run in multiple environments; otherwise, check them in: 31 | Gemfile.lock 32 | .ruby-version 33 | .ruby-gemset 34 | gemfiles/*.lock 35 | 36 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 37 | .rvmrc 38 | 39 | **/*.sqlite 40 | **/*.log 41 | 42 | initializers/authy.rb 43 | .byebug_history 44 | 45 | .rspec_status -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require ./spec/spec_helper -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise "rails-5-2" do 2 | gem "rails", "~> 5.2.0" 3 | gem "sqlite3", "~> 1.3.13" 4 | 5 | group :development, :test do 6 | gem 'factory_girl_rails', :require => false 7 | gem 'rspec-rails', "~>4.0.0.beta3", :require => false 8 | gem 'database_cleaner', :require => false 9 | end 10 | end if RUBY_VERSION.to_f < 3.0 11 | 12 | appraise "rails-6" do 13 | gem "rails", "~> 6.0.0" 14 | gem "sqlite3", "~> 1.4" 15 | gem "net-smtp" 16 | 17 | group :development, :test do 18 | gem 'factory_girl_rails', :require => false 19 | gem 'rspec-rails', "~>4.0.0.beta3", :require => false 20 | gem 'database_cleaner', :require => false 21 | end 22 | end if RUBY_VERSION.to_f >= 2.5 -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## [2.3.1] - 2022-05-30 Final release 9 | 10 | ### Changed 11 | 12 | - Added deprecation notices to README and Gemspec 13 | 14 | ## [2.3.0] - 2021-01-07 15 | 16 | ### Fixed 17 | 18 | - Fixes calls to `I18n.t` with keyword arguments to support Ruby 3.0 19 | - Replaces Travis CI with GitHub Actions 20 | - Updates webmock development dependency 21 | - Removes sdoc from Gemfile 22 | 23 | ## [2.2.1] - 2020-10-13 24 | 25 | ### Fixed 26 | 27 | - If the app offers a QR code scan and user fails to verify authy installation, the QR code wasn't shown again. Fixed in (#149) 28 | 29 | ## [2.2.0] - 2020-06-04 30 | 31 | ### Fixed 32 | 33 | - Don't delete user in Authy if another user has the same authy_id (#144) 34 | 35 | ## [2.1.0] - 2020-05-05 36 | 37 | ### Added 38 | 39 | - Support for generic authenticator tokens (#141) 40 | 41 | ### Fixed 42 | 43 | - Can remember device when enabling 2FA for the first time (#139) 44 | 45 | ## [2.0.0] - 2020-04-28 46 | 47 | Releasing this as version 2 because there is a significant change in dependencies. Minimum version of Rails is now 5 and of Devise is now 4. Otherwise the gem should work as before. 48 | 49 | ### Added 50 | 51 | - HTTP Only flag to remember_device cookie (#116 thanks @agronv) 52 | - Remembers device when user logs in with One Touch (#128 thanks @cplopez4) 53 | - Autocomplete attributes for HTML form (#130) 54 | 55 | ### Changed 56 | 57 | - Mocked API calls in test suite (#123) 58 | - Full test suite refactor (#124) 59 | - Increased required version for Devise and Rails (#125) 60 | - Stopped calling `signed_in?` before it is needed (#126) 61 | 62 | ### Fixes 63 | 64 | - Remembers user correctly when logging in with One Touch (#129) 65 | 66 | ## [1.11.1] - 2019-02-02 67 | 68 | ### Fixed 69 | 70 | - Using the version before loading it broke everything. :facepalm: 71 | 72 | ## [1.11.0] - 2019-02-01 73 | 74 | ### Fixed 75 | 76 | - Corrects for label in verify_authy view (#103 thanks @mstruebing) 77 | - Corrects heading in verify_authy view (#104 thanks @mstruebing) 78 | 79 | ### Changed 80 | 81 | - Allows you to define paths for request_sms and request_phone_call (#108 thanks @dedene) 82 | 83 | ### Added 84 | 85 | - Now sets a distinct user agent through the Authy gem (#110) 86 | 87 | ## [1.10.0] - 2018-09-26 88 | 89 | ### Changed 90 | 91 | - Moves OneTouch approval request copy to locale file. 92 | 93 | ### Removed 94 | 95 | - Demo app now lives in its own repo 96 | 97 | ## [1.9.0] - 2018-09-04 98 | 99 | ### Fixed 100 | 101 | - Generated migration now includes version number for Rails 5 102 | 103 | ### Changed 104 | 105 | - Removes Jeweler in favour of administering the gemspec by hand 106 | - Removes demo app files from gem package 107 | 108 | ## [1.8.3] - 2018-07-05 109 | 110 | ### Fixed 111 | 112 | - Fixes Ruby interpolation in HAML for onetouch (thanks @muan) 113 | - Records Authy authentication after install verification (thanks @nukturnal) 114 | - Forgets remember device cookie when disabling Authy (thanks @senekis) 115 | 116 | ### Changed 117 | 118 | - Updated testing Rubies in CI 119 | 120 | ## Older releases 121 | 122 | **_The following releases happened before the changelog was started. Some history will be added for clarity._** 123 | 124 | ## [1.8.2] - 2017-12-22 125 | 126 | ## [1.8.1] - 2016-12-06 127 | 128 | ## [1.8.0] - 2016-10-25 129 | 130 | ## [1.7.0] - 2015-12-22 131 | 132 | ## [1.6.0] - 2015-01-07 133 | 134 | ## [1.5.3] - 2014-06-11 135 | 136 | ## [1.5.2] - 2014-06-11 137 | 138 | ## [1.5.1] - 2014-04-24 139 | 140 | ## [1.5.0] - 2014-01-07 141 | 142 | ## [1.4.0] - 2013-12-17 143 | 144 | ## [1.3.0] - 2013-11-16 145 | 146 | ## [1.2.2] - 2013-09-04 147 | 148 | ## [1.2.1] - 2013-04-22 149 | 150 | ## [1.2.0] - 2013-04-22 [YANKED] 151 | 152 | ## [1.0.0] - 2013-04-10 153 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2021 Authy Inc 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 🚨🚨🚨 2 | 3 | **This library is no longer actively maintained.** The Authy API has been replaced with the [Twilio Verify API](https://www.twilio.com/docs/verify). Twilio will support the Authy API through November 1, 2022 for SMS/Voice. After this date, we’ll start to deprecate the service for SMS/Voice. Any requests sent to the API after May 1, 2023, will automatically receive an error. Push and TOTP will continue to be supported through July 2023. 4 | 5 | [Learn more about migrating from Authy to Verify.](https://www.twilio.com/blog/migrate-authy-to-verify) 6 | 7 | Please visit the Twilio Docs for: 8 | * [Verify + Ruby (Rails) quickstart](https://www.twilio.com/docs/verify/quickstarts/ruby-rails) 9 | * [Twilio Ruby helper library](https://www.twilio.com/docs/libraries/ruby) 10 | * [Verify API reference](https://www.twilio.com/docs/verify/api) 11 | * **Coming soon**: Look out for a new Devise plugin to use Twilio Verify with Devise 12 | 13 | Please direct any questions to [Twilio Support](https://support.twilio.com/hc/en-us). Thank you! 14 | 15 | 🚨🚨🚨 16 | 17 | --- 18 | 19 | # Authy Devise [![Build Status](https://github.com/twilio/authy-devise/workflows/build/badge.svg)](https://github.com/twilio/authy-devise/actions) 20 | 21 | This is a [Devise](https://github.com/plataformatec/devise) extension to add [Two-Factor Authentication with Authy](https://www.twilio.com/docs/authy) to your Rails application. 22 | 23 | * [Pre-requisites](#pre-requisites) 24 | * [Demo](#demo) 25 | * [Getting started](#getting-started) 26 | * [Configuring Models](#configuring-models) 27 | * [With the generator](#with-the-generator) 28 | * [Manually](#manually) 29 | * [Final steps](#final-steps) 30 | * [Custom Views](#custom-views) 31 | * [Request a phone call](#request-a-phone-call) 32 | * [Custom Redirect Paths (eg. using modules)](#custom-redirect-paths-eg-using-modules) 33 | * [I18n](#i18n) 34 | * [Session variables](#session-variables) 35 | * [OneTouch support](#onetouch-support) 36 | * [Generic authenticator token support](#generic-authenticator-token-support) 37 | * [Rails 5 CSRF protection](#rails-5-csrf-protection) 38 | * [Running Tests](#running-tests) 39 | * [Notice: Twilio Authy API’s Sandbox feature will stop working on Sep 30, 2021](#notice-twilio-authy-apis-sandbox-feature-will-stop-working-on-sep-30-2021) 40 | * [Copyright](#copyright) 41 | 42 | ## Pre-requisites 43 | 44 | To use the Authy API you will need a Twilio Account, [sign up for a free Twilio account here](https://www.twilio.com/try-twilio). 45 | 46 | Create an [Authy Application in the Twilio console](https://www.twilio.com/console/authy/applications) and take note of the API key. 47 | 48 | ## Demo 49 | 50 | See [this repo for a full demo of using `authy-devise`](https://github.com/twilio/authy-devise-demo). 51 | 52 | ## Getting started 53 | 54 | First get your Authy API key from [the Twilio console](https://www.twilio.com/console/authy/applications). We recommend you store your API key as an environment variable. 55 | 56 | ```bash 57 | $ export AUTHY_API_KEY=YOUR_AUTHY_API_KEY 58 | ``` 59 | 60 | Next add the gem to your Gemfile: 61 | 62 | ```ruby 63 | gem 'devise' 64 | gem 'devise-authy' 65 | ``` 66 | 67 | And then run `bundle install` 68 | 69 | Add `Devise Authy` to your App: 70 | 71 | rails g devise_authy:install 72 | 73 | --haml: Generate the views in Haml 74 | --sass: Generate the stylesheets in Sass 75 | 76 | ### Configuring Models 77 | 78 | You can add devise_authy to your user model in two ways. 79 | 80 | #### With the generator 81 | 82 | Run the following command: 83 | 84 | ```bash 85 | rails g devise_authy [MODEL_NAME] 86 | ``` 87 | 88 | To support account locking (recommended), you must add `:authy_lockable` to the `devise :authy_authenticatable, ...` configuration in your model as this is not yet supported by the generator. 89 | 90 | #### Manually 91 | 92 | Add `:authy_authenticatable` and `:authy_lockable` to the `devise` options in your Devise user model: 93 | 94 | ```ruby 95 | devise :authy_authenticatable, :authy_lockable, :database_authenticatable, :lockable 96 | ``` 97 | 98 | (Note, `:authy_lockable` is optional but recommended. It should be used with Devise's own `:lockable` module). 99 | 100 | Also add a new migration. For example, if you are adding to the `User` model, use this migration: 101 | 102 | ```ruby 103 | class DeviseAuthyAddToUsers < ActiveRecord::Migration[6.0] 104 | def self.up 105 | change_table :users do |t| 106 | t.string :authy_id 107 | t.datetime :last_sign_in_with_authy 108 | t.boolean :authy_enabled, :default => false 109 | end 110 | 111 | add_index :users, :authy_id 112 | end 113 | 114 | def self.down 115 | change_table :users do |t| 116 | t.remove :authy_id, :last_sign_in_with_authy, :authy_enabled 117 | end 118 | end 119 | end 120 | ``` 121 | 122 | #### Final steps 123 | 124 | For either method above, run the migrations: 125 | 126 | ```bash 127 | rake db:migrate 128 | ``` 129 | 130 | **[Optional]** Update the default routes to point to something like: 131 | 132 | ```ruby 133 | devise_for :users, :path_names => { 134 | :verify_authy => "/verify-token", 135 | :enable_authy => "/enable-two-factor", 136 | :verify_authy_installation => "/verify-installation", 137 | :authy_onetouch_status => "/onetouch-status" 138 | } 139 | ``` 140 | 141 | Now whenever a user wants to enable two-factor authentication they can go to: 142 | 143 | http://your-app/users/enable-two-factor 144 | 145 | And when the user logs in they will be redirected to: 146 | 147 | http://your-app/users/verify-token 148 | 149 | ## Custom Views 150 | 151 | If you want to customise your views, you can modify the files that are located at: 152 | 153 | app/views/devise/devise_authy/enable_authy.html.erb 154 | app/views/devise/devise_authy/verify_authy.html.erb 155 | app/views/devise/devise_authy/verify_authy_installation.html.erb 156 | 157 | ### Request a phone call 158 | 159 | The default views come with a button to force a request for an SMS message. You can also add a button that will request a phone call instead. Simply add the helper method to your view: 160 | 161 | <%= authy_request_phone_call_link %> 162 | 163 | ## Custom Redirect Paths (eg. using modules) 164 | 165 | If you want to customise the redirects you can override them within your own controller like this: 166 | 167 | ```ruby 168 | class MyCustomModule::DeviseAuthyController < Devise::DeviseAuthyController 169 | 170 | protected 171 | def after_authy_enabled_path_for(resource) 172 | my_own_path 173 | end 174 | 175 | def after_authy_verified_path_for(resource) 176 | my_own_path 177 | end 178 | 179 | def after_authy_disabled_path_for(resource) 180 | my_own_path 181 | end 182 | 183 | def invalid_resource_path 184 | my_own_path 185 | end 186 | end 187 | ``` 188 | 189 | And tell the router to use this controller 190 | 191 | ```ruby 192 | devise_for :users, controllers: {devise_authy: 'my_custom_module/devise_authy'} 193 | ``` 194 | 195 | ## I18n 196 | 197 | The install generator also copies a `Devise Authy` i18n file which you can find at: 198 | 199 | config/locales/devise.authy.en.yml 200 | 201 | ## Session variables 202 | 203 | If you want to know if the user is signed in using Two-Factor authentication, 204 | you can use the following session variable: 205 | 206 | ```ruby 207 | session["#{resource_name}_authy_token_checked"] 208 | 209 | # Eg. 210 | session["user_authy_token_checked"] 211 | ``` 212 | 213 | ## OneTouch support 214 | 215 | To enable [Authy push authentication](https://www.twilio.com/authy/features/push), you need to modify the Devise config file `config/initializers/devise.rb` and add configuration: 216 | 217 | ``` 218 | config.authy_enable_onetouch = true 219 | ``` 220 | 221 | ## Generic authenticator token support 222 | 223 | Authy supports other authenticator apps by providing a QR code that your users can scan. 224 | 225 | > **To use this feature, you need to enable it in your [Twilio Console](https://www.twilio.com/console/authy/applications)** 226 | 227 | Once you have enabled generic authenticator tokens, you can enable this in devise-authy by modifying the Devise config file `config/initializers/devise.rb` and adding the configuration: 228 | 229 | ``` 230 | config.authy_enable_qr_code = true 231 | ``` 232 | 233 | This will display a QR code on the verification screen (you still need to take a user's phone number and country code). If you have implemented your own views, the QR code URL is available on the verification page as `@authy_qr_code`. 234 | 235 | ## Rails 5 CSRF protection 236 | 237 | In Rails 5 `protect_from_forgery` is no longer prepended to the `before_action` chain. If you call `authenticate_user` before `protect_from_forgery` your request will result in a "Can't verify CSRF token authenticity" error. 238 | 239 | To remedy this, add `prepend: true` to your `protect_from_forgery` call, like in this example from the [Authy Devise demo app](https://github.com/twilio/authy-devise-demo): 240 | 241 | ```ruby 242 | class ApplicationController < ActionController::Base 243 | protect_from_forgery with: :exception, prepend: true 244 | end 245 | ``` 246 | 247 | ## Running Tests 248 | 249 | Run the following command: 250 | 251 | ```bash 252 | $ bundle exec rspec 253 | ``` 254 | 255 | ## Notice: Twilio Authy API’s Sandbox feature will stop working on Sep 30, 2021 256 | Twilio is discontinuing the Authy API’s Sandbox, a feature that allows customers to run continuous integration tests against a mock Authy API for free. The Sandbox is no longer being maintained, so we will be taking the final deprecation step of shutting it down on September 30, 2021. The rest of the Authy API product will continue working as-is. 257 | 258 | This repo previously used the sandbox API as part of the test suite, but that has been since removed. 259 | 260 | You will only be affected if you are using the sandbox API in your own application or test suite. 261 | 262 | For more information please read this article on [how we are discontinuing the Twilio Authy sandbox API](https://support.authy.com/hc/en-us/articles/1260803396889-Notice-Twilio-Authy-API-s-Sandbox-feature-will-stop-working-on-Sep-30-2021). 263 | 264 | ## Copyright 265 | 266 | Copyright (c) 2012-2021 Authy Inc. See LICENSE.txt for further details. 267 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'rubygems' 4 | require 'bundler' 5 | require 'bundler/gem_tasks' 6 | begin 7 | Bundler.setup(:default, :development) 8 | rescue Bundler::BundlerError => e 9 | $stderr.puts e.message 10 | $stderr.puts "Run `bundle install` to install missing gems" 11 | exit e.status_code 12 | end 13 | require 'rake' 14 | 15 | require 'rspec/core' 16 | require 'rspec/core/rake_task' 17 | RSpec::Core::RakeTask.new(:spec) do |spec| 18 | spec.pattern = FileList['spec/**/*_spec.rb'] 19 | end 20 | 21 | RSpec::Core::RakeTask.new(:rcov) do |spec| 22 | spec.pattern = 'spec/**/*_spec.rb' 23 | spec.rcov = true 24 | end 25 | 26 | task :default => :spec 27 | 28 | require 'yard' 29 | YARD::Rake::YardocTask.new 30 | -------------------------------------------------------------------------------- /app/assets/javascripts/devise_authy.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | $('a#authy-request-sms-link').unbind('ajax:success'); 3 | $('a#authy-request-sms-link').bind('ajax:success', function(evt, data, status, xhr) { 4 | alert(data.message); 5 | }); 6 | 7 | $('a#authy-request-phone-call-link').unbind('ajax:success'); 8 | $('a#authy-request-phone-call-link').bind('ajax:success', function(evt, data, status, xhr) { 9 | alert(data.message); 10 | }); 11 | }); 12 | 13 | -------------------------------------------------------------------------------- /app/assets/stylesheets/devise_authy.css: -------------------------------------------------------------------------------- 1 | .devise_authy { 2 | margin-left: auto; 3 | margin-right: auto; 4 | width: 350px; 5 | } 6 | 7 | .authy-form legend { 8 | display: block; 9 | width: 100%; 10 | padding: 0; 11 | margin-bottom: 20px; 12 | font-size: 21px; 13 | line-height: 40px; 14 | color: #333; 15 | border-bottom: 1px solid #E5E5E5; 16 | } 17 | 18 | .authy-form label, 19 | .authy-form input, 20 | .authy-form button { 21 | font-size: 14px; 22 | font-weight: normal; 23 | line-height: 20px; 24 | padding: 8px; 25 | margin: 8px; 26 | } 27 | -------------------------------------------------------------------------------- /app/assets/stylesheets/devise_authy.sass: -------------------------------------------------------------------------------- 1 | .devise_authy 2 | margin-left: auto 3 | margin-right: auto 4 | width: 350px 5 | 6 | .authy-form 7 | legend 8 | display: block 9 | width: 100% 10 | padding: 0 11 | margin-bottom: 20px 12 | font-size: 21px 13 | line-height: 40px 14 | color: #333 15 | border-bottom: 1px solid #E5E5E5 16 | 17 | label, 18 | input, 19 | button 20 | font-size: 14px 21 | font-weight: normal 22 | line-height: 20px 23 | padding: 8px 24 | margin: 8px 25 | -------------------------------------------------------------------------------- /app/controllers/devise/devise_authy_controller.rb: -------------------------------------------------------------------------------- 1 | class Devise::DeviseAuthyController < DeviseController 2 | prepend_before_action :find_resource, :only => [ 3 | :request_phone_call, :request_sms 4 | ] 5 | prepend_before_action :find_resource_and_require_password_checked, :only => [ 6 | :GET_verify_authy, :POST_verify_authy, :GET_authy_onetouch_status 7 | ] 8 | 9 | prepend_before_action :check_resource_has_authy_id, :only => [ 10 | :GET_verify_authy_installation, :POST_verify_authy_installation 11 | ] 12 | 13 | prepend_before_action :check_resource_not_authy_enabled, :only => [ 14 | :GET_verify_authy_installation, :POST_verify_authy_installation 15 | ] 16 | 17 | prepend_before_action :authenticate_scope!, :only => [ 18 | :GET_enable_authy, :POST_enable_authy, :GET_verify_authy_installation, 19 | :POST_verify_authy_installation, :POST_disable_authy 20 | ] 21 | 22 | include Devise::Controllers::Helpers 23 | 24 | def GET_verify_authy 25 | if resource_class.authy_enable_onetouch 26 | approval_request = send_one_touch_request(@resource.authy_id)['approval_request'] 27 | @onetouch_uuid = approval_request['uuid'] if approval_request.present? 28 | end 29 | render :verify_authy 30 | end 31 | 32 | # verify 2fa 33 | def POST_verify_authy 34 | token = Authy::API.verify({ 35 | :id => @resource.authy_id, 36 | :token => params[:token], 37 | :force => true 38 | }) 39 | 40 | if token.ok? 41 | remember_device(@resource.id) if params[:remember_device].to_i == 1 42 | remember_user 43 | record_authy_authentication 44 | respond_with resource, :location => after_sign_in_path_for(@resource) 45 | else 46 | handle_invalid_token :verify_authy, :invalid_token 47 | end 48 | end 49 | 50 | # enable 2fa 51 | def GET_enable_authy 52 | if resource.authy_id.blank? || !resource.authy_enabled 53 | render :enable_authy 54 | else 55 | set_flash_message(:notice, :already_enabled) 56 | redirect_to after_authy_enabled_path_for(resource) 57 | end 58 | end 59 | 60 | def POST_enable_authy 61 | @authy_user = Authy::API.register_user( 62 | :email => resource.email, 63 | :cellphone => params[:cellphone], 64 | :country_code => params[:country_code] 65 | ) 66 | 67 | if @authy_user.ok? 68 | resource.authy_id = @authy_user.id 69 | if resource.save 70 | redirect_to [resource_name, :verify_authy_installation] and return 71 | else 72 | set_flash_message(:error, :not_enabled) 73 | redirect_to after_authy_enabled_path_for(resource) and return 74 | end 75 | else 76 | set_flash_message(:error, :not_enabled) 77 | render :enable_authy 78 | end 79 | end 80 | 81 | # Disable 2FA 82 | def POST_disable_authy 83 | authy_id = resource.authy_id 84 | resource.assign_attributes(:authy_enabled => false, :authy_id => nil) 85 | resource.save(:validate => false) 86 | 87 | other_resource = resource.class.find_by(:authy_id => authy_id) 88 | if other_resource 89 | # If another resource has the same authy_id, do not delete the user from 90 | # the API. 91 | forget_device 92 | set_flash_message(:notice, :disabled) 93 | else 94 | response = Authy::API.delete_user(:id => authy_id) 95 | if response.ok? 96 | forget_device 97 | set_flash_message(:notice, :disabled) 98 | else 99 | # If deleting the user from the API fails, set everything back to what 100 | # it was before. 101 | # I'm not sure this is a good idea, but it was existing behaviour. 102 | # Could be changed in a major version bump. 103 | resource.assign_attributes(:authy_enabled => true, :authy_id => authy_id) 104 | resource.save(:validate => false) 105 | set_flash_message(:error, :not_disabled) 106 | end 107 | end 108 | redirect_to after_authy_disabled_path_for(resource) 109 | end 110 | 111 | def GET_verify_authy_installation 112 | if resource_class.authy_enable_qr_code 113 | response = Authy::API.request_qr_code(id: resource.authy_id) 114 | @authy_qr_code = response.qr_code 115 | end 116 | render :verify_authy_installation 117 | end 118 | 119 | def POST_verify_authy_installation 120 | token = Authy::API.verify({ 121 | :id => self.resource.authy_id, 122 | :token => params[:token], 123 | :force => true 124 | }) 125 | 126 | self.resource.authy_enabled = token.ok? 127 | 128 | if token.ok? && self.resource.save 129 | remember_device(@resource.id) if params[:remember_device].to_i == 1 130 | record_authy_authentication 131 | set_flash_message(:notice, :enabled) 132 | redirect_to after_authy_verified_path_for(resource) 133 | else 134 | if resource_class.authy_enable_qr_code 135 | response = Authy::API.request_qr_code(id: resource.authy_id) 136 | @authy_qr_code = response.qr_code 137 | end 138 | handle_invalid_token :verify_authy_installation, :not_enabled 139 | end 140 | end 141 | 142 | def GET_authy_onetouch_status 143 | response = Authy::OneTouch.approval_request_status(:uuid => params[:onetouch_uuid]) 144 | status = response.dig('approval_request', 'status') 145 | case status 146 | when 'pending' 147 | head 202 148 | when 'approved' 149 | remember_device(@resource.id) if params[:remember_device].to_i == 1 150 | remember_user 151 | record_authy_authentication 152 | render json: { redirect: after_sign_in_path_for(@resource) } 153 | when 'denied' 154 | head :unauthorized 155 | else 156 | head :internal_server_error 157 | end 158 | end 159 | 160 | def request_phone_call 161 | unless @resource 162 | render :json => { :sent => false, :message => "User couldn't be found." } 163 | return 164 | end 165 | 166 | response = Authy::API.request_phone_call(:id => @resource.authy_id, :force => true) 167 | render :json => { :sent => response.ok?, :message => response.message } 168 | end 169 | 170 | def request_sms 171 | if !@resource 172 | render :json => {:sent => false, :message => "User couldn't be found."} 173 | return 174 | end 175 | 176 | response = Authy::API.request_sms(:id => @resource.authy_id, :force => true) 177 | render :json => {:sent => response.ok?, :message => response.message} 178 | end 179 | 180 | private 181 | 182 | def authenticate_scope! 183 | send(:"authenticate_#{resource_name}!", :force => true) 184 | self.resource = send("current_#{resource_name}") 185 | @resource = resource 186 | end 187 | 188 | def find_resource 189 | @resource = send("current_#{resource_name}") 190 | 191 | if @resource.nil? 192 | @resource = resource_class.find_by_id(session["#{resource_name}_id"]) 193 | end 194 | end 195 | 196 | def find_resource_and_require_password_checked 197 | find_resource 198 | 199 | if @resource.nil? || session[:"#{resource_name}_password_checked"].to_s != "true" 200 | redirect_to invalid_resource_path 201 | end 202 | end 203 | 204 | def check_resource_has_authy_id 205 | redirect_to [resource_name, :enable_authy] if !resource.authy_id 206 | end 207 | 208 | def check_resource_not_authy_enabled 209 | if resource.authy_id && resource.authy_enabled 210 | redirect_to after_authy_verified_path_for(resource) 211 | end 212 | end 213 | 214 | protected 215 | 216 | def after_authy_enabled_path_for(resource) 217 | root_path 218 | end 219 | 220 | def after_authy_verified_path_for(resource) 221 | after_authy_enabled_path_for(resource) 222 | end 223 | 224 | def after_authy_disabled_path_for(resource) 225 | root_path 226 | end 227 | 228 | def invalid_resource_path 229 | root_path 230 | end 231 | 232 | def handle_invalid_token(view, error_message) 233 | if @resource.respond_to?(:invalid_authy_attempt!) && @resource.invalid_authy_attempt! 234 | after_account_is_locked 235 | else 236 | set_flash_message(:error, error_message) 237 | render view 238 | end 239 | end 240 | 241 | def after_account_is_locked 242 | sign_out_and_redirect @resource 243 | end 244 | 245 | def remember_user 246 | if session.delete("#{resource_name}_remember_me") == true && @resource.respond_to?(:remember_me=) 247 | @resource.remember_me = true 248 | end 249 | end 250 | end 251 | -------------------------------------------------------------------------------- /app/controllers/devise_authy/passwords_controller.rb: -------------------------------------------------------------------------------- 1 | class DeviseAuthy::PasswordsController < Devise::PasswordsController 2 | ## 3 | # In the passwords controller a user can update their password using a 4 | # recovery token. If `Devise.sign_in_after_reset_password` is `true` then the 5 | # user is signed in immediately with the 6 | # `Devise::Controllers::SignInOut#sign_in` method. However, if the user has 7 | # 2FA enabled they should enter their second factor before they are signed in. 8 | # 9 | # This method overrides `Devise::Controllers::SignInOut#sign_in` but only 10 | # within the `Devise::PasswordsController`. If the user needs to verify 2FA 11 | # then `sign_in` returns `true`. This short circuits the method before it can 12 | # call `warden.set_user` and log the user in. 13 | # 14 | # The user is redirected to `after_resetting_password_path_for(user)` at which 15 | # point, since the user is not logged in, redirects again to sign in. 16 | # 17 | # This doesn't retain the expected behaviour of 18 | # `Devise.sign_in_after_reset_password`, but is forgivable because this 19 | # shouldn't be an avenue to bypass 2FA. 20 | def sign_in(resource_or_scope, *args) 21 | resource = args.last || resource_or_scope 22 | 23 | if resource.respond_to?(:with_authy_authentication?) && resource.with_authy_authentication?(request) 24 | # Do nothing. Because we need verify the 2FA 25 | true 26 | else 27 | super 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/views/devise/enable_authy.html.erb: -------------------------------------------------------------------------------- 1 |

<%= I18n.t('authy_register_title', scope: 'devise') %>

2 | 3 | <%= enable_authy_form do %> 4 | <%= text_field_tag :country_code, '', :autocomplete => :off, :placeholder => I18n.t('devise.country'), :id => "authy-countries"%> 5 | <%= text_field_tag :cellphone, '', :autocomplete => :off, :placeholder => I18n.t('devise.cellphone'), :id => "authy-cellphone"%> 6 |

<%= submit_tag I18n.t('enable_authy', scope: 'devise') %>

7 | <% end %> -------------------------------------------------------------------------------- /app/views/devise/enable_authy.html.haml: -------------------------------------------------------------------------------- 1 | %h2= I18n.t('authy_register_title', scope: 'devise') 2 | = enable_authy_form do 3 | = text_field_tag :country_code, '', :autocomplete => :off, :placeholder => I18n.t('devise.country'), :id => "authy-countries" 4 | = text_field_tag :cellphone, '', :autocomplete => :off, :placeholder => I18n.t('devise.cellphone'), :id => "authy-cellphone" 5 | %p= submit_tag I18n.t('enable_authy', scope: 'devise') 6 | -------------------------------------------------------------------------------- /app/views/devise/verify_authy.html.erb: -------------------------------------------------------------------------------- 1 |

2 | <%= I18n.t('submit_token_title', scope: 'devise') %> 3 |

4 | 5 | <%= verify_authy_form do %> 6 | <%= I18n.t('submit_token_title', scope: 'devise') %> 7 | <%= label_tag 'authy-token' %> 8 | <%= text_field_tag :token, "", :autocomplete => "one-time-code", :inputmode => "numeric", :pattern => "[0-9]*", :id => 'authy-token' %> 9 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | <%= authy_request_sms_link %> 20 | <%= submit_tag I18n.t('submit_token', scope: 'devise'), :class => 'btn' %> 21 | <% end %> 22 | 23 | <% if @onetouch_uuid %> 24 | 38 | <% end %> 39 | -------------------------------------------------------------------------------- /app/views/devise/verify_authy.html.haml: -------------------------------------------------------------------------------- 1 | %h2= I18n.t('authy_register_title', scope: 'devise') 2 | 3 | = verify_authy_form do 4 | %legend= I18n.t('submit_token_title', scope: 'devise') 5 | = hidden_field_tag :"#{resource_name}_id", @resource.id 6 | = label_tag 'authy-token' 7 | = text_field_tag :token, "", :autocomplete => "one-time-code", :inputmode => "numeric", :pattern => "[0-9]*", :id => 'authy-token' 8 | %label 9 | = check_box_tag :remember_device 10 | %span= I18n.t('remember_device', scope: 'devise') 11 | 12 | / Help Tooltip 13 | / You need to configure a help message. 14 | / See documentation: https://github.com/authy/authy-form-helpers#help-tooltip 15 | / = link_to '?', '#', :id => 'authy-help', :'data-message' => 'a message' 16 | 17 | = authy_request_sms_link 18 | = submit_tag I18n.t('submit_token', scope: 'devise'), :class => 'btn' 19 | 20 | - if @onetouch_uuid 21 | :javascript 22 | (function(){ 23 | var onetouchInterval = setInterval(function(){ 24 | var onetouchRequest = new XMLHttpRequest(); 25 | var rememberDevice = document.getElementById("remember_device").checked ? '1' : '0'; 26 | onetouchRequest.addEventListener("load", function(){ 27 | if(this.status != 202) clearInterval(onetouchInterval); 28 | if(this.status == 200) window.location = JSON.parse(this.responseText).redirect; 29 | }); 30 | onetouchRequest.open("GET", "#{polymorphic_path [resource_name, :authy_onetouch_status]}?remember_device="+rememberDevice+"&onetouch_uuid=#{@onetouch_uuid}"); 31 | onetouchRequest.send(); 32 | }, 3000); 33 | })(); 34 | -------------------------------------------------------------------------------- /app/views/devise/verify_authy_installation.html.erb: -------------------------------------------------------------------------------- 1 |

<%= I18n.t('authy_verify_installation_title', scope: 'devise') %>

2 | 3 | <% if @authy_qr_code %> 4 | <%= image_tag @authy_qr_code, :size => '256x256', :alt => I18n.t('authy_qr_code_alt', scope: 'devise') %> 5 |

<%= I18n.t('authy_qr_code_instructions', scope: 'devise') %>

6 | <% end %> 7 | 8 | <%= verify_authy_installation_form do %> 9 | <%= I18n.t('submit_token_title', scope: 'devise') %> 10 | <%= label_tag :token %> 11 | <%= text_field_tag :token, "", :autocomplete => "one-time-code", :inputmode => "numeric", :pattern => "[0-9]*", :id => 'authy-token' %> 12 | 16 | <%= authy_request_sms_link %> 17 | <%= submit_tag I18n.t('enable_my_account', scope: 'devise'), :class => 'btn' %> 18 | <% end %> -------------------------------------------------------------------------------- /app/views/devise/verify_authy_installation.html.haml: -------------------------------------------------------------------------------- 1 | %h2= I18n.t('authy_verify_installation_title', scope: 'devise') 2 | 3 | - if @authy_qr_code 4 | = image_tag @authy_qr_code, :size => '256x256', :alt => I18n.t('authy_qr_code_alt', scope: 'devise') 5 | %p= I18n.t('authy_qr_code_instructions', scope: 'devise') 6 | 7 | = verify_authy_installation_form do 8 | %legend= I18n.t('submit_token_title', scope: 'devise') 9 | = label_tag :token 10 | = text_field_tag :token, "", :autocomplete => "one-time-code", :inputmode => "numeric", :pattern => "[0-9]*", :id => 'authy-token' 11 | %label 12 | = check_box_tag :remember_device 13 | %span= I18n.t('remember_device', scope: 'devise') 14 | = authy_request_sms_link 15 | = submit_tag I18n.t('enable_my_account', scope: 'devise'), :class => 'btn' 16 | 17 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rubygems" 4 | require "bundler" 5 | 6 | Bundler.require :default, :development 7 | 8 | Combustion.initialize! :all 9 | run Combustion::Application 10 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | devise: 3 | submit_token: 'Check Token' 4 | submit_token_title: 'Please enter your Authy token:' 5 | authy_register_title: 'Enable Two factor authentication' 6 | enable_authy: 'Enable' 7 | cellphone: 'Enter your cellphone' 8 | country: 'Enter your country' 9 | request_sms: 'Request SMS' 10 | request_phone_call: 'Request phone call' 11 | remember_device: 'Remember Device' 12 | request_to_login: 'Request to Login' 13 | 14 | authy_verify_installation_title: 'Verify your account' 15 | enable_my_account: 'Enable my account' 16 | 17 | authy_qr_code_alt: 'QR code for scanning with your authenticator app.' 18 | authy_qr_code_instructions: 'Scan this QR code with your authenticator application and enter the code below.' 19 | 20 | devise_authy: 21 | user: 22 | enabled: 'Two factor authentication was enabled' 23 | not_enabled: 'Something went wrong while enabling two factor authentication' 24 | disabled: 'Two factor authentication was disabled' 25 | not_disabled: 'Something went wrong while disabling two factor authentication' 26 | signed_in: 'Signed in with Authy successfully.' 27 | already_enabled: 'Two factor authentication is already enabled.' 28 | invalid_token: 'The entered token is invalid' 29 | -------------------------------------------------------------------------------- /devise-authy.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path("../lib", __FILE__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require "devise-authy/version" 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = "devise-authy" 9 | spec.version = DeviseAuthy::VERSION 10 | spec.authors = ["Authy Inc."] 11 | spec.email = ["support@authy.com"] 12 | 13 | spec.summary = %q{Deprecated: please see README for details} 14 | spec.description = %q{Authy plugin to add two factor authentication to Devise. This gem is deprecated, please see the README for details.} 15 | spec.homepage = "https://github.com/twilio/authy-devise" 16 | spec.license = "MIT" 17 | 18 | spec.metadata = { 19 | "bug_tracker_uri" => "https://github.com/twilio/authy-devise/issues", 20 | "change_log_uri" => "https://github.com/twilio/authy-devise/blob/master/CHANGELOG.md", 21 | "documentation_uri" => "https://github.com/twilio/authy-devise", 22 | "homepage_uri" => "https://github.com/twilio/authy-devise", 23 | "source_code_uri" => "https://github.com/twilio/authy-devise" 24 | } 25 | 26 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 27 | f.match(%r{^(test|spec|features)/}) 28 | end 29 | spec.require_paths = ["lib"] 30 | 31 | spec.add_dependency "devise", ">= 4.0.0" 32 | spec.add_dependency "authy", "~> 3.0" 33 | 34 | spec.add_development_dependency "appraisal", "~> 2.2" 35 | spec.add_development_dependency "bundler", ">= 1.16" 36 | spec.add_development_dependency "rake" 37 | spec.add_development_dependency "combustion", "~> 1.1" 38 | spec.add_development_dependency "rspec", "~> 3.0" 39 | spec.add_development_dependency "rspec-rails" 40 | spec.add_development_dependency "rails-controller-testing", "~> 1.0" 41 | spec.add_development_dependency "yard", "~> 0.9.11" 42 | spec.add_development_dependency "rdoc", "~> 4.3.0" 43 | spec.add_development_dependency "simplecov", "~> 0.17.1" 44 | spec.add_development_dependency "webmock", "~> 3.11.0" 45 | spec.add_development_dependency "rails", ">= 5" 46 | spec.add_development_dependency "sqlite3" 47 | spec.add_development_dependency "generator_spec" 48 | spec.add_development_dependency "database_cleaner", "~> 1.7" 49 | spec.add_development_dependency "factory_bot_rails", "~> 5.1.1" 50 | end 51 | -------------------------------------------------------------------------------- /gemfiles/.bundle/config: -------------------------------------------------------------------------------- 1 | --- 2 | BUNDLE_RETRY: "1" 3 | -------------------------------------------------------------------------------- /gemfiles/rails_5_2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 5.2.0" 6 | gem "sqlite3", "~> 1.3.13" 7 | 8 | group :development, :test do 9 | gem "factory_girl_rails", require: false 10 | gem "rspec-rails", "~>4.0.0.beta3", require: false 11 | gem "database_cleaner", require: false 12 | end 13 | 14 | gemspec path: "../" 15 | -------------------------------------------------------------------------------- /gemfiles/rails_6.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 6.0.0" 6 | gem "sqlite3", "~> 1.4" 7 | gem "net-smtp" 8 | 9 | group :development, :test do 10 | gem "factory_girl_rails", require: false 11 | gem "rspec-rails", "~>4.0.0.beta3", require: false 12 | gem "database_cleaner", require: false 13 | end 14 | 15 | gemspec path: "../" 16 | -------------------------------------------------------------------------------- /lib/devise-authy.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/concern' 2 | require 'active_support/core_ext/integer/time' 3 | require 'devise' 4 | require 'authy' 5 | 6 | module Devise 7 | mattr_accessor :authy_remember_device, :authy_enable_onetouch, :authy_enable_qr_code 8 | @@authy_remember_device = 1.month 9 | @@authy_enable_onetouch = false 10 | @@authy_enable_qr_code = false 11 | end 12 | 13 | module DeviseAuthy 14 | autoload :Mapping, 'devise-authy/mapping' 15 | 16 | module Controllers 17 | autoload :Passwords, 'devise-authy/controllers/passwords' 18 | autoload :Helpers, 'devise-authy/controllers/helpers' 19 | end 20 | 21 | module Views 22 | autoload :Helpers, 'devise-authy/controllers/view_helpers' 23 | end 24 | end 25 | 26 | require 'devise-authy/routes' 27 | require 'devise-authy/rails' 28 | require 'devise-authy/models/authy_authenticatable' 29 | require 'devise-authy/models/authy_lockable' 30 | require 'devise-authy/version' 31 | 32 | Authy.user_agent = "DeviseAuthy/#{DeviseAuthy::VERSION} - #{Authy.user_agent}" 33 | 34 | Devise.add_module :authy_authenticatable, :model => 'devise-authy/models/authy_authenticatable', :controller => :devise_authy, :route => :authy 35 | Devise.add_module :authy_lockable, :model => 'devise-authy/models/authy_lockable' 36 | 37 | warn "DEPRECATION WARNING: The authy-devise library is no longer actively maintained. The Authy API is being replaced by the Twilio Verify API. Please see the README for more details." -------------------------------------------------------------------------------- /lib/devise-authy/controllers/helpers.rb: -------------------------------------------------------------------------------- 1 | module DeviseAuthy 2 | module Controllers 3 | module Helpers 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | before_action :check_request_and_redirect_to_verify_token, :if => :is_signing_in? 8 | end 9 | 10 | private 11 | 12 | def remember_device(id) 13 | cookies.signed[:remember_device] = { 14 | :value => {expires: Time.now.to_i, id: id}.to_json, 15 | :secure => !(Rails.env.test? || Rails.env.development?), 16 | :httponly => !(Rails.env.test? || Rails.env.development?), 17 | :expires => resource_class.authy_remember_device.from_now 18 | } 19 | end 20 | 21 | def forget_device 22 | cookies.delete :remember_device 23 | end 24 | 25 | def require_token? 26 | id = warden.session(resource_name)[:id] 27 | cookie = cookies.signed[:remember_device] 28 | return true if cookie.blank? 29 | 30 | # require token for old cookies which just have expiration time and no id 31 | return true if cookie.to_s =~ %r{\A\d+\z} 32 | 33 | cookie = JSON.parse(cookie) rescue "" 34 | return cookie.blank? || (Time.now.to_i - cookie['expires'].to_i) > \ 35 | resource_class.authy_remember_device.to_i || cookie['id'] != id 36 | end 37 | 38 | def is_devise_sessions_controller? 39 | self.class == Devise::SessionsController || self.class.ancestors.include?(Devise::SessionsController) 40 | end 41 | 42 | def is_signing_in? 43 | if devise_controller? && 44 | is_devise_sessions_controller? && 45 | self.action_name == "create" 46 | return true 47 | end 48 | 49 | return false 50 | end 51 | 52 | def check_request_and_redirect_to_verify_token 53 | if signed_in?(resource_name) && 54 | warden.session(resource_name)[:with_authy_authentication] && 55 | require_token? 56 | # login with 2fa 57 | id = warden.session(resource_name)[:id] 58 | 59 | remember_me = (params.fetch(resource_name, {})[:remember_me].to_s == "1") 60 | return_to = session["#{resource_name}_return_to"] 61 | sign_out 62 | 63 | session["#{resource_name}_id"] = id 64 | # this is safe to put in the session because the cookie is signed 65 | session["#{resource_name}_password_checked"] = true 66 | session["#{resource_name}_remember_me"] = remember_me 67 | session["#{resource_name}_return_to"] = return_to if return_to 68 | 69 | redirect_to verify_authy_path_for(resource_name) 70 | return 71 | end 72 | end 73 | 74 | def verify_authy_path_for(resource_or_scope = nil) 75 | scope = Devise::Mapping.find_scope!(resource_or_scope) 76 | send(:"#{scope}_verify_authy_path") 77 | end 78 | 79 | def send_one_touch_request(authy_id) 80 | Authy::OneTouch.send_approval_request(id: authy_id, message: I18n.t('request_to_login', scope: 'devise')) 81 | end 82 | 83 | def record_authy_authentication 84 | @resource.update_attribute(:last_sign_in_with_authy, DateTime.now) 85 | session["#{resource_name}_authy_token_checked"] = true 86 | sign_in(resource_name, @resource) 87 | set_flash_message(:notice, :signed_in) if is_navigational_format? 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/devise-authy/controllers/view_helpers.rb: -------------------------------------------------------------------------------- 1 | module DeviseAuthy 2 | module Views 3 | module Helpers 4 | def authy_request_phone_call_link(opts = {}) 5 | title = opts.delete(:title) do 6 | I18n.t('request_phone_call', scope: 'devise') 7 | end 8 | opts = { 9 | :id => "authy-request-phone-call-link", 10 | :method => :post, 11 | :remote => true 12 | }.merge(opts) 13 | 14 | link_to( 15 | title, 16 | url_for([resource_name.to_sym, :request_phone_call]), 17 | opts 18 | ) 19 | end 20 | 21 | def authy_request_sms_link(opts = {}) 22 | title = opts.delete(:title) do 23 | I18n.t('request_sms', scope: 'devise') 24 | end 25 | opts = { 26 | :id => "authy-request-sms-link", 27 | :method => :post, 28 | :remote => true 29 | }.merge(opts) 30 | 31 | link_to( 32 | title, 33 | url_for([resource_name.to_sym, :request_sms]), 34 | opts 35 | ) 36 | end 37 | 38 | def verify_authy_form(opts = {}, &block) 39 | opts = default_opts.merge(:id => 'devise_authy').merge(opts) 40 | form_tag([resource_name.to_sym, :verify_authy], opts) do 41 | buffer = hidden_field_tag(:"#{resource_name}_id", @resource.id) 42 | buffer << capture(&block) 43 | end 44 | end 45 | 46 | def enable_authy_form(opts = {}, &block) 47 | opts = default_opts.merge(opts) 48 | form_tag([resource_name.to_sym, :enable_authy], opts) do 49 | capture(&block) 50 | end 51 | end 52 | 53 | def verify_authy_installation_form(opts = {}, &block) 54 | opts = default_opts.merge(opts) 55 | form_tag([resource_name.to_sym, :verify_authy_installation], opts) do 56 | capture(&block) 57 | end 58 | end 59 | 60 | private 61 | 62 | def default_opts 63 | { :class => 'authy-form', :method => :post } 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/devise-authy/hooks/authy_authenticatable.rb: -------------------------------------------------------------------------------- 1 | Warden::Manager.after_authentication do |user, auth, options| 2 | if user.respond_to?(:with_authy_authentication?) 3 | if auth.session(options[:scope])[:with_authy_authentication] = user.with_authy_authentication?(auth.request) 4 | auth.session(options[:scope])[:id] = user.id 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/devise-authy/mapping.rb: -------------------------------------------------------------------------------- 1 | module DeviseAuthy 2 | module Mapping 3 | private 4 | def default_controllers(options) 5 | options[:controllers] ||= {} 6 | options[:controllers][:passwords] ||= "devise_authy/passwords" 7 | super 8 | end 9 | 10 | def default_path_names(options) 11 | options[:path_names] ||= {} 12 | options[:path_names][:request_sms] ||= 'request-sms' 13 | options[:path_names][:request_phone_call] ||= 'request-phone-call' 14 | super 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/devise-authy/models/authy_authenticatable.rb: -------------------------------------------------------------------------------- 1 | require 'devise-authy/hooks/authy_authenticatable' 2 | module Devise 3 | module Models 4 | module AuthyAuthenticatable 5 | extend ActiveSupport::Concern 6 | 7 | def with_authy_authentication?(request) 8 | if self.authy_id.present? && self.authy_enabled 9 | return true 10 | end 11 | 12 | return false 13 | end 14 | 15 | module ClassMethods 16 | def find_by_authy_id(authy_id) 17 | where(authy_id: authy_id).first 18 | end 19 | 20 | Devise::Models.config(self, :authy_remember_device, :authy_enable_onetouch, :authy_enable_qr_code) 21 | end 22 | end 23 | end 24 | end 25 | 26 | -------------------------------------------------------------------------------- /lib/devise-authy/models/authy_lockable.rb: -------------------------------------------------------------------------------- 1 | module Devise 2 | 3 | module Models 4 | 5 | # Handles blocking a user access after a certain number of attempts. 6 | # Requires proper configuration of the Devise::Models::Lockable module. 7 | # 8 | module AuthyLockable 9 | 10 | extend ActiveSupport::Concern 11 | 12 | # Public: Determine if this is a lockable resource, via Devise::Models::Lockable. 13 | # Returns true 14 | # Raises an error if the Devise::Models::Lockable module is not configured. 15 | def lockable? 16 | raise 'Devise lockable extension required' unless respond_to? :lock_access! 17 | Devise.lock_strategy == :failed_attempts 18 | end 19 | 20 | # Public: Handle a failed 2FA attempt. If the resource is lockable via 21 | # Devise::Models::Lockable module then enforce that setting. 22 | # 23 | # Returns true if the user is locked out. 24 | def invalid_authy_attempt! 25 | return false unless lockable? 26 | 27 | self.failed_attempts ||= 0 28 | self.failed_attempts += 1 29 | 30 | if attempts_exceeded? 31 | lock_access! unless access_locked? 32 | true 33 | else 34 | save validate: false 35 | false 36 | end 37 | end 38 | 39 | end 40 | 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /lib/devise-authy/rails.rb: -------------------------------------------------------------------------------- 1 | module DeviseAuthy 2 | class Engine < ::Rails::Engine 3 | ActiveSupport.on_load(:action_controller) do 4 | include DeviseAuthy::Controllers::Helpers 5 | end 6 | ActiveSupport.on_load(:action_view) do 7 | include DeviseAuthy::Views::Helpers 8 | end 9 | 10 | # extend mapping with after_initialize because it's not reloaded 11 | config.after_initialize do 12 | Devise::Mapping.send :prepend, DeviseAuthy::Mapping 13 | end 14 | end 15 | end 16 | 17 | -------------------------------------------------------------------------------- /lib/devise-authy/routes.rb: -------------------------------------------------------------------------------- 1 | module ActionDispatch::Routing 2 | class Mapper 3 | protected 4 | 5 | def devise_authy(mapping, controllers) 6 | match "/#{mapping.path_names[:verify_authy]}", :controller => controllers[:devise_authy], :action => :GET_verify_authy, :as => :verify_authy, :via => :get 7 | match "/#{mapping.path_names[:verify_authy]}", :controller => controllers[:devise_authy], :action => :POST_verify_authy, :as => nil, :via => :post 8 | 9 | match "/#{mapping.path_names[:enable_authy]}", :controller => controllers[:devise_authy], :action => :GET_enable_authy, :as => :enable_authy, :via => :get 10 | match "/#{mapping.path_names[:enable_authy]}", :controller => controllers[:devise_authy], :action => :POST_enable_authy, :as => nil, :via => :post 11 | 12 | match "/#{mapping.path_names[:disable_authy]}", :controller => controllers[:devise_authy], :action => :POST_disable_authy, :as => :disable_authy, :via => :post 13 | 14 | match "/#{mapping.path_names[:verify_authy_installation]}", :controller => controllers[:devise_authy], :action => :GET_verify_authy_installation, :as => :verify_authy_installation, :via => :get 15 | match "/#{mapping.path_names[:verify_authy_installation]}", :controller => controllers[:devise_authy], :action => :POST_verify_authy_installation, :as => nil, :via => :post 16 | 17 | match "/#{mapping.path_names[:authy_onetouch_status]}", :controller => controllers[:devise_authy], :action => :GET_authy_onetouch_status, as: :authy_onetouch_status, via: :get 18 | 19 | match "/#{mapping.path_names[:request_sms]}", :controller => controllers[:devise_authy], :action => :request_sms, :as => :request_sms, :via => :post 20 | match "/#{mapping.path_names[:request_phone_call]}", :controller => controllers[:devise_authy], :action => :request_phone_call, :as => :request_phone_call, :via => :post 21 | end 22 | end 23 | end 24 | 25 | -------------------------------------------------------------------------------- /lib/devise-authy/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DeviseAuthy 4 | VERSION = '2.3.1' 5 | end 6 | -------------------------------------------------------------------------------- /lib/generators/active_record/devise_authy_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators/active_record' 2 | 3 | module ActiveRecord 4 | module Generators 5 | class DeviseAuthyGenerator < ActiveRecord::Generators::Base 6 | source_root File.expand_path("../templates", __FILE__) 7 | 8 | def copy_devise_migration 9 | migration_template "migration.rb", "db/migrate/devise_authy_add_to_#{table_name}.rb", migration_version: migration_version 10 | end 11 | 12 | private 13 | 14 | def versioned_migrations? 15 | Rails::VERSION::MAJOR >= 5 16 | end 17 | 18 | def migration_version 19 | "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]" if versioned_migrations? 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/generators/active_record/templates/migration.rb: -------------------------------------------------------------------------------- 1 | class DeviseAuthyAddTo<%= table_name.camelize %> < ActiveRecord::Migration<%= migration_version %> 2 | def self.up 3 | change_table :<%= table_name %> do |t| 4 | t.string :authy_id 5 | t.datetime :last_sign_in_with_authy 6 | t.boolean :authy_enabled, :default => false 7 | end 8 | 9 | add_index :<%= table_name %>, :authy_id 10 | end 11 | 12 | def self.down 13 | change_table :<%= table_name %> do |t| 14 | t.remove :authy_id, :last_sign_in_with_authy, :authy_enabled 15 | end 16 | end 17 | end 18 | 19 | -------------------------------------------------------------------------------- /lib/generators/devise_authy/devise_authy_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DeviseAuthy 4 | module Generators 5 | class DeviseAuthyGenerator < Rails::Generators::NamedBase 6 | namespace "devise_authy" 7 | 8 | desc "Add :authy_authenticatable directive in the given model, plus accessors. Also generate migration for ActiveRecord" 9 | 10 | def inject_devise_authy_content 11 | path = File.join(destination_root, "app", "models", "#{file_path}.rb") 12 | if File.exist?(path) && 13 | !File.read(path).include?("authy_authenticatable") 14 | inject_into_file(path, 15 | "authy_authenticatable, :", 16 | :after => "devise :") 17 | end 18 | 19 | if File.exist?(path) && 20 | !File.read(path).include?(":authy_id") 21 | inject_into_file(path, 22 | ":authy_id, :last_sign_in_with_authy, ", 23 | :after => "attr_accessible ") 24 | end 25 | end 26 | 27 | hook_for :orm 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/generators/devise_authy/install_generator.rb: -------------------------------------------------------------------------------- 1 | require "rails/generators" 2 | 3 | module DeviseAuthy 4 | module Generators 5 | # Install Generator 6 | class InstallGenerator < ::Rails::Generators::Base 7 | source_root File.expand_path("../../templates", __FILE__) 8 | 9 | class_option :haml, :type => :boolean, :required => false, :default => false, :desc => "Generate views in Haml" 10 | class_option :sass, :type => :boolean, :required => false, :default => false, :desc => "Generate stylesheet in Sass" 11 | 12 | desc "Install the devise authy extension" 13 | 14 | def add_configs 15 | inject_into_file "config/initializers/devise.rb", "\n" + 16 | " # ==> Devise Authy Authentication Extension\n" + 17 | " # How long should the user's device be remembered for.\n" + 18 | " # config.authy_remember_device = 1.month\n\n" + 19 | " # Should Authy OneTouch be enabled?\n" + 20 | " # config.authy_enable_onetouch = false\n\n" + 21 | " # Should generating QR codes for other authenticator apps be enabled?\n" + 22 | " # Note: you need to enable this in your Twilio console.\n" + 23 | " # config.authy_enable_qr_code = false\n\n", :after => "Devise.setup do |config|\n" 24 | end 25 | 26 | def add_initializer 27 | initializer("authy.rb") do 28 | "Authy.api_key = ENV[\"AUTHY_API_KEY\"]\n" \ 29 | "Authy.api_uri = \"https://api.authy.com/\"" 30 | end 31 | end 32 | 33 | def copy_locale 34 | copy_file "../../../config/locales/en.yml", "config/locales/devise.authy.en.yml" 35 | end 36 | 37 | def copy_views 38 | if options.haml? 39 | copy_file '../../../app/views/devise/enable_authy.html.haml', 'app/views/devise/devise_authy/enable_authy.html.haml' 40 | copy_file '../../../app/views/devise/verify_authy.html.haml', 'app/views/devise/devise_authy/verify_authy.html.haml' 41 | copy_file '../../../app/views/devise/verify_authy_installation.html.haml', 'app/views/devise/devise_authy/verify_authy_installation.html.haml' 42 | else 43 | copy_file '../../../app/views/devise/enable_authy.html.erb', 'app/views/devise/devise_authy/enable_authy.html.erb' 44 | copy_file '../../../app/views/devise/verify_authy.html.erb', 'app/views/devise/devise_authy/verify_authy.html.erb' 45 | copy_file '../../../app/views/devise/verify_authy_installation.html.erb', 'app/views/devise/devise_authy/verify_authy_installation.html.erb' 46 | end 47 | end 48 | 49 | def copy_assets 50 | if options.sass? 51 | copy_file '../../../app/assets/stylesheets/devise_authy.sass', 'app/assets/stylesheets/devise_authy.sass' 52 | else 53 | copy_file '../../../app/assets/stylesheets/devise_authy.css', 'app/assets/stylesheets/devise_authy.css' 54 | end 55 | copy_file '../../../app/assets/javascripts/devise_authy.js', 'app/assets/javascripts/devise_authy.js' 56 | end 57 | 58 | def inject_assets_in_layout 59 | { 60 | :haml => { 61 | :before => %r{%body\s*$}, 62 | :content => %@ 63 | =javascript_include_tag "https://www.authy.com/form.authy.min.js" 64 | =stylesheet_link_tag "https://www.authy.com/form.authy.min.css" 65 | @ 66 | }, 67 | :erb => { 68 | :before => %r{\s*<\/\s*head\s*>\s*}, 69 | :content => %@ 70 | <%=javascript_include_tag "https://www.authy.com/form.authy.min.js" %> 71 | <%=stylesheet_link_tag "https://www.authy.com/form.authy.min.css" %> 72 | @ 73 | } 74 | }.each do |extension, opts| 75 | file_path = File.join(destination_root, "app", "views", "layouts", "application.html.#{extension}") 76 | if File.exist?(file_path) && !File.read(file_path).include?("form.authy.min.js") 77 | inject_into_file(file_path, opts.delete(:content), opts) 78 | end 79 | end 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /spec/config/routes_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "routes with devise_authy", type: :controller do 4 | describe "with default devise_for" do 5 | it "route to devise_authy#GET_verify_authy" do 6 | expect(get: '/users/verify_authy').to route_to("devise/devise_authy#GET_verify_authy") 7 | end 8 | 9 | it "routes to devise_authy#POST_verify_authy" do 10 | expect(post: '/users/verify_authy').to route_to("devise/devise_authy#POST_verify_authy") 11 | end 12 | 13 | it "routes to devise_authy#GET_enable_authy" do 14 | expect(get: '/users/enable_authy').to route_to("devise/devise_authy#GET_enable_authy") 15 | end 16 | 17 | it "routes to devise_authy#POST_enable_authy" do 18 | expect(post: '/users/enable_authy').to route_to("devise/devise_authy#POST_enable_authy") 19 | end 20 | 21 | it "routes to devise_authy#POST_disable_authy" do 22 | expect(post: '/users/disable_authy').to route_to("devise/devise_authy#POST_disable_authy") 23 | end 24 | 25 | it "route to devise_authy#GET_verify_authy_installation" do 26 | expect(get: '/users/verify_authy_installation').to route_to("devise/devise_authy#GET_verify_authy_installation") 27 | end 28 | 29 | it "routes to devise_authy#POST_verify_authy_installation" do 30 | expect(post: '/users/verify_authy_installation').to route_to("devise/devise_authy#POST_verify_authy_installation") 31 | end 32 | 33 | it "routes to devise_authy#request_sms" do 34 | expect(post: '/users/request-sms').to route_to("devise/devise_authy#request_sms") 35 | end 36 | 37 | it "routes to devise_authy#request_phone_call" do 38 | expect(post: '/users/request-phone-call').to route_to("devise/devise_authy#request_phone_call") 39 | end 40 | 41 | it "routes to devise_authy#GET_authy_onetouch_status" do 42 | expect(get: '/users/authy_onetouch_status').to route_to("devise/devise_authy#GET_authy_onetouch_status") 43 | end 44 | end 45 | 46 | describe "with customised mapping" do 47 | # See routing in spec/internal/config/routes.rb for the mapping 48 | it "updates to new routes set in the mapping" do 49 | expect(get: '/lockable_users/verify-token').to route_to("devise/devise_authy#GET_verify_authy") 50 | expect(post: '/lockable_users/verify-token').to route_to("devise/devise_authy#POST_verify_authy") 51 | expect(get: '/lockable_users/enable-two-factor').to route_to("devise/devise_authy#GET_enable_authy") 52 | expect(post: '/lockable_users/enable-two-factor').to route_to("devise/devise_authy#POST_enable_authy") 53 | expect(get: '/lockable_users/verify-installation').to route_to("devise/devise_authy#GET_verify_authy_installation") 54 | expect(post: '/lockable_users/verify-installation').to route_to("devise/devise_authy#POST_verify_authy_installation") 55 | expect(get: '/lockable_users/onetouch-status').to route_to("devise/devise_authy#GET_authy_onetouch_status") 56 | end 57 | 58 | it "doesn't change routes not in custom mapping" do 59 | expect(post: '/lockable_users/disable_authy').to route_to("devise/devise_authy#POST_disable_authy") 60 | expect(post: '/lockable_users/request-sms').to route_to("devise/devise_authy#request_sms") 61 | expect(post: '/lockable_users/request-phone-call').to route_to("devise/devise_authy#request_phone_call") 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/controllers/devise_authy_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Devise::DeviseAuthyController, type: :controller do 4 | let(:user) { create(:authy_user) } 5 | before(:each) { request.env["devise.mapping"] = Devise.mappings[:user] } 6 | 7 | describe "first step of authentication not complete" do 8 | describe "with no user details in the session" do 9 | describe "#GET_verify_authy" do 10 | it "should redirect to the root_path" do 11 | get :GET_verify_authy 12 | expect(response).to redirect_to(root_path) 13 | end 14 | 15 | it "should not make a OneTouch request" do 16 | expect(Authy::OneTouch).not_to receive(:send_approval_request) 17 | get :GET_verify_authy 18 | end 19 | end 20 | 21 | describe "#POST_verify_authy" do 22 | it "should redirect to the root_path" do 23 | post :POST_verify_authy 24 | expect(response).to redirect_to(root_path) 25 | end 26 | 27 | it "should not verify a token" do 28 | expect(Authy::API).not_to receive(:verify) 29 | post :POST_verify_authy 30 | end 31 | end 32 | 33 | describe "#GET_authy_onetouch_status" do 34 | it "should redirect to the root_path" do 35 | get :GET_authy_onetouch_status 36 | expect(response).to redirect_to(root_path) 37 | end 38 | 39 | it "should not request the one touch status" do 40 | expect(Authy::OneTouch).not_to receive(:approval_request_status) 41 | get :GET_authy_onetouch_status 42 | end 43 | end 44 | end 45 | 46 | describe "without checking the password" do 47 | before(:each) { request.session["user_id"] = user.id } 48 | 49 | describe "#GET_verify_authy" do 50 | it "should redirect to the root_path" do 51 | get :GET_verify_authy 52 | expect(response).to redirect_to(root_path) 53 | end 54 | 55 | it "should not make a OneTouch request" do 56 | expect(Authy::OneTouch).not_to receive(:send_approval_request) 57 | get :GET_verify_authy 58 | end 59 | end 60 | 61 | describe "#POST_verify_authy" do 62 | it "should redirect to the root_path" do 63 | post :POST_verify_authy 64 | expect(response).to redirect_to(root_path) 65 | end 66 | 67 | it "should not verify a token" do 68 | expect(Authy::API).not_to receive(:verify) 69 | post :POST_verify_authy 70 | end 71 | end 72 | 73 | describe "#GET_authy_onetouch_status" do 74 | it "should redirect to the root_path" do 75 | get :GET_authy_onetouch_status 76 | expect(response).to redirect_to(root_path) 77 | end 78 | 79 | it "should not request the one touch status" do 80 | expect(Authy::OneTouch).not_to receive(:approval_request_status) 81 | get :GET_authy_onetouch_status 82 | end 83 | end 84 | end 85 | end 86 | 87 | describe "when the first step of authentication is complete" do 88 | before do 89 | request.session["user_id"] = user.id 90 | request.session["user_password_checked"] = true 91 | end 92 | 93 | describe "GET #verify_authy" do 94 | it "Should render the second step of authentication" do 95 | get :GET_verify_authy 96 | expect(response).to render_template('verify_authy') 97 | end 98 | 99 | it "should not make a OneTouch request" do 100 | expect(Authy::OneTouch).not_to receive(:send_approval_request) 101 | get :GET_verify_authy 102 | end 103 | 104 | describe "when OneTouch is enabled" do 105 | before(:each) do 106 | Devise.authy_enable_onetouch = true 107 | end 108 | 109 | after(:each) do 110 | Devise.authy_enable_onetouch = false 111 | end 112 | 113 | it "should make a OneTouch request and assign the uuid" do 114 | expect(Authy::OneTouch).to receive(:send_approval_request) 115 | .with(id: user.authy_id, message: 'Request to Login') 116 | .and_return('approval_request' => { 'uuid' => 'uuid' }).once 117 | get :GET_verify_authy 118 | expect(assigns[:onetouch_uuid]).to eq('uuid') 119 | end 120 | end 121 | end 122 | 123 | describe "POST #verify_authy" do 124 | let(:verify_success) { double("Authy::Response", :ok? => true) } 125 | let(:verify_failure) { double("Authy::Response", :ok? => false) } 126 | let(:valid_authy_token) { rand(0..999999).to_s.rjust(6, '0') } 127 | let(:invalid_authy_token) { rand(0..999999).to_s.rjust(6, '0') } 128 | 129 | describe "with a valid token" do 130 | before(:each) { 131 | expect(Authy::API).to receive(:verify).with({ 132 | :id => user.authy_id, 133 | :token => valid_authy_token, 134 | :force => true 135 | }).and_return(verify_success) 136 | } 137 | 138 | describe "without remembering" do 139 | before(:each) { 140 | post :POST_verify_authy, params: { :token => valid_authy_token } 141 | } 142 | 143 | it "should log the user in" do 144 | expect(subject.current_user).to eq(user) 145 | expect(session["user_authy_token_checked"]).to be true 146 | end 147 | 148 | it "should set the last_sign_in_with_authy field on the user" do 149 | expect(user.last_sign_in_with_authy).to be_nil 150 | user.reload 151 | expect(user.last_sign_in_with_authy).not_to be_nil 152 | expect(user.last_sign_in_with_authy).to be_within(1).of(Time.zone.now) 153 | end 154 | 155 | it "should redirect to the root_path and set a flash notice" do 156 | expect(response).to redirect_to(root_path) 157 | expect(flash[:notice]).not_to be_nil 158 | expect(flash[:error]).to be nil 159 | end 160 | 161 | it "should not set a remember_device cookie" do 162 | expect(cookies["remember_device"]).to be_nil 163 | end 164 | 165 | it "should not remember the user" do 166 | user.reload 167 | expect(user.remember_created_at).to be nil 168 | end 169 | end 170 | 171 | describe "and remember device selected" do 172 | before(:each) { 173 | post :POST_verify_authy, params: { 174 | :token => valid_authy_token, 175 | :remember_device => '1' 176 | } 177 | } 178 | 179 | it "should set a signed remember_device cookie" do 180 | jar = ActionDispatch::Cookies::CookieJar.build(request, cookies.to_hash) 181 | cookie = jar.signed["remember_device"] 182 | expect(cookie).not_to be_nil 183 | parsed_cookie = JSON.parse(cookie) 184 | expect(parsed_cookie["id"]).to eq(user.id) 185 | end 186 | end 187 | 188 | describe "and remember_me in the session" do 189 | before(:each) do 190 | request.session["user_remember_me"] = true 191 | post :POST_verify_authy, params: { :token => valid_authy_token } 192 | end 193 | 194 | it "should remember the user" do 195 | user.reload 196 | expect(user.remember_created_at).to be_within(1).of(Time.zone.now) 197 | end 198 | end 199 | end 200 | 201 | describe "with an invalid token" do 202 | before(:each) { 203 | expect(Authy::API).to receive(:verify).with({ 204 | :id => user.authy_id, 205 | :token => invalid_authy_token, 206 | :force => true 207 | }).and_return(verify_failure) 208 | post :POST_verify_authy, params: { :token => invalid_authy_token } 209 | } 210 | 211 | it "Shouldn't log the user in" do 212 | expect(subject.current_user).to be nil 213 | end 214 | 215 | it "should redirect to the verification page" do 216 | expect(response).to render_template('verify_authy') 217 | end 218 | 219 | it "should set an error message in the flash" do 220 | expect(flash[:notice]).to be nil 221 | expect(flash[:error]).not_to be nil 222 | end 223 | end 224 | 225 | describe 'with a lockable user' do 226 | let(:lockable_user) { create(:lockable_authy_user) } 227 | before(:all) { Devise.lock_strategy = :failed_attempts } 228 | 229 | before(:each) do 230 | request.session["user_id"] = lockable_user.id 231 | request.session["user_password_checked"] = true 232 | end 233 | 234 | it 'locks the account when failed_attempts exceeds maximum' do 235 | expect(Authy::API).to receive(:verify).exactly(Devise.maximum_attempts).times.with({ 236 | :id => lockable_user.authy_id, 237 | :token => invalid_authy_token, 238 | :force => true 239 | }).and_return(verify_failure) 240 | (Devise.maximum_attempts).times do 241 | post :POST_verify_authy, params: { token: invalid_authy_token } 242 | end 243 | 244 | lockable_user.reload 245 | expect(lockable_user.access_locked?).to be true 246 | end 247 | end 248 | 249 | describe 'with a user that is not lockable' do 250 | it 'does not lock the account when failed_attempts exceeds maximum' do 251 | request.session['user_id'] = user.id 252 | request.session['user_password_checked'] = true 253 | 254 | expect(Authy::API).to receive(:verify).exactly(Devise.maximum_attempts).times.with({ 255 | :id => user.authy_id, 256 | :token => invalid_authy_token, 257 | :force => true 258 | }).and_return(verify_failure) 259 | 260 | Devise.maximum_attempts.times do 261 | post :POST_verify_authy, params: { token: invalid_authy_token } 262 | end 263 | 264 | user.reload 265 | expect(user.locked_at).to be_nil 266 | end 267 | end 268 | end 269 | 270 | describe "GET #authy_onetouch_status" do 271 | let(:uuid) { SecureRandom.uuid } 272 | 273 | it "should return a 202 status code when pending" do 274 | allow(Authy::OneTouch).to receive(:approval_request_status) 275 | .with(:uuid => uuid) 276 | .and_return({ 'approval_request' => { 'status' => 'pending' }}) 277 | get :GET_authy_onetouch_status, params: { onetouch_uuid: uuid } 278 | expect(response.code).to eq("202") 279 | end 280 | 281 | it "should return a 401 status code when denied" do 282 | allow(Authy::OneTouch).to receive(:approval_request_status) 283 | .with(:uuid => uuid) 284 | .and_return({ 'approval_request' => { 'status' => 'denied' }}) 285 | get :GET_authy_onetouch_status, params: { onetouch_uuid: uuid } 286 | expect(response.code).to eq("401") 287 | end 288 | 289 | it "should return a 500 status code when something else happens" do 290 | allow(Authy::OneTouch).to receive(:approval_request_status) 291 | .with(:uuid => uuid) 292 | .and_return({}) 293 | get :GET_authy_onetouch_status, params: { onetouch_uuid: uuid } 294 | expect(response.code).to eq("500") 295 | end 296 | 297 | describe "when approved" do 298 | before(:each) do 299 | allow(Authy::OneTouch).to receive(:approval_request_status) 300 | .with(:uuid => uuid) 301 | .and_return({ 'approval_request' => { 'status' => 'approved' }}) 302 | get :GET_authy_onetouch_status, params: { onetouch_uuid: uuid, remember_device: '0' } 303 | end 304 | 305 | it "should return a 200 status code" do 306 | expect(response.code).to eq("200") 307 | end 308 | 309 | it "should render a JSON object with the redirect path" do 310 | expect(response.body).to eq({ redirect: root_path }.to_json) 311 | end 312 | 313 | it "should not remember the user" do 314 | expect(cookies["remember_device"]).to be_nil 315 | end 316 | 317 | it "should sign the user in" do 318 | expect(subject.current_user).to eq(user) 319 | expect(session["user_authy_token_checked"]).to be true 320 | user.reload 321 | expect(user.last_sign_in_with_authy).to be_within(1).of(Time.zone.now) 322 | end 323 | end 324 | 325 | describe "when approved and 2fa remembered" do 326 | before(:each) do 327 | allow(Authy::OneTouch).to receive(:approval_request_status) 328 | .with(:uuid => uuid) 329 | .and_return({ 'approval_request' => { 'status' => 'approved' }}) 330 | get :GET_authy_onetouch_status, params: { onetouch_uuid: uuid, remember_device: '1' } 331 | end 332 | 333 | it "should return a 200 status code" do 334 | expect(response.code).to eq("200") 335 | end 336 | 337 | it "should render a JSON object with the redirect path" do 338 | expect(response.body).to eq({ redirect: root_path }.to_json) 339 | end 340 | 341 | it "should set a signed remember_device cookie" do 342 | jar = ActionDispatch::Cookies::CookieJar.build(request, cookies.to_hash) 343 | cookie = jar.signed["remember_device"] 344 | expect(cookie).not_to be_nil 345 | parsed_cookie = JSON.parse(cookie) 346 | expect(parsed_cookie["id"]).to eq(user.id) 347 | end 348 | 349 | it "should sign the user in" do 350 | expect(subject.current_user).to eq(user) 351 | expect(session["user_authy_token_checked"]).to be true 352 | user.reload 353 | expect(user.last_sign_in_with_authy).to be_within(1).of(Time.zone.now) 354 | end 355 | end 356 | 357 | describe "when approved and remember_me in the session" do 358 | before(:each) do 359 | request.session["user_remember_me"] = true 360 | allow(Authy::API).to receive(:get_request) 361 | .with("onetouch/json/approval_requests/#{uuid}") 362 | .and_return({ 'approval_request' => { 'status' => 'approved' }}) 363 | get :GET_authy_onetouch_status, params: { onetouch_uuid: uuid, remember_device: '0' } 364 | end 365 | 366 | it "should remember the user" do 367 | user.reload 368 | expect(user.remember_created_at).to be_within(1).of(Time.zone.now) 369 | end 370 | end 371 | end 372 | end 373 | 374 | describe "enabling/disabling authy" do 375 | describe "with no-one logged in" do 376 | it "GET #enable_authy should redirect to sign in" do 377 | get :GET_enable_authy 378 | expect(response).to redirect_to(new_user_session_path) 379 | end 380 | 381 | it "POST #enable_authy should redirect to sign in" do 382 | post :POST_enable_authy 383 | expect(response).to redirect_to(new_user_session_path) 384 | end 385 | 386 | it "GET #verify_authy_installation should redirect to sign in" do 387 | get :GET_verify_authy_installation 388 | expect(response).to redirect_to(new_user_session_path) 389 | end 390 | 391 | it "POST #verify_authy_installation should redirect to sign in" do 392 | post :POST_verify_authy_installation 393 | expect(response).to redirect_to(new_user_session_path) 394 | end 395 | 396 | it "POST #disable_authy should redirect to sign in" do 397 | post :POST_disable_authy 398 | expect(response).to redirect_to(new_user_session_path) 399 | end 400 | end 401 | 402 | describe "with a logged in user" do 403 | before(:each) { sign_in(user) } 404 | 405 | describe "GET #enable_authy" do 406 | it "should render enable authy view if user isn't enabled" do 407 | user.update_attribute(:authy_enabled, false) 408 | get :GET_enable_authy 409 | expect(response).to render_template("enable_authy") 410 | end 411 | 412 | it "should render enable authy view if user doens't have an authy_id" do 413 | user.update_attribute(:authy_id, nil) 414 | get :GET_enable_authy 415 | expect(response).to render_template("enable_authy") 416 | end 417 | 418 | it "should redirect and set flash if authy is enabled" do 419 | user.update_attribute(:authy_enabled, true) 420 | get :GET_enable_authy 421 | expect(response).to redirect_to(root_path) 422 | expect(flash[:notice]).not_to be nil 423 | end 424 | end 425 | 426 | describe "POST #enable_authy" do 427 | let(:user) { create(:user) } 428 | let(:cellphone) { '3010008090' } 429 | let(:country_code) { '57' } 430 | 431 | describe "with a successful registration to Authy" do 432 | before(:each) do 433 | expect(Authy::API).to receive(:register_user).with( 434 | :email => user.email, 435 | :cellphone => cellphone, 436 | :country_code => country_code 437 | ).and_return(double("Authy::User", :ok? => true, :id => "123")) 438 | post :POST_enable_authy, :params => { :cellphone => cellphone, :country_code => country_code } 439 | end 440 | 441 | it "save the authy_id to the user" do 442 | user.reload 443 | expect(user.authy_id).to eq("123") 444 | end 445 | 446 | it "should not enable the user yet" do 447 | user.reload 448 | expect(user.authy_enabled).to be(false) 449 | end 450 | 451 | it "should redirect to the verification page" do 452 | expect(response).to redirect_to(user_verify_authy_installation_path) 453 | end 454 | end 455 | 456 | describe "but a user that can't be saved" do 457 | before(:each) do 458 | expect(user).to receive(:save).and_return(false) 459 | expect(subject).to receive(:current_user).and_return(user) 460 | expect(Authy::API).to receive(:register_user).with( 461 | :email => user.email, 462 | :cellphone => cellphone, 463 | :country_code => country_code 464 | ).and_return(double("Authy::User", :ok? => true, :id => "123")) 465 | post :POST_enable_authy, :params => { :cellphone => cellphone, :country_code => country_code } 466 | end 467 | 468 | it "should set an error flash" do 469 | expect(flash[:error]).not_to be nil 470 | end 471 | 472 | it "should redirect" do 473 | expect(response).to redirect_to(root_path) 474 | end 475 | end 476 | 477 | describe "with an unsuccessful registration to Authy" do 478 | before(:each) do 479 | expect(Authy::API).to receive(:register_user).with( 480 | :email => user.email, 481 | :cellphone => cellphone, 482 | :country_code => country_code 483 | ).and_return(double("Authy::User", :ok? => false)) 484 | 485 | post :POST_enable_authy, :params => { :cellphone => cellphone, :country_code => country_code } 486 | end 487 | 488 | it "does not update the authy_id" do 489 | old_authy_id = user.authy_id 490 | user.reload 491 | expect(user.authy_id).to eq(old_authy_id) 492 | end 493 | 494 | it "shows an error flash" do 495 | expect(flash[:error]).to eq("Something went wrong while enabling two factor authentication") 496 | end 497 | 498 | it "renders enable_authy page again" do 499 | expect(response).to render_template('enable_authy') 500 | end 501 | end 502 | end 503 | 504 | describe "GET verify_authy_installation" do 505 | describe "with a user that hasn't enabled authy yet" do 506 | let(:user) { create(:user) } 507 | before(:each) { sign_in(user) } 508 | 509 | it "should redirect to enable authy" do 510 | get :GET_verify_authy_installation 511 | expect(response).to redirect_to user_enable_authy_path 512 | end 513 | end 514 | 515 | describe "with a user that has enabled authy" do 516 | it "should redirect to after authy verified path" do 517 | get :GET_verify_authy_installation 518 | expect(response).to redirect_to root_path 519 | end 520 | end 521 | 522 | describe "with a user with an authy id without authy enabled" do 523 | before(:each) { user.update_attribute(:authy_enabled, false) } 524 | 525 | it "should render the authy verification page" do 526 | get :GET_verify_authy_installation 527 | expect(response).to render_template('verify_authy_installation') 528 | end 529 | 530 | describe "with qr codes turned on" do 531 | before(:each) do 532 | Devise.authy_enable_qr_code = true 533 | end 534 | 535 | after(:each) do 536 | Devise.authy_enable_qr_code = false 537 | end 538 | 539 | it "should hit API for a QR code" do 540 | expect(Authy::API).to receive(:request_qr_code).with( 541 | :id => user.authy_id 542 | ).and_return(double("Authy::Request", :qr_code => 'https://example.com/qr.png')) 543 | 544 | get :GET_verify_authy_installation 545 | expect(response).to render_template('verify_authy_installation') 546 | expect(assigns[:authy_qr_code]).to eq('https://example.com/qr.png') 547 | end 548 | end 549 | end 550 | end 551 | 552 | describe "POST verify_authy_installation" do 553 | let(:token) { "000000" } 554 | 555 | describe "with a user without an authy id" do 556 | let(:user) { create(:user) } 557 | it "redirects to enable path" do 558 | post :POST_verify_authy_installation, :params => { :token => token } 559 | expect(response).to redirect_to(user_enable_authy_path) 560 | end 561 | end 562 | 563 | describe "with a user that has an authy id and is enabled" do 564 | it "redirects to after authy verified path" do 565 | post :POST_verify_authy_installation, :params => { :token => token } 566 | expect(response).to redirect_to(root_path) 567 | end 568 | end 569 | 570 | describe "with a user that has an authy id but isn't enabled" do 571 | before(:each) { user.update_attribute(:authy_enabled, false) } 572 | 573 | describe "successful verification" do 574 | before(:each) do 575 | expect(Authy::API).to receive(:verify).with({ 576 | :id => user.authy_id, 577 | :token => token, 578 | :force => true 579 | }).and_return(double("Authy::Response", :ok? => true)) 580 | post :POST_verify_authy_installation, :params => { :token => token, :remember_device => '0' } 581 | end 582 | 583 | it "should enable authy for user" do 584 | user.reload 585 | expect(user.authy_enabled).to be true 586 | end 587 | 588 | it "should set {resource}_authy_token_checked in the session" do 589 | expect(session["user_authy_token_checked"]).to be true 590 | end 591 | 592 | it "should set a flash notice and redirect" do 593 | expect(response).to redirect_to(root_path) 594 | expect(flash[:notice]).to eq('Two factor authentication was enabled') 595 | end 596 | 597 | it "should not set a remember_device cookie" do 598 | expect(cookies["remember_device"]).to be_nil 599 | end 600 | end 601 | 602 | describe "successful verification with remember device" do 603 | before(:each) do 604 | expect(Authy::API).to receive(:verify).with({ 605 | :id => user.authy_id, 606 | :token => token, 607 | :force => true 608 | }).and_return(double("Authy::Response", :ok? => true)) 609 | post :POST_verify_authy_installation, :params => { :token => token, :remember_device => '1' } 610 | end 611 | 612 | it "should enable authy for user" do 613 | user.reload 614 | expect(user.authy_enabled).to be true 615 | end 616 | it "should set {resource}_authy_token_checked in the session" do 617 | expect(session["user_authy_token_checked"]).to be true 618 | end 619 | it "should set a flash notice and redirect" do 620 | expect(response).to redirect_to(root_path) 621 | expect(flash[:notice]).to eq('Two factor authentication was enabled') 622 | end 623 | 624 | it "should set a signed remember_device cookie" do 625 | jar = ActionDispatch::Cookies::CookieJar.build(request, cookies.to_hash) 626 | cookie = jar.signed["remember_device"] 627 | expect(cookie).not_to be_nil 628 | parsed_cookie = JSON.parse(cookie) 629 | expect(parsed_cookie["id"]).to eq(user.id) 630 | end 631 | end 632 | 633 | describe "unsuccessful verification" do 634 | before(:each) do 635 | expect(Authy::API).to receive(:verify).with({ 636 | :id => user.authy_id, 637 | :token => token, 638 | :force => true 639 | }).and_return(double("Authy::Response", :ok? => false)) 640 | post :POST_verify_authy_installation, :params => { :token => token } 641 | end 642 | 643 | it "should not enable authy for user" do 644 | user.reload 645 | expect(user.authy_enabled).to be false 646 | end 647 | 648 | it "should set an error flash and render verify_authy_installation" do 649 | expect(response).to render_template('verify_authy_installation') 650 | expect(flash[:error]).to eq('Something went wrong while enabling two factor authentication') 651 | end 652 | end 653 | 654 | describe "unsuccessful verification with qr codes turned on" do 655 | before(:each) do 656 | Devise.authy_enable_qr_code = true 657 | end 658 | 659 | after(:each) do 660 | Devise.authy_enable_qr_code = false 661 | end 662 | 663 | it "should hit API for a QR code" do 664 | expect(Authy::API).to receive(:verify).with({ 665 | :id => user.authy_id, 666 | :token => token, 667 | :force => true 668 | }).and_return(double("Authy::Response", :ok? => false)) 669 | expect(Authy::API).to receive(:request_qr_code).with( 670 | :id => user.authy_id 671 | ).and_return(double("Authy::Request", :qr_code => 'https://example.com/qr.png')) 672 | 673 | post :POST_verify_authy_installation, :params => { :token => token } 674 | expect(response).to render_template('verify_authy_installation') 675 | expect(assigns[:authy_qr_code]).to eq('https://example.com/qr.png') 676 | end 677 | end 678 | end 679 | end 680 | 681 | describe "POST disable_authy" do 682 | describe "successfully" do 683 | before(:each) do 684 | cookies.signed[:remember_device] = { 685 | :value => {expires: Time.now.to_i, id: user.id}.to_json, 686 | :secure => false, 687 | :expires => User.authy_remember_device.from_now 688 | } 689 | expect(Authy::API).to receive(:delete_user) 690 | .with(:id => user.authy_id) 691 | .and_return(double("Authy::Response", :ok? => true)) 692 | 693 | post :POST_disable_authy 694 | end 695 | 696 | it "should disable 2FA" do 697 | user.reload 698 | expect(user.authy_id).to be nil 699 | expect(user.authy_enabled).to be false 700 | end 701 | 702 | it "should forget the device cookie" do 703 | expect(response.cookies[:remember_device]).to be nil 704 | end 705 | 706 | it "should set a flash notice and redirect" do 707 | expect(flash.now[:notice]).to eq("Two factor authentication was disabled") 708 | expect(response).to redirect_to(root_path) 709 | end 710 | end 711 | 712 | describe "with more than one user using the same authy_id" do 713 | # It is valid for more than one user to share an authy_id 714 | # https://github.com/twilio/authy-devise/issues/143 715 | before(:each) do 716 | @other_user = create(:authy_user, :authy_id => user.authy_id) 717 | cookies.signed[:remember_device] = { 718 | :value => {expires: Time.now.to_i, id: user.id}.to_json, 719 | :secure => false, 720 | :expires => User.authy_remember_device.from_now 721 | } 722 | expect(Authy::API).not_to receive(:delete_user) 723 | 724 | post :POST_disable_authy 725 | end 726 | 727 | it "should disable 2FA" do 728 | user.reload 729 | expect(user.authy_id).to be nil 730 | expect(user.authy_enabled).to be false 731 | end 732 | 733 | it "should forget the device cookie" do 734 | expect(response.cookies[:remember_device]).to be nil 735 | end 736 | 737 | it "should set a flash notice and redirect" do 738 | expect(flash.now[:notice]).to eq("Two factor authentication was disabled") 739 | expect(response).to redirect_to(root_path) 740 | end 741 | end 742 | 743 | describe "unsuccessfully" do 744 | before(:each) do 745 | cookies.signed[:remember_device] = { 746 | :value => {expires: Time.now.to_i, id: user.id}.to_json, 747 | :secure => false, 748 | :expires => User.authy_remember_device.from_now 749 | } 750 | expect(Authy::API).to receive(:delete_user) 751 | .with(:id => user.authy_id) 752 | .and_return(double("Authy::Response", :ok? => false)) 753 | 754 | post :POST_disable_authy 755 | end 756 | 757 | it "should not disable 2FA" do 758 | user.reload 759 | expect(user.authy_id).not_to be nil 760 | expect(user.authy_enabled).to be true 761 | end 762 | 763 | it "should not forget the device cookie" do 764 | expect(cookies[:remember_device]).not_to be_nil 765 | end 766 | 767 | it "should set a flash error and redirect" do 768 | expect(flash[:error]).to eq("Something went wrong while disabling two factor authentication") 769 | expect(response).to redirect_to(root_path) 770 | end 771 | end 772 | end 773 | end 774 | end 775 | 776 | describe "requesting authentication tokens" do 777 | describe "without a user" do 778 | it "Should not request sms if user couldn't be found" do 779 | expect(Authy::API).not_to receive(:request_sms) 780 | 781 | post :request_sms 782 | 783 | expect(response.media_type).to eq('application/json') 784 | body = JSON.parse(response.body) 785 | expect(body['sent']).to be false 786 | expect(body['message']).to eq("User couldn't be found.") 787 | end 788 | 789 | it "Should not request a phone call if user couldn't be found" do 790 | expect(Authy::API).not_to receive(:request_phone_call) 791 | 792 | post :request_phone_call 793 | 794 | expect(response.media_type).to eq('application/json') 795 | body = JSON.parse(response.body) 796 | expect(body['sent']).to be false 797 | expect(body['message']).to eq("User couldn't be found.") 798 | end 799 | end 800 | 801 | describe "#request_sms" do 802 | before(:each) do 803 | expect(Authy::API).to receive(:request_sms) 804 | .with(:id => user.authy_id, :force => true) 805 | .and_return( 806 | double("Authy::Response", :ok? => true, :message => "Token was sent.") 807 | ) 808 | end 809 | describe "with a logged in user" do 810 | before(:each) { sign_in user } 811 | 812 | it "should send an SMS and respond with JSON" do 813 | post :request_sms 814 | expect(response.media_type).to eq('application/json') 815 | body = JSON.parse(response.body) 816 | 817 | expect(body['sent']).to be_truthy 818 | expect(body['message']).to eq("Token was sent.") 819 | end 820 | end 821 | 822 | describe "with a user_id in the session" do 823 | before(:each) { session["user_id"] = user.id } 824 | 825 | it "should send an SMS and respond with JSON" do 826 | post :request_sms 827 | expect(response.media_type).to eq('application/json') 828 | body = JSON.parse(response.body) 829 | 830 | expect(body['sent']).to be_truthy 831 | expect(body['message']).to eq("Token was sent.") 832 | end 833 | end 834 | end 835 | 836 | describe "#request_phone_call" do 837 | before(:each) do 838 | expect(Authy::API).to receive(:request_phone_call) 839 | .with(:id => user.authy_id, :force => true) 840 | .and_return( 841 | double("Authy::Response", :ok? => true, :message => "Token was sent.") 842 | ) 843 | end 844 | describe "with a logged in user" do 845 | before(:each) { sign_in user } 846 | 847 | it "should send an SMS and respond with JSON" do 848 | post :request_phone_call 849 | expect(response.media_type).to eq('application/json') 850 | body = JSON.parse(response.body) 851 | 852 | expect(body['sent']).to be_truthy 853 | expect(body['message']).to eq("Token was sent.") 854 | end 855 | end 856 | 857 | describe "with a user_id in the session" do 858 | before(:each) { session["user_id"] = user.id } 859 | 860 | it "should send an SMS and respond with JSON" do 861 | post :request_phone_call 862 | expect(response.media_type).to eq('application/json') 863 | body = JSON.parse(response.body) 864 | 865 | expect(body['sent']).to be_truthy 866 | expect(body['message']).to eq("Token was sent.") 867 | end 868 | end 869 | end 870 | end 871 | end 872 | -------------------------------------------------------------------------------- /spec/controllers/devise_sessions_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Devise::SessionsController, type: :controller do 4 | before(:each) { request.env["devise.mapping"] = Devise.mappings[:user] } 5 | 6 | describe "signing in" do 7 | describe "without an authy enabled user" do 8 | let(:user) { create(:user) } 9 | 10 | it "should sign the user in" do 11 | post :create, :params => { user: { email: user.email, password: user.password } } 12 | expect(subject.current_user).to eq(user) 13 | end 14 | 15 | it "should redirect" do 16 | post :create, :params => { user: { email: user.email, password: user.password } } 17 | expect(response).to redirect_to(root_path) 18 | end 19 | end 20 | 21 | describe "with an authy enabled user" do 22 | let(:authy_user) { create(:authy_user) } 23 | 24 | it "should redirect to verify authy path" do 25 | post :create, :params => { user: { email: authy_user.email, password: authy_user.password } } 26 | expect(response).to redirect_to user_verify_authy_path 27 | end 28 | 29 | it "should store id, password_checked in the session" do 30 | post :create, :params => { user: { email: authy_user.email, password: authy_user.password } } 31 | expect(session["user_id"]).to eq(authy_user.id) 32 | expect(session["user_password_checked"]).to be true 33 | expect(session["user_remember_me"]).to be false 34 | expect(session["user_return_to"]).to be nil 35 | end 36 | 37 | it "should store remember me in session if set" do 38 | post :create, :params => { user: { email: authy_user.email, password: authy_user.password, remember_me: '1' } } 39 | expect(session["user_id"]).to eq(authy_user.id) 40 | expect(session["user_password_checked"]).to be true 41 | expect(session["user_remember_me"]).to be true 42 | expect(session["user_return_to"]).to be nil 43 | end 44 | 45 | it "should keep user_return_to if set" do 46 | post :create, :params => { 47 | user: { 48 | email: authy_user.email, 49 | password: authy_user.password, 50 | remember_me: "1" 51 | } 52 | }, :session => { 53 | user_return_to: "/dashboard" 54 | } 55 | expect(session["user_id"]).to eq(authy_user.id) 56 | expect(session["user_password_checked"]).to be true 57 | expect(session["user_remember_me"]).to be true 58 | expect(session["user_return_to"]).to be "/dashboard" 59 | end 60 | 61 | it "should not sign the user in yet" do 62 | post :create, :params => { user: { email: authy_user.email, password: authy_user.password } } 63 | expect(subject.current_user).to be nil 64 | end 65 | 66 | it "should sign the user in and redirect to root if user is remembered" do 67 | cookies.signed[:remember_device] = { 68 | :value => {expires: Time.now.to_i, id: authy_user.id}.to_json, 69 | :secure => false, 70 | :expires => User.authy_remember_device.from_now 71 | } 72 | post :create, :params => { user: { email: authy_user.email, password: authy_user.password } } 73 | expect(subject.current_user).to eq(authy_user) 74 | expect(response).to redirect_to(root_path) 75 | end 76 | end 77 | end 78 | end -------------------------------------------------------------------------------- /spec/controllers/passwords_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe DeviseAuthy::PasswordsController, type: :controller do 4 | before(:each) { request.env["devise.mapping"] = Devise.mappings[:user] } 5 | 6 | describe "during Devise :recoverable flow reset password stage" do 7 | describe "with a user without authy id" do 8 | let(:user) { create(:user) } 9 | 10 | it "should sign in the user after password reset" do 11 | token = user.send_reset_password_instructions 12 | put :update, :params => { :user => { 13 | :reset_password_token => token, 14 | :password => "password", 15 | :password_confirmation => "password" 16 | }} 17 | expect(subject.current_user).to eq(user) 18 | end 19 | end 20 | 21 | describe "with a user with authy id and authy enabled" do 22 | let(:user) { create(:authy_user) } 23 | 24 | it "should not sign in the user after password reset" do 25 | token = user.send_reset_password_instructions 26 | put :update, :params => { :user => { 27 | :reset_password_token => token, 28 | :password => "password", 29 | :password_confirmation => "password" 30 | }} 31 | expect(subject.current_user).to be nil 32 | end 33 | end 34 | end 35 | end -------------------------------------------------------------------------------- /spec/devise-authy/version_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe DeviseAuthy::VERSION do 4 | it "has a version number" do 5 | expect(DeviseAuthy::VERSION).not_to be nil 6 | end 7 | end -------------------------------------------------------------------------------- /spec/factories.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | sequence :email do |n| 5 | "person#{n}@example.com" 6 | end 7 | 8 | sequence :authy_id do |n| 9 | n.to_s 10 | end 11 | 12 | factory :user do 13 | email { generate(:email) } 14 | password { "correct horse battery staple" } 15 | 16 | factory :authy_user do 17 | authy_id { generate(:authy_id) } 18 | authy_enabled { true } 19 | end 20 | end 21 | 22 | factory :lockable_user, class: LockableUser do 23 | email { generate(:email) } 24 | password { "correct horse battery staple" } 25 | end 26 | 27 | factory :lockable_authy_user, class: LockableUser do 28 | email { generate(:email) } 29 | password { "correct horse battery staple" } 30 | authy_id { generate(:authy_id) } 31 | authy_enabled { true } 32 | end 33 | end -------------------------------------------------------------------------------- /spec/generators/active_record/devise_authy_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "generators/active_record/devise_authy_generator" 3 | 4 | RSpec.describe ActiveRecord::Generators::DeviseAuthyGenerator, type: :generator do 5 | destination File.expand_path("../../tmp", __FILE__) 6 | 7 | after(:all) do 8 | prepare_destination 9 | end 10 | 11 | before(:all) do 12 | prepare_destination 13 | run_generator ["user"] 14 | end 15 | 16 | it "copies the migration file across" do 17 | expect(destination_root).to have_structure { 18 | directory "db" do 19 | directory "migrate" do 20 | migration "devise_authy_add_to_users.rb" do 21 | contains "DeviseAuthyAddToUsers" 22 | contains "ActiveRecord::Migration[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]" 23 | end 24 | end 25 | end 26 | } 27 | end 28 | end -------------------------------------------------------------------------------- /spec/generators/devise_authy/devise_authy_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "generators/devise_authy/devise_authy_generator" 3 | 4 | RSpec.describe DeviseAuthy::Generators::DeviseAuthyGenerator, type: :generator do 5 | destination File.expand_path("../../tmp", __FILE__) 6 | 7 | after(:all) do 8 | prepare_destination 9 | end 10 | 11 | def prepare_app 12 | FileUtils.mkdir_p(File.join(destination_root, "app", "models")) 13 | File.open(File.join(destination_root, "app", "models", "user.rb"), "w") do |file| 14 | file << "class User < ActiveRecord::Base\n" \ 15 | " devise :database_authenticatable, :registerable,\n" \ 16 | " :recoverable, :rememberable, :trackable, :validatable\n" \ 17 | " attr_accessible :email\n" \ 18 | "end" 19 | end 20 | end 21 | 22 | before(:all) do 23 | prepare_destination 24 | prepare_app 25 | run_generator ["user"] 26 | end 27 | 28 | it "adds authy_authenticatable module and authy attributes" do 29 | expect(destination_root).to have_structure { 30 | directory "app" do 31 | directory "models" do 32 | file "user.rb" do 33 | contains "devise :authy_authenticatable" 34 | contains "attr_accessible :authy_id, :last_sign_in_with_authy, :email" 35 | end 36 | end 37 | end 38 | } 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/generators/devise_authy/install_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "generators/devise_authy/install_generator" 3 | 4 | RSpec.describe DeviseAuthy::Generators::InstallGenerator, type: :generator do 5 | destination File.expand_path("../../tmp", __FILE__) 6 | 7 | after(:all) do 8 | prepare_destination 9 | end 10 | 11 | def prepare_app 12 | FileUtils.mkdir_p(File.join(destination_root, "config", "initializers")) 13 | File.open(File.join(destination_root, "config", "initializers", "devise.rb"), "w") do |file| 14 | file << "Devise.setup do |config|\n\nend" 15 | end 16 | end 17 | 18 | def prepare_html_layout 19 | FileUtils.mkdir_p(File.join(destination_root, "app", "views", "layouts")) 20 | File.open(File.join(destination_root, "app", "views", "layouts", "application.html.erb"), "w") do |file| 21 | file << "Application" 22 | end 23 | end 24 | 25 | describe "with no arguments" do 26 | before(:all) do 27 | prepare_destination 28 | prepare_app 29 | prepare_html_layout 30 | run_generator 31 | end 32 | 33 | it "copies across the locale file" do 34 | expect(destination_root).to have_structure { 35 | directory "config" do 36 | directory "locales" do 37 | file "devise.authy.en.yml" do 38 | contains "Two factor authentication was enabled" 39 | end 40 | end 41 | end 42 | } 43 | end 44 | 45 | it "injects devise config" do 46 | devise_config = File.read(File.join(destination_root, "config", "initializers", "devise.rb")) 47 | expect(devise_config).to match("Devise Authy Authentication Extension") 48 | expect(devise_config).to match("# config.authy_remember_device = 1.month") 49 | expect(devise_config).to match("# config.authy_enable_onetouch = false") 50 | expect(devise_config).to match("# config.authy_enable_qr_code = false") 51 | end 52 | 53 | it "creates an authy initializer" do 54 | expect(destination_root).to have_structure { 55 | directory "config" do 56 | directory "initializers" do 57 | file "authy.rb" do 58 | contains "Authy.api_key = ENV[\"AUTHY_API_KEY\"]\n" 59 | contains "Authy.api_uri = \"https://api.authy.com/\"" 60 | end 61 | end 62 | end 63 | } 64 | end 65 | 66 | it "copies over the HTML views" do 67 | expect(destination_root).to have_structure { 68 | directory "app" do 69 | directory "views" do 70 | directory "devise" do 71 | directory "devise_authy" do 72 | file "enable_authy.html.erb" 73 | file "verify_authy_installation.html.erb" 74 | file "verify_authy.html.erb" 75 | end 76 | end 77 | end 78 | end 79 | } 80 | end 81 | 82 | it "copies over the CSS and JS assets" do 83 | expect(destination_root).to have_structure { 84 | directory "app" do 85 | directory "assets" do 86 | directory "stylesheets" do 87 | file "devise_authy.css" 88 | end 89 | directory "javascripts" do 90 | file "devise_authy.js" 91 | end 92 | end 93 | end 94 | } 95 | end 96 | 97 | it "injects JS and CSS into the head of the application layout" do 98 | expect(destination_root).to have_structure { 99 | directory "app" do 100 | directory "views" do 101 | directory "layouts" do 102 | file "application.html.erb" do 103 | contains "<%=javascript_include_tag \"https://www.authy.com/form.authy.min.js\" %>" 104 | contains "<%=stylesheet_link_tag \"https://www.authy.com/form.authy.min.css\" %>" 105 | end 106 | end 107 | end 108 | end 109 | } 110 | end 111 | end 112 | 113 | describe "with haml views" do 114 | before(:all) do 115 | prepare_destination 116 | prepare_app 117 | prepare_html_layout 118 | run_generator %w(--haml) 119 | end 120 | 121 | it "copies over the HAML views" do 122 | expect(destination_root).to have_structure { 123 | directory "app" do 124 | directory "views" do 125 | directory "devise" do 126 | directory "devise_authy" do 127 | file "enable_authy.html.haml" 128 | file "verify_authy_installation.html.haml" 129 | file "verify_authy.html.haml" 130 | end 131 | end 132 | end 133 | end 134 | } 135 | end 136 | end 137 | 138 | describe "with sass" do 139 | before(:all) do 140 | prepare_destination 141 | prepare_app 142 | prepare_html_layout 143 | run_generator %w(--sass) 144 | end 145 | 146 | it "copies over SASS and JS assets" do 147 | expect(destination_root).to have_structure { 148 | directory "app" do 149 | directory "assets" do 150 | directory "stylesheets" do 151 | file "devise_authy.sass" 152 | end 153 | directory "javascripts" do 154 | file "devise_authy.js" 155 | end 156 | end 157 | end 158 | } 159 | end 160 | end 161 | end -------------------------------------------------------------------------------- /spec/helpers/view_helpers_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe DeviseAuthy::Views::Helpers, type: :helper do 4 | describe "request phone call link" do 5 | it "produces an anchor to the request-phone-call endpoint" do 6 | link = helper.authy_request_phone_call_link 7 | expect(link).to match(%r|href="/users/request-phone-call"|) 8 | expect(link).to match(%r|data-method="post"|) 9 | expect(link).to match(%r|data-remote="true"|) 10 | expect(link).to match(%r|id="authy-request-phone-call-link"|) 11 | expect(link).to match(%r|>Request phone call<|) 12 | end 13 | 14 | it "has customisable text" do 15 | link = helper.authy_request_phone_call_link(title: "Make it ring!") 16 | expect(link).to match(%r|>Make it ring!<|) 17 | end 18 | end 19 | 20 | describe "request sms link" do 21 | it "produces an anchor to the request-sms endpoint" do 22 | link = helper.authy_request_sms_link 23 | expect(link).to match(%r|href="/users/request-sms"|) 24 | expect(link).to match(%r|data-method="post"|) 25 | expect(link).to match(%r|data-remote="true"|) 26 | expect(link).to match(%r|id="authy-request-sms-link"|) 27 | expect(link).to match(%r|>Request SMS<|) 28 | end 29 | 30 | it "has customisable text" do 31 | link = helper.authy_request_phone_call_link(title: "Send a message!") 32 | expect(link).to match(%r|>Send a message!<|) 33 | end 34 | end 35 | 36 | describe "with a user" do 37 | let(:user) { create(:user) } 38 | 39 | describe "verify_authy_form" do 40 | it "creates a verify form with the user id as a field" do 41 | assign(:resource, user) 42 | form = helper.verify_authy_form { "I'm in a form" } 43 | expect(form).to match(%r|action="/users/verify_authy"|) 44 | expect(form).to match(%| Devise Authy Authentication Extension 8 | # How long should the user's device be remembered for. 9 | # config.authy_remember_device = 1.month 10 | 11 | # Should Authy OneTouch be enabled? 12 | # config.authy_enable_onetouch = false 13 | 14 | # The secret key used by Devise. Devise uses this key to generate 15 | # random tokens. Changing this key will render invalid all existing 16 | # confirmation, reset password and unlock tokens in the database. 17 | # Devise will use the `secret_key_base` as its `secret_key` 18 | # by default. You can change it below and use your own secret key. 19 | # config.secret_key = '321d6cc6978fef8134d1554aa530499c5ace14f036cbf2c037d9c617fcf8f4975a46863fed0e31a28b5f407cf47efac6c2608a5649f8feb2dbc5deb1988cfb7f' 20 | 21 | # ==> Controller configuration 22 | # Configure the parent class to the devise controllers. 23 | # config.parent_controller = 'DeviseController' 24 | 25 | # ==> Mailer Configuration 26 | # Configure the e-mail address which will be shown in Devise::Mailer, 27 | # note that it will be overwritten if you use your own mailer class 28 | # with default "from" parameter. 29 | config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' 30 | 31 | # Configure the class responsible to send e-mails. 32 | # config.mailer = 'Devise::Mailer' 33 | 34 | # Configure the parent class responsible to send e-mails. 35 | # config.parent_mailer = 'ActionMailer::Base' 36 | 37 | # ==> ORM configuration 38 | # Load and configure the ORM. Supports :active_record (default) and 39 | # :mongoid (bson_ext recommended) by default. Other ORMs may be 40 | # available as additional gems. 41 | require 'devise/orm/active_record' 42 | 43 | # ==> Configuration for any authentication mechanism 44 | # Configure which keys are used when authenticating a user. The default is 45 | # just :email. You can configure it to use [:username, :subdomain], so for 46 | # authenticating a user, both parameters are required. Remember that those 47 | # parameters are used only when authenticating and not when retrieving from 48 | # session. If you need permissions, you should implement that in a before filter. 49 | # You can also supply a hash where the value is a boolean determining whether 50 | # or not authentication should be aborted when the value is not present. 51 | # config.authentication_keys = [:email] 52 | 53 | # Configure parameters from the request object used for authentication. Each entry 54 | # given should be a request method and it will automatically be passed to the 55 | # find_for_authentication method and considered in your model lookup. For instance, 56 | # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. 57 | # The same considerations mentioned for authentication_keys also apply to request_keys. 58 | # config.request_keys = [] 59 | 60 | # Configure which authentication keys should be case-insensitive. 61 | # These keys will be downcased upon creating or modifying a user and when used 62 | # to authenticate or find a user. Default is :email. 63 | config.case_insensitive_keys = [:email] 64 | 65 | # Configure which authentication keys should have whitespace stripped. 66 | # These keys will have whitespace before and after removed upon creating or 67 | # modifying a user and when used to authenticate or find a user. Default is :email. 68 | config.strip_whitespace_keys = [:email] 69 | 70 | # Tell if authentication through request.params is enabled. True by default. 71 | # It can be set to an array that will enable params authentication only for the 72 | # given strategies, for example, `config.params_authenticatable = [:database]` will 73 | # enable it only for database (email + password) authentication. 74 | # config.params_authenticatable = true 75 | 76 | # Tell if authentication through HTTP Auth is enabled. False by default. 77 | # It can be set to an array that will enable http authentication only for the 78 | # given strategies, for example, `config.http_authenticatable = [:database]` will 79 | # enable it only for database authentication. The supported strategies are: 80 | # :database = Support basic authentication with authentication key + password 81 | # config.http_authenticatable = false 82 | 83 | # If 401 status code should be returned for AJAX requests. True by default. 84 | # config.http_authenticatable_on_xhr = true 85 | 86 | # The realm used in Http Basic Authentication. 'Application' by default. 87 | # config.http_authentication_realm = 'Application' 88 | 89 | # It will change confirmation, password recovery and other workflows 90 | # to behave the same regardless if the e-mail provided was right or wrong. 91 | # Does not affect registerable. 92 | # config.paranoid = true 93 | 94 | # By default Devise will store the user in session. You can skip storage for 95 | # particular strategies by setting this option. 96 | # Notice that if you are skipping storage for all authentication paths, you 97 | # may want to disable generating routes to Devise's sessions controller by 98 | # passing skip: :sessions to `devise_for` in your config/routes.rb 99 | config.skip_session_storage = [:http_auth] 100 | 101 | # By default, Devise cleans up the CSRF token on authentication to 102 | # avoid CSRF token fixation attacks. This means that, when using AJAX 103 | # requests for sign in and sign up, you need to get a new CSRF token 104 | # from the server. You can disable this option at your own risk. 105 | # config.clean_up_csrf_token_on_authentication = true 106 | 107 | # When false, Devise will not attempt to reload routes on eager load. 108 | # This can reduce the time taken to boot the app but if your application 109 | # requires the Devise mappings to be loaded during boot time the application 110 | # won't boot properly. 111 | # config.reload_routes = true 112 | 113 | # ==> Configuration for :database_authenticatable 114 | # For bcrypt, this is the cost for hashing the password and defaults to 11. If 115 | # using other algorithms, it sets how many times you want the password to be hashed. 116 | # 117 | # Limiting the stretches to just one in testing will increase the performance of 118 | # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use 119 | # a value less than 10 in other environments. Note that, for bcrypt (the default 120 | # algorithm), the cost increases exponentially with the number of stretches (e.g. 121 | # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). 122 | config.stretches = Rails.env.test? ? 1 : 11 123 | 124 | # Set up a pepper to generate the hashed password. 125 | # config.pepper = '08625a2b405468dde9dfd86840a4d7227873444ac54192e67be28ffcadfb1b5da3fa3fc56a14817d3082e97ec1694a99ca945f21cd7aa4e1224a2491e7c84e27' 126 | 127 | # Send a notification to the original email when the user's email is changed. 128 | # config.send_email_changed_notification = false 129 | 130 | # Send a notification email when the user's password is changed. 131 | # config.send_password_change_notification = false 132 | 133 | # ==> Configuration for :confirmable 134 | # A period that the user is allowed to access the website even without 135 | # confirming their account. For instance, if set to 2.days, the user will be 136 | # able to access the website for two days without confirming their account, 137 | # access will be blocked just in the third day. Default is 0.days, meaning 138 | # the user cannot access the website without confirming their account. 139 | # config.allow_unconfirmed_access_for = 2.days 140 | 141 | # A period that the user is allowed to confirm their account before their 142 | # token becomes invalid. For example, if set to 3.days, the user can confirm 143 | # their account within 3 days after the mail was sent, but on the fourth day 144 | # their account can't be confirmed with the token any more. 145 | # Default is nil, meaning there is no restriction on how long a user can take 146 | # before confirming their account. 147 | # config.confirm_within = 3.days 148 | 149 | # If true, requires any email changes to be confirmed (exactly the same way as 150 | # initial account confirmation) to be applied. Requires additional unconfirmed_email 151 | # db field (see migrations). Until confirmed, new email is stored in 152 | # unconfirmed_email column, and copied to email column on successful confirmation. 153 | config.reconfirmable = true 154 | 155 | # Defines which key will be used when confirming an account 156 | # config.confirmation_keys = [:email] 157 | 158 | # ==> Configuration for :rememberable 159 | # The time the user will be remembered without asking for credentials again. 160 | # config.remember_for = 2.weeks 161 | 162 | # Invalidates all the remember me tokens when the user signs out. 163 | config.expire_all_remember_me_on_sign_out = true 164 | 165 | # If true, extends the user's remember period when remembered via cookie. 166 | # config.extend_remember_period = false 167 | 168 | # Options to be passed to the created cookie. For instance, you can set 169 | # secure: true in order to force SSL only cookies. 170 | # config.rememberable_options = {} 171 | 172 | # ==> Configuration for :validatable 173 | # Range for password length. 174 | config.password_length = 6..128 175 | 176 | # Email regex used to validate email formats. It simply asserts that 177 | # one (and only one) @ exists in the given string. This is mainly 178 | # to give user feedback and not to assert the e-mail validity. 179 | config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ 180 | 181 | # ==> Configuration for :timeoutable 182 | # The time you want to timeout the user session without activity. After this 183 | # time the user will be asked for credentials again. Default is 30 minutes. 184 | # config.timeout_in = 30.minutes 185 | 186 | # ==> Configuration for :lockable 187 | # Defines which strategy will be used to lock an account. 188 | # :failed_attempts = Locks an account after a number of failed attempts to sign in. 189 | # :none = No lock strategy. You should handle locking by yourself. 190 | config.lock_strategy = :failed_attempts 191 | 192 | # Defines which key will be used when locking and unlocking an account 193 | # config.unlock_keys = [:email] 194 | 195 | # Defines which strategy will be used to unlock an account. 196 | # :email = Sends an unlock link to the user email 197 | # :time = Re-enables login after a certain amount of time (see :unlock_in below) 198 | # :both = Enables both strategies 199 | # :none = No unlock strategy. You should handle unlocking by yourself. 200 | config.unlock_strategy = :time 201 | 202 | # Number of authentication tries before locking an account if lock_strategy 203 | # is failed attempts. 204 | # config.maximum_attempts = 20 205 | 206 | # Time interval to unlock the account if :time is enabled as unlock_strategy. 207 | # config.unlock_in = 1.hour 208 | 209 | # Warn on the last attempt before the account is locked. 210 | # config.last_attempt_warning = true 211 | 212 | # ==> Configuration for :recoverable 213 | # 214 | # Defines which key will be used when recovering the password for an account 215 | # config.reset_password_keys = [:email] 216 | 217 | # Time interval you can reset your password with a reset password key. 218 | # Don't put a too small interval or your users won't have the time to 219 | # change their passwords. 220 | config.reset_password_within = 6.hours 221 | 222 | # When set to false, does not sign a user in automatically after their password is 223 | # reset. Defaults to true, so a user is signed in automatically after a reset. 224 | # config.sign_in_after_reset_password = true 225 | 226 | # ==> Configuration for :encryptable 227 | # Allow you to use another hashing or encryption algorithm besides bcrypt (default). 228 | # You can use :sha1, :sha512 or algorithms from others authentication tools as 229 | # :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20 230 | # for default behavior) and :restful_authentication_sha1 (then you should set 231 | # stretches to 10, and copy REST_AUTH_SITE_KEY to pepper). 232 | # 233 | # Require the `devise-encryptable` gem when using anything other than bcrypt 234 | # config.encryptor = :sha512 235 | 236 | # ==> Scopes configuration 237 | # Turn scoped views on. Before rendering "sessions/new", it will first check for 238 | # "users/sessions/new". It's turned off by default because it's slower if you 239 | # are using only default views. 240 | # config.scoped_views = false 241 | 242 | # Configure the default scope given to Warden. By default it's the first 243 | # devise role declared in your routes (usually :user). 244 | # config.default_scope = :user 245 | 246 | # Set this configuration to false if you want /users/sign_out to sign out 247 | # only the current scope. By default, Devise signs out all scopes. 248 | # config.sign_out_all_scopes = true 249 | 250 | # ==> Navigation configuration 251 | # Lists the formats that should be treated as navigational. Formats like 252 | # :html, should redirect to the sign in page when the user does not have 253 | # access, but formats like :xml or :json, should return 401. 254 | # 255 | # If you have any extra navigational formats, like :iphone or :mobile, you 256 | # should add them to the navigational formats lists. 257 | # 258 | # The "*/*" below is required to match Internet Explorer requests. 259 | # config.navigational_formats = ['*/*', :html] 260 | 261 | # The default HTTP method used to sign out a resource. Default is :delete. 262 | config.sign_out_via = :delete 263 | 264 | # ==> OmniAuth 265 | # Add a new OmniAuth provider. Check the wiki for more information on setting 266 | # up on your models and hooks. 267 | # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' 268 | 269 | # ==> Warden configuration 270 | # If you want to use other strategies, that are not supported by Devise, or 271 | # change the failure app, you can configure them inside the config.warden block. 272 | # 273 | # config.warden do |manager| 274 | # manager.intercept_401 = false 275 | # manager.default_strategies(scope: :user).unshift :some_external_strategy 276 | # end 277 | 278 | # ==> Mountable engine configurations 279 | # When using Devise inside an engine, let's call it `MyEngine`, and this engine 280 | # is mountable, there are some extra configurations to be taken into account. 281 | # The following options are available, assuming the engine is mounted as: 282 | # 283 | # mount MyEngine, at: '/my_engine' 284 | # 285 | # The router that invoked `devise_for`, in the example above, would be: 286 | # config.router_name = :my_engine 287 | # 288 | # When using OmniAuth, Devise cannot automatically set OmniAuth path, 289 | # so you need to do it manually. For the users scope, it would be: 290 | # config.omniauth_path_prefix = '/my_engine/users/auth' 291 | 292 | # ==> Turbolinks configuration 293 | # If your app is using Turbolinks, Turbolinks::Controller needs to be included to make redirection work correctly: 294 | # 295 | # ActiveSupport.on_load(:devise_failure_app) do 296 | # include Turbolinks::Controller 297 | # end 298 | end 299 | -------------------------------------------------------------------------------- /spec/internal/config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.routes.draw do 4 | devise_for :users 5 | devise_for :lockable_users, # for testing authy_lockable 6 | class: 'LockableUser', 7 | :path_names => { 8 | :verify_authy => "/verify-token", 9 | :enable_authy => "/enable-two-factor", 10 | :verify_authy_installation => "/verify-installation", 11 | :authy_onetouch_status => "/onetouch-status" 12 | } 13 | root 'home#index' 14 | end 15 | -------------------------------------------------------------------------------- /spec/internal/db/schema.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ActiveRecord::Schema.define do 4 | create_table "users", force: :cascade do |t| 5 | # devise - database_authenticable, registerable 6 | t.string "email", default: "", null: false 7 | t.string "encrypted_password", default: "", null: false 8 | # devise - recoverable 9 | t.string "reset_password_token" 10 | t.datetime "reset_password_sent_at" 11 | # devise - rememberable 12 | t.datetime "remember_created_at" 13 | # devise - trackable 14 | t.integer "sign_in_count", default: 0 15 | t.datetime "current_sign_in_at" 16 | t.datetime "last_sign_in_at" 17 | t.string "current_sign_in_ip" 18 | t.string "last_sign_in_ip" 19 | # devise - lockable 20 | t.integer "failed_attempts", default: 0 21 | t.string "unlock_token" 22 | t.datetime "locked_at" 23 | # devise - authy_authenticable 24 | t.string "authy_id" 25 | t.datetime "last_sign_in_with_authy" 26 | t.boolean "authy_enabled", default: false 27 | # single table inheritance so we can have lockable users 28 | t.string "type" 29 | 30 | t.datetime "created_at", null: false 31 | t.datetime "updated_at", null: false 32 | t.index ["authy_id"], name: "index_users_on_authy_id" 33 | t.index ["email"], name: "index_users_on_email", unique: true 34 | t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true 35 | t.index ["unlock_token"], name: "index_users_on_unlock_token", unique: true 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/internal/log/.gitignore: -------------------------------------------------------------------------------- 1 | *.log -------------------------------------------------------------------------------- /spec/internal/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/authy-devise/217c495b822a75e831533ec04f042e47eced6dde/spec/internal/public/favicon.ico -------------------------------------------------------------------------------- /spec/models/lockable_user_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe LockableUser, type: :model do 4 | describe "with a user with an authy id" do 5 | let(:user) { create(:lockable_authy_user) } 6 | 7 | describe "#lockable?" do 8 | it "is true if lock_strategy is :failed_attempts" do 9 | old_lock_strategy = Devise.lock_strategy 10 | Devise.lock_strategy = :failed_attempts 11 | expect(user.lockable?).to be true 12 | Devise.lock_strategy = old_lock_strategy 13 | end 14 | 15 | it "is false if lock_strategy is anything other than :failed_attempts" do 16 | old_lock_strategy = Devise.lock_strategy 17 | Devise.lock_strategy = :none 18 | expect(user.lockable?).to be false 19 | Devise.lock_strategy = old_lock_strategy 20 | end 21 | end 22 | 23 | describe "#invalid_authy_attempt!" do 24 | before(:all) { 25 | @old_lock_strategy = Devise.lock_strategy 26 | Devise.lock_strategy = :failed_attempts 27 | } 28 | after(:all) { 29 | Devise.lock_strategy = @old_lock_strategy 30 | } 31 | 32 | it "if failed_attempts is nil it treats it as though it was 0" do 33 | user.update_attribute(:failed_attempts, nil) 34 | user.invalid_authy_attempt! 35 | expect(user.failed_attempts).to be 1 36 | end 37 | 38 | it 'updates failed_attempts once per attempt' do 39 | 10.times { user.invalid_authy_attempt! } 40 | expect(user.failed_attempts).to eq(10) 41 | end 42 | 43 | it 'respects the maximum attempts configuration for Devise::Models::Lockable' do 44 | Devise.maximum_attempts = 3 45 | 2.times { user.invalid_authy_attempt! } 46 | expect(user.send(:attempts_exceeded?)).to be false # protected method 47 | user.invalid_authy_attempt! 48 | expect(user.send(:attempts_exceeded?)).to be true 49 | expect(user.access_locked?).to be true 50 | end 51 | 52 | it "returns false until the account is locked" do 53 | Devise.maximum_attempts = 3 54 | 2.times { expect(user.invalid_authy_attempt!).to be false } 55 | expect(user.invalid_authy_attempt!).to be true 56 | end 57 | 58 | end 59 | 60 | end 61 | 62 | end -------------------------------------------------------------------------------- /spec/models/user_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe User, type: :model do 4 | describe "with a user with an authy id" do 5 | let!(:user) { create(:authy_user) } 6 | 7 | describe "User#find_by_authy_id" do 8 | it "should find the user" do 9 | expect(User.first).not_to be nil 10 | expect(User.find_by_authy_id(user.authy_id)).to eq(user) 11 | end 12 | 13 | it "shouldn't find the user with the wrong id" do 14 | expect(User.find_by_authy_id('21')).to be nil 15 | end 16 | end 17 | 18 | describe "user#with_authy_authentication?" do 19 | it "should be false when authy isn't enabled" do 20 | user.authy_enabled = false 21 | request = double("request") 22 | expect(user.with_authy_authentication?(request)).to be false 23 | end 24 | it "should be true when authy is enabled" do 25 | user.authy_enabled = true 26 | request = double("request") 27 | expect(user.with_authy_authentication?(request)).to be true 28 | end 29 | end 30 | 31 | end 32 | describe "with a user without an authy id" do 33 | let!(:user) { create(:user) } 34 | 35 | describe "user#with_authy_authentication?" do 36 | it "should be false regardless of authy_enabled field" do 37 | request = double("request") 38 | expect(user.with_authy_authentication?(request)).to be false 39 | user.authy_enabled = true 40 | expect(user.with_authy_authentication?(request)).to be false 41 | end 42 | end 43 | end 44 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | ENV["RAILS_ENV"] = "test" 3 | 4 | require "bundler" 5 | 6 | require "simplecov" 7 | SimpleCov.start do 8 | add_filter "/spec/" 9 | end 10 | 11 | Bundler.require :default, :development 12 | require "devise" 13 | require "./lib/devise-authy" 14 | Combustion.initialize!(:all) 15 | 16 | require "rspec/rails" 17 | require "webmock/rspec" 18 | require "generator_spec" 19 | require "database_cleaner" 20 | require "./spec/factories.rb" 21 | 22 | RSpec.configure do |config| 23 | # Enable flags like --only-failures and --next-failure 24 | config.example_status_persistence_file_path = ".rspec_status" 25 | 26 | # Disable RSpec exposing methods globally on `Module` and `main` 27 | config.disable_monkey_patching! 28 | 29 | # Include Devise test helpers in controller tests 30 | config.include Devise::Test::ControllerHelpers, :type => :controller 31 | 32 | config.expect_with :rspec do |c| 33 | c.syntax = :expect 34 | end 35 | 36 | if config.respond_to?(:use_transactional_tests) 37 | config.use_transactional_tests = false 38 | else 39 | config.use_transactional_fixtures = false 40 | end 41 | 42 | config.before(:suite) do 43 | DatabaseCleaner.strategy = :truncation 44 | DatabaseCleaner.clean_with(:truncation) 45 | end 46 | 47 | config.around(:each) do |example| 48 | DatabaseCleaner.cleaning do 49 | example.run 50 | end 51 | end 52 | 53 | config.include FactoryBot::Syntax::Methods 54 | end --------------------------------------------------------------------------------