├── .codeclimate.yml ├── .gitignore ├── .rubocop.yml ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── app ├── controllers │ └── devise │ │ └── two_factor_authentication_controller.rb └── views │ └── devise │ └── two_factor_authentication │ ├── max_login_attempts_reached.html.erb │ └── show.html.erb ├── config └── locales │ ├── de.yml │ ├── en.yml │ ├── es.yml │ ├── fr.yml │ └── ru.yml ├── lib ├── generators │ ├── active_record │ │ ├── templates │ │ │ └── migration.rb │ │ └── two_factor_authentication_generator.rb │ └── two_factor_authentication │ │ └── two_factor_authentication_generator.rb ├── two_factor_authentication.rb └── two_factor_authentication │ ├── controllers │ └── helpers.rb │ ├── hooks │ └── two_factor_authenticatable.rb │ ├── models │ └── two_factor_authenticatable.rb │ ├── orm │ └── active_record.rb │ ├── rails.rb │ ├── routes.rb │ ├── schema.rb │ └── version.rb ├── spec ├── controllers │ └── two_factor_authentication_controller_spec.rb ├── features │ └── two_factor_authenticatable_spec.rb ├── generators │ └── active_record │ │ └── two_factor_authentication_generator_spec.rb ├── lib │ └── two_factor_authentication │ │ └── models │ │ └── two_factor_authenticatable_spec.rb ├── rails_app │ ├── .gitignore │ ├── README.md │ ├── Rakefile │ ├── app │ │ ├── assets │ │ │ ├── javascripts │ │ │ │ └── application.js │ │ │ └── stylesheets │ │ │ │ └── application.css │ │ ├── controllers │ │ │ ├── application_controller.rb │ │ │ └── home_controller.rb │ │ ├── helpers │ │ │ └── application_helper.rb │ │ ├── mailers │ │ │ └── .gitkeep │ │ ├── models │ │ │ ├── .gitkeep │ │ │ ├── admin.rb │ │ │ ├── encrypted_user.rb │ │ │ ├── guest_user.rb │ │ │ └── user.rb │ │ └── views │ │ │ ├── home │ │ │ ├── dashboard.html.erb │ │ │ └── index.html.erb │ │ │ └── layouts │ │ │ └── application.html.erb │ ├── config.ru │ ├── config │ │ ├── application.rb │ │ ├── boot.rb │ │ ├── database.yml │ │ ├── environment.rb │ │ ├── environments │ │ │ ├── development.rb │ │ │ ├── production.rb │ │ │ └── test.rb │ │ ├── initializers │ │ │ ├── backtrace_silencers.rb │ │ │ ├── cookies_serializer.rb │ │ │ ├── devise.rb │ │ │ ├── inflections.rb │ │ │ ├── mime_types.rb │ │ │ ├── secret_token.rb │ │ │ ├── session_store.rb │ │ │ └── wrap_parameters.rb │ │ ├── locales │ │ │ ├── devise.en.yml │ │ │ └── en.yml │ │ └── routes.rb │ ├── db │ │ ├── migrate │ │ │ ├── 20140403184646_devise_create_users.rb │ │ │ ├── 20140407172619_two_factor_authentication_add_to_users.rb │ │ │ ├── 20140407215513_add_nickanme_to_users.rb │ │ │ ├── 20151224171231_add_encrypted_columns_to_user.rb │ │ │ ├── 20151224180310_populate_otp_column.rb │ │ │ ├── 20151228230340_remove_otp_secret_key_from_user.rb │ │ │ └── 20160209032439_devise_create_admins.rb │ │ └── schema.rb │ ├── lib │ │ ├── assets │ │ │ └── .gitkeep │ │ └── sms_provider.rb │ ├── public │ │ ├── 404.html │ │ ├── 422.html │ │ ├── 500.html │ │ └── favicon.ico │ └── script │ │ └── rails ├── spec_helper.rb └── support │ ├── authenticated_model_helper.rb │ ├── capybara.rb │ ├── controller_helper.rb │ ├── features_spec_helper.rb │ ├── sms_provider.rb │ └── totp_helper.rb └── two_factor_authentication.gemspec /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | brakeman: 3 | enabled: true 4 | duplication: 5 | enabled: true 6 | config: 7 | languages: 8 | - ruby 9 | # mass_threshold: 30 10 | exclude_paths: 11 | - 'spec/**/*' 12 | fixme: 13 | enabled: true 14 | rubocop: 15 | enabled: true 16 | 17 | ratings: 18 | paths: 19 | - app/** 20 | - lib/** 21 | - '**.rb' 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | 6 | # Temporary files of every sort 7 | .DS_Store 8 | .idea 9 | .rvmrc 10 | .stgit* 11 | *.swap 12 | *.swo 13 | *.swp 14 | *~ 15 | bin/* 16 | nbproject 17 | patches-* 18 | capybara-*.html 19 | dump.rdb 20 | *.ids 21 | .rbenv-version 22 | .ruby-gemset 23 | .ruby-version 24 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | Include: 3 | - '**/Gemfile' 4 | - '**/Rakefile' 5 | UseCache: true 6 | 7 | Lint/AssignmentInCondition: 8 | Description: Don't use assignment in conditions. 9 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#safe-assignment-in-condition 10 | Enabled: true 11 | AllowSafeAssignment: true 12 | Lint/EachWithObjectArgument: 13 | Description: Check for immutable argument given to each_with_object. 14 | Enabled: true 15 | Lint/HandleExceptions: 16 | Description: Don't suppress exception. 17 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#dont-hide-exceptions 18 | Enabled: true 19 | Lint/LiteralInCondition: 20 | Description: Checks of literals used in conditions. 21 | Enabled: true 22 | Lint/LiteralInInterpolation: 23 | Description: Checks for literals used in interpolation. 24 | Enabled: true 25 | Lint/ParenthesesAsGroupedExpression: 26 | Description: Checks for method calls with a space before the opening parenthesis. 27 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#parens-no-spaces 28 | Enabled: true 29 | 30 | Metrics/AbcSize: 31 | Description: A calculated magnitude based on number of assignments, branches, and 32 | conditions. 33 | Enabled: true 34 | Max: 15 35 | Exclude: 36 | - spec/**/* 37 | Metrics/ClassLength: 38 | Description: Avoid classes longer than 100 lines of code. 39 | Enabled: true 40 | CountComments: false 41 | Max: 100 42 | Exclude: 43 | - spec/**/* 44 | Metrics/CyclomaticComplexity: 45 | Description: A complexity metric that is strongly correlated to the number of test 46 | cases needed to validate a method. 47 | Enabled: true 48 | Max: 6 49 | Metrics/LineLength: 50 | Description: Limit lines to 80 characters. 51 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#80-character-limits 52 | Enabled: true 53 | Max: 100 54 | AllowURI: true 55 | URISchemes: 56 | - http 57 | - https 58 | Metrics/MethodLength: 59 | Description: Avoid methods longer than 10 lines of code. 60 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#short-methods 61 | Enabled: true 62 | CountComments: false 63 | Max: 10 64 | Exclude: 65 | - spec/**/* 66 | Metrics/ModuleLength: 67 | CountComments: false 68 | Max: 100 69 | Description: Avoid modules longer than 100 lines of code. 70 | Enabled: true 71 | Exclude: 72 | - spec/**/* 73 | Metrics/ParameterLists: 74 | Description: Avoid parameter lists longer than three or four parameters. 75 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#too-many-params 76 | Enabled: true 77 | Max: 5 78 | CountKeywordArgs: true 79 | Metrics/PerceivedComplexity: 80 | Description: A complexity metric geared towards measuring complexity for a human 81 | reader. 82 | Enabled: true 83 | Max: 7 84 | 85 | Rails/ScopeArgs: 86 | Description: Checks the arguments of ActiveRecord scopes. 87 | Enabled: true 88 | Rails/TimeZone: 89 | # The value `strict` means that `Time` should be used with `zone`. 90 | # The value `flexible` allows usage of `in_time_zone` instead of `zone`. 91 | Enabled: true 92 | EnforcedStyle: flexible 93 | SupportedStyles: 94 | - strict 95 | - flexible 96 | 97 | Style/AccessorMethodName: 98 | Description: Check the naming of accessor methods for get_/set_. 99 | Enabled: false 100 | Style/AndOr: 101 | Description: Use &&/|| instead of and/or. 102 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-and-or-or 103 | Enabled: true 104 | EnforcedStyle: conditionals 105 | SupportedStyles: 106 | - always 107 | - conditionals 108 | Style/Alias: 109 | Description: Use alias_method instead of alias. 110 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#alias-method 111 | Enabled: true 112 | Style/ClassAndModuleChildren: 113 | EnforcedStyle: nested 114 | SupportedStyles: 115 | - nested 116 | - compact 117 | Style/CollectionMethods: 118 | Description: Preferred collection methods. 119 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#map-find-select-reduce-size 120 | Enabled: true 121 | PreferredMethods: 122 | collect: map 123 | collect!: map! 124 | find: detect 125 | find_all: select 126 | reduce: inject 127 | Style/Documentation: 128 | Description: Document classes and non-namespace modules. 129 | Enabled: false 130 | Style/DotPosition: 131 | Description: Checks the position of the dot in multi-line method calls. 132 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#consistent-multi-line-chains 133 | Enabled: true 134 | EnforcedStyle: trailing 135 | SupportedStyles: 136 | - leading 137 | - trailing 138 | Style/DoubleNegation: 139 | Description: Checks for uses of double negation (!!). 140 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-bang-bang 141 | Enabled: true 142 | Style/EachWithObject: 143 | Description: Prefer `each_with_object` over `inject` or `reduce`. 144 | Enabled: true 145 | Style/EmptyLiteral: 146 | Description: Prefer literals to Array.new/Hash.new/String.new. 147 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#literal-array-hash 148 | Enabled: true 149 | Style/FileName: 150 | Description: Use snake_case for source file names. 151 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#snake-case-files 152 | Enabled: true 153 | Exclude: [] 154 | Style/GuardClause: 155 | Description: Check for conditionals that can be replaced with guard clauses 156 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals 157 | Enabled: true 158 | MinBodyLength: 1 159 | Style/IfUnlessModifier: 160 | Description: Favor modifier if/unless usage when you have a single-line body. 161 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#if-as-a-modifier 162 | Enabled: false 163 | MaxLineLength: 80 164 | Style/InlineComment: 165 | Description: Avoid inline comments. 166 | Enabled: false 167 | Style/ModuleFunction: 168 | Description: Checks for usage of `extend self` in modules. 169 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#module-function 170 | Enabled: false 171 | Style/OneLineConditional: 172 | Description: Favor the ternary operator(?:) over if/then/else/end constructs. 173 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#ternary-operator 174 | Enabled: false 175 | Style/OptionHash: 176 | Description: Don't use option hashes when you can use keyword arguments. 177 | Enabled: false 178 | Style/PercentLiteralDelimiters: 179 | Description: Use `%`-literal delimiters consistently 180 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#percent-literal-braces 181 | Enabled: true 182 | PreferredDelimiters: 183 | "%": "()" 184 | "%i": "()" 185 | "%q": "()" 186 | "%Q": "()" 187 | "%r": "{}" 188 | "%s": "()" 189 | "%w": "()" 190 | "%W": "()" 191 | "%x": "()" 192 | Style/PerlBackrefs: 193 | Description: Avoid Perl-style regex back references. 194 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-perl-regexp-last-matchers 195 | Enabled: false 196 | Style/PredicateName: 197 | Description: Check the names of predicate methods. 198 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#bool-methods-qmark 199 | Enabled: true 200 | NamePrefix: 201 | - is_ 202 | - has_ 203 | - have_ 204 | NamePrefixBlacklist: 205 | - is_ 206 | Exclude: 207 | - spec/**/* 208 | Style/RaiseArgs: 209 | Description: Checks the arguments passed to raise/fail. 210 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#exception-class-messages 211 | Enabled: true 212 | EnforcedStyle: exploded 213 | SupportedStyles: 214 | - compact 215 | - exploded 216 | Style/Send: 217 | Description: Prefer `Object#__send__` or `Object#public_send` to `send`, as `send` 218 | may overlap with existing methods. 219 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#prefer-public-send 220 | Enabled: false 221 | Style/SignalException: 222 | Description: Checks for proper usage of fail and raise. 223 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#fail-method 224 | Enabled: true 225 | EnforcedStyle: semantic 226 | SupportedStyles: 227 | - only_raise 228 | - only_fail 229 | - semantic 230 | Style/SingleLineBlockParams: 231 | Description: Enforces the names of some block params. 232 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#reduce-blocks 233 | Enabled: true 234 | Methods: 235 | - reduce: 236 | - a 237 | - e 238 | - inject: 239 | - a 240 | - e 241 | Style/SingleLineMethods: 242 | Description: Avoid single-line methods. 243 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-single-line-methods 244 | Enabled: true 245 | AllowIfMethodIsEmpty: true 246 | Style/SpecialGlobalVars: 247 | Description: Avoid Perl-style global variables. 248 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-cryptic-perlisms 249 | Enabled: false 250 | Style/StringLiterals: 251 | Description: Checks if uses of quotes match the configured preference. 252 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#consistent-string-literals 253 | Enabled: true 254 | EnforcedStyle: single_quotes 255 | SupportedStyles: 256 | - single_quotes 257 | - double_quotes 258 | Style/StringLiteralsInInterpolation: 259 | Description: Checks if uses of quotes inside expressions in interpolated strings 260 | match the configured preference. 261 | Enabled: true 262 | EnforcedStyle: single_quotes 263 | SupportedStyles: 264 | - single_quotes 265 | - double_quotes 266 | Style/TrailingCommaInArguments: 267 | Description: 'Checks for trailing comma in argument lists.' 268 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas' 269 | Enabled: true 270 | EnforcedStyleForMultiline: no_comma 271 | SupportedStyles: 272 | - comma 273 | - consistent_comma 274 | - no_comma 275 | Style/TrailingCommaInLiteral: 276 | Description: 'Checks for trailing comma in array and hash literals.' 277 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas' 278 | Enabled: true 279 | EnforcedStyleForMultiline: no_comma 280 | SupportedStyles: 281 | - comma 282 | - consistent_comma 283 | - no_comma 284 | Style/VariableInterpolation: 285 | Description: Don't interpolate global, instance and class variables directly in 286 | strings. 287 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#curlies-interpolate 288 | Enabled: false 289 | Style/WhenThen: 290 | Description: Use when x then ... for one-line cases. 291 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#one-line-cases 292 | Enabled: false 293 | Style/ZeroLengthPredicate: 294 | Description: 'Use #empty? when testing for objects of length 0.' 295 | Enabled: true 296 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | env: 4 | - "RAILS_VERSION=4.2" 5 | - "RAILS_VERSION=5.2" 6 | - "RAILS_VERSION=master" 7 | 8 | rvm: 9 | - 2.3.8 10 | - 2.4.5 11 | - 2.5.3 12 | 13 | matrix: 14 | fast_finish: true 15 | allow_failures: 16 | - env: "RAILS_VERSION=master" 17 | include: 18 | - rvm: 2.2 19 | env: RAILS_VERSION=4.2 20 | 21 | before_install: 22 | - gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true 23 | - gem install bundler -v '< 2' 24 | 25 | before_script: 26 | - bundle exec rake app:db:setup 27 | 28 | script: bundle exec rake spec 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [Unreleased](https://github.com/Houdini/two_factor_authentication/tree/HEAD) 4 | 5 | [Full Changelog](https://github.com/Houdini/two_factor_authentication/compare/v1.1.5...HEAD) 6 | 7 | **Merged pull requests:** 8 | 9 | - Fix class detection in reset\_otp\_state\_for\(user\) [\#69](https://github.com/Houdini/two_factor_authentication/pull/69) ([monfresh](https://github.com/monfresh)) 10 | - Add ability to resend code [\#52](https://github.com/Houdini/two_factor_authentication/pull/52) ([iDiogenes](https://github.com/iDiogenes)) 11 | 12 | ## [v1.1.5](https://github.com/Houdini/two_factor_authentication/tree/v1.1.5) (2016-02-01) 13 | [Full Changelog](https://github.com/Houdini/two_factor_authentication/compare/v1.1.4...v1.1.5) 14 | 15 | **Closed issues:** 16 | 17 | - How should I integrate Devise two factor authentication with custom sessions controller? [\#60](https://github.com/Houdini/two_factor_authentication/issues/60) 18 | 19 | **Merged pull requests:** 20 | 21 | - added french translation [\#68](https://github.com/Houdini/two_factor_authentication/pull/68) ([qsypoq](https://github.com/qsypoq)) 22 | - Drop support for Ruby 1.9.3 & update .travis.yml [\#67](https://github.com/Houdini/two_factor_authentication/pull/67) ([monfresh](https://github.com/monfresh)) 23 | - Fix reset\_otp\_state specs [\#66](https://github.com/Houdini/two_factor_authentication/pull/66) ([monfresh](https://github.com/monfresh)) 24 | - Add a CHANGELOG.md [\#65](https://github.com/Houdini/two_factor_authentication/pull/65) ([monfresh](https://github.com/monfresh)) 25 | - Update bundler on Travis before installing gems [\#63](https://github.com/Houdini/two_factor_authentication/pull/63) ([monfresh](https://github.com/monfresh)) 26 | - Add support for OTP secret key encryption [\#62](https://github.com/Houdini/two_factor_authentication/pull/62) ([monfresh](https://github.com/monfresh)) 27 | - Allow executing code after sign in and before sign out [\#61](https://github.com/Houdini/two_factor_authentication/pull/61) ([monfresh](https://github.com/monfresh)) 28 | 29 | ## [v1.1.4](https://github.com/Houdini/two_factor_authentication/tree/v1.1.4) (2016-01-01) 30 | [Full Changelog](https://github.com/Houdini/two_factor_authentication/compare/v1.1.3...v1.1.4) 31 | 32 | **Closed issues:** 33 | 34 | - Old OTP can be used after a new one has been generated [\#59](https://github.com/Houdini/two_factor_authentication/issues/59) 35 | - Do we have any two\_factor\_method like authenticate\_user! [\#58](https://github.com/Houdini/two_factor_authentication/issues/58) 36 | - Configuration [\#57](https://github.com/Houdini/two_factor_authentication/issues/57) 37 | 38 | **Merged pull requests:** 39 | 40 | - Abstract logic for two factor success and fail into separate methods.… [\#56](https://github.com/Houdini/two_factor_authentication/pull/56) ([kpheasey](https://github.com/kpheasey)) 41 | - Move require rotp library to the file where it is used [\#55](https://github.com/Houdini/two_factor_authentication/pull/55) ([gkopylov](https://github.com/gkopylov)) 42 | - Add support for remembering a user's 2FA session in a cookie [\#54](https://github.com/Houdini/two_factor_authentication/pull/54) ([boffbowsh](https://github.com/boffbowsh)) 43 | - Test against Ruby 2.2 and Rails 4.2 [\#53](https://github.com/Houdini/two_factor_authentication/pull/53) ([boffbowsh](https://github.com/boffbowsh)) 44 | - Eliminates appended '?' to redirects that have no query string [\#46](https://github.com/Houdini/two_factor_authentication/pull/46) ([daveriess](https://github.com/daveriess)) 45 | 46 | ## [v1.1.3](https://github.com/Houdini/two_factor_authentication/tree/v1.1.3) (2014-12-14) 47 | [Full Changelog](https://github.com/Houdini/two_factor_authentication/compare/v1.1.2...v1.1.3) 48 | 49 | **Closed issues:** 50 | 51 | - rails g two\_factor\_authentication MODEL does not append .rb to end of migration [\#40](https://github.com/Houdini/two_factor_authentication/issues/40) 52 | 53 | **Merged pull requests:** 54 | 55 | - Allows length of OTP to be configured [\#44](https://github.com/Houdini/two_factor_authentication/pull/44) ([amoose](https://github.com/amoose)) 56 | - Missing translation. [\#43](https://github.com/Houdini/two_factor_authentication/pull/43) ([sadfuzzy](https://github.com/sadfuzzy)) 57 | - Preserve query parameters in \_return\_to for redirect. [\#42](https://github.com/Houdini/two_factor_authentication/pull/42) ([omb-awong](https://github.com/omb-awong)) 58 | - Add file extension to ActiveRecord generator [\#41](https://github.com/Houdini/two_factor_authentication/pull/41) ([jackturnbull](https://github.com/jackturnbull)) 59 | 60 | ## [v1.1.2](https://github.com/Houdini/two_factor_authentication/tree/v1.1.2) (2014-07-14) 61 | [Full Changelog](https://github.com/Houdini/two_factor_authentication/compare/v1.1.1...v1.1.2) 62 | 63 | **Closed issues:** 64 | 65 | - NoMethodError \(undefined method `scan' for nil:NilClass\) [\#37](https://github.com/Houdini/two_factor_authentication/issues/37) 66 | 67 | **Merged pull requests:** 68 | 69 | - Updated readme with rake task to update existing users with OTP secret k... [\#39](https://github.com/Houdini/two_factor_authentication/pull/39) ([Znow](https://github.com/Znow)) 70 | - Updated readme with view overriding [\#38](https://github.com/Houdini/two_factor_authentication/pull/38) ([Znow](https://github.com/Znow)) 71 | 72 | ## [v1.1.1](https://github.com/Houdini/two_factor_authentication/tree/v1.1.1) (2014-05-31) 73 | [Full Changelog](https://github.com/Houdini/two_factor_authentication/compare/v1.1...v1.1.1) 74 | 75 | **Closed issues:** 76 | 77 | - Override views [\#36](https://github.com/Houdini/two_factor_authentication/issues/36) 78 | - NoMethodError in Devise::TwoFactorAuthenticationController\#update [\#30](https://github.com/Houdini/two_factor_authentication/issues/30) 79 | 80 | **Merged pull requests:** 81 | 82 | - Use Strings and not Symbols for keys when storing variable in warden session [\#35](https://github.com/Houdini/two_factor_authentication/pull/35) ([karolsarnacki](https://github.com/karolsarnacki)) 83 | - Chore/extract reused hash key [\#34](https://github.com/Houdini/two_factor_authentication/pull/34) ([rud](https://github.com/rud)) 84 | - Pad OTP codes with less than 6 digits [\#31](https://github.com/Houdini/two_factor_authentication/pull/31) ([brissmyr](https://github.com/brissmyr)) 85 | 86 | ## [v1.1](https://github.com/Houdini/two_factor_authentication/tree/v1.1) (2014-04-16) 87 | **Closed issues:** 88 | 89 | - Update [\#15](https://github.com/Houdini/two_factor_authentication/issues/15) 90 | - Data in formats other than HTML left unprotected [\#6](https://github.com/Houdini/two_factor_authentication/issues/6) 91 | - Wordlists [\#5](https://github.com/Houdini/two_factor_authentication/issues/5) 92 | - devise - wrong number of arguments \(1 for 0\) [\#3](https://github.com/Houdini/two_factor_authentication/issues/3) 93 | - gem? [\#1](https://github.com/Houdini/two_factor_authentication/issues/1) 94 | 95 | **Merged pull requests:** 96 | 97 | - added is\_fully\_authenticated helper for current version [\#28](https://github.com/Houdini/two_factor_authentication/pull/28) ([edg3r](https://github.com/edg3r)) 98 | - Adds integration spec to ensure authentication code is sent on sign in [\#27](https://github.com/Houdini/two_factor_authentication/pull/27) ([rossta](https://github.com/rossta)) 99 | - ensure return\_to location is properly stored [\#26](https://github.com/Houdini/two_factor_authentication/pull/26) ([rossta](https://github.com/rossta)) 100 | - travis badge in README [\#25](https://github.com/Houdini/two_factor_authentication/pull/25) ([rossta](https://github.com/rossta)) 101 | - Integration specs [\#24](https://github.com/Houdini/two_factor_authentication/pull/24) ([rossta](https://github.com/rossta)) 102 | - README updates [\#23](https://github.com/Houdini/two_factor_authentication/pull/23) ([rossta](https://github.com/rossta)) 103 | - extract method \#max\_login\_attempts [\#22](https://github.com/Houdini/two_factor_authentication/pull/22) ([rossta](https://github.com/rossta)) 104 | - extract method \#populate\_otp\_column [\#21](https://github.com/Houdini/two_factor_authentication/pull/21) ([rossta](https://github.com/rossta)) 105 | - specs for Model\#provisioning\_uri [\#20](https://github.com/Houdini/two_factor_authentication/pull/20) ([rossta](https://github.com/rossta)) 106 | - Provide options for \#provisioning\_uri [\#19](https://github.com/Houdini/two_factor_authentication/pull/19) ([rossta](https://github.com/rossta)) 107 | - Use time-based authentication codes [\#16](https://github.com/Houdini/two_factor_authentication/pull/16) ([mattmueller](https://github.com/mattmueller)) 108 | - Add ru locales and locales for max\_limit\_reached view [\#13](https://github.com/Houdini/two_factor_authentication/pull/13) ([edg3r](https://github.com/edg3r)) 109 | - Update README.md [\#11](https://github.com/Houdini/two_factor_authentication/pull/11) ([edg3r](https://github.com/edg3r)) 110 | - Changed route from user to admin\_user [\#10](https://github.com/Houdini/two_factor_authentication/pull/10) ([ilanstern](https://github.com/ilanstern)) 111 | - Changed :notice to :error when setting flash message on attempt failure. [\#9](https://github.com/Houdini/two_factor_authentication/pull/9) ([johnmichaelbradley](https://github.com/johnmichaelbradley)) 112 | - Typo and punctuation corrections. [\#8](https://github.com/Houdini/two_factor_authentication/pull/8) ([johnmichaelbradley](https://github.com/johnmichaelbradley)) 113 | - Respond with 401 for request non-HTML requests [\#7](https://github.com/Houdini/two_factor_authentication/pull/7) ([WojtekKruszewski](https://github.com/WojtekKruszewski)) 114 | - need\_two\_factor\_authentication? method should accept request param. [\#4](https://github.com/Houdini/two_factor_authentication/pull/4) ([VladimirMikhailov](https://github.com/VladimirMikhailov)) 115 | - Add generators to make it easier to install and fix deprecation warnings [\#2](https://github.com/Houdini/two_factor_authentication/pull/2) ([carvil](https://github.com/carvil)) 116 | 117 | 118 | 119 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* 120 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in devise_ip_filter.gemspec 4 | gemspec 5 | 6 | rails_version = ENV["RAILS_VERSION"] || "default" 7 | 8 | rails = case rails_version 9 | when "master" 10 | {github: "rails/rails"} 11 | when "default" 12 | "~> 5.2" 13 | else 14 | "~> #{rails_version}" 15 | end 16 | 17 | gem "rails", rails 18 | 19 | if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.2.0') 20 | gem "test-unit", "~> 3.0" 21 | end 22 | 23 | group :test, :development do 24 | gem 'sqlite3' 25 | end 26 | 27 | group :test do 28 | gem 'rack_session_access' 29 | gem 'ammeter' 30 | end 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 Dmitrii Golub 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Two factor authentication for Devise 2 | 3 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Houdini/two_factor_authentication?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | [![Build Status](https://travis-ci.org/Houdini/two_factor_authentication.svg?branch=master)](https://travis-ci.org/Houdini/two_factor_authentication) 6 | [![Code Climate](https://codeclimate.com/github/Houdini/two_factor_authentication.svg)](https://codeclimate.com/github/Houdini/two_factor_authentication) 7 | 8 | ## Features 9 | 10 | * Support for 2 types of OTP codes 11 | 1. Codes delivered directly to the user 12 | 2. TOTP (Google Authenticator) codes based on a shared secret (HMAC) 13 | * Configurable OTP code digit length 14 | * Configurable max login attempts 15 | * Customizable logic to determine if a user needs two factor authentication 16 | * Configurable period where users won't be asked for 2FA again 17 | * Option to encrypt the TOTP secret in the database, with iv and salt 18 | 19 | ## Configuration 20 | 21 | ### Initial Setup 22 | 23 | In a Rails environment, require the gem in your Gemfile: 24 | 25 | gem 'two_factor_authentication' 26 | 27 | Once that's done, run: 28 | 29 | bundle install 30 | 31 | Note that Ruby 2.1 or greater is required. 32 | 33 | ### Installation 34 | 35 | #### Automatic initial setup 36 | 37 | To set up the model and database migration file automatically, run the 38 | following command: 39 | 40 | bundle exec rails g two_factor_authentication MODEL 41 | 42 | Where MODEL is your model name (e.g. User or Admin). This generator will add 43 | `:two_factor_authenticatable` to your model's Devise options and create a 44 | migration in `db/migrate/`, which will add the following columns to your table: 45 | 46 | - `:second_factor_attempts_count` 47 | - `:encrypted_otp_secret_key` 48 | - `:encrypted_otp_secret_key_iv` 49 | - `:encrypted_otp_secret_key_salt` 50 | - `:direct_otp` 51 | - `:direct_otp_sent_at` 52 | - `:totp_timestamp` 53 | 54 | #### Manual initial setup 55 | 56 | If you prefer to set up the model and migration manually, add the 57 | `:two_factor_authenticatable` option to your existing devise options, such as: 58 | 59 | ```ruby 60 | devise :database_authenticatable, :registerable, :recoverable, :rememberable, 61 | :trackable, :validatable, :two_factor_authenticatable 62 | ``` 63 | 64 | Then create your migration file using the Rails generator, such as: 65 | 66 | ``` 67 | rails g migration AddTwoFactorFieldsToUsers second_factor_attempts_count:integer encrypted_otp_secret_key:string:index encrypted_otp_secret_key_iv:string encrypted_otp_secret_key_salt:string direct_otp:string direct_otp_sent_at:datetime totp_timestamp:timestamp 68 | ``` 69 | 70 | Open your migration file (it will be in the `db/migrate` directory and will be 71 | named something like `20151230163930_add_two_factor_fields_to_users.rb`), and 72 | add `unique: true` to the `add_index` line so that it looks like this: 73 | 74 | ```ruby 75 | add_index :users, :encrypted_otp_secret_key, unique: true 76 | ``` 77 | Save the file. 78 | 79 | #### Complete the setup 80 | 81 | Run the migration with: 82 | 83 | bundle exec rake db:migrate 84 | 85 | Add the following line to your model to fully enable two-factor auth: 86 | 87 | has_one_time_password(encrypted: true) 88 | 89 | Set config values in `config/initializers/devise.rb`: 90 | 91 | ```ruby 92 | config.max_login_attempts = 3 # Maximum second factor attempts count. 93 | config.allowed_otp_drift_seconds = 30 # Allowed TOTP time drift between client and server. 94 | config.otp_length = 6 # TOTP code length 95 | config.direct_otp_valid_for = 5.minutes # Time before direct OTP becomes invalid 96 | config.direct_otp_length = 6 # Direct OTP code length 97 | config.remember_otp_session_for_seconds = 30.days # Time before browser has to perform 2fA again. Default is 0. 98 | config.otp_secret_encryption_key = ENV['OTP_SECRET_ENCRYPTION_KEY'] 99 | config.second_factor_resource_id = 'id' # Field or method name used to set value for 2fA remember cookie 100 | config.delete_cookie_on_logout = false # Delete cookie when user signs out, to force 2fA again on login 101 | ``` 102 | The `otp_secret_encryption_key` must be a random key that is not stored in the 103 | DB, and is not checked in to your repo. It is recommended to store it in an 104 | environment variable, and you can generate it with `bundle exec rake secret`. 105 | 106 | Override the method in your model in order to send direct OTP codes. This is 107 | automatically called when a user logs in unless they have TOTP enabled (see 108 | below): 109 | 110 | ```ruby 111 | def send_two_factor_authentication_code(code) 112 | # Send code via SMS, etc. 113 | end 114 | ``` 115 | 116 | ### Customisation and Usage 117 | 118 | By default, second factor authentication is required for each user. You can 119 | change that by overriding the following method in your model: 120 | 121 | ```ruby 122 | def need_two_factor_authentication?(request) 123 | request.ip != '127.0.0.1' 124 | end 125 | ``` 126 | 127 | In the example above, two factor authentication will not be required for local 128 | users. 129 | 130 | This gem is compatible with [Google Authenticator](https://support.google.com/accounts/answer/1066447?hl=en). 131 | To enable this a shared secret must be generated by invoking the following 132 | method on your model: 133 | 134 | ```ruby 135 | user.generate_totp_secret 136 | ``` 137 | 138 | This must then be shared via a provisioning uri: 139 | 140 | ```ruby 141 | user.provisioning_uri # This assumes a user model with an email attribute 142 | ``` 143 | 144 | This provisioning uri can then be turned in to a QR code if desired so that 145 | users may add the app to Google Authenticator easily. Once this is done, they 146 | may retrieve a one-time password directly from the Google Authenticator app. 147 | 148 | #### Overriding the view 149 | 150 | The default view that shows the form can be overridden by adding a 151 | file named `show.html.erb` (or `show.html.haml` if you prefer HAML) 152 | inside `app/views/devise/two_factor_authentication/` and customizing it. 153 | Below is an example using ERB: 154 | 155 | 156 | ```html 157 |

