├── .erb_lint.yml ├── .github ├── dependabot.yml └── workflows │ ├── dynamic-readme.yml │ ├── dynamic-security.yml │ └── tests.yml ├── .gitignore ├── .yardopts ├── Appraisals ├── CHANGELOG.md ├── CODEOWNERS ├── CONTRIBUTING.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── RELEASING.md ├── Rakefile ├── SECURITY.md ├── app ├── controllers │ └── clearance │ │ ├── base_controller.rb │ │ ├── passwords_controller.rb │ │ ├── sessions_controller.rb │ │ └── users_controller.rb ├── mailers │ └── clearance_mailer.rb └── views │ ├── clearance_mailer │ ├── change_password.html.erb │ └── change_password.text.erb │ ├── passwords │ ├── create.html.erb │ ├── edit.html.erb │ └── new.html.erb │ ├── sessions │ ├── _form.html.erb │ └── new.html.erb │ └── users │ ├── _form.html.erb │ └── new.html.erb ├── bin ├── appraisal ├── rake ├── rspec └── setup ├── clearance.gemspec ├── config ├── locales │ └── clearance.en.yml └── routes.rb ├── gemfiles ├── rails_7.1.gemfile ├── rails_7.2.gemfile └── rails_8.0.gemfile ├── lib ├── clearance.rb ├── clearance │ ├── authentication.rb │ ├── authorization.rb │ ├── back_door.rb │ ├── configuration.rb │ ├── constraints.rb │ ├── constraints │ │ ├── signed_in.rb │ │ └── signed_out.rb │ ├── controller.rb │ ├── default_sign_in_guard.rb │ ├── engine.rb │ ├── password_strategies.rb │ ├── password_strategies │ │ ├── argon2.rb │ │ └── bcrypt.rb │ ├── rack_session.rb │ ├── rspec.rb │ ├── session.rb │ ├── session_status.rb │ ├── sign_in_guard.rb │ ├── test_unit.rb │ ├── testing │ │ ├── controller_helpers.rb │ │ ├── deny_access_matcher.rb │ │ └── view_helpers.rb │ ├── token.rb │ ├── user.rb │ └── version.rb └── generators │ └── clearance │ ├── install │ ├── install_generator.rb │ └── templates │ │ ├── README │ │ ├── clearance.rb │ │ ├── db │ │ └── migrate │ │ │ ├── add_clearance_to_users.rb.erb │ │ │ └── create_users.rb.erb │ │ └── user.rb.erb │ ├── routes │ ├── routes_generator.rb │ └── templates │ │ └── routes.rb │ ├── specs │ ├── USAGE │ ├── specs_generator.rb │ └── templates │ │ ├── factories │ │ └── clearance.rb │ │ ├── features │ │ └── clearance │ │ │ ├── user_signs_out_spec.rb.tt │ │ │ ├── visitor_resets_password_spec.rb.tt │ │ │ ├── visitor_signs_in_spec.rb.tt │ │ │ ├── visitor_signs_up_spec.rb.tt │ │ │ └── visitor_updates_password_spec.rb.tt │ │ └── support │ │ ├── clearance.rb │ │ └── features │ │ └── clearance_helpers.rb │ └── views │ ├── USAGE │ └── views_generator.rb └── spec ├── acceptance └── clearance_installation_spec.rb ├── app_templates ├── app │ ├── controllers │ │ └── application_controller.rb │ └── models │ │ └── user.rb ├── config │ ├── initializers │ │ └── clearance.rb │ └── routes.rb └── testapp │ ├── Gemfile │ ├── app │ ├── controllers │ │ └── home_controller.rb │ └── views │ │ └── layouts │ │ └── application.html.erb │ └── config │ ├── initializers │ └── action_mailer.rb │ └── routes.rb ├── clearance ├── back_door_spec.rb ├── constraints │ ├── signed_in_spec.rb │ └── signed_out_spec.rb ├── controller_spec.rb ├── default_sign_in_guard_spec.rb ├── rack_session_spec.rb ├── session_spec.rb ├── sign_in_guard_spec.rb ├── testing │ ├── controller_helpers_spec.rb │ ├── deny_access_matcher_spec.rb │ └── view_helpers_spec.rb └── token_spec.rb ├── configuration_spec.rb ├── controllers ├── apis_controller_spec.rb ├── forgeries_controller_spec.rb ├── passwords_controller_spec.rb ├── permissions_controller_spec.rb ├── sessions_controller_spec.rb └── users_controller_spec.rb ├── dummy ├── Rakefile ├── app │ ├── assets │ │ └── config │ │ │ └── manifest.js │ ├── controllers │ │ └── application_controller.rb │ └── models │ │ ├── user.rb │ │ └── user_with_optional_password.rb ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ └── test.rb │ └── routes.rb └── db │ ├── .keep │ ├── migrate │ └── 20110111224543_create_clearance_users.rb │ └── schema.rb ├── factories └── users.rb ├── generators └── clearance │ ├── install │ └── install_generator_spec.rb │ ├── routes │ └── routes_generator_spec.rb │ ├── specs │ └── specs_generator_spec.rb │ └── views │ └── views_generator_spec.rb ├── helpers └── helper_helpers_spec.rb ├── mailers └── clearance_mailer_spec.rb ├── models └── user_spec.rb ├── password_strategies ├── argon2_spec.rb ├── bcrypt_spec.rb └── password_strategies_spec.rb ├── requests ├── authentication_cookie_spec.rb ├── backdoor_spec.rb ├── cookie_options_spec.rb ├── csrf_rotation_spec.rb ├── password_maintenance_spec.rb └── token_expiration_spec.rb ├── routing └── clearance_routes_spec.rb ├── spec_helper.rb ├── support ├── clearance.rb ├── fake_model_with_password_strategy.rb ├── fake_model_without_password_strategy.rb ├── generator_spec_helpers.rb └── request_with_remember_token.rb └── views └── view_helpers_spec.rb /.erb_lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | EnableDefaultLinters: true 3 | linters: 4 | ErbSafety: 5 | enabled: true 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: bundler 5 | directory: "/" 6 | schedule: 7 | interval: daily 8 | open-pull-requests-limit: 5 9 | 10 | - package-ecosystem: github-actions 11 | directory: "/" 12 | schedule: 13 | interval: weekly 14 | time: "02:00" 15 | timezone: "Etc/UTC" 16 | -------------------------------------------------------------------------------- /.github/workflows/dynamic-readme.yml: -------------------------------------------------------------------------------- 1 | name: update-templates 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - README.md 9 | workflow_dispatch: 10 | 11 | jobs: 12 | update-templates: 13 | permissions: 14 | contents: write 15 | pull-requests: write 16 | pages: write 17 | uses: thoughtbot/templates/.github/workflows/dynamic-readme.yaml@main 18 | secrets: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /.github/workflows/dynamic-security.yml: -------------------------------------------------------------------------------- 1 | name: update-security 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - SECURITY.md 9 | workflow_dispatch: 10 | 11 | jobs: 12 | update-security: 13 | permissions: 14 | contents: write 15 | pull-requests: write 16 | pages: write 17 | uses: thoughtbot/templates/.github/workflows/dynamic-security.yaml@main 18 | secrets: 19 | token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: CI Tests 2 | 3 | on: 4 | push: 5 | branches: "main" 6 | pull_request: 7 | branches: "*" 8 | 9 | jobs: 10 | test: 11 | name: "Ruby ${{ matrix.ruby }}, Rails ${{ matrix.gemfile }}" 12 | 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | gemfile: 19 | - "7.1" 20 | - "7.2" 21 | - "8.0" 22 | ruby: 23 | - "3.2.8" 24 | - "3.3.7" 25 | 26 | env: 27 | BUNDLE_GEMFILE: gemfiles/rails_${{ matrix.gemfile }}.gemfile 28 | RAILS_ENV: test 29 | 30 | steps: 31 | - uses: actions/checkout@v4 32 | 33 | - name: "Install Ruby ${{ matrix.ruby }}" 34 | uses: ruby/setup-ruby@v1 35 | with: 36 | ruby-version: ${{ matrix.ruby }} 37 | bundler-cache: true 38 | 39 | - name: "Reset app database" 40 | run: | 41 | bundle exec rake db:drop 42 | bundle exec rake db:setup 43 | 44 | - name: "Run tests" 45 | run: bundle exec rake 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | !.keep 2 | *.DS_Store 3 | *.swo 4 | *.swp 5 | *~ 6 | .bundle 7 | .idea 8 | .tool-versions 9 | spec/dummy/db/*.sqlite3* 10 | spec/dummy/log 11 | gemfiles/*.lock 12 | gemfiles/vendor/ 13 | log/*.log 14 | pkg 15 | tmp/ 16 | doc/ 17 | .yardoc/ 18 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --protected 2 | --private 3 | --hide-api private 4 | --exclude templates 5 | --markup markdown 6 | --markup-provider redcarpet 7 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise "rails_7.1" do 2 | gem "railties", "~> 7.1.0" 3 | end 4 | 5 | appraise "rails_7.2" do 6 | gem "railties", "~> 7.2.0" 7 | end 8 | 9 | appraise "rails_8.0" do 10 | gem "railties", "~> 8.0.0" 11 | gem 'sqlite3', '>= 2.1' 12 | end 13 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @sej3506 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | We love pull requests from everyone. By participating in this project, you agree 2 | to abide by the thoughtbot [code of conduct]. 3 | 4 | [code of conduct]: https://thoughtbot.com/open-source-code-of-conduct 5 | 6 | 1. Fork the repo. 7 | 8 | 2. Run `./bin/setup`. 9 | 10 | 3. Run the tests. We only take pull requests with passing tests, and it's great 11 | to know that you have a clean slate: `bundle exec appraisal rake` 12 | 13 | 4. Add a test for your change. Only refactoring and documentation changes 14 | require no new tests. If you are adding functionality or fixing a 15 | bug, we need a test! 16 | 17 | 5. Make the test pass. 18 | 19 | 6. Push to your fork and submit a pull request. 20 | 21 | At this point you're waiting on us. We like to at least comment on, if not 22 | accept, pull requests within three business days (and, typically, one business 23 | day). We may suggest some changes or improvements or alternatives. 24 | 25 | Some things that will increase the chance that your pull request is accepted, 26 | taken straight from the Ruby on Rails guide: 27 | 28 | * Use Rails idioms and helpers 29 | * Include tests that fail without your code, and pass with it 30 | * Update the documentation, the surrounding one, examples elsewhere, guides, 31 | whatever is affected by your contribution 32 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem 'addressable' 6 | gem 'ammeter' 7 | gem 'appraisal' 8 | gem 'capybara' 9 | gem 'database_cleaner' 10 | gem 'erb_lint', require: false 11 | gem 'factory_bot_rails' 12 | gem 'nokogiri' 13 | gem 'pry', require: false 14 | gem 'rails-controller-testing' 15 | gem 'rspec-rails' 16 | gem 'shoulda-matchers' 17 | gem 'sqlite3', '~> 1.7' 18 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | clearance (2.10.0) 5 | actionmailer (>= 5.0) 6 | activemodel (>= 5.0) 7 | activerecord (>= 5.0) 8 | argon2 (~> 2.0, >= 2.0.2) 9 | bcrypt (>= 3.1.1) 10 | email_validator (~> 2.0) 11 | railties (>= 5.0) 12 | 13 | GEM 14 | remote: https://rubygems.org/ 15 | specs: 16 | actionmailer (7.2.0) 17 | actionpack (= 7.2.0) 18 | actionview (= 7.2.0) 19 | activejob (= 7.2.0) 20 | activesupport (= 7.2.0) 21 | mail (>= 2.8.0) 22 | rails-dom-testing (~> 2.2) 23 | actionpack (7.2.0) 24 | actionview (= 7.2.0) 25 | activesupport (= 7.2.0) 26 | nokogiri (>= 1.8.5) 27 | racc 28 | rack (>= 2.2.4, < 3.2) 29 | rack-session (>= 1.0.1) 30 | rack-test (>= 0.6.3) 31 | rails-dom-testing (~> 2.2) 32 | rails-html-sanitizer (~> 1.6) 33 | useragent (~> 0.16) 34 | actionview (7.2.0) 35 | activesupport (= 7.2.0) 36 | builder (~> 3.1) 37 | erubi (~> 1.11) 38 | rails-dom-testing (~> 2.2) 39 | rails-html-sanitizer (~> 1.6) 40 | activejob (7.2.0) 41 | activesupport (= 7.2.0) 42 | globalid (>= 0.3.6) 43 | activemodel (7.2.0) 44 | activesupport (= 7.2.0) 45 | activerecord (7.2.0) 46 | activemodel (= 7.2.0) 47 | activesupport (= 7.2.0) 48 | timeout (>= 0.4.0) 49 | activesupport (7.2.0) 50 | base64 51 | bigdecimal 52 | concurrent-ruby (~> 1.0, >= 1.3.1) 53 | connection_pool (>= 2.2.5) 54 | drb 55 | i18n (>= 1.6, < 2) 56 | logger (>= 1.4.2) 57 | minitest (>= 5.1) 58 | securerandom (>= 0.3) 59 | tzinfo (~> 2.0, >= 2.0.5) 60 | addressable (2.8.7) 61 | public_suffix (>= 2.0.2, < 7.0) 62 | ammeter (1.1.7) 63 | activesupport (>= 3.0) 64 | railties (>= 3.0) 65 | rspec-rails (>= 2.2) 66 | appraisal (2.5.0) 67 | bundler 68 | rake 69 | thor (>= 0.14.0) 70 | argon2 (2.3.2) 71 | ffi (~> 1.15) 72 | ffi-compiler (~> 1.0) 73 | ast (2.4.3) 74 | base64 (0.2.0) 75 | bcrypt (3.1.20) 76 | better_html (2.1.1) 77 | actionview (>= 6.0) 78 | activesupport (>= 6.0) 79 | ast (~> 2.0) 80 | erubi (~> 1.4) 81 | parser (>= 2.4) 82 | smart_properties 83 | bigdecimal (3.1.9) 84 | builder (3.3.0) 85 | capybara (3.40.0) 86 | addressable 87 | matrix 88 | mini_mime (>= 0.1.3) 89 | nokogiri (~> 1.11) 90 | rack (>= 1.6.0) 91 | rack-test (>= 0.6.3) 92 | regexp_parser (>= 1.5, < 3.0) 93 | xpath (~> 3.2) 94 | coderay (1.1.3) 95 | concurrent-ruby (1.3.5) 96 | connection_pool (2.5.0) 97 | crass (1.0.6) 98 | database_cleaner (2.0.2) 99 | database_cleaner-active_record (>= 2, < 3) 100 | database_cleaner-active_record (2.2.0) 101 | activerecord (>= 5.a) 102 | database_cleaner-core (~> 2.0.0) 103 | database_cleaner-core (2.0.1) 104 | date (3.4.1) 105 | diff-lcs (1.5.1) 106 | drb (2.2.1) 107 | email_validator (2.2.4) 108 | activemodel 109 | erb_lint (0.9.0) 110 | activesupport 111 | better_html (>= 2.0.1) 112 | parser (>= 2.7.1.4) 113 | rainbow 114 | rubocop (>= 1) 115 | smart_properties 116 | erubi (1.13.1) 117 | factory_bot (6.4.6) 118 | activesupport (>= 5.0.0) 119 | factory_bot_rails (6.4.3) 120 | factory_bot (~> 6.4) 121 | railties (>= 5.0.0) 122 | ffi (1.17.1) 123 | ffi-compiler (1.3.2) 124 | ffi (>= 1.15.5) 125 | rake 126 | globalid (1.2.1) 127 | activesupport (>= 6.1) 128 | i18n (1.14.7) 129 | concurrent-ruby (~> 1.0) 130 | io-console (0.7.2) 131 | irb (1.14.0) 132 | rdoc (>= 4.0.0) 133 | reline (>= 0.4.2) 134 | json (2.10.2) 135 | language_server-protocol (3.17.0.4) 136 | lint_roller (1.1.0) 137 | logger (1.7.0) 138 | loofah (2.24.0) 139 | crass (~> 1.0.2) 140 | nokogiri (>= 1.12.0) 141 | mail (2.8.1) 142 | mini_mime (>= 0.1.1) 143 | net-imap 144 | net-pop 145 | net-smtp 146 | matrix (0.4.2) 147 | method_source (1.1.0) 148 | mini_mime (1.1.5) 149 | mini_portile2 (2.8.8) 150 | minitest (5.25.5) 151 | net-imap (0.5.6) 152 | date 153 | net-protocol 154 | net-pop (0.1.2) 155 | net-protocol 156 | net-protocol (0.2.2) 157 | timeout 158 | net-smtp (0.5.1) 159 | net-protocol 160 | nokogiri (1.18.6) 161 | mini_portile2 (~> 2.8.2) 162 | racc (~> 1.4) 163 | parallel (1.26.3) 164 | parser (3.3.7.3) 165 | ast (~> 2.4.1) 166 | racc 167 | prism (1.4.0) 168 | pry (0.14.2) 169 | coderay (~> 1.1) 170 | method_source (~> 1.0) 171 | psych (5.1.2) 172 | stringio 173 | public_suffix (6.0.1) 174 | racc (1.8.1) 175 | rack (3.1.7) 176 | rack-session (2.0.0) 177 | rack (>= 3.0.0) 178 | rack-test (2.1.0) 179 | rack (>= 1.3) 180 | rackup (2.1.0) 181 | rack (>= 3) 182 | webrick (~> 1.8) 183 | rails-controller-testing (1.0.5) 184 | actionpack (>= 5.0.1.rc1) 185 | actionview (>= 5.0.1.rc1) 186 | activesupport (>= 5.0.1.rc1) 187 | rails-dom-testing (2.2.0) 188 | activesupport (>= 5.0.0) 189 | minitest 190 | nokogiri (>= 1.6) 191 | rails-html-sanitizer (1.6.2) 192 | loofah (~> 2.21) 193 | nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) 194 | railties (7.2.0) 195 | actionpack (= 7.2.0) 196 | activesupport (= 7.2.0) 197 | irb (~> 1.13) 198 | rackup (>= 1.0.0) 199 | rake (>= 12.2) 200 | thor (~> 1.0, >= 1.2.2) 201 | zeitwerk (~> 2.6) 202 | rainbow (3.1.1) 203 | rake (13.2.1) 204 | rdoc (6.7.0) 205 | psych (>= 4.0.0) 206 | regexp_parser (2.10.0) 207 | reline (0.5.9) 208 | io-console (~> 0.5) 209 | rspec-core (3.13.0) 210 | rspec-support (~> 3.13.0) 211 | rspec-expectations (3.13.1) 212 | diff-lcs (>= 1.2.0, < 2.0) 213 | rspec-support (~> 3.13.0) 214 | rspec-mocks (3.13.1) 215 | diff-lcs (>= 1.2.0, < 2.0) 216 | rspec-support (~> 3.13.0) 217 | rspec-rails (6.1.4) 218 | actionpack (>= 6.1) 219 | activesupport (>= 6.1) 220 | railties (>= 6.1) 221 | rspec-core (~> 3.13) 222 | rspec-expectations (~> 3.13) 223 | rspec-mocks (~> 3.13) 224 | rspec-support (~> 3.13) 225 | rspec-support (3.13.1) 226 | rubocop (1.75.1) 227 | json (~> 2.3) 228 | language_server-protocol (~> 3.17.0.2) 229 | lint_roller (~> 1.1.0) 230 | parallel (~> 1.10) 231 | parser (>= 3.3.0.2) 232 | rainbow (>= 2.2.2, < 4.0) 233 | regexp_parser (>= 2.9.3, < 3.0) 234 | rubocop-ast (>= 1.43.0, < 2.0) 235 | ruby-progressbar (~> 1.7) 236 | unicode-display_width (>= 2.4.0, < 4.0) 237 | rubocop-ast (1.43.0) 238 | parser (>= 3.3.7.2) 239 | prism (~> 1.4) 240 | ruby-progressbar (1.13.0) 241 | securerandom (0.4.1) 242 | shoulda-matchers (6.4.0) 243 | activesupport (>= 5.2.0) 244 | smart_properties (1.17.0) 245 | sqlite3 (1.7.3) 246 | mini_portile2 (~> 2.8.0) 247 | stringio (3.1.1) 248 | thor (1.3.1) 249 | timeout (0.4.1) 250 | tzinfo (2.0.6) 251 | concurrent-ruby (~> 1.0) 252 | unicode-display_width (3.1.4) 253 | unicode-emoji (~> 4.0, >= 4.0.4) 254 | unicode-emoji (4.0.4) 255 | useragent (0.16.10) 256 | webrick (1.8.1) 257 | xpath (3.2.0) 258 | nokogiri (~> 1.8) 259 | zeitwerk (2.6.17) 260 | 261 | PLATFORMS 262 | ruby 263 | 264 | DEPENDENCIES 265 | addressable 266 | ammeter 267 | appraisal 268 | capybara 269 | clearance! 270 | database_cleaner 271 | erb_lint 272 | factory_bot_rails 273 | nokogiri 274 | pry 275 | rails-controller-testing 276 | rspec-rails 277 | shoulda-matchers 278 | sqlite3 (~> 1.7) 279 | 280 | BUNDLED WITH 281 | 2.3.15 282 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2009 thoughtbot, inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | 1. Update version file accordingly. 4 | 1. Run `bundle install` to update Gemfile.lock 5 | 1. Update `CHANGELOG.md` to reflect the changes since last release. 6 | 1. Commit changes. 7 | There shouldn't be code changes, 8 | and thus CI doesn't need to run, 9 | you can then add "[ci skip]" to the commit message. 10 | 1. Push the new commit 11 | 1. Tag the release: `git tag -s vVERSION` 12 | - We recommend the [_quick guide on how to sign a commit_] from GitHub. 13 | 1. Push changes: `git push --tags` 14 | 1. Build and publish: 15 | ```bash 16 | gem build clearance.gemspec 17 | gem push clearance-*.gem 18 | ``` 19 | 1. Add a new GitHub release using the recent `CHANGELOG.md` as the content. Sample 20 | URL: https://github.com/thoughtbot/clearance/releases/new?tag=vVERSION 21 | 1. Announce the new release, 22 | making sure to say "thank you" to the contributors 23 | who helped shape this version! 24 | 25 | [_quick guide on how to sign a commit_]: https://docs.github.com/en/github/authenticating-to-github/signing-commits 26 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | 3 | APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__) 4 | load "rails/tasks/engine.rake" 5 | 6 | require "bundler/gem_tasks" 7 | 8 | require "rspec/core/rake_task" 9 | 10 | desc "Run specs other than spec/acceptance" 11 | RSpec::Core::RakeTask.new("spec") do |task| 12 | task.exclude_pattern = "spec/acceptance/**/*_spec.rb" 13 | task.verbose = false 14 | end 15 | 16 | desc "Run acceptance specs in spec/acceptance" 17 | RSpec::Core::RakeTask.new("spec:acceptance") do |task| 18 | task.pattern = "spec/acceptance/**/*_spec.rb" 19 | task.verbose = false 20 | end 21 | 22 | desc "Lint ERB templates" 23 | task :erb_lint do 24 | sh("bundle", "exec", "erb_lint", "app/views/**/*.erb") 25 | end 26 | 27 | desc "Run the specs and acceptance tests" 28 | task default: %w(spec spec:acceptance erb_lint) 29 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | # Security Policy 3 | 4 | ## Supported Versions 5 | 6 | Only the the latest version of this project is supported at a given time. If 7 | you find a security issue with an older version, please try updating to the 8 | latest version first. 9 | 10 | If for some reason you can't update to the latest version, please let us know 11 | your reasons so that we can have a better understanding of your situation. 12 | 13 | ## Reporting a Vulnerability 14 | 15 | For security inquiries or vulnerability reports, visit 16 | . 17 | 18 | If you have any suggestions to improve this policy, visit . 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/controllers/clearance/base_controller.rb: -------------------------------------------------------------------------------- 1 | module Clearance 2 | # Top-level base class that all Clearance controllers inherit from. 3 | # Inherits from `ApplicationController` by default and can be overridden by 4 | # setting a new value with {Configuration#parent_controller=}. 5 | # @!parse 6 | # class BaseController < ApplicationController; end 7 | class BaseController < Clearance.configuration.parent_controller 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/controllers/clearance/passwords_controller.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/deprecation' 2 | 3 | class Clearance::PasswordsController < Clearance::BaseController 4 | before_action :ensure_existing_user, only: [:edit, :update] 5 | before_action :ensure_email_present, only: [:create] 6 | skip_before_action :require_login, only: [:create, :edit, :new, :update], raise: false 7 | 8 | def new 9 | render template: "passwords/new" 10 | end 11 | 12 | def create 13 | if user = find_user_for_create 14 | user.forgot_password! 15 | deliver_email(user) 16 | end 17 | 18 | render template: "passwords/create", status: :accepted 19 | end 20 | 21 | def edit 22 | @user = find_user_for_edit 23 | 24 | if params[:token] 25 | session[:password_reset_token] = params[:token] 26 | redirect_to url_for 27 | else 28 | render template: "passwords/edit" 29 | end 30 | end 31 | 32 | def update 33 | @user = find_user_for_update 34 | 35 | if @user.update_password(password_from_password_reset_params) 36 | sign_in @user if Clearance.configuration.sign_in_on_password_reset? 37 | redirect_to url_after_update, status: :see_other 38 | session[:password_reset_token] = nil 39 | else 40 | flash_failure_after_update 41 | render template: "passwords/edit", status: :unprocessable_entity 42 | end 43 | end 44 | 45 | private 46 | 47 | def deliver_email(user) 48 | ::ClearanceMailer.change_password(user).deliver_now 49 | end 50 | 51 | def password_from_password_reset_params 52 | params.dig(:password_reset, :password) 53 | end 54 | 55 | def find_user_by_id_and_confirmation_token 56 | user_param = Clearance.configuration.user_id_parameter 57 | token = params[:token] || session[:password_reset_token] 58 | 59 | Clearance.configuration.user_model. 60 | find_by(id: params[user_param], confirmation_token: token.to_s) 61 | end 62 | 63 | def email_from_password_params 64 | params.dig(:password, :email) 65 | end 66 | 67 | def find_user_for_create 68 | Clearance.configuration.user_model. 69 | find_by_normalized_email(email_from_password_params) 70 | end 71 | 72 | def find_user_for_edit 73 | find_user_by_id_and_confirmation_token 74 | end 75 | 76 | def find_user_for_update 77 | find_user_by_id_and_confirmation_token 78 | end 79 | 80 | def ensure_email_present 81 | if email_from_password_params.blank? 82 | flash_failure_when_missing_email 83 | render template: "passwords/new", status: :unprocessable_entity 84 | end 85 | end 86 | 87 | def ensure_existing_user 88 | unless find_user_by_id_and_confirmation_token 89 | flash_failure_when_forbidden 90 | render template: "passwords/new", status: :unprocessable_entity 91 | end 92 | end 93 | 94 | def flash_failure_when_forbidden 95 | flash.now[:alert] = translate(:forbidden, 96 | scope: [:clearance, :controllers, :passwords], 97 | default: t("flashes.failure_when_forbidden")) 98 | end 99 | 100 | def flash_failure_after_update 101 | flash.now[:alert] = translate(:blank_password, 102 | scope: [:clearance, :controllers, :passwords], 103 | default: t("flashes.failure_after_update")) 104 | end 105 | 106 | def flash_failure_when_missing_email 107 | flash.now[:alert] = translate(:missing_email, 108 | scope: [:clearance, :controllers, :passwords], 109 | default: t("flashes.failure_when_missing_email")) 110 | end 111 | 112 | def url_after_update 113 | Clearance.configuration.redirect_url 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /app/controllers/clearance/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class Clearance::SessionsController < Clearance::BaseController 2 | before_action :redirect_signed_in_users, only: [:new] 3 | skip_before_action :require_login, only: [:create, :new, :destroy], raise: false 4 | 5 | def create 6 | @user = authenticate(params) 7 | 8 | sign_in(@user) do |status| 9 | if status.success? 10 | redirect_back_or url_after_create 11 | else 12 | flash.now.alert = status.failure_message 13 | render template: "sessions/new", status: :unauthorized 14 | end 15 | end 16 | end 17 | 18 | def destroy 19 | sign_out 20 | redirect_to url_after_destroy, status: :see_other 21 | end 22 | 23 | def new 24 | render template: "sessions/new" 25 | end 26 | 27 | private 28 | 29 | def redirect_signed_in_users 30 | if signed_in? 31 | redirect_to url_for_signed_in_users 32 | end 33 | end 34 | 35 | def url_after_create 36 | Clearance.configuration.redirect_url 37 | end 38 | 39 | def url_after_destroy 40 | Clearance.configuration.url_after_destroy || sign_in_url 41 | end 42 | 43 | def url_for_signed_in_users 44 | url_after_create 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /app/controllers/clearance/users_controller.rb: -------------------------------------------------------------------------------- 1 | class Clearance::UsersController < Clearance::BaseController 2 | before_action :redirect_signed_in_users, only: [:create, :new] 3 | skip_before_action :require_login, only: [:create, :new], raise: false 4 | 5 | def new 6 | @user = user_from_params 7 | render template: "users/new" 8 | end 9 | 10 | def create 11 | @user = user_from_params 12 | 13 | if @user.save 14 | sign_in @user 15 | redirect_back_or url_after_create 16 | else 17 | render template: "users/new", status: :unprocessable_entity 18 | end 19 | end 20 | 21 | private 22 | 23 | def redirect_signed_in_users 24 | if signed_in? 25 | redirect_to Clearance.configuration.redirect_url 26 | end 27 | end 28 | 29 | def url_after_create 30 | Clearance.configuration.redirect_url 31 | end 32 | 33 | def user_from_params 34 | email = user_params.delete(:email) 35 | password = user_params.delete(:password) 36 | 37 | Clearance.configuration.user_model.new(user_params).tap do |user| 38 | user.email = email 39 | user.password = password 40 | end 41 | end 42 | 43 | def user_params 44 | params[Clearance.configuration.user_parameter] || {} 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /app/mailers/clearance_mailer.rb: -------------------------------------------------------------------------------- 1 | class ClearanceMailer < ActionMailer::Base 2 | def change_password(user) 3 | @user = user 4 | mail( 5 | from: Clearance.configuration.mailer_sender, 6 | to: @user.email, 7 | subject: I18n.t( 8 | :change_password, 9 | scope: [:clearance, :models, :clearance_mailer] 10 | ), 11 | ) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/views/clearance_mailer/change_password.html.erb: -------------------------------------------------------------------------------- 1 |