Hi, you received a code by email, please enter it below, thanks!

158 | 159 | <%= form_tag([resource_name, :two_factor_authentication], :method => :put) do %> 160 | <%= text_field_tag :code %> 161 | <%= submit_tag "Log in!" %> 162 | <% end %> 163 | 164 | <%= link_to "Sign out", destroy_user_session_path, :method => :delete %> 165 | ``` 166 | 167 | #### Upgrading from version 1.X to 2.X 168 | 169 | The following database fields are new in version 2. 170 | 171 | - `direct_otp` 172 | - `direct_otp_sent_at` 173 | - `totp_timestamp` 174 | 175 | To add them, generate a migration such as: 176 | 177 | $ rails g migration AddTwoFactorFieldsToUsers direct_otp:string direct_otp_sent_at:datetime totp_timestamp:timestamp 178 | 179 | The `otp_secret_key` is only required for users who use TOTP (Google Authenticator) codes, 180 | so unless it has been shared with the user it should be set to `nil`. The 181 | following pseudo-code is an example of how this might be done: 182 | 183 | ```ruby 184 | User.find_each do |user| do 185 | if !uses_authenticator_app(user) 186 | user.otp_secret_key = nil 187 | user.save! 188 | end 189 | end 190 | ``` 191 | 192 | #### Adding the TOTP encryption option to an existing app 193 | 194 | If you've already been using this gem, and want to start encrypting the OTP 195 | secret key in the database (recommended), you'll need to perform the following 196 | steps: 197 | 198 | 1. Generate a migration to add the necessary columns to your model's table: 199 | 200 | ``` 201 | rails g migration AddEncryptionFieldsToUsers encrypted_otp_secret_key:string:index encrypted_otp_secret_key_iv:string encrypted_otp_secret_key_salt:string 202 | ``` 203 | 204 | Open your migration file (it will be in the `db/migrate` directory and will be 205 | named something like `20151230163930_add_encryption_fields_to_users.rb`), and 206 | add `unique: true` to the `add_index` line so that it looks like this: 207 | 208 | ```ruby 209 | add_index :users, :encrypted_otp_secret_key, unique: true 210 | ``` 211 | Save the file. 212 | 213 | 2. Run the migration: `bundle exec rake db:migrate` 214 | 215 | 2. Update the gem: `bundle update two_factor_authentication` 216 | 217 | 3. Add `encrypted: true` to `has_one_time_password` in your model. 218 | For example: `has_one_time_password(encrypted: true)` 219 | 220 | 4. Generate a migration to populate the new encryption fields: 221 | ``` 222 | rails g migration PopulateEncryptedOtpFields 223 | ``` 224 | 225 | Open the generated file, and replace its contents with the following: 226 | ```ruby 227 | class PopulateEncryptedOtpFields < ActiveRecord::Migration 228 | def up 229 | User.reset_column_information 230 | 231 | User.find_each do |user| 232 | user.otp_secret_key = user.read_attribute('otp_secret_key') 233 | user.save! 234 | end 235 | end 236 | 237 | def down 238 | User.reset_column_information 239 | 240 | User.find_each do |user| 241 | user.otp_secret_key = ROTP::Base32.random_base32 242 | user.save! 243 | end 244 | end 245 | end 246 | ``` 247 | 248 | 5. Generate a migration to remove the `:otp_secret_key` column: 249 | ``` 250 | rails g migration RemoveOtpSecretKeyFromUsers otp_secret_key:string 251 | ``` 252 | 253 | 6. Run the migrations: `bundle exec rake db:migrate` 254 | 255 | If, for some reason, you want to switch back to the old non-encrypted version, 256 | use these steps: 257 | 258 | 1. Remove `(encrypted: true)` from `has_one_time_password` 259 | 260 | 2. Roll back the last 3 migrations (assuming you haven't added any new ones 261 | after them): 262 | ``` 263 | bundle exec rake db:rollback STEP=3 264 | ``` 265 | 266 | #### Critical Security Note! Add before_action to your user registration controllers 267 | 268 | You should have a file registrations_controller.rb in your controllers folder 269 | to overwrite/customize user registrations. It should include the lines below, for 2FA protection of user model updates, meaning that users can only access the users/edit page after confirming 2FA fully, not simply by logging in. Otherwise the entire 2FA system can be bypassed! 270 | 271 | ```ruby 272 | class RegistrationsController < Devise::RegistrationsController 273 | before_action :confirm_two_factor_authenticated, except: [:new, :create, :cancel] 274 | 275 | protected 276 | 277 | def confirm_two_factor_authenticated 278 | return if is_fully_authenticated? 279 | 280 | flash[:error] = t('devise.errors.messages.user_not_authenticated') 281 | redirect_to user_two_factor_authentication_url 282 | end 283 | end 284 | ``` 285 | 286 | #### Critical Security Note! Add 2FA validation to your custom user actions 287 | 288 | Make sure you are passing the 2FA secret codes securely and checking for them upon critical user actions, such as API key updates, user email or pgp pubkey updates, or any other changess to private/secure account-related details. Validate the secret during the initial 2FA key/secret verification by the user also, of course. 289 | 290 | For example, a simple account_controller.rb may look something like this: 291 | 292 | ``` 293 | require 'json' 294 | 295 | class AccountController < ApplicationController 296 | before_action :require_signed_in! 297 | before_action :authenticate_user! 298 | respond_to :html, :json 299 | 300 | def account_API 301 | resp = {} 302 | begin 303 | if(account_params["twoFAKey"] && account_params["twoFASecret"]) 304 | current_user.otp_secret_key = account_params["twoFAKey"] 305 | if(current_user.authenticate_totp(account_params["twoFASecret"])) 306 | # user has validated their temporary 2FA code, save it to their account, enable 2FA on this account 307 | current_user.save! 308 | resp['success'] = "passed 2FA validation!" 309 | else 310 | resp['error'] = "failed 2FA validation!" 311 | end 312 | elsif(param[:userAccountStuff] && param[:userAccountWidget]) 313 | #before updating important user account stuff and widgets, 314 | #check to see that the 2FA secret has also been passed in, and verify it... 315 | if(account_params["twoFASecret"] && current_user.totp_enabled? && current_user.authenticate_totp(account_params["twoFASecret"])) 316 | # user has passed 2FA checks, do cool user account stuff here 317 | ... 318 | else 319 | # user failed 2FA check! No cool user stuff happens! 320 | resp[error] = 'You failed 2FA validation!' 321 | end 322 | 323 | ... 324 | end 325 | else 326 | resp['error'] = 'unknown format error, not saved!' 327 | end 328 | rescue Exception => e 329 | puts "WARNING: account api threw error : '#{e}' for user #{current_user.username}" 330 | #print "error trace: #{e.backtrace}\n" 331 | resp['error'] = "unanticipated server response" 332 | end 333 | render json: resp.to_json 334 | end 335 | 336 | def account_params 337 | params.require(:twoFA).permit(:userAccountStuff, :userAcountWidget, :twoFAKey, :twoFASecret) 338 | end 339 | end 340 | ``` 341 | 342 | 343 | ### Example App 344 | 345 | [TwoFactorAuthenticationExample](https://github.com/Houdini/TwoFactorAuthenticationExample) 346 | 347 | 348 | ### Example user actions 349 | 350 | to use an ENV VAR for the 2FA encryption key: 351 | 352 | config.otp_secret_encryption_key = ENV['OTP_SECRET_ENCRYPTION_KEY'] 353 | 354 | to set up TOTP for Google Authenticator for user: 355 | 356 | ``` 357 | current_user.otp_secret_key = current_user.generate_totp_secret 358 | current_user.save! 359 | ``` 360 | 361 | ( encrypted db fields are set upon user model save action, 362 | rails c access relies on setting env var: OTP_SECRET_ENCRYPTION_KEY ) 363 | 364 | to check if user has input the correct code (from the QR display page) 365 | before saving the user model: 366 | 367 | ``` 368 | current_user.authenticate_totp('123456') 369 | ``` 370 | 371 | additional note: 372 | 373 | ``` 374 | current_user.otp_secret_key 375 | ``` 376 | 377 | This returns the OTP secret key in plaintext for the user (if you have set the env var) in the console 378 | the string used for generating the QR given to the user for their Google Auth is something like: 379 | 380 | otpauth://totp/LABEL?secret=p6wwetjnkjnrcmpd (example secret used here) 381 | 382 | where LABEL should be something like "example.com (Username)", which shows up in their GA app to remind them the code is for example.com 383 | 384 | this returns true or false with an allowed_otp_drift_seconds 'grace period' 385 | 386 | to set TOTP to DISABLED for a user account: 387 | 388 | ``` 389 | current_user.second_factor_attempts_count=nil 390 | current_user.encrypted_otp_secret_key=nil 391 | current_user.encrypted_otp_secret_key_iv=nil 392 | current_user.encrypted_otp_secret_key_salt=nil 393 | current_user.direct_otp=nil 394 | current_user.direct_otp_sent_at=nil 395 | current_user.totp_timestamp=nil 396 | current_user.direct_otp=nil 397 | current_user.otp_secret_key=nil 398 | current_user.save! (if in ruby code instead of console) 399 | current_user.direct_otp? => false 400 | current_user.totp_enabled? => false 401 | ``` 402 | 403 | 404 | 405 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | APP_RAKEFILE = File.expand_path("../spec/rails_app/Rakefile", __FILE__) 4 | load 'rails/tasks/engine.rake' 5 | 6 | require 'rspec/core/rake_task' 7 | 8 | desc "Run all specs in spec directory (excluding plugin specs)" 9 | RSpec::Core::RakeTask.new(:spec => 'app:db:test:prepare') 10 | 11 | task :default => :spec 12 | 13 | # To test against a specific version of Rails 14 | # export RAILS_VERSION=3.2.0; bundle update; rake 15 | -------------------------------------------------------------------------------- /app/controllers/devise/two_factor_authentication_controller.rb: -------------------------------------------------------------------------------- 1 | require 'devise/version' 2 | 3 | class Devise::TwoFactorAuthenticationController < DeviseController 4 | prepend_before_action :authenticate_scope! 5 | before_action :prepare_and_validate, :handle_two_factor_authentication 6 | 7 | def show 8 | end 9 | 10 | def update 11 | render :show and return if params[:code].nil? 12 | 13 | if resource.authenticate_otp(params[:code]) 14 | after_two_factor_success_for(resource) 15 | else 16 | after_two_factor_fail_for(resource) 17 | end 18 | end 19 | 20 | def resend_code 21 | resource.send_new_otp 22 | redirect_to send("#{resource_name}_two_factor_authentication_path"), notice: I18n.t('devise.two_factor_authentication.code_has_been_sent') 23 | end 24 | 25 | private 26 | 27 | def after_two_factor_success_for(resource) 28 | set_remember_two_factor_cookie(resource) 29 | 30 | warden.session(resource_name)[TwoFactorAuthentication::NEED_AUTHENTICATION] = false 31 | # For compatability with devise versions below v4.2.0 32 | # https://github.com/plataformatec/devise/commit/2044fffa25d781fcbaf090e7728b48b65c854ccb 33 | if respond_to?(:bypass_sign_in) 34 | bypass_sign_in(resource, scope: resource_name) 35 | else 36 | sign_in(resource_name, resource, bypass: true) 37 | end 38 | set_flash_message :notice, :success 39 | resource.update_attribute(:second_factor_attempts_count, 0) 40 | 41 | redirect_to after_two_factor_success_path_for(resource) 42 | end 43 | 44 | def set_remember_two_factor_cookie(resource) 45 | expires_seconds = resource.class.remember_otp_session_for_seconds 46 | 47 | if expires_seconds && expires_seconds > 0 48 | cookies.signed[TwoFactorAuthentication::REMEMBER_TFA_COOKIE_NAME] = { 49 | value: "#{resource.class}-#{resource.public_send(Devise.second_factor_resource_id)}", 50 | expires: expires_seconds.seconds.from_now 51 | } 52 | end 53 | end 54 | 55 | def after_two_factor_success_path_for(resource) 56 | stored_location_for(resource_name) || :root 57 | end 58 | 59 | def after_two_factor_fail_for(resource) 60 | resource.second_factor_attempts_count += 1 61 | resource.save 62 | set_flash_message :alert, :attempt_failed, now: true 63 | 64 | if resource.max_login_attempts? 65 | sign_out(resource) 66 | render :max_login_attempts_reached 67 | else 68 | render :show 69 | end 70 | end 71 | 72 | def authenticate_scope! 73 | self.resource = send("current_#{resource_name}") 74 | end 75 | 76 | def prepare_and_validate 77 | redirect_to :root and return if resource.nil? 78 | @limit = resource.max_login_attempts 79 | if resource.max_login_attempts? 80 | sign_out(resource) 81 | render :max_login_attempts_reached and return 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /app/views/devise/two_factor_authentication/max_login_attempts_reached.html.erb: -------------------------------------------------------------------------------- 1 |

<%= I18n.t("devise.two_factor_authentication.max_login_attempts_reached") %> = <%= @limit %>.

2 |

<%= I18n.t("devise.two_factor_authentication.contact_administrator") %>

3 | 4 | -------------------------------------------------------------------------------- /app/views/devise/two_factor_authentication/show.html.erb: -------------------------------------------------------------------------------- 1 | <% if resource.direct_otp %> 2 |

Enter the code that was sent to you

3 | <% else %> 4 |

Enter the code from your authenticator app

5 | <% end %> 6 | 7 |

<%= flash[:notice] %>

8 | 9 | <%= form_tag([resource_name, :two_factor_authentication], :method => :put) do %> 10 | <%= text_field_tag :code, '', autofocus: true %> 11 | <%= submit_tag "Submit" %> 12 | <% end %> 13 | 14 | <% if resource.direct_otp %> 15 | <%= link_to "Resend Code", send("resend_code_#{resource_name}_two_factor_authentication_path"), action: :get %> 16 | <% else %> 17 | <%= link_to "Send me a code instead", send("resend_code_#{resource_name}_two_factor_authentication_path"), action: :get %> 18 | <% end %> 19 | <%= link_to "Sign out", send("destroy_#{resource_name}_session_path"), :method => :delete %> 20 | -------------------------------------------------------------------------------- /config/locales/de.yml: -------------------------------------------------------------------------------- 1 | de: 2 | devise: 3 | two_factor_authentication: 4 | success: "Ihre Zwei-Faktor-Authentifizierung war erfolgreich." 5 | attempt_failed: "Authentifizierungsversuch fehlgeschlagen." 6 | max_login_attempts_reached: "Ihr Zugang wurde ganz verweigert, da Sie Ihr Versuchslimit erreicht haben." 7 | contact_administrator: "Kontaktieren Sie bitte einen Ihrer Administratoren." 8 | code_has_been_sent: "Ihr Einmal-Passwort wurde verschickt." 9 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | devise: 3 | two_factor_authentication: 4 | success: "Two factor authentication successful." 5 | attempt_failed: "Attempt failed." 6 | max_login_attempts_reached: "Access completely denied as you have reached your attempts limit" 7 | contact_administrator: "Please contact your system administrator." 8 | code_has_been_sent: "Your authentication code has been sent." 9 | -------------------------------------------------------------------------------- /config/locales/es.yml: -------------------------------------------------------------------------------- 1 | es: 2 | devise: 3 | two_factor_authentication: 4 | success: "Autenticación multi-factor realizada exitosamente." 5 | attempt_failed: "La autenticación ha fallado." 6 | max_login_attempts_reached: "Has llegado al límite de intentos fallidos, acceso denegado." 7 | contact_administrator: "Contacte a su administrador de sistema." 8 | code_has_been_sent: "El código de autenticación ha sido enviado." 9 | -------------------------------------------------------------------------------- /config/locales/fr.yml: -------------------------------------------------------------------------------- 1 | fr: 2 | devise: 3 | two_factor_authentication: 4 | success: "Validation en deux étapes effectuée avec succès." 5 | attempt_failed: "La connexion a échoué." 6 | max_login_attempts_reached: "Limite de tentatives atteinte, accès refusé." 7 | contact_administrator: "Merci de contacter votre administrateur système." 8 | code_has_been_sent: "Votre code de validation envoyé." 9 | -------------------------------------------------------------------------------- /config/locales/ru.yml: -------------------------------------------------------------------------------- 1 | ru: 2 | devise: 3 | two_factor_authentication: 4 | success: "Двухфакторная авторизация успешно пройдена." 5 | attempt_failed: "Неверный код." 6 | max_login_attempts_reached: "Доступ заблокирован. Превышено число попыток авторизации" 7 | contact_administrator: "Пожалуйста, свяжитесь с системным администратором." 8 | code_has_been_sent: "Ваш персональный код был отправлен." 9 | -------------------------------------------------------------------------------- /lib/generators/active_record/templates/migration.rb: -------------------------------------------------------------------------------- 1 | class TwoFactorAuthenticationAddTo<%= table_name.camelize %> < ActiveRecord::Migration 2 | disable_ddl_transaction! 3 | 4 | def change 5 | add_column :<%= table_name %>, :second_factor_attempts_count, :integer, default: 0 6 | add_column :<%= table_name %>, :encrypted_otp_secret_key, :string 7 | add_column :<%= table_name %>, :encrypted_otp_secret_key_iv, :string 8 | add_column :<%= table_name %>, :encrypted_otp_secret_key_salt, :string 9 | add_column :<%= table_name %>, :direct_otp, :string 10 | add_column :<%= table_name %>, :direct_otp_sent_at, :datetime 11 | add_column :<%= table_name %>, :totp_timestamp, :timestamp 12 | 13 | add_index :<%= table_name %>, :encrypted_otp_secret_key, unique: true, algorithm: :concurrently 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/generators/active_record/two_factor_authentication_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators/active_record' 2 | 3 | module ActiveRecord 4 | module Generators 5 | class TwoFactorAuthenticationGenerator < ActiveRecord::Generators::Base 6 | source_root File.expand_path("../templates", __FILE__) 7 | 8 | def copy_two_factor_authentication_migration 9 | migration_template "migration.rb", "db/migrate/two_factor_authentication_add_to_#{table_name}.rb" 10 | end 11 | 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/generators/two_factor_authentication/two_factor_authentication_generator.rb: -------------------------------------------------------------------------------- 1 | module TwoFactorAuthenticatable 2 | module Generators 3 | class TwoFactorAuthenticationGenerator < Rails::Generators::NamedBase 4 | namespace "two_factor_authentication" 5 | 6 | desc "Adds :two_factor_authenticable directive in the given model. It also generates an active record migration." 7 | 8 | def inject_two_factor_authentication_content 9 | path = File.join("app", "models", "#{file_path}.rb") 10 | inject_into_file(path, "two_factor_authenticatable, :", :after => "devise :") if File.exists?(path) 11 | end 12 | 13 | hook_for :orm 14 | 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/two_factor_authentication.rb: -------------------------------------------------------------------------------- 1 | require 'two_factor_authentication/version' 2 | require 'devise' 3 | require 'active_support/concern' 4 | require "active_model" 5 | require "active_support/core_ext/class/attribute_accessors" 6 | require "cgi" 7 | 8 | module Devise 9 | mattr_accessor :max_login_attempts 10 | @@max_login_attempts = 3 11 | 12 | mattr_accessor :allowed_otp_drift_seconds 13 | @@allowed_otp_drift_seconds = 30 14 | 15 | mattr_accessor :otp_length 16 | @@otp_length = 6 17 | 18 | mattr_accessor :direct_otp_length 19 | @@direct_otp_length = 6 20 | 21 | mattr_accessor :direct_otp_valid_for 22 | @@direct_otp_valid_for = 5.minutes 23 | 24 | mattr_accessor :remember_otp_session_for_seconds 25 | @@remember_otp_session_for_seconds = 0 26 | 27 | mattr_accessor :otp_secret_encryption_key 28 | @@otp_secret_encryption_key = '' 29 | 30 | mattr_accessor :second_factor_resource_id 31 | @@second_factor_resource_id = 'id' 32 | 33 | mattr_accessor :delete_cookie_on_logout 34 | @@delete_cookie_on_logout = false 35 | end 36 | 37 | module TwoFactorAuthentication 38 | NEED_AUTHENTICATION = 'need_two_factor_authentication' 39 | REMEMBER_TFA_COOKIE_NAME = "remember_tfa" 40 | 41 | autoload :Schema, 'two_factor_authentication/schema' 42 | module Controllers 43 | autoload :Helpers, 'two_factor_authentication/controllers/helpers' 44 | end 45 | end 46 | 47 | Devise.add_module :two_factor_authenticatable, :model => 'two_factor_authentication/models/two_factor_authenticatable', :controller => :two_factor_authentication, :route => :two_factor_authentication 48 | 49 | require 'two_factor_authentication/orm/active_record' if defined?(ActiveRecord::Base) 50 | require 'two_factor_authentication/routes' 51 | require 'two_factor_authentication/models/two_factor_authenticatable' 52 | require 'two_factor_authentication/rails' 53 | -------------------------------------------------------------------------------- /lib/two_factor_authentication/controllers/helpers.rb: -------------------------------------------------------------------------------- 1 | module TwoFactorAuthentication 2 | module Controllers 3 | module Helpers 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | before_action :handle_two_factor_authentication 8 | end 9 | 10 | private 11 | 12 | def handle_two_factor_authentication 13 | unless devise_controller? 14 | Devise.mappings.keys.flatten.any? do |scope| 15 | if signed_in?(scope) and warden.session(scope)[TwoFactorAuthentication::NEED_AUTHENTICATION] 16 | handle_failed_second_factor(scope) 17 | end 18 | end 19 | end 20 | end 21 | 22 | def handle_failed_second_factor(scope) 23 | if request.format.present? 24 | if request.format.html? 25 | session["#{scope}_return_to"] = request.original_fullpath if request.get? 26 | redirect_to two_factor_authentication_path_for(scope) 27 | elsif request.format.json? 28 | session["#{scope}_return_to"] = root_path(format: :html) 29 | render json: { redirect_to: two_factor_authentication_path_for(scope) }, status: :unauthorized 30 | end 31 | else 32 | head :unauthorized 33 | end 34 | end 35 | 36 | def two_factor_authentication_path_for(resource_or_scope = nil) 37 | scope = Devise::Mapping.find_scope!(resource_or_scope) 38 | change_path = "#{scope}_two_factor_authentication_path" 39 | send(change_path) 40 | end 41 | 42 | end 43 | end 44 | end 45 | 46 | module Devise 47 | module Controllers 48 | module Helpers 49 | def is_fully_authenticated? 50 | !session["warden.user.user.session"].try(:[], TwoFactorAuthentication::NEED_AUTHENTICATION) 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/two_factor_authentication/hooks/two_factor_authenticatable.rb: -------------------------------------------------------------------------------- 1 | Warden::Manager.after_authentication do |user, auth, options| 2 | if auth.env["action_dispatch.cookies"] 3 | expected_cookie_value = "#{user.class}-#{user.public_send(Devise.second_factor_resource_id)}" 4 | actual_cookie_value = auth.env["action_dispatch.cookies"].signed[TwoFactorAuthentication::REMEMBER_TFA_COOKIE_NAME] 5 | bypass_by_cookie = actual_cookie_value == expected_cookie_value 6 | end 7 | 8 | if user.respond_to?(:need_two_factor_authentication?) && !bypass_by_cookie 9 | if auth.session(options[:scope])[TwoFactorAuthentication::NEED_AUTHENTICATION] = user.need_two_factor_authentication?(auth.request) 10 | user.send_new_otp if user.send_new_otp_after_login? 11 | end 12 | end 13 | end 14 | 15 | Warden::Manager.before_logout do |user, auth, _options| 16 | auth.cookies.delete TwoFactorAuthentication::REMEMBER_TFA_COOKIE_NAME if Devise.delete_cookie_on_logout 17 | end 18 | -------------------------------------------------------------------------------- /lib/two_factor_authentication/models/two_factor_authenticatable.rb: -------------------------------------------------------------------------------- 1 | require 'two_factor_authentication/hooks/two_factor_authenticatable' 2 | require 'rotp' 3 | require 'encryptor' 4 | 5 | module Devise 6 | module Models 7 | module TwoFactorAuthenticatable 8 | extend ActiveSupport::Concern 9 | 10 | module ClassMethods 11 | def has_one_time_password(options = {}) 12 | include InstanceMethodsOnActivation 13 | include EncryptionInstanceMethods if options[:encrypted] == true 14 | end 15 | 16 | ::Devise::Models.config( 17 | self, :max_login_attempts, :allowed_otp_drift_seconds, :otp_length, 18 | :remember_otp_session_for_seconds, :otp_secret_encryption_key, 19 | :direct_otp_length, :direct_otp_valid_for, :totp_timestamp, :delete_cookie_on_logout 20 | ) 21 | end 22 | 23 | module InstanceMethodsOnActivation 24 | def authenticate_otp(code, options = {}) 25 | return true if direct_otp && authenticate_direct_otp(code) 26 | return true if totp_enabled? && authenticate_totp(code, options) 27 | false 28 | end 29 | 30 | def authenticate_direct_otp(code) 31 | return false if direct_otp.nil? || direct_otp != code || direct_otp_expired? 32 | clear_direct_otp 33 | true 34 | end 35 | 36 | def authenticate_totp(code, options = {}) 37 | totp_secret = options[:otp_secret_key] || otp_secret_key 38 | digits = options[:otp_length] || self.class.otp_length 39 | drift = options[:drift] || self.class.allowed_otp_drift_seconds 40 | raise "authenticate_totp called with no otp_secret_key set" if totp_secret.nil? 41 | totp = ROTP::TOTP.new(totp_secret, digits: digits) 42 | new_timestamp = totp.verify( 43 | without_spaces(code), 44 | drift_ahead: drift, drift_behind: drift, after: totp_timestamp 45 | ) 46 | return false unless new_timestamp 47 | self.totp_timestamp = new_timestamp 48 | true 49 | end 50 | 51 | def provisioning_uri(account = nil, options = {}) 52 | totp_secret = options[:otp_secret_key] || otp_secret_key 53 | options[:digits] ||= options[:otp_length] || self.class.otp_length 54 | raise "provisioning_uri called with no otp_secret_key set" if totp_secret.nil? 55 | account ||= email if respond_to?(:email) 56 | ROTP::TOTP.new(totp_secret, options).provisioning_uri(account) 57 | end 58 | 59 | def need_two_factor_authentication?(request) 60 | true 61 | end 62 | 63 | def send_new_otp(options = {}) 64 | create_direct_otp options 65 | send_two_factor_authentication_code(direct_otp) 66 | end 67 | 68 | def send_new_otp_after_login? 69 | !totp_enabled? 70 | end 71 | 72 | def send_two_factor_authentication_code(code) 73 | raise NotImplementedError.new("No default implementation - please define in your class.") 74 | end 75 | 76 | def max_login_attempts? 77 | second_factor_attempts_count.to_i >= max_login_attempts.to_i 78 | end 79 | 80 | def max_login_attempts 81 | self.class.max_login_attempts 82 | end 83 | 84 | def totp_enabled? 85 | respond_to?(:otp_secret_key) && !otp_secret_key.nil? 86 | end 87 | 88 | def confirm_totp_secret(secret, code, options = {}) 89 | return false unless authenticate_totp(code, {otp_secret_key: secret}) 90 | self.otp_secret_key = secret 91 | true 92 | end 93 | 94 | def generate_totp_secret 95 | # ROTP gem since version 5 to version 5.1 96 | # at version 5.1 ROTP gem reinstates. 97 | # Details: https://github.com/mdp/rotp/blob/master/CHANGELOG.md#510 98 | ROTP::Base32.try(:random) || ROTP::Base32.random_base32 99 | end 100 | 101 | def create_direct_otp(options = {}) 102 | # Create a new random OTP and store it in the database 103 | digits = options[:length] || self.class.direct_otp_length || 6 104 | update_attributes( 105 | direct_otp: random_base10(digits), 106 | direct_otp_sent_at: Time.now.utc 107 | ) 108 | end 109 | 110 | private 111 | 112 | def without_spaces(code) 113 | code.gsub(/\s/, '') 114 | end 115 | 116 | def random_base10(digits) 117 | SecureRandom.random_number(10**digits).to_s.rjust(digits, '0') 118 | end 119 | 120 | def direct_otp_expired? 121 | Time.now.utc > direct_otp_sent_at + self.class.direct_otp_valid_for 122 | end 123 | 124 | def clear_direct_otp 125 | update_attributes(direct_otp: nil, direct_otp_sent_at: nil) 126 | end 127 | end 128 | 129 | module EncryptionInstanceMethods 130 | def otp_secret_key 131 | otp_decrypt(encrypted_otp_secret_key) 132 | end 133 | 134 | def otp_secret_key=(value) 135 | self.encrypted_otp_secret_key = otp_encrypt(value) 136 | end 137 | 138 | private 139 | 140 | def otp_decrypt(encrypted_value) 141 | return encrypted_value if encrypted_value.blank? 142 | 143 | encrypted_value = encrypted_value.unpack('m').first 144 | 145 | value = ::Encryptor.decrypt(encryption_options_for(encrypted_value)) 146 | 147 | if defined?(Encoding) 148 | encoding = Encoding.default_internal || Encoding.default_external 149 | value = value.force_encoding(encoding.name) 150 | end 151 | 152 | value 153 | end 154 | 155 | def otp_encrypt(value) 156 | return value if value.blank? 157 | 158 | value = value.to_s 159 | encrypted_value = ::Encryptor.encrypt(encryption_options_for(value)) 160 | 161 | encrypted_value = [encrypted_value].pack('m') 162 | 163 | encrypted_value 164 | end 165 | 166 | def encryption_options_for(value) 167 | { 168 | value: value, 169 | key: Devise.otp_secret_encryption_key, 170 | iv: iv_for_attribute, 171 | salt: salt_for_attribute, 172 | algorithm: 'aes-256-cbc' 173 | } 174 | end 175 | 176 | def iv_for_attribute(algorithm = 'aes-256-cbc') 177 | iv = encrypted_otp_secret_key_iv 178 | 179 | if iv.nil? 180 | algo = OpenSSL::Cipher.new(algorithm) 181 | iv = [algo.random_iv].pack('m') 182 | self.encrypted_otp_secret_key_iv = iv 183 | end 184 | 185 | iv.unpack('m').first if iv.present? 186 | end 187 | 188 | def salt_for_attribute 189 | salt = encrypted_otp_secret_key_salt || 190 | self.encrypted_otp_secret_key_salt = generate_random_base64_encoded_salt 191 | 192 | decode_salt_if_encoded(salt) 193 | end 194 | 195 | def generate_random_base64_encoded_salt 196 | prefix = '_' 197 | prefix + [SecureRandom.random_bytes].pack('m') 198 | end 199 | 200 | def decode_salt_if_encoded(salt) 201 | salt.slice(0).eql?('_') ? salt.slice(1..-1).unpack('m').first : salt 202 | end 203 | end 204 | end 205 | end 206 | end 207 | -------------------------------------------------------------------------------- /lib/two_factor_authentication/orm/active_record.rb: -------------------------------------------------------------------------------- 1 | require "active_record" 2 | 3 | module TwoFactorAuthentication 4 | module Orm 5 | module ActiveRecord 6 | module Schema 7 | include TwoFactorAuthentication::Schema 8 | end 9 | end 10 | end 11 | end 12 | 13 | ActiveRecord::ConnectionAdapters::Table.send :include, TwoFactorAuthentication::Orm::ActiveRecord::Schema 14 | ActiveRecord::ConnectionAdapters::TableDefinition.send :include, TwoFactorAuthentication::Orm::ActiveRecord::Schema 15 | -------------------------------------------------------------------------------- /lib/two_factor_authentication/rails.rb: -------------------------------------------------------------------------------- 1 | module TwoFactorAuthentication 2 | class Engine < ::Rails::Engine 3 | ActiveSupport.on_load(:action_controller) do 4 | include TwoFactorAuthentication::Controllers::Helpers 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/two_factor_authentication/routes.rb: -------------------------------------------------------------------------------- 1 | module ActionDispatch::Routing 2 | class Mapper 3 | protected 4 | 5 | def devise_two_factor_authentication(mapping, controllers) 6 | resource :two_factor_authentication, :only => [:show, :update, :resend_code], :path => mapping.path_names[:two_factor_authentication], :controller => controllers[:two_factor_authentication] do 7 | collection { get "resend_code" } 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/two_factor_authentication/schema.rb: -------------------------------------------------------------------------------- 1 | module TwoFactorAuthentication 2 | module Schema 3 | def second_factor_attempts_count 4 | apply_devise_schema :second_factor_attempts_count, Integer, :default => 0 5 | end 6 | 7 | def encrypted_otp_secret_key 8 | apply_devise_schema :encrypted_otp_secret_key, String 9 | end 10 | 11 | def encrypted_otp_secret_key_iv 12 | apply_devise_schema :encrypted_otp_secret_key_iv, String 13 | end 14 | 15 | def encrypted_otp_secret_key_salt 16 | apply_devise_schema :encrypted_otp_secret_key_salt, String 17 | end 18 | 19 | def direct_otp 20 | apply_devise_schema :direct_otp, String 21 | end 22 | 23 | def direct_otp_sent_at 24 | apply_devise_schema :direct_otp_sent_at, DateTime 25 | end 26 | 27 | def totp_timestamp 28 | apply_devise_schema :totp_timestamp, Timestamp 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/two_factor_authentication/version.rb: -------------------------------------------------------------------------------- 1 | module TwoFactorAuthentication 2 | VERSION = "2.2.0".freeze 3 | end 4 | -------------------------------------------------------------------------------- /spec/controllers/two_factor_authentication_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Devise::TwoFactorAuthenticationController, type: :controller do 4 | describe 'is_fully_authenticated? helper' do 5 | def post_code(code) 6 | if Rails::VERSION::MAJOR >= 5 7 | post :update, params: { code: code } 8 | else 9 | post :update, code: code 10 | end 11 | end 12 | 13 | before do 14 | sign_in 15 | end 16 | 17 | context 'after user enters valid OTP code' do 18 | it 'returns true' do 19 | controller.current_user.send_new_otp 20 | post_code controller.current_user.direct_otp 21 | expect(subject.is_fully_authenticated?).to eq true 22 | end 23 | end 24 | 25 | context 'when user has not entered any OTP yet' do 26 | it 'returns false' do 27 | get :show 28 | 29 | expect(subject.is_fully_authenticated?).to eq false 30 | end 31 | end 32 | 33 | context 'when user enters an invalid OTP' do 34 | it 'returns false' do 35 | post_code '12345' 36 | 37 | expect(subject.is_fully_authenticated?).to eq false 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/features/two_factor_authenticatable_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | include AuthenticatedModelHelper 3 | 4 | feature "User of two factor authentication" do 5 | context 'sending two factor authentication code via SMS' do 6 | shared_examples 'sends and authenticates code' do |user, type| 7 | before do 8 | user.reload 9 | if type == 'encrypted' 10 | allow(User).to receive(:has_one_time_password).with(encrypted: true) 11 | end 12 | end 13 | 14 | it 'does not send an SMS before the user has signed in' do 15 | expect(SMSProvider.messages).to be_empty 16 | end 17 | 18 | it 'sends code via SMS after sign in' do 19 | visit new_user_session_path 20 | complete_sign_in_form_for(user) 21 | 22 | expect(page).to have_content 'Enter the code that was sent to you' 23 | 24 | expect(SMSProvider.messages.size).to eq(1) 25 | message = SMSProvider.last_message 26 | expect(message.to).to eq(user.phone_number) 27 | expect(message.body).to eq(user.reload.direct_otp) 28 | end 29 | 30 | it 'authenticates a valid OTP code' do 31 | visit new_user_session_path 32 | complete_sign_in_form_for(user) 33 | 34 | expect(page).to have_content('You are signed in as Marissa') 35 | 36 | fill_in 'code', with: SMSProvider.last_message.body 37 | click_button 'Submit' 38 | 39 | within('.flash.notice') do 40 | expect(page).to have_content('Two factor authentication successful.') 41 | end 42 | 43 | expect(current_path).to eq root_path 44 | end 45 | end 46 | 47 | it_behaves_like 'sends and authenticates code', create_user('not_encrypted') 48 | it_behaves_like 'sends and authenticates code', create_user, 'encrypted' 49 | end 50 | 51 | scenario "must be logged in" do 52 | visit user_two_factor_authentication_path 53 | 54 | expect(page).to have_content("Welcome Home") 55 | expect(page).to have_content("You are signed out") 56 | end 57 | 58 | context "when logged in" do 59 | let(:user) { create_user } 60 | 61 | background do 62 | login_as user 63 | end 64 | 65 | scenario "is redirected to TFA when path requires authentication" do 66 | visit dashboard_path + "?A=param%20a&B=param%20b" 67 | 68 | expect(page).to_not have_content("Your Personal Dashboard") 69 | 70 | fill_in "code", with: SMSProvider.last_message.body 71 | click_button "Submit" 72 | 73 | expect(page).to have_content("Your Personal Dashboard") 74 | expect(page).to have_content("You are signed in as Marissa") 75 | expect(page).to have_content("Param A is param a") 76 | expect(page).to have_content("Param B is param b") 77 | end 78 | 79 | scenario "is locked out after max failed attempts" do 80 | visit user_two_factor_authentication_path 81 | 82 | max_attempts = User.max_login_attempts 83 | 84 | max_attempts.times do 85 | fill_in "code", with: "incorrect#{rand(100)}" 86 | click_button "Submit" 87 | 88 | within(".flash.alert") do 89 | expect(page).to have_content("Attempt failed") 90 | end 91 | end 92 | 93 | expect(page).to have_content("Access completely denied") 94 | expect(page).to have_content("You are signed out") 95 | end 96 | 97 | scenario "cannot retry authentication after max attempts" do 98 | user.update_attribute(:second_factor_attempts_count, User.max_login_attempts) 99 | 100 | visit user_two_factor_authentication_path 101 | 102 | expect(page).to have_content("Access completely denied") 103 | expect(page).to have_content("You are signed out") 104 | end 105 | 106 | describe "rememberable TFA" do 107 | before do 108 | @original_remember_otp_session_for_seconds = User.remember_otp_session_for_seconds 109 | User.remember_otp_session_for_seconds = 30.days 110 | end 111 | 112 | after do 113 | User.remember_otp_session_for_seconds = @original_remember_otp_session_for_seconds 114 | end 115 | 116 | scenario "doesn't require TFA code again within 30 days" do 117 | sms_sign_in 118 | 119 | logout 120 | 121 | login_as user 122 | visit dashboard_path 123 | expect(page).to have_content("Your Personal Dashboard") 124 | expect(page).to have_content("You are signed in as Marissa") 125 | end 126 | 127 | scenario "requires TFA code again after 30 days" do 128 | sms_sign_in 129 | 130 | logout 131 | 132 | Timecop.travel(30.days.from_now) 133 | login_as user 134 | visit dashboard_path 135 | expect(page).to have_content("You are signed in as Marissa") 136 | expect(page).to have_content("Enter the code that was sent to you") 137 | end 138 | 139 | scenario 'TFA should be different for different users' do 140 | sms_sign_in 141 | 142 | tfa_cookie1 = get_tfa_cookie() 143 | 144 | logout 145 | reset_session! 146 | 147 | user2 = create_user() 148 | login_as(user2) 149 | sms_sign_in 150 | 151 | tfa_cookie2 = get_tfa_cookie() 152 | 153 | expect(tfa_cookie1).not_to eq tfa_cookie2 154 | end 155 | 156 | def sms_sign_in 157 | SMSProvider.messages.clear() 158 | visit user_two_factor_authentication_path 159 | fill_in 'code', with: SMSProvider.last_message.body 160 | click_button 'Submit' 161 | end 162 | 163 | scenario 'TFA should be unique for specific user' do 164 | sms_sign_in 165 | 166 | tfa_cookie1 = get_tfa_cookie() 167 | 168 | logout 169 | reset_session! 170 | 171 | user2 = create_user() 172 | set_tfa_cookie(tfa_cookie1) 173 | login_as(user2) 174 | visit dashboard_path 175 | expect(page).to have_content("Enter the code that was sent to you") 176 | end 177 | 178 | scenario 'Delete cookie when user logs out if enabled' do 179 | user.class.delete_cookie_on_logout = true 180 | 181 | login_as user 182 | logout 183 | 184 | login_as user 185 | 186 | visit dashboard_path 187 | expect(page).to have_content("Enter the code that was sent to you") 188 | end 189 | end 190 | 191 | it 'sets the warden session need_two_factor_authentication key to true' do 192 | session_hash = { 'need_two_factor_authentication' => true } 193 | 194 | expect(page.get_rack_session_key('warden.user.user.session')).to eq session_hash 195 | end 196 | end 197 | 198 | describe 'signing in' do 199 | let(:user) { create_user } 200 | let(:admin) { create_admin } 201 | 202 | scenario 'user signs is' do 203 | visit new_user_session_path 204 | complete_sign_in_form_for(user) 205 | 206 | expect(page).to have_content('Signed in successfully.') 207 | end 208 | 209 | scenario 'admin signs in' do 210 | visit new_admin_session_path 211 | complete_sign_in_form_for(admin) 212 | 213 | expect(page).to have_content('Signed in successfully.') 214 | end 215 | end 216 | 217 | describe 'signing out' do 218 | let(:user) { create_user } 219 | let(:admin) { create_admin } 220 | 221 | scenario 'user signs out' do 222 | visit new_user_session_path 223 | complete_sign_in_form_for(user) 224 | visit destroy_user_session_path 225 | 226 | expect(page).to have_content('Signed out successfully.') 227 | end 228 | 229 | scenario 'admin signs out' do 230 | visit new_admin_session_path 231 | complete_sign_in_form_for(admin) 232 | visit destroy_admin_session_path 233 | 234 | expect(page).to have_content('Signed out successfully.') 235 | end 236 | end 237 | end 238 | -------------------------------------------------------------------------------- /spec/generators/active_record/two_factor_authentication_generator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'generators/active_record/two_factor_authentication_generator' 4 | 5 | describe ActiveRecord::Generators::TwoFactorAuthenticationGenerator, type: :generator do 6 | destination File.expand_path('../../../../../tmp', __FILE__) 7 | 8 | before do 9 | prepare_destination 10 | end 11 | 12 | it 'runs all methods in the generator' do 13 | gen = generator %w(users) 14 | expect(gen).to receive(:copy_two_factor_authentication_migration) 15 | gen.invoke_all 16 | end 17 | 18 | describe 'the generated files' do 19 | before do 20 | run_generator %w(users) 21 | end 22 | 23 | describe 'the migration' do 24 | subject { migration_file('db/migrate/two_factor_authentication_add_to_users.rb') } 25 | 26 | it { is_expected.to exist } 27 | it { is_expected.to be_a_migration } 28 | it { is_expected.to contain /def change/ } 29 | it { is_expected.to contain /add_column :users, :second_factor_attempts_count, :integer, default: 0/ } 30 | it { is_expected.to contain /add_column :users, :encrypted_otp_secret_key, :string/ } 31 | it { is_expected.to contain /add_column :users, :encrypted_otp_secret_key_iv, :string/ } 32 | it { is_expected.to contain /add_column :users, :encrypted_otp_secret_key_salt, :string/ } 33 | it { is_expected.to contain /add_index :users, :encrypted_otp_secret_key, unique: true/ } 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/lib/two_factor_authentication/models/two_factor_authenticatable_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | include AuthenticatedModelHelper 3 | 4 | describe Devise::Models::TwoFactorAuthenticatable do 5 | describe '#create_direct_otp' do 6 | let(:instance) { build_guest_user } 7 | 8 | it 'set direct_otp field' do 9 | expect(instance.direct_otp).to be_nil 10 | instance.create_direct_otp 11 | expect(instance.direct_otp).not_to be_nil 12 | end 13 | 14 | it 'set direct_otp_send_at field to current time' do 15 | Timecop.freeze() do 16 | instance.create_direct_otp 17 | expect(instance.direct_otp_sent_at).to eq(Time.now) 18 | end 19 | end 20 | 21 | it 'honors .direct_otp_length' do 22 | expect(instance.class).to receive(:direct_otp_length).and_return(10) 23 | instance.create_direct_otp 24 | expect(instance.direct_otp.length).to equal(10) 25 | 26 | expect(instance.class).to receive(:direct_otp_length).and_return(6) 27 | instance.create_direct_otp 28 | expect(instance.direct_otp.length).to equal(6) 29 | end 30 | 31 | it "honors 'direct_otp_length' in options paramater" do 32 | instance.create_direct_otp(length: 8) 33 | expect(instance.direct_otp.length).to equal(8) 34 | instance.create_direct_otp(length: 10) 35 | expect(instance.direct_otp.length).to equal(10) 36 | end 37 | end 38 | 39 | describe '#authenticate_direct_otp' do 40 | let(:instance) { build_guest_user } 41 | it 'fails if no direct_otp has been set' do 42 | expect(instance.authenticate_direct_otp('12345')).to eq(false) 43 | end 44 | 45 | context 'after generating an OTP' do 46 | before :each do 47 | instance.create_direct_otp 48 | end 49 | 50 | it 'accepts correct OTP' do 51 | Timecop.freeze(Time.now + instance.class.direct_otp_valid_for - 1.second) 52 | expect(instance.authenticate_direct_otp(instance.direct_otp)).to eq(true) 53 | end 54 | 55 | it 'rejects invalid OTP' do 56 | Timecop.freeze(Time.now + instance.class.direct_otp_valid_for - 1.second) 57 | expect(instance.authenticate_direct_otp('12340')).to eq(false) 58 | end 59 | 60 | it 'rejects expired OTP' do 61 | Timecop.freeze(Time.now + instance.class.direct_otp_valid_for + 1.second) 62 | expect(instance.authenticate_direct_otp(instance.direct_otp)).to eq(false) 63 | end 64 | 65 | it 'prevents code re-use' do 66 | expect(instance.authenticate_direct_otp(instance.direct_otp)).to eq(true) 67 | expect(instance.authenticate_direct_otp(instance.direct_otp)).to eq(false) 68 | end 69 | end 70 | end 71 | 72 | describe '#authenticate_totp' do 73 | shared_examples 'authenticate_totp' do |instance| 74 | before :each do 75 | instance.otp_secret_key = '2z6hxkdwi3uvrnpn' 76 | instance.totp_timestamp = nil 77 | @totp_helper = TotpHelper.new(instance.otp_secret_key, instance.class.otp_length) 78 | end 79 | 80 | def do_invoke(code, user) 81 | user.authenticate_totp(code) 82 | end 83 | 84 | it 'authenticates a recently created code' do 85 | code = @totp_helper.totp_code 86 | expect(do_invoke(code, instance)).to eq(true) 87 | end 88 | 89 | it 'authenticates a code entered with a space' do 90 | code = @totp_helper.totp_code.insert(3, ' ') 91 | expect(do_invoke(code, instance)).to eq(true) 92 | end 93 | 94 | it 'does not authenticate an old code' do 95 | code = @totp_helper.totp_code(1.minutes.ago.to_i) 96 | expect(do_invoke(code, instance)).to eq(false) 97 | end 98 | 99 | it 'prevents code reuse' do 100 | code = @totp_helper.totp_code 101 | expect(do_invoke(code, instance)).to eq(true) 102 | expect(do_invoke(code, instance)).to eq(false) 103 | end 104 | end 105 | 106 | it_behaves_like 'authenticate_totp', GuestUser.new 107 | it_behaves_like 'authenticate_totp', EncryptedUser.new 108 | end 109 | 110 | describe '#send_two_factor_authentication_code' do 111 | let(:instance) { build_guest_user } 112 | 113 | it 'raises an error by default' do 114 | expect { instance.send_two_factor_authentication_code(123) }. 115 | to raise_error(NotImplementedError) 116 | end 117 | 118 | it 'is overrideable' do 119 | def instance.send_two_factor_authentication_code(code) 120 | 'Code sent' 121 | end 122 | expect(instance.send_two_factor_authentication_code(123)).to eq('Code sent') 123 | end 124 | end 125 | 126 | describe '#provisioning_uri' do 127 | 128 | shared_examples 'provisioning_uri' do |instance| 129 | it 'fails until generate_totp_secret is called' do 130 | expect { instance.provisioning_uri }.to raise_error(Exception) 131 | end 132 | 133 | describe 'with secret set' do 134 | before do 135 | instance.email = 'houdini@example.com' 136 | instance.otp_secret_key = instance.generate_totp_secret 137 | end 138 | 139 | it "returns uri with user's email" do 140 | expect(instance.provisioning_uri). 141 | to match(%r{otpauth://totp/houdini@example.com\?secret=\w{32}}) 142 | end 143 | 144 | it 'returns uri with issuer option' do 145 | expect(instance.provisioning_uri('houdini')). 146 | to match(%r{otpauth://totp/houdini\?secret=\w{32}$}) 147 | end 148 | 149 | it 'returns uri with issuer option' do 150 | require 'cgi' 151 | uri = URI.parse(instance.provisioning_uri('houdini', issuer: 'Magic')) 152 | params = CGI.parse(uri.query) 153 | 154 | expect(uri.scheme).to eq('otpauth') 155 | expect(uri.host).to eq('totp') 156 | expect(uri.path).to eq('/Magic:houdini') 157 | expect(params['issuer'].shift).to eq('Magic') 158 | expect(params['secret'].shift).to match(/\w{32}/) 159 | end 160 | end 161 | end 162 | 163 | it_behaves_like 'provisioning_uri', GuestUser.new 164 | it_behaves_like 'provisioning_uri', EncryptedUser.new 165 | end 166 | 167 | describe '#generate_totp_secret' do 168 | shared_examples 'generate_totp_secret' do |klass| 169 | let(:instance) { klass.new } 170 | 171 | it 'returns a 32 character string' do 172 | secret = instance.generate_totp_secret 173 | 174 | expect(secret).to match(/\w{32}/) 175 | end 176 | end 177 | 178 | it_behaves_like 'generate_totp_secret', GuestUser 179 | it_behaves_like 'generate_totp_secret', EncryptedUser 180 | end 181 | 182 | describe '#confirm_totp_secret' do 183 | shared_examples 'confirm_totp_secret' do |klass| 184 | let(:instance) { klass.new } 185 | let(:secret) { instance.generate_totp_secret } 186 | let(:totp_helper) { TotpHelper.new(secret, instance.class.otp_length) } 187 | 188 | it 'populates otp_secret_key column when given correct code' do 189 | instance.confirm_totp_secret(secret, totp_helper.totp_code) 190 | 191 | expect(instance.otp_secret_key).to match(secret) 192 | end 193 | 194 | it 'does not populate otp_secret_key when when given incorrect code' do 195 | instance.confirm_totp_secret(secret, '123') 196 | expect(instance.otp_secret_key).to be_nil 197 | end 198 | 199 | it 'returns true when given correct code' do 200 | expect(instance.confirm_totp_secret(secret, totp_helper.totp_code)).to be true 201 | end 202 | 203 | it 'returns false when given incorrect code' do 204 | expect(instance.confirm_totp_secret(secret, '123')).to be false 205 | end 206 | 207 | end 208 | 209 | it_behaves_like 'confirm_totp_secret', GuestUser 210 | it_behaves_like 'confirm_totp_secret', EncryptedUser 211 | end 212 | 213 | describe '#max_login_attempts' do 214 | let(:instance) { build_guest_user } 215 | 216 | before do 217 | @original_max_login_attempts = GuestUser.max_login_attempts 218 | GuestUser.max_login_attempts = 3 219 | end 220 | 221 | after { GuestUser.max_login_attempts = @original_max_login_attempts } 222 | 223 | it 'returns class setting' do 224 | expect(instance.max_login_attempts).to eq(3) 225 | end 226 | 227 | it 'returns false as boolean' do 228 | instance.second_factor_attempts_count = nil 229 | expect(instance.max_login_attempts?).to be_falsey 230 | instance.second_factor_attempts_count = 0 231 | expect(instance.max_login_attempts?).to be_falsey 232 | instance.second_factor_attempts_count = 1 233 | expect(instance.max_login_attempts?).to be_falsey 234 | instance.second_factor_attempts_count = 2 235 | expect(instance.max_login_attempts?).to be_falsey 236 | end 237 | 238 | it 'returns true as boolean after too many attempts' do 239 | instance.second_factor_attempts_count = 3 240 | expect(instance.max_login_attempts?).to be_truthy 241 | instance.second_factor_attempts_count = 4 242 | expect(instance.max_login_attempts?).to be_truthy 243 | end 244 | end 245 | 246 | describe '.has_one_time_password' do 247 | context 'when encrypted: true option is passed' do 248 | let(:instance) { EncryptedUser.new } 249 | 250 | it 'encrypts otp_secret_key with iv, salt, and encoding' do 251 | instance.otp_secret_key = '2z6hxkdwi3uvrnpn' 252 | 253 | expect(instance.encrypted_otp_secret_key).to match(/.{44}/) 254 | 255 | expect(instance.encrypted_otp_secret_key_iv).to match(/.{24}/) 256 | 257 | expect(instance.encrypted_otp_secret_key_salt).to match(/.{25}/) 258 | end 259 | 260 | it 'does not encrypt a nil otp_secret_key' do 261 | instance.otp_secret_key = nil 262 | 263 | expect(instance.encrypted_otp_secret_key).to be_nil 264 | 265 | expect(instance.encrypted_otp_secret_key_iv).to be_nil 266 | 267 | expect(instance.encrypted_otp_secret_key_salt).to be_nil 268 | end 269 | 270 | it 'does not encrypt an empty otp_secret_key' do 271 | instance.otp_secret_key = '' 272 | 273 | expect(instance.encrypted_otp_secret_key).to eq '' 274 | 275 | expect(instance.encrypted_otp_secret_key_iv).to be_nil 276 | 277 | expect(instance.encrypted_otp_secret_key_salt).to be_nil 278 | end 279 | 280 | it 'raises an error when Devise.otp_secret_encryption_key is not set' do 281 | allow(Devise).to receive(:otp_secret_encryption_key).and_return nil 282 | 283 | # This error is raised by the encryptor gem 284 | expect { instance.otp_secret_key = '2z6hxkdwi3uvrnpn' }. 285 | to raise_error ArgumentError 286 | end 287 | 288 | it 'passes in the correct options to Encryptor. 289 | We test here output of 290 | Devise::Models::TwoFactorAuthenticatable::EncryptionInstanceMethods.encryption_options_for' do 291 | instance.otp_secret_key = 'testing' 292 | iv = instance.encrypted_otp_secret_key_iv 293 | salt = instance.encrypted_otp_secret_key_salt 294 | 295 | # it's important here to put the same crypto algorithm from that method 296 | encrypted = Encryptor.encrypt( 297 | value: 'testing', 298 | key: Devise.otp_secret_encryption_key, 299 | iv: iv.unpack('m').first, 300 | salt: salt.unpack('m').first, 301 | algorithm: 'aes-256-cbc' 302 | ) 303 | 304 | expect(instance.encrypted_otp_secret_key).to eq [encrypted].pack('m') 305 | end 306 | 307 | it 'varies the iv per instance' do 308 | instance.otp_secret_key = 'testing' 309 | user2 = EncryptedUser.new 310 | user2.otp_secret_key = 'testing' 311 | 312 | expect(user2.encrypted_otp_secret_key_iv). 313 | to_not eq instance.encrypted_otp_secret_key_iv 314 | end 315 | 316 | it 'varies the salt per instance' do 317 | instance.otp_secret_key = 'testing' 318 | user2 = EncryptedUser.new 319 | user2.otp_secret_key = 'testing' 320 | 321 | expect(user2.encrypted_otp_secret_key_salt). 322 | to_not eq instance.encrypted_otp_secret_key_salt 323 | end 324 | end 325 | end 326 | end 327 | -------------------------------------------------------------------------------- /spec/rails_app/.gitignore: -------------------------------------------------------------------------------- 1 | log/ 2 | tmp/ 3 | *.sqlite3 4 | -------------------------------------------------------------------------------- /spec/rails_app/README.md: -------------------------------------------------------------------------------- 1 | # Dummy 2 | 3 | You have found the dummy rails app used for integration testing of the `two_factor_authentication` gem. 4 | -------------------------------------------------------------------------------- /spec/rails_app/Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | # Add your own tasks in files placed in lib/tasks ending in .rake, 3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 4 | 5 | require File.expand_path('../config/application', __FILE__) 6 | 7 | Dummy::Application.load_tasks 8 | -------------------------------------------------------------------------------- /spec/rails_app/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | //= require_tree . 2 | -------------------------------------------------------------------------------- /spec/rails_app/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | *= require_self 3 | *= require_tree . 4 | */ 5 | -------------------------------------------------------------------------------- /spec/rails_app/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery 3 | end 4 | -------------------------------------------------------------------------------- /spec/rails_app/app/controllers/home_controller.rb: -------------------------------------------------------------------------------- 1 | class HomeController < ApplicationController 2 | before_action :authenticate_user!, only: :dashboard 3 | 4 | def index 5 | end 6 | 7 | def dashboard 8 | end 9 | 10 | end 11 | -------------------------------------------------------------------------------- /spec/rails_app/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | 3 | def render_flash 4 | flash.map do |name, message| 5 | content_tag(:p, message, class: "flash #{name}") 6 | end.join.html_safe 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/rails_app/app/mailers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houdini/two_factor_authentication/c68d9eae8efef1e341dda731d5f98f3755b2761a/spec/rails_app/app/mailers/.gitkeep -------------------------------------------------------------------------------- /spec/rails_app/app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houdini/two_factor_authentication/c68d9eae8efef1e341dda731d5f98f3755b2761a/spec/rails_app/app/models/.gitkeep -------------------------------------------------------------------------------- /spec/rails_app/app/models/admin.rb: -------------------------------------------------------------------------------- 1 | class Admin < ActiveRecord::Base 2 | # Include default devise modules. Others available are: 3 | # :confirmable, :lockable, :timeoutable and :omniauthable 4 | devise :database_authenticatable, :registerable, 5 | :recoverable, :rememberable, :trackable, :validatable 6 | end 7 | -------------------------------------------------------------------------------- /spec/rails_app/app/models/encrypted_user.rb: -------------------------------------------------------------------------------- 1 | class EncryptedUser 2 | extend ActiveModel::Callbacks 3 | include ActiveModel::Validations 4 | include Devise::Models::TwoFactorAuthenticatable 5 | 6 | define_model_callbacks :create 7 | attr_accessor :encrypted_otp_secret_key, 8 | :encrypted_otp_secret_key_iv, 9 | :encrypted_otp_secret_key_salt, 10 | :email, 11 | :second_factor_attempts_count, 12 | :totp_timestamp 13 | 14 | has_one_time_password(encrypted: true) 15 | end 16 | -------------------------------------------------------------------------------- /spec/rails_app/app/models/guest_user.rb: -------------------------------------------------------------------------------- 1 | class GuestUser 2 | extend ActiveModel::Callbacks 3 | include ActiveModel::Validations 4 | include Devise::Models::TwoFactorAuthenticatable 5 | 6 | define_model_callbacks :create 7 | attr_accessor :direct_otp, :direct_otp_sent_at, :otp_secret_key, :email, 8 | :second_factor_attempts_count, :totp_timestamp 9 | 10 | def update_attributes(attrs) 11 | attrs.each do |key, value| 12 | send(key.to_s + '=', value) 13 | end 14 | end 15 | 16 | has_one_time_password 17 | end 18 | -------------------------------------------------------------------------------- /spec/rails_app/app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | devise :two_factor_authenticatable, :database_authenticatable, :registerable, 3 | :recoverable, :rememberable, :trackable, :validatable 4 | 5 | has_one_time_password 6 | 7 | def send_two_factor_authentication_code(code) 8 | SMSProvider.send_message(to: phone_number, body: code) 9 | end 10 | 11 | def phone_number 12 | '14159341234' 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/rails_app/app/views/home/dashboard.html.erb: -------------------------------------------------------------------------------- 1 |