<%= t(".opening") %>

2 | 3 |

4 | <%= link_to t(".link_text", default: "Change my password"), 5 | url_for([@user, :password, action: :edit, token: @user.confirmation_token]) %> 6 |

7 | 8 |

<%= t(".closing") %>

9 | -------------------------------------------------------------------------------- /app/views/clearance_mailer/change_password.text.erb: -------------------------------------------------------------------------------- 1 | <%= t(".opening") %> 2 | 3 | <%= url_for([@user, :password, action: :edit, token: @user.confirmation_token]) %> 4 | 5 | <%= t(".closing") %> 6 | -------------------------------------------------------------------------------- /app/views/passwords/create.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

<%= t(".description") %>

3 |
4 | -------------------------------------------------------------------------------- /app/views/passwords/edit.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

<%= t(".title") %>

3 | 4 |

<%= t(".description") %>

5 | 6 | <%= form_for :password_reset, 7 | url: [@user, :password, token: @user.confirmation_token], 8 | html: { method: :put } do |form| %> 9 |
10 | <%= form.label :password %> 11 | <%= form.password_field :password %> 12 |
13 | 14 |
15 | <%= form.submit %> 16 |
17 | <% end %> 18 |
19 | -------------------------------------------------------------------------------- /app/views/passwords/new.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

<%= t(".title") %>

3 | 4 |

<%= t(".description") %>