Your Personal Dashboard

2 | 3 |

Hi <%= current_user.nickname %>

4 | 5 |

Your registered email address is <%= current_user.email %>

6 | 7 |

Param A is <%= params[:A] %>

8 | 9 |

Param B is <%= params[:B] %>

10 | 11 |

You can only see this page after successfully completing two factor authentication

12 | -------------------------------------------------------------------------------- /spec/rails_app/app/views/home/index.html.erb: -------------------------------------------------------------------------------- 1 |

Welcome Home

2 | 3 |

Find me in app/views/home/index.html.erb

4 | -------------------------------------------------------------------------------- /spec/rails_app/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= stylesheet_link_tag "application", :media => "all" %> 6 | <%= javascript_include_tag "application" %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 17 | <%= render_flash %> 18 | <%= yield %> 19 | 20 | 21 | -------------------------------------------------------------------------------- /spec/rails_app/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Dummy::Application 5 | -------------------------------------------------------------------------------- /spec/rails_app/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require "active_record/railtie" 4 | require "action_controller/railtie" 5 | require "action_mailer/railtie" 6 | require "sprockets/railtie" 7 | 8 | Bundler.require(*Rails.groups) 9 | require "two_factor_authentication" 10 | 11 | module Dummy 12 | class Application < Rails::Application 13 | # Settings in config/environments/* take precedence over those specified here. 14 | # Application configuration should go into files in config/initializers 15 | # -- all .rb files in that directory are automatically loaded. 16 | 17 | # Custom directories with classes and modules you want to be autoloadable. 18 | # config.autoload_paths += %W(#{config.root}/extras) 19 | config.autoload_paths += %W(#{config.root}/lib) 20 | 21 | # Only load the plugins named here, in the order given (default is alphabetical). 22 | # :all can be used as a placeholder for all plugins not explicitly named. 23 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 24 | 25 | # Activate observers that should always be running. 26 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 27 | 28 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 29 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 30 | # config.time_zone = 'Central Time (US & Canada)' 31 | 32 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 33 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 34 | # config.i18n.default_locale = :de 35 | 36 | # Configure the default encoding used in templates for Ruby 1.9. 37 | config.encoding = "utf-8" 38 | 39 | # Configure sensitive parameters which will be filtered from the log file. 40 | config.filter_parameters += [:password] 41 | 42 | # Enable escaping HTML in JSON. 43 | config.active_support.escape_html_entities_in_json = true 44 | 45 | # Use SQL instead of Active Record's schema dumper when creating the database. 46 | # This is necessary if your schema can't be completely dumped by the schema dumper, 47 | # like if you have constraints or database-specific column types 48 | # config.active_record.schema_format = :sql 49 | 50 | # Enable the asset pipeline 51 | config.assets.enabled = true 52 | 53 | # Version of your assets, change this if you want to expire all your assets 54 | config.assets.version = '1.0' 55 | 56 | config.action_mailer.default_url_options = { host: 'localhost:3000' } 57 | 58 | config.i18n.enforce_available_locales = false 59 | 60 | config.secret_key_base = 'secretvalue' 61 | end 62 | end 63 | 64 | -------------------------------------------------------------------------------- /spec/rails_app/config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | gemfile = File.expand_path('../../../../Gemfile', __FILE__) 3 | 4 | if File.exist?(gemfile) 5 | ENV['BUNDLE_GEMFILE'] = gemfile 6 | require 'bundler' 7 | Bundler.setup 8 | end 9 | 10 | $:.unshift File.expand_path('../../../../lib', __FILE__) -------------------------------------------------------------------------------- /spec/rails_app/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | development: 7 | adapter: sqlite3 8 | database: db/development.sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | # Warning: The database defined as "test" will be erased and 13 | # re-generated from your development database when you run "rake". 14 | # Do not set this db to the same as development or production. 15 | test: 16 | adapter: sqlite3 17 | database: db/test.sqlite3 18 | pool: 5 19 | timeout: 5000 20 | -------------------------------------------------------------------------------- /spec/rails_app/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | Dummy::Application.initialize! 6 | -------------------------------------------------------------------------------- /spec/rails_app/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | config.eager_load = false 9 | 10 | # Show full error reports and disable caching 11 | config.consider_all_requests_local = true 12 | config.action_controller.perform_caching = false 13 | 14 | # Don't care if the mailer can't send 15 | config.action_mailer.raise_delivery_errors = false 16 | 17 | # Print deprecation notices to the Rails logger 18 | config.active_support.deprecation = :log 19 | 20 | # Only use best-standards-support built into browsers 21 | config.action_dispatch.best_standards_support = :builtin 22 | 23 | # Do not compress assets 24 | config.assets.compress = false 25 | 26 | # Expands the lines which load the assets 27 | config.assets.debug = true 28 | end 29 | -------------------------------------------------------------------------------- /spec/rails_app/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # Code is not reloaded between requests 5 | config.cache_classes = true 6 | config.eager_load = false 7 | 8 | # Full error reports are disabled and caching is turned on 9 | config.consider_all_requests_local = false 10 | config.action_controller.perform_caching = true 11 | 12 | # Disable Rails's static asset server (Apache or nginx will already do this) 13 | config.serve_static_assets = false 14 | 15 | # Compress JavaScripts and CSS 16 | config.assets.compress = true 17 | 18 | # Don't fallback to assets pipeline if a precompiled asset is missed 19 | config.assets.compile = false 20 | 21 | # Generate digests for assets URLs 22 | config.assets.digest = true 23 | 24 | # Defaults to nil and saved in location specified by config.assets.prefix 25 | # config.assets.manifest = YOUR_PATH 26 | 27 | # Specifies the header that your server uses for sending files 28 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 29 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 30 | 31 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 32 | # config.force_ssl = true 33 | 34 | # See everything in the log (default is :info) 35 | # config.log_level = :debug 36 | 37 | # Prepend all log lines with the following tags 38 | # config.log_tags = [ :subdomain, :uuid ] 39 | 40 | # Use a different logger for distributed setups 41 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 42 | 43 | # Use a different cache store in production 44 | # config.cache_store = :mem_cache_store 45 | 46 | # Enable serving of images, stylesheets, and JavaScripts from an asset server 47 | # config.action_controller.asset_host = "http://assets.example.com" 48 | 49 | # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) 50 | # config.assets.precompile += %w( search.js ) 51 | 52 | # Disable delivery errors, bad email addresses will be ignored 53 | # config.action_mailer.raise_delivery_errors = false 54 | 55 | # Enable threaded mode 56 | # config.threadsafe! 57 | 58 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 59 | # the I18n.default_locale when a translation can not be found) 60 | config.i18n.fallbacks = true 61 | 62 | # Send deprecation notices to registered listeners 63 | config.active_support.deprecation = :notify 64 | 65 | # Log the query plan for queries taking more than this (works 66 | # with SQLite, MySQL, and PostgreSQL) 67 | # config.active_record.auto_explain_threshold_in_seconds = 0.5 68 | end 69 | -------------------------------------------------------------------------------- /spec/rails_app/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | config.eager_load = false 10 | 11 | # Configure static asset server for tests with Cache-Control for performance 12 | if Rails::VERSION::MAJOR == 4 && Rails::VERSION::MINOR >= 2 || 13 | Rails::VERSION::MAJOR >= 5 14 | config.serve_static_files = true 15 | else 16 | config.serve_static_assets = true 17 | end 18 | 19 | config.static_cache_control = "public, max-age=3600" 20 | 21 | # Show full error reports and disable caching 22 | config.consider_all_requests_local = true 23 | config.action_controller.perform_caching = false 24 | 25 | # Raise exceptions instead of rendering exception templates 26 | config.action_dispatch.show_exceptions = false 27 | 28 | # Disable request forgery protection in test environment 29 | config.action_controller.allow_forgery_protection = false 30 | 31 | # Tell Action Mailer not to deliver emails to the real world. 32 | # The :test delivery method accumulates sent emails in the 33 | # ActionMailer::Base.deliveries array. 34 | config.action_mailer.delivery_method = :test 35 | 36 | # Print deprecation notices to the stderr 37 | config.active_support.deprecation = :stderr 38 | 39 | # For testing session variables in Capybara specs 40 | config.middleware.use RackSessionAccess::Middleware 41 | end 42 | -------------------------------------------------------------------------------- /spec/rails_app/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /spec/rails_app/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | if Rails.version > '4.1.0' 2 | Rails.application.config.action_dispatch.cookies_serializer = :json 3 | end 4 | -------------------------------------------------------------------------------- /spec/rails_app/config/initializers/devise.rb: -------------------------------------------------------------------------------- 1 | # Use this hook to configure devise mailer, warden hooks and so forth. 2 | # Many of these configuration options can be set straight in your model. 3 | Devise.setup do |config| 4 | # The secret key used by Devise. Devise uses this key to generate 5 | # random tokens. Changing this key will render invalid all existing 6 | # confirmation, reset password and unlock tokens in the database. 7 | config.secret_key = 'd13ac2811507a08f5af301635a806f4e438d053def28750d6ae77d8c9dd9470dc56df6cf1c40f9fcd8ce5730c2ce69097f7d5a78f6a303b31c5b8d8cbe907a3a' 8 | 9 | # ==> Mailer Configuration 10 | # Configure the e-mail address which will be shown in Devise::Mailer, 11 | # note that it will be overwritten if you use your own mailer class 12 | # with default "from" parameter. 13 | config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' 14 | 15 | # Configure the class responsible to send e-mails. 16 | # config.mailer = 'Devise::Mailer' 17 | 18 | # ==> ORM configuration 19 | # Load and configure the ORM. Supports :active_record (default) and 20 | # :mongoid (bson_ext recommended) by default. Other ORMs may be 21 | # available as additional gems. 22 | require 'devise/orm/active_record' 23 | 24 | # ==> Configuration for any authentication mechanism 25 | # Configure which keys are used when authenticating a user. The default is 26 | # just :email. You can configure it to use [:username, :subdomain], so for 27 | # authenticating a user, both parameters are required. Remember that those 28 | # parameters are used only when authenticating and not when retrieving from 29 | # session. If you need permissions, you should implement that in a before filter. 30 | # You can also supply a hash where the value is a boolean determining whether 31 | # or not authentication should be aborted when the value is not present. 32 | # config.authentication_keys = [ :email ] 33 | 34 | # Configure parameters from the request object used for authentication. Each entry 35 | # given should be a request method and it will automatically be passed to the 36 | # find_for_authentication method and considered in your model lookup. For instance, 37 | # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. 38 | # The same considerations mentioned for authentication_keys also apply to request_keys. 39 | # config.request_keys = [] 40 | 41 | # Configure which authentication keys should be case-insensitive. 42 | # These keys will be downcased upon creating or modifying a user and when used 43 | # to authenticate or find a user. Default is :email. 44 | config.case_insensitive_keys = [ :email ] 45 | 46 | # Configure which authentication keys should have whitespace stripped. 47 | # These keys will have whitespace before and after removed upon creating or 48 | # modifying a user and when used to authenticate or find a user. Default is :email. 49 | config.strip_whitespace_keys = [ :email ] 50 | 51 | # Tell if authentication through request.params is enabled. True by default. 52 | # It can be set to an array that will enable params authentication only for the 53 | # given strategies, for example, `config.params_authenticatable = [:database]` will 54 | # enable it only for database (email + password) authentication. 55 | # config.params_authenticatable = true 56 | 57 | # Tell if authentication through HTTP Auth is enabled. False by default. 58 | # It can be set to an array that will enable http authentication only for the 59 | # given strategies, for example, `config.http_authenticatable = [:database]` will 60 | # enable it only for database authentication. The supported strategies are: 61 | # :database = Support basic authentication with authentication key + password 62 | # config.http_authenticatable = false 63 | 64 | # If http headers should be returned for AJAX requests. True by default. 65 | # config.http_authenticatable_on_xhr = true 66 | 67 | # The realm used in Http Basic Authentication. 'Application' by default. 68 | # config.http_authentication_realm = 'Application' 69 | 70 | # It will change confirmation, password recovery and other workflows 71 | # to behave the same regardless if the e-mail provided was right or wrong. 72 | # Does not affect registerable. 73 | # config.paranoid = true 74 | 75 | # By default Devise will store the user in session. You can skip storage for 76 | # particular strategies by setting this option. 77 | # Notice that if you are skipping storage for all authentication paths, you 78 | # may want to disable generating routes to Devise's sessions controller by 79 | # passing skip: :sessions to `devise_for` in your config/routes.rb 80 | config.skip_session_storage = [:http_auth] 81 | 82 | # By default, Devise cleans up the CSRF token on authentication to 83 | # avoid CSRF token fixation attacks. This means that, when using AJAX 84 | # requests for sign in and sign up, you need to get a new CSRF token 85 | # from the server. You can disable this option at your own risk. 86 | # config.clean_up_csrf_token_on_authentication = true 87 | 88 | # ==> Configuration for :database_authenticatable 89 | # For bcrypt, this is the cost for hashing the password and defaults to 10. If 90 | # using other encryptors, it sets how many times you want the password re-encrypted. 91 | # 92 | # Limiting the stretches to just one in testing will increase the performance of 93 | # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use 94 | # a value less than 10 in other environments. Note that, for bcrypt (the default 95 | # encryptor), the cost increases exponentially with the number of stretches (e.g. 96 | # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). 97 | config.stretches = Rails.env.test? ? 1 : 10 98 | 99 | # Setup a pepper to generate the encrypted password. 100 | # config.pepper = '4ee5a40e29eaa2141d0d30fe4dbec3e5f11386452c42f4f2e8e159092b839ae4edac028709d4c604c16354a4dab5f70a88bda9d1bb6258bf01b9c3915df472c5' 101 | 102 | # ==> Configuration for :confirmable 103 | # A period that the user is allowed to access the website even without 104 | # confirming their account. For instance, if set to 2.days, the user will be 105 | # able to access the website for two days without confirming their account, 106 | # access will be blocked just in the third day. Default is 0.days, meaning 107 | # the user cannot access the website without confirming their account. 108 | # config.allow_unconfirmed_access_for = 2.days 109 | 110 | # A period that the user is allowed to confirm their account before their 111 | # token becomes invalid. For example, if set to 3.days, the user can confirm 112 | # their account within 3 days after the mail was sent, but on the fourth day 113 | # their account can't be confirmed with the token any more. 114 | # Default is nil, meaning there is no restriction on how long a user can take 115 | # before confirming their account. 116 | # config.confirm_within = 3.days 117 | 118 | # If true, requires any email changes to be confirmed (exactly the same way as 119 | # initial account confirmation) to be applied. Requires additional unconfirmed_email 120 | # db field (see migrations). Until confirmed, new email is stored in 121 | # unconfirmed_email column, and copied to email column on successful confirmation. 122 | config.reconfirmable = true 123 | 124 | # Defines which key will be used when confirming an account 125 | # config.confirmation_keys = [ :email ] 126 | 127 | # ==> Configuration for :rememberable 128 | # The time the user will be remembered without asking for credentials again. 129 | # config.remember_for = 2.weeks 130 | 131 | # If true, extends the user's remember period when remembered via cookie. 132 | # config.extend_remember_period = false 133 | 134 | # Options to be passed to the created cookie. For instance, you can set 135 | # secure: true in order to force SSL only cookies. 136 | # config.rememberable_options = {} 137 | 138 | # ==> Configuration for :validatable 139 | # Range for password length. 140 | config.password_length = 8..128 141 | 142 | # Email regex used to validate email formats. It simply asserts that 143 | # one (and only one) @ exists in the given string. This is mainly 144 | # to give user feedback and not to assert the e-mail validity. 145 | # config.email_regexp = /\A[^@]+@[^@]+\z/ 146 | 147 | # ==> Configuration for :timeoutable 148 | # The time you want to timeout the user session without activity. After this 149 | # time the user will be asked for credentials again. Default is 30 minutes. 150 | # config.timeout_in = 30.minutes 151 | 152 | # If true, expires auth token on session timeout. 153 | # config.expire_auth_token_on_timeout = false 154 | 155 | # ==> Configuration for :lockable 156 | # Defines which strategy will be used to lock an account. 157 | # :failed_attempts = Locks an account after a number of failed attempts to sign in. 158 | # :none = No lock strategy. You should handle locking by yourself. 159 | # config.lock_strategy = :failed_attempts 160 | 161 | # Defines which key will be used when locking and unlocking an account 162 | # config.unlock_keys = [ :email ] 163 | 164 | # Defines which strategy will be used to unlock an account. 165 | # :email = Sends an unlock link to the user email 166 | # :time = Re-enables login after a certain amount of time (see :unlock_in below) 167 | # :both = Enables both strategies 168 | # :none = No unlock strategy. You should handle unlocking by yourself. 169 | # config.unlock_strategy = :both 170 | 171 | # Number of authentication tries before locking an account if lock_strategy 172 | # is failed attempts. 173 | # config.maximum_attempts = 20 174 | 175 | # Time interval to unlock the account if :time is enabled as unlock_strategy. 176 | # config.unlock_in = 1.hour 177 | 178 | # Warn on the last attempt before the account is locked. 179 | # config.last_attempt_warning = false 180 | 181 | # ==> Configuration for :recoverable 182 | # 183 | # Defines which key will be used when recovering the password for an account 184 | # config.reset_password_keys = [ :email ] 185 | 186 | # Time interval you can reset your password with a reset password key. 187 | # Don't put a too small interval or your users won't have the time to 188 | # change their passwords. 189 | config.reset_password_within = 6.hours 190 | 191 | # ==> Configuration for :encryptable 192 | # Allow you to use another encryption algorithm besides bcrypt (default). You can use 193 | # :sha1, :sha512 or encryptors from others authentication tools as :clearance_sha1, 194 | # :authlogic_sha512 (then you should set stretches above to 20 for default behavior) 195 | # and :restful_authentication_sha1 (then you should set stretches to 10, and copy 196 | # REST_AUTH_SITE_KEY to pepper). 197 | # 198 | # Require the `devise-encryptable` gem when using anything other than bcrypt 199 | # config.encryptor = :sha512 200 | 201 | # ==> Scopes configuration 202 | # Turn scoped views on. Before rendering "sessions/new", it will first check for 203 | # "users/sessions/new". It's turned off by default because it's slower if you 204 | # are using only default views. 205 | # config.scoped_views = false 206 | 207 | # Configure the default scope given to Warden. By default it's the first 208 | # devise role declared in your routes (usually :user). 209 | config.default_scope = :user 210 | 211 | # Set this configuration to false if you want /users/sign_out to sign out 212 | # only the current scope. By default, Devise signs out all scopes. 213 | config.sign_out_all_scopes = false 214 | 215 | # ==> Navigation configuration 216 | # Lists the formats that should be treated as navigational. Formats like 217 | # :html, should redirect to the sign in page when the user does not have 218 | # access, but formats like :xml or :json, should return 401. 219 | # 220 | # If you have any extra navigational formats, like :iphone or :mobile, you 221 | # should add them to the navigational formats lists. 222 | # 223 | # The "*/*" below is required to match Internet Explorer requests. 224 | # config.navigational_formats = ['*/*', :html] 225 | 226 | # The default HTTP method used to sign out a resource. Default is :delete. 227 | config.sign_out_via = Rails.env.test? ? :get : :delete 228 | 229 | # ==> OmniAuth 230 | # Add a new OmniAuth provider. Check the wiki for more information on setting 231 | # up on your models and hooks. 232 | # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' 233 | 234 | # ==> Warden configuration 235 | # If you want to use other strategies, that are not supported by Devise, or 236 | # change the failure app, you can configure them inside the config.warden block. 237 | # 238 | # config.warden do |manager| 239 | # manager.intercept_401 = false 240 | # manager.default_strategies(scope: :user).unshift :some_external_strategy 241 | # end 242 | 243 | # ==> Mountable engine configurations 244 | # When using Devise inside an engine, let's call it `MyEngine`, and this engine 245 | # is mountable, there are some extra configurations to be taken into account. 246 | # The following options are available, assuming the engine is mounted as: 247 | # 248 | # mount MyEngine, at: '/my_engine' 249 | # 250 | # The router that invoked `devise_for`, in the example above, would be: 251 | # config.router_name = :my_engine 252 | # 253 | # When using omniauth, Devise cannot automatically set Omniauth path, 254 | # so you need to do it manually. For the users scope, it would be: 255 | # config.omniauth_path_prefix = '/my_engine/users/auth' 256 | 257 | config.otp_secret_encryption_key = '0a8283fba984da1de24e4df1e93046cb53c5787944ef037b2dbf3e61d20fe11f25e25a855cec605fdf65b162329890d7230afdf64f681b4c32020281054e73ec' 258 | end 259 | -------------------------------------------------------------------------------- /spec/rails_app/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | # 12 | # These inflection rules are supported but not enabled by default: 13 | # ActiveSupport::Inflector.inflections do |inflect| 14 | # inflect.acronym 'RESTful' 15 | # end 16 | -------------------------------------------------------------------------------- /spec/rails_app/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /spec/rails_app/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | Dummy::Application.config.secret_token = 'e75d8cdfc7c99757a5d4b427bde4b4b1072eb169c022cdbb038bdbcefb3901ef60ac912b6fb14260db099156520b9cc8838e4bf8e209b7246fad891950825032' 8 | -------------------------------------------------------------------------------- /spec/rails_app/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Dummy::Application.config.session_store :cookie_store, key: '_rails_app_session' 4 | 5 | # Use the database for sessions instead of the cookie-based default, 6 | # which shouldn't be used to store highly confidential information 7 | # (create the session table with "rails generate session_migration") 8 | # Dummy::Application.config.session_store :active_record_store 9 | -------------------------------------------------------------------------------- /spec/rails_app/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # Disable root element in JSON by default. 12 | ActiveSupport.on_load(:active_record) do 13 | self.include_root_in_json = false 14 | end 15 | -------------------------------------------------------------------------------- /spec/rails_app/config/locales/devise.en.yml: -------------------------------------------------------------------------------- 1 | # Additional translations at https://github.com/plataformatec/devise/wiki/I18n 2 | 3 | en: 4 | devise: 5 | confirmations: 6 | confirmed: "Your account was successfully confirmed." 7 | send_instructions: "You will receive an email with instructions about how to confirm your account in a few minutes." 8 | send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions about how to confirm your account in a few minutes." 9 | failure: 10 | already_authenticated: "You are already signed in." 11 | inactive: "Your account is not activated yet." 12 | invalid: "Invalid email or password." 13 | locked: "Your account is locked." 14 | last_attempt: "You have one more attempt before your account will be locked." 15 | not_found_in_database: "Invalid email or password." 16 | timeout: "Your session expired. Please sign in again to continue." 17 | unauthenticated: "You need to sign in or sign up before continuing." 18 | unconfirmed: "You have to confirm your account before continuing." 19 | mailer: 20 | confirmation_instructions: 21 | subject: "Confirmation instructions" 22 | reset_password_instructions: 23 | subject: "Reset password instructions" 24 | unlock_instructions: 25 | subject: "Unlock Instructions" 26 | omniauth_callbacks: 27 | failure: "Could not authenticate you from %{kind} because \"%{reason}\"." 28 | success: "Successfully authenticated from %{kind} account." 29 | passwords: 30 | no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." 31 | send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." 32 | send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." 33 | updated: "Your password was changed successfully. You are now signed in." 34 | updated_not_active: "Your password was changed successfully." 35 | registrations: 36 | destroyed: "Bye! Your account was successfully cancelled. We hope to see you again soon." 37 | signed_up: "Welcome! You have signed up successfully." 38 | signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." 39 | signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." 40 | signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please open the link to activate your account." 41 | update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and click on the confirm link to finalize confirming your new email address." 42 | updated: "You updated your account successfully." 43 | sessions: 44 | signed_in: "Signed in successfully." 45 | signed_out: "Signed out successfully." 46 | unlocks: 47 | send_instructions: "You will receive an email with instructions about how to unlock your account in a few minutes." 48 | send_paranoid_instructions: "If your account exists, you will receive an email with instructions about how to unlock it in a few minutes." 49 | unlocked: "Your account has been unlocked successfully. Please sign in to continue." 50 | errors: 51 | messages: 52 | already_confirmed: "was already confirmed, please try signing in" 53 | confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" 54 | expired: "has expired, please request a new one" 55 | not_found: "not found" 56 | not_locked: "was not locked" 57 | not_saved: 58 | one: "1 error prohibited this %{resource} from being saved:" 59 | other: "%{count} errors prohibited this %{resource} from being saved:" 60 | -------------------------------------------------------------------------------- /spec/rails_app/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" 6 | -------------------------------------------------------------------------------- /spec/rails_app/config/routes.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.routes.draw do 2 | devise_for :admins 3 | root to: "home#index" 4 | 5 | match "/dashboard", to: "home#dashboard", as: :dashboard, via: [:get] 6 | 7 | devise_for :users 8 | 9 | # The priority is based upon order of creation: 10 | # first created -> highest priority. 11 | 12 | # Sample of regular route: 13 | # match 'products/:id' => 'catalog#view' 14 | # Keep in mind you can assign values other than :controller and :action 15 | 16 | # Sample of named route: 17 | # match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase 18 | # This route can be invoked with purchase_url(:id => product.id) 19 | 20 | # Sample resource route (maps HTTP verbs to controller actions automatically): 21 | # resources :products 22 | 23 | # Sample resource route with options: 24 | # resources :products do 25 | # member do 26 | # get 'short' 27 | # post 'toggle' 28 | # end 29 | # 30 | # collection do 31 | # get 'sold' 32 | # end 33 | # end 34 | 35 | # Sample resource route with sub-resources: 36 | # resources :products do 37 | # resources :comments, :sales 38 | # resource :seller 39 | # end 40 | 41 | # Sample resource route with more complex sub-resources 42 | # resources :products do 43 | # resources :comments 44 | # resources :sales do 45 | # get 'recent', :on => :collection 46 | # end 47 | # end 48 | 49 | # Sample resource route within a namespace: 50 | # namespace :admin do 51 | # # Directs /admin/products/* to Admin::ProductsController 52 | # # (app/controllers/admin/products_controller.rb) 53 | # resources :products 54 | # end 55 | 56 | # You can have the root of your site routed with "root" 57 | # just remember to delete public/index.html. 58 | # root :to => 'welcome#index' 59 | 60 | # See how all your routes lay out with "rake routes" 61 | 62 | # This is a legacy wild controller route that's not recommended for RESTful applications. 63 | # Note: This route will make all actions in every controller accessible via GET requests. 64 | # match ':controller(/:action(/:id))(.:format)' 65 | end 66 | -------------------------------------------------------------------------------- /spec/rails_app/db/migrate/20140403184646_devise_create_users.rb: -------------------------------------------------------------------------------- 1 | class DeviseCreateUsers < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table(:users) do |t| 4 | ## Database authenticatable 5 | t.string :email, null: false, default: "" 6 | t.string :encrypted_password, null: false, default: "" 7 | 8 | ## Recoverable 9 | t.string :reset_password_token 10 | t.datetime :reset_password_sent_at 11 | 12 | ## Rememberable 13 | t.datetime :remember_created_at 14 | 15 | ## Trackable 16 | t.integer :sign_in_count, default: 0, null: false 17 | t.datetime :current_sign_in_at 18 | t.datetime :last_sign_in_at 19 | t.string :current_sign_in_ip 20 | t.string :last_sign_in_ip 21 | 22 | ## Confirmable 23 | # t.string :confirmation_token 24 | # t.datetime :confirmed_at 25 | # t.datetime :confirmation_sent_at 26 | # t.string :unconfirmed_email # Only if using reconfirmable 27 | 28 | ## Lockable 29 | # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts 30 | # t.string :unlock_token # Only if unlock strategy is :email or :both 31 | # t.datetime :locked_at 32 | 33 | 34 | t.timestamps null: false 35 | end 36 | 37 | add_index :users, :email, unique: true 38 | add_index :users, :reset_password_token, unique: true 39 | # add_index :users, :confirmation_token, unique: true 40 | # add_index :users, :unlock_token, unique: true 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/rails_app/db/migrate/20140407172619_two_factor_authentication_add_to_users.rb: -------------------------------------------------------------------------------- 1 | class TwoFactorAuthenticationAddToUsers < ActiveRecord::Migration[4.2] 2 | def up 3 | change_table :users do |t| 4 | t.string :otp_secret_key 5 | t.integer :second_factor_attempts_count, :default => 0 6 | end 7 | 8 | add_index :users, :otp_secret_key, :unique => true 9 | end 10 | 11 | def down 12 | remove_column :users, :otp_secret_key 13 | remove_column :users, :second_factor_attempts_count 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/rails_app/db/migrate/20140407215513_add_nickanme_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddNickanmeToUsers < ActiveRecord::Migration[4.2] 2 | def change 3 | change_table :users do |t| 4 | t.column :nickname, :string, limit: 64 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/rails_app/db/migrate/20151224171231_add_encrypted_columns_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddEncryptedColumnsToUser < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :users, :encrypted_otp_secret_key, :string 4 | add_column :users, :encrypted_otp_secret_key_iv, :string 5 | add_column :users, :encrypted_otp_secret_key_salt, :string 6 | 7 | add_index :users, :encrypted_otp_secret_key, unique: true 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/rails_app/db/migrate/20151224180310_populate_otp_column.rb: -------------------------------------------------------------------------------- 1 | class PopulateOtpColumn < ActiveRecord::Migration[4.2] 2 | def up 3 | User.reset_column_information 4 | 5 | User.find_each do |user| 6 | user.otp_secret_key = user.read_attribute('otp_secret_key') 7 | user.save! 8 | end 9 | end 10 | 11 | def down 12 | User.reset_column_information 13 | 14 | User.find_each do |user| 15 | user.otp_secret_key = ROTP::Base32.random_base32 16 | user.save! 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/rails_app/db/migrate/20151228230340_remove_otp_secret_key_from_user.rb: -------------------------------------------------------------------------------- 1 | class RemoveOtpSecretKeyFromUser < ActiveRecord::Migration[4.2] 2 | def change 3 | remove_column :users, :otp_secret_key, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/rails_app/db/migrate/20160209032439_devise_create_admins.rb: -------------------------------------------------------------------------------- 1 | class DeviseCreateAdmins < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table(:admins) do |t| 4 | ## Database authenticatable 5 | t.string :email, null: false, default: "" 6 | t.string :encrypted_password, null: false, default: "" 7 | 8 | ## Recoverable 9 | t.string :reset_password_token 10 | t.datetime :reset_password_sent_at 11 | 12 | ## Rememberable 13 | t.datetime :remember_created_at 14 | 15 | ## Trackable 16 | t.integer :sign_in_count, default: 0, null: false 17 | t.datetime :current_sign_in_at 18 | t.datetime :last_sign_in_at 19 | t.string :current_sign_in_ip 20 | t.string :last_sign_in_ip 21 | 22 | ## Confirmable 23 | # t.string :confirmation_token 24 | # t.datetime :confirmed_at 25 | # t.datetime :confirmation_sent_at 26 | # t.string :unconfirmed_email # Only if using reconfirmable 27 | 28 | ## Lockable 29 | # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts 30 | # t.string :unlock_token # Only if unlock strategy is :email or :both 31 | # t.datetime :locked_at 32 | 33 | 34 | t.timestamps null: false 35 | end 36 | 37 | add_index :admins, :email, unique: true 38 | add_index :admins, :reset_password_token, unique: true 39 | # add_index :admins, :confirmation_token, unique: true 40 | # add_index :admins, :unlock_token, unique: true 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/rails_app/db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # Note that this schema.rb definition is the authoritative source for your 6 | # database schema. If you need to create the application database on another 7 | # system, you should be using db:schema:load, not running all the migrations 8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 9 | # you'll amass, the slower it'll run and the greater likelihood for issues). 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 2016_02_09_032439) do 14 | 15 | create_table "admins", force: :cascade do |t| 16 | t.string "email", default: "", null: false 17 | t.string "encrypted_password", default: "", null: false 18 | t.string "reset_password_token" 19 | t.datetime "reset_password_sent_at" 20 | t.datetime "remember_created_at" 21 | t.integer "sign_in_count", default: 0, null: false 22 | t.datetime "current_sign_in_at" 23 | t.datetime "last_sign_in_at" 24 | t.string "current_sign_in_ip" 25 | t.string "last_sign_in_ip" 26 | t.datetime "created_at", null: false 27 | t.datetime "updated_at", null: false 28 | t.index ["email"], name: "index_admins_on_email", unique: true 29 | t.index ["reset_password_token"], name: "index_admins_on_reset_password_token", unique: true 30 | end 31 | 32 | create_table "users", force: :cascade do |t| 33 | t.string "email", default: "", null: false 34 | t.string "encrypted_password", default: "", null: false 35 | t.string "reset_password_token" 36 | t.datetime "reset_password_sent_at" 37 | t.datetime "remember_created_at" 38 | t.integer "sign_in_count", default: 0, null: false 39 | t.datetime "current_sign_in_at" 40 | t.datetime "last_sign_in_at" 41 | t.string "current_sign_in_ip" 42 | t.string "last_sign_in_ip" 43 | t.datetime "created_at", null: false 44 | t.datetime "updated_at", null: false 45 | t.integer "second_factor_attempts_count", default: 0 46 | t.string "nickname", limit: 64 47 | t.string "encrypted_otp_secret_key" 48 | t.string "encrypted_otp_secret_key_iv" 49 | t.string "encrypted_otp_secret_key_salt" 50 | t.index ["email"], name: "index_users_on_email", unique: true 51 | t.index ["encrypted_otp_secret_key"], name: "index_users_on_encrypted_otp_secret_key", unique: true 52 | t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true 53 | end 54 | 55 | end 56 | -------------------------------------------------------------------------------- /spec/rails_app/lib/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houdini/two_factor_authentication/c68d9eae8efef1e341dda731d5f98f3755b2761a/spec/rails_app/lib/assets/.gitkeep -------------------------------------------------------------------------------- /spec/rails_app/lib/sms_provider.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | 3 | class SMSProvider 4 | Message = Class.new(OpenStruct) 5 | 6 | class_attribute :messages 7 | self.messages = [] 8 | 9 | def self.send_message(opts = {}) 10 | self.messages << Message.new(opts) 11 | end 12 | 13 | def self.last_message 14 | self.messages.last 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /spec/rails_app/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The page you were looking for doesn't exist.

23 |

You may have mistyped the address or the page may have moved.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /spec/rails_app/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The change you wanted was rejected.

23 |

Maybe you tried to change something you didn't have access to.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /spec/rails_app/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

We're sorry, but something went wrong.

23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /spec/rails_app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houdini/two_factor_authentication/c68d9eae8efef1e341dda731d5f98f3755b2761a/spec/rails_app/public/favicon.ico -------------------------------------------------------------------------------- /spec/rails_app/script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | APP_PATH = File.expand_path('../../config/application', __FILE__) 5 | require File.expand_path('../../config/boot', __FILE__) 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] ||= "test" 2 | require File.expand_path("../rails_app/config/environment.rb", __FILE__) 3 | 4 | require 'rspec/rails' 5 | require 'timecop' 6 | require 'rack_session_access/capybara' 7 | 8 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 9 | RSpec.configure do |config| 10 | config.run_all_when_everything_filtered = true 11 | config.filter_run :focus 12 | 13 | config.use_transactional_examples = true 14 | 15 | config.include Capybara::DSL 16 | 17 | # Run specs in random order to surface order dependencies. If you find an 18 | # order dependency and want to debug it, you can fix the order by providing 19 | # the seed, which is printed after each run. 20 | # --seed 1234 21 | config.order = 'random' 22 | 23 | config.after(:each) { Timecop.return } 24 | end 25 | 26 | Dir["#{Dir.pwd}/spec/support/**/*.rb"].each {|f| require f} 27 | -------------------------------------------------------------------------------- /spec/support/authenticated_model_helper.rb: -------------------------------------------------------------------------------- 1 | module AuthenticatedModelHelper 2 | def build_guest_user 3 | GuestUser.new 4 | end 5 | 6 | def create_user(type = 'encrypted', attributes = {}) 7 | create_table_for_nonencrypted_user if type == 'not_encrypted' 8 | 9 | User.create!(valid_attributes(attributes)) 10 | end 11 | 12 | def create_admin 13 | Admin.create!(valid_attributes.except(:nickname)) 14 | end 15 | 16 | def valid_attributes(attributes={}) 17 | { 18 | nickname: 'Marissa', 19 | email: generate_unique_email, 20 | password: 'password', 21 | password_confirmation: 'password' 22 | }.merge(attributes) 23 | end 24 | 25 | def generate_unique_email 26 | @@email_count ||= 0 27 | @@email_count += 1 28 | "user#{@@email_count}@example.com" 29 | end 30 | 31 | def create_table_for_nonencrypted_user 32 | ActiveRecord::Migration.suppress_messages do 33 | ActiveRecord::Schema.define(version: 1) do 34 | create_table 'users', force: :cascade do |t| 35 | t.string 'email', default: '', null: false 36 | t.string 'encrypted_password', default: '', null: false 37 | t.string 'reset_password_token' 38 | t.datetime 'reset_password_sent_at' 39 | t.datetime 'remember_created_at' 40 | t.integer 'sign_in_count', default: 0, null: false 41 | t.datetime 'current_sign_in_at' 42 | t.datetime 'last_sign_in_at' 43 | t.string 'current_sign_in_ip' 44 | t.string 'last_sign_in_ip' 45 | t.datetime 'created_at', null: false 46 | t.datetime 'updated_at', null: false 47 | t.integer 'second_factor_attempts_count', default: 0 48 | t.string 'nickname', limit: 64 49 | t.string 'otp_secret_key' 50 | t.string 'direct_otp' 51 | t.datetime 'direct_otp_sent_at' 52 | t.timestamp 'totp_timestamp' 53 | end 54 | end 55 | end 56 | end 57 | end 58 | 59 | RSpec.configuration.send(:include, AuthenticatedModelHelper) 60 | -------------------------------------------------------------------------------- /spec/support/capybara.rb: -------------------------------------------------------------------------------- 1 | require 'capybara/rspec' 2 | 3 | Capybara.app = Dummy::Application 4 | -------------------------------------------------------------------------------- /spec/support/controller_helper.rb: -------------------------------------------------------------------------------- 1 | module ControllerHelper 2 | def sign_in(user = create_user('not_encrypted')) 3 | allow(warden).to receive(:authenticated?).with(:user).and_return(true) 4 | allow(controller).to receive(:current_user).and_return(user) 5 | warden.session(:user)[TwoFactorAuthentication::NEED_AUTHENTICATION] = true 6 | end 7 | end 8 | 9 | RSpec.configure do |config| 10 | config.include Devise::Test::ControllerHelpers, type: :controller 11 | config.include ControllerHelper, type: :controller 12 | 13 | config.before(:example, type: :controller) do 14 | @request.env['devise.mapping'] = Devise.mappings[:user] 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/support/features_spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'warden' 2 | 3 | module FeaturesSpecHelper 4 | def warden 5 | request.env['warden'] 6 | end 7 | 8 | def complete_sign_in_form_for(user) 9 | fill_in "Email", with: user.email 10 | fill_in "Password", with: 'password' 11 | find('.actions input').click # 'Sign in' or 'Log in' 12 | end 13 | 14 | def set_cookie key, value 15 | page.driver.browser.set_cookie [key, value].join('=') 16 | end 17 | 18 | def get_cookie key 19 | Capybara.current_session.driver.request.cookies[key] 20 | end 21 | 22 | def set_tfa_cookie value 23 | set_cookie TwoFactorAuthentication::REMEMBER_TFA_COOKIE_NAME, value 24 | end 25 | 26 | def get_tfa_cookie 27 | get_cookie TwoFactorAuthentication::REMEMBER_TFA_COOKIE_NAME 28 | end 29 | end 30 | 31 | RSpec.configure do |config| 32 | config.include Warden::Test::Helpers, type: :feature 33 | config.include FeaturesSpecHelper, type: :feature 34 | 35 | config.before(:each) do 36 | Warden.test_mode! 37 | end 38 | 39 | config.after(:each) do 40 | Warden.test_reset! 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/support/sms_provider.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |c| 2 | c.before(:each) do 3 | SMSProvider.messages.clear 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/support/totp_helper.rb: -------------------------------------------------------------------------------- 1 | # Helper class to simulate a user generating TOTP codes from a secret key 2 | class TotpHelper 3 | def initialize(secret_key, otp_length) 4 | @secret_key = secret_key 5 | @otp_length = otp_length 6 | end 7 | 8 | def totp_code(time = Time.now) 9 | ROTP::TOTP.new(@secret_key, digits: @otp_length).at(time) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /two_factor_authentication.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "two_factor_authentication/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "two_factor_authentication" 7 | s.version = TwoFactorAuthentication::VERSION.dup 8 | s.authors = ["Dmitrii Golub"] 9 | s.email = ["dmitrii.golub@gmail.com"] 10 | s.homepage = "https://github.com/Houdini/two_factor_authentication" 11 | s.summary = %q{Two factor authentication plugin for devise} 12 | s.license = "MIT" 13 | s.description = <<-EOF 14 | ### Features ### 15 | * control sms code pattern 16 | * configure max login attempts 17 | * per user level control if he really need two factor authentication 18 | * your own sms logic 19 | EOF 20 | 21 | s.rubyforge_project = "two_factor_authentication" 22 | 23 | s.files = `git ls-files`.split("\n") 24 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 25 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 26 | s.require_paths = ["lib"] 27 | 28 | s.add_runtime_dependency 'rails', '>= 3.1.1' 29 | s.add_runtime_dependency 'devise' 30 | s.add_runtime_dependency 'randexp' 31 | s.add_runtime_dependency 'rotp', '>= 4.0.0' 32 | s.add_runtime_dependency 'encryptor' 33 | 34 | s.add_development_dependency 'bundler' 35 | s.add_development_dependency 'rake' 36 | s.add_development_dependency 'rspec-rails', '>= 3.0.1' 37 | s.add_development_dependency 'capybara', '~> 2.5' 38 | s.add_development_dependency 'pry' 39 | s.add_development_dependency 'timecop' 40 | end 41 | --------------------------------------------------------------------------------