5 | 6 | <%= form_for :password, url: passwords_path do |form| %> 7 |
8 | <%= form.label :email %> 9 | <%= form.email_field :email %> 10 |
11 | 12 |
13 | <%= form.submit %> 14 |
15 | <% end %> 16 |
17 | -------------------------------------------------------------------------------- /app/views/sessions/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for :session, url: session_path do |form| %> 2 |
3 | <%= form.label :email %> 4 | <%= form.email_field :email %> 5 |
6 | 7 |
8 | <%= form.label :password %> 9 | <%= form.password_field :password %> 10 |
11 | 12 |
13 | <%= form.submit %> 14 |
15 | 16 | 24 | <% end %> 25 | -------------------------------------------------------------------------------- /app/views/sessions/new.html.erb: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /app/views/users/_form.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= form.label :email %> 3 | <%= form.email_field :email %> 4 |
5 | 6 |
7 | <%= form.label :password %> 8 | <%= form.password_field :password %> 9 |
10 | -------------------------------------------------------------------------------- /app/views/users/new.html.erb: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /bin/appraisal: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # This file was generated by Bundler. 4 | # 5 | # The application 'appraisal' is installed as part of a gem, and 6 | # this file is here to facilitate running it. 7 | # 8 | 9 | require 'pathname' 10 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile", 11 | Pathname.new(__FILE__).realpath) 12 | 13 | require 'rubygems' 14 | require 'bundler/setup' 15 | 16 | load Gem.bin_path('appraisal', 'appraisal') 17 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # This file was generated by Bundler. 4 | # 5 | # The application 'rake' is installed as part of a gem, and 6 | # this file is here to facilitate running it. 7 | # 8 | 9 | require 'pathname' 10 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile", 11 | Pathname.new(__FILE__).realpath) 12 | 13 | require 'rubygems' 14 | require 'bundler/setup' 15 | 16 | load Gem.bin_path('rake', 'rake') 17 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # This file was generated by Bundler. 4 | # 5 | # The application 'rspec' is installed as part of a gem, and 6 | # this file is here to facilitate running it. 7 | # 8 | 9 | require 'pathname' 10 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile", 11 | Pathname.new(__FILE__).realpath) 12 | 13 | require 'rubygems' 14 | require 'bundler/setup' 15 | 16 | load Gem.bin_path('rspec-core', 'rspec') 17 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # Install required gems, including Appraisal, which helps us test against 6 | # multiple Rails versions 7 | gem install bundler --conservative 8 | bundle check || bundle install 9 | 10 | if [ -z "$CI" ]; then 11 | bundle exec appraisal install 12 | fi 13 | 14 | # Set up database for the application that Clearance tests against 15 | RAILS_ENV=test bundle exec rake db:drop 16 | RAILS_ENV=test bundle exec rake db:setup 17 | -------------------------------------------------------------------------------- /clearance.gemspec: -------------------------------------------------------------------------------- 1 | require_relative 'lib/clearance/version' 2 | 3 | Gem::Specification.new do |s| 4 | s.add_dependency 'bcrypt', '>= 3.1.1' 5 | s.add_dependency 'argon2', '~> 2.0', '>= 2.0.2' 6 | s.add_dependency 'email_validator', '~> 2.0' 7 | s.add_dependency 'railties', '>= 5.0' 8 | s.add_dependency 'activemodel', '>= 5.0' 9 | s.add_dependency 'activerecord', '>= 5.0' 10 | s.add_dependency 'actionmailer', '>= 5.0' 11 | s.authors = [ 12 | 'Dan Croak', 13 | 'Eugene Bolshakov', 14 | 'Mike Burns', 15 | 'Joe Ferris', 16 | 'Nick Quaranto', 17 | 'Josh Nichols', 18 | 'Matt Jankowski', 19 | 'Josh Clayton', 20 | 'Gabe Berke-Williams', 21 | 'Greg Lazarev', 22 | 'Mike Breen', 23 | 'Prem Sichanugrist', 24 | 'Harlow Ward', 25 | 'Ryan McGeary', 26 | 'Derek Prior', 27 | 'Jason Morrison', 28 | 'Galen Frechette', 29 | 'Josh Steiner', 30 | 'Dorian Marié', 31 | 'Sara Jackson' 32 | ] 33 | s.description = <<-DESCRIPTION 34 | Clearance is built to support authentication and authorization via an 35 | email/password sign-in mechanism in applications. 36 | 37 | It provides some core classes commonly used for these features, along with 38 | some opinionated defaults - but is intended to be easy to override. 39 | DESCRIPTION 40 | s.email = 'support@thoughtbot.com' 41 | s.extra_rdoc_files = %w(LICENSE README.md) 42 | s.files = `git ls-files`.split("\n") 43 | s.homepage = 'https://github.com/thoughtbot/clearance' 44 | s.license = 'MIT' 45 | s.name = %q{clearance} 46 | s.rdoc_options = ['--charset=UTF-8'] 47 | s.require_paths = ['lib'] 48 | s.required_ruby_version = Gem::Requirement.new('>= 3.1.6') 49 | s.summary = 'Rails authentication & authorization with email & password.' 50 | s.test_files = `git ls-files -- {spec}/*`.split("\n") 51 | s.version = Clearance::VERSION 52 | end 53 | -------------------------------------------------------------------------------- /config/locales/clearance.en.yml: -------------------------------------------------------------------------------- 1 | --- 2 | en: 3 | clearance: 4 | models: 5 | clearance_mailer: 6 | change_password: Change your password 7 | clearance_mailer: 8 | change_password: 9 | closing: If you didn't request this, ignore this email. Your password has 10 | not been changed. 11 | link_text: Change my password 12 | opening: "Someone, hopefully you, requested we send you a link to change 13 | your password:" 14 | flashes: 15 | failure_after_create: Bad email or password. 16 | failure_after_update: Password can't be blank. 17 | failure_when_forbidden: Please double check the URL or try submitting 18 | the form again. 19 | failure_when_not_signed_in: Please sign in to continue. 20 | failure_when_missing_email: Email can't be blank. 21 | helpers: 22 | label: 23 | password: 24 | email: Email address 25 | password_reset: 26 | password: Choose password 27 | session: 28 | password: Password 29 | user: 30 | password: Password 31 | submit: 32 | password: 33 | submit: Reset password 34 | password_reset: 35 | submit: Save this password 36 | session: 37 | submit: Sign in 38 | user: 39 | create: Sign up 40 | layouts: 41 | application: 42 | sign_in: Sign in 43 | sign_out: Sign out 44 | passwords: 45 | create: 46 | description: You will receive an email within the next few minutes. It 47 | contains instructions for changing your password. 48 | edit: 49 | description: Your password has been reset. Choose a new password below. 50 | title: Change your password 51 | new: 52 | description: To be emailed a link to reset your password, please enter 53 | your email address. 54 | title: Reset your password 55 | sessions: 56 | form: 57 | forgot_password: Forgot password? 58 | sign_up: Sign up 59 | new: 60 | title: Sign in 61 | users: 62 | new: 63 | sign_in: Sign in 64 | title: Sign up 65 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | if Clearance.configuration.routes_enabled? 2 | Rails.application.routes.draw do 3 | resources :passwords, 4 | controller: 'clearance/passwords', 5 | only: [:create, :new] 6 | 7 | resource :session, 8 | controller: 'clearance/sessions', 9 | only: [:create] 10 | 11 | resources :users, 12 | controller: 'clearance/users', 13 | only: Clearance.configuration.user_actions do 14 | if Clearance.configuration.allow_password_reset? 15 | resource :password, 16 | controller: 'clearance/passwords', 17 | only: [:edit, :update] 18 | end 19 | end 20 | 21 | get '/sign_in' => 'clearance/sessions#new', as: 'sign_in' 22 | delete '/sign_out' => 'clearance/sessions#destroy', as: 'sign_out' 23 | 24 | if Clearance.configuration.allow_sign_up? 25 | get '/sign_up' => 'clearance/users#new', as: 'sign_up' 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /gemfiles/rails_7.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "addressable" 6 | gem "ammeter" 7 | gem "appraisal" 8 | gem "capybara" 9 | gem "database_cleaner" 10 | gem "erb_lint", require: false 11 | gem "factory_bot_rails" 12 | gem "nokogiri" 13 | gem "pry", require: false 14 | gem "rails-controller-testing" 15 | gem "rspec-rails" 16 | gem "shoulda-matchers" 17 | gem "sqlite3", "~> 1.7" 18 | gem "railties", "~> 7.1.0" 19 | 20 | gemspec path: "../" 21 | -------------------------------------------------------------------------------- /gemfiles/rails_7.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "addressable" 6 | gem "ammeter" 7 | gem "appraisal" 8 | gem "capybara" 9 | gem "database_cleaner" 10 | gem "erb_lint", require: false 11 | gem "factory_bot_rails" 12 | gem "nokogiri" 13 | gem "pry", require: false 14 | gem "rails-controller-testing" 15 | gem "rspec-rails" 16 | gem "shoulda-matchers" 17 | gem "sqlite3", "~> 1.7" 18 | gem "railties", "~> 7.2.0" 19 | 20 | gemspec path: "../" 21 | -------------------------------------------------------------------------------- /gemfiles/rails_8.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "addressable" 6 | gem "ammeter" 7 | gem "appraisal" 8 | gem "capybara" 9 | gem "database_cleaner" 10 | gem "erb_lint", require: false 11 | gem "factory_bot_rails" 12 | gem "nokogiri" 13 | gem "pry", require: false 14 | gem "rails-controller-testing" 15 | gem "rspec-rails" 16 | gem "shoulda-matchers" 17 | gem "sqlite3", ">= 2.1" 18 | gem "railties", "~> 8.0.0" 19 | 20 | gemspec path: "../" 21 | -------------------------------------------------------------------------------- /lib/clearance.rb: -------------------------------------------------------------------------------- 1 | require 'clearance/configuration' 2 | require 'clearance/sign_in_guard' 3 | require 'clearance/session' 4 | require 'clearance/rack_session' 5 | require 'clearance/back_door' 6 | require 'clearance/controller' 7 | require 'clearance/user' 8 | require 'clearance/password_strategies' 9 | require 'clearance/constraints' 10 | require 'clearance/engine' 11 | 12 | module Clearance 13 | end 14 | -------------------------------------------------------------------------------- /lib/clearance/authentication.rb: -------------------------------------------------------------------------------- 1 | module Clearance 2 | module Authentication 3 | extend ActiveSupport::Concern 4 | 5 | included do 6 | if respond_to?(:helper_method) 7 | helper_method :current_user, :signed_in?, :signed_out? 8 | end 9 | 10 | private( 11 | :authenticate, 12 | :current_user, 13 | :handle_unverified_request, 14 | :sign_in, 15 | :sign_out, 16 | :signed_in?, 17 | :signed_out? 18 | ) 19 | end 20 | 21 | # Authenticate a user with a provided email and password 22 | # @param [ActionController::Parameters] params The parameters from the 23 | # sign in form. `params[:session][:email]` and 24 | # `params[:session][:password]` are required. 25 | # @return [User, nil] The user or nil if authentication fails. 26 | def authenticate(params) 27 | session_params = params.require(:session) 28 | 29 | Clearance.configuration.user_model.authenticate( 30 | session_params[:email], session_params[:password] 31 | ) 32 | end 33 | 34 | # Get the user from the current clearance session. Exposed as a 35 | # `helper_method`, making it visible to views. Prefer {#signed_in?} or 36 | # {#signed_out?} if you only want to check for the presence of a current 37 | # user rather than access the actual user. 38 | # 39 | # @return [User, nil] The user if one is signed in or nil otherwise. 40 | def current_user 41 | clearance_session.current_user 42 | end 43 | 44 | # Sign in the provided user. 45 | # @param [User] user 46 | # 47 | # Signing in will run the stack of {Configuration#sign_in_guards}. 48 | # 49 | # You can provide a block to this method to handle the result of that stack. 50 | # Your block will receive either a {SuccessStatus} or {FailureStatus} 51 | # 52 | # sign_in(user) do |status| 53 | # if status.success? 54 | # # ... 55 | # else 56 | # # ... 57 | # end 58 | # end 59 | # 60 | # For an example of how clearance uses this internally, see 61 | # {SessionsController#create}. 62 | # 63 | # Signing in will also regenerate the CSRF token for the current session, 64 | # provided {Configuration#rotate_csrf_on_sign_in?} is set. 65 | def sign_in(user, &block) 66 | clearance_session.sign_in(user, &block) 67 | 68 | if signed_in? && Clearance.configuration.rotate_csrf_on_sign_in? 69 | if request.respond_to?(:reset_csrf_token) 70 | # Rails 7.1+ 71 | request.reset_csrf_token 72 | else 73 | request.session.try(:delete, :_csrf_token) 74 | end 75 | form_authenticity_token 76 | end 77 | end 78 | 79 | # Destroy the current user's Clearance session. 80 | # See {Session#sign_out} for specifics. 81 | def sign_out 82 | clearance_session.sign_out 83 | end 84 | 85 | # True if there is a currently-signed-in user. Exposed as a `helper_method`, 86 | # making it available to views. 87 | # 88 | # Using `signed_in?` is preferable to checking {#current_user} against nil 89 | # as it will allow you to introduce a null user object more simply at a 90 | # later date. 91 | # 92 | # @return [Boolean] 93 | def signed_in? 94 | clearance_session.signed_in? 95 | end 96 | 97 | # True if there is no currently-signed-in user. Exposed as a 98 | # `helper_method`, making it available to views. 99 | # 100 | # Usings `signed_out?` is preferable to checking for presence of 101 | # {#current_user} as it will allow you to introduce a null user object more 102 | # simply at a later date. 103 | def signed_out? 104 | !signed_in? 105 | end 106 | 107 | # CSRF protection in Rails >= 3.0.4 108 | # 109 | # http://weblog.rubyonrails.org/2011/2/8/csrf-protection-bypass-in-ruby-on-rails 110 | # @private 111 | def handle_unverified_request 112 | super 113 | sign_out 114 | end 115 | 116 | protected 117 | 118 | # @api private 119 | def clearance_session 120 | request.env[:clearance] 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /lib/clearance/authorization.rb: -------------------------------------------------------------------------------- 1 | module Clearance 2 | module Authorization 3 | extend ActiveSupport::Concern 4 | 5 | included do 6 | private :deny_access, :require_login 7 | end 8 | 9 | # Use as a `before_action` to require a user be signed in to proceed. 10 | # {Authentication#signed_in?} is used to determine if there is a signed in 11 | # user or not. 12 | # 13 | # class PostsController < ApplicationController 14 | # before_action :require_login 15 | # 16 | # def index 17 | # # ... 18 | # end 19 | # end 20 | def require_login 21 | unless signed_in? 22 | deny_access(I18n.t("flashes.failure_when_not_signed_in")) 23 | end 24 | end 25 | 26 | # Responds to unauthorized requests in a manner fitting the request format. 27 | # `js`, `json`, and `xml` requests will receive a 401 with no body. All 28 | # other formats will be redirected appropriately and can optionally have the 29 | # flash message set. 30 | # 31 | # When redirecting, the originally requested url will be stored in the 32 | # session (`session[:return_to]`), allowing it to be used as a redirect url 33 | # once the user has successfully signed in. 34 | # 35 | # If there is a signed in user, the request will be redirected according to 36 | # the value returned from {#url_after_denied_access_when_signed_in}. 37 | # 38 | # If there is no signed in user, the request will be redirected according to 39 | # the value returned from {#url_after_denied_access_when_signed_out}. 40 | # For the exact redirect behavior, see {#redirect_request}. 41 | # 42 | # @param [String] flash_message 43 | def deny_access(flash_message = nil) 44 | respond_to do |format| 45 | format.any(:js, :json, :xml) { head :unauthorized } 46 | format.any { redirect_request(flash_message) } 47 | end 48 | end 49 | 50 | protected 51 | 52 | # @api private 53 | def redirect_request(flash_message) 54 | store_location 55 | 56 | if flash_message 57 | flash[:alert] = flash_message 58 | end 59 | 60 | if signed_in? 61 | redirect_to url_after_denied_access_when_signed_in 62 | else 63 | redirect_to url_after_denied_access_when_signed_out 64 | end 65 | end 66 | 67 | # @api private 68 | def clear_return_to 69 | session[:return_to] = nil 70 | end 71 | 72 | # @api private 73 | def store_location 74 | if request.get? 75 | session[:return_to] = request.original_fullpath 76 | end 77 | end 78 | 79 | # @api private 80 | def redirect_back_or(default, **options) 81 | redirect_to(return_to || default, **options) 82 | clear_return_to 83 | end 84 | 85 | # @api private 86 | def return_to 87 | if return_to_url 88 | uri = URI.parse(return_to_url) 89 | path = path_without_leading_slashes(uri) 90 | "#{path}?#{uri.query}".chomp("?") + "##{uri.fragment}".chomp("#") 91 | end 92 | end 93 | 94 | # @api private 95 | def path_without_leading_slashes(uri) 96 | uri.path.sub(/\A\/+/, "/") 97 | end 98 | 99 | # @api private 100 | def return_to_url 101 | session[:return_to] 102 | end 103 | 104 | # Used as the redirect location when {#deny_access} is called and there is a 105 | # currently signed in user. 106 | # 107 | # @return [String] 108 | def url_after_denied_access_when_signed_in 109 | Clearance.configuration.redirect_url 110 | end 111 | 112 | # Used as the redirect location when {#deny_access} is called and there is 113 | # no currently signed in user. 114 | # 115 | # @return [String] 116 | def url_after_denied_access_when_signed_out 117 | Clearance.configuration.url_after_denied_access_when_signed_out || sign_in_url 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /lib/clearance/back_door.rb: -------------------------------------------------------------------------------- 1 | module Clearance 2 | # Middleware which allows signing in by passing as=USER_ID in a query 3 | # parameter. If `User#to_param` is overriden you may pass a block to 4 | # override the default user lookup behaviour 5 | # 6 | # Designed to eliminate time in integration tests wasted by visiting and 7 | # submitting the sign in form. 8 | # 9 | # Configuration: 10 | # 11 | # # config/environments/test.rb 12 | # MyRailsApp::Application.configure do 13 | # # ... 14 | # config.middleware.use Clearance::BackDoor 15 | # # ... 16 | # end 17 | # 18 | # # or if `User#to_param` is overridden (to `username` for example): 19 | # 20 | # # config/environments/test.rb 21 | # MyRailsApp::Application.configure do 22 | # # ... 23 | # config.middleware.use Clearance::BackDoor do |username| 24 | # User.find_by(username: username) 25 | # end 26 | # # ... 27 | # end 28 | # 29 | # Usage: 30 | # 31 | # visit new_feedback_path(as: user) 32 | class BackDoor 33 | def initialize(app, &block) 34 | unless environment_is_allowed? 35 | raise error_message 36 | end 37 | 38 | @app = app 39 | @block = block 40 | end 41 | 42 | def call(env) 43 | sign_in_through_the_back_door(env) 44 | @app.call(env) 45 | end 46 | 47 | private 48 | 49 | # @api private 50 | def sign_in_through_the_back_door(env) 51 | params = Rack::Utils.parse_query(env[Rack::QUERY_STRING]) 52 | user_param = params.delete("as") 53 | 54 | if user_param.present? 55 | query_string = Rack::Utils.build_query(params) 56 | env[Rack::QUERY_STRING] = query_string 57 | user = find_user(user_param) 58 | env[:clearance].sign_in(user) 59 | end 60 | end 61 | 62 | # @api private 63 | def find_user(user_param) 64 | if @block 65 | @block.call(user_param) 66 | else 67 | Clearance.configuration.user_model.find(user_param) 68 | end 69 | end 70 | 71 | # @api private 72 | def environment_is_allowed? 73 | allowed_environments.include? Rails.env 74 | end 75 | 76 | # @api private 77 | def allowed_environments 78 | Clearance.configuration.allowed_backdoor_environments || [] 79 | end 80 | 81 | # @api private 82 | def error_message 83 | unless allowed_environments.empty? 84 | <<-EOS.squish 85 | Can't use auth backdoor outside of 86 | configured environments (#{allowed_environments.join(", ")}). 87 | EOS 88 | else 89 | "BackDoor auth is disabled." 90 | end 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/clearance/constraints.rb: -------------------------------------------------------------------------------- 1 | require 'clearance/constraints/signed_in' 2 | require 'clearance/constraints/signed_out' 3 | 4 | module Clearance 5 | # Clearance provides Rails routing constraints that can control access and the 6 | # visibility of routes at the routing layer. The {Constraints::SignedIn} 7 | # constraint can be used to make routes visible only to signed in users. The 8 | # {Constraints::SignedOut} constraint can be used to make routes visible only 9 | # to signed out users. 10 | # 11 | # @see http://guides.rubyonrails.org/routing.html#advanced-constraints 12 | module Constraints 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/clearance/constraints/signed_in.rb: -------------------------------------------------------------------------------- 1 | module Clearance 2 | module Constraints 3 | # Can be applied to make a set of routes visible only to users that are 4 | # signed in. 5 | # 6 | # # config/routes.rb 7 | # constraints Clearance::Constraints::SignedIn.new do 8 | # resources :posts 9 | # end 10 | # 11 | # In the example above, requests to `/posts` from users that are not signed 12 | # in will result in a 404. You can make additional assertions about the user 13 | # by passing a block. For instance, if you want to require that the 14 | # signed-in user be an admin: 15 | # 16 | # # config/routes.rb 17 | # constraints Clearance::Constraints::SignedIn.new { |user| user.admin? } do 18 | # resources :posts 19 | # end 20 | class SignedIn 21 | def initialize(&block) 22 | @block = block || lambda { |user| true } 23 | end 24 | 25 | def matches?(request) 26 | @request = request 27 | signed_in? && current_user_fulfills_additional_requirements? 28 | end 29 | 30 | private 31 | 32 | # @api private 33 | def clearance_session 34 | @request.env[:clearance] 35 | end 36 | 37 | # @api private 38 | def current_user 39 | clearance_session.current_user 40 | end 41 | 42 | # @api private 43 | def current_user_fulfills_additional_requirements? 44 | @block.call current_user 45 | end 46 | 47 | # @api private 48 | def signed_in? 49 | clearance_session.present? && clearance_session.signed_in? 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/clearance/constraints/signed_out.rb: -------------------------------------------------------------------------------- 1 | module Clearance 2 | module Constraints 3 | # Can be applied to make a set of routes visible only to users that are 4 | # signed out. 5 | # 6 | # # config/routes.rb 7 | # constraints Clearance::Constraints::SignedOut.new do 8 | # resources :registrations, only: [:new, :create] 9 | # end 10 | # 11 | # In the example above, requests to `/registrations/new` from users that are 12 | # signed in will result in a 404. 13 | class SignedOut 14 | def matches?(request) 15 | @request = request 16 | missing_session? || clearance_session.signed_out? 17 | end 18 | 19 | private 20 | 21 | # @api private 22 | def clearance_session 23 | @request.env[:clearance] 24 | end 25 | 26 | # @api private 27 | def missing_session? 28 | clearance_session.nil? 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/clearance/controller.rb: -------------------------------------------------------------------------------- 1 | require 'clearance/authentication' 2 | require 'clearance/authorization' 3 | 4 | module Clearance 5 | # Adds clearance controller helpers to the controller it is mixed into. 6 | # 7 | # This exposes clearance controller and helper methods such as `current_user`. 8 | # See {Authentication} and {Authorization} documentation for complete 9 | # documentation on the methods. 10 | # 11 | # The `clearance:install` generator automatically adds this mixin to 12 | # `ApplicationController`, which is the recommended configuration. 13 | # 14 | # class ApplicationController < ActionController::Base 15 | # include Clearance::Controller 16 | # end 17 | # 18 | module Controller 19 | extend ActiveSupport::Concern 20 | 21 | include Clearance::Authentication 22 | include Clearance::Authorization 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/clearance/default_sign_in_guard.rb: -------------------------------------------------------------------------------- 1 | module Clearance 2 | # Runs as the base {SignInGuard} for all requests, regardless of configured 3 | # {Configuration#sign_in_guards}. 4 | class DefaultSignInGuard < SignInGuard 5 | # Runs the default sign in guard. 6 | # 7 | # If there is a value set in the clearance session object, then the guard 8 | # returns {SuccessStatus}. Otherwise, it returns {FailureStatus} with the 9 | # message returned by {#default_failure_message}. 10 | # 11 | # @return [SuccessStatus, FailureStatus] 12 | def call 13 | if session.signed_in? 14 | success 15 | else 16 | failure default_failure_message.html_safe 17 | end 18 | end 19 | 20 | # The default failure message pulled from the i18n framework. 21 | # 22 | # Will use the value returned from the following i18n keys, in this order: 23 | # 24 | # * `clearance.controllers.sessions.bad_email_or_password` 25 | # * `flashes.failure_after_create` 26 | # 27 | # @return [String] 28 | def default_failure_message 29 | I18n.t( 30 | :bad_email_or_password, 31 | scope: [:clearance, :controllers, :sessions], 32 | default: I18n.t('flashes.failure_after_create').html_safe 33 | ) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/clearance/engine.rb: -------------------------------------------------------------------------------- 1 | require "rails/engine" 2 | 3 | module Clearance 4 | # Makes Clearance behavior available to Rails apps on initialization. By using 5 | # a Rails Engine rather than a Railtie, Clearance can automatically expose its 6 | # own routes and views to the hosting application. 7 | # 8 | # Requiring `clearance` (likely by having it in your `Gemfile`) will 9 | # automatically require the engine. You can opt-out of Clearance's internal 10 | # routes by using {Configuration#routes=}. You can override the Clearance 11 | # views by running `rails generate clearance:views`. 12 | # 13 | # In addition to providing routes and views, the Clearance engine: 14 | # 15 | # * Ensures `password` and `token` parameters are filtered out of Rails logs. 16 | # * Mounts the {RackSession} middleware in the appropriate location 17 | # * Reloads classes referenced in your {Configuration} on every request in 18 | # development mode. 19 | # 20 | class Engine < Rails::Engine 21 | initializer "clearance.filter" do |app| 22 | app.config.filter_parameters += [:password, :token] 23 | end 24 | 25 | config.app_middleware.use(Clearance::RackSession) 26 | 27 | config.to_prepare do 28 | Clearance.configuration.reload_user_model 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/clearance/password_strategies.rb: -------------------------------------------------------------------------------- 1 | module Clearance 2 | # Control how users are authenticated and how passwords are stored. 3 | # 4 | # The default password strategy is {Clearance::PasswordStrategies::BCrypt}, 5 | # but this can be overridden in {Clearance::Configuration}. 6 | # 7 | # You can supply your own password strategy by implementing a module that 8 | # responds to the proper interface methods. Once this module is configured as 9 | # your password strategy, Clearance will mix it into your Clearance User 10 | # class. Thus, your module can access any methods or attributes on User. 11 | # 12 | # Password strategies need to respond to `authenticated?(password)` and 13 | # `password=(new_password)`. For an example of how to implement these methods, 14 | # see {Clearance::PasswordStrategies::BCrypt}. 15 | module PasswordStrategies 16 | autoload :BCrypt, "clearance/password_strategies/bcrypt" 17 | autoload :Argon2, "clearance/password_strategies/argon2" 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/clearance/password_strategies/argon2.rb: -------------------------------------------------------------------------------- 1 | module Clearance 2 | module PasswordStrategies 3 | # Uses Argon2 to authenticate users and store encrypted passwords. 4 | 5 | module Argon2 6 | require "argon2" 7 | 8 | def authenticated?(password) 9 | if encrypted_password.present? 10 | ::Argon2::Password.verify_password(password, encrypted_password) 11 | end 12 | end 13 | 14 | def password=(new_password) 15 | @password = new_password 16 | 17 | if new_password.present? 18 | self.encrypted_password = ::Argon2::Password.new.create(new_password) 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/clearance/password_strategies/bcrypt.rb: -------------------------------------------------------------------------------- 1 | module Clearance 2 | module PasswordStrategies 3 | # Uses BCrypt to authenticate users and store encrypted passwords. 4 | # 5 | # BCrypt has a `cost` argument which determines how computationally 6 | # expensive the hash is to calculate. The higher the cost, the harder it is 7 | # for attackers to crack passwords even if they posess a database dump of 8 | # the encrypted passwords. Clearance uses the `bcrypt-ruby` default cost 9 | # except in the test environment, where it uses the minimum cost value for 10 | # speed. If you wish to increase the cost over the default, you can do so 11 | # by setting a higher cost in an initializer: 12 | # `BCrypt::Engine.cost = 12` 13 | module BCrypt 14 | require 'bcrypt' 15 | 16 | def authenticated?(password) 17 | if encrypted_password.present? 18 | ::BCrypt::Password.new(encrypted_password) == password 19 | end 20 | end 21 | 22 | def password=(new_password) 23 | @password = new_password 24 | 25 | if new_password.present? 26 | self.encrypted_password = ::BCrypt::Password.create( 27 | new_password, 28 | cost: configured_bcrypt_cost, 29 | ) 30 | end 31 | end 32 | 33 | def configured_bcrypt_cost 34 | if defined?(::Rails) && ::Rails.env.test? 35 | ::BCrypt::Engine::MIN_COST 36 | else 37 | ::BCrypt::Engine.cost 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/clearance/rack_session.rb: -------------------------------------------------------------------------------- 1 | module Clearance 2 | # Rack middleware that manages the Clearance {Session}. This middleware is 3 | # automatically mounted by the Clearance {Engine}. 4 | # 5 | # * maintains the session cookie specified by your {Configuration}. 6 | # * exposes previously cookied sessions to Clearance and your app at 7 | # `request.env[:clearance]`, which {Authentication#current_user} pulls the 8 | # user from. 9 | # 10 | # @see Session 11 | # @see Configuration#cookie_name 12 | # 13 | class RackSession 14 | def initialize(app) 15 | @app = app 16 | end 17 | 18 | # Reads previously existing sessions from a cookie and maintains the cookie 19 | # on each response. 20 | def call(env) 21 | session = Clearance::Session.new(env) 22 | env[:clearance] = session 23 | response = @app.call(env) 24 | 25 | if session.authentication_successful? 26 | session.add_cookie_to_headers 27 | end 28 | 29 | response 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/clearance/rspec.rb: -------------------------------------------------------------------------------- 1 | require "rspec/rails" 2 | require "clearance/testing/deny_access_matcher" 3 | require "clearance/testing/controller_helpers" 4 | require "clearance/testing/view_helpers" 5 | 6 | RSpec.configure do |config| 7 | config.include Clearance::Testing::Matchers, type: :controller 8 | config.include Clearance::Testing::ControllerHelpers, type: :controller 9 | config.include Clearance::Testing::ViewHelpers, type: :view 10 | config.include Clearance::Testing::ViewHelpers, type: :helper 11 | 12 | config.before(:each, type: :view) do 13 | view.extend Clearance::Testing::ViewHelpers::CurrentUser 14 | end 15 | 16 | config.before(:each, type: :helper) do 17 | view.extend Clearance::Testing::ViewHelpers::CurrentUser 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/clearance/session.rb: -------------------------------------------------------------------------------- 1 | require 'clearance/default_sign_in_guard' 2 | 3 | module Clearance 4 | # Represents a clearance session, ultimately persisted in 5 | # `request.env[:clearance]` by {RackSession}. 6 | class Session 7 | # @param env The current rack environment 8 | def initialize(env) 9 | @env = env 10 | @current_user = nil 11 | @cookies = nil 12 | end 13 | 14 | # Called by {RackSession} to add the Clearance session cookie to a response. 15 | # 16 | # @return [void] 17 | def add_cookie_to_headers 18 | if signed_in_with_remember_token? 19 | set_remember_token(current_user.remember_token) 20 | end 21 | end 22 | 23 | # The current user represented by this session. 24 | # 25 | # @return [User, nil] 26 | def current_user 27 | if remember_token.present? 28 | @current_user ||= user_from_remember_token(remember_token) 29 | end 30 | 31 | @current_user 32 | end 33 | 34 | # Sign the provided user in, if approved by the configured sign in guards. 35 | # If the sign in guard stack returns {SuccessStatus}, the {#current_user} 36 | # will be set and then remember token cookie will be set to the user's 37 | # remember token. If the stack returns {FailureStatus}, {#current_user} will 38 | # be nil. 39 | # 40 | # In either event, the resulting status will be yielded to a provided block, 41 | # if provided. See {SessionsController#create} for an example of how this 42 | # can be used. 43 | # 44 | # @param [User] user 45 | # @yieldparam [SuccessStatus,FailureStatus] status Result of the sign in 46 | # operation. 47 | # @return [void] 48 | def sign_in(user, &block) 49 | @current_user = user 50 | status = run_sign_in_stack 51 | 52 | if status.success? 53 | # Sign in succeeded, and when {RackSession} is run and calls 54 | # {#add_cookie_to_headers} it will set the cookie with the 55 | # remember_token for the current_user 56 | else 57 | @current_user = nil 58 | end 59 | 60 | if block_given? 61 | block.call(status) 62 | end 63 | end 64 | 65 | # Invalidates the users remember token and removes the remember token cookie 66 | # from the store. The invalidation of the remember token causes any other 67 | # sessions that are signed in from other locations to also be invalidated on 68 | # their next request. This is because all Clearance sessions for a given 69 | # user share a remember token. 70 | # 71 | # @return [void] 72 | def sign_out 73 | if signed_in? 74 | current_user.reset_remember_token! 75 | end 76 | 77 | @current_user = nil 78 | cookies.delete remember_token_cookie, delete_cookie_options 79 | end 80 | 81 | # True if {#current_user} is set. 82 | # 83 | # @return [Boolean] 84 | def signed_in? 85 | current_user.present? 86 | end 87 | 88 | # True if {#current_user} is not set 89 | # 90 | # @return [Boolean] 91 | def signed_out? 92 | ! signed_in? 93 | end 94 | 95 | # True if a successful authentication has been performed 96 | # 97 | # @return [Boolean] 98 | def authentication_successful? 99 | !!@current_user 100 | end 101 | 102 | private 103 | 104 | # @api private 105 | def cookies 106 | @cookies ||= ActionDispatch::Request.new(@env).cookie_jar 107 | end 108 | 109 | # @api private 110 | def set_remember_token(token) 111 | case Clearance.configuration.signed_cookie 112 | when true, :migrate 113 | cookies.signed[remember_token_cookie] = cookie_options(token) 114 | when false 115 | cookies[remember_token_cookie] = cookie_options(token) 116 | end 117 | remember_token 118 | end 119 | 120 | # @api private 121 | def remember_token 122 | case Clearance.configuration.signed_cookie 123 | when true 124 | cookies.signed[remember_token_cookie] 125 | when :migrate 126 | cookies.signed[remember_token_cookie] || cookies[remember_token_cookie] 127 | when false 128 | cookies[remember_token_cookie] 129 | end 130 | end 131 | 132 | # @api private 133 | def remember_token_expires 134 | expires_configuration.call(cookies) 135 | end 136 | 137 | # @api private 138 | def signed_in_with_remember_token? 139 | current_user&.remember_token 140 | end 141 | 142 | # @api private 143 | def remember_token_cookie 144 | Clearance.configuration.cookie_name.freeze 145 | end 146 | 147 | # @api private 148 | def expires_configuration 149 | Clearance.configuration.cookie_expiration 150 | end 151 | 152 | # @api private 153 | def user_from_remember_token(token) 154 | Clearance.configuration.user_model.where(remember_token: token).first 155 | end 156 | 157 | # @api private 158 | def run_sign_in_stack 159 | @stack ||= initialize_sign_in_guard_stack 160 | @stack.call 161 | end 162 | 163 | # @api private 164 | def initialize_sign_in_guard_stack 165 | default_guard = DefaultSignInGuard.new(self) 166 | guards = Clearance.configuration.sign_in_guards 167 | 168 | guards.inject(default_guard) do |stack, guard_class| 169 | guard_class.to_s.constantize.new(self, stack) 170 | end 171 | end 172 | 173 | # @api private 174 | def cookie_options(value) 175 | { 176 | domain: domain, 177 | expires: remember_token_expires, 178 | httponly: Clearance.configuration.httponly, 179 | same_site: Clearance.configuration.same_site, 180 | path: Clearance.configuration.cookie_path, 181 | secure: Clearance.configuration.secure_cookie, 182 | value: value, 183 | } 184 | end 185 | 186 | # @api private 187 | def delete_cookie_options 188 | {}.tap do |options| 189 | options[:domain] = domain if configured_cookie_domain 190 | end 191 | end 192 | 193 | # @api private 194 | def domain 195 | if configured_cookie_domain.respond_to?(:call) 196 | configured_cookie_domain.call(request_with_env) 197 | else 198 | configured_cookie_domain 199 | end 200 | end 201 | 202 | # @api private 203 | def configured_cookie_domain 204 | Clearance.configuration.cookie_domain 205 | end 206 | 207 | # @api private 208 | def request_with_env 209 | ActionDispatch::Request.new(@env) 210 | end 211 | end 212 | end 213 | -------------------------------------------------------------------------------- /lib/clearance/session_status.rb: -------------------------------------------------------------------------------- 1 | module Clearance 2 | # Indicates a user was successfully signed in, passing all {SignInGuard}s. 3 | class SuccessStatus 4 | # Is true, indicating that the sign in was successful. 5 | def success? 6 | true 7 | end 8 | end 9 | 10 | # Indicates a failure in the {SignInGuard} stack which prevented successful 11 | # sign in. 12 | class FailureStatus 13 | # The reason the sign in failed. 14 | attr_reader :failure_message 15 | 16 | # @param [String] failure_message The reason the sign in failed. 17 | def initialize(failure_message) 18 | @failure_message = failure_message 19 | end 20 | 21 | # Is false, indicating that the sign in was unsuccessful. 22 | def success? 23 | false 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/clearance/sign_in_guard.rb: -------------------------------------------------------------------------------- 1 | require 'clearance/session_status' 2 | 3 | module Clearance 4 | # The base class for {DefaultSignInGuard} and all custom sign in guards. 5 | # 6 | # Sign in guards provide you with fine-grained control over the process of 7 | # signing in a user. Each guard is run in order and can do one of the 8 | # following: 9 | # 10 | # * Fail the sign in process 11 | # * Call the next guard in the stack 12 | # * Short circuit all remaining guards, declaring sign in successfull. 13 | # 14 | # Sign In Guards could be used, for instance, to require that a user confirm 15 | # their email address before being allowed to sign in. 16 | # 17 | # # in config/initializers/clearance.rb 18 | # Clearance.configure do |config| 19 | # config.sign_in_guards = ["ConfirmationGuard"] 20 | # end 21 | # 22 | # # in app/guards/confirmation_guard.rb 23 | # class ConfirmationGuard < Clearance::SignInGuard 24 | # def call 25 | # if signed_in? && current_user.email_confirmed? 26 | # next_guard 27 | # else 28 | # failure("You must confirm your email address.") 29 | # end 30 | # end 31 | # end 32 | # 33 | # Calling `success` or `failure` in any guard short circuits all of the 34 | # remaining guards in the stack. In most cases, you will want to either call 35 | # `failure` or `next_guard`. The {DefaultSignInGuard} will always be the final 36 | # guard called and will handle calling `success` if appropriate. 37 | # 38 | # The stack is designed such that calling `call` will eventually return 39 | # {SuccessStatus} or {FailureStatus}, thus halting the chain. 40 | class SignInGuard 41 | # Creates an instance of a sign in guard. 42 | # 43 | # This is called by {Session} automatically using the array of guards 44 | # configured in {Configuration#sign_in_guards} and the {DefaultSignInGuard}. 45 | # There is no reason for users of Clearance to concern themselves with the 46 | # initialization of each guard or the stack as a whole. 47 | # 48 | # @param [Session] session The current clearance session 49 | # @param [[SignInGuard]] stack The sign in guards that come after this 50 | # guard in the stack 51 | def initialize(session, stack = []) 52 | @session = session 53 | @stack = stack 54 | end 55 | 56 | # Indicates the entire sign in operation is successful and that no further 57 | # guards should be run. 58 | # 59 | # In most cases your guards will want to delegate this responsibility to the 60 | # {DefaultSignInGuard}, allowing the entire stack to execute. In that case, 61 | # your custom guard would likely want to call `next_guard` instead. 62 | # 63 | # @return [SuccessStatus] 64 | def success 65 | SuccessStatus.new 66 | end 67 | 68 | # Indicates this guard failed, and the entire sign in process should fail as 69 | # a result. 70 | # 71 | # @param [String] message The reason the guard failed. 72 | # @return [FailureStatus] 73 | def failure(message) 74 | FailureStatus.new(message) 75 | end 76 | 77 | # Passes off responsibility for determining success or failure to the next 78 | # guard in the stack. 79 | # 80 | # @return [SuccessStatus, FailureStatus] 81 | def next_guard 82 | stack.call 83 | end 84 | 85 | private 86 | 87 | attr_reader :stack, :session 88 | 89 | # True if there is a currently a user stored in the clearance environment. 90 | def signed_in? 91 | session.signed_in? 92 | end 93 | 94 | # The user currently stored in the clearance environment. 95 | def current_user 96 | session.current_user 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/clearance/test_unit.rb: -------------------------------------------------------------------------------- 1 | require "clearance/testing/deny_access_matcher" 2 | require "clearance/testing/controller_helpers" 3 | 4 | ActionController::TestCase.extend Clearance::Testing::Matchers 5 | 6 | class ActionController::TestCase 7 | include Clearance::Testing::ControllerHelpers 8 | end 9 | -------------------------------------------------------------------------------- /lib/clearance/testing/controller_helpers.rb: -------------------------------------------------------------------------------- 1 | module Clearance 2 | module Testing 3 | # Provides helpers to your controller specs. 4 | # These are typically used in tests by requiring `clearance/rspec` or 5 | # `clearance/test_unit` as appropriate in your `rails_helper.rb` or 6 | # `test_helper.rb` files. 7 | module ControllerHelpers 8 | # @api private 9 | def setup_controller_request_and_response 10 | super 11 | @request.env[:clearance] = Clearance::Session.new(@request.env) 12 | end 13 | 14 | # Signs in a user that is created using FactoryGirl. 15 | # The factory name is derrived from your `user_class` Clearance 16 | # configuration. 17 | # 18 | # @raise [RuntimeError] if FactoryGirl is not defined. 19 | def sign_in 20 | constructor = factory_module("sign_in") 21 | 22 | factory = Clearance.configuration.user_model.to_s.underscore.to_sym 23 | sign_in_as constructor.create(factory) 24 | end 25 | 26 | # Signs in the provided user. 27 | # 28 | # @return user 29 | def sign_in_as(user) 30 | @request.env[:clearance].sign_in(user) 31 | user 32 | end 33 | 34 | # Signs out a user that may be signed in. 35 | # 36 | # @return [void] 37 | def sign_out 38 | @request.env[:clearance].sign_out 39 | end 40 | 41 | # Determines the appropriate factory library 42 | # 43 | # @api private 44 | # @raise [RuntimeError] if both FactoryGirl and FactoryBot are not 45 | # defined. 46 | def factory_module(provider) 47 | if defined?(FactoryBot) 48 | FactoryBot 49 | elsif defined?(FactoryGirl) 50 | FactoryGirl 51 | else 52 | raise("Clearance's `#{provider}` helper requires factory_bot") 53 | end 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/clearance/testing/deny_access_matcher.rb: -------------------------------------------------------------------------------- 1 | module Clearance 2 | module Testing 3 | # Provides matchers to be used in your controller specs. 4 | # These are typically exposed to your controller specs by 5 | # requiring `clearance/rspec` or `clearance/test_unit` as 6 | # appropriate in your `rails_helper.rb` or `test_helper.rb` 7 | # files. 8 | module Matchers 9 | # The `deny_access` matcher is used to assert that a 10 | # request is denied access by clearance. 11 | # @option opts [String] :flash The expected flash alert message. Defaults 12 | # to nil, which means the flash will not be checked. 13 | # @option opts [String] :redirect The expected redirect url. Defaults to 14 | # `'/'` if signed in or the `sign_in_url` if signed out. 15 | # 16 | # class PostsController < ActionController::Base 17 | # before_action :require_login 18 | # 19 | # def index 20 | # @posts = Post.all 21 | # end 22 | # end 23 | # 24 | # describe PostsController do 25 | # describe "#index" do 26 | # it "denies access to users not signed in" do 27 | # get :index 28 | # 29 | # expect(controller).to deny_access 30 | # end 31 | # end 32 | # end 33 | def deny_access(opts = {}) 34 | DenyAccessMatcher.new(self, opts) 35 | end 36 | 37 | # @api private 38 | class DenyAccessMatcher 39 | attr_reader :failure_message, :failure_message_when_negated 40 | 41 | def initialize(context, opts) 42 | @context = context 43 | @flash = opts[:flash] 44 | @url = opts[:redirect] 45 | 46 | @failure_message = '' 47 | @failure_message_when_negated = '' 48 | end 49 | 50 | def description 51 | 'deny access' 52 | end 53 | 54 | def matches?(controller) 55 | @controller = controller 56 | sets_the_flash? && redirects_to_url? 57 | end 58 | 59 | def failure_message_for_should 60 | failure_message 61 | end 62 | 63 | def failure_message_for_should_not 64 | failure_message_when_negated 65 | end 66 | 67 | private 68 | 69 | def denied_access_url 70 | if clearance_session.signed_in? 71 | Clearance.configuration.redirect_url 72 | else 73 | @controller.sign_in_url 74 | end 75 | end 76 | 77 | def clearance_session 78 | @controller.request.env[:clearance] 79 | end 80 | 81 | def flash_alert_value 82 | @controller.flash[:alert] 83 | end 84 | 85 | def redirects_to_url? 86 | @url ||= denied_access_url 87 | 88 | begin 89 | @context.send(:assert_redirected_to, @url) 90 | @failure_message_when_negated << 91 | "Didn't expect to redirect to #{@url}." 92 | true 93 | rescue ::Minitest::Assertion, ::Test::Unit::AssertionFailedError 94 | @failure_message << "Expected to redirect to #{@url} but did not." 95 | false 96 | end 97 | end 98 | 99 | def sets_the_flash? 100 | if @flash.blank? 101 | true 102 | elsif flash_alert_value == @flash 103 | @failure_message_when_negated << 104 | "Didn't expect to set the flash to #{@flash}" 105 | true 106 | else 107 | @failure_message << "Expected the flash to be set to #{@flash} "\ 108 | "but was #{flash_alert_value}" 109 | false 110 | end 111 | end 112 | end 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /lib/clearance/testing/view_helpers.rb: -------------------------------------------------------------------------------- 1 | module Clearance 2 | module Testing 3 | # Provides helpers to your view and helper specs. 4 | # Using these helpers makes `current_user`, `signed_in?` and `signed_out?` 5 | # behave properly in view and helper specs. 6 | module ViewHelpers 7 | # Sets current_user on the view under test to a new instance of your user 8 | # model. 9 | def sign_in 10 | view.current_user = Clearance.configuration.user_model.new 11 | end 12 | 13 | # Sets current_user on the view under test to the supplied user. 14 | def sign_in_as(user) 15 | view.current_user = user 16 | end 17 | 18 | # @api private 19 | module CurrentUser 20 | attr_accessor :current_user 21 | 22 | def signed_in? 23 | current_user.present? 24 | end 25 | 26 | def signed_out? 27 | !signed_in? 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/clearance/token.rb: -------------------------------------------------------------------------------- 1 | module Clearance 2 | # Random token used for password reset and remember tokens. 3 | # Clearance tokens are also public API and are intended to be used anywhere 4 | # you need a random token to correspond to a given user (e.g. you added an 5 | # email confirmation token). 6 | class Token 7 | # Generate a new random, 20 byte hex token. 8 | # 9 | # @return [String] 10 | def self.new 11 | SecureRandom.hex(20).encode('UTF-8') 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/clearance/user.rb: -------------------------------------------------------------------------------- 1 | require 'digest/sha1' 2 | require 'active_model' 3 | require 'email_validator' 4 | require 'clearance/token' 5 | 6 | module Clearance 7 | # Required to be included in your configued user class, which is `User` by 8 | # default, but can be changed with {Configuration#user_model=}. 9 | # 10 | # class User 11 | # include Clearance::User 12 | # 13 | # # ... 14 | # end 15 | # 16 | # This will also include methods exposed by your password strategy, which can 17 | # be configured with {Configuration#password_strategy=}. By default, this is 18 | # {PasswordStrategies::BCrypt}. 19 | # 20 | # ## Validations 21 | # 22 | # These validations are added to the class that the {User} module is mixed 23 | # into. 24 | # 25 | # * If {#email_optional?} is false, {#email} is validated for presence, 26 | # uniqueness and email format (using the `email_validator` gem in strict 27 | # mode). 28 | # * If {#skip_password_validation?} is false, {#password} is validated 29 | # for presence. 30 | # 31 | # ## Callbacks 32 | # 33 | # * {#normalize_email} will be called on `before_validation` 34 | # * {#generate_remember_token} will be called on `before_create` 35 | # 36 | # @!attribute email 37 | # @return [String] The user's email. 38 | # 39 | # @!attribute encrypted_password 40 | # @return [String] The user's encrypted password. 41 | # 42 | # @!attribute remember_token 43 | # @return [String] The value used to identify this user in their {Session} 44 | # cookie. 45 | # 46 | # @!attribute confirmation_token 47 | # @return [String] The value used to identify this user in the password 48 | # reset link. 49 | # 50 | # @!attribute [r] password 51 | # @return [String] Transient (non-persisted) attribute that is set when 52 | # updating a user's password. Only the {#encrypted_password} is persisted. 53 | # 54 | # @!method password= 55 | # Sets the user's encrypted_password by using the configured Password 56 | # Strategy's `password=` method. By default, this will be 57 | # {PasswordStrategies::BCrypt#password=}, but can be changed with 58 | # {Configuration#password_strategy}. 59 | # 60 | # @see PasswordStrategies 61 | # @return [void] 62 | # 63 | # @!method authenticated?(password) 64 | # Check's the provided password against the user's encrypted password using 65 | # the configured password strategy. By default, this will be 66 | # {PasswordStrategies::BCrypt#authenticated?}, but can be changed with 67 | # {Configuration#password_strategy}. 68 | # 69 | # @see PasswordStrategies 70 | # @param [String] password 71 | # The password to check. 72 | # @return [Boolean] 73 | # True if the password provided is correct for the user. 74 | # 75 | # @!method self.authenticate 76 | # Finds the user with the given email and authenticates them with the 77 | # provided password. If the email corresponds to a user and the provided 78 | # password is correct for that user, this method will return that user. 79 | # Otherwise it will return nil. 80 | # 81 | # @return [User, nil] 82 | # 83 | # @!method self.find_by_normalized_email 84 | # Finds the user with the given email. The email with be normalized via 85 | # {#normalize_email}. 86 | # 87 | # @return [User, nil] 88 | # 89 | # @!method self.normalize_email 90 | # Normalizes the provided email by downcasing and removing all spaces. 91 | # This is used by {find_by_normalized_email} and is also called when 92 | # validating a user to ensure only normalized emails are stored in the 93 | # database. 94 | # 95 | # @return [String] 96 | # 97 | module User 98 | extend ActiveSupport::Concern 99 | 100 | included do 101 | attr_reader :password 102 | 103 | include Validations 104 | include Callbacks 105 | include password_strategy 106 | 107 | def password=(value) 108 | encrypted_password_will_change! 109 | super 110 | end 111 | end 112 | 113 | # @api private 114 | module ClassMethods 115 | def authenticate(email, password) 116 | if user = find_by_normalized_email(email) 117 | if password.present? && user.authenticated?(password) 118 | user 119 | end 120 | else 121 | prevent_timing_attack 122 | end 123 | end 124 | 125 | def find_by_normalized_email(email) 126 | find_by(email: normalize_email(email)) 127 | end 128 | 129 | def normalize_email(email) 130 | email.to_s.downcase.gsub(/\s+/, "") 131 | end 132 | 133 | private 134 | 135 | DUMMY_PASSWORD = "*" 136 | 137 | def prevent_timing_attack 138 | new(password: DUMMY_PASSWORD) 139 | nil 140 | end 141 | 142 | def password_strategy 143 | Clearance.configuration.password_strategy || PasswordStrategies::BCrypt 144 | end 145 | end 146 | 147 | # @api private 148 | module Validations 149 | extend ActiveSupport::Concern 150 | 151 | included do 152 | validates :email, 153 | email: { mode: :strict }, 154 | presence: true, 155 | uniqueness: { allow_blank: true, case_sensitive: true }, 156 | unless: :email_optional? 157 | 158 | validates :password, presence: true, unless: :skip_password_validation? 159 | end 160 | end 161 | 162 | # @api private 163 | module Callbacks 164 | extend ActiveSupport::Concern 165 | 166 | included do 167 | before_validation :normalize_email 168 | before_create :generate_remember_token 169 | end 170 | end 171 | 172 | # Generates a {#confirmation_token} for the user, which allows them to reset 173 | # their password via an email link. 174 | # 175 | # Calling `forgot_password!` will cause the user model to be saved without 176 | # validations. Any other changes you made to this user instance will also 177 | # be persisted, without validation. It is inteded to be called on an 178 | # instance with no changes (`dirty? == false`). 179 | # 180 | # @return [Boolean] Was the save successful? 181 | def forgot_password! 182 | generate_confirmation_token 183 | save validate: false 184 | end 185 | 186 | # Generates a new {#remember_token} for the user, which will have the effect 187 | # of signing all of the user's current sessions out. This is called 188 | # internally by {Session#sign_out}. 189 | # 190 | # Calling `reset_remember_token!` will cause the user model to be saved 191 | # without validations. Any other changes you made to this user instance will 192 | # also be persisted, without validation. It is inteded to be called on an 193 | # instance with no changes (`dirty? == false`). 194 | # 195 | # @return [Boolean] Was the save successful? 196 | def reset_remember_token! 197 | generate_remember_token 198 | save validate: false 199 | end 200 | 201 | # Sets the user's password to the new value, using the `password=` method on 202 | # the configured password strategy. By default, this is 203 | # {PasswordStrategies::BCrypt#password=}. 204 | # 205 | # This also has the side-effect of blanking the {#confirmation_token} and 206 | # rotating the `#remember_token`. 207 | # 208 | # Validations will be run as part of this update. If the user instance is 209 | # not valid, the password change will not be persisted, and this method will 210 | # return `false`. 211 | # 212 | # @return [Boolean] Was the save successful? 213 | def update_password(new_password) 214 | self.password = new_password 215 | 216 | if valid? 217 | self.confirmation_token = nil 218 | generate_remember_token 219 | end 220 | 221 | save 222 | end 223 | 224 | private 225 | 226 | # Sets the email on this instance to the value returned by 227 | # {.normalize_email} 228 | # 229 | # @return [String] 230 | def normalize_email 231 | self.email = self.class.normalize_email(email) 232 | end 233 | 234 | # Always false. Override this method in your user model to allow for other 235 | # forms of user authentication (username, Facebook, etc). 236 | # 237 | # @return [Boolean] 238 | def email_optional? 239 | false 240 | end 241 | 242 | # Always false. Override this method in your user model to allow for other 243 | # forms of user authentication (username, Facebook, etc). 244 | # 245 | # @return [Boolean] 246 | def password_optional? 247 | false 248 | end 249 | 250 | # True if {#password_optional?} is true or if the user already has an 251 | # {#encrypted_password} that is not changing. 252 | # 253 | # @return [Boolean] 254 | def skip_password_validation? 255 | password_optional? || 256 | (encrypted_password.present? && !encrypted_password_changed?) 257 | end 258 | 259 | # Sets the {#confirmation_token} on the instance to a new value generated by 260 | # {Token.new}. The change is not automatically persisted. If you would like 261 | # to generate and save in a single method call, use {#forgot_password!}. 262 | # 263 | # @return [String] The new confirmation token 264 | def generate_confirmation_token 265 | self.confirmation_token = Clearance::Token.new 266 | end 267 | 268 | # Sets the {#remember_token} on the instance to a new value generated by 269 | # {Token.new}. The change is not automatically persisted. If you would like 270 | # to generate and save in a single method call, use 271 | # {#reset_remember_token!}. 272 | # 273 | # @return [String] The new remember token 274 | def generate_remember_token 275 | self.remember_token = Clearance::Token.new 276 | end 277 | 278 | private_constant :Callbacks, :ClassMethods, :Validations 279 | end 280 | end 281 | -------------------------------------------------------------------------------- /lib/clearance/version.rb: -------------------------------------------------------------------------------- 1 | module Clearance 2 | VERSION = "2.10.0".freeze 3 | end 4 | -------------------------------------------------------------------------------- /lib/generators/clearance/install/install_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators/base' 2 | require 'rails/generators/active_record' 3 | 4 | module Clearance 5 | module Generators 6 | class InstallGenerator < Rails::Generators::Base 7 | include Rails::Generators::Migration 8 | source_root File.expand_path('../templates', __FILE__) 9 | 10 | def create_clearance_initializer 11 | copy_file 'clearance.rb', 'config/initializers/clearance.rb' 12 | end 13 | 14 | def inject_clearance_into_application_controller 15 | inject_into_class( 16 | "app/controllers/application_controller.rb", 17 | ApplicationController, 18 | " include Clearance::Controller\n" 19 | ) 20 | end 21 | 22 | def create_or_inject_clearance_into_user_model 23 | if File.exist? "app/models/user.rb" 24 | inject_into_file( 25 | "app/models/user.rb", 26 | " include Clearance::User\n\n", 27 | after: "class User < #{models_inherit_from}\n", 28 | ) 29 | else 30 | @inherit_from = models_inherit_from 31 | template("user.rb.erb", "app/models/user.rb") 32 | end 33 | end 34 | 35 | def create_clearance_migration 36 | if users_table_exists? 37 | create_add_columns_migration 38 | else 39 | copy_migration "create_users" 40 | end 41 | end 42 | 43 | def display_readme_in_terminal 44 | readme 'README' 45 | end 46 | 47 | private 48 | 49 | def create_add_columns_migration 50 | if migration_needed? 51 | config = { 52 | new_columns: new_columns, 53 | new_indexes: new_indexes 54 | } 55 | 56 | copy_migration("add_clearance_to_users", config) 57 | end 58 | end 59 | 60 | def copy_migration(migration_name, config = {}) 61 | unless migration_exists?(migration_name) 62 | migration_template( 63 | "db/migrate/#{migration_name}.rb.erb", 64 | "db/migrate/#{migration_name}.rb", 65 | config.merge(migration_version: migration_version), 66 | ) 67 | end 68 | end 69 | 70 | def migration_needed? 71 | new_columns.any? || new_indexes.any? 72 | end 73 | 74 | def new_columns 75 | @new_columns ||= { 76 | email: "t.string :email", 77 | encrypted_password: "t.string :encrypted_password, limit: 128", 78 | confirmation_token: "t.string :confirmation_token, limit: 128", 79 | remember_token: "t.string :remember_token, limit: 128", 80 | }.reject { |column| existing_users_columns.include?(column.to_s) } 81 | end 82 | 83 | def new_indexes 84 | @new_indexes ||= { 85 | index_users_on_email: 86 | "add_index :users, :email", 87 | index_users_on_confirmation_token: 88 | "add_index :users, :confirmation_token, unique: true", 89 | index_users_on_remember_token: 90 | "add_index :users, :remember_token, unique: true", 91 | }.reject { |index| existing_users_indexes.include?(index.to_s) } 92 | end 93 | 94 | def migration_exists?(name) 95 | existing_migrations.include?(name) 96 | end 97 | 98 | def existing_migrations 99 | @existing_migrations ||= Dir.glob("db/migrate/*.rb").map do |file| 100 | migration_name_without_timestamp(file) 101 | end 102 | end 103 | 104 | def migration_name_without_timestamp(file) 105 | file.sub(%r{^.*(db/migrate/)(?:\d+_)?}, '') 106 | end 107 | 108 | def users_table_exists? 109 | ActiveRecord::Base.connection.data_source_exists?(:users) 110 | end 111 | 112 | def existing_users_columns 113 | ActiveRecord::Base.connection.columns(:users).map(&:name) 114 | end 115 | 116 | def existing_users_indexes 117 | ActiveRecord::Base.connection.indexes(:users).map(&:name) 118 | end 119 | 120 | # for generating a timestamp when using `create_migration` 121 | def self.next_migration_number(dir) 122 | ActiveRecord::Generators::Base.next_migration_number(dir) 123 | end 124 | 125 | def migration_version 126 | "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]" 127 | end 128 | 129 | def migration_primary_key_type_string 130 | if configured_key_type 131 | ", id: :#{configured_key_type}" 132 | end 133 | end 134 | 135 | def configured_key_type 136 | active_record = Rails.configuration.generators.active_record 137 | active_record ||= Rails.configuration.generators.options[:active_record] 138 | 139 | active_record[:primary_key_type] 140 | end 141 | 142 | def models_inherit_from 143 | "ApplicationRecord" 144 | end 145 | end 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /lib/generators/clearance/install/templates/README: -------------------------------------------------------------------------------- 1 | 2 | ******************************************************************************* 3 | 4 | Next steps: 5 | 6 | 1. Configure the mailer to create full URLs in emails: 7 | 8 | # config/environments/{development,test}.rb 9 | config.action_mailer.default_url_options = { host: 'localhost:3000' } 10 | 11 | In the production environment it should be your application's full hostname. 12 | 13 | 2. Display user session status. 14 | 15 | From somewhere in your layout, render sign in and sign out buttons: 16 | 17 | <% if signed_in? %> 18 | Signed in as: <%= current_user.email %> 19 | <%= button_to 'Sign out', sign_out_path, method: :delete %> 20 | <% else %> 21 | <%= link_to 'Sign in', sign_in_path %> 22 | <% end %> 23 | 24 | 3. Render the flash contents. 25 | 26 | Make sure the flash is being rendered in your views using something like: 27 | 28 |
29 | <% flash.each do |key, value| %> 30 |
<%= value %>
31 | <% end %> 32 |
33 | 34 | 4. Migrate: 35 | 36 | Run `rails db:migrate` to add the clearance database changes. 37 | 38 | ******************************************************************************* 39 | -------------------------------------------------------------------------------- /lib/generators/clearance/install/templates/clearance.rb: -------------------------------------------------------------------------------- 1 | Clearance.configure do |config| 2 | config.mailer_sender = "reply@example.com" 3 | config.rotate_csrf_on_sign_in = true 4 | end 5 | -------------------------------------------------------------------------------- /lib/generators/clearance/install/templates/db/migrate/add_clearance_to_users.rb.erb: -------------------------------------------------------------------------------- 1 | class AddClearanceToUsers < ActiveRecord::Migration<%= migration_version %> 2 | def self.up 3 | <% if config[:new_columns].any? -%> 4 | change_table :users do |t| 5 | <% config[:new_columns].values.each do |column| -%> 6 | <%= column %> 7 | <% end -%> 8 | end 9 | <% end -%> 10 | <% if config[:new_indexes].any? -%> 11 | <% config[:new_indexes].values.each do |index| -%> 12 | <%= index %> 13 | <% end -%> 14 | <% end -%> 15 | <% if config[:new_columns].keys.include?(:remember_token) -%> 16 | Clearance.configuration.user_model.where(remember_token: nil).each do |user| 17 | user.update_columns(remember_token: Clearance::Token.new) 18 | end 19 | <% end -%> 20 | end 21 | 22 | def self.down 23 | <% config[:new_indexes].values.each do |index| -%> 24 | <%= index.sub("add_index", "remove_index") %> 25 | <% end -%> 26 | <% if config[:new_columns].any? -%> 27 | change_table :users do |t| 28 | <% config[:new_columns].keys.each do |key| -%> 29 | t.remove <%= key.inspect %> 30 | <% end -%> 31 | end 32 | <% end -%> 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/generators/clearance/install/templates/db/migrate/create_users.rb.erb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration<%= migration_version %> 2 | def change 3 | create_table :users<%= migration_primary_key_type_string %> do |t| 4 | t.timestamps null: false 5 | t.string :email, null: false 6 | t.string :encrypted_password, limit: 128, null: false 7 | t.string :confirmation_token, limit: 128 8 | t.string :remember_token, limit: 128, null: false 9 | end 10 | 11 | add_index :users, :email 12 | add_index :users, :confirmation_token, unique: true 13 | add_index :users, :remember_token, unique: true 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/generators/clearance/install/templates/user.rb.erb: -------------------------------------------------------------------------------- 1 | class User < <%= @inherit_from %> 2 | include Clearance::User 3 | end 4 | -------------------------------------------------------------------------------- /lib/generators/clearance/routes/routes_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators/base' 2 | 3 | module Clearance 4 | module Generators 5 | class RoutesGenerator < Rails::Generators::Base 6 | source_root File.expand_path('../templates', __FILE__) 7 | 8 | def inject_clearance_routes_into_application_routes 9 | route(clearance_routes) 10 | end 11 | 12 | def disable_clearance_internal_routes 13 | inject_into_file( 14 | "config/initializers/clearance.rb", 15 | " config.routes = false\n", 16 | after: "Clearance.configure do |config|\n", 17 | ) 18 | end 19 | 20 | private 21 | 22 | def clearance_routes 23 | File.read(routes_file_path) 24 | end 25 | 26 | def routes_file_path 27 | File.expand_path(find_in_source_paths('routes.rb')) 28 | end 29 | 30 | def route(routing_code) 31 | log :route, "all clearance routes" 32 | sentinel = /\.routes\.draw do\s*\n/m 33 | 34 | in_root do 35 | inject_into_file( 36 | "config/routes.rb", 37 | routing_code, 38 | after: sentinel, 39 | verbose: false, 40 | force: true, 41 | ) 42 | end 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/generators/clearance/routes/templates/routes.rb: -------------------------------------------------------------------------------- 1 | resources :passwords, controller: "clearance/passwords", only: [:create, :new] 2 | resource :session, controller: "clearance/sessions", only: [:create] 3 | 4 | resources :users, controller: "clearance/users", only: [:create] do 5 | resource :password, 6 | controller: "clearance/passwords", 7 | only: [:edit, :update] 8 | end 9 | 10 | get "/sign_in" => "clearance/sessions#new", as: "sign_in" 11 | delete "/sign_out" => "clearance/sessions#destroy", as: "sign_out" 12 | get "/sign_up" => "clearance/users#new", as: "sign_up" 13 | -------------------------------------------------------------------------------- /lib/generators/clearance/specs/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Generate RSpec integration tests 3 | 4 | Examples: 5 | rails generate clearance:specs -------------------------------------------------------------------------------- /lib/generators/clearance/specs/specs_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators/base' 2 | require 'rspec/rails/version' 3 | 4 | module Clearance 5 | module Generators 6 | class SpecsGenerator < Rails::Generators::Base 7 | source_root File.expand_path('../templates', __FILE__) 8 | 9 | def create_specs 10 | @helper_file = rspec_helper_file 11 | directory '.', 'spec' 12 | end 13 | 14 | private 15 | 16 | def rspec_helper_file 17 | if RSpec::Rails::Version::STRING.to_i > 2 18 | "rails_helper" 19 | else 20 | "spec_helper" 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/generators/clearance/specs/templates/factories/clearance.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | sequence :email do |n| 3 | "user#{n}@example.com" 4 | end 5 | 6 | factory :user do 7 | email 8 | password { "password" } 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/generators/clearance/specs/templates/features/clearance/user_signs_out_spec.rb.tt: -------------------------------------------------------------------------------- 1 | require "<%= @helper_file %>" 2 | require "support/features/clearance_helpers" 3 | 4 | RSpec.feature "User signs out" do 5 | scenario "signs out" do 6 | sign_in 7 | sign_out 8 | 9 | expect_user_to_be_signed_out 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/generators/clearance/specs/templates/features/clearance/visitor_resets_password_spec.rb.tt: -------------------------------------------------------------------------------- 1 | require "<%= @helper_file %>" 2 | require "support/features/clearance_helpers" 3 | 4 | RSpec.feature "Visitor resets password" do 5 | before { ActionMailer::Base.deliveries.clear } 6 | <% if defined?(ActiveJob) -%> 7 | 8 | around do |example| 9 | original_adapter = ActiveJob::Base.queue_adapter 10 | ActiveJob::Base.queue_adapter = :inline 11 | example.run 12 | ActiveJob::Base.queue_adapter = original_adapter 13 | end 14 | <% end -%> 15 | 16 | scenario "by navigating to the page" do 17 | visit sign_in_path 18 | 19 | click_link I18n.t("sessions.form.forgot_password") 20 | 21 | expect(current_path).to eq new_password_path 22 | end 23 | 24 | scenario "with valid email" do 25 | user = user_with_reset_password 26 | 27 | expect_page_to_display_change_password_message 28 | expect_reset_notification_to_be_sent_to user 29 | end 30 | 31 | scenario "with non-user account" do 32 | reset_password_for "unknown.email@example.com" 33 | 34 | expect_page_to_display_change_password_message 35 | expect_mailer_to_have_no_deliveries 36 | end 37 | 38 | private 39 | 40 | def expect_reset_notification_to_be_sent_to(user) 41 | expect(user.confirmation_token).not_to be_blank 42 | expect_mailer_to_have_delivery( 43 | user.email, 44 | "password", 45 | user.confirmation_token, 46 | ) 47 | end 48 | 49 | def expect_page_to_display_change_password_message 50 | expect(page).to have_content I18n.t("passwords.create.description") 51 | end 52 | 53 | def expect_mailer_to_have_delivery(recipient, subject, body) 54 | expect(ActionMailer::Base.deliveries).not_to be_empty 55 | 56 | message = ActionMailer::Base.deliveries.any? do |email| 57 | email.to == [recipient] && 58 | email.subject =~ /#{subject}/i && 59 | email.html_part.body =~ /#{body}/ && 60 | email.text_part.body =~ /#{body}/ 61 | end 62 | 63 | expect(message).to be 64 | end 65 | 66 | def expect_mailer_to_have_no_deliveries 67 | expect(ActionMailer::Base.deliveries).to be_empty 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/generators/clearance/specs/templates/features/clearance/visitor_signs_in_spec.rb.tt: -------------------------------------------------------------------------------- 1 | require "<%= @helper_file %>" 2 | require "support/features/clearance_helpers" 3 | 4 | RSpec.feature "Visitor signs in" do 5 | scenario "with valid email and password" do 6 | create_user "user@example.com", "password" 7 | sign_in_with "user@example.com", "password" 8 | 9 | expect_user_to_be_signed_in 10 | end 11 | 12 | scenario "with valid mixed-case email and password " do 13 | create_user "user.name@example.com", "password" 14 | sign_in_with "User.Name@example.com", "password" 15 | 16 | expect_user_to_be_signed_in 17 | end 18 | 19 | scenario "tries with invalid password" do 20 | create_user "user@example.com", "password" 21 | sign_in_with "user@example.com", "wrong_password" 22 | 23 | expect_page_to_display_sign_in_error 24 | expect_user_to_be_signed_out 25 | end 26 | 27 | scenario "tries with invalid email" do 28 | sign_in_with "unknown.email@example.com", "password" 29 | 30 | expect_page_to_display_sign_in_error 31 | expect_user_to_be_signed_out 32 | end 33 | 34 | private 35 | 36 | def create_user(email, password) 37 | FactoryBot.create(:user, email: email, password: password) 38 | end 39 | 40 | def expect_page_to_display_sign_in_error 41 | expect(page.body).to include( 42 | I18n.t("flashes.failure_after_create", sign_up_path: sign_up_path), 43 | ) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/generators/clearance/specs/templates/features/clearance/visitor_signs_up_spec.rb.tt: -------------------------------------------------------------------------------- 1 | require "<%= @helper_file %>" 2 | require "support/features/clearance_helpers" 3 | 4 | RSpec.feature "Visitor signs up" do 5 | scenario "by navigating to the page" do 6 | visit sign_in_path 7 | 8 | click_link I18n.t("sessions.form.sign_up") 9 | 10 | expect(current_path).to eq sign_up_path 11 | end 12 | 13 | scenario "with valid email and password" do 14 | sign_up_with "valid@example.com", "password" 15 | 16 | expect_user_to_be_signed_in 17 | end 18 | 19 | scenario "tries with invalid email" do 20 | sign_up_with "invalid_email", "password" 21 | 22 | expect_user_to_be_signed_out 23 | end 24 | 25 | scenario "tries with blank password" do 26 | sign_up_with "valid@example.com", "" 27 | 28 | expect_user_to_be_signed_out 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/generators/clearance/specs/templates/features/clearance/visitor_updates_password_spec.rb.tt: -------------------------------------------------------------------------------- 1 | require "<%= @helper_file %>" 2 | require "support/features/clearance_helpers" 3 | 4 | RSpec.feature "Visitor updates password" do 5 | scenario "with valid password" do 6 | user = user_with_reset_password 7 | update_password user, "newpassword" 8 | 9 | expect_user_to_be_signed_in 10 | end 11 | 12 | scenario "signs in with new password" do 13 | user = user_with_reset_password 14 | update_password user, "newpassword" 15 | sign_out 16 | sign_in_with user.email, "newpassword" 17 | 18 | expect_user_to_be_signed_in 19 | end 20 | 21 | scenario "tries with a blank password" do 22 | user = user_with_reset_password 23 | visit_password_reset_page_for user 24 | change_password_to "" 25 | 26 | expect(page).to have_content I18n.t("flashes.failure_after_update") 27 | expect_user_to_be_signed_out 28 | end 29 | 30 | private 31 | 32 | def update_password(user, password) 33 | visit_password_reset_page_for user 34 | change_password_to password 35 | end 36 | 37 | def visit_password_reset_page_for(user) 38 | visit edit_user_password_path( 39 | user_id: user, 40 | token: user.confirmation_token, 41 | ) 42 | end 43 | 44 | def change_password_to(password) 45 | fill_in "password_reset_password", with: password 46 | click_button I18n.t("helpers.submit.password_reset.submit") 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/generators/clearance/specs/templates/support/clearance.rb: -------------------------------------------------------------------------------- 1 | require "clearance/rspec" 2 | -------------------------------------------------------------------------------- /lib/generators/clearance/specs/templates/support/features/clearance_helpers.rb: -------------------------------------------------------------------------------- 1 | module Features 2 | module ClearanceHelpers 3 | def reset_password_for(email) 4 | visit new_password_path 5 | fill_in "password_email", with: email 6 | click_button I18n.t("helpers.submit.password.submit") 7 | end 8 | 9 | def sign_in 10 | password = "password" 11 | user = FactoryBot.create(:user, password: password) 12 | sign_in_with user.email, password 13 | end 14 | 15 | def sign_in_with(email, password) 16 | visit sign_in_path 17 | fill_in "session_email", with: email 18 | fill_in "session_password", with: password 19 | click_button I18n.t("helpers.submit.session.submit") 20 | end 21 | 22 | def sign_out 23 | click_button I18n.t("layouts.application.sign_out") 24 | end 25 | 26 | def sign_up_with(email, password) 27 | visit sign_up_path 28 | fill_in "user_email", with: email 29 | fill_in "user_password", with: password 30 | click_button I18n.t("helpers.submit.user.create") 31 | end 32 | 33 | def expect_user_to_be_signed_in 34 | visit root_path 35 | expect(page).to have_button I18n.t("layouts.application.sign_out") 36 | end 37 | 38 | def expect_user_to_be_signed_out 39 | visit root_path 40 | expect(page).to have_content I18n.t("layouts.application.sign_in") 41 | end 42 | 43 | def user_with_reset_password 44 | user = FactoryBot.create(:user) 45 | reset_password_for user.email 46 | user.reload 47 | end 48 | end 49 | end 50 | 51 | RSpec.configure do |config| 52 | config.include Features::ClearanceHelpers, type: :feature 53 | end 54 | -------------------------------------------------------------------------------- /lib/generators/clearance/views/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Override the default clearance views. This generator will copy all off the 3 | base clearance views into your project. 4 | 5 | Examples: 6 | rails generate clearance:views 7 | 8 | View: app/views/clearance_mailer/change_password.html.erb 9 | View: app/views/passwords/create.html.erb 10 | View: app/views/passwords/edit.html.erb 11 | View: app/views/passwords/new.html.erb 12 | View: app/views/sessions/_form.html.erb 13 | View: app/views/sessions/new.html.erb 14 | View: app/views/users/_form.html.erb 15 | View: app/views/users/new.html.erb 16 | -------------------------------------------------------------------------------- /lib/generators/clearance/views/views_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators/base' 2 | 3 | module Clearance 4 | module Generators 5 | class ViewsGenerator < Rails::Generators::Base 6 | source_root File.expand_path("../../../../..", __FILE__) 7 | 8 | def create_views 9 | views.each do |view| 10 | copy_file view 11 | end 12 | end 13 | 14 | def create_locales 15 | locales.each do |locale| 16 | copy_file locale 17 | end 18 | end 19 | 20 | private 21 | 22 | def views 23 | files_within_root('.', 'app/views/**/*.*') 24 | end 25 | 26 | def locales 27 | files_within_root('.', 'config/locales/**/*.*') 28 | end 29 | 30 | def files_within_root(prefix, glob) 31 | root = "#{self.class.source_root}/#{prefix}" 32 | 33 | Dir["#{root}/#{glob}"].sort.map do |full_path| 34 | full_path.sub(root, '.').gsub('/./', '/') 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/acceptance/clearance_installation_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "Clearance Installation" do 4 | around do |example| 5 | Dir.chdir("tmp") do 6 | FileUtils.rm_rf("testapp") 7 | example.run 8 | end 9 | end 10 | 11 | it "can successfully run specs" do 12 | app_name = "testapp" 13 | 14 | generate_test_app(app_name) 15 | 16 | Dir.chdir(app_name) do 17 | configure_test_app 18 | install_dependencies 19 | configure_rspec 20 | install_clearance 21 | run_specs 22 | end 23 | end 24 | 25 | def generate_test_app(app_name) 26 | successfully <<-CMD.squish 27 | bundle exec rails new #{app_name} 28 | --no-rc 29 | --skip-action-cable 30 | --skip-active-storage 31 | --skip-bootsnap 32 | --skip-bundle 33 | --skip-gemfile 34 | --skip-git 35 | --skip-javascript 36 | --skip-keeps 37 | --skip-sprockets 38 | --skip-asset-pipeline 39 | CMD 40 | end 41 | 42 | def testapp_templates 43 | File.expand_path("../../app_templates/testapp/", __FILE__) 44 | end 45 | 46 | def configure_test_app 47 | FileUtils.rm_f("public/index.html") 48 | FileUtils.cp_r(testapp_templates, "..") 49 | end 50 | 51 | def install_dependencies 52 | successfully "bundle install --local" 53 | end 54 | 55 | def configure_rspec 56 | successfully "bundle exec rails generate rspec:install" 57 | end 58 | 59 | def install_clearance 60 | successfully "bundle exec rails generate clearance:install" 61 | successfully "bundle exec rails generate clearance:specs" 62 | successfully "bundle exec rake db:migrate db:test:prepare" 63 | end 64 | 65 | def run_specs 66 | successfully "bundle exec rspec", false 67 | end 68 | 69 | def successfully(command, silent = true) 70 | if silent 71 | silencer = "1>/dev/null" 72 | else 73 | silencer = "" 74 | end 75 | 76 | return_value = system("#{command} #{silencer}") 77 | 78 | expect(return_value).to eq true 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/app_templates/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | end 3 | -------------------------------------------------------------------------------- /spec/app_templates/app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ApplicationRecord 2 | def previously_existed? 3 | true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/app_templates/config/initializers/clearance.rb: -------------------------------------------------------------------------------- 1 | Clearance.configure do |config| 2 | end 3 | -------------------------------------------------------------------------------- /spec/app_templates/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | root to: "application#show" 3 | end 4 | -------------------------------------------------------------------------------- /spec/app_templates/testapp/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "rails" 4 | gem "sqlite3" 5 | gem "rspec-rails" 6 | gem "capybara" 7 | gem "factory_bot_rails" 8 | gem "database_cleaner" 9 | gem "clearance", path: "../.." 10 | -------------------------------------------------------------------------------- /spec/app_templates/testapp/app/controllers/home_controller.rb: -------------------------------------------------------------------------------- 1 | class HomeController < ApplicationController 2 | def show 3 | render html: "", layout: "application" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/app_templates/testapp/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= javascript_include_tag 'application' %> 5 | <%= csrf_meta_tag %> 6 | 7 | 8 | 15 | 16 |
17 | <% flash.each do |key, value| -%> 18 |
<%=h value %>
19 | <% end %> 20 |
21 | 22 | <%= yield %> 23 | 24 | 25 | -------------------------------------------------------------------------------- /spec/app_templates/testapp/config/initializers/action_mailer.rb: -------------------------------------------------------------------------------- 1 | ActionMailer::Base.default_url_options[:host] = "localhost" 2 | -------------------------------------------------------------------------------- /spec/app_templates/testapp/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | root to: "home#show" 3 | end 4 | -------------------------------------------------------------------------------- /spec/clearance/back_door_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Clearance::BackDoor do 4 | it "signs in as a given user" do 5 | user_id = "123" 6 | user = double("user") 7 | allow(User).to receive(:find).with(user_id).and_return(user) 8 | env = env_for_user_id(user_id) 9 | back_door = Clearance::BackDoor.new(mock_app) 10 | 11 | result = back_door.call(env) 12 | 13 | expect(env[:clearance]).to have_received(:sign_in).with(user) 14 | expect(result).to eq mock_app.call(env) 15 | end 16 | 17 | it "delegates directly without a user" do 18 | env = env_without_user_id 19 | back_door = Clearance::BackDoor.new(mock_app) 20 | 21 | result = back_door.call(env) 22 | 23 | expect(env[:clearance]).not_to have_received(:sign_in) 24 | expect(result).to eq mock_app.call(env) 25 | end 26 | 27 | it "can set the user via a block" do 28 | env = env_for_username("foo") 29 | user = double("user") 30 | allow(User).to receive(:find_by).with(username: "foo").and_return(user) 31 | back_door = Clearance::BackDoor.new(mock_app) do |username| 32 | User.find_by(username: username) 33 | end 34 | 35 | result = back_door.call(env) 36 | 37 | expect(env[:clearance]).to have_received(:sign_in).with(user) 38 | expect(result).to eq mock_app.call(env) 39 | end 40 | 41 | it "can't be used outside the allowed environments" do 42 | with_environment("production") do 43 | expect { Clearance::BackDoor.new(mock_app) }. 44 | to raise_exception "Can't use auth backdoor outside of configured \ 45 | environments (test, ci, development).".squish 46 | end 47 | end 48 | 49 | it "strips 'as' from the params" do 50 | user_id = "123" 51 | user = double("user") 52 | allow(User).to receive(:find).with(user_id).and_return(user) 53 | env = build_env(as: user_id, foo: :bar) 54 | back_door = Clearance::BackDoor.new(mock_app) 55 | 56 | back_door.call(env) 57 | 58 | expect(env["QUERY_STRING"]).to eq("foo=bar") 59 | end 60 | 61 | context "when the environments are disabled" do 62 | before do 63 | Clearance.configuration.allowed_backdoor_environments = nil 64 | end 65 | 66 | it "raises an error for a default allowed env" do 67 | with_environment("test") do 68 | expect { Clearance::BackDoor.new(mock_app) }. 69 | to raise_exception "BackDoor auth is disabled." 70 | end 71 | end 72 | end 73 | 74 | context "when the environments are not defaults" do 75 | before do 76 | Clearance.configuration.allowed_backdoor_environments = ['demo'] 77 | end 78 | 79 | it "can be used with configured allowed environments" do 80 | with_environment("demo") do 81 | user_id = "123" 82 | user = double("user") 83 | allow(User).to receive(:find).with(user_id).and_return(user) 84 | env = env_for_user_id(user_id) 85 | back_door = Clearance::BackDoor.new(mock_app) 86 | 87 | result = back_door.call(env) 88 | 89 | expect(env[:clearance]).to have_received(:sign_in).with(user) 90 | expect(result).to eq mock_app.call(env) 91 | end 92 | end 93 | end 94 | 95 | def env_without_user_id 96 | env_for_user_id("") 97 | end 98 | 99 | def build_env(params) 100 | query = Rack::Utils.build_query(params) 101 | clearance = double("clearance", sign_in: true) 102 | Rack::MockRequest.env_for("/?#{query}").merge(clearance: clearance) 103 | end 104 | 105 | def env_for_user_id(user_id) 106 | build_env(as: user_id) 107 | end 108 | 109 | def env_for_username(username) 110 | build_env(as: username) 111 | end 112 | 113 | def mock_app 114 | lambda { |env| [200, {}, ["okay"]] } 115 | end 116 | 117 | def with_environment(environment) 118 | original_env = Rails.env 119 | Rails.env = environment 120 | 121 | yield 122 | ensure 123 | Rails.env = original_env 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /spec/clearance/constraints/signed_in_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Clearance::Constraints::SignedIn do 4 | it 'returns true when user is signed in' do 5 | user = create(:user) 6 | constraint = Clearance::Constraints::SignedIn.new 7 | request = request_with_remember_token(user.remember_token) 8 | expect(constraint.matches?(request)).to eq true 9 | end 10 | 11 | it 'returns false when user is not signed in' do 12 | constraint = Clearance::Constraints::SignedIn.new 13 | request = request_without_remember_token 14 | expect(constraint.matches?(request)).to eq false 15 | end 16 | 17 | it 'returns false when clearance session data is not present' do 18 | constraint = Clearance::Constraints::SignedIn.new 19 | request = Rack::Request.new({}) 20 | expect(constraint.matches?(request)).to eq false 21 | end 22 | 23 | it 'yields a signed-in user to a provided block' do 24 | user = create(:user, email: 'before@example.com') 25 | 26 | constraint = Clearance::Constraints::SignedIn.new do |signed_in_user| 27 | signed_in_user.update_attribute :email, 'after@example.com' 28 | end 29 | 30 | constraint.matches?(request_with_remember_token(user.remember_token)) 31 | expect(user.reload.email).to eq 'after@example.com' 32 | end 33 | 34 | it 'does not yield a user if they are not signed in' do 35 | user = create(:user, email: 'before@example.com') 36 | 37 | constraint = Clearance::Constraints::SignedIn.new do |signed_in_user| 38 | signed_in_user.update_attribute :email, 'after@example.com' 39 | end 40 | 41 | constraint.matches?(request_without_remember_token) 42 | expect(user.reload.email).to eq 'before@example.com' 43 | end 44 | 45 | it 'matches if the user-provided block returns true' do 46 | user = create(:user) 47 | constraint = Clearance::Constraints::SignedIn.new { true } 48 | request = request_with_remember_token(user.remember_token) 49 | expect(constraint.matches?(request)).to eq true 50 | end 51 | 52 | it 'does not match if the user-provided block returns false' do 53 | user = create(:user) 54 | constraint = Clearance::Constraints::SignedIn.new { false } 55 | request = request_with_remember_token(user.remember_token) 56 | expect(constraint.matches?(request)).to eq false 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/clearance/constraints/signed_out_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Clearance::Constraints::SignedOut do 4 | it 'returns true when user is signed out' do 5 | constraint = Clearance::Constraints::SignedOut.new 6 | request = request_without_remember_token 7 | expect(constraint.matches?(request)).to eq true 8 | end 9 | 10 | it 'returns false when user is not signed out' do 11 | user = create(:user) 12 | constraint = Clearance::Constraints::SignedOut.new 13 | request = request_with_remember_token(user.remember_token) 14 | expect(constraint.matches?(request)).to eq false 15 | end 16 | 17 | it 'returns true when clearance info is missing from request' do 18 | constraint = Clearance::Constraints::SignedOut.new 19 | request = Rack::Request.new({}) 20 | expect(constraint.matches?(request)).to eq true 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/clearance/controller_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Clearance::Controller, type: :controller do 4 | controller(ActionController::Base) do 5 | include Clearance::Controller 6 | end 7 | 8 | it "exposes no action methods" do 9 | expect(controller.action_methods).to be_empty 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/clearance/default_sign_in_guard_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Clearance::DefaultSignInGuard do 4 | context 'session is signed in' do 5 | it 'returns success' do 6 | session = double("Session", signed_in?: true) 7 | guard = Clearance::DefaultSignInGuard.new(session) 8 | 9 | expect(guard.call).to be_a Clearance::SuccessStatus 10 | end 11 | end 12 | 13 | context 'session is not signed in' do 14 | it 'returns failure' do 15 | session = double("Session", signed_in?: false) 16 | guard = Clearance::DefaultSignInGuard.new(session) 17 | 18 | response = guard.call 19 | 20 | expect(response).to be_a Clearance::FailureStatus 21 | expect(response.failure_message).to eq default_failure_message 22 | end 23 | end 24 | 25 | def default_failure_message 26 | I18n.t('flashes.failure_after_create').html_safe 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/clearance/rack_session_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Clearance::RackSession do 4 | it 'injects a clearance session into the environment' do 5 | headers = { 'X-Roaring-Lobster' => 'Red' } 6 | app = Rack::Builder.new do 7 | use Clearance::RackSession 8 | run lambda { |env| Rack::Response.new(env[:clearance], 200, headers).finish } 9 | end 10 | 11 | env = Rack::MockRequest.env_for('/') 12 | expected_session = "the session" 13 | allow(expected_session).to receive(:add_cookie_to_headers) 14 | allow(expected_session).to receive(:authentication_successful?). 15 | and_return(true) 16 | allow(Clearance::Session).to receive(:new). 17 | with(env). 18 | and_return(expected_session) 19 | 20 | response = Rack::MockResponse.new(*app.call(env)) 21 | 22 | expect(response.body).to eq expected_session 23 | expect(expected_session).to have_received(:add_cookie_to_headers) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/clearance/sign_in_guard_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Clearance 4 | describe SignInGuard do 5 | it 'handles success' do 6 | sign_in_guard = SignInGuard.new(double("session")) 7 | status = double("status") 8 | allow(SuccessStatus).to receive(:new).and_return(status) 9 | 10 | expect(sign_in_guard.success).to eq(status) 11 | end 12 | 13 | it 'handles failure' do 14 | sign_in_guard = SignInGuard.new(double("session")) 15 | status = double("status") 16 | failure_message = "Failed" 17 | allow(FailureStatus).to receive(:new). 18 | with(failure_message). 19 | and_return(status) 20 | 21 | expect(sign_in_guard.failure(failure_message)).to eq(status) 22 | end 23 | 24 | it 'can proceed to the next guard' do 25 | guards = double("guards", call: true) 26 | sign_in_guard = SignInGuard.new(double("session"), guards) 27 | sign_in_guard.next_guard 28 | expect(guards).to have_received(:call) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/clearance/testing/controller_helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Clearance::Testing::ControllerHelpers do 4 | class TestClass 5 | include Clearance::Testing::ControllerHelpers 6 | 7 | def initialize 8 | @request = Class.new do 9 | def env 10 | { clearance: Clearance::Session.new({}) } 11 | end 12 | end.new 13 | end 14 | end 15 | 16 | describe "#sign_in" do 17 | it "creates an instance of the clearance user model with FactoryBot" do 18 | MyUserModel = Class.new 19 | allow(FactoryBot).to receive(:create) 20 | allow(Clearance.configuration).to receive(:user_model). 21 | and_return(MyUserModel) 22 | 23 | TestClass.new.sign_in 24 | 25 | expect(FactoryBot).to have_received(:create).with(:my_user_model) 26 | end 27 | end 28 | 29 | describe "#sign_in_as" do 30 | it "returns the user if signed in successfully" do 31 | user = build(:user) 32 | 33 | returned_user = TestClass.new.sign_in_as user 34 | 35 | expect(returned_user).to eq user 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/clearance/testing/deny_access_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | class PretendFriendsController < ActionController::Base 4 | include Clearance::Controller 5 | before_action :require_login 6 | 7 | def index 8 | end 9 | end 10 | 11 | describe PretendFriendsController, type: :controller do 12 | before do 13 | Rails.application.routes.draw do 14 | resources :pretend_friends, only: :index 15 | get "/sign_in" => "clearance/sessions#new", as: "sign_in" 16 | end 17 | end 18 | 19 | after do 20 | Rails.application.reload_routes! 21 | end 22 | 23 | it "checks contents of deny access flash" do 24 | get :index 25 | 26 | expect(subject).to deny_access(flash: failure_message) 27 | end 28 | 29 | def failure_message 30 | I18n.t("flashes.failure_when_not_signed_in") 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/clearance/testing/view_helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Clearance::Testing::ViewHelpers do 4 | describe "#sign_in" do 5 | it "sets the signed in user to a new user object" do 6 | user_model = Class.new 7 | allow(Clearance.configuration).to receive(:user_model). 8 | and_return(user_model) 9 | 10 | view = test_view_class.new 11 | view.sign_in 12 | 13 | expect(view.current_user).to be_an_instance_of(user_model) 14 | end 15 | end 16 | 17 | describe "#sign_in_as" do 18 | it "sets the signed in user to the object provided" do 19 | user = double("User") 20 | 21 | view = test_view_class.new 22 | view.sign_in_as(user) 23 | 24 | expect(view.current_user).to eq user 25 | end 26 | end 27 | 28 | def test_view_class 29 | Class.new do 30 | include Clearance::Testing::ViewHelpers 31 | 32 | def view 33 | @view ||= extend Clearance::Testing::ViewHelpers::CurrentUser 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/clearance/token_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Clearance::Token do 4 | it 'is a random hex string' do 5 | token = 'my_token' 6 | allow(SecureRandom).to receive(:hex).with(20).and_return(token) 7 | 8 | expect(Clearance::Token.new).to eq token 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Clearance::Configuration do 4 | let(:config) { Clearance.configuration } 5 | 6 | context "when no user_model_name is specified" do 7 | it "defaults to User" do 8 | expect(Clearance.configuration.user_model).to eq ::User 9 | end 10 | end 11 | 12 | context "when a custom user_model_name is specified" do 13 | before(:each) do 14 | MyUser = Class.new 15 | end 16 | 17 | after(:each) do 18 | Object.send(:remove_const, :MyUser) 19 | end 20 | 21 | it "is used instead of User" do 22 | Clearance.configure { |config| config.user_model = MyUser } 23 | 24 | expect(Clearance.configuration.user_model).to eq ::MyUser 25 | end 26 | 27 | it "can be specified as a string to avoid triggering autoloading" do 28 | Clearance.configure { |config| config.user_model = "MyUser" } 29 | 30 | expect(Clearance.configuration.user_model).to eq ::MyUser 31 | end 32 | end 33 | 34 | context "when no parent_controller is specified" do 35 | it "defaults to ApplicationController" do 36 | expect(config.parent_controller).to eq ::ApplicationController 37 | end 38 | end 39 | 40 | context "when a custom parent_controller is specified" do 41 | before(:each) do 42 | MyController = Class.new 43 | end 44 | 45 | after(:each) do 46 | Object.send(:remove_const, :MyController) 47 | end 48 | 49 | it "is used instead of ApplicationController" do 50 | Clearance.configure { |config| config.parent_controller = MyController } 51 | 52 | expect(config.parent_controller).to eq ::MyController 53 | end 54 | end 55 | 56 | context "when secure_cookie is set to true" do 57 | it "returns true" do 58 | Clearance.configure { |config| config.secure_cookie = true } 59 | expect(Clearance.configuration.secure_cookie).to eq true 60 | end 61 | end 62 | 63 | context "when secure_cookie is not specified" do 64 | it "defaults to false" do 65 | expect(Clearance.configuration.secure_cookie).to eq false 66 | end 67 | end 68 | 69 | context "when signed_cookie is set to true" do 70 | it "returns true" do 71 | Clearance.configure { |config| config.signed_cookie = true } 72 | expect(Clearance.configuration.signed_cookie).to eq true 73 | end 74 | end 75 | 76 | context "when signed_cookie is not specified" do 77 | it "defaults to false" do 78 | expect(Clearance.configuration.signed_cookie).to eq false 79 | end 80 | end 81 | 82 | context "when signed_cookie is set to :migrate" do 83 | it "returns :migrate" do 84 | Clearance.configure { |config| config.signed_cookie = :migrate } 85 | expect(Clearance.configuration.signed_cookie).to eq :migrate 86 | end 87 | end 88 | 89 | context "when signed_cookie is set to an unexpected value" do 90 | it "returns :migrate" do 91 | expect { 92 | Clearance.configure { |config| config.signed_cookie = "unknown" } 93 | }.to raise_exception(RuntimeError) 94 | end 95 | end 96 | 97 | context "when no redirect URL specified" do 98 | it 'returns "/" as redirect URL' do 99 | expect(Clearance::Configuration.new.redirect_url).to eq "/" 100 | end 101 | end 102 | 103 | context "when redirect URL is specified" do 104 | it "returns new redirect URL" do 105 | new_redirect_url = "/admin" 106 | Clearance.configure { |config| config.redirect_url = new_redirect_url } 107 | 108 | expect(Clearance.configuration.redirect_url).to eq new_redirect_url 109 | end 110 | end 111 | 112 | context "when no url_after_destroy value specified" do 113 | it "returns nil as the default" do 114 | expect(Clearance::Configuration.new.url_after_destroy).to be_nil 115 | end 116 | end 117 | 118 | context "when url_after_destroy value is specified" do 119 | it "returns the url_after_destroy value" do 120 | Clearance.configure { |config| config.url_after_destroy = "/redirect" } 121 | 122 | expect(Clearance.configuration.url_after_destroy).to eq "/redirect" 123 | end 124 | end 125 | 126 | context "when no url_after_denied_access_when_signed_out value specified" do 127 | it "returns nil as the default" do 128 | expect(Clearance::Configuration.new.url_after_denied_access_when_signed_out).to be_nil 129 | end 130 | end 131 | 132 | context "when url_after_denied_access_when_signed_out value is specified" do 133 | it "returns the url_after_denied_access_when_signed_out value" do 134 | Clearance.configure { |config| config.url_after_denied_access_when_signed_out = "/redirect" } 135 | 136 | expect(Clearance.configuration.url_after_denied_access_when_signed_out).to eq "/redirect" 137 | end 138 | end 139 | 140 | context "when specifying sign in guards" do 141 | it "returns the stack with added guards" do 142 | DummyGuard = Class.new 143 | Clearance.configure { |config| config.sign_in_guards = [DummyGuard] } 144 | 145 | expect(Clearance.configuration.sign_in_guards).to eq [DummyGuard] 146 | end 147 | end 148 | 149 | context "when cookie domain is specified" do 150 | it "returns configured value" do 151 | domain = ".example.com" 152 | Clearance.configure { |config| config.cookie_domain = domain } 153 | 154 | expect(Clearance.configuration.cookie_domain).to eq domain 155 | end 156 | end 157 | 158 | context "when cookie path is specified" do 159 | it "returns configured value" do 160 | path = "/user" 161 | Clearance.configure { |config| config.cookie_path = path } 162 | 163 | expect(Clearance.configuration.cookie_path).to eq path 164 | end 165 | end 166 | 167 | describe "#allow_sign_up?" do 168 | context "when allow_sign_up is configured to false" do 169 | it "returns false" do 170 | Clearance.configure { |config| config.allow_sign_up = false } 171 | expect(Clearance.configuration.allow_sign_up?).to eq false 172 | end 173 | end 174 | 175 | context "when allow_sign_up has not been configured" do 176 | it "returns true" do 177 | expect(Clearance.configuration.allow_sign_up?).to eq true 178 | end 179 | end 180 | end 181 | 182 | describe "#allow_password_reset?" do 183 | context "when allow_password_reset is configured to false" do 184 | it "returns false" do 185 | Clearance.configure { |config| config.allow_password_reset = false } 186 | expect(Clearance.configuration.allow_password_reset?).to eq false 187 | end 188 | end 189 | 190 | context "when allow_sign_up has not been configured" do 191 | it "returns true" do 192 | expect(Clearance.configuration.allow_password_reset?).to eq true 193 | end 194 | end 195 | end 196 | 197 | describe "#user_actions" do 198 | context "when allow_sign_up is configured to false" do 199 | it "returns empty array" do 200 | Clearance.configure { |config| config.allow_sign_up = false } 201 | expect(Clearance.configuration.user_actions).to eq [] 202 | end 203 | end 204 | 205 | context "when sign_up has not been configured" do 206 | it "returns create" do 207 | expect(Clearance.configuration.user_actions).to eq [:create] 208 | end 209 | end 210 | end 211 | 212 | describe "#user_parameter" do 213 | context "when user_parameter is configured" do 214 | it "returns the configured parameter" do 215 | Clearance.configure { |config| config.user_parameter = :custom_param } 216 | expect(Clearance.configuration.user_parameter).to eq :custom_param 217 | end 218 | end 219 | 220 | it "returns the parameter key to use based on the user_model by default" do 221 | Account = Class.new(ActiveRecord::Base) 222 | Clearance.configure { |config| config.user_model = Account } 223 | 224 | expect(Clearance.configuration.user_parameter).to eq :account 225 | end 226 | end 227 | 228 | describe "#user_id_parameter" do 229 | it "returns the parameter key to use based on the user_model" do 230 | CustomUser = Class.new(ActiveRecord::Base) 231 | Clearance.configure { |config| config.user_model = CustomUser } 232 | 233 | expect(Clearance.configuration.user_id_parameter).to eq :custom_user_id 234 | end 235 | end 236 | 237 | describe "#routes_enabled?" do 238 | it "is true by default" do 239 | expect(Clearance.configuration.routes_enabled?).to be true 240 | end 241 | 242 | it "is false when routes are set to false" do 243 | Clearance.configure { |config| config.routes = false } 244 | expect(Clearance.configuration.routes_enabled?).to be false 245 | end 246 | end 247 | 248 | describe "#reload_user_model" do 249 | it "returns the user model class if one has already been configured" do 250 | ConfiguredUser = Class.new 251 | Clearance.configure { |config| config.user_model = ConfiguredUser } 252 | 253 | expect(Clearance.configuration.reload_user_model).to eq ConfiguredUser 254 | end 255 | 256 | it "returns nil if the user_model has not been configured" do 257 | Clearance.configuration = Clearance::Configuration.new 258 | 259 | expect(Clearance.configuration.reload_user_model).to be_nil 260 | end 261 | end 262 | 263 | describe "#rotate_csrf_on_sign_in?" do 264 | it "is true when `rotate_csrf_on_sign_in` is set to true" do 265 | Clearance.configure { |config| config.rotate_csrf_on_sign_in = true } 266 | 267 | expect(Clearance.configuration.rotate_csrf_on_sign_in?).to be true 268 | end 269 | 270 | it "is false when `rotate_csrf_on_sign_in` is set to false" do 271 | Clearance.configure { |config| config.rotate_csrf_on_sign_in = false } 272 | 273 | expect(Clearance.configuration.rotate_csrf_on_sign_in?).to be false 274 | end 275 | 276 | it "is false when `rotate_csrf_on_sign_in` is set to nil" do 277 | Clearance.configure { |config| config.rotate_csrf_on_sign_in = nil } 278 | 279 | expect(Clearance.configuration.rotate_csrf_on_sign_in?).to be false 280 | end 281 | end 282 | end 283 | -------------------------------------------------------------------------------- /spec/controllers/apis_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class ApisController < ActionController::Base 4 | include Clearance::Controller 5 | 6 | before_action :require_login 7 | 8 | def show 9 | head :ok 10 | end 11 | end 12 | 13 | describe ApisController do 14 | before do 15 | Rails.application.routes.draw do 16 | resource :api, only: [:show] 17 | end 18 | end 19 | 20 | after do 21 | Rails.application.reload_routes! 22 | end 23 | 24 | it 'responds with HTTP status code 401 when denied' do 25 | get :show, format: :js 26 | expect(subject).to respond_with(:unauthorized) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/controllers/forgeries_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class ForgeriesController < ActionController::Base 4 | include Clearance::Controller 5 | 6 | protect_from_forgery 7 | 8 | before_action :require_login 9 | 10 | # This is off in test by default, but we need it for this test 11 | self.allow_forgery_protection = true 12 | 13 | def create 14 | redirect_to action: 'index' 15 | end 16 | end 17 | 18 | describe ForgeriesController do 19 | context 'signed in user' do 20 | before do 21 | Rails.application.routes.draw do 22 | resources :forgeries 23 | get '/sign_in' => 'clearance/sessions#new', as: 'sign_in' 24 | end 25 | 26 | @user = create(:user) 27 | @user.update_attribute(:remember_token, 'old-token') 28 | @request.cookies['remember_token'] = 'old-token' 29 | end 30 | 31 | after do 32 | Rails.application.reload_routes! 33 | end 34 | 35 | it 'succeeds with authentic token' do 36 | token = controller.send(:form_authenticity_token) 37 | post :create, params: { 38 | authenticity_token: token, 39 | } 40 | expect(subject).to redirect_to(action: 'index') 41 | end 42 | 43 | it 'fails with invalid token' do 44 | post :create, params: { 45 | authenticity_token: "hax0r", 46 | } 47 | expect(subject).to deny_access 48 | end 49 | 50 | it 'fails with no token' do 51 | post :create 52 | expect(subject).to deny_access 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/controllers/passwords_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Clearance::PasswordsController do 4 | it { is_expected.to be_a Clearance::BaseController } 5 | 6 | describe "#new" do 7 | it "renders the password reset form" do 8 | get :new 9 | 10 | expect(response).to be_successful 11 | expect(response).to render_template(:new) 12 | end 13 | end 14 | 15 | describe "#create" do 16 | context "email corresponds to an existing user" do 17 | it "generates a password change token" do 18 | user = create(:user) 19 | 20 | post :create, params: { 21 | password: { email: user.email.upcase }, 22 | } 23 | 24 | expect(user.reload.confirmation_token).not_to be_nil 25 | end 26 | 27 | it "sends the password reset email" do 28 | ActionMailer::Base.deliveries.clear 29 | user = create(:user) 30 | 31 | post :create, params: { 32 | password: { email: user.email }, 33 | } 34 | 35 | email = ActionMailer::Base.deliveries.last 36 | expect(email.subject).to match(I18n.t("passwords.edit.title")) 37 | end 38 | 39 | it "re-renders the page when turbo is enabled" do 40 | user = create(:user) 41 | 42 | post :create, params: { 43 | password: { email: user.email.upcase }, 44 | } 45 | 46 | expect(response).to have_http_status(:accepted) 47 | end 48 | end 49 | 50 | context "email param is missing" do 51 | it "displays flash error on new page" do 52 | post :create, params: { 53 | password: {}, 54 | } 55 | 56 | expect(flash.now[:alert]).to match(I18n.t("flashes.failure_when_missing_email")) 57 | expect(response).to render_template(:new) 58 | end 59 | 60 | it "re-renders the page when turbo is enabled" do 61 | post :create, params: { 62 | password: {}, 63 | } 64 | 65 | expect(response).to have_http_status(:unprocessable_entity) 66 | end 67 | end 68 | 69 | context "email param is blank" do 70 | it "displays flash error on new page" do 71 | post :create, params: { 72 | password: { 73 | email: "", 74 | }, 75 | } 76 | 77 | expect(flash.now[:alert]).to match(I18n.t("flashes.failure_when_missing_email")) 78 | expect(response).to render_template(:new) 79 | end 80 | 81 | it "re-renders the page when turbo is enabled" do 82 | post :create, params: { 83 | password: { 84 | email: "", 85 | }, 86 | } 87 | 88 | expect(response).to have_http_status(:unprocessable_entity) 89 | end 90 | end 91 | 92 | context "email does not belong to an existing user" do 93 | it "does not deliver an email" do 94 | ActionMailer::Base.deliveries.clear 95 | email = "this_user_does_not_exist@non_existent_domain.com" 96 | 97 | post :create, params: { 98 | password: { email: email }, 99 | } 100 | 101 | expect(ActionMailer::Base.deliveries).to be_empty 102 | end 103 | 104 | it "still responds with error so as not to leak registered users" do 105 | email = "this_user_does_not_exist@non_existent_domain.com" 106 | 107 | post :create, params: { 108 | password: { email: email }, 109 | } 110 | 111 | expect(response).to be_successful 112 | expect(response).to render_template "passwords/create" 113 | end 114 | 115 | it "has the same status code as a successful request" do 116 | email = "this_user_does_not_exist@non_existent_domain.com" 117 | 118 | post :create, params: { 119 | password: { email: email }, 120 | } 121 | 122 | expect(response).to have_http_status(:accepted) 123 | end 124 | end 125 | end 126 | 127 | describe "#edit" do 128 | context "valid id and token are supplied in url" do 129 | it "redirects to the edit page with token now removed from url" do 130 | user = create(:user, :with_forgotten_password) 131 | 132 | get :edit, params: { 133 | user_id: user, 134 | token: user.confirmation_token, 135 | } 136 | 137 | expect(response).to be_redirect 138 | expect(response).to have_http_status(:found) 139 | expect(response).to redirect_to edit_user_password_url(user) 140 | expect(session[:password_reset_token]).to eq user.confirmation_token 141 | end 142 | end 143 | 144 | context "valid id in url and valid token in session" do 145 | it "renders the password reset form" do 146 | user = create(:user, :with_forgotten_password) 147 | 148 | request.session[:password_reset_token] = user.confirmation_token 149 | get :edit, params: { 150 | user_id: user, 151 | } 152 | 153 | expect(response).to be_successful 154 | expect(response).to render_template(:edit) 155 | expect(assigns(:user)).to eq user 156 | end 157 | end 158 | 159 | context "blank token is supplied" do 160 | it "renders the new password reset form with a flash alert" do 161 | get :edit, params: { 162 | user_id: 1, 163 | token: "", 164 | } 165 | 166 | expect(response).to render_template(:new) 167 | expect(flash.now[:alert]).to match(I18n.t("flashes.failure_when_forbidden")) 168 | end 169 | end 170 | 171 | context "invalid token is supplied" do 172 | it "renders the new password reset form with a flash alert" do 173 | user = create(:user, :with_forgotten_password) 174 | 175 | get :edit, params: { 176 | user_id: 1, 177 | token: user.confirmation_token + "a", 178 | } 179 | 180 | expect(response).to render_template(:new) 181 | expect(flash.now[:alert]).to match(I18n.t("flashes.failure_when_forbidden")) 182 | end 183 | end 184 | 185 | context "old token in session and recent token in params" do 186 | it "updates password reset session and redirect to edit page" do 187 | user = create(:user, :with_forgotten_password) 188 | request.session[:password_reset_token] = user.confirmation_token 189 | 190 | user.forgot_password! 191 | get :edit, params: { 192 | user_id: user.id, 193 | token: user.reload.confirmation_token, 194 | } 195 | 196 | expect(response).to redirect_to(edit_user_password_url(user)) 197 | expect(session[:password_reset_token]).to eq(user.confirmation_token) 198 | end 199 | end 200 | end 201 | 202 | describe "#update" do 203 | context "valid id, token, and new password provided" do 204 | it "updates the user's password" do 205 | user = create(:user, :with_forgotten_password) 206 | old_encrypted_password = user.encrypted_password 207 | 208 | put :update, params: update_parameters( 209 | user, 210 | new_password: "my_new_password", 211 | ) 212 | 213 | expect(user.reload.encrypted_password).not_to eq old_encrypted_password 214 | expect(response).to have_http_status(:see_other) 215 | end 216 | 217 | it "signs in the user" do 218 | user = create(:user, :with_forgotten_password) 219 | 220 | put :update, params: update_parameters( 221 | user, 222 | new_password: "my_new_password", 223 | ) 224 | 225 | expect(current_user).to eq(user) 226 | end 227 | 228 | context "when Clearance is configured to not sign in the user" do 229 | it "doesn't sign in the user" do 230 | Clearance.configure do |config| 231 | config.sign_in_on_password_reset = false 232 | end 233 | 234 | user = create(:user, :with_forgotten_password) 235 | 236 | put :update, params: update_parameters( 237 | user, 238 | new_password: "my_new_password", 239 | ) 240 | 241 | expect(current_user).to be_nil 242 | end 243 | end 244 | end 245 | 246 | context "password update fails" do 247 | it "does not update the password" do 248 | user = create(:user, :with_forgotten_password) 249 | old_encrypted_password = user.encrypted_password 250 | 251 | put :update, params: update_parameters( 252 | user, 253 | new_password: "", 254 | ) 255 | 256 | user.reload 257 | expect(user.encrypted_password).to eq old_encrypted_password 258 | expect(user.confirmation_token).to be_present 259 | end 260 | 261 | it "does not raise NoMethodError from incomplete password_reset params" do 262 | user = create(:user, :with_forgotten_password) 263 | 264 | expect do 265 | put :update, params: { 266 | user_id: user, 267 | token: user.confirmation_token, 268 | password_reset: {}, 269 | } 270 | end.not_to raise_error 271 | end 272 | 273 | it "re-renders the password edit form" do 274 | user = create(:user, :with_forgotten_password) 275 | 276 | put :update, params: update_parameters( 277 | user, 278 | new_password: "", 279 | ) 280 | 281 | expect(flash.now[:alert]).to match(I18n.t("flashes.failure_after_update")) 282 | expect(response).to have_http_status(:unprocessable_entity) 283 | expect(response).to render_template(:edit) 284 | end 285 | 286 | it "doesn't sign in the user" do 287 | user = create(:user, :with_forgotten_password) 288 | 289 | put :update, params: update_parameters( 290 | user, 291 | new_password: "", 292 | ) 293 | 294 | expect(current_user).to be_nil 295 | end 296 | end 297 | end 298 | 299 | def update_parameters(user, options = {}) 300 | new_password = options.fetch(:new_password) 301 | 302 | { 303 | user_id: user, 304 | token: user.confirmation_token, 305 | password_reset: { password: new_password } 306 | } 307 | end 308 | 309 | def current_user 310 | request.env[:clearance].current_user 311 | end 312 | end 313 | -------------------------------------------------------------------------------- /spec/controllers/permissions_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class PermissionsController < ActionController::Base 4 | include Clearance::Controller 5 | 6 | before_action :require_login, only: :show 7 | 8 | def new 9 | head :ok 10 | end 11 | 12 | def show 13 | head :ok 14 | end 15 | end 16 | 17 | describe PermissionsController do 18 | before do 19 | Rails.application.routes.draw do 20 | resource :permission, only: [:new, :show] 21 | get '/sign_in' => 'clearance/sessions#new', as: 'sign_in' 22 | end 23 | end 24 | 25 | after do 26 | Rails.application.reload_routes! 27 | end 28 | 29 | context 'with signed in user' do 30 | before { sign_in } 31 | 32 | it 'allows access to new' do 33 | get :new 34 | 35 | expect(subject).not_to deny_access 36 | end 37 | 38 | it 'allows access to show' do 39 | get :show 40 | 41 | expect(subject).not_to deny_access 42 | end 43 | end 44 | 45 | context 'with visitor' do 46 | it 'allows access to new' do 47 | get :new 48 | 49 | expect(subject).not_to deny_access 50 | end 51 | 52 | it 'denies access to show' do 53 | get :show 54 | 55 | expect(subject).to deny_access(redirect: sign_in_url) 56 | end 57 | 58 | it "denies access to show and display a flash message" do 59 | get :show 60 | 61 | expect(flash[:alert]).to match(I18n.t("flashes.failure_when_not_signed_in")) 62 | end 63 | end 64 | 65 | context 'when remember_token is blank' do 66 | it 'denies acess to show' do 67 | user = create(:user) 68 | user.update(remember_token: '') 69 | cookies[:remember_token] = '' 70 | 71 | get :show 72 | 73 | expect(subject).to deny_access 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /spec/controllers/sessions_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Clearance::SessionsController do 4 | it { should be_a Clearance::BaseController } 5 | 6 | describe "on GET to #new" do 7 | context "when a user is not signed in" do 8 | before { get :new } 9 | 10 | it { should respond_with(:success) } 11 | it { should render_template(:new) } 12 | it { should_not set_flash } 13 | end 14 | 15 | context "when a user is signed in" do 16 | before do 17 | sign_in 18 | get :new 19 | end 20 | 21 | it { should redirect_to(Clearance.configuration.redirect_url) } 22 | it { should_not set_flash } 23 | end 24 | end 25 | 26 | describe "on POST to #create" do 27 | context "when missing parameters" do 28 | it "raises an error" do 29 | expect do 30 | post :create 31 | end.to raise_error(ActionController::ParameterMissing) 32 | end 33 | end 34 | 35 | context "when password is optional" do 36 | it "renders the page with error" do 37 | user = create(:user_with_optional_password) 38 | 39 | post :create, params: { 40 | session: { email: user.email, password: user.password }, 41 | } 42 | 43 | expect(response).to render_template(:new) 44 | expect(flash[:alert]).to match(I18n.t("flashes.failure_after_create")) 45 | end 46 | end 47 | 48 | context "with good credentials" do 49 | before do 50 | @user = create(:user) 51 | @user.update_attribute :remember_token, "old-token" 52 | post :create, params: { 53 | session: { email: @user.email, password: @user.password }, 54 | } 55 | end 56 | 57 | it { should redirect_to_url_after_create } 58 | 59 | it "sets the user in the clearance session" do 60 | expect(request.env[:clearance].current_user).to eq @user 61 | end 62 | 63 | it "should not change the remember token" do 64 | expect(@user.reload.remember_token).to eq "old-token" 65 | end 66 | end 67 | 68 | context "with good credentials and a session return url" do 69 | it "redirects to the return URL removing leading slashes" do 70 | user = create(:user) 71 | url = "/url_in_the_session?foo=bar#baz" 72 | return_url = "//////#{url}" 73 | request.session[:return_to] = return_url 74 | 75 | post :create, params: { 76 | session: { email: user.email, password: user.password }, 77 | } 78 | 79 | should redirect_to(url) 80 | end 81 | 82 | it "redirects to the return URL maintaining query and fragment" do 83 | user = create(:user) 84 | return_url = "/url_in_the_session?foo=bar#baz" 85 | request.session[:return_to] = return_url 86 | 87 | post :create, params: { 88 | session: { email: user.email, password: user.password }, 89 | } 90 | 91 | should redirect_to(return_url) 92 | end 93 | 94 | it "redirects to the return URL maintaining query without fragment" do 95 | user = create(:user) 96 | return_url = "/url_in_the_session?foo=bar" 97 | request.session[:return_to] = return_url 98 | 99 | post :create, params: { 100 | session: { email: user.email, password: user.password }, 101 | } 102 | 103 | should redirect_to(return_url) 104 | end 105 | 106 | it "redirects to the return URL without query or fragment" do 107 | user = create(:user) 108 | return_url = "/url_in_the_session" 109 | request.session[:return_to] = return_url 110 | 111 | post :create, params: { 112 | session: { email: user.email, password: user.password }, 113 | } 114 | 115 | should redirect_to(return_url) 116 | end 117 | end 118 | end 119 | 120 | describe "on DELETE to #destroy" do 121 | let(:configured_redirect_url) { nil } 122 | 123 | before do 124 | Clearance.configure { |config| config.url_after_destroy = configured_redirect_url } 125 | end 126 | 127 | context "given a signed out user" do 128 | before do 129 | sign_out 130 | delete :destroy 131 | end 132 | 133 | it { should redirect_to_url_after_destroy } 134 | it { expect(response).to have_http_status(:see_other) } 135 | 136 | context "when the custom redirect URL is set" do 137 | let(:configured_redirect_url) { "/redirected" } 138 | 139 | it { should redirect_to(configured_redirect_url) } 140 | end 141 | end 142 | 143 | context "with a cookie" do 144 | before do 145 | @user = create(:user) 146 | @user.update_attribute :remember_token, "old-token" 147 | @request.cookies["remember_token"] = "old-token" 148 | delete :destroy 149 | end 150 | 151 | it { should redirect_to_url_after_destroy } 152 | 153 | it "should reset the remember token" do 154 | expect(@user.reload.remember_token).not_to eq "old-token" 155 | end 156 | 157 | it "should unset the current user" do 158 | expect(request.env[:clearance].current_user).to be_nil 159 | end 160 | 161 | context "when the custom redirect URL is set" do 162 | let(:configured_redirect_url) { "/redirected" } 163 | 164 | it { should redirect_to(configured_redirect_url) } 165 | end 166 | end 167 | end 168 | end 169 | -------------------------------------------------------------------------------- /spec/controllers/users_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Clearance::UsersController do 4 | it { should be_a Clearance::BaseController } 5 | 6 | describe "on GET to #new" do 7 | context "when signed out" do 8 | it "renders a form for a new user" do 9 | get :new 10 | 11 | expect(response).to be_successful 12 | expect(response).to render_template(:new) 13 | end 14 | 15 | it "defaults email field to the value provided in the query string" do 16 | get :new, params: { 17 | user: { email: "a@example.com" }, 18 | } 19 | 20 | expect(assigns(:user).email).to eq "a@example.com" 21 | expect(response).to be_successful 22 | expect(response).to render_template(:new) 23 | end 24 | end 25 | 26 | context "when signed in" do 27 | it "redirects to the configured clearance redirect url" do 28 | sign_in 29 | 30 | get :new 31 | 32 | expect(response).to redirect_to(Clearance.configuration.redirect_url) 33 | end 34 | end 35 | end 36 | 37 | describe "on POST to #create" do 38 | context "when signed out" do 39 | context "with valid attributes" do 40 | it "assigns and creates a user then redirects to the redirect_url" do 41 | user_attributes = FactoryBot.attributes_for(:user) 42 | old_user_count = User.count 43 | 44 | post :create, params: { 45 | user: user_attributes, 46 | } 47 | 48 | expect(assigns(:user)).to be_present 49 | expect(User.count).to eq old_user_count + 1 50 | expect(response).to redirect_to(Clearance.configuration.redirect_url) 51 | end 52 | end 53 | 54 | context "with valid attributes and a session return url" do 55 | it "assigns and creates a user then redirects to the return_url" do 56 | user_attributes = FactoryBot.attributes_for(:user) 57 | old_user_count = User.count 58 | return_url = "/url_in_the_session" 59 | @request.session[:return_to] = return_url 60 | 61 | post :create, params: { 62 | user: user_attributes, 63 | } 64 | 65 | expect(assigns(:user)).to be_present 66 | expect(User.count).to eq old_user_count + 1 67 | expect(response).to redirect_to(return_url) 68 | end 69 | end 70 | 71 | context "with invalid attributes" do 72 | it "renders the page with error" do 73 | user_attributes = FactoryBot.attributes_for(:user, email: nil) 74 | old_user_count = User.count 75 | 76 | post :create, params: { 77 | user: user_attributes, 78 | } 79 | 80 | expect(User.count).to eq old_user_count 81 | expect(response).to have_http_status(:unprocessable_entity) 82 | end 83 | end 84 | end 85 | 86 | context "when signed in" do 87 | it "redirects to the configured clearance redirect url" do 88 | sign_in 89 | 90 | post :create, params: { 91 | user: {}, 92 | } 93 | 94 | expect(response).to redirect_to(Clearance.configuration.redirect_url) 95 | end 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /spec/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative "config/application" 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/config/manifest.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/clearance/7ee5a24545497f4a70b3eeb10df2301eabdc91c2/spec/dummy/app/assets/config/manifest.js -------------------------------------------------------------------------------- /spec/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | include Clearance::Controller 3 | 4 | def show 5 | render inline: "Hello user #<%= current_user.id %> #{params.to_json}", layout: false 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy/app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | include Clearance::User 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/models/user_with_optional_password.rb: -------------------------------------------------------------------------------- 1 | class UserWithOptionalPassword < User 2 | private 3 | 4 | def password_optional? 5 | true 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative "config/environment" 4 | 5 | run Rails.application 6 | Rails.application.load_server 7 | -------------------------------------------------------------------------------- /spec/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative "boot" 2 | 3 | require "rails/all" 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module Dummy 10 | class Application < Rails::Application 11 | config.load_defaults Rails::VERSION::STRING.to_f 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", __dir__) 3 | 4 | require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"]) 5 | $LOAD_PATH.unshift File.expand_path("../../../lib", __dir__) 6 | -------------------------------------------------------------------------------- /spec/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: sqlite3 3 | database: db/development.sqlite3 4 | pool: 5 5 | timeout: 5000 6 | 7 | test: 8 | adapter: sqlite3 9 | database: db/test.sqlite3 10 | pool: 5 11 | timeout: 5000 12 | -------------------------------------------------------------------------------- /spec/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | config.enable_reloading = false 5 | 6 | config.eager_load = ENV["CI"].present? 7 | 8 | config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{1.hour.to_i}" } 9 | 10 | # Show full error reports and disable caching. 11 | config.consider_all_requests_local = true 12 | config.action_controller.perform_caching = false 13 | config.cache_store = :null_store 14 | 15 | config.action_dispatch.show_exceptions = :rescuable 16 | 17 | config.action_controller.allow_forgery_protection = false 18 | 19 | config.action_mailer.perform_caching = false 20 | config.action_mailer.delivery_method = :test 21 | 22 | config.action_mailer.default_url_options = { host: "www.example.com" } 23 | 24 | config.active_support.deprecation = :stderr 25 | config.active_support.disallowed_deprecation = :raise 26 | config.active_support.disallowed_deprecation_warnings = [] 27 | 28 | config.factory_bot.definition_file_paths = [File.expand_path('../../../factories', __dir__)] 29 | 30 | config.middleware.use Clearance::BackDoor 31 | end 32 | -------------------------------------------------------------------------------- /spec/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | root to: "application#show" 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/db/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/clearance/7ee5a24545497f4a70b3eeb10df2301eabdc91c2/spec/dummy/db/.keep -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20110111224543_create_clearance_users.rb: -------------------------------------------------------------------------------- 1 | class CreateClearanceUsers < ActiveRecord::Migration[Rails::VERSION::STRING.to_f] 2 | def self.up 3 | create_table :users do |t| 4 | t.timestamps null: false 5 | t.string :email, null: false 6 | t.string :encrypted_password, limit: 128, null: false 7 | t.string :confirmation_token, limit: 128 8 | t.string :remember_token, limit: 128, null: false 9 | end 10 | 11 | add_index :users, :email 12 | add_index :users, :confirmation_token, unique: true 13 | add_index :users, :remember_token, unique: true 14 | end 15 | 16 | def self.down 17 | drop_table :users 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/dummy/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 | # This file is the source Rails uses to define your schema when running `bin/rails 6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 2011_01_11_224543) do 14 | create_table "users", force: :cascade do |t| 15 | t.datetime "created_at", null: false 16 | t.datetime "updated_at", null: false 17 | t.string "email", null: false 18 | t.string "encrypted_password", limit: 128, null: false 19 | t.string "confirmation_token", limit: 128 20 | t.string "remember_token", limit: 128, null: false 21 | t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true 22 | t.index ["email"], name: "index_users_on_email" 23 | t.index ["remember_token"], name: "index_users_on_remember_token", unique: true 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/factories/users.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | sequence :email do |n| 3 | "user#{n}@example.com" 4 | end 5 | 6 | factory :user do 7 | email 8 | password { 'password' } 9 | 10 | trait :with_forgotten_password do 11 | confirmation_token { Clearance::Token.new } 12 | end 13 | 14 | factory :user_with_optional_password, class: 'UserWithOptionalPassword' do 15 | password { nil } 16 | encrypted_password { '' } 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/generators/clearance/install/install_generator_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "generators/clearance/install/install_generator" 3 | 4 | describe Clearance::Generators::InstallGenerator, :generator do 5 | describe "initializer" do 6 | it "is copied to the application" do 7 | provide_existing_application_controller 8 | 9 | run_generator 10 | initializer = file("config/initializers/clearance.rb") 11 | 12 | expect(initializer).to exist 13 | expect(initializer).to have_correct_syntax 14 | expect(initializer).to contain("Clearance.configure do |config|") 15 | end 16 | end 17 | 18 | describe "application controller" do 19 | it "includes Clearance::Controller" do 20 | provide_existing_application_controller 21 | 22 | run_generator 23 | application_controller = file("app/controllers/application_controller.rb") 24 | 25 | expect(application_controller).to have_correct_syntax 26 | expect(application_controller).to contain("include Clearance::Controller") 27 | end 28 | end 29 | 30 | describe "user_model" do 31 | context "no existing user class" do 32 | it "creates a user class including Clearance::User" do 33 | provide_existing_application_controller 34 | 35 | run_generator 36 | user_class = file("app/models/user.rb") 37 | 38 | expect(user_class).to exist 39 | expect(user_class).to have_correct_syntax 40 | expect(user_class).to contain_models_inherit_from 41 | expect(user_class).to contain("include Clearance::User") 42 | end 43 | end 44 | 45 | context "user class already exists" do 46 | it "includes Clearance::User" do 47 | provide_existing_application_controller 48 | provide_existing_user_class 49 | 50 | run_generator 51 | user_class = file("app/models/user.rb") 52 | 53 | expect(user_class).to exist 54 | expect(user_class).to have_correct_syntax 55 | expect(user_class).to contain_models_inherit_from 56 | expect(user_class).to contain("include Clearance::User") 57 | expect(user_class).to have_method("previously_existed?") 58 | end 59 | end 60 | end 61 | 62 | describe "user migration" do 63 | context "users table does not exist" do 64 | it "creates a migration to create the users table" do 65 | provide_existing_application_controller 66 | table_does_not_exist(:users) 67 | 68 | run_generator 69 | migration = migration_file("db/migrate/create_users.rb") 70 | 71 | expect(migration).to exist 72 | expect(migration).to have_correct_syntax 73 | expect(migration).to contain("create_table :users do") 74 | end 75 | 76 | context "active record configured for uuid" do 77 | around do |example| 78 | preserve_original_primary_key_type_setting do 79 | Rails.application.config.generators do |g| 80 | g.orm :active_record, primary_key_type: :uuid 81 | end 82 | example.run 83 | end 84 | end 85 | 86 | it "creates a migration to create the users table with key type set" do 87 | provide_existing_application_controller 88 | table_does_not_exist(:users) 89 | 90 | run_generator 91 | migration = migration_file("db/migrate/create_users.rb") 92 | 93 | expect(migration).to exist 94 | expect(migration).to have_correct_syntax 95 | expect(migration).to contain("create_table :users, id: :uuid do") 96 | end 97 | end 98 | end 99 | 100 | context "existing users table with all clearance columns and indexes" do 101 | it "does not create a migration" do 102 | provide_existing_application_controller 103 | 104 | run_generator 105 | create_migration = migration_file("db/migrate/create_users.rb") 106 | add_migration = migration_file("db/migrate/add_clearance_to_users.rb") 107 | 108 | expect(create_migration).not_to exist 109 | expect(add_migration).not_to exist 110 | end 111 | end 112 | 113 | context "existing users table missing some columns and indexes" do 114 | it "create a migration to add missing columns and indexes" do 115 | provide_existing_application_controller 116 | Struct.new("Named", :name) 117 | existing_columns = [Struct::Named.new("remember_token")] 118 | existing_indexes = [Struct::Named.new("index_users_on_remember_token")] 119 | 120 | allow(ActiveRecord::Base.connection).to receive(:columns). 121 | with(:users). 122 | and_return(existing_columns) 123 | 124 | allow(ActiveRecord::Base.connection).to receive(:indexes). 125 | with(:users). 126 | and_return(existing_indexes) 127 | 128 | run_generator 129 | migration = migration_file("db/migrate/add_clearance_to_users.rb") 130 | 131 | expect(migration).to exist 132 | expect(migration).to have_correct_syntax 133 | expect(migration).to contain("change_table :users") 134 | expect(migration).to contain("t.string :email") 135 | expect(migration).to contain("add_index :users, :email") 136 | expect(migration).not_to contain("t.string :remember_token") 137 | expect(migration).not_to contain("add_index :users, :remember_token") 138 | expect(migration).to( 139 | contain("add_index :users, :confirmation_token, unique: true"), 140 | ) 141 | expect(migration).to( 142 | contain("remove_index :users, :confirmation_token, unique: true"), 143 | ) 144 | end 145 | end 146 | end 147 | 148 | def table_does_not_exist(name) 149 | connection = ActiveRecord::Base.connection 150 | allow(connection).to receive(:data_source_exists?). 151 | with(name). 152 | and_return(false) 153 | end 154 | 155 | def preserve_original_primary_key_type_setting 156 | active_record = Rails.configuration.generators.active_record 157 | active_record ||= Rails.configuration.generators.options[:active_record] 158 | original = active_record[:primary_key_type] 159 | 160 | yield 161 | 162 | Rails.application.config.generators do |g| 163 | g.orm :active_record, primary_key_type: original 164 | end 165 | end 166 | 167 | def contain_models_inherit_from 168 | contain "< #{models_inherit_from}\n" 169 | end 170 | 171 | def models_inherit_from 172 | "ApplicationRecord" 173 | end 174 | end 175 | -------------------------------------------------------------------------------- /spec/generators/clearance/routes/routes_generator_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "generators/clearance/routes/routes_generator" 3 | 4 | describe Clearance::Generators::RoutesGenerator, :generator do 5 | it "adds clearance routes to host application routes" do 6 | provide_existing_routes_file 7 | provide_existing_initializer 8 | 9 | routes = file("config/routes.rb") 10 | initializer = file("config/initializers/clearance.rb") 11 | 12 | run_generator 13 | 14 | expect(initializer).to have_correct_syntax 15 | expect(initializer).to contain("config.routes = false") 16 | expect(routes).to have_correct_syntax 17 | expect(routes).to contain( 18 | 'get "/sign_in" => "clearance/sessions#new", as: "sign_in"' 19 | ) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/generators/clearance/specs/specs_generator_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "generators/clearance/specs/specs_generator" 3 | 4 | describe Clearance::Generators::SpecsGenerator, :generator do 5 | it "copies specs to host app" do 6 | run_generator 7 | 8 | specs = %w( 9 | factories/clearance 10 | features/clearance/user_signs_out_spec 11 | features/clearance/visitor_resets_password_spec 12 | features/clearance/visitor_signs_in_spec 13 | features/clearance/visitor_signs_up_spec 14 | features/clearance/visitor_updates_password_spec 15 | support/clearance 16 | support/features/clearance_helpers 17 | ) 18 | 19 | spec_files = specs.map { |spec| file("spec/#{spec}.rb") } 20 | 21 | spec_files.each do |spec_file| 22 | expect(spec_file).to exist 23 | expect(spec_file).to have_correct_syntax 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/generators/clearance/views/views_generator_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "generators/clearance/views/views_generator" 3 | 4 | describe Clearance::Generators::ViewsGenerator, :generator do 5 | it "copies clearance views to the host application" do 6 | run_generator 7 | 8 | views = %w( 9 | clearance_mailer/change_password.html.erb 10 | clearance_mailer/change_password.text.erb 11 | passwords/create.html.erb 12 | passwords/edit.html.erb 13 | passwords/new.html.erb 14 | sessions/_form.html.erb 15 | sessions/new.html.erb 16 | users/_form.html.erb 17 | users/new.html.erb 18 | ) 19 | 20 | view_files = views.map { |view| file("app/views/#{view}") } 21 | 22 | view_files.each do |each| 23 | expect(each).to exist 24 | end 25 | end 26 | 27 | it "copies clearance locales to the host application" do 28 | run_generator 29 | 30 | locale = file("config/locales/clearance.en.yml") 31 | 32 | expect(locale).to exist 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/helpers/helper_helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "Clearance RSpec helper spec configuration", type: :helper do 4 | it "lets me use clearance's helper methods in helper specs" do 5 | user = double("User") 6 | sign_in_as(user) 7 | 8 | expect(helper.current_user).to eq user 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/mailers/clearance_mailer_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe ClearanceMailer do 4 | it "is from DO_NOT_REPLY" do 5 | user = create(:user) 6 | user.forgot_password! 7 | 8 | email = ClearanceMailer.change_password(user) 9 | 10 | expect(Clearance.configuration.mailer_sender).to eq(email.from[0]) 11 | end 12 | 13 | it "is sent to user" do 14 | user = create(:user) 15 | user.forgot_password! 16 | 17 | email = ClearanceMailer.change_password(user) 18 | 19 | expect(email.to.first).to eq(user.email) 20 | end 21 | 22 | it "sets its subject" do 23 | user = create(:user) 24 | user.forgot_password! 25 | 26 | email = ClearanceMailer.change_password(user) 27 | 28 | expect(email.subject).to include("Change your password") 29 | end 30 | 31 | it "has html and plain text parts" do 32 | user = create(:user) 33 | user.forgot_password! 34 | 35 | email = ClearanceMailer.change_password(user) 36 | 37 | expect(email.body.parts.length).to eq 2 38 | expect(email.text_part).to be_present 39 | expect(email.html_part).to be_present 40 | end 41 | 42 | it "contains a link to edit the password" do 43 | user = create(:user) 44 | user.forgot_password! 45 | host = ActionMailer::Base.default_url_options[:host] 46 | link = "http://#{host}/users/#{user.id}/password/edit" \ 47 | "?token=#{user.confirmation_token}" 48 | 49 | email = ClearanceMailer.change_password(user) 50 | 51 | expect(email.text_part.body).to include(link) 52 | expect(email.html_part.body).to include(link) 53 | expect(email.html_part.body).to have_css( 54 | "a", 55 | text: I18n.t("clearance_mailer.change_password.link_text") 56 | ) 57 | end 58 | 59 | context "when using a custom model" do 60 | it "contains a link for a custom model" do 61 | define_people_routes 62 | Person = Class.new(User) 63 | person = Person.new(email: "person@example.com", password: "password") 64 | 65 | person.forgot_password! 66 | host = ActionMailer::Base.default_url_options[:host] 67 | link = "http://#{host}/people/#{person.id}/password/edit" \ 68 | "?token=#{person.confirmation_token}" 69 | 70 | email = ClearanceMailer.change_password(person) 71 | 72 | expect(email.text_part.body).to include(link) 73 | expect(email.html_part.body).to include(link) 74 | 75 | Object.send(:remove_const, :Person) 76 | Rails.application.reload_routes! 77 | end 78 | 79 | def define_people_routes 80 | Rails.application.routes.draw do 81 | resources :people, controller: "clearance/users", only: :create do 82 | resource( 83 | :password, 84 | controller: "clearance/passwords", 85 | only: %i[edit update], 86 | ) 87 | end 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /spec/models/user_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe User do 4 | it { is_expected.to have_db_index(:email) } 5 | it { is_expected.to have_db_index(:remember_token) } 6 | it { is_expected.to validate_presence_of(:email) } 7 | it { is_expected.to validate_presence_of(:password) } 8 | it { is_expected.to allow_value("foo@example.co.uk").for(:email) } 9 | it { is_expected.to allow_value("foo@example.com").for(:email) } 10 | it { is_expected.to allow_value("foo+bar@example.com").for(:email) } 11 | it { is_expected.not_to allow_value("example.com").for(:email) } 12 | it { is_expected.not_to allow_value("foo").for(:email) } 13 | it { is_expected.not_to allow_value("foo@").for(:email) } 14 | it { is_expected.not_to allow_value("foo@bar").for(:email) } 15 | it { is_expected.not_to allow_value("foo;@example.com").for(:email) } 16 | it { is_expected.not_to allow_value("foo@.example.com").for(:email) } 17 | it { is_expected.not_to allow_value("foo@example..com").for(:email) } 18 | 19 | describe "#email" do 20 | it "stores email in down case and removes whitespace" do 21 | user = create(:user, email: "Jo hn.Do e @exa mp le.c om") 22 | 23 | expect(user.email).to eq "john.doe@example.com" 24 | end 25 | 26 | subject { create(:user) } 27 | it { is_expected.to validate_uniqueness_of(:email).case_insensitive } 28 | end 29 | 30 | describe ".authenticate" do 31 | it "is authenticated with correct email and password" do 32 | user = create(:user) 33 | password = user.password 34 | 35 | expect(User.authenticate(user.email, password)).to eq(user) 36 | end 37 | 38 | it "is authenticated with correct uppercased email and correct password" do 39 | user = create(:user) 40 | password = user.password 41 | 42 | expect(User.authenticate(user.email.upcase, password)).to eq(user) 43 | end 44 | 45 | it "is not authenticated with incorrect credentials" do 46 | user = create(:user) 47 | 48 | expect(User.authenticate(user.email, "bad_password")).to be_nil 49 | end 50 | 51 | it "takes the same amount of time to authenticate regardless of whether user exists" do 52 | user = create(:user) 53 | password = user.password 54 | 55 | user_exists_time = Benchmark.realtime do 56 | User.authenticate(user.email, password) 57 | end 58 | 59 | user_does_not_exist_time = Benchmark.realtime do 60 | User.authenticate("bad_email@example.com", password) 61 | end 62 | 63 | expect(user_does_not_exist_time). to be_within(0.01).of(user_exists_time) 64 | end 65 | 66 | it "takes the same amount of time to fail authentication regardless of whether user exists" do 67 | user = create(:user) 68 | 69 | user_exists_time = Benchmark.realtime do 70 | User.authenticate(user.email, "bad_password") 71 | end 72 | 73 | user_does_not_exist_time = Benchmark.realtime do 74 | User.authenticate("bad_email@example.com", "bad_password") 75 | end 76 | 77 | expect(user_does_not_exist_time). to be_within(0.01).of(user_exists_time) 78 | end 79 | 80 | it "is retrieved via a case-insensitive search" do 81 | user = create(:user) 82 | 83 | expect(User.find_by_normalized_email(user.email.upcase)).to eq(user) 84 | end 85 | end 86 | 87 | describe ".reset_remember_token" do 88 | context "when resetting authentication with reset_remember_token!" do 89 | it "changes the remember token" do 90 | old_token = "old_token" 91 | user = create(:user, remember_token: old_token) 92 | 93 | user.reset_remember_token! 94 | 95 | expect(user.remember_token).not_to eq old_token 96 | end 97 | end 98 | end 99 | 100 | describe "#update_password" do 101 | context "with a valid password" do 102 | it "changes the encrypted password" do 103 | user = create(:user, :with_forgotten_password) 104 | old_encrypted_password = user.encrypted_password 105 | 106 | user.update_password("new_password") 107 | 108 | expect(user.encrypted_password).not_to eq old_encrypted_password 109 | end 110 | 111 | it "clears the confirmation token" do 112 | user = create(:user, :with_forgotten_password) 113 | 114 | user.update_password("new_password") 115 | 116 | expect(user.confirmation_token).to be_nil 117 | end 118 | 119 | it "sets the remember token" do 120 | user = create(:user, :with_forgotten_password) 121 | 122 | user.update_password("my_new_password") 123 | 124 | user.reload 125 | expect(user.remember_token).not_to be_nil 126 | end 127 | end 128 | 129 | context "with blank password" do 130 | it "does not change the encrypted password" do 131 | user = create(:user, :with_forgotten_password) 132 | old_encrypted_password = user.encrypted_password 133 | 134 | user.update_password("") 135 | 136 | expect(user.encrypted_password.to_s).to eq old_encrypted_password 137 | end 138 | 139 | it "does not clear the confirmation token" do 140 | user = create(:user, :with_forgotten_password) 141 | 142 | user.update_password("") 143 | 144 | expect(user.confirmation_token).not_to be_nil 145 | end 146 | end 147 | end 148 | 149 | describe "before_create callbacks" do 150 | describe "#generate_remember_token" do 151 | it "does not generate same remember token for users with same password" do 152 | allow(Time).to receive(:now).and_return(Time.now) 153 | password = "secret" 154 | first_user = create(:user, password: password) 155 | second_user = create(:user, password: password) 156 | 157 | expect(second_user.remember_token).not_to eq first_user.remember_token 158 | end 159 | end 160 | end 161 | 162 | describe "#forgot_password!" do 163 | it "generates the confirmation token" do 164 | user = create(:user, confirmation_token: nil) 165 | 166 | user.forgot_password! 167 | 168 | expect(user.confirmation_token).not_to be_nil 169 | end 170 | end 171 | 172 | describe "a user with an optional email" do 173 | subject { user } 174 | 175 | it { is_expected.to allow_value(nil).for(:email) } 176 | it { is_expected.to allow_value("").for(:email) } 177 | 178 | def user 179 | @user ||= User.new 180 | allow(@user).to receive(:email_optional?).and_return(true) 181 | @user 182 | end 183 | end 184 | 185 | describe "user factory" do 186 | it "should create a valid user with just an overridden password" do 187 | expect(build(:user, password: "test")).to be_valid 188 | end 189 | end 190 | 191 | describe "email address normalization" do 192 | it "downcases the address and strips spaces" do 193 | email = "Jo hn.Do e @exa mp le.c om" 194 | 195 | expect(User.normalize_email(email)).to eq "john.doe@example.com" 196 | end 197 | end 198 | 199 | describe "the password setter on a User" do 200 | it "sets password to the plain-text password" do 201 | password = "password" 202 | subject.send(:password=, password) 203 | 204 | expect(subject.password).to eq password 205 | end 206 | 207 | it "also sets encrypted_password" do 208 | password = "password" 209 | subject.send(:password=, password) 210 | 211 | expect(subject.encrypted_password).to_not be_nil 212 | end 213 | end 214 | end 215 | 216 | describe UserWithOptionalPassword do 217 | it { is_expected.to allow_value(nil).for(:password) } 218 | it { is_expected.to allow_value("").for(:password) } 219 | 220 | it "cannot authenticate with blank password" do 221 | user = create(:user_with_optional_password) 222 | 223 | expect(UserWithOptionalPassword.authenticate(user.email, "")).to be_nil 224 | end 225 | end 226 | -------------------------------------------------------------------------------- /spec/password_strategies/argon2_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Clearance::PasswordStrategies::Argon2 do 4 | include FakeModelWithPasswordStrategy 5 | 6 | describe "#password=" do 7 | it "encrypts the password into encrypted_password" do 8 | stub_argon2_password 9 | model_instance = fake_model_with_argon2_strategy 10 | 11 | model_instance.password = password 12 | 13 | expect(model_instance.encrypted_password).to eq encrypted_password 14 | end 15 | 16 | it "encrypts with Argon2 using default cost in non test environments" do 17 | hasher = stub_argon2_password 18 | model_instance = fake_model_with_argon2_strategy 19 | allow(Rails).to receive(:env). 20 | and_return(ActiveSupport::StringInquirer.new("production")) 21 | 22 | model_instance.password = password 23 | 24 | expect(hasher).to have_received(:create).with(password) 25 | end 26 | 27 | it "encrypts with Argon2 using minimum cost in test environment" do 28 | hasher = stub_argon2_password 29 | model_instance = fake_model_with_argon2_strategy 30 | 31 | model_instance.password = password 32 | 33 | expect(hasher).to have_received(:create).with(password) 34 | end 35 | 36 | def stub_argon2_password 37 | hasher = double(Argon2::Password) 38 | allow(hasher).to receive(:create).and_return(encrypted_password) 39 | allow(Argon2::Password).to receive(:new).and_return(hasher) 40 | hasher 41 | end 42 | 43 | def encrypted_password 44 | @encrypted_password ||= double("encrypted password") 45 | end 46 | end 47 | 48 | describe "#authenticated?" do 49 | context "given a password" do 50 | it "is authenticated with Argon2" do 51 | model_instance = fake_model_with_argon2_strategy 52 | 53 | model_instance.password = password 54 | 55 | expect(model_instance).to be_authenticated(password) 56 | end 57 | end 58 | 59 | context "given no password" do 60 | it "is not authenticated" do 61 | model_instance = fake_model_with_argon2_strategy 62 | 63 | password = nil 64 | 65 | expect(model_instance).not_to be_authenticated(password) 66 | end 67 | end 68 | end 69 | 70 | def fake_model_with_argon2_strategy 71 | @fake_model_with_argon2_strategy ||= fake_model_with_password_strategy( 72 | Clearance::PasswordStrategies::Argon2, 73 | ) 74 | end 75 | 76 | def password 77 | "password" 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /spec/password_strategies/bcrypt_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | include FakeModelWithPasswordStrategy 3 | 4 | describe Clearance::PasswordStrategies::BCrypt do 5 | describe "#password=" do 6 | it "encrypts the password into encrypted_password" do 7 | stub_bcrypt_password 8 | model_instance = fake_model_with_bcrypt_strategy 9 | 10 | model_instance.password = password 11 | 12 | expect(model_instance.encrypted_password).to eq encrypted_password 13 | end 14 | 15 | it "encrypts with BCrypt using default cost in non test environments" do 16 | stub_bcrypt_password 17 | model_instance = fake_model_with_bcrypt_strategy 18 | allow(Rails).to receive(:env). 19 | and_return(ActiveSupport::StringInquirer.new("production")) 20 | 21 | model_instance.password = password 22 | 23 | expect(BCrypt::Password).to have_received(:create).with( 24 | password, 25 | cost: ::BCrypt::Engine::DEFAULT_COST, 26 | ) 27 | end 28 | 29 | it "uses an explicity configured BCrypt cost" do 30 | stub_bcrypt_cost(8) 31 | bcrypt_password = BCrypt::Password.create(password, cost: nil) 32 | 33 | expect(bcrypt_password.cost).to eq(8) 34 | end 35 | 36 | it "uses the default BCrypt cost value implicitly" do 37 | bcrypt_password = BCrypt::Password.create(password, cost: nil) 38 | 39 | expect(bcrypt_password.cost).to eq(BCrypt::Engine::DEFAULT_COST) 40 | end 41 | 42 | it "encrypts with BCrypt using minimum cost in test environment" do 43 | stub_bcrypt_password 44 | model_instance = fake_model_with_bcrypt_strategy 45 | 46 | model_instance.password = password 47 | 48 | expect(BCrypt::Password).to have_received(:create).with( 49 | password, 50 | cost: ::BCrypt::Engine::MIN_COST 51 | ) 52 | end 53 | 54 | def stub_bcrypt_password 55 | allow(BCrypt::Password).to receive(:create).and_return(encrypted_password) 56 | end 57 | 58 | def stub_bcrypt_cost(cost) 59 | allow(BCrypt::Engine).to receive(:cost).and_return(cost) 60 | end 61 | 62 | def encrypted_password 63 | @encrypted_password ||= double("encrypted password") 64 | end 65 | end 66 | 67 | describe "#authenticated?" do 68 | context "given a password" do 69 | it "is authenticated with BCrypt" do 70 | model_instance = fake_model_with_bcrypt_strategy 71 | 72 | model_instance.password = password 73 | 74 | expect(model_instance).to be_authenticated(password) 75 | end 76 | end 77 | 78 | context "given no password" do 79 | it "is not authenticated" do 80 | model_instance = fake_model_with_bcrypt_strategy 81 | 82 | password = nil 83 | 84 | expect(model_instance).not_to be_authenticated(password) 85 | end 86 | end 87 | end 88 | 89 | def fake_model_with_bcrypt_strategy 90 | @model_instance ||= fake_model_with_password_strategy( 91 | Clearance::PasswordStrategies::BCrypt 92 | ) 93 | end 94 | 95 | def password 96 | "password" 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /spec/password_strategies/password_strategies_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | include FakeModelWithoutPasswordStrategy 3 | 4 | describe "Password strategy configuration" do 5 | describe "when Clearance.configuration.password_strategy is set" do 6 | it "includes the value it is set to" do 7 | mock_password_strategy = Module.new 8 | 9 | Clearance.configuration.password_strategy = mock_password_strategy 10 | 11 | expect(model_instance).to be_kind_of(mock_password_strategy) 12 | end 13 | end 14 | 15 | describe "when Clearance.configuration.password_strategy is not set" do 16 | it "includes Clearance::PasswordStrategies::BCrypt" do 17 | Clearance.configuration.password_strategy = nil 18 | 19 | expect(model_instance).to be_kind_of( 20 | Clearance::PasswordStrategies::BCrypt 21 | ) 22 | end 23 | end 24 | 25 | def model_instance 26 | fake_model_without_password_strategy 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/requests/authentication_cookie_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | class PagesController < ApplicationController 4 | include Clearance::Controller 5 | before_action :require_login, only: :private 6 | 7 | # A page requiring user authentication 8 | def private 9 | head :ok 10 | end 11 | 12 | # A page that does not require user authentication 13 | def public 14 | head :ok 15 | end 16 | end 17 | 18 | describe "Authentication cookies in the response" do 19 | before do 20 | draw_test_routes 21 | create_user_and_sign_in 22 | end 23 | 24 | after do 25 | Rails.application.reload_routes! 26 | end 27 | 28 | it "are not present if the request does not authenticate" do 29 | get public_path 30 | 31 | expect(headers["Set-Cookie"]).to be_nil 32 | end 33 | 34 | it "are present if the request does authenticate" do 35 | get private_path 36 | 37 | expect(headers["Set-Cookie"]).to match(/remember_token=/) 38 | end 39 | 40 | def draw_test_routes 41 | Rails.application.routes.draw do 42 | get "/private" => "pages#private", as: :private 43 | get "/public" => "pages#public", as: :public 44 | resource :session, controller: "clearance/sessions", only: [:create] 45 | end 46 | end 47 | 48 | def create_user_and_sign_in 49 | user = create(:user, password: "password") 50 | 51 | post session_path, params: { 52 | session: { email: user.email, password: "password" }, 53 | } 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/requests/backdoor_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "Backdoor Middleware" do 4 | it "allows signing in using query parameter" do 5 | user = create(:user) 6 | 7 | get root_path(as: user.to_param) 8 | 9 | expect(cookies["remember_token"]).to eq user.remember_token 10 | end 11 | 12 | it "removes the `as` param but leaves other parameters unchanged" do 13 | user = create(:user) 14 | 15 | get root_path(as: user.to_param, foo: 'bar') 16 | 17 | expect(response.body).to include('{"foo":"bar","controller":"application","action":"show"}') 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/requests/cookie_options_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "Cookie options" do 4 | before do 5 | Clearance.configuration.httponly = httponly 6 | end 7 | 8 | context "when httponly config value is false" do 9 | let(:httponly) { false } 10 | describe "sign in" do 11 | before do 12 | user = create(:user, password: "password") 13 | get sign_in_path 14 | 15 | post session_path, params: { 16 | session: { email: user.email, password: "password" }, 17 | } 18 | end 19 | 20 | it { should_have_one_remember_token } 21 | 22 | it "should not have the httponly flag set" do 23 | expect(remember_token_cookies.last).not_to match(/HttpOnly/) 24 | end 25 | end 26 | end 27 | 28 | context "when httponly config value is true" do 29 | let(:httponly) { true } 30 | describe "sign in" do 31 | before do 32 | user = create(:user, password: "password") 33 | get sign_in_path 34 | 35 | post session_path, params: { 36 | session: { email: user.email, password: "password" }, 37 | } 38 | end 39 | 40 | it { should_have_one_remember_token } 41 | 42 | it "should have the httponly flag set" do 43 | expect(remember_token_cookies.last.downcase).to match(/httponly/) 44 | end 45 | end 46 | end 47 | 48 | def should_have_one_remember_token 49 | expect(remember_token_cookies.length).to eq(1), 50 | "expected one 'remember_token' cookie:\n#{remember_token_cookies}" 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/requests/csrf_rotation_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "CSRF Rotation" do 4 | around do |example| 5 | ActionController::Base.allow_forgery_protection = true 6 | example.run 7 | ActionController::Base.allow_forgery_protection = false 8 | end 9 | 10 | context "Clearance is configured to rotate CSRF token on sign in" do 11 | describe "sign in" do 12 | it "rotates the CSRF token" do 13 | Clearance.configure { |config| config.rotate_csrf_on_sign_in = true } 14 | get sign_in_path 15 | user = create(:user, password: "password") 16 | original_token = csrf_token 17 | 18 | post session_path, params: { 19 | authenticity_token: csrf_token, session: { email: user.email, password: "password" } 20 | } 21 | 22 | expect(csrf_token).not_to eq original_token 23 | expect(csrf_token).to be_present 24 | end 25 | end 26 | end 27 | 28 | def csrf_token 29 | session[:_csrf_token] 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/requests/password_maintenance_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "Password maintenance" do 4 | context "when updating the password" do 5 | it "signs the user in and redirects" do 6 | user = create(:user, :with_forgotten_password) 7 | 8 | put user_password_url(user), params: { 9 | user_id: user, 10 | token: user.confirmation_token, 11 | password_reset: { password: "my_new_password" }, 12 | } 13 | 14 | expect(response).to redirect_to(Clearance.configuration.redirect_url) 15 | expect(response).to have_http_status(:see_other) 16 | expect(cookies["remember_token"]).to be_present 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/requests/token_expiration_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "Token expiration" do 4 | describe "after signing in" do 5 | before do 6 | freeze_time 7 | create_user_and_sign_in 8 | @initial_cookies = remember_token_cookies 9 | end 10 | 11 | after do 12 | unfreeze_time 13 | end 14 | 15 | it "should have a remember_token cookie with a future expiration" do 16 | expect(first_cookie.expires).to be_between( 17 | 1.years.from_now - 1.second, 18 | 1.years.from_now, 19 | ) 20 | end 21 | end 22 | 23 | describe "after signing in and making a followup request" do 24 | before do 25 | create_user_and_sign_in 26 | @initial_cookies = remember_token_cookies 27 | 28 | travel_to(1.minute.from_now) do 29 | get root_path 30 | @followup_cookies = remember_token_cookies 31 | end 32 | end 33 | 34 | it "should set a remember_token on each request with updated expiration" do 35 | expect(@followup_cookies.length).to be >= 1, 36 | "remember token wasn't set on second request" 37 | 38 | expect(second_cookie.expires).to be > first_cookie.expires 39 | end 40 | end 41 | 42 | def first_cookie 43 | Rack::Test::Cookie.new @initial_cookies.last 44 | end 45 | 46 | def second_cookie 47 | Rack::Test::Cookie.new @followup_cookies.last 48 | end 49 | 50 | def create_user_and_sign_in 51 | user = create(:user, password: "password") 52 | 53 | get sign_in_path 54 | 55 | post session_path, params: { 56 | session: { email: user.email, password: "password" }, 57 | } 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/routing/clearance_routes_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'routes for Clearance' do 4 | context 'routes enabled' do 5 | it 'draws the default routes' do 6 | expect(get: 'sign_up').to be_routable 7 | expect(get: 'sign_in').to be_routable 8 | expect(get: 'passwords/new').to be_routable 9 | expect(post: 'session').to be_routable 10 | expect(post: 'passwords').to be_routable 11 | expect(post: 'users').to be_routable 12 | end 13 | end 14 | 15 | context 'routes disabled' do 16 | around do |example| 17 | Clearance.configure { |config| config.routes = false } 18 | Rails.application.reload_routes! 19 | example.run 20 | Clearance.configuration = Clearance::Configuration.new 21 | Rails.application.reload_routes! 22 | end 23 | 24 | it 'does not draw any routes' do 25 | expect(get: 'sign_up').not_to be_routable 26 | expect(get: 'sign_in').not_to be_routable 27 | expect(get: 'passwords/new').not_to be_routable 28 | expect(post: 'session').not_to be_routable 29 | expect(post: 'passwords').not_to be_routable 30 | expect(post: 'users').not_to be_routable 31 | end 32 | end 33 | 34 | context 'signup disabled' do 35 | around do |example| 36 | Clearance.configure { |config| config.allow_sign_up = false } 37 | Rails.application.reload_routes! 38 | example.run 39 | Clearance.configuration = Clearance::Configuration.new 40 | Rails.application.reload_routes! 41 | end 42 | 43 | it 'does not route sign_up' do 44 | expect(get: 'sign_up').not_to be_routable 45 | end 46 | 47 | it 'does not route to users#create' do 48 | expect(post: 'users').not_to be_routable 49 | end 50 | 51 | it 'does not route to users#new' do 52 | expect(get: 'users/new').not_to be_routable 53 | end 54 | end 55 | 56 | context 'signup enabled' do 57 | it 'does route sign_up' do 58 | expect(get: 'sign_up').to be_routable 59 | end 60 | 61 | it 'does route to users#create' do 62 | expect(post: 'users').to be_routable 63 | end 64 | end 65 | 66 | context 'password reset disabled' do 67 | around do |example| 68 | Clearance.configure { |config| config.allow_password_reset = false } 69 | Rails.application.reload_routes! 70 | example.run 71 | Clearance.configuration = Clearance::Configuration.new 72 | Rails.application.reload_routes! 73 | end 74 | 75 | it 'does not route password edit' do 76 | user = create(:user) 77 | expect(get: "users/#{user.id}/password/edit").not_to be_routable 78 | end 79 | 80 | it 'does not route to clearance/passwords#update' do 81 | user = create(:user) 82 | expect(patch: "/users/#{user.id}/password").not_to be_routable 83 | end 84 | end 85 | 86 | context 'reset enabled' do 87 | it 'does route password edit' do 88 | user = create(:user) 89 | expect(get: "users/#{user.id}/password/edit").to be_routable 90 | end 91 | 92 | it 'does route to clearance/passwords#update' do 93 | user = create(:user) 94 | expect(patch: "/users/#{user.id}/password").to be_routable 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] ||= "test" 2 | require_relative "dummy/config/environment" 3 | 4 | require "rspec/rails" 5 | require "clearance/rspec" 6 | 7 | Dir[File.expand_path("spec/support/**/*.rb")].each { |f| require f } 8 | 9 | RSpec.configure do |config| 10 | config.include ActiveSupport::Testing::TimeHelpers 11 | config.include FactoryBot::Syntax::Methods 12 | config.infer_spec_type_from_file_location! 13 | config.order = :random 14 | config.use_transactional_fixtures = true 15 | 16 | config.expect_with :rspec do |expectations| 17 | expectations.syntax = :expect 18 | end 19 | 20 | config.mock_with :rspec do |mocks| 21 | mocks.syntax = :expect 22 | end 23 | 24 | config.before { restore_default_warning_free_config } 25 | end 26 | 27 | Shoulda::Matchers.configure do |config| 28 | config.integrate do |with| 29 | with.test_framework :rspec 30 | with.library :action_controller 31 | with.library :active_model 32 | with.library :active_record 33 | end 34 | end 35 | 36 | def restore_default_warning_free_config 37 | Clearance.configuration = nil 38 | end 39 | -------------------------------------------------------------------------------- /spec/support/clearance.rb: -------------------------------------------------------------------------------- 1 | require 'clearance' 2 | 3 | Clearance.configure do |config| 4 | # need an empty block to initialize the configuration object 5 | end 6 | 7 | # NOTE: to run the entire suite with signed cookies 8 | # you can set the signed_cookie default to true 9 | # and run all specs. 10 | # However, to fake the actual signing process you 11 | # can monkey-patch ActionDispatch so signed cookies 12 | # behave like normal ones 13 | # 14 | # class ActionDispatch::Cookies::CookieJar 15 | # def signed; self; end 16 | # end 17 | 18 | module Clearance 19 | module Test 20 | module Redirects 21 | def redirect_to_url_after_create 22 | redirect_to(@controller.send(:url_after_create)) 23 | end 24 | 25 | def redirect_to_url_after_update 26 | redirect_to(@controller.send(:url_after_update)) 27 | end 28 | 29 | def redirect_to_url_after_destroy 30 | redirect_to(@controller.send(:url_after_destroy)) 31 | end 32 | end 33 | end 34 | end 35 | 36 | RSpec.configure do |config| 37 | config.include Clearance::Test::Redirects 38 | end 39 | -------------------------------------------------------------------------------- /spec/support/fake_model_with_password_strategy.rb: -------------------------------------------------------------------------------- 1 | module FakeModelWithPasswordStrategy 2 | def fake_model_with_password_strategy(password_strategy) 3 | Class.new do 4 | attr_reader :password 5 | attr_accessor :encrypted_password, :salt 6 | 7 | include password_strategy 8 | end.new 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/support/fake_model_without_password_strategy.rb: -------------------------------------------------------------------------------- 1 | module FakeModelWithoutPasswordStrategy 2 | def fake_model_without_password_strategy 3 | Class.new do 4 | include ActiveModel::Validations 5 | 6 | validates_with UniquenessValidator 7 | 8 | def self.before_validation(*); end 9 | def self.before_create(*); end 10 | 11 | include Clearance::User 12 | end.new 13 | end 14 | end 15 | 16 | class UniquenessValidator < ActiveModel::Validator 17 | def validate(_record) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/support/generator_spec_helpers.rb: -------------------------------------------------------------------------------- 1 | require "ammeter/rspec/generator/example.rb" 2 | require "ammeter/rspec/generator/matchers.rb" 3 | require "ammeter/init" 4 | 5 | module GeneratorSpecHelpers 6 | module FileMethods 7 | def file(path) 8 | Pathname.new(super) 9 | end 10 | 11 | def migration_file(path) 12 | Pathname.new(super) 13 | end 14 | end 15 | 16 | TEMPLATE_PATH = File.expand_path("../../app_templates", __FILE__) 17 | 18 | def provide_existing_routes_file 19 | copy_to_generator_root("config", "routes.rb") 20 | end 21 | 22 | def provide_existing_initializer 23 | copy_to_generator_root("config/initializers", "clearance.rb") 24 | end 25 | 26 | def provide_existing_application_controller 27 | copy_to_generator_root("app/controllers", "application_controller.rb") 28 | end 29 | 30 | def provide_existing_user_class 31 | copy_to_generator_root("app/models", "user.rb") 32 | allow(File).to receive(:exist?).and_call_original 33 | allow(File).to receive(:exist?).with("app/models/user.rb").and_return(true) 34 | end 35 | 36 | private 37 | 38 | def copy_to_generator_root(destination, template) 39 | template_file = File.join(TEMPLATE_PATH, destination, template) 40 | destination = File.join(destination_root, destination) 41 | 42 | FileUtils.mkdir_p(destination) 43 | FileUtils.cp(template_file, destination) 44 | end 45 | end 46 | 47 | RSpec.configure do |config| 48 | config.include GeneratorSpecHelpers 49 | config.prepend GeneratorSpecHelpers::FileMethods 50 | 51 | config.before(:example, :generator) do 52 | destination File.expand_path("../../../tmp", __FILE__) 53 | prepare_destination 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/support/request_with_remember_token.rb: -------------------------------------------------------------------------------- 1 | module RememberTokenHelpers 2 | def request_with_remember_token(remember_token) 3 | cookies = ActionDispatch::Request.new({}).cookie_jar 4 | if Clearance.configuration.signed_cookie 5 | cookies.signed[Clearance.configuration.cookie_name] = remember_token 6 | else 7 | cookies[Clearance.configuration.cookie_name] = remember_token 8 | end 9 | 10 | env = { clearance: Clearance::Session.new(cookies.request.env) } 11 | Rack::Request.new env 12 | end 13 | 14 | def request_without_remember_token 15 | request_with_remember_token nil 16 | end 17 | 18 | def remember_token_cookies 19 | set_cookie_header = headers["Set-Cookie"] || headers["set-cookie"] 20 | cookie_lines = Array(set_cookie_header).join("\n").lines.map(&:chomp) 21 | cookie_lines.select { |name| name =~ /^remember_token/ } 22 | end 23 | end 24 | 25 | RSpec.configure do |config| 26 | config.include RememberTokenHelpers 27 | end 28 | -------------------------------------------------------------------------------- /spec/views/view_helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "Clearance RSpec view spec configuration", type: :view do 4 | it "lets me use clearance's helper methods in view specs" do 5 | user = double("User") 6 | sign_in_as(user) 7 | 8 | expect(view.current_user).to eq user 9 | end 10 | end 11 | --------------------------------------------------------------------------------