diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..ea071307 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: athix diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..582e1fac --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,20 @@ +Please complete all sections. + +### Configuration + +- Sorcery Version: `` +- Ruby Version: `` +- Framework: `` +- Platform: `` + +### Expected Behavior + +Tell us what should happen. + +### Actual Behavior + +Tell us what happens instead. + +### Steps to Reproduce + +Please list all steps to reproduce the issue. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..6bd1eef5 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,5 @@ +Please ensure your pull request includes the following: + +- [ ] Description of changes +- [ ] Update to CHANGELOG.md with short description and link to pull request +- [ ] Changes have related RSpec tests that ensure functionality does not break diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml new file mode 100644 index 00000000..a965e5d1 --- /dev/null +++ b/.github/workflows/ruby.yml @@ -0,0 +1,49 @@ +name: Test Suite + +# Run against all commits and pull requests. +on: [ push, pull_request ] + +jobs: + test_matrix: + + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + ruby: + - 2.4 + - 2.5 + - 2.6 + - 2.7 + - 3.0 + + rails: + - '52' + - '60' + + exclude: + - ruby: 2.4 + rails: '60' + - ruby: 3.0 + rails: '52' + + env: + BUNDLE_GEMFILE: gemfiles/rails_${{ matrix.rails }}.gemfile + + steps: + - uses: actions/checkout@v2 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + - name: Run tests + run: bundle exec rake spec + + finish: + runs-on: ubuntu-latest + needs: [ test_matrix ] + steps: + - name: Wait for status checks + run: echo "All Green!" diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 00000000..fa6928d0 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,55 @@ +inherit_from: .rubocop_todo.yml + +AllCops: + Exclude: + - 'lib/generators/sorcery/templates/**/*' + TargetRubyVersion: 2.6 + +# See: https://github.com/rubocop-hq/rubocop/issues/3344 +Style/DoubleNegation: + Enabled: false + +#################### +## Pre-1.0.0 Code ## +#################### + +Metrics/AbcSize: + Exclude: + - 'lib/**/*' + - 'spec/**/*' +Metrics/BlockLength: + Exclude: + - 'lib/**/*' + - 'spec/**/*' +Layout/LineLength: + Exclude: + - 'lib/**/*' + - 'spec/**/*' +Metrics/ClassLength: + Exclude: + - 'lib/**/*' + - 'spec/**/*' +Metrics/CyclomaticComplexity: + Exclude: + - 'lib/**/*' + - 'spec/**/*' +Metrics/MethodLength: + Exclude: + - 'lib/**/*' + - 'spec/**/*' +Metrics/PerceivedComplexity: + Exclude: + - 'lib/**/*' + - 'spec/**/*' +Naming/AccessorMethodName: + Exclude: + - 'lib/**/*' + - 'spec/**/*' +Naming/PredicateName: + Exclude: + - 'lib/**/*' + - 'spec/**/*' +Style/Documentation: + Exclude: + - 'lib/**/*' + - 'spec/**/*' diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 00000000..9cb44610 --- /dev/null +++ b/.rubocop_todo.yml @@ -0,0 +1,163 @@ +# This configuration was generated by +# `rubocop --auto-gen-config` +# on 2021-04-04 05:00:11 UTC using RuboCop version 0.88.0. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# Offense count: 1 +# Configuration parameters: Include. +# Include: **/*.gemspec +Gemspec/RequiredRubyVersion: + Exclude: + - 'sorcery.gemspec' + +# Offense count: 2 +# Cop supports --auto-correct. +# Configuration parameters: IndentationWidth. +# SupportedStyles: special_inside_parentheses, consistent, align_braces +Layout/FirstHashElementIndentation: + EnforcedStyle: consistent + +# Offense count: 83 +# Cop supports --auto-correct. +# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. +# SupportedHashRocketStyles: key, separator, table +# SupportedColonStyles: key, separator, table +# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit +Layout/HashAlignment: + Enabled: false + +# Offense count: 3 +# Cop supports --auto-correct. +# Configuration parameters: AllowInHeredoc. +Layout/TrailingWhitespace: + Exclude: + - 'lib/sorcery/controller/submodules/external.rb' + +# Offense count: 2 +# Configuration parameters: AllowSafeAssignment. +Lint/AssignmentInCondition: + Exclude: + - 'spec/rails_app/app/controllers/sorcery_controller.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +Lint/NonDeterministicRequireOrder: + Exclude: + - 'spec/spec_helper.rb' + +# Offense count: 4 +# Cop supports --auto-correct. +Lint/RedundantCopDisableDirective: + Exclude: + - 'lib/sorcery/controller.rb' + - 'lib/sorcery/model.rb' + - 'spec/rails_app/config/application.rb' + - 'spec/shared_examples/user_shared_examples.rb' + +# Offense count: 4 +# Cop supports --auto-correct. +Lint/SendWithMixinArgument: + Exclude: + - 'lib/sorcery.rb' + - 'lib/sorcery/engine.rb' + - 'lib/sorcery/test_helpers/internal/rails.rb' + +# Offense count: 2 +# Cop supports --auto-correct. +# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. +Lint/UnusedBlockArgument: + Exclude: + - 'spec/shared_examples/user_shared_examples.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions. +# SupportedStyles: assign_to_condition, assign_inside_condition +Style/ConditionalAssignment: + Exclude: + - 'lib/sorcery/adapters/active_record_adapter.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +Style/ExpandPathArguments: + Exclude: + - 'spec/rails_app/config.ru' + +# Offense count: 1 +# Configuration parameters: EnforcedStyle. +# SupportedStyles: annotated, template, unannotated +Style/FormatStringToken: + Exclude: + - 'lib/generators/sorcery/install_generator.rb' + +# Offense count: 125 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle. +# SupportedStyles: always, always_true, never +Style/FrozenStringLiteralComment: + Enabled: false + +# Offense count: 3 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. +# SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys +Style/HashSyntax: + Exclude: + - 'lib/sorcery/adapters/active_record_adapter.rb' + - 'lib/sorcery/test_helpers/rails/integration.rb' + +# Offense count: 34 +# Cop supports --auto-correct. +Style/IfUnlessModifier: + Enabled: false + +# Offense count: 1 +# Cop supports --auto-correct. +Style/MultilineIfModifier: + Exclude: + - 'lib/sorcery/providers/line.rb' + +# Offense count: 2 +# Cop supports --auto-correct. +Style/RedundantBegin: + Exclude: + - 'lib/sorcery/controller.rb' + - 'lib/sorcery/model.rb' + +# Offense count: 4 +# Cop supports --auto-correct. +# Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods. +# AllowedMethods: present?, blank?, presence, try, try! +Style/SafeNavigation: + Exclude: + - 'lib/sorcery/controller/config.rb' + - 'lib/sorcery/controller/submodules/brute_force_protection.rb' + - 'lib/sorcery/controller/submodules/remember_me.rb' + - 'lib/sorcery/model.rb' + +# Offense count: 7 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. +# SupportedStyles: single_quotes, double_quotes +Style/StringLiterals: + Exclude: + - 'spec/controllers/controller_oauth2_spec.rb' + - 'spec/sorcery_crypto_providers_spec.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, MinSize. +# SupportedStyles: percent, brackets +Style/SymbolArray: + Exclude: + - 'Rakefile' + +# Offense count: 2 +# Cop supports --auto-correct. +Style/UnpackFirst: + Exclude: + - 'lib/sorcery/crypto_providers/aes256.rb' + - 'spec/sorcery_crypto_providers_spec.rb' diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 381b868e..00000000 --- a/.travis.yml +++ /dev/null @@ -1,46 +0,0 @@ -language: ruby -rvm: - - jruby - - 2.2.6 - - 2.3.3 - - 2.4.0 - -env: - global: - - JRUBY_OPTS="--2.0" - -gemfile: - - Gemfile - - gemfiles/active_record-rails40.gemfile - - gemfiles/active_record-rails41.gemfile - - gemfiles/active_record-rails42.gemfile - -before_script: - - mysql -e 'create database sorcery_test;' - -before_install: - - rvm get stable --auto-dotfiles - - gem update bundler - -matrix: - allow_failures: - - rvm: jruby - - exclude: - - rvm: 2.2.6 - gemfile: gemfiles/active_record-rails40.gemfile - - - rvm: 2.3.3 - gemfile: gemfiles/active_record-rails40.gemfile - - - rvm: 2.4.0 - gemfile: gemfiles/active_record-rails40.gemfile - - - rvm: 2.4.0 - gemfile: gemfiles/active_record-rails41.gemfile - - - rvm: 2.4.0 - gemfile: gemfiles/active_record-rails42.gemfile - - - rvm: jruby - gemfile: Gemfile diff --git a/CHANGELOG.md b/CHANGELOG.md index 6553ae0b..6c6aacb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,91 @@ # Changelog ## HEAD +* Inline core migration index definition [#281](https://github.com/Sorcery/sorcery/pull/281) +* Fix MongoID adapter breaking on save [#284](https://github.com/Sorcery/sorcery/pull/284) + +## 0.16.1 + +* Fix default table name being incorrect in migration generator [#274](https://github.com/Sorcery/sorcery/pull/274) +* Update `oauth` dependency per CVE-2016-11086 + +## 0.16.0 + +* Add BattleNet Provider [#260](https://github.com/Sorcery/sorcery/pull/260) +* Fix failing isolated tests [#249](https://github.com/Sorcery/sorcery/pull/249) +* Support LINE login v2.1 [#251](https://github.com/Sorcery/sorcery/pull/251) +* Update generators to better support namespaces [#237](https://github.com/Sorcery/sorcery/pull/237) +* Add support for Rails 6 [#238](https://github.com/Sorcery/sorcery/pull/238) +* Fix ruby 2.7 deprecation warnings [#241](https://github.com/Sorcery/sorcery/pull/241) +* Use set to ensure unique arrays [#233](https://github.com/Sorcery/sorcery/pull/233) + +## 0.15.1 + +* Update `oauth` dependency per CVE-2016-11086 + +## 0.15.0 + +* Fix brute force vuln due to callbacks no being ran [#235](https://github.com/Sorcery/sorcery/pull/235) +* Revert on_load change due to breaking existing applications [#234](https://github.com/Sorcery/sorcery/pull/234) +* Add forget_me! and force_forget_me! test cases [#216](https://github.com/Sorcery/sorcery/pull/216) +* In `generic_send_email`, check responds_to [#211](https://github.com/Sorcery/sorcery/pull/211) +* Fix typo [#219](https://github.com/Sorcery/sorcery/pull/219) +* Fix deprecation warnings in Rails 6 [#209](https://github.com/Sorcery/sorcery/pull/209) +* Add ruby 2.6.5 to the travis build [#215](https://github.com/Sorcery/sorcery/pull/215) +* Add discord provider [#185](https://github.com/Sorcery/sorcery/pull/185) +* Remove MySQL database creation call [#214](https://github.com/Sorcery/sorcery/pull/214) +* Use id instead of uid for VK provider [#199](https://github.com/Sorcery/sorcery/pull/199) +* Don't :return_t JSON requests after login [#197](https://github.com/Sorcery/sorcery/pull/197) +* Fix email scope for LinkedIn Provider [#191](https://github.com/Sorcery/sorcery/pull/191) +* Ignore cookies when undefined cookies [#187](https://github.com/Sorcery/sorcery/pull/187) +* Allow for custom providers with multi-word class names. [#190](https://github.com/Sorcery/sorcery/pull/190) + +## 0.14.0 + +* Update LinkedIn to use OAuth 2 [#189](https://github.com/Sorcery/sorcery/pull/189) +* Support the LINE login auth [#80](https://github.com/Sorcery/sorcery/pull/80) +* Allow BCrypt to have app-specific secret token [#173](https://github.com/Sorcery/sorcery/pull/173) +* Add #change_password method to reset_password module. [#165](https://github.com/Sorcery/sorcery/pull/165) +* Clean up initializer comments [#153](https://github.com/Sorcery/sorcery/pull/153) +* Allow load_from_magic_login_token to accept a block [#152](https://github.com/Sorcery/sorcery/pull/152) +* Fix CipherError class name [#142](https://github.com/Sorcery/sorcery/pull/142) +* Fix `update_failed_logins_count` being called twice when login failed [#163](https://github.com/Sorcery/sorcery/pull/163) +* Update migration templates to use new hash syntax [#170](https://github.com/Sorcery/sorcery/pull/170) +* Support for Rails 4.2 and lower soft-dropped [#171](https://github.com/Sorcery/sorcery/pull/171) + +## 0.13.0 + +* Add support for Rails 5.2 / Ruby 2.5 [#129](https://github.com/Sorcery/sorcery/pull/129) +* Fix migration files not being generated [#128](https://github.com/Sorcery/sorcery/pull/128) +* Add support for ActionController::API [#133](https://github.com/Sorcery/sorcery/pull/133), [#150](https://github.com/Sorcery/sorcery/pull/150), [#159](https://github.com/Sorcery/sorcery/pull/159) +* Update activation email to use after_commit callback [#130](https://github.com/Sorcery/sorcery/pull/130) +* Add opt-in `invalidate_active_sessions!` method [#110](https://github.com/Sorcery/sorcery/pull/110) +* Pass along `remember_me` to `#auto_login` [#136](https://github.com/Sorcery/sorcery/pull/136) +* Respect SessionTimeout on login via RememberMe [#102](https://github.com/Sorcery/sorcery/pull/102) +* Added `demodulize` on authentication class name association name fetch [#147](https://github.com/Sorcery/sorcery/pull/147) +* Remove Gemnasium badge [#140](https://github.com/Sorcery/sorcery/pull/140) +* Add Instragram provider [#51](https://github.com/Sorcery/sorcery/pull/51) +* Remove `publish_actions` permission for facebook [#139](https://github.com/Sorcery/sorcery/pull/139) +* Prepare for 1.0.0 [#157](https://github.com/Sorcery/sorcery/pull/157) +* Add Auth0 provider [#160](https://github.com/Sorcery/sorcery/pull/160) + +## 0.12.0 + +* Fix magic_login not inheriting from migration_class_name [#99](https://github.com/Sorcery/sorcery/pull/99) +* Update YARD dependency [#100](https://github.com/Sorcery/sorcery/pull/100) +* Make `#update_attributes` behave like `#update` [#98](https://github.com/Sorcery/sorcery/pull/98) +* Add tests to the magic login submodule [#95](https://github.com/Sorcery/sorcery/pull/95) +* Set user.stretches to 1 in test env by default [#81](https://github.com/Sorcery/sorcery/pull/81) +* Allow user to be loaded from other source when session expires. fix #89 [#94](https://github.com/Sorcery/sorcery/pull/94) +* Added a new ArgumentError for not defined user_class in config [#82](https://github.com/Sorcery/sorcery/pull/82) +* Updated Required Ruby version to 2.2 [#85](https://github.com/Sorcery/sorcery/pull/85) * Add configuration for token randomness [#67](https://github.com/Sorcery/sorcery/pull/67) * Add facebook user_info_path option to initializer.rb [#63](https://github.com/Sorcery/sorcery/pull/63) * Add new function: `build_from` (allows building a user instance from OAuth without saving) [#54](https://github.com/Sorcery/sorcery/pull/54) +* Add rubocop configuration and TODO list [#107](https://github.com/Sorcery/sorcery/pull/107) +* Add support for VK OAuth (thanks to @Hirurg103) [#109](https://github.com/Sorcery/sorcery/pull/109) +* Fix token leak via referrer header [#56](https://github.com/Sorcery/sorcery/pull/56) +* Add `login_user` helper for request specs [#57](https://github.com/Sorcery/sorcery/pull/57) ## 0.11.0 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..fa802928 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,14 @@ +# The Sorcery Community Code of Conduct + +This document provides a few simple community guidelines for a safe, respectful, +productive, and collaborative place for any person who is willing to contribute +to the Sorcery community. It applies to all "collaborative spaces", which are +defined as community communications channels (such as mailing lists, submitted +patches, commit comments, etc.). + +* Participants will be tolerant of opposing views. +* Participants must ensure that their language and actions are free of personal + attacks and disparaging personal remarks. +* When interpreting the words and actions of others, participants should always + assume good intentions. +* Behaviour which can be reasonably considered harassment will not be tolerated. diff --git a/Gemfile b/Gemfile index 7d2e1a5b..532fc3ce 100644 --- a/Gemfile +++ b/Gemfile @@ -1,8 +1,8 @@ source 'https://rubygems.org' -gem 'rails', '~> 5.0.0' +gem 'pry' +gem 'rails' gem 'rails-controller-testing' gem 'sqlite3' -gem 'pry' gemspec diff --git a/LICENSE.txt b/LICENSE.md similarity index 94% rename from LICENSE.txt rename to LICENSE.md index cb0d98c0..e43f00c5 100644 --- a/LICENSE.txt +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright (c) 2010 Noam Ben-Ari +Copyright (c) 2010 [Noam Ben-Ari](mailto:nbenari@gmail.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.md b/README.md index 7f927359..1f3b374f 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,7 @@ [![Gem Version](https://badge.fury.io/rb/sorcery.svg)](https://rubygems.org/gems/sorcery) [![Gem Downloads](https://img.shields.io/gem/dt/sorcery.svg)](https://rubygems.org/gems/sorcery) [![Build Status](https://travis-ci.org/Sorcery/sorcery.svg?branch=master)](https://travis-ci.org/Sorcery/sorcery) -[![Dependency Status](https://gemnasium.com/badges/github.com/Sorcery/sorcery.svg)](https://gemnasium.com/github.com/Sorcery/sorcery) [![Code Climate](https://codeclimate.com/github/Sorcery/sorcery.svg)](https://codeclimate.com/github/Sorcery/sorcery) -[![Inline docs](http://inch-ci.org/github/Sorcery/sorcery.svg?branch=master)](http://inch-ci.org/github/Sorcery/sorcery) [![Join the chat at https://gitter.im/Sorcery/sorcery](https://badges.gitter.im/join_chat.svg)](https://gitter.im/Sorcery/sorcery?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) Magical Authentication for Rails. Supports ActiveRecord, DataMapper, Mongoid and MongoMapper. @@ -100,7 +98,14 @@ force_forget_me! # Forgets all sessions by clearing the token, even if remember_ User.load_from_reset_password_token(token) @user.generate_reset_password_token! # Use if you want to send the email by yourself @user.deliver_reset_password_instructions! # Generates the token and sends the email -@user.change_password!(new_password) +@user.change_password(new_password) +@user.change_password!(new_password) # Same as change_password but raises exception on save +``` + +### Session Timeout + +```ruby +invalidate_active_sessions! #Invalidate all sessions with a login_time or last_action_time before the current time. Must Opt-in ``` ### User Activation @@ -184,6 +189,7 @@ Inside the initializer, the comments will tell you what each setting does. - Configurable session timeout - Optionally session timeout will be calculated from last user action +- Optionally enable a method to clear all active sessions, expects an `invalidate_sessions_before` datetime attribute. **Brute Force Protection** (see [lib/sorcery/model/submodules/brute_force_protection.rb](https://github.com/Sorcery/sorcery/blob/master/lib/sorcery/model/submodules/brute_force_protection.rb)): @@ -231,14 +237,14 @@ Feel free to ask questions using these contact details: **Current Maintainers:** -- Chase Gilliam ([@Ch4s3](https://github.com/Ch4s3)) | [Email](mailto:chase.gilliam@gmail.com) -- Josh Buker ([@athix](https://github.com/athix)) | [Email](mailto:jbuker@aeonsplice.com) +- Josh Buker ([@athix](https://github.com/athix)) | [Email](mailto:crypto+sorcery@joshbuker.com?subject=Sorcery) **Past Maintainers:** - Noam Ben-Ari ([@NoamB](https://github.com/NoamB)) | [Email](mailto:nbenari@gmail.com) | [Twitter](https://twitter.com/nbenari) - Kir Shatrov ([@kirs](https://github.com/kirs)) | [Email](mailto:shatrov@me.com) | [Twitter](https://twitter.com/Kiiiir) - Grzegorz Witek ([@arnvald](https://github.com/arnvald)) | [Email](mailto:arnvald.to@gmail.com) | [Twitter](https://twitter.com/arnvald) +- Chase Gilliam ([@Ch4s3](https://github.com/Ch4s3)) | [Email](mailto:chase.gilliam@gmail.com) ## License diff --git a/Rakefile b/Rakefile index 2470119d..d44e016c 100644 --- a/Rakefile +++ b/Rakefile @@ -1,6 +1,8 @@ require 'bundler/gem_tasks' require 'rspec/core/rake_task' +require 'rubocop/rake_task' RSpec::Core::RakeTask.new(:spec) +RuboCop::RakeTask.new -task default: :spec +task default: [:rubocop, :spec] diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..e1d53e77 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,19 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| --------- | ------------------ | +| ~> 0.16.0 | :white_check_mark: | +| ~> 0.15.0 | :white_check_mark: | +| < 0.15.0 | :x: | + +## Reporting a Vulnerability + +Email the current maintainer(s) with a description of the vulnerability. You +should expect a response within 48 hours. If the vulnerability is accepted, a +Github advisory will be created and eventually released with a CVE corresponding +to the issue found. + +A list of the current maintainers can be found on the README under the contact +section. See: [README.md](https://github.com/Sorcery/sorcery#contact) diff --git a/gemfiles/active_record-rails40.gemfile b/gemfiles/active_record-rails40.gemfile deleted file mode 100644 index 9d5aa3e0..00000000 --- a/gemfiles/active_record-rails40.gemfile +++ /dev/null @@ -1,7 +0,0 @@ -source 'https://rubygems.org' - -gem 'sqlite3', platform: :mri -gem 'activerecord-jdbcsqlite3-adapter', platform: :jruby -gem 'rails', '~> 4.0.1' - -gemspec path: '..' diff --git a/gemfiles/active_record-rails41.gemfile b/gemfiles/active_record-rails41.gemfile deleted file mode 100644 index 397ce6a7..00000000 --- a/gemfiles/active_record-rails41.gemfile +++ /dev/null @@ -1,7 +0,0 @@ -source 'https://rubygems.org' - -gem 'sqlite3', platform: :mri -gem 'activerecord-jdbcsqlite3-adapter', platform: :jruby -gem 'rails', '~> 4.1.0' - -gemspec path: '..' diff --git a/gemfiles/active_record-rails42.gemfile b/gemfiles/active_record-rails42.gemfile deleted file mode 100644 index 63c4d0f7..00000000 --- a/gemfiles/active_record-rails42.gemfile +++ /dev/null @@ -1,7 +0,0 @@ -source 'https://rubygems.org' - -gem 'sqlite3', platform: :mri -gem 'activerecord-jdbcsqlite3-adapter', platform: :jruby -gem 'rails', '~> 4.2.0' - -gemspec path: '..' diff --git a/gemfiles/rails_52.gemfile b/gemfiles/rails_52.gemfile new file mode 100644 index 00000000..ff1c7211 --- /dev/null +++ b/gemfiles/rails_52.gemfile @@ -0,0 +1,7 @@ +source 'https://rubygems.org' + +gem 'rails', '~> 5.2.0' +gem 'rails-controller-testing' +gem 'sqlite3', '~> 1.3.6' + +gemspec path: '..' diff --git a/gemfiles/rails_60.gemfile b/gemfiles/rails_60.gemfile new file mode 100644 index 00000000..cae26b8b --- /dev/null +++ b/gemfiles/rails_60.gemfile @@ -0,0 +1,7 @@ +source 'https://rubygems.org' + +gem 'rails', '~> 6.0.0' +gem 'rails-controller-testing' +gem 'sqlite3', '~> 1.4' + +gemspec path: '..' diff --git a/lib/generators/sorcery/USAGE b/lib/generators/sorcery/USAGE index 36b1a263..6e735aac 100644 --- a/lib/generators/sorcery/USAGE +++ b/lib/generators/sorcery/USAGE @@ -1,6 +1,6 @@ Description: Generates the necessary files to get you up and running with Sorcery gem - + Examples: rails generate sorcery:install diff --git a/lib/generators/sorcery/helpers.rb b/lib/generators/sorcery/helpers.rb index f0830785..3f37d0cb 100644 --- a/lib/generators/sorcery/helpers.rb +++ b/lib/generators/sorcery/helpers.rb @@ -12,6 +12,10 @@ def model_class_name options[:model] ? options[:model].classify : 'User' end + def tableized_model_class + options[:model] ? options[:model].gsub(/::/, '').tableize : 'users' + end + def model_path @model_path ||= File.join('app', 'models', "#{file_path}.rb") end diff --git a/lib/generators/sorcery/install_generator.rb b/lib/generators/sorcery/install_generator.rb index 8c9db77b..c3fefa32 100644 --- a/lib/generators/sorcery/install_generator.rb +++ b/lib/generators/sorcery/install_generator.rb @@ -7,7 +7,7 @@ class InstallGenerator < Rails::Generators::Base include Rails::Generators::Migration include Sorcery::Generators::Helpers - source_root File.expand_path('../templates', __FILE__) + source_root File.expand_path('templates', __dir__) argument :submodules, optional: true, type: :array, banner: 'submodules' @@ -21,9 +21,9 @@ class InstallGenerator < Rails::Generators::Base desc: "Specify if you want to add submodules to an existing model\n\t\t\t # (will generate migrations files, and add submodules to config file)" def check_deprecated_options - if options[:migrations] - warn('[DEPRECATED] `--migrations` option is deprecated, please use `--only-submodules` instead') - end + return unless options[:migrations] + + warn('[DEPRECATED] `--migrations` option is deprecated, please use `--only-submodules` instead') end # Copy the initializer file to config/initializers folder. @@ -33,23 +33,22 @@ def copy_initializer_file def configure_initializer_file # Add submodules to the initializer file. - if submodules - submodule_names = submodules.collect { |submodule| ':' + submodule } + return unless submodules - gsub_file sorcery_config_path, /submodules = \[.*\]/ do |str| - current_submodule_names = (str =~ /\[(.*)\]/ ? Regexp.last_match(1) : '').delete(' ').split(',') - "submodules = [#{(current_submodule_names | submodule_names).join(', ')}]" - end + submodule_names = submodules.collect { |submodule| ':' + submodule } + + gsub_file sorcery_config_path, /submodules = \[.*\]/ do |str| + current_submodule_names = (str =~ /\[(.*)\]/ ? Regexp.last_match(1) : '').delete(' ').split(',') + "submodules = [#{(current_submodule_names | submodule_names).join(', ')}]" end end def configure_model # Generate the model and add 'authenticates_with_sorcery!' unless you passed --only-submodules - unless only_submodules? - generate "model #{model_class_name} --skip-migration" + return if only_submodules? - inject_sorcery_to_model - end + generate "model #{model_class_name} --skip-migration" + inject_sorcery_to_model end def inject_sorcery_to_model @@ -61,14 +60,15 @@ def inject_sorcery_to_model # Copy the migrations files to db/migrate folder def copy_migration_files # Copy core migration file in all cases except when you pass --only-submodules. - return unless defined?(Sorcery::Generators::InstallGenerator::ActiveRecord) + return unless defined?(ActiveRecord) + migration_template 'migration/core.rb', 'db/migrate/sorcery_core.rb', migration_class_name: migration_class_name unless only_submodules? - if submodules - submodules.each do |submodule| - unless submodule == 'http_basic_auth' || submodule == 'session_timeout' || submodule == 'core' - migration_template "migration/#{submodule}.rb", "db/migrate/sorcery_#{submodule}.rb", migration_class_name: migration_class_name - end + return unless submodules + + submodules.each do |submodule| + unless %w[http_basic_auth session_timeout core].include?(submodule) + migration_template "migration/#{submodule}.rb", "db/migrate/sorcery_#{submodule}.rb", migration_class_name: migration_class_name end end end @@ -79,7 +79,7 @@ def self.next_migration_number(dirname) sleep 1 # make sure each time we get a different timestamp Time.new.utc.strftime('%Y%m%d%H%M%S') else - '%.3d' % (current_migration_number(dirname) + 1) + format('%.3d', (current_migration_number(dirname) + 1)) end end diff --git a/lib/generators/sorcery/templates/initializer.rb b/lib/generators/sorcery/templates/initializer.rb index a1338253..a026ff90 100644 --- a/lib/generators/sorcery/templates/initializer.rb +++ b/lib/generators/sorcery/templates/initializer.rb @@ -1,7 +1,9 @@ # The first thing you need to configure is which modules you need in your app. # The default is nothing which will include only core features (password encryption, login/logout). +# # Available submodules are: :user_activation, :http_basic_auth, :remember_me, -# :reset_password, :session_timeout, :brute_force_protection, :activity_logging, :external +# :reset_password, :session_timeout, :brute_force_protection, :activity_logging, +# :magic_login, :external Rails.application.config.sorcery.submodules = [] # Here you can configure each submodule's features. @@ -13,8 +15,8 @@ # # config.not_authenticated_action = - # When a non logged in user tries to enter a page that requires login, save - # the URL he wanted to reach, and send him there after login, using 'redirect_back_or_to'. + # When a non logged-in user tries to enter a page that requires login, save + # the URL he wants to reach, and send him there after login, using 'redirect_back_or_to'. # Default: `true` # # config.save_return_to_url = @@ -46,6 +48,11 @@ # # config.session_timeout_from_last_action = + # Invalidate active sessions. Requires an `invalidate_sessions_before` timestamp column + # Default: `false` + # + # config.session_timeout_invalidate_active_sessions_enabled = + # -- http_basic_auth -- # What realm to display for which controller name. For example {"My App" => "Application"} # Default: `{"application" => "Application"}` @@ -53,23 +60,24 @@ # config.controller_to_realm_map = # -- activity logging -- - # will register the time of last user login, every login. + # Will register the time of last user login, every login. # Default: `true` # # config.register_login_time = - # will register the time of last user logout, every logout. + # Will register the time of last user logout, every logout. # Default: `true` # # config.register_logout_time = - # will register the time of last user action, every action. + # Will register the time of last user action, every action. # Default: `true` # # config.register_last_activity_time = # -- external -- - # What providers are supported by this app, i.e. [:twitter, :facebook, :github, :linkedin, :xing, :google, :liveid, :salesforce, :slack] . + # What providers are supported by this app + # i.e. [:twitter, :facebook, :github, :linkedin, :xing, :google, :liveid, :salesforce, :slack, :line]. # Default: `[]` # # config.external_providers = @@ -80,16 +88,19 @@ # # config.ca_file = - # For information about LinkedIn API: - # - user info fields go to https://developer.linkedin.com/documents/profile-fields - # - access permissions go to https://developer.linkedin.com/documents/authentication#granting + # Linkedin requires r_emailaddress scope to fetch user's email address. + # You can skip including the email field if you use an intermediary signup form. (using build_from method). + # The r_emailaddress scope is only necessary if you are using the create_from method directly. # # config.linkedin.key = "" # config.linkedin.secret = "" # config.linkedin.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=linkedin" - # config.linkedin.user_info_fields = ['first-name', 'last-name'] - # config.linkedin.user_info_mapping = {first_name: "firstName", last_name: "lastName"} - # config.linkedin.access_permissions = ['r_basicprofile'] + # config.linkedin.user_info_mapping = { + # first_name: 'localizedFirstName', + # last_name: 'localizedLastName', + # email: 'emailAddress' + # } + # config.linkedin.scope = "r_liteprofile r_emailaddress" # # # For information about XING API: @@ -102,7 +113,7 @@ # # # Twitter will not accept any requests nor redirect uri containing localhost, - # make sure you use 0.0.0.0:3000 to access your app in development + # Make sure you use 0.0.0.0:3000 to access your app in development # # config.twitter.key = "" # config.twitter.secret = "" @@ -114,11 +125,17 @@ # config.facebook.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=facebook" # config.facebook.user_info_path = "me?fields=email" # config.facebook.user_info_mapping = {:email => "email"} - # config.facebook.access_permissions = ["email", "publish_actions"] + # config.facebook.access_permissions = ["email"] # config.facebook.display = "page" # config.facebook.api_version = "v2.3" # config.facebook.parse = :json # + # config.instagram.key = "" + # config.instagram.secret = "" + # config.instagram.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=instagram" + # config.instagram.user_info_mapping = {:email => "username"} + # config.instagram.access_permissions = ["basic", "public_content", "follower_list", "comments", "relationships", "likes"] + # # config.github.key = "" # config.github.secret = "" # config.github.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=github" @@ -134,6 +151,18 @@ # config.wechat.secret = "" # config.wechat.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=wechat" # + # For Auth0, site is required and should match the domain provided by Auth0. + # + # config.auth0.key = "" + # config.auth0.secret = "" + # config.auth0.callback_url = "https://0.0.0.0:3000/oauth/callback?provider=auth0" + # config.auth0.site = "https://example.auth0.com" + # + # config.qq.key = "" + # config.qq.secret = "" + # config.qq.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=qq" + # config.qq.user_info_mapping = {:username => "nickname"} + # # config.google.key = "" # config.google.secret = "" # config.google.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=google" @@ -141,7 +170,8 @@ # config.google.scope = "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile" # # For Microsoft Graph, the key will be your App ID, and the secret will be your app password/public key. - # The callback URL "can't contain a query string or invalid special characters", see: https://docs.microsoft.com/en-us/azure/active-directory/active-directory-v2-limitations#restrictions-on-redirect-uris + # The callback URL "can't contain a query string or invalid special characters" + # See: https://docs.microsoft.com/en-us/azure/active-directory/active-directory-v2-limitations#restrictions-on-redirect-uris # More information at https://graph.microsoft.io/en-us/docs # # config.microsoft.key = "" @@ -154,6 +184,7 @@ # config.vk.secret = "" # config.vk.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=vk" # config.vk.user_info_mapping = {:login => "domain", :name => "full_name"} + # config.vk.api_version = "5.71" # # config.slack.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=slack" # config.slack.key = '' @@ -171,7 +202,7 @@ # For information about JIRA API: # https://developer.atlassian.com/display/JIRADEV/JIRA+REST+API+Example+-+OAuth+authentication - # to obtain the consumer key and the public key you can use the jira-ruby gem https://github.com/sumoheavy/jira-ruby + # To obtain the consumer key and the public key you can use the jira-ruby gem https://github.com/sumoheavy/jira-ruby # or run openssl req -x509 -nodes -newkey rsa:1024 -sha1 -keyout rsakey.pem -out rsacert.pem to obtain the public key # Make sure you have configured the application link properly @@ -184,7 +215,7 @@ # For information about Salesforce API: # https://developer.salesforce.com/signup & # https://www.salesforce.com/us/developer/docs/api_rest/ - # Salesforce callback_url must be https. You can run the following to generate self-signed ssl cert + # Salesforce callback_url must be https. You can run the following to generate self-signed ssl cert: # openssl req -new -newkey rsa:2048 -sha1 -days 365 -nodes -x509 -keyout server.key -out server.crt # Make sure you have configured the application link properly # config.salesforce.key = '123123' @@ -193,179 +224,262 @@ # config.salesforce.scope = "full" # config.salesforce.user_info_mapping = {:email => "email"} + # config.line.key = "" + # config.line.secret = "" + # config.line.callback_url = "http://mydomain.com:3000/oauth/callback?provider=line" + # config.line.scope = "profile" + # config.line.bot_prompt = "normal" + # config.line.user_info_mapping = {name: 'displayName'} + + + # For information about Discord API + # https://discordapp.com/developers/docs/topics/oauth2 + # config.discord.key = "xxxxxx" + # config.discord.secret = "xxxxxx" + # config.discord.callback_url = "http://localhost:3000/oauth/callback?provider=discord" + # config.discord.scope = "email guilds" + + # For information about Battlenet API + # https://develop.battle.net/documentation/guides/using-oauth + # config.battlenet.site = "https://eu.battle.net/" #See Website for other Regional Domains + # config.battlenet.key = "xxxxxx" + # config.battlenet.secret = "xxxxxx" + # config.battlenet.callback_url = "http://localhost:3000/oauth/callback?provider=battlenet" + # config.battlenet.scope = "openid" # --- user config --- config.user_config do |user| # -- core -- - # specify username attributes, for example: [:username, :email]. + # Specify username attributes, for example: [:username, :email]. # Default: `[:email]` # # user.username_attribute_names = - # change *virtual* password attribute, the one which is used until an encrypted one is generated. + # Change *virtual* password attribute, the one which is used until an encrypted one is generated. # Default: `:password` # # user.password_attribute_name = - # downcase the username before trying to authenticate, default is false + # Downcase the username before trying to authenticate, default is false # Default: `false` # # user.downcase_username_before_authenticating = - # change default email attribute. + # Change default email attribute. # Default: `:email` # # user.email_attribute_name = - # change default crypted_password attribute. + # Change default crypted_password attribute. # Default: `:crypted_password` # # user.crypted_password_attribute_name = - # what pattern to use to join the password with the salt + # What pattern to use to join the password with the salt # Default: `""` # # user.salt_join_token = - # change default salt attribute. + # Change default salt attribute. # Default: `:salt` # # user.salt_attribute_name = - # how many times to apply encryption to the password. - # Default: `nil` + # How many times to apply encryption to the password. + # Default: 1 in test env, `nil` otherwise # - # user.stretches = + user.stretches = 1 if Rails.env.test? - # encryption key used to encrypt reversible encryptions such as AES256. + # Encryption key used to encrypt reversible encryptions such as AES256. # WARNING: If used for users' passwords, changing this key will leave passwords undecryptable! # Default: `nil` # # user.encryption_key = - # use an external encryption class. + # Use an external encryption class. # Default: `nil` # # user.custom_encryption_provider = - # encryption algorithm name. See 'encryption_algorithm=' for available options. + # Encryption algorithm name. See 'encryption_algorithm=' for available options. # Default: `:bcrypt` # # user.encryption_algorithm = - # make this configuration inheritable for subclasses. Useful for ActiveRecord's STI. + # Make this configuration inheritable for subclasses. Useful for ActiveRecord's STI. # Default: `false` # # user.subclasses_inherit_config = # -- remember_me -- + # change default remember_me_token attribute. + # Default: `:remember_me_token` + # + # user.remember_me_token_attribute_name = + + # change default remember_me_token_expires_at attribute. + # Default: `:remember_me_token_expires_at` + # + # user.remember_me_token_expires_at_attribute_name = + # How long in seconds the session length will be - # Default: `604800` + # Default: `60 * 60 * 24 * 7` # # user.remember_me_for = - # when true sorcery will persist a single remember me token for all - # logins/logouts (supporting remembering on multiple browsers simultaneously). + # When true, sorcery will persist a single remember me token for all + # logins/logouts (to support remembering on multiple browsers simultaneously). # Default: false # # user.remember_me_token_persist_globally = # -- user_activation -- - # the attribute name to hold activation state (active/pending). + # The attribute name to hold activation state (active/pending). # Default: `:activation_state` # # user.activation_state_attribute_name = - # the attribute name to hold activation code (sent by email). + # The attribute name to hold activation code (sent by email). # Default: `:activation_token` # # user.activation_token_attribute_name = - # the attribute name to hold activation code expiration date. + # The attribute name to hold activation code expiration date. # Default: `:activation_token_expires_at` # # user.activation_token_expires_at_attribute_name = - # how many seconds before the activation code expires. nil for never expires. + # How many seconds before the activation code expires. nil for never expires. # Default: `nil` # # user.activation_token_expiration_period = - # your mailer class. Required. + # REQUIRED: + # User activation mailer class. # Default: `nil` # # user.user_activation_mailer = - # when true sorcery will not automatically - # email activation details and allow you to - # manually handle how and when email is sent. + # When true, sorcery will not automatically + # send the activation details email, and allow you to + # manually handle how and when the email is sent. # Default: `false` # # user.activation_mailer_disabled = - # method to send email related + # Method to send email related # options: `:deliver_later`, `:deliver_now`, `:deliver` # Default: :deliver (Rails version < 4.2) or :deliver_now (Rails version 4.2+) # # user.email_delivery_method = - # activation needed email method on your mailer class. + # Activation needed email method on your mailer class. # Default: `:activation_needed_email` # # user.activation_needed_email_method_name = - # activation success email method on your mailer class. + # Activation success email method on your mailer class. # Default: `:activation_success_email` # # user.activation_success_email_method_name = - # do you want to prevent or allow users that did not activate by email to login? + # Do you want to prevent users who did not activate by email from logging in? # Default: `true` # # user.prevent_non_active_users_to_login = # -- reset_password -- - # reset password code attribute name. + # Password reset token attribute name. # Default: `:reset_password_token` # # user.reset_password_token_attribute_name = - # expires at attribute name. + # Password token expiry attribute name. # Default: `:reset_password_token_expires_at` # # user.reset_password_token_expires_at_attribute_name = - # when was email sent, used for hammering protection. + # When was password reset email sent. Used for hammering protection. # Default: `:reset_password_email_sent_at` # # user.reset_password_email_sent_at_attribute_name = - # mailer class. Needed. + # REQUIRED: + # Password reset mailer class. # Default: `nil` # # user.reset_password_mailer = - # reset password email method on your mailer class. + # Reset password email method on your mailer class. # Default: `:reset_password_email` # # user.reset_password_email_method_name = - # when true sorcery will not automatically - # email password reset details and allow you to - # manually handle how and when email is sent + # When true, sorcery will not automatically + # send the password reset details email, and allow you to + # manually handle how and when the email is sent # Default: `false` # # user.reset_password_mailer_disabled = - # how many seconds before the reset request expires. nil for never expires. + # How many seconds before the reset request expires. nil for never expires. # Default: `nil` # # user.reset_password_expiration_period = - # hammering protection, how long in seconds to wait before allowing another email to be sent. + # Hammering protection: how long in seconds to wait before allowing another email to be sent. # Default: `5 * 60` # # user.reset_password_time_between_emails = + # Access counter to a reset password page attribute name + # Default: `:access_count_to_reset_password_page` + # + # user.reset_password_page_access_count_attribute_name = + + # -- magic_login -- + # Magic login code attribute name. + # Default: `:magic_login_token` + # + # user.magic_login_token_attribute_name = + + # Magic login expiry attribute name. + # Default: `:magic_login_token_expires_at` + # + # user.magic_login_token_expires_at_attribute_name = + + # When was magic login email sent — used for hammering protection. + # Default: `:magic_login_email_sent_at` + # + # user.magic_login_email_sent_at_attribute_name = + + # REQUIRED: + # Magic login mailer class. + # Default: `nil` + # + # user.magic_login_mailer_class = + + # Magic login email method on your mailer class. + # Default: `:magic_login_email` + # + # user.magic_login_email_method_name = + + # When true, sorcery will not automatically + # send magic login details email, and allow you to + # manually handle how and when the email is sent + # Default: `true` + # + # user.magic_login_mailer_disabled = + + # How many seconds before the request expires. nil for never expires. + # Default: `nil` + # + # user.magic_login_expiration_period = + + # Hammering protection: how long in seconds to wait before allowing another email to be sent. + # Default: `5 * 60` + # + # user.magic_login_time_between_emails = + # -- brute_force_protection -- # Failed logins attribute name. # Default: `:failed_logins_count` @@ -377,12 +491,12 @@ # # user.lock_expires_at_attribute_name = - # How many failed logins allowed. + # How many failed logins are allowed. # Default: `50` # # user.consecutive_login_retries_amount_limit = - # How long the user should be banned. in seconds. 0 for permanent. + # How long the user should be banned, in seconds. 0 for permanent. # Default: `60 * 60` # # user.login_lock_time_period = @@ -397,16 +511,17 @@ # # user.unlock_token_email_method_name = - # when true sorcery will not automatically - # send email with unlock token + # When true, sorcery will not automatically + # send email with the unlock token # Default: `false` # # user.unlock_token_mailer_disabled = true - # Unlock token mailer class + # REQUIRED: + # Unlock token mailer class. # Default: `nil` # - # user.unlock_token_mailer = UserMailer + # user.unlock_token_mailer = # -- activity logging -- # Last login attribute name. @@ -424,7 +539,7 @@ # # user.last_activity_at_attribute_name = - # How long since last activity is the user defined logged out? + # How long since user's last activity will they be considered logged out? # Default: `10 * 60` # # user.activity_timeout = @@ -435,17 +550,17 @@ # # user.authentications_class = - # User's identifier in authentications class. + # User's identifier in the `authentications` class. # Default: `:user_id` # # user.authentications_user_id_attribute_name = - # Provider's identifier in authentications class. + # Provider's identifier in the `authentications` class. # Default: `:provider` # # user.provider_attribute_name = - # User's external unique identifier in authentications class. + # User's external unique identifier in the `authentications` class. # Default: `:uid` # # user.provider_uid_attribute_name = @@ -453,5 +568,5 @@ # This line must come after the 'user config' block. # Define which model authenticates with sorcery. - config.user_class = '<%= model_class_name %>' + config.user_class = "<%= model_class_name %>" end diff --git a/lib/generators/sorcery/templates/migration/activity_logging.rb b/lib/generators/sorcery/templates/migration/activity_logging.rb index e308ed4d..ed905ce7 100644 --- a/lib/generators/sorcery/templates/migration/activity_logging.rb +++ b/lib/generators/sorcery/templates/migration/activity_logging.rb @@ -1,10 +1,10 @@ class SorceryActivityLogging < <%= migration_class_name %> def change - add_column :<%= model_class_name.tableize %>, :last_login_at, :datetime, :default => nil - add_column :<%= model_class_name.tableize %>, :last_logout_at, :datetime, :default => nil - add_column :<%= model_class_name.tableize %>, :last_activity_at, :datetime, :default => nil - add_column :<%= model_class_name.tableize %>, :last_login_from_ip_address, :string, :default => nil + add_column :<%= tableized_model_class %>, :last_login_at, :datetime, default: nil + add_column :<%= tableized_model_class %>, :last_logout_at, :datetime, default: nil + add_column :<%= tableized_model_class %>, :last_activity_at, :datetime, default: nil + add_column :<%= tableized_model_class %>, :last_login_from_ip_address, :string, default: nil - add_index :<%= model_class_name.tableize %>, [:last_logout_at, :last_activity_at] + add_index :<%= tableized_model_class %>, [:last_logout_at, :last_activity_at] end end diff --git a/lib/generators/sorcery/templates/migration/brute_force_protection.rb b/lib/generators/sorcery/templates/migration/brute_force_protection.rb index 33cf6390..ea549180 100644 --- a/lib/generators/sorcery/templates/migration/brute_force_protection.rb +++ b/lib/generators/sorcery/templates/migration/brute_force_protection.rb @@ -1,9 +1,9 @@ class SorceryBruteForceProtection < <%= migration_class_name %> def change - add_column :<%= model_class_name.tableize %>, :failed_logins_count, :integer, :default => 0 - add_column :<%= model_class_name.tableize %>, :lock_expires_at, :datetime, :default => nil - add_column :<%= model_class_name.tableize %>, :unlock_token, :string, :default => nil + add_column :<%= tableized_model_class %>, :failed_logins_count, :integer, default: 0 + add_column :<%= tableized_model_class %>, :lock_expires_at, :datetime, default: nil + add_column :<%= tableized_model_class %>, :unlock_token, :string, default: nil - add_index :<%= model_class_name.tableize %>, :unlock_token + add_index :<%= tableized_model_class %>, :unlock_token end end diff --git a/lib/generators/sorcery/templates/migration/core.rb b/lib/generators/sorcery/templates/migration/core.rb index af5f721c..b1ce82d8 100644 --- a/lib/generators/sorcery/templates/migration/core.rb +++ b/lib/generators/sorcery/templates/migration/core.rb @@ -1,13 +1,11 @@ class SorceryCore < <%= migration_class_name %> def change - create_table :<%= model_class_name.tableize %> do |t| - t.string :email, :null => false + create_table :<%= tableized_model_class %> do |t| + t.string :email, null: false, index: { unique: true } t.string :crypted_password t.string :salt - t.timestamps :null => false + t.timestamps null: false end - - add_index :<%= model_class_name.tableize %>, :email, unique: true end end diff --git a/lib/generators/sorcery/templates/migration/external.rb b/lib/generators/sorcery/templates/migration/external.rb index 8ac7510b..e8b7a960 100644 --- a/lib/generators/sorcery/templates/migration/external.rb +++ b/lib/generators/sorcery/templates/migration/external.rb @@ -1,10 +1,10 @@ class SorceryExternal < <%= migration_class_name %> def change create_table :authentications do |t| - t.integer :<%= model_class_name.tableize.singularize %>_id, :null => false - t.string :provider, :uid, :null => false + t.integer :<%= tableized_model_class.singularize %>_id, null: false + t.string :provider, :uid, null: false - t.timestamps :null => false + t.timestamps null: false end add_index :authentications, [:provider, :uid] diff --git a/lib/generators/sorcery/templates/migration/magic_login.rb b/lib/generators/sorcery/templates/migration/magic_login.rb new file mode 100644 index 00000000..73d4aff2 --- /dev/null +++ b/lib/generators/sorcery/templates/migration/magic_login.rb @@ -0,0 +1,9 @@ +class SorceryMagicLogin < <%= migration_class_name %> + def change + add_column :<%= tableized_model_class %>, :magic_login_token, :string, default: nil + add_column :<%= tableized_model_class %>, :magic_login_token_expires_at, :datetime, default: nil + add_column :<%= tableized_model_class %>, :magic_login_email_sent_at, :datetime, default: nil + + add_index :<%= tableized_model_class %>, :magic_login_token + end +end diff --git a/lib/generators/sorcery/templates/migration/remember_me.rb b/lib/generators/sorcery/templates/migration/remember_me.rb index 605660a3..37370536 100644 --- a/lib/generators/sorcery/templates/migration/remember_me.rb +++ b/lib/generators/sorcery/templates/migration/remember_me.rb @@ -1,8 +1,8 @@ class SorceryRememberMe < <%= migration_class_name %> def change - add_column :<%= model_class_name.tableize %>, :remember_me_token, :string, :default => nil - add_column :<%= model_class_name.tableize %>, :remember_me_token_expires_at, :datetime, :default => nil + add_column :<%= tableized_model_class %>, :remember_me_token, :string, default: nil + add_column :<%= tableized_model_class %>, :remember_me_token_expires_at, :datetime, default: nil - add_index :<%= model_class_name.tableize %>, :remember_me_token + add_index :<%= tableized_model_class %>, :remember_me_token end end diff --git a/lib/generators/sorcery/templates/migration/reset_password.rb b/lib/generators/sorcery/templates/migration/reset_password.rb index cf00c0b8..708f6c11 100644 --- a/lib/generators/sorcery/templates/migration/reset_password.rb +++ b/lib/generators/sorcery/templates/migration/reset_password.rb @@ -1,9 +1,10 @@ class SorceryResetPassword < <%= migration_class_name %> def change - add_column :<%= model_class_name.tableize %>, :reset_password_token, :string, :default => nil - add_column :<%= model_class_name.tableize %>, :reset_password_token_expires_at, :datetime, :default => nil - add_column :<%= model_class_name.tableize %>, :reset_password_email_sent_at, :datetime, :default => nil + add_column :<%= tableized_model_class %>, :reset_password_token, :string, default: nil + add_column :<%= tableized_model_class %>, :reset_password_token_expires_at, :datetime, default: nil + add_column :<%= tableized_model_class %>, :reset_password_email_sent_at, :datetime, default: nil + add_column :<%= tableized_model_class %>, :access_count_to_reset_password_page, :integer, default: 0 - add_index :<%= model_class_name.tableize %>, :reset_password_token + add_index :<%= tableized_model_class %>, :reset_password_token end end diff --git a/lib/generators/sorcery/templates/migration/user_activation.rb b/lib/generators/sorcery/templates/migration/user_activation.rb index c3a56547..f115286f 100644 --- a/lib/generators/sorcery/templates/migration/user_activation.rb +++ b/lib/generators/sorcery/templates/migration/user_activation.rb @@ -1,9 +1,9 @@ class SorceryUserActivation < <%= migration_class_name %> def change - add_column :<%= model_class_name.tableize %>, :activation_state, :string, :default => nil - add_column :<%= model_class_name.tableize %>, :activation_token, :string, :default => nil - add_column :<%= model_class_name.tableize %>, :activation_token_expires_at, :datetime, :default => nil + add_column :<%= tableized_model_class %>, :activation_state, :string, default: nil + add_column :<%= tableized_model_class %>, :activation_token, :string, default: nil + add_column :<%= tableized_model_class %>, :activation_token_expires_at, :datetime, default: nil - add_index :<%= model_class_name.tableize %>, :activation_token + add_index :<%= tableized_model_class %>, :activation_token end end diff --git a/lib/sorcery.rb b/lib/sorcery.rb index e0df9312..2a0af8cc 100644 --- a/lib/sorcery.rb +++ b/lib/sorcery.rb @@ -18,6 +18,7 @@ module Submodules require 'sorcery/model/submodules/activity_logging' require 'sorcery/model/submodules/brute_force_protection' require 'sorcery/model/submodules/external' + require 'sorcery/model/submodules/magic_login' end end @@ -56,6 +57,7 @@ module TestHelpers module Rails require 'sorcery/test_helpers/rails/controller' require 'sorcery/test_helpers/rails/integration' + require 'sorcery/test_helpers/rails/request' end module Internal diff --git a/lib/sorcery/adapters/active_record_adapter.rb b/lib/sorcery/adapters/active_record_adapter.rb index 266031e7..dac9facf 100644 --- a/lib/sorcery/adapters/active_record_adapter.rb +++ b/lib/sorcery/adapters/active_record_adapter.rb @@ -6,12 +6,13 @@ def update_attributes(attrs) @model.send(:"#{name}=", value) end primary_key = @model.class.primary_key - @model.class.where(:"#{primary_key}" => @model.send(:"#{primary_key}")).update_all(attrs) + updated_count = @model.class.where(:"#{primary_key}" => @model.send(:"#{primary_key}")).update_all(attrs) + updated_count == 1 end def save(options = {}) mthd = options.delete(:raise_on_failure) ? :save! : :save - @model.send(mthd, options) + @model.send(mthd, **options) end def increment(field) @@ -34,7 +35,7 @@ def define_field(name, type, options = {}) end def define_callback(time, event, method_name, options = {}) - @klass.send "#{time}_#{event}", method_name, options.slice(:if) + @klass.send "#{time}_#{event}", method_name, **options.slice(:if, :on) end def find_by_oauth_credentials(provider, uid) diff --git a/lib/sorcery/adapters/mongoid_adapter.rb b/lib/sorcery/adapters/mongoid_adapter.rb index 9009c335..438a5095 100644 --- a/lib/sorcery/adapters/mongoid_adapter.rb +++ b/lib/sorcery/adapters/mongoid_adapter.rb @@ -10,7 +10,7 @@ def update_attributes(attrs) attrs[name] = value.utc if value.is_a?(ActiveSupport::TimeWithZone) @model.send(:"#{name}=", value) end - @model.class.where(:_id => @model.id).update_all(attrs) + @model.class.where(_id: @model.id).update_all(attrs) end def update_attribute(name, value) @@ -23,21 +23,29 @@ def save(options = {}) end def mongoid_4? - Gem::Version.new(::Mongoid::VERSION) >= Gem::Version.new("4.0.0.alpha") + Gem::Version.new(::Mongoid::VERSION) >= Gem::Version.new('4.0.0.alpha') end class << self - - def define_field(name, type, options={}) + def define_field(name, type, options = {}) @klass.field name, options.slice(:default).merge(type: type) end - def define_callback(time, event, method_name, options={}) - @klass.send "#{time}_#{event}", method_name, options.slice(:if) + def define_callback(time, event, method_name, options = {}) + @klass.send callback_name(time, event, options), method_name, **options.slice(:if) + end + + def callback_name(time, event, options) + if event == :commit + options[:on] == :create ? "#{time}_create" : "#{time}_save" + else + "#{time}_#{event}" + end end def credential_regex(credential) - return { :$regex => /^#{Regexp.escape(credential)}$/i } if (@klass.sorcery_config.downcase_username_before_authenticating) + return { :$regex => /^#{Regexp.escape(credential)}$/i } if @klass.sorcery_config.downcase_username_before_authenticating + credential end @@ -73,7 +81,7 @@ def find_by_id(id) end def find_by_username(username) - query = @klass.sorcery_config.username_attribute_names.map {|name| {name => username}} + query = @klass.sorcery_config.username_attribute_names.map { |name| { name => username } } @klass.any_of(*query).first end @@ -87,9 +95,13 @@ def find_by_email(email) def get_current_users config = @klass.sorcery_config - @klass.where(config.last_activity_at_attribute_name.ne => nil) \ - .where("this.#{config.last_logout_at_attribute_name} == null || this.#{config.last_activity_at_attribute_name} > this.#{config.last_logout_at_attribute_name}") \ - .where(config.last_activity_at_attribute_name.gt => config.activity_timeout.seconds.ago.utc).order_by([:_id,:asc]) + @klass.where( + config.last_activity_at_attribute_name.ne => nil + ).where( + "this.#{config.last_logout_at_attribute_name} == null || this.#{config.last_activity_at_attribute_name} > this.#{config.last_logout_at_attribute_name}" + ).where( + config.last_activity_at_attribute_name.gt => config.activity_timeout.seconds.ago.utc + ).order_by(%i[_id asc]) end end end diff --git a/lib/sorcery/controller.rb b/lib/sorcery/controller.rb index 8b3010d1..ba4a69fd 100644 --- a/lib/sorcery/controller.rb +++ b/lib/sorcery/controller.rb @@ -4,11 +4,14 @@ def self.included(klass) klass.class_eval do include InstanceMethods Config.submodules.each do |mod| + # FIXME: Is there a cleaner way to handle missing submodules? + # rubocop:disable Lint/HandleExceptions begin include Submodules.const_get(mod.to_s.split('_').map(&:capitalize).join) rescue NameError # don't stop on a missing submodule. end + # rubocop:enable Lint/HandleExceptions end end Config.update! @@ -20,10 +23,13 @@ module InstanceMethods # Will trigger auto-login attempts via the call to logged_in? # If all attempts to auto-login fail, the failure callback will be called. def require_login - unless logged_in? - session[:return_to_url] = request.url if Config.save_return_to_url && request.get? && !request.xhr? - send(Config.not_authenticated_action) + return if logged_in? + + if Config.save_return_to_url && request.get? && !request.xhr? && !request.format.json? + session[:return_to_url] = request.url end + + send(Config.not_authenticated_action) end # Takes credentials and returns a user on successful authentication. @@ -37,7 +43,10 @@ def login(*credentials) yield(user, failure_reason) if block_given? + # FIXME: Does using `break` or `return nil` change functionality? + # rubocop:disable Lint/NonLocalExitFromIterator return + # rubocop:enable Lint/NonLocalExitFromIterator end old_session = session.dup.to_hash @@ -47,30 +56,26 @@ def login(*credentials) end form_authenticity_token - auto_login(user) + auto_login(user, credentials[2]) after_login!(user, credentials) block_given? ? yield(current_user, nil) : current_user end end - # put this into the catch block to rescue undefined method `destroy_session' - # hotfix for https://github.com/NoamB/sorcery/issues/464 - # can be removed when Rails 4.1 is out def reset_sorcery_session reset_session # protect from session fixation attacks - rescue NoMethodError end # Resets the session and runs hooks before and after. def logout - if logged_in? - user = current_user - before_logout! - @current_user = nil - reset_sorcery_session - after_logout!(user) - end + return unless logged_in? + + user = current_user + before_logout! + @current_user = nil + reset_sorcery_session + after_logout!(user) end def logged_in? @@ -153,8 +158,14 @@ def after_logout!(user) Config.after_logout.each { |c| send(c, user) } end + def after_remember_me!(user) + Config.after_remember_me.each { |c| send(c, user) } + end + def user_class @user_class ||= Config.user_class.to_s.constantize + rescue NameError + raise ArgumentError, 'You have incorrectly defined user_class or have forgotten to define it in intitializer file (config.user_class = \'User\').' end end end diff --git a/lib/sorcery/controller/config.rb b/lib/sorcery/controller/config.rb index 99e1f3fb..1a9bf112 100644 --- a/lib/sorcery/controller/config.rb +++ b/lib/sorcery/controller/config.rb @@ -18,17 +18,19 @@ class << self attr_accessor :after_failed_login attr_accessor :before_logout attr_accessor :after_logout + attr_accessor :after_remember_me def init! @defaults = { :@user_class => nil, :@submodules => [], :@not_authenticated_action => :not_authenticated, - :@login_sources => [], - :@after_login => [], - :@after_failed_login => [], - :@before_logout => [], - :@after_logout => [], + :@login_sources => Set.new, + :@after_login => Set.new, + :@after_failed_login => Set.new, + :@before_logout => Set.new, + :@after_logout => Set.new, + :@after_remember_me => Set.new, :@save_return_to_url => true, :@cookie_domain => nil } diff --git a/lib/sorcery/controller/submodules/activity_logging.rb b/lib/sorcery/controller/submodules/activity_logging.rb index 5aaf578c..2339b0a4 100644 --- a/lib/sorcery/controller/submodules/activity_logging.rb +++ b/lib/sorcery/controller/submodules/activity_logging.rb @@ -30,9 +30,11 @@ def merge_activity_logging_defaults! end merge_activity_logging_defaults! end - Config.after_login << :register_login_time_to_db - Config.after_login << :register_last_ip_address - Config.before_logout << :register_logout_time_to_db + + Config.after_login << :register_login_time_to_db + Config.after_login << :register_last_ip_address + Config.before_logout << :register_logout_time_to_db + base.after_action :register_last_activity_time_to_db end @@ -43,6 +45,7 @@ module InstanceMethods # This runs as a hook just after a successful login. def register_login_time_to_db(user, _credentials) return unless Config.register_login_time + user.set_last_login_at(Time.now.in_time_zone) end @@ -50,6 +53,7 @@ def register_login_time_to_db(user, _credentials) # This runs as a hook just before a logout. def register_logout_time_to_db return unless Config.register_logout_time + current_user.set_last_logout_at(Time.now.in_time_zone) end @@ -58,6 +62,7 @@ def register_logout_time_to_db def register_last_activity_time_to_db return unless Config.register_last_activity_time return unless logged_in? + current_user.set_last_activity_at(Time.now.in_time_zone) end @@ -65,6 +70,7 @@ def register_last_activity_time_to_db # This runs as a hook just after a successful login. def register_last_ip_address(_user, _credentials) return unless Config.register_last_ip_address + current_user.set_last_ip_address(request.remote_ip) end end diff --git a/lib/sorcery/controller/submodules/external.rb b/lib/sorcery/controller/submodules/external.rb index fcff2d30..54074c42 100644 --- a/lib/sorcery/controller/submodules/external.rb +++ b/lib/sorcery/controller/submodules/external.rb @@ -22,7 +22,13 @@ def self.included(base) require 'sorcery/providers/paypal' require 'sorcery/providers/slack' require 'sorcery/providers/wechat' + require 'sorcery/providers/qq' require 'sorcery/providers/microsoft' + require 'sorcery/providers/instagram' + require 'sorcery/providers/auth0' + require 'sorcery/providers/line' + require 'sorcery/providers/discord' + require 'sorcery/providers/battlenet' Config.module_eval do class << self @@ -33,17 +39,17 @@ def external_providers=(providers) @external_providers = providers providers.each do |name| - class_eval <<-E + class_eval <<-RUBY, __FILE__, __LINE__ + 1 def self.#{name} - @#{name} ||= Sorcery::Providers.const_get('#{name}'.to_s.capitalize).new + @#{name} ||= Sorcery::Providers.const_get('#{name}'.to_s.classify).new end - E + RUBY end end def merge_external_defaults! @defaults.merge!(:@external_providers => [], - :@ca_file => File.join(File.expand_path(File.dirname(__FILE__)), '../../protocols/certs/ca-bundle.crt')) + :@ca_file => File.join(__dir__, '../../protocols/certs/ca-bundle.crt')) end end merge_external_defaults! @@ -56,6 +62,7 @@ module InstanceMethods # save the singleton ProviderClient instance into @provider def sorcery_get_provider(provider_name) return unless Config.external_providers.include?(provider_name.to_sym) + Config.send(provider_name.to_sym) end @@ -64,12 +71,11 @@ def sorcery_get_provider(provider_name) def sorcery_login_url(provider_name, args = {}) @provider = sorcery_get_provider provider_name sorcery_fixup_callback_url @provider - if @provider.respond_to?(:login_url) && @provider.has_callback? - @provider.state = args[:state] - return @provider.login_url(params, session) - else - return nil - end + + return nil unless @provider.respond_to?(:login_url) && @provider.has_callback? + + @provider.state = args[:state] + @provider.login_url(params, session) end # get the user hash from a provider using information from the params and session. @@ -88,6 +94,7 @@ def sorcery_fetch_user_hash(provider_name) # cache them in instance variables. @access_token ||= @provider.process_callback(params, session) # sends request to oauth agent to get the token @user_hash ||= @provider.get_user_hash(@access_token) # uses the token to send another request to the oauth agent requesting user info + nil end # for backwards compatibility @@ -98,46 +105,47 @@ def access_token(*_args) # this method should be somewhere else. It only does something once per application per provider. def sorcery_fixup_callback_url(provider) provider.original_callback_url ||= provider.callback_url - if provider.original_callback_url.present? && provider.original_callback_url[0] == '/' - uri = URI.parse(request.url.gsub(/\?.*$/, '')) - uri.path = '' - uri.query = nil - uri.scheme = 'https' if request.env['HTTP_X_FORWARDED_PROTO'] == 'https' - host = uri.to_s - provider.callback_url = "#{host}#{@provider.original_callback_url}" - end + + return unless provider.original_callback_url.present? && provider.original_callback_url[0] == '/' + + uri = URI.parse(request.url.gsub(/\?.*$/, '')) + uri.path = '' + uri.query = nil + uri.scheme = 'https' if request.env['HTTP_X_FORWARDED_PROTO'] == 'https' + host = uri.to_s + provider.callback_url = "#{host}#{@provider.original_callback_url}" end # sends user to authenticate at the provider's website. # after authentication the user is redirected to the callback defined in the provider config def login_at(provider_name, args = {}) - redirect_to sorcery_login_url(provider_name, args) + redirect_to sorcery_login_url(provider_name, args), allow_other_host: true end # tries to login the user from provider's callback def login_from(provider_name, should_remember = false) sorcery_fetch_user_hash provider_name - if user = user_class.load_from_provider(provider_name, @user_hash[:uid].to_s) - # we found the user. - # clear the session - return_to_url = session[:return_to_url] - reset_sorcery_session - session[:return_to_url] = return_to_url + return unless (user = user_class.load_from_provider(provider_name, @user_hash[:uid].to_s)) - # sign in the user - auto_login(user, should_remember) - after_login!(user) + # we found the user. + # clear the session + return_to_url = session[:return_to_url] + reset_sorcery_session + session[:return_to_url] = return_to_url - # return the user - user - end + # sign in the user + auto_login(user, should_remember) + after_login!(user) + + # return the user + user end # If user is logged, he can add all available providers into his account def add_provider_to_user(provider_name) sorcery_fetch_user_hash provider_name - config = user_class.sorcery_config + # config = user_class.sorcery_config # TODO: Unused, remove? current_user.add_provider_to_user(provider_name.to_s, @user_hash[:uid].to_s) end @@ -181,7 +189,7 @@ def create_and_validate_from(provider_name) # def create_from(provider_name, &block) sorcery_fetch_user_hash provider_name - config = user_class.sorcery_config + # config = user_class.sorcery_config # TODO: Unused, remove? attrs = user_attrs(@provider.user_info_mapping, @user_hash) @user = user_class.create_from_provider(provider_name, @user_hash[:uid], attrs, &block) @@ -190,7 +198,7 @@ def create_from(provider_name, &block) # follows the same patterns as create_from, but builds the user instead of creating def build_from(provider_name, &block) sorcery_fetch_user_hash provider_name - config = user_class.sorcery_config + # config = user_class.sorcery_config # TODO: Unused, remove? attrs = user_attrs(@provider.user_info_mapping, @user_hash) @user = user_class.build_from_provider(attrs, &block) @@ -202,7 +210,7 @@ def user_attrs(user_info_mapping, user_hash) if (varr = v.split('/')).size > 1 attribute_value = begin varr.inject(user_hash[:user_info]) { |hash, value| hash[value] } - rescue + rescue StandardError nil end attribute_value.nil? ? attrs : attrs.merge!(k => attribute_value) diff --git a/lib/sorcery/controller/submodules/http_basic_auth.rb b/lib/sorcery/controller/submodules/http_basic_auth.rb index 672615f3..0f4ec34c 100644 --- a/lib/sorcery/controller/submodules/http_basic_auth.rb +++ b/lib/sorcery/controller/submodules/http_basic_auth.rb @@ -19,6 +19,7 @@ def merge_http_basic_auth_defaults! end merge_http_basic_auth_defaults! end + Config.login_sources << :login_from_basic_auth end @@ -57,6 +58,7 @@ def realm_name_by_controller while current_controller != ActionController::Base result = Config.controller_to_realm_map[current_controller.controller_name] return result if result + current_controller = current_controller.superclass end nil diff --git a/lib/sorcery/controller/submodules/remember_me.rb b/lib/sorcery/controller/submodules/remember_me.rb index 48ccce9e..e74daced 100644 --- a/lib/sorcery/controller/submodules/remember_me.rb +++ b/lib/sorcery/controller/submodules/remember_me.rb @@ -17,8 +17,8 @@ def merge_remember_me_defaults! end merge_remember_me_defaults! end + Config.login_sources << :login_from_cookie - Config.after_login << :remember_me_if_asked_to Config.before_logout << :forget_me! end @@ -51,20 +51,15 @@ def auto_login(user, should_remember = false) protected - # calls remember_me! if a third credential was passed to the login method. - # Runs as a hook after login. - def remember_me_if_asked_to(_user, credentials) - remember_me! if credentials.size == 3 && credentials[2] && credentials[2] != '0' - end - # Checks the cookie for a remember me token, tried to find a user with that token # and logs the user in if found. # Runs as a login source. See 'current_user' method for how it is used. def login_from_cookie - user = cookies.signed[:remember_me_token] && user_class.sorcery_adapter.find_by_remember_me_token(cookies.signed[:remember_me_token]) + user = cookies.signed[:remember_me_token] && user_class.sorcery_adapter.find_by_remember_me_token(cookies.signed[:remember_me_token]) if defined? cookies if user && user.has_remember_me_token? set_remember_me_cookie!(user) session[:user_id] = user.id.to_s + after_remember_me!(user) @current_user = user else @current_user = false diff --git a/lib/sorcery/controller/submodules/session_timeout.rb b/lib/sorcery/controller/submodules/session_timeout.rb index 79185c4b..f02b221f 100644 --- a/lib/sorcery/controller/submodules/session_timeout.rb +++ b/lib/sorcery/controller/submodules/session_timeout.rb @@ -12,24 +12,38 @@ class << self attr_accessor :session_timeout # use the last action as the beginning of session timeout. attr_accessor :session_timeout_from_last_action + # allow users to invalidate active sessions + attr_accessor :session_timeout_invalidate_active_sessions_enabled def merge_session_timeout_defaults! - @defaults.merge!(:@session_timeout => 3600, # 1.hour - :@session_timeout_from_last_action => false) + @defaults.merge!(:@session_timeout => 3600, # 1.hour + :@session_timeout_from_last_action => false, + :@session_timeout_invalidate_active_sessions_enabled => false) end end merge_session_timeout_defaults! end + Config.after_login << :register_login_time + Config.after_remember_me << :register_login_time + base.prepend_before_action :validate_session end module InstanceMethods + def invalidate_active_sessions! + return unless Config.session_timeout_invalidate_active_sessions_enabled + return unless current_user.present? + + current_user.send(:invalidate_sessions_before=, Time.now.in_time_zone) + current_user.save + end + protected # Registers last login to be used as the timeout starting point. # Runs as a hook after a successful login. - def register_login_time(_user, _credentials) + def register_login_time(_user, _credentials = nil) session[:login_time] = session[:last_action_time] = Time.now.in_time_zone end @@ -37,9 +51,9 @@ def register_login_time(_user, _credentials) # To be used as a before_action, before require_login def validate_session session_to_use = Config.session_timeout_from_last_action ? session[:last_action_time] : session[:login_time] - if session_to_use && sorcery_session_expired?(session_to_use.to_time) + if (session_to_use && sorcery_session_expired?(session_to_use.to_time)) || sorcery_session_invalidated? reset_sorcery_session - @current_user = nil + remove_instance_variable :@current_user if defined? @current_user else session[:last_action_time] = Time.now.in_time_zone end @@ -48,6 +62,15 @@ def validate_session def sorcery_session_expired?(time) Time.now.in_time_zone - time > Config.session_timeout end + + # Use login time if present, otherwise use last action time. + def sorcery_session_invalidated? + return false unless Config.session_timeout_invalidate_active_sessions_enabled + return false unless current_user.present? && current_user.try(:invalidate_sessions_before).present? + + time = session[:login_time] || session[:last_action_time] || Time.now.in_time_zone + time < current_user.invalidate_sessions_before + end end end end diff --git a/lib/sorcery/crypto_providers/aes256.rb b/lib/sorcery/crypto_providers/aes256.rb index 616f0f92..4fc542da 100644 --- a/lib/sorcery/crypto_providers/aes256.rb +++ b/lib/sorcery/crypto_providers/aes256.rb @@ -29,7 +29,7 @@ def encrypt(*tokens) def matches?(crypted, *tokens) decrypt(crypted) == tokens.join - rescue OpenSSL::CipherError + rescue OpenSSL::Cipher::CipherError false end @@ -43,6 +43,7 @@ def decrypt(crypted) def aes raise ArgumentError, "#{name} expects a 32 bytes long key. Please use Sorcery::Model::Config.encryption_key to set it." if @key.nil? || @key == '' + @aes ||= OpenSSL::Cipher.new('AES-256-ECB') end end diff --git a/lib/sorcery/crypto_providers/bcrypt.rb b/lib/sorcery/crypto_providers/bcrypt.rb index 4c1a5655..bf2b5d8c 100644 --- a/lib/sorcery/crypto_providers/bcrypt.rb +++ b/lib/sorcery/crypto_providers/bcrypt.rb @@ -40,6 +40,10 @@ module CryptoProviders # You are good to go! class BCrypt class << self + # Setting the option :pepper allows users to append an app-specific secret token. + # Basically it's equivalent to :salt_join_token option, but have a different name to ensure + # backward compatibility in generating/matching passwords. + attr_accessor :pepper # This is the :cost option for the BCrpyt library. # The higher the cost the more secure it is and the longer is take the generate a hash. By default this is 10. # Set this to whatever you want, play around with it to get that perfect balance between @@ -60,6 +64,7 @@ def encrypt(*tokens) def matches?(hash, *tokens) hash = new_from_hash(hash) return false if hash.nil? || hash == {} + hash == join_tokens(tokens) end @@ -76,18 +81,19 @@ def cost_matches?(hash) def reset! @cost = 10 + @pepper = '' end private def join_tokens(tokens) - tokens.flatten.join + tokens.flatten.join.concat(pepper.to_s) # make sure to add pepper in case tokens have only one element end def new_from_hash(hash) ::BCrypt::Password.new(hash) rescue ::BCrypt::Errors::InvalidHash - return nil + nil end end end diff --git a/lib/sorcery/engine.rb b/lib/sorcery/engine.rb index 8042cf9a..eb5de9b3 100644 --- a/lib/sorcery/engine.rb +++ b/lib/sorcery/engine.rb @@ -7,10 +7,23 @@ module Sorcery class Engine < Rails::Engine config.sorcery = ::Sorcery::Controller::Config + # TODO: Should this include a modified version of the helper methods? initializer 'extend Controller with sorcery' do - ActionController::Base.send(:include, Sorcery::Controller) - ActionController::Base.helper_method :current_user - ActionController::Base.helper_method :logged_in? + # FIXME: on_load is needed to fix Rails 6 deprecations, but it breaks + # applications due to undefined method errors. + # ActiveSupport.on_load(:action_controller_api) do + if defined?(ActionController::API) + ActionController::API.send(:include, Sorcery::Controller) + end + + # FIXME: on_load is needed to fix Rails 6 deprecations, but it breaks + # applications due to undefined method errors. + # ActiveSupport.on_load(:action_controller_base) do + if defined?(ActionController::Base) + ActionController::Base.send(:include, Sorcery::Controller) + ActionController::Base.helper_method :current_user + ActionController::Base.helper_method :logged_in? + end end end end diff --git a/lib/sorcery/model.rb b/lib/sorcery/model.rb index c36fb2fd..6a500fc4 100644 --- a/lib/sorcery/model.rb +++ b/lib/sorcery/model.rb @@ -47,12 +47,15 @@ def include_required_submodules! class_eval do @sorcery_config.submodules = ::Sorcery::Controller::Config.submodules @sorcery_config.submodules.each do |mod| + # TODO: Is there a cleaner way to handle missing submodules? + # rubocop:disable Lint/HandleExceptions begin include Submodules.const_get(mod.to_s.split('_').map(&:capitalize).join) rescue NameError # don't stop on a missing submodule. Needed because some submodules are only defined # in the controller side. end + # rubocop:enable Lint/HandleExceptions end end end @@ -99,10 +102,6 @@ def authenticate(*credentials, &block) set_encryption_attributes - unless user.valid_password?(credentials[1]) - return authentication_response(user: user, failure: :invalid_password, &block) - end - if user.respond_to?(:active_for_authentication?) && !user.active_for_authentication? return authentication_response(user: user, failure: :inactive, &block) end @@ -115,6 +114,10 @@ def authenticate(*credentials, &block) end end + unless user.valid_password?(credentials[1]) + return authentication_response(user: user, failure: :invalid_password, &block) + end + authentication_response(user: user, return_value: user, &block) end @@ -139,6 +142,7 @@ def authentication_response(options = {}) def set_encryption_attributes @sorcery_config.encryption_provider.stretches = @sorcery_config.stretches if @sorcery_config.encryption_provider.respond_to?(:stretches) && @sorcery_config.stretches @sorcery_config.encryption_provider.join_token = @sorcery_config.salt_join_token if @sorcery_config.encryption_provider.respond_to?(:join_token) && @sorcery_config.salt_join_token + @sorcery_config.encryption_provider.pepper = @sorcery_config.pepper if @sorcery_config.encryption_provider.respond_to?(:pepper) && @sorcery_config.pepper end def add_config_inheritance @@ -192,9 +196,9 @@ def clear_virtual_password config = sorcery_config send(:"#{config.password_attribute_name}=", nil) - if respond_to?(:"#{config.password_attribute_name}_confirmation=") - send(:"#{config.password_attribute_name}_confirmation=", nil) - end + return unless respond_to?(:"#{config.password_attribute_name}_confirmation=") + + send(:"#{config.password_attribute_name}_confirmation=", nil) end # calls the requested email method on the configured mailer @@ -202,9 +206,9 @@ def clear_virtual_password def generic_send_email(method, mailer) config = sorcery_config mail = config.send(mailer).send(config.send(method), self) - if defined?(ActionMailer) && config.send(mailer).is_a?(Class) && config.send(mailer) < ActionMailer::Base - mail.send(config.email_delivery_method) - end + return unless mail.respond_to?(config.email_delivery_method) + + mail.send(config.email_delivery_method) end end end diff --git a/lib/sorcery/model/config.rb b/lib/sorcery/model/config.rb index 0bad9332..976e466d 100644 --- a/lib/sorcery/model/config.rb +++ b/lib/sorcery/model/config.rb @@ -4,8 +4,6 @@ module Sorcery module Model class Config - # change default username attribute, for example, to use :email as the login. - attr_accessor :username_attribute_names # change *virtual* password attribute, the one which is used until an encrypted one is generated. attr_accessor :password_attribute_name # change default email attribute. @@ -14,7 +12,11 @@ class Config attr_accessor :downcase_username_before_authenticating # change default crypted_password attribute. attr_accessor :crypted_password_attribute_name + # application-specific secret token that is joined with the password and its salt. + # Currently available with BCrypt (default crypt provider) only. + attr_accessor :pepper # what pattern to use to join the password with the salt + # APPLICABLE TO MD5, SHA1, SHA256, SHA512. Other crypt providers (incl. BCrypt) ignore this parameter. attr_accessor :salt_join_token # change default salt attribute. attr_accessor :salt_attribute_name @@ -38,6 +40,8 @@ class Config # Set token randomness attr_accessor :token_randomness + # change default username attribute, for example, to use :email as the login. See 'username_attribute_names=' below. + attr_reader :username_attribute_names # change default encryption_provider. attr_reader :encryption_provider # use an external encryption class. @@ -57,6 +61,7 @@ def initialize :@encryption_provider => CryptoProviders::BCrypt, :@custom_encryption_provider => nil, :@encryption_key => nil, + :@pepper => '', :@salt_join_token => '', :@salt_attribute_name => :salt, :@stretches => nil, @@ -96,7 +101,7 @@ def encryption_algorithm=(algo) when :bcrypt then CryptoProviders::BCrypt when :custom then @custom_encryption_provider else raise ArgumentError, "Encryption algorithm supplied, #{algo}, is invalid" - end + end end private diff --git a/lib/sorcery/model/submodules/brute_force_protection.rb b/lib/sorcery/model/submodules/brute_force_protection.rb index 51950e38..18c55074 100644 --- a/lib/sorcery/model/submodules/brute_force_protection.rb +++ b/lib/sorcery/model/submodules/brute_force_protection.rb @@ -14,7 +14,6 @@ def self.included(base) :consecutive_login_retries_amount_limit, # how many failed logins allowed. :login_lock_time_period, # how long the user should be banned. # in seconds. 0 for permanent. - :unlock_token_attribute_name, # Unlock token attribute name :unlock_token_email_method_name, # Mailer method name :unlock_token_mailer_disabled, # When true, dont send unlock token via email @@ -70,9 +69,9 @@ def register_failed_login! sorcery_adapter.increment(config.failed_logins_count_attribute_name) - if send(config.failed_logins_count_attribute_name) >= config.consecutive_login_retries_amount_limit - login_lock! - end + return unless send(config.failed_logins_count_attribute_name) >= config.consecutive_login_retries_amount_limit + + login_lock! end # /!\ @@ -98,9 +97,9 @@ def login_lock! config.unlock_token_attribute_name => TemporaryToken.generate_random_token } sorcery_adapter.update_attributes(attributes) - unless config.unlock_token_mailer_disabled || config.unlock_token_mailer.nil? - send_unlock_token_email! - end + return if config.unlock_token_mailer_disabled || config.unlock_token_mailer.nil? + + send_unlock_token_email! end def login_unlocked? diff --git a/lib/sorcery/model/submodules/external.rb b/lib/sorcery/model/submodules/external.rb index 3a84c23b..7c33d8a0 100644 --- a/lib/sorcery/model/submodules/external.rb +++ b/lib/sorcery/model/submodules/external.rb @@ -40,12 +40,13 @@ module ClassMethods def load_from_provider(provider, uid) config = sorcery_config authentication = config.authentications_class.sorcery_adapter.find_by_oauth_credentials(provider, uid) - user = sorcery_adapter.find_by_id(authentication.send(config.authentications_user_id_attribute_name)) if authentication + # Return user if matching authentication found + sorcery_adapter.find_by_id(authentication.send(config.authentications_user_id_attribute_name)) if authentication end def create_and_validate_from_provider(provider, uid, attrs) user = new(attrs) - user.send(sorcery_config.authentications_class.to_s.downcase.pluralize).build( + user.send(sorcery_config.authentications_class.name.demodulize.underscore.pluralize).build( sorcery_config.provider_uid_attribute_name => uid, sorcery_config.provider_attribute_name => provider ) @@ -92,7 +93,7 @@ def build_from_provider(attrs) module InstanceMethods def add_provider_to_user(provider, uid) - authentications = sorcery_config.authentications_class.name.underscore.pluralize + authentications = sorcery_config.authentications_class.name.demodulize.underscore.pluralize # first check to see if user has a particular authentication already if sorcery_adapter.find_authentication_by_oauth_credentials(authentications, provider, uid).nil? user = send(authentications).build(sorcery_config.provider_uid_attribute_name => uid, diff --git a/lib/sorcery/model/submodules/magic_login.rb b/lib/sorcery/model/submodules/magic_login.rb new file mode 100644 index 00000000..629d5d68 --- /dev/null +++ b/lib/sorcery/model/submodules/magic_login.rb @@ -0,0 +1,130 @@ +module Sorcery + module Model + module Submodules + # This submodule adds the ability to login via email without password. + # When the user requests an email is sent to him with a url. + # The url includes a token, which is also saved with the user's record in the db. + # The token has configurable expiration. + # When the user clicks the url in the email, providing the token has not yet expired, + # he will be able to login. + # + # When using this submodule, supplying a mailer is mandatory. + module MagicLogin + def self.included(base) + base.sorcery_config.class_eval do + attr_accessor :magic_login_token_attribute_name, # magic login code attribute name. + :magic_login_token_expires_at_attribute_name, # expires at attribute name. + :magic_login_email_sent_at_attribute_name, # when was email sent, used for hammering + # protection. + :magic_login_mailer_class, # mailer class. Needed. + :magic_login_mailer_disabled, # when true sorcery will not automatically + # email magic login details and allow you to + # manually handle how and when email is sent + :magic_login_email_method_name, # magic login email method on your + # mailer class. + :magic_login_expiration_period, # how many seconds before the request + # expires. nil for never expires. + :magic_login_time_between_emails # hammering protection, how long to wait + # before allowing another email to be sent. + end + + base.sorcery_config.instance_eval do + @defaults.merge!(:@magic_login_token_attribute_name => :magic_login_token, + :@magic_login_token_expires_at_attribute_name => :magic_login_token_expires_at, + :@magic_login_email_sent_at_attribute_name => :magic_login_email_sent_at, + :@magic_login_mailer_class => nil, + :@magic_login_mailer_disabled => true, + :@magic_login_email_method_name => :magic_login_email, + :@magic_login_expiration_period => 15 * 60, + :@magic_login_time_between_emails => 5 * 60) + + reset! + end + + base.extend(ClassMethods) + + base.sorcery_config.after_config << :validate_mailer_defined + base.sorcery_config.after_config << :define_magic_login_fields + + base.send(:include, InstanceMethods) + end + + module ClassMethods + # Find user by token, also checks for expiration. + # Returns the user if token found and is valid. + def load_from_magic_login_token(token, &block) + load_from_token( + token, + @sorcery_config.magic_login_token_attribute_name, + @sorcery_config.magic_login_token_expires_at_attribute_name, + &block + ) + end + + protected + + # This submodule requires the developer to define his own mailer class to be used by it + # when magic_login_mailer_disabled is false + def validate_mailer_defined + msg = 'To use magic_login submodule, you must define a mailer (config.magic_login_mailer_class = YourMailerClass).' + raise ArgumentError, msg if @sorcery_config.magic_login_mailer_class.nil? && @sorcery_config.magic_login_mailer_disabled == false + end + + def define_magic_login_fields + sorcery_adapter.define_field sorcery_config.magic_login_token_attribute_name, String + sorcery_adapter.define_field sorcery_config.magic_login_token_expires_at_attribute_name, Time + sorcery_adapter.define_field sorcery_config.magic_login_email_sent_at_attribute_name, Time + end + end + + module InstanceMethods + # generates a reset code with expiration + def generate_magic_login_token! + config = sorcery_config + attributes = { + config.magic_login_token_attribute_name => TemporaryToken.generate_random_token, + config.magic_login_email_sent_at_attribute_name => Time.now.in_time_zone + } + attributes[config.magic_login_token_expires_at_attribute_name] = Time.now.in_time_zone + config.magic_login_expiration_period if config.magic_login_expiration_period + + sorcery_adapter.update_attributes(attributes) + end + + # generates a magic login code with expiration and sends an email to the user. + def deliver_magic_login_instructions! + mail = false + config = sorcery_config + # hammering protection + return false if !config.magic_login_time_between_emails.nil? && + send(config.magic_login_email_sent_at_attribute_name) && + send(config.magic_login_email_sent_at_attribute_name) > config.magic_login_time_between_emails.seconds.ago + + self.class.sorcery_adapter.transaction do + generate_magic_login_token! + unless config.magic_login_mailer_disabled + send_magic_login_email! + mail = true + end + end + mail + end + + # Clears the token. + def clear_magic_login_token! + config = sorcery_config + sorcery_adapter.update_attributes( + config.magic_login_token_attribute_name => nil, + config.magic_login_token_expires_at_attribute_name => nil + ) + end + + protected + + def send_magic_login_email! + generic_send_email(:magic_login_email_method_name, :magic_login_mailer_class) + end + end + end + end + end +end diff --git a/lib/sorcery/model/submodules/reset_password.rb b/lib/sorcery/model/submodules/reset_password.rb index 0948a63f..0cbaab93 100644 --- a/lib/sorcery/model/submodules/reset_password.rb +++ b/lib/sorcery/model/submodules/reset_password.rb @@ -16,6 +16,8 @@ def self.included(base) attr_accessor :reset_password_token_attribute_name # Expires at attribute name. attr_accessor :reset_password_token_expires_at_attribute_name + # Counter access to reset password page + attr_accessor :reset_password_page_access_count_attribute_name # When was email sent, used for hammering protection. attr_accessor :reset_password_email_sent_at_attribute_name # Mailer class (needed) @@ -34,6 +36,8 @@ def self.included(base) base.sorcery_config.instance_eval do @defaults.merge!(:@reset_password_token_attribute_name => :reset_password_token, :@reset_password_token_expires_at_attribute_name => :reset_password_token_expires_at, + :@reset_password_page_access_count_attribute_name => + :access_count_to_reset_password_page, :@reset_password_email_sent_at_attribute_name => :reset_password_email_sent_at, :@reset_password_mailer => nil, :@reset_password_mailer_disabled => false, @@ -97,6 +101,7 @@ def deliver_reset_password_instructions! config = sorcery_config # hammering protection return false if config.reset_password_time_between_emails.present? && send(config.reset_password_email_sent_at_attribute_name) && send(config.reset_password_email_sent_at_attribute_name) > config.reset_password_time_between_emails.seconds.ago.utc + self.class.sorcery_adapter.transaction do generate_reset_password_token! mail = send_reset_password_email! unless config.reset_password_mailer_disabled @@ -104,11 +109,29 @@ def deliver_reset_password_instructions! mail end + # Increment access_count_to_reset_password_page attribute. + # For example, access_count_to_reset_password_page attribute is over 1, which + # means the user doesn't have a right to access. + def increment_password_reset_page_access_counter + sorcery_adapter.increment(sorcery_config.reset_password_page_access_count_attribute_name) + end + + # Reset access_count_to_reset_password_page attribute into 0. + # This is expected to be used after sending an instruction email. + def reset_password_reset_page_access_counter + send(:"#{sorcery_config.reset_password_page_access_count_attribute_name}=", 0) + sorcery_adapter.save + end + # Clears token and tries to update the new password for the user. - def change_password!(new_password) + def change_password(new_password, raise_on_failure: false) clear_reset_password_token send(:"#{sorcery_config.password_attribute_name}=", new_password) - sorcery_adapter.save + sorcery_adapter.save raise_on_failure: raise_on_failure + end + + def change_password!(new_password) + change_password(new_password, raise_on_failure: true) end protected diff --git a/lib/sorcery/model/submodules/user_activation.rb b/lib/sorcery/model/submodules/user_activation.rb index 1c58bbbb..9d65e1d6 100644 --- a/lib/sorcery/model/submodules/user_activation.rb +++ b/lib/sorcery/model/submodules/user_activation.rb @@ -45,7 +45,7 @@ def self.included(base) # don't setup activation if no password supplied - this user is created automatically sorcery_adapter.define_callback :before, :create, :setup_activation, if: proc { |user| user.send(sorcery_config.password_attribute_name).present? } # don't send activation needed email if no crypted password created - this user is external (OAuth etc.) - sorcery_adapter.define_callback :after, :create, :send_activation_needed_email!, if: :send_activation_needed_email? + sorcery_adapter.define_callback :after, :commit, :send_activation_needed_email!, on: :create, if: :send_activation_needed_email? end base.sorcery_config.after_config << :validate_mailer_defined diff --git a/lib/sorcery/protocols/oauth.rb b/lib/sorcery/protocols/oauth.rb index 448602cf..bb59d4d4 100644 --- a/lib/sorcery/protocols/oauth.rb +++ b/lib/sorcery/protocols/oauth.rb @@ -9,6 +9,7 @@ def oauth_version def get_request_token(token = nil, secret = nil) return ::OAuth::RequestToken.new(get_consumer, token, secret) if token && secret + get_consumer.get_request_token(oauth_callback: @callback_url) end diff --git a/lib/sorcery/providers/auth0.rb b/lib/sorcery/providers/auth0.rb new file mode 100644 index 00000000..0978c2fa --- /dev/null +++ b/lib/sorcery/providers/auth0.rb @@ -0,0 +1,46 @@ +module Sorcery + module Providers + # This class adds support for OAuth with Auth0.com + # + # config.auth0.key = + # config.auth0.secret = + # config.auth0.domain = + # ... + # + class Auth0 < Base + include Protocols::Oauth2 + + attr_accessor :auth_path, :token_path, :user_info_path, :scope + + def initialize + super + + @auth_path = '/authorize' + @token_path = '/oauth/token' + @user_info_path = '/userinfo' + @scope = 'openid profile email' + end + + def get_user_hash(access_token) + response = access_token.get(user_info_path) + + auth_hash(access_token).tap do |h| + h[:user_info] = JSON.parse(response.body) + h[:uid] = h[:user_info]['sub'] + end + end + + def login_url(_params, _session) + authorize_url(authorize_url: auth_path) + end + + def process_callback(params, _session) + args = {}.tap do |a| + a[:code] = params[:code] if params[:code] + end + + get_access_token(args, token_url: token_path, token_method: :post) + end + end + end +end diff --git a/lib/sorcery/providers/battlenet.rb b/lib/sorcery/providers/battlenet.rb new file mode 100644 index 00000000..3e486eef --- /dev/null +++ b/lib/sorcery/providers/battlenet.rb @@ -0,0 +1,51 @@ +module Sorcery + module Providers + # This class adds support for OAuth with BattleNet + + class Battlenet < Base + include Protocols::Oauth2 + + attr_accessor :auth_path, :scope, :token_url, :user_info_path + + def initialize + super + + @scope = 'openid' + @site = 'https://eu.battle.net/' + @auth_path = '/oauth/authorize' + @token_url = '/oauth/token' + @user_info_path = '/oauth/userinfo' + @state = SecureRandom.hex(16) + end + + def get_user_hash(access_token) + response = access_token.get(user_info_path) + body = JSON.parse(response.body) + auth_hash(access_token).tap do |h| + h[:user_info] = body + h[:battletag] = body['battletag'] + h[:uid] = body['id'] + end + end + + # calculates and returns the url to which the user should be redirected, + # to get authenticated at the external provider's site. + def login_url(_params, _session) + authorize_url(authorize_url: auth_path) + end + + # tries to login the user from access token + def process_callback(params, _session) + args = { code: params[:code] } + get_access_token( + args, + token_url: token_url, + client_id: @key, + client_secret: @secret, + grant_type: 'authorization_code', + token_method: :post + ) + end + end + end +end diff --git a/lib/sorcery/providers/discord.rb b/lib/sorcery/providers/discord.rb new file mode 100644 index 00000000..8c9bf5c4 --- /dev/null +++ b/lib/sorcery/providers/discord.rb @@ -0,0 +1,52 @@ +module Sorcery + module Providers + # This class adds support for OAuth with discordapp.com + + class Discord < Base + include Protocols::Oauth2 + + attr_accessor :auth_path, :scope, :token_url, :user_info_path + + def initialize + super + + @scope = 'identify' + @site = 'https://discordapp.com/' + @auth_path = '/api/oauth2/authorize' + @token_url = '/api/oauth2/token' + @user_info_path = '/api/users/@me' + @state = SecureRandom.hex(16) + end + + def get_user_hash(access_token) + response = access_token.get(user_info_path) + body = JSON.parse(response.body) + auth_hash(access_token).tap do |h| + h[:user_info] = body + h[:uid] = body['id'] + end + end + + # calculates and returns the url to which the user should be redirected, + # to get authenticated at the external provider's site. + def login_url(_params, _session) + authorize_url(authorize_url: auth_path) + end + + # tries to login the user from access token + def process_callback(params, _session) + args = {}.tap do |a| + a[:code] = params[:code] if params[:code] + end + get_access_token( + args, + token_url: token_url, + client_id: @key, + client_secret: @secret, + grant_type: 'authorization_code', + token_method: :post + ) + end + end + end +end diff --git a/lib/sorcery/providers/heroku.rb b/lib/sorcery/providers/heroku.rb index cd420d56..ac6427bd 100644 --- a/lib/sorcery/providers/heroku.rb +++ b/lib/sorcery/providers/heroku.rb @@ -45,6 +45,7 @@ def login_url(_params, _session) # tries to login the user from access token def process_callback(params, _session) raise 'Invalid state. Potential Cross Site Forgery' if params[:state] != state + args = {}.tap do |a| a[:code] = params[:code] if params[:code] end diff --git a/lib/sorcery/providers/instagram.rb b/lib/sorcery/providers/instagram.rb new file mode 100644 index 00000000..a01e35e3 --- /dev/null +++ b/lib/sorcery/providers/instagram.rb @@ -0,0 +1,73 @@ +module Sorcery + module Providers + # This class adds support for OAuth with Instagram.com. + class Instagram < Base + include Protocols::Oauth2 + + attr_accessor :access_permissions, :token_url, + :authorization_path, :user_info_path, + :scope, :user_info_fields + + def initialize + super + + @site = 'https://api.instagram.com' + @token_url = '/oauth/access_token' + @authorization_path = '/oauth/authorize/' + @user_info_path = '/v1/users/self' + @scope = 'basic' + end + + def self.included(base) + base.extend Sorcery::Providers + end + + # provider implements method to build Oauth client + def login_url(_params, _session) + authorize_url(token_url: @token_url) + end + + # overrides oauth2#authorize_url to allow customized scope. + def authorize_url(opts = {}) + @scope = access_permissions.present? ? access_permissions.join(' ') : scope + super(opts.merge(token_url: @token_url)) + end + + # pass oauth2 param `code` provided by instgrm server + def process_callback(params, _session) + args = {}.tap do |a| + a[:code] = params[:code] if params[:code] + end + get_access_token( + args, + token_url: @token_url, + client_id: @key, + client_secret: @secret + ) + end + + # see `user_info_mapping` in config/initializer, + # given `user_info_mapping` to specify + # {:db_attribute_name => 'instagram_attr_name'} + # so that Sorcery can build AR model from attr names + # + # NOTE: instead of just getting the user info + # from the access_token (which already returns them), + # testing strategy relies on querying user_info_path + def get_user_hash(access_token) + call_api_params = { + access_token: access_token.token, + client_id: access_token[:client_id] + } + response = access_token.get( + "#{user_info_path}?#{call_api_params.to_param}" + ) + + user_attrs = {} + user_attrs[:user_info] = JSON.parse(response.body)['data'] + user_attrs[:uid] = user_attrs[:user_info]['id'] + user_attrs + end + end + end +end diff --git a/lib/sorcery/providers/line.rb b/lib/sorcery/providers/line.rb new file mode 100644 index 00000000..7ad87c98 --- /dev/null +++ b/lib/sorcery/providers/line.rb @@ -0,0 +1,63 @@ +module Sorcery + module Providers + # This class adds support for OAuth with line.com. + # + # config.line.key = + # config.line.secret = + # ... + # + class Line < Base + include Protocols::Oauth2 + + attr_accessor :token_url, :user_info_path, :auth_path, :scope, :bot_prompt + + def initialize + super + + @site = 'https://access.line.me' + @user_info_path = 'https://api.line.me/v2/profile' + @token_url = 'https://api.line.me/oauth2/v2.1/token' + @auth_path = 'oauth2/v2.1/authorize' + @scope = 'profile' + end + + def get_user_hash(access_token) + response = access_token.get(user_info_path) + auth_hash(access_token).tap do |h| + h[:user_info] = JSON.parse(response.body) + h[:uid] = h[:user_info]['userId'].to_s + end + end + + # calculates and returns the url to which the user should be redirected, + # to get authenticated at the external provider's site. + def login_url(_params, _session) + @state = SecureRandom.hex(16) + authorize_url(authorize_url: auth_path) + end + + # overrides oauth2#authorize_url to add bot_prompt query. + def authorize_url(options = {}) + options.merge!({ + connection_opts: { params: { bot_prompt: bot_prompt } } + }) if bot_prompt.present? + + super(options) + end + + # tries to login the user from access token + def process_callback(params, _session) + args = {}.tap do |a| + a[:code] = params[:code] if params[:code] + end + + get_access_token( + args, + token_url: token_url, + token_method: :post, + grant_type: 'authorization_code' + ) + end + end + end +end diff --git a/lib/sorcery/providers/linkedin.rb b/lib/sorcery/providers/linkedin.rb index 239ad877..f4545323 100644 --- a/lib/sorcery/providers/linkedin.rb +++ b/lib/sorcery/providers/linkedin.rb @@ -1,65 +1,74 @@ module Sorcery module Providers - # This class adds support for OAuth with Linkedin.com. + # This class adds support for OAuth with LinkedIn. # # config.linkedin.key = # config.linkedin.secret = # ... # class Linkedin < Base - include Protocols::Oauth + include Protocols::Oauth2 - attr_accessor :authorize_path, :access_permissions, :access_token_path, - :request_token_path, :user_info_fields, :user_info_path + attr_accessor :auth_url, :scope, :token_url, :user_info_url, :email_info_url def initialize - @configuration = { - site: 'https://api.linkedin.com', - authorize_path: '/uas/oauth/authenticate', - request_token_path: '/uas/oauth/requestToken', - access_token_path: '/uas/oauth/accessToken' - } - @user_info_path = '/v1/people/~' - end + super - # Override included get_consumer method to provide authorize_path - def get_consumer - # Add access permissions to request token path - @configuration[:request_token_path] += '?scope=' + access_permissions.join('+') unless access_permissions.blank? || @configuration[:request_token_path].include?('?scope=') - ::OAuth::Consumer.new(@key, @secret, @configuration) + @site = 'https://api.linkedin.com' + @auth_url = '/oauth/v2/authorization' + @token_url = '/oauth/v2/accessToken' + @user_info_url = 'https://api.linkedin.com/v2/me' + @email_info_url = 'https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))' + @scope = 'r_liteprofile r_emailaddress' + @state = SecureRandom.hex(16) end def get_user_hash(access_token) - # Always include id for provider uid and prevent accidental duplication via setting `user_info_field = ['id']` (needed in Sorcery 0.9.1) - info_fields = user_info_fields ? user_info_fields.reject{|n| n == 'id'} : [] - fields = info_fields.any? ? 'id,' + info_fields.join(',') : 'id' - response = access_token.get("#{@user_info_path}:(#{fields})", 'x-li-format' => 'json') + user_info = get_user_info(access_token) auth_hash(access_token).tap do |h| - h[:user_info] = JSON.parse(response.body) - h[:uid] = h[:user_info]['id'].to_s + h[:user_info] = user_info + h[:uid] = h[:user_info]['id'] end end # calculates and returns the url to which the user should be redirected, # to get authenticated at the external provider's site. - def login_url(_params, session) - req_token = get_request_token - session[:request_token] = req_token.token - session[:request_token_secret] = req_token.secret - authorize_url(request_token: req_token.token, request_token_secret: req_token.secret) + def login_url(_params, _session) + authorize_url(authorize_url: auth_url) end # tries to login the user from access token - def process_callback(params, session) - args = { - oauth_verifier: params[:oauth_verifier], - request_token: session[:request_token], - request_token_secret: session[:request_token_secret] - } + def process_callback(params, _session) + args = {}.tap do |a| + a[:code] = params[:code] if params[:code] + end + + get_access_token(args, token_url: token_url, token_method: :post) + end + + def get_user_info(access_token) + response = access_token.get(user_info_url) + user_info = JSON.parse(response.body) + + if email_in_scope? + email = fetch_email(access_token) + + return user_info.merge(email) + end + + user_info + end + + def email_in_scope? + scope.include?('r_emailaddress') + end + + def fetch_email(access_token) + email_response = access_token.get(email_info_url) + email_info = JSON.parse(email_response.body)['elements'].first - args[:code] = params[:code] if params[:code] - get_access_token(args) + email_info['handle~'] end end end diff --git a/lib/sorcery/providers/qq.rb b/lib/sorcery/providers/qq.rb new file mode 100644 index 00000000..64c0ca17 --- /dev/null +++ b/lib/sorcery/providers/qq.rb @@ -0,0 +1,79 @@ +module Sorcery + module Providers + # This class adds support for OAuth with graph.qq.com. + # + class Qq < Base + include Protocols::Oauth2 + + attr_reader :parse + attr_accessor :auth_url, :scope, :token_url, :user_info_path, :openid_path + + def initialize + super + + @scope = 'get_user_info' + @auth_url = 'https://graph.qq.com/oauth2.0/authorize' + @openid_path = 'https://graph.qq.com/oauth2.0/me' + @user_info_path = 'https://graph.qq.com/user/get_user_info' + @token_url = 'https://graph.qq.com/oauth2.0/token' + @parse = :query + @state = SecureRandom.hex(16) + end + + def authorize_url(options = {}) + oauth_params = { + response_type: 'code', + client_id: @key, + redirect_uri: @callback_url, + state: @state, + scope: @scope + } + "#{options[:authorize_url]}?#{oauth_params.to_query}#qq_redirect" + end + + def get_user_hash(access_token) + openid_response = access_token.get(openid_path, params: { + access_token: access_token.token + }) + + openid = openid_response.body.match(/"openid":"(\w{3,32})"/) || [nil, ''] + + info_response = access_token.get(user_info_path, params: { + access_token: access_token.token, + oauth_consumer_key: @key, + openid: openid[1] + }) + + {}.tap do |h| + h[:user_info] = JSON.parse(info_response.body) + h[:uid] = openid[1] + end + end + + def get_access_token(args, options = {}) + client = build_client(options) + + client.auth_code.get_token( + args[:code], + { client_id: @key, client_secret: @secret, redirect_uri: @callback_url, parse: @parse}, + options + ) + end + + def login_url(_params, _session) + authorize_url authorize_url: auth_url + end + + def process_callback(params, _session) + args = {}.tap do |a| + a[:code] = params[:code] if params[:code].present? + end + + get_access_token( + args, + token_url: token_url, + ) + end + end + end +end diff --git a/lib/sorcery/providers/vk.rb b/lib/sorcery/providers/vk.rb index 3e7fbcbc..16896c54 100644 --- a/lib/sorcery/providers/vk.rb +++ b/lib/sorcery/providers/vk.rb @@ -9,7 +9,7 @@ module Providers class Vk < Base include Protocols::Oauth2 - attr_accessor :auth_path, :token_path, :user_info_url, :scope + attr_accessor :auth_path, :token_path, :user_info_url, :scope, :api_version def initialize super @@ -28,15 +28,16 @@ def get_user_hash(access_token) access_token: access_token.token, uids: access_token.params['user_id'], fields: user_info_mapping.values.join(','), - scope: scope + scope: scope, + v: api_version.to_s } response = access_token.get(user_info_url, params: params) - if user_hash[:user_info] = JSON.parse(response.body) + if (user_hash[:user_info] = JSON.parse(response.body)) user_hash[:user_info] = user_hash[:user_info]['response'][0] user_hash[:user_info]['full_name'] = [user_hash[:user_info]['first_name'], user_hash[:user_info]['last_name']].join(' ') - user_hash[:uid] = user_hash[:user_info]['uid'] + user_hash[:uid] = user_hash[:user_info]['id'] user_hash[:user_info]['email'] = access_token.params['email'] end user_hash diff --git a/lib/sorcery/providers/wechat.rb b/lib/sorcery/providers/wechat.rb index 329246d0..fb023429 100644 --- a/lib/sorcery/providers/wechat.rb +++ b/lib/sorcery/providers/wechat.rb @@ -37,10 +37,13 @@ def authorize_url(options = {}) end def get_user_hash(access_token) - response = access_token.get(user_info_path, params: { - access_token: access_token.token, - openid: access_token.params['openid'], - }) + response = access_token.get( + user_info_path, + params: { + access_token: access_token.token, + openid: access_token.params['openid'] + } + ) {}.tap do |h| h[:user_info] = JSON.parse(response.body) @@ -70,10 +73,9 @@ def process_callback(params, _session) args, token_url: token_url, mode: mode, - param_name: param_name, + param_name: param_name ) end end end end - diff --git a/lib/sorcery/test_helpers/internal.rb b/lib/sorcery/test_helpers/internal.rb index 6f603c27..1201ac2b 100644 --- a/lib/sorcery/test_helpers/internal.rb +++ b/lib/sorcery/test_helpers/internal.rb @@ -17,7 +17,7 @@ def cost # a patch to fix a bug in testing that happens when you 'destroy' a session twice. # After the first destroy, the session is an ordinary hash, and then when destroy # is called again there's an exception. - class ::Hash + class ::Hash # rubocop:disable Style/ClassAndModuleChildren def destroy clear end @@ -69,9 +69,10 @@ def update_model(&block) def reload_user_class User && Object.send(:remove_const, 'User') load 'user.rb' - if User.respond_to?(:reset_column_information) - User.reset_column_information - end + + return unless User.respond_to?(:reset_column_information) + + User.reset_column_information end end end diff --git a/lib/sorcery/test_helpers/internal/rails.rb b/lib/sorcery/test_helpers/internal/rails.rb index 28e4a45d..a9eb4365 100644 --- a/lib/sorcery/test_helpers/internal/rails.rb +++ b/lib/sorcery/test_helpers/internal/rails.rb @@ -4,11 +4,11 @@ module Internal module Rails include ::Sorcery::TestHelpers::Rails::Controller - SUBMODULES_AUTO_ADDED_CONTROLLER_FILTERS = [ - :register_last_activity_time_to_db, - :deny_banned_user, - :validate_session - ] + SUBMODULES_AUTO_ADDED_CONTROLLER_FILTERS = %i[ + register_last_activity_time_to_db + deny_banned_user + validate_session + ].freeze def sorcery_reload!(submodules = [], options = {}) reload_user_class @@ -40,11 +40,11 @@ def sorcery_reload!(submodules = [], options = {}) end end User.authenticates_with_sorcery! - if defined?(DataMapper) && User.ancestors.include?(DataMapper::Resource) - DataMapper.auto_migrate! - User.finalize - Authentication.finalize - end + return unless defined?(DataMapper) && User.ancestors.include?(DataMapper::Resource) + + DataMapper.auto_migrate! + User.finalize + Authentication.finalize end def sorcery_controller_property_set(property, value) @@ -64,7 +64,7 @@ def clear_user_without_logout end if ::Rails.version < '5.0.0' - %w(get post put).each do |method| + %w[get post put].each do |method| define_method(method) do |action, options = {}| super action, options[:params] || {}, options[:session] end diff --git a/lib/sorcery/test_helpers/rails/request.rb b/lib/sorcery/test_helpers/rails/request.rb new file mode 100644 index 00000000..83f3bd1a --- /dev/null +++ b/lib/sorcery/test_helpers/rails/request.rb @@ -0,0 +1,20 @@ +module Sorcery + module TestHelpers + module Rails + module Request + # Accepts arguments for user to login, the password, route to use and HTTP method + # Defaults - @user, 'secret', 'user_sessions_url' and http_method: POST + def login_user(user = nil, password = 'secret', route = nil, http_method = :post) + user ||= @user + route ||= user_sessions_url + + username_attr = user.sorcery_config.username_attribute_names.first + username = user.send(username_attr) + password_attr = user.sorcery_config.password_attribute_name + + send(http_method, route, params: { "#{username_attr}": username, "#{password_attr}": password }) + end + end + end + end +end diff --git a/lib/sorcery/version.rb b/lib/sorcery/version.rb index 7d2e2123..6a15147e 100644 --- a/lib/sorcery/version.rb +++ b/lib/sorcery/version.rb @@ -1,3 +1,3 @@ module Sorcery - VERSION = '0.11.0' + VERSION = '0.16.1'.freeze end diff --git a/sorcery.gemspec b/sorcery.gemspec index 644ce0ce..c5fae94e 100644 --- a/sorcery.gemspec +++ b/sorcery.gemspec @@ -1,33 +1,49 @@ -lib = File.expand_path('../lib', __FILE__) +lib = File.expand_path('lib', __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'sorcery/version' +# rubocop:disable Metrics/BlockLength Gem::Specification.new do |s| s.name = 'sorcery' s.version = Sorcery::VERSION - s.authors = ['Noam Ben Ari', 'Kir Shatrov', 'Grzegorz Witek', 'Chase Gilliam'] - s.email = 'chase.gilliam@gmail.com' + s.authors = [ + 'Noam Ben Ari', + 'Kir Shatrov', + 'Grzegorz Witek', + 'Chase Gilliam', + 'Josh Buker' + ] + s.email = [ + 'crypto@joshbuker.com' + ] + + # TODO: Cleanup formatting. + # rubocop:disable Layout/LineLength s.description = 'Provides common authentication needs such as signing in/out, activating by email and resetting password.' s.summary = 'Magical authentication for Rails applications' s.homepage = 'https://github.com/Sorcery/sorcery' s.post_install_message = "As of version 1.0 oauth/oauth2 won't be automatically bundled so you may need to add those dependencies to your Gemfile.\n" s.post_install_message += 'You may need oauth2 if you use external providers such as any of these: https://github.com/Sorcery/sorcery/tree/master/lib/sorcery/providers' + # rubocop:enable Layout/LineLength s.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR) s.require_paths = ['lib'] s.licenses = ['MIT'] - s.required_ruby_version = '>= 2.0.0' + s.required_ruby_version = '>= 2.4.9' - s.add_dependency 'oauth', '~> 0.4', '>= 0.4.4' - s.add_dependency 'oauth2', '~> 1.0', '>= 0.8.0' s.add_dependency 'bcrypt', '~> 3.1' + s.add_dependency 'oauth', '~> 0.5', '>= 0.5.5' + s.add_dependency 'oauth2', '~> 1.0', '>= 0.8.0' - s.add_development_dependency 'yard', '~> 0.6.0' - s.add_development_dependency 'timecop' + s.add_development_dependency 'byebug', '~> 10.0.0' + s.add_development_dependency 'rspec-rails', '~> 3.7.0' + s.add_development_dependency 'rubocop' s.add_development_dependency 'simplecov', '>= 0.3.8' - s.add_development_dependency 'rspec-rails', '~> 3.5.0' - s.add_development_dependency 'test-unit', '~> 3.1.0' - s.add_development_dependency 'byebug', '~> 9.0.0' + s.add_development_dependency 'test-unit', '~> 3.2.0' + s.add_development_dependency 'timecop' + s.add_development_dependency 'webmock', '~> 3.3.0' + s.add_development_dependency 'yard', '~> 0.9.0', '>= 0.9.12' end +# rubocop:enable Metrics/BlockLength diff --git a/spec/active_record/user_activation_spec.rb b/spec/active_record/user_activation_spec.rb index 82a7020d..4d9f62ac 100644 --- a/spec/active_record/user_activation_spec.rb +++ b/spec/active_record/user_activation_spec.rb @@ -5,12 +5,12 @@ describe User, 'with activation submodule', active_record: true do before(:all) do - ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate/activation") + MigrationHelper.migrate("#{Rails.root}/db/migrate/activation") User.reset_column_information end after(:all) do - ActiveRecord::Migrator.rollback("#{Rails.root}/db/migrate/activation") + MigrationHelper.rollback("#{Rails.root}/db/migrate/activation") end it_behaves_like 'rails_3_activation_model' diff --git a/spec/active_record/user_activity_logging_spec.rb b/spec/active_record/user_activity_logging_spec.rb index b185b45b..3b3c90d9 100644 --- a/spec/active_record/user_activity_logging_spec.rb +++ b/spec/active_record/user_activity_logging_spec.rb @@ -3,12 +3,12 @@ describe User, 'with activity logging submodule', active_record: true do before(:all) do - ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate/activity_logging") + MigrationHelper.migrate("#{Rails.root}/db/migrate/activity_logging") User.reset_column_information end after(:all) do - ActiveRecord::Migrator.rollback("#{Rails.root}/db/migrate/activity_logging") + MigrationHelper.rollback("#{Rails.root}/db/migrate/activity_logging") end it_behaves_like 'rails_3_activity_logging_model' diff --git a/spec/active_record/user_brute_force_protection_spec.rb b/spec/active_record/user_brute_force_protection_spec.rb index 544b8ff6..6bc25811 100644 --- a/spec/active_record/user_brute_force_protection_spec.rb +++ b/spec/active_record/user_brute_force_protection_spec.rb @@ -3,12 +3,12 @@ describe User, 'with brute_force_protection submodule', active_record: true do before(:all) do - ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate/brute_force_protection") + MigrationHelper.migrate("#{Rails.root}/db/migrate/brute_force_protection") User.reset_column_information end after(:all) do - ActiveRecord::Migrator.rollback("#{Rails.root}/db/migrate/brute_force_protection") + MigrationHelper.rollback("#{Rails.root}/db/migrate/brute_force_protection") end it_behaves_like 'rails_3_brute_force_protection_model' diff --git a/spec/active_record/user_magic_login_spec.rb b/spec/active_record/user_magic_login_spec.rb new file mode 100644 index 00000000..d40f8bdb --- /dev/null +++ b/spec/active_record/user_magic_login_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' +require 'shared_examples/user_magic_login_shared_examples' + +describe User, 'with magic_login submodule', active_record: true do + before(:all) do + MigrationHelper.migrate("#{Rails.root}/db/migrate/magic_login") + User.reset_column_information + end + + after(:all) do + MigrationHelper.rollback("#{Rails.root}/db/migrate/magic_login") + end + + it_behaves_like 'magic_login_model' +end diff --git a/spec/active_record/user_oauth_spec.rb b/spec/active_record/user_oauth_spec.rb index 33a751be..0f5b779f 100644 --- a/spec/active_record/user_oauth_spec.rb +++ b/spec/active_record/user_oauth_spec.rb @@ -3,12 +3,12 @@ describe User, 'with oauth submodule', active_record: true do before(:all) do - ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate/external") + MigrationHelper.migrate("#{Rails.root}/db/migrate/external") User.reset_column_information end after(:all) do - ActiveRecord::Migrator.rollback("#{Rails.root}/db/migrate/external") + MigrationHelper.rollback("#{Rails.root}/db/migrate/external") end it_behaves_like 'rails_3_oauth_model' diff --git a/spec/active_record/user_remember_me_spec.rb b/spec/active_record/user_remember_me_spec.rb index 948b3c08..0c5ef16c 100644 --- a/spec/active_record/user_remember_me_spec.rb +++ b/spec/active_record/user_remember_me_spec.rb @@ -3,12 +3,12 @@ describe User, 'with remember_me submodule', active_record: true do before(:all) do - ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate/remember_me") + MigrationHelper.migrate("#{Rails.root}/db/migrate/remember_me") User.reset_column_information end after(:all) do - ActiveRecord::Migrator.rollback("#{Rails.root}/db/migrate/remember_me") + MigrationHelper.rollback("#{Rails.root}/db/migrate/remember_me") end it_behaves_like 'rails_3_remember_me_model' diff --git a/spec/active_record/user_reset_password_spec.rb b/spec/active_record/user_reset_password_spec.rb index 1af1f266..3cac7186 100644 --- a/spec/active_record/user_reset_password_spec.rb +++ b/spec/active_record/user_reset_password_spec.rb @@ -3,12 +3,12 @@ describe User, 'with reset_password submodule', active_record: true do before(:all) do - ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate/reset_password") + MigrationHelper.migrate("#{Rails.root}/db/migrate/reset_password") User.reset_column_information end after(:all) do - ActiveRecord::Migrator.rollback("#{Rails.root}/db/migrate/reset_password") + MigrationHelper.rollback("#{Rails.root}/db/migrate/reset_password") end it_behaves_like 'rails_3_reset_password_model' diff --git a/spec/active_record/user_spec.rb b/spec/active_record/user_spec.rb index 407eeef1..9c982821 100644 --- a/spec/active_record/user_spec.rb +++ b/spec/active_record/user_spec.rb @@ -22,16 +22,6 @@ it_should_behave_like 'rails_3_core_model' describe 'external users' do - before(:all) do - ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate/external") - User.reset_column_information - sorcery_reload! - end - - after(:all) do - ActiveRecord::Migrator.rollback("#{Rails.root}/db/migrate/external") - end - it_should_behave_like 'external_user' end end diff --git a/spec/controllers/controller_http_basic_auth_spec.rb b/spec/controllers/controller_http_basic_auth_spec.rb index 7a633a2c..f6b8f94f 100644 --- a/spec/controllers/controller_http_basic_auth_spec.rb +++ b/spec/controllers/controller_http_basic_auth_spec.rb @@ -28,7 +28,7 @@ expect(User).to receive('authenticate').with('bla@bla.com', 'secret').and_return(user) get :test_http_basic_auth, params: {}, session: { http_authentication_used: true } - expect(response).to be_a_success + expect(response).to be_successful end it 'fails authentication if credentials are wrong' do diff --git a/spec/controllers/controller_oauth2_spec.rb b/spec/controllers/controller_oauth2_spec.rb index 653564d8..c538e9ed 100644 --- a/spec/controllers/controller_oauth2_spec.rb +++ b/spec/controllers/controller_oauth2_spec.rb @@ -5,7 +5,9 @@ describe SorceryController, active_record: true, type: :controller do before(:all) do if SORCERY_ORM == :active_record - ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate/external") + MigrationHelper.migrate("#{Rails.root}/db/migrate/external") + MigrationHelper.migrate("#{Rails.root}/db/migrate/activation") + MigrationHelper.migrate("#{Rails.root}/db/migrate/activity_logging") User.reset_column_information end @@ -15,7 +17,9 @@ after(:all) do if SORCERY_ORM == :active_record - ActiveRecord::Migrator.rollback("#{Rails.root}/db/migrate/external") + MigrationHelper.rollback("#{Rails.root}/db/migrate/external") + MigrationHelper.rollback("#{Rails.root}/db/migrate/activity_logging") + MigrationHelper.rollback("#{Rails.root}/db/migrate/activation") end end @@ -112,12 +116,21 @@ end context 'when callback_url begin with http://' do + before do + sorcery_controller_external_property_set(:facebook, :callback_url, '/oauth/twitter/callback') + sorcery_controller_external_property_set(:facebook, :api_version, 'v2.2') + end + it 'login_at redirects correctly' do create_new_user get :login_at_test_facebook expect(response).to be_a_redirect expect(response).to redirect_to("https://www.facebook.com/v2.2/dialog/oauth?client_id=#{::Sorcery::Controller::Config.facebook.key}&display=page&redirect_uri=http%3A%2F%2Ftest.host%2Foauth%2Ftwitter%2Fcallback&response_type=code&scope=email&state") end + + after do + sorcery_controller_external_property_set(:facebook, :callback_url, 'http://blabla.com') + end end it "'login_from' logins if user exists" do @@ -151,7 +164,7 @@ expect(flash[:notice]).to eq 'Success!' end - [:github, :google, :liveid, :vk, :salesforce, :paypal, :slack, :wechat, :microsoft].each do |provider| + %i[github google liveid vk salesforce paypal slack wechat microsoft instagram auth0 discord battlenet qq].each do |provider| describe "with #{provider}" do it 'login_at redirects correctly' do get :"login_at_test_#{provider}" @@ -196,51 +209,79 @@ describe 'OAuth with User Activation features' do before(:all) do - if SORCERY_ORM == :active_record - ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate/activation") - end - - sorcery_reload!([:user_activation,:external], :user_activation_mailer => ::SorceryMailer) - sorcery_controller_property_set(:external_providers, [:facebook, :github, :google, :liveid, :vk, :salesforce, :paypal, :slack, :wechat, :microsoft]) + sorcery_reload!(%i[user_activation external], user_activation_mailer: ::SorceryMailer) + sorcery_controller_property_set( + :external_providers, + %i[ + facebook + github + google + liveid + vk + salesforce + paypal + slack + wechat + microsoft + instagram + auth0 + line + discord + battlenet + qq + ] + ) # TODO: refactor - sorcery_controller_external_property_set(:facebook, :key, "eYVNBjBDi33aa9GkA3w") - sorcery_controller_external_property_set(:facebook, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") - sorcery_controller_external_property_set(:facebook, :callback_url, "http://blabla.com") - sorcery_controller_external_property_set(:github, :key, "eYVNBjBDi33aa9GkA3w") - sorcery_controller_external_property_set(:github, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") - sorcery_controller_external_property_set(:github, :callback_url, "http://blabla.com") - sorcery_controller_external_property_set(:google, :key, "eYVNBjBDi33aa9GkA3w") - sorcery_controller_external_property_set(:google, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") - sorcery_controller_external_property_set(:google, :callback_url, "http://blabla.com") - sorcery_controller_external_property_set(:liveid, :key, "eYVNBjBDi33aa9GkA3w") - sorcery_controller_external_property_set(:liveid, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") - sorcery_controller_external_property_set(:liveid, :callback_url, "http://blabla.com") - sorcery_controller_external_property_set(:vk, :key, "eYVNBjBDi33aa9GkA3w") - sorcery_controller_external_property_set(:vk, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") - sorcery_controller_external_property_set(:vk, :callback_url, "http://blabla.com") - sorcery_controller_external_property_set(:salesforce, :key, "eYVNBjBDi33aa9GkA3w") - sorcery_controller_external_property_set(:salesforce, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") - sorcery_controller_external_property_set(:salesforce, :callback_url, "http://blabla.com") - sorcery_controller_external_property_set(:paypal, :key, "eYVNBjBDi33aa9GkA3w") - sorcery_controller_external_property_set(:paypal, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") - sorcery_controller_external_property_set(:paypal, :callback_url, "http://blabla.com") - sorcery_controller_external_property_set(:slack, :key, "eYVNBjBDi33aa9GkA3w") - sorcery_controller_external_property_set(:slack, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") - sorcery_controller_external_property_set(:slack, :callback_url, "http://blabla.com") - sorcery_controller_external_property_set(:wechat, :key, "eYVNBjBDi33aa9GkA3w") - sorcery_controller_external_property_set(:wechat, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") - sorcery_controller_external_property_set(:wechat, :callback_url, "http://blabla.com") - sorcery_controller_external_property_set(:microsoft, :key, "eYVNBjBDi33aa9GkA3w") - sorcery_controller_external_property_set(:microsoft, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") - sorcery_controller_external_property_set(:microsoft, :callback_url, "http://blabla.com") - end - - after(:all) do - if SORCERY_ORM == :active_record - ActiveRecord::Migrator.rollback("#{Rails.root}/db/migrate/external") - ActiveRecord::Migrator.rollback("#{Rails.root}/db/migrate/activation") - end + sorcery_controller_external_property_set(:facebook, :key, 'eYVNBjBDi33aa9GkA3w') + sorcery_controller_external_property_set(:facebook, :secret, 'XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8') + sorcery_controller_external_property_set(:facebook, :callback_url, 'http://blabla.com') + sorcery_controller_external_property_set(:github, :key, 'eYVNBjBDi33aa9GkA3w') + sorcery_controller_external_property_set(:github, :secret, 'XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8') + sorcery_controller_external_property_set(:github, :callback_url, 'http://blabla.com') + sorcery_controller_external_property_set(:google, :key, 'eYVNBjBDi33aa9GkA3w') + sorcery_controller_external_property_set(:google, :secret, 'XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8') + sorcery_controller_external_property_set(:google, :callback_url, 'http://blabla.com') + sorcery_controller_external_property_set(:liveid, :key, 'eYVNBjBDi33aa9GkA3w') + sorcery_controller_external_property_set(:liveid, :secret, 'XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8') + sorcery_controller_external_property_set(:liveid, :callback_url, 'http://blabla.com') + sorcery_controller_external_property_set(:vk, :key, 'eYVNBjBDi33aa9GkA3w') + sorcery_controller_external_property_set(:vk, :secret, 'XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8') + sorcery_controller_external_property_set(:vk, :callback_url, 'http://blabla.com') + sorcery_controller_external_property_set(:salesforce, :key, 'eYVNBjBDi33aa9GkA3w') + sorcery_controller_external_property_set(:salesforce, :secret, 'XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8') + sorcery_controller_external_property_set(:salesforce, :callback_url, 'http://blabla.com') + sorcery_controller_external_property_set(:paypal, :key, 'eYVNBjBDi33aa9GkA3w') + sorcery_controller_external_property_set(:paypal, :secret, 'XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8') + sorcery_controller_external_property_set(:paypal, :callback_url, 'http://blabla.com') + sorcery_controller_external_property_set(:slack, :key, 'eYVNBjBDi33aa9GkA3w') + sorcery_controller_external_property_set(:slack, :secret, 'XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8') + sorcery_controller_external_property_set(:slack, :callback_url, 'http://blabla.com') + sorcery_controller_external_property_set(:wechat, :key, 'eYVNBjBDi33aa9GkA3w') + sorcery_controller_external_property_set(:wechat, :secret, 'XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8') + sorcery_controller_external_property_set(:wechat, :callback_url, 'http://blabla.com') + sorcery_controller_external_property_set(:microsoft, :key, 'eYVNBjBDi33aa9GkA3w') + sorcery_controller_external_property_set(:microsoft, :secret, 'XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8') + sorcery_controller_external_property_set(:microsoft, :callback_url, 'http://blabla.com') + sorcery_controller_external_property_set(:instagram, :key, 'eYVNBjBDi33aa9GkA3w') + sorcery_controller_external_property_set(:instagram, :secret, 'XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8') + sorcery_controller_external_property_set(:instagram, :callback_url, 'http://blabla.com') + sorcery_controller_external_property_set(:auth0, :key, 'eYVNBjBDi33aa9GkA3w') + sorcery_controller_external_property_set(:auth0, :secret, 'XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8') + sorcery_controller_external_property_set(:auth0, :callback_url, 'http://blabla.com') + sorcery_controller_external_property_set(:auth0, :site, 'https://sorcery-test.auth0.com') + sorcery_controller_external_property_set(:line, :key, "eYVNBjBDi33aa9GkA3w") + sorcery_controller_external_property_set(:line, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") + sorcery_controller_external_property_set(:line, :callback_url, "http://blabla.com") + sorcery_controller_external_property_set(:discord, :key, 'eYVNBjBDi33aa9GkA3w') + sorcery_controller_external_property_set(:discord, :secret, 'XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8') + sorcery_controller_external_property_set(:discord, :callback_url, 'http://blabla.com') + sorcery_controller_external_property_set(:battlenet, :key, '4c43d4862c774ca5bbde89873bf0d338') + sorcery_controller_external_property_set(:battlenet, :secret, 'TxY7IwKOykACd8kUxPyVGTqBs44UBDdX') + sorcery_controller_external_property_set(:battlenet, :callback_url, 'http://blabla.com') + sorcery_controller_external_property_set(:qq, :key, '4c43d4862c774ca5bbde89873bf0d338') + sorcery_controller_external_property_set(:qq, :secret, 'TxY7IwKOykACd8kUxPyVGTqBs44UBDdX') + sorcery_controller_external_property_set(:qq, :callback_url, 'http://blabla.com') end after(:each) do @@ -263,7 +304,7 @@ expect(ActionMailer::Base.deliveries.size).to eq old_size end - [:github, :google, :liveid, :vk, :salesforce, :paypal, :wechat, :microsoft].each do |provider| + %i[github google liveid vk salesforce paypal wechat microsoft instagram auth0 discord battlenet qq].each do |provider| it "does not send activation email to external users (#{provider})" do old_size = ActionMailer::Base.deliveries.size create_new_external_user provider @@ -275,6 +316,7 @@ create_new_external_user provider old_size = ActionMailer::Base.deliveries.size @user.activate! + expect(ActionMailer::Base.deliveries.size).to eq old_size end end end @@ -283,17 +325,10 @@ let(:user) { double('user', id: 42) } before(:all) do - sorcery_reload!([:activity_logging, :external]) - end - - after(:all) do - if SORCERY_ORM == :active_record - ActiveRecord::Migrator.rollback("#{Rails.root}/db/migrate/external") - ActiveRecord::Migrator.rollback("#{Rails.root}/db/migrate/activity_logging") - end + sorcery_reload!(%i[activity_logging external]) end - %w(facebook github google liveid vk salesforce slack).each do |provider| + %w[facebook github google liveid vk salesforce slack discord battlenet].each do |provider| context "when #{provider}" do before(:each) do sorcery_controller_property_set(:register_login_time, true) @@ -327,12 +362,12 @@ describe 'OAuth with session timeout features' do before(:all) do - sorcery_reload!([:session_timeout, :external]) + sorcery_reload!(%i[session_timeout external]) end let(:user) { double('user', id: 42) } - %w(facebook github google liveid vk salesforce slack).each do |provider| + %w[facebook github google liveid vk salesforce slack discord battlenet qq].each do |provider| context "when #{provider}" do before(:each) do sorcery_model_property_set(:authentications_class, Authentication) @@ -369,41 +404,80 @@ def stub_all_oauth2_requests! access_token = double(OAuth2::AccessToken) allow(access_token).to receive(:token_param=) + # Needed for Instagram + allow(access_token).to receive(:[]).with(:client_id) { 'eYVNBjBDi33aa9GkA3w' } response = double(OAuth2::Response) allow(response).to receive(:body) { - { - 'id' => '123', - 'user_id' => '123', # Needed for Salesforce - 'name' => 'Noam Ben Ari', - 'first_name' => 'Noam', - 'last_name' => 'Ben Ari', - 'link' => 'http://www.facebook.com/nbenari1', - 'hometown' => { 'id' => '110619208966868', 'name' => 'Haifa, Israel' }, - 'location' => { 'id' => '106906559341067', 'name' => 'Pardes Hanah, Hefa, Israel' }, - 'bio' => "I'm a new daddy, and enjoying it!", - 'gender' => 'male', - 'email' => 'nbenari@gmail.com', - 'timezone' => 2, - 'locale' => 'en_US', - 'languages' => [{ 'id' => '108405449189952', 'name' => 'Hebrew' }, { 'id' => '106059522759137', 'name' => 'English' }, { 'id' => '112624162082677', 'name' => 'Russian' }], - 'verified' => true, - 'updated_time' => '2011-02-16T20:59:38+0000', - # response for VK auth - 'response' => [ - { - 'uid' => '123', - 'first_name' => 'Noam', - 'last_name' => 'Ben Ari' - } - ], - 'user' => { - 'name' => 'Sonny Whether', - 'id' => '123', - 'email' => 'bobby@example.com' - }, - # response for wechat auth - 'unionid' => '123', - }.to_json } + { + 'id' => '123', + 'user_id' => '123', # Needed for Salesforce + 'sub' => '123', # Needed for Auth0 + 'name' => 'Noam Ben Ari', + 'first_name' => 'Noam', + 'last_name' => 'Ben Ari', + 'link' => 'http://www.facebook.com/nbenari1', + 'hometown' => { + 'id' => '110619208966868', + 'name' => 'Haifa, Israel' + }, + 'location' => { + 'id' => '106906559341067', + 'name' => 'Pardes Hanah, Hefa, Israel' + }, + 'bio' => "I'm a new daddy, and enjoying it!", + 'gender' => 'male', + 'email' => 'nbenari@gmail.com', + 'timezone' => 2, + 'locale' => 'en_US', + 'languages' => [ + { + 'id' => '108405449189952', + 'name' => 'Hebrew' + }, + { + 'id' => '106059522759137', + 'name' => 'English' + }, + { + 'id' => '112624162082677', + 'name' => 'Russian' + } + ], + 'verified' => true, + 'updated_time' => '2011-02-16T20:59:38+0000', + # response for VK auth + 'response' => [ + { + 'id' => '123', + 'first_name' => 'Noam', + 'last_name' => 'Ben Ari' + } + ], + 'user' => { + 'name' => 'Sonny Whether', + 'id' => '123', + 'email' => 'bobby@example.com' + }, + # response for wechat auth + 'unionid' => '123', + # response for qq auth + 'openid' => '123', + # response for instagram + 'data' => { + 'username' => 'pnmahoney', + 'bio' => 'turn WHAT down?', + 'website' => '', + 'profile_picture' => 'http://photos-d.ak.instagram.com/hphotos-ak-xpa1/10454121_417985815007395_867850883_a.jpg', + 'full_name' => 'Patrick Mahoney', + 'counts' => { + 'media' => 2, + 'followed_by' => 100, + 'follows' => 71 + }, + 'id' => '123' + } + }.to_json + } allow(access_token).to receive(:get) { response } allow(access_token).to receive(:token) { '187041a618229fdaf16613e96e1caabc1e86e46bbfad228de41520e63fe45873684c365a14417289599f3' } # access_token params for VK auth @@ -412,37 +486,76 @@ def stub_all_oauth2_requests! end def set_external_property - sorcery_controller_property_set(:external_providers, [:facebook, :github, :google, :liveid, :vk, :salesforce, :paypal, :slack, :wechat, :microsoft]) - sorcery_controller_external_property_set(:facebook, :key, "eYVNBjBDi33aa9GkA3w") - sorcery_controller_external_property_set(:facebook, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") - sorcery_controller_external_property_set(:facebook, :callback_url, "http://blabla.com") - sorcery_controller_external_property_set(:github, :key, "eYVNBjBDi33aa9GkA3w") - sorcery_controller_external_property_set(:github, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") - sorcery_controller_external_property_set(:github, :callback_url, "http://blabla.com") - sorcery_controller_external_property_set(:google, :key, "eYVNBjBDi33aa9GkA3w") - sorcery_controller_external_property_set(:google, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") - sorcery_controller_external_property_set(:google, :callback_url, "http://blabla.com") - sorcery_controller_external_property_set(:liveid, :key, "eYVNBjBDi33aa9GkA3w") - sorcery_controller_external_property_set(:liveid, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") - sorcery_controller_external_property_set(:liveid, :callback_url, "http://blabla.com") - sorcery_controller_external_property_set(:vk, :key, "eYVNBjBDi33aa9GkA3w") - sorcery_controller_external_property_set(:vk, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") - sorcery_controller_external_property_set(:vk, :callback_url, "http://blabla.com") - sorcery_controller_external_property_set(:salesforce, :key, "eYVNBjBDi33aa9GkA3w") - sorcery_controller_external_property_set(:salesforce, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") - sorcery_controller_external_property_set(:salesforce, :callback_url, "http://blabla.com") - sorcery_controller_external_property_set(:paypal, :key, "eYVNBjBDi33aa9GkA3w") - sorcery_controller_external_property_set(:paypal, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") - sorcery_controller_external_property_set(:paypal, :callback_url, "http://blabla.com") - sorcery_controller_external_property_set(:slack, :key, "eYVNBjBDi33aa9GkA3w") - sorcery_controller_external_property_set(:slack, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") - sorcery_controller_external_property_set(:slack, :callback_url, "http://blabla.com") - sorcery_controller_external_property_set(:wechat, :key, "eYVNBjBDi33aa9GkA3w") - sorcery_controller_external_property_set(:wechat, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") - sorcery_controller_external_property_set(:wechat, :callback_url, "http://blabla.com") - sorcery_controller_external_property_set(:microsoft, :key, "eYVNBjBDi33aa9GkA3w") - sorcery_controller_external_property_set(:microsoft, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") - sorcery_controller_external_property_set(:microsoft, :callback_url, "http://blabla.com") + sorcery_controller_property_set( + :external_providers, + %i[ + facebook + github + google + liveid + vk + salesforce + paypal + slack + wechat + microsoft + instagram + auth0 + line + discord + battlenet + qq + ] + ) + sorcery_controller_external_property_set(:facebook, :key, 'eYVNBjBDi33aa9GkA3w') + sorcery_controller_external_property_set(:facebook, :secret, 'XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8') + sorcery_controller_external_property_set(:facebook, :callback_url, 'http://blabla.com') + sorcery_controller_external_property_set(:github, :key, 'eYVNBjBDi33aa9GkA3w') + sorcery_controller_external_property_set(:github, :secret, 'XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8') + sorcery_controller_external_property_set(:github, :callback_url, 'http://blabla.com') + sorcery_controller_external_property_set(:google, :key, 'eYVNBjBDi33aa9GkA3w') + sorcery_controller_external_property_set(:google, :secret, 'XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8') + sorcery_controller_external_property_set(:google, :callback_url, 'http://blabla.com') + sorcery_controller_external_property_set(:liveid, :key, 'eYVNBjBDi33aa9GkA3w') + sorcery_controller_external_property_set(:liveid, :secret, 'XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8') + sorcery_controller_external_property_set(:liveid, :callback_url, 'http://blabla.com') + sorcery_controller_external_property_set(:vk, :key, 'eYVNBjBDi33aa9GkA3w') + sorcery_controller_external_property_set(:vk, :secret, 'XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8') + sorcery_controller_external_property_set(:vk, :callback_url, 'http://blabla.com') + sorcery_controller_external_property_set(:salesforce, :key, 'eYVNBjBDi33aa9GkA3w') + sorcery_controller_external_property_set(:salesforce, :secret, 'XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8') + sorcery_controller_external_property_set(:salesforce, :callback_url, 'http://blabla.com') + sorcery_controller_external_property_set(:paypal, :key, 'eYVNBjBDi33aa9GkA3w') + sorcery_controller_external_property_set(:paypal, :secret, 'XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8') + sorcery_controller_external_property_set(:paypal, :callback_url, 'http://blabla.com') + sorcery_controller_external_property_set(:slack, :key, 'eYVNBjBDi33aa9GkA3w') + sorcery_controller_external_property_set(:slack, :secret, 'XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8') + sorcery_controller_external_property_set(:slack, :callback_url, 'http://blabla.com') + sorcery_controller_external_property_set(:wechat, :key, 'eYVNBjBDi33aa9GkA3w') + sorcery_controller_external_property_set(:wechat, :secret, 'XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8') + sorcery_controller_external_property_set(:wechat, :callback_url, 'http://blabla.com') + sorcery_controller_external_property_set(:microsoft, :key, 'eYVNBjBDi33aa9GkA3w') + sorcery_controller_external_property_set(:microsoft, :secret, 'XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8') + sorcery_controller_external_property_set(:microsoft, :callback_url, 'http://blabla.com') + sorcery_controller_external_property_set(:instagram, :key, 'eYVNBjBDi33aa9GkA3w') + sorcery_controller_external_property_set(:instagram, :secret, 'XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8') + sorcery_controller_external_property_set(:instagram, :callback_url, 'http://blabla.com') + sorcery_controller_external_property_set(:auth0, :key, 'eYVNBjBDi33aa9GkA3w') + sorcery_controller_external_property_set(:auth0, :secret, 'XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8') + sorcery_controller_external_property_set(:auth0, :callback_url, 'http://blabla.com') + sorcery_controller_external_property_set(:auth0, :site, 'https://sorcery-test.auth0.com') + sorcery_controller_external_property_set(:line, :key, "eYVNBjBDi33aa9GkA3w") + sorcery_controller_external_property_set(:line, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") + sorcery_controller_external_property_set(:line, :callback_url, "http://blabla.com") + sorcery_controller_external_property_set(:discord, :key, 'eYVNBjBDi33aa9GkA3w') + sorcery_controller_external_property_set(:discord, :secret, 'XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8') + sorcery_controller_external_property_set(:discord, :callback_url, 'http://blabla.com') + sorcery_controller_external_property_set(:battlenet, :key, '4c43d4862c774ca5bbde89873bf0d338') + sorcery_controller_external_property_set(:battlenet, :secret, 'TxY7IwKOykACd8kUxPyVGTqBs44UBDdX') + sorcery_controller_external_property_set(:battlenet, :callback_url, 'http://blabla.com') + sorcery_controller_external_property_set(:qq, :key, "eYVNBjBDi33aa9GkA3w") + sorcery_controller_external_property_set(:qq, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") + sorcery_controller_external_property_set(:qq, :callback_url, "http://blabla.com") end def provider_url(provider) @@ -455,7 +568,12 @@ def provider_url(provider) salesforce: "https://login.salesforce.com/services/oauth2/authorize?client_id=#{::Sorcery::Controller::Config.salesforce.key}&display&redirect_uri=http%3A%2F%2Fblabla.com&response_type=code&scope#{'=' + ::Sorcery::Controller::Config.salesforce.scope unless ::Sorcery::Controller::Config.salesforce.scope.nil?}&state", slack: "https://slack.com/oauth/authorize?client_id=#{::Sorcery::Controller::Config.slack.key}&display&redirect_uri=http%3A%2F%2Fblabla.com&response_type=code&scope=identity.basic%2C+identity.email&state", wechat: "https://open.weixin.qq.com/connect/qrconnect?appid=#{::Sorcery::Controller::Config.wechat.key}&redirect_uri=http%3A%2F%2Fblabla.com&response_type=code&scope=snsapi_login&state=#wechat_redirect", - microsoft: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=#{::Sorcery::Controller::Config.microsoft.key}&display&redirect_uri=http%3A%2F%2Fblabla.com&response_type=code&scope=openid+email+https%3A%2F%2Fgraph.microsoft.com%2FUser.Read&state" + microsoft: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=#{::Sorcery::Controller::Config.microsoft.key}&display&redirect_uri=http%3A%2F%2Fblabla.com&response_type=code&scope=openid+email+https%3A%2F%2Fgraph.microsoft.com%2FUser.Read&state", + instagram: "https://api.instagram.com/oauth/authorize?client_id=#{::Sorcery::Controller::Config.instagram.key}&display&redirect_uri=http%3A%2F%2Fblabla.com&response_type=code&scope=#{::Sorcery::Controller::Config.instagram.scope}&state", + auth0: "https://sorcery-test.auth0.com/authorize?client_id=#{::Sorcery::Controller::Config.auth0.key}&display&redirect_uri=http%3A%2F%2Fblabla.com&response_type=code&scope=openid+profile+email&state", + discord: "https://discordapp.com/api/oauth2/authorize?client_id=#{::Sorcery::Controller::Config.discord.key}&display&redirect_uri=http%3A%2F%2Fblabla.com&response_type=code&scope=identify&state", + battlenet: "https://eu.battle.net/oauth/authorize?client_id=#{::Sorcery::Controller::Config.battlenet.key}&display&redirect_uri=http%3A%2F%2Fblabla.com&response_type=code&scope=openid&state", + qq: "https://graph.qq.com/oauth2.0/authorize?client_id=#{::Sorcery::Controller::Config.wechat.key}&redirect_uri=http%3A%2F%2Fblabla.com&response_type=code&scope=get_user_info&state=#qq_redirect", }[provider] end end diff --git a/spec/controllers/controller_oauth_spec.rb b/spec/controllers/controller_oauth_spec.rb index c796f084..1cac58bf 100644 --- a/spec/controllers/controller_oauth_spec.rb +++ b/spec/controllers/controller_oauth_spec.rb @@ -20,7 +20,7 @@ def stub_all_oauth_requests! 'in_reply_to_status_id_str' => nil, 'created_at' => 'Sun Mar 06 23:01:12 +0000 2011', 'contributors' => nil, 'place' => nil, 'retweeted' => false, 'in_reply_to_status_id' => nil, 'in_reply_to_user_id_str' => nil, 'coordinates' => nil, 'retweet_count' => 0, - 'id' => 44533012284706816, 'id_str' => '44533012284706816' + 'id' => 44_533_012_284_706_816, 'id_str' => '44533012284706816' }, 'show_all_inline_media' => false, 'geo_enabled' => true, 'profile_sidebar_border_color' => 'a8c7f7', 'url' => nil, 'followers_count' => 10, @@ -32,7 +32,7 @@ def stub_all_oauth_requests! 'is_translator' => false, 'contributors_enabled' => false, 'protected' => false, 'follow_request_sent' => false, 'time_zone' => 'Greenland', 'profile_text_color' => '333333', 'name' => 'Noam Ben Ari', 'friends_count' => 10, 'profile_sidebar_fill_color' => 'C0DFEC', - 'id' => 123, 'id_str' => '91434812', 'profile_background_tile' => false, 'utc_offset' => -10800 + 'id' => 123, 'id_str' => '91434812', 'profile_background_tile' => false, 'utc_offset' => -10_800 }.to_json session[:request_token] = req_token.token @@ -50,7 +50,7 @@ def stub_all_oauth_requests! before(:all) do sorcery_reload!([:external]) - sorcery_controller_property_set(:external_providers, [:twitter, :jira]) + sorcery_controller_property_set(:external_providers, %i[twitter jira]) sorcery_controller_external_property_set(:twitter, :key, 'eYVNBjBDi33aa9GkA3w') sorcery_controller_external_property_set(:twitter, :secret, 'XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8') sorcery_controller_external_property_set(:twitter, :callback_url, 'http://blabla.com') @@ -84,11 +84,17 @@ def stub_all_oauth_requests! end context 'when callback_url begin with http://' do + before do + sorcery_controller_external_property_set(:twitter, :callback_url, '/oauth/twitter/callback') + end it 'login_at redirects correctly', pending: true do get :login_at_test expect(response).to be_a_redirect expect(response).to redirect_to('http://myapi.com/oauth/authorize?oauth_callback=http%3A%2F%2Fblabla.com&oauth_token=') end + after do + sorcery_controller_external_property_set(:twitter, :callback_url, 'http://blabla.com') + end end it 'logins if user exists' do @@ -179,7 +185,7 @@ def stub_all_oauth_requests! describe SorceryController, 'OAuth with user activation features' do before(:all) do - sorcery_reload!([:activity_logging, :external]) + sorcery_reload!(%i[activity_logging external]) end context 'when twitter' do @@ -215,16 +221,16 @@ def stub_all_oauth_requests! describe SorceryController, 'OAuth with session timeout features' do before(:all) do if SORCERY_ORM == :active_record - ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate/external") + MigrationHelper.migrate("#{Rails.root}/db/migrate/external") User.reset_column_information end - sorcery_reload!([:session_timeout, :external]) + sorcery_reload!(%i[session_timeout external]) end after(:all) do if SORCERY_ORM == :active_record - ActiveRecord::Migrator.rollback("#{Rails.root}/db/migrate/external") + MigrationHelper.rollback("#{Rails.root}/db/migrate/external") end end diff --git a/spec/controllers/controller_remember_me_spec.rb b/spec/controllers/controller_remember_me_spec.rb index b9908764..386060e3 100644 --- a/spec/controllers/controller_remember_me_spec.rb +++ b/spec/controllers/controller_remember_me_spec.rb @@ -6,12 +6,18 @@ # ----------------- REMEMBER ME ----------------------- context 'with remember me features' do before(:all) do + if SORCERY_ORM == :active_record + MigrationHelper.migrate("#{Rails.root}/db/migrate/remember_me") + User.reset_column_information + end + sorcery_reload!([:remember_me]) end - after(:each) do - session = nil - cookies = nil + after(:all) do + if SORCERY_ORM == :active_record + MigrationHelper.rollback("#{Rails.root}/db/migrate/remember_me") + end end before(:each) do @@ -31,17 +37,17 @@ end it 'clears cookie on forget_me!' do - cookies['remember_me_token'] == { value: 'asd54234dsfsd43534', expires: 3600 } - get :test_logout + request.cookies[:remember_me_token] = { value: 'asd54234dsfsd43534', expires: 3600 } + get :test_logout_with_forget_me - expect(cookies['remember_me_token']).to be_nil + expect(response.cookies[:remember_me_token]).to be_nil end it 'clears cookie on force_forget_me!' do - cookies['remember_me_token'] == { value: 'asd54234dsfsd43534', expires: 3600 } + request.cookies[:remember_me_token] = { value: 'asd54234dsfsd43534', expires: 3600 } get :test_logout_with_force_forget_me - expect(cookies['remember_me_token']).to be_nil + expect(response.cookies[:remember_me_token]).to be_nil end it 'login(email,password,remember_me) logs user in and remembers' do @@ -80,6 +86,8 @@ expect(User.sorcery_adapter).to receive(:find_by_remember_me_token).with('token').and_return(user) + expect(subject).to receive(:after_remember_me!).with(user) + get :test_login_from_cookie expect(assigns[:current_user]).to eq user diff --git a/spec/controllers/controller_session_timeout_spec.rb b/spec/controllers/controller_session_timeout_spec.rb index 3abc2eda..43e565e2 100644 --- a/spec/controllers/controller_session_timeout_spec.rb +++ b/spec/controllers/controller_session_timeout_spec.rb @@ -24,7 +24,7 @@ get :test_should_be_logged_in expect(session[:user_id]).not_to be_nil - expect(response).to be_a_success + expect(response).to be_successful end it 'resets session after session timeout' do @@ -36,6 +36,87 @@ expect(response).to be_a_redirect end + context "with 'invalidate_active_sessions_enabled'" do + it 'does not reset the session if invalidate_sessions_before is nil' do + sorcery_controller_property_set(:session_timeout_invalidate_active_sessions_enabled, true) + login_user user + allow(user).to receive(:invalidate_sessions_before) { nil } + + get :test_should_be_logged_in + + expect(session[:user_id]).not_to be_nil + expect(response).to be_successful + end + + it 'does not reset the session if it was not created before invalidate_sessions_before' do + sorcery_controller_property_set(:session_timeout_invalidate_active_sessions_enabled, true) + login_user user + allow(user).to receive(:invalidate_sessions_before) { Time.now.in_time_zone - 10.minutes } + + get :test_should_be_logged_in + + expect(session[:user_id]).not_to be_nil + expect(response).to be_successful + end + + it 'resets the session if the session was created before invalidate_sessions_before' do + sorcery_controller_property_set(:session_timeout_invalidate_active_sessions_enabled, true) + login_user user + allow(user).to receive(:invalidate_sessions_before) { Time.now.in_time_zone } + get :test_should_be_logged_in + + expect(session[:user_id]).to be_nil + expect(response).to be_a_redirect + end + + it 'resets active sessions on next action if invalidate_active_sessions! is called' do + sorcery_controller_property_set(:session_timeout_invalidate_active_sessions_enabled, true) + # precondition that the user is logged in + login_user user + get :test_should_be_logged_in + expect(response).to be_successful + + allow(user).to receive(:send) { |_method, value| allow(user).to receive(:invalidate_sessions_before) { value } } + allow(user).to receive(:save) + get :test_invalidate_active_session + expect(response).to be_successful + + get :test_should_be_logged_in + expect(session[:user_id]).to be_nil + expect(response).to be_a_redirect + end + + it 'allows login after invalidate_active_sessions! is called' do + sorcery_controller_property_set(:session_timeout_invalidate_active_sessions_enabled, true) + # precondition that the user is logged in + login_user user + get :test_should_be_logged_in + expect(response).to be_successful + + allow(user).to receive(:send) { |_method, value| allow(user).to receive(:invalidate_sessions_before) { value } } + allow(user).to receive(:save) + # Call to invalidate + get :test_invalidate_active_session + expect(response).to be_successful + + # Check that existing sessions were logged out + get :test_should_be_logged_in + expect(session[:user_id]).to be_nil + expect(response).to be_a_redirect + + # Check that new session is allowed to login + login_user user + get :test_should_be_logged_in + expect(response).to be_successful + expect(session[:user_id]).not_to be_nil + + # Check an additional request to make sure not logged out on next request + get :test_should_be_logged_in + expect(response).to be_successful + expect(session[:user_id]).not_to be_nil + end + end + it 'works if the session is stored as a string or a Time' do session[:login_time] = Time.now.to_s # TODO: ??? @@ -44,7 +125,7 @@ get :test_login, params: { email: 'bla@bla.com', password: 'secret' } expect(session[:user_id]).not_to be_nil - expect(response).to be_a_success + expect(response).to be_successful end context "with 'session_timeout_from_last_action'" do @@ -62,7 +143,7 @@ get :test_should_be_logged_in expect(session[:user_id]).not_to be_nil - expect(response).to be_a_success + expect(response).to be_successful end it "with 'session_timeout_from_last_action' logs out if there was no activity" do @@ -75,5 +156,11 @@ expect(response).to be_a_redirect end end + + it 'registers login time on remember_me callback' do + expect(subject).to receive(:register_login_time).with(user) + + subject.send(:after_remember_me!, user) + end end end diff --git a/spec/controllers/controller_spec.rb b/spec/controllers/controller_spec.rb index a1ba1744..def1891b 100644 --- a/spec/controllers/controller_spec.rb +++ b/spec/controllers/controller_spec.rb @@ -132,7 +132,7 @@ sorcery_controller_property_set(:not_authenticated_action, :test_not_authenticated_action) get :test_logout - expect(response).to be_a_success + expect(response).to be_successful end it 'require_login before_action saves the url that the user originally wanted' do @@ -143,13 +143,23 @@ end it 'require_login before_action does not save the url that the user originally wanted upon all non-get http methods' do - [:post, :put, :delete].each do |m| + %i[post put delete].each do |m| send(m, :some_action) expect(session[:return_to_url]).to be_nil end end + it 'require_login before_action does not save the url for JSON requests' do + get :some_action, format: :json + expect(session[:return_to_url]).to be_nil + end + + it 'require_login before_action does not save the url for XHR requests' do + get :some_action, xhr: true + expect(session[:return_to_url]).to be_nil + end + it 'on successful login the user is redirected to the url he originally wanted' do session[:return_to_url] = 'http://test.host/some_action' post :test_return_to, params: { email: 'bla@bla.com', password: 'secret' } @@ -161,7 +171,7 @@ # --- auto_login(user) --- specify { should respond_to(:auto_login) } - it 'auto_login(user) los in a user instance' do + it 'auto_login(user) logs in a user instance' do session[:user_id] = nil subject.auto_login(user) diff --git a/spec/orm/active_record.rb b/spec/orm/active_record.rb index 50a1c874..9aec5857 100644 --- a/spec/orm/active_record.rb +++ b/spec/orm/active_record.rb @@ -9,11 +9,11 @@ class TestUser < ActiveRecord::Base end def setup_orm - ActiveRecord::Migrator.migrate(migrations_path) + MigrationHelper.migrate(migrations_path) end def teardown_orm - ActiveRecord::Migrator.rollback(migrations_path) + MigrationHelper.rollback(migrations_path) end def migrations_path diff --git a/spec/providers/example_provider_spec.rb b/spec/providers/example_provider_spec.rb new file mode 100644 index 00000000..c8317f98 --- /dev/null +++ b/spec/providers/example_provider_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'sorcery/providers/base' + +describe Sorcery::Providers::ExampleProvider do + before(:all) do + sorcery_reload!([:external]) + sorcery_controller_property_set(:external_providers, [:example_provider]) + end + + context 'fetching a multi-word custom provider' do + it 'returns the provider' do + expect(Sorcery::Controller::Config.example_provider).to be_a(Sorcery::Providers::ExampleProvider) + end + end +end diff --git a/spec/providers/example_spec.rb b/spec/providers/example_spec.rb new file mode 100644 index 00000000..a022695c --- /dev/null +++ b/spec/providers/example_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'sorcery/providers/base' + +describe Sorcery::Providers::Example do + before(:all) do + sorcery_reload!([:external]) + sorcery_controller_property_set(:external_providers, [:example]) + end + + context 'fetching a single-word custom provider' do + it 'returns the provider' do + expect(Sorcery::Controller::Config.example).to be_a(Sorcery::Providers::Example) + end + end +end diff --git a/spec/providers/vk_spec.rb b/spec/providers/vk_spec.rb new file mode 100644 index 00000000..2a918d48 --- /dev/null +++ b/spec/providers/vk_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' +require 'sorcery/providers/base' +require 'sorcery/providers/vk' +require 'webmock/rspec' + +describe Sorcery::Providers::Vk do + include WebMock::API + + let(:provider) { Sorcery::Controller::Config.vk } + + before(:all) do + sorcery_reload!([:external]) + sorcery_controller_property_set(:external_providers, [:vk]) + sorcery_controller_external_property_set(:vk, :key, 'KEY') + sorcery_controller_external_property_set(:vk, :secret, 'SECRET') + end + + def stub_vk_authorize + stub_request(:post, %r{https\:\/\/oauth\.vk\.com\/access_token}).to_return( + status: 200, + body: '{"access_token":"TOKEN","expires_in":86329,"user_id":1}', + headers: { 'content-type' => 'application/json' } + ) + end + + context 'getting user info hash' do + it 'should provide VK API version' do + stub_vk_authorize + sorcery_controller_external_property_set(:vk, :api_version, '5.71') + + get_user = stub_request( + :get, + 'https://api.vk.com/method/getProfiles?access_token=TOKEN&fields=&scope=email&uids=1&v=5.71' + ).to_return(body: '{"response":[{"id":1}]}') + + token = provider.process_callback({ code: 'CODE' }, nil) + provider.get_user_hash(token) + + expect(get_user).to have_been_requested + end + end +end diff --git a/spec/rails_app/app/assets/config/manifest.js b/spec/rails_app/app/assets/config/manifest.js new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/spec/rails_app/app/assets/config/manifest.js @@ -0,0 +1 @@ +{} diff --git a/spec/rails_app/app/controllers/application_controller.rb b/spec/rails_app/app/controllers/application_controller.rb new file mode 100644 index 00000000..09705d12 --- /dev/null +++ b/spec/rails_app/app/controllers/application_controller.rb @@ -0,0 +1,2 @@ +class ApplicationController < ActionController::Base +end diff --git a/spec/rails_app/app/controllers/sorcery_controller.rb b/spec/rails_app/app/controllers/sorcery_controller.rb index 1d916fe5..69be072e 100644 --- a/spec/rails_app/app/controllers/sorcery_controller.rb +++ b/spec/rails_app/app/controllers/sorcery_controller.rb @@ -1,10 +1,16 @@ require 'oauth' -class SorceryController < ActionController::Base +class SorceryController < ApplicationController protect_from_forgery before_action :require_login_from_http_basic, only: [:test_http_basic_auth] - before_action :require_login, only: [:test_logout, :test_logout_with_force_forget_me, :test_should_be_logged_in, :some_action] + before_action :require_login, only: %i[ + test_logout + test_logout_with_forget_me + test_logout_with_force_forget_me + test_should_be_logged_in + some_action + ] def index; end @@ -45,6 +51,13 @@ def test_logout_with_remember head :ok end + def test_logout_with_forget_me + remember_me! + forget_me! + logout + head :ok + end + def test_logout_with_force_forget_me remember_me! force_forget_me! @@ -52,6 +65,11 @@ def test_logout_with_force_forget_me head :ok end + def test_invalidate_active_session + invalidate_active_sessions! + head :ok + end + def test_login_with_remember @user = login(params[:email], params[:password]) remember_me! @@ -108,6 +126,10 @@ def login_at_test_microsoft login_at(:microsoft) end + def login_at_test_qq + login_at(:qq) + end + def login_at_test_google login_at(:google) end @@ -132,12 +154,32 @@ def login_at_test_slack login_at(:slack) end + def login_at_test_line + login_at(:line) + end + def login_at_test_with_state login_at(:facebook, state: 'bla') end + def login_at_test_instagram + login_at(:instagram) + end + + def login_at_test_auth0 + login_at(:auth0) + end + + def login_at_test_discord + login_at(:discord) + end + + def login_at_test_battlenet + login_at(:battlenet) + end + def test_login_from_twitter - if @user = login_from(:twitter) + if (@user = login_from(:twitter)) redirect_to 'bla', notice: 'Success!' else redirect_to 'blu', alert: 'Failed!' @@ -147,7 +189,7 @@ def test_login_from_twitter alias test_login_from test_login_from_twitter def test_login_from_facebook - if @user = login_from(:facebook) + if (@user = login_from(:facebook)) redirect_to 'bla', notice: 'Success!' else redirect_to 'blu', alert: 'Failed!' @@ -155,7 +197,7 @@ def test_login_from_facebook end def test_login_from_github - if @user = login_from(:github) + if (@user = login_from(:github)) redirect_to 'bla', notice: 'Success!' else redirect_to 'blu', alert: 'Failed!' @@ -163,7 +205,7 @@ def test_login_from_github end def test_login_from_paypal - if @user = login_from(:paypal) + if (@user = login_from(:paypal)) redirect_to 'bla', notice: 'Success!' else redirect_to 'blu', alert: 'Failed!' @@ -171,7 +213,23 @@ def test_login_from_paypal end def test_login_from_wechat - if @user = login_from(:wechat) + if (@user = login_from(:wechat)) + redirect_to 'bla', notice: 'Success!' + else + redirect_to 'blu', alert: 'Failed!' + end + end + + def test_login_from_qq + if @user = login_from(:qq) + redirect_to 'bla', notice: 'Success!' + else + redirect_to 'blu', alert: 'Failed!' + end + end + + def test_login_from_qq + if @user = login_from(:qq) redirect_to 'bla', notice: 'Success!' else redirect_to 'blu', alert: 'Failed!' @@ -179,7 +237,7 @@ def test_login_from_wechat end def test_login_from_microsoft - if @user = login_from(:microsoft) + if (@user = login_from(:microsoft)) redirect_to 'bla', notice: 'Success!' else redirect_to 'blu', alert: 'Failed!' @@ -187,7 +245,7 @@ def test_login_from_microsoft end def test_login_from_google - if @user = login_from(:google) + if (@user = login_from(:google)) redirect_to 'bla', notice: 'Success!' else redirect_to 'blu', alert: 'Failed!' @@ -195,7 +253,7 @@ def test_login_from_google end def test_login_from_liveid - if @user = login_from(:liveid) + if (@user = login_from(:liveid)) redirect_to 'bla', notice: 'Success!' else redirect_to 'blu', alert: 'Failed!' @@ -203,7 +261,7 @@ def test_login_from_liveid end def test_login_from_vk - if @user = login_from(:vk) + if (@user = login_from(:vk)) redirect_to 'bla', notice: 'Success!' else redirect_to 'blu', alert: 'Failed!' @@ -211,7 +269,7 @@ def test_login_from_vk end def test_login_from_jira - if @user = login_from(:jira) + if (@user = login_from(:jira)) redirect_to 'bla', notice: 'Success!' else redirect_to 'blu', alert: 'Failed!' @@ -219,7 +277,7 @@ def test_login_from_jira end def test_login_from_salesforce - if @user = login_from(:salesforce) + if (@user = login_from(:salesforce)) redirect_to 'bla', notice: 'Success!' else redirect_to 'blu', alert: 'Failed!' @@ -227,7 +285,47 @@ def test_login_from_salesforce end def test_login_from_slack - if @user = login_from(:slack) + if (@user = login_from(:slack)) + redirect_to 'bla', notice: 'Success!' + else + redirect_to 'blu', alert: 'Failed!' + end + end + + def test_login_from_instagram + if (@user = login_from(:instagram)) + redirect_to 'bla', notice: 'Success!' + else + redirect_to 'blu', alert: 'Failed!' + end + end + + def test_login_from_auth0 + if (@user = login_from(:auth0)) + redirect_to 'bla', notice: 'Success!' + else + redirect_to 'blu', alert: 'Failed!' + end + end + + def test_login_from_line + if @user = login_from(:line) + redirect_to 'bla', notice: 'Success!' + else + redirect_to 'blu', alert: 'Failed!' + end + end + + def test_login_from_discord + if (@user = login_from(:discord)) + redirect_to 'bla', notice: 'Success!' + else + redirect_to 'blu', alert: 'Failed!' + end + end + + def test_login_from_battlenet + if (@user = login_from(:battlenet)) redirect_to 'bla', notice: 'Success!' else redirect_to 'blu', alert: 'Failed!' @@ -235,7 +333,7 @@ def test_login_from_slack end def test_return_to_with_external_twitter - if @user = login_from(:twitter) + if (@user = login_from(:twitter)) redirect_back_or_to 'bla', notice: 'Success!' else redirect_to 'blu', alert: 'Failed!' @@ -243,7 +341,7 @@ def test_return_to_with_external_twitter end def test_return_to_with_external_jira - if @user = login_from(:jira) + if (@user = login_from(:jira)) redirect_back_or_to 'bla', notice: 'Success!' else redirect_to 'blu', alert: 'Failed!' @@ -253,7 +351,7 @@ def test_return_to_with_external_jira alias test_return_to_with_external test_return_to_with_external_twitter def test_return_to_with_external_facebook - if @user = login_from(:facebook) + if (@user = login_from(:facebook)) redirect_back_or_to 'bla', notice: 'Success!' else redirect_to 'blu', alert: 'Failed!' @@ -261,7 +359,7 @@ def test_return_to_with_external_facebook end def test_return_to_with_external_github - if @user = login_from(:github) + if (@user = login_from(:github)) redirect_back_or_to 'bla', notice: 'Success!' else redirect_to 'blu', alert: 'Failed!' @@ -269,7 +367,7 @@ def test_return_to_with_external_github end def test_return_to_with_external_paypal - if @user = login_from(:paypal) + if (@user = login_from(:paypal)) redirect_back_or_to 'bla', notice: 'Success!' else redirect_to 'blu', alert: 'Failed!' @@ -277,7 +375,7 @@ def test_return_to_with_external_paypal end def test_return_to_with_external_wechat - if @user = login_from(:wechat) + if (@user = login_from(:wechat)) redirect_back_or_to 'bla', notice: 'Success!' else redirect_to 'blu', alert: 'Failed!' @@ -285,7 +383,23 @@ def test_return_to_with_external_wechat end def test_return_to_with_external_microsoft - if @user = login_from(:microsoft) + if (@user = login_from(:microsoft)) + redirect_back_or_to 'bla', notice: 'Success!' + else + redirect_to 'blu', alert: 'Failed!' + end + end + + def test_return_to_with_external_qq + if @user = login_from(:qq) + redirect_back_or_to 'bla', notice: 'Success!' + else + redirect_to 'blu', alert: 'Failed!' + end + end + + def test_return_to_with_external_qq + if @user = login_from(:qq) redirect_back_or_to 'bla', notice: 'Success!' else redirect_to 'blu', alert: 'Failed!' @@ -293,7 +407,7 @@ def test_return_to_with_external_microsoft end def test_return_to_with_external_google - if @user = login_from(:google) + if (@user = login_from(:google)) redirect_back_or_to 'bla', notice: 'Success!' else redirect_to 'blu', alert: 'Failed!' @@ -301,7 +415,7 @@ def test_return_to_with_external_google end def test_return_to_with_external_liveid - if @user = login_from(:liveid) + if (@user = login_from(:liveid)) redirect_back_or_to 'bla', notice: 'Success!' else redirect_to 'blu', alert: 'Failed!' @@ -309,7 +423,7 @@ def test_return_to_with_external_liveid end def test_return_to_with_external_vk - if @user = login_from(:vk) + if (@user = login_from(:vk)) redirect_back_or_to 'bla', notice: 'Success!' else redirect_to 'blu', alert: 'Failed!' @@ -317,7 +431,7 @@ def test_return_to_with_external_vk end def test_return_to_with_external_salesforce - if @user = login_from(:salesforce) + if (@user = login_from(:salesforce)) redirect_back_or_to 'bla', notice: 'Success!' else redirect_to 'blu', alert: 'Failed!' @@ -325,7 +439,47 @@ def test_return_to_with_external_salesforce end def test_return_to_with_external_slack - if @user = login_from(:slack) + if (@user = login_from(:slack)) + redirect_back_or_to 'bla', notice: 'Success!' + else + redirect_to 'blu', alert: 'Failed!' + end + end + + def test_return_to_with_external_instagram + if (@user = login_from(:instagram)) + redirect_back_or_to 'bla', notice: 'Success!' + else + redirect_to 'blu', alert: 'Failed!' + end + end + + def test_return_to_with_external_auth0 + if (@user = login_from(:auth0)) + redirect_back_or_to 'bla', notice: 'Success!' + else + redirect_to 'blu', alert: 'Failed!' + end + end + + def test_return_to_with_external_line + if @user = login_from(:line) + redirect_back_or_to 'bla', notice: 'Success!' + else + redirect_to 'blu', alert: 'Failed!' + end + end + + def test_return_to_with_external_discord + if (@user = login_from(:discord)) + redirect_back_or_to 'bla', notice: 'Success!' + else + redirect_to 'blu', alert: 'Failed!' + end + end + + def test_return_to_with_external_battlenet + if (@user = login_from(:battlenet)) redirect_back_or_to 'bla', notice: 'Success!' else redirect_to 'blu', alert: 'Failed!' @@ -335,7 +489,7 @@ def test_return_to_with_external_slack def test_create_from_provider provider = params[:provider] login_from(provider) - if @user = create_from(provider) + if (@user = create_from(provider)) redirect_to 'bla', notice: 'Success!' else redirect_to 'blu', alert: 'Failed!' @@ -344,12 +498,13 @@ def test_create_from_provider def test_add_second_provider provider = params[:provider] - if logged_in? - if @user = add_provider_to_user(provider) - redirect_to 'bla', notice: 'Success!' - else - redirect_to 'blu', alert: 'Failed!' - end + + return unless logged_in? + + if (@user = add_provider_to_user(provider)) + redirect_to 'bla', notice: 'Success!' + else + redirect_to 'blu', alert: 'Failed!' end end diff --git a/spec/rails_app/app/mailers/sorcery_mailer.rb b/spec/rails_app/app/mailers/sorcery_mailer.rb index 4a415d43..88ca918f 100644 --- a/spec/rails_app/app/mailers/sorcery_mailer.rb +++ b/spec/rails_app/app/mailers/sorcery_mailer.rb @@ -28,4 +28,11 @@ def send_unlock_token_email(user) mail(to: user.email, subject: 'Your account has been locked due to many wrong logins') end + + def magic_login_email(user) + @user = user + @url = 'http://example.com/login' + mail(to: user.email, + subject: 'Magic Login') + end end diff --git a/spec/rails_app/app/views/sorcery_mailer/magic_login_email.html.erb b/spec/rails_app/app/views/sorcery_mailer/magic_login_email.html.erb new file mode 100644 index 00000000..cf8243f3 --- /dev/null +++ b/spec/rails_app/app/views/sorcery_mailer/magic_login_email.html.erb @@ -0,0 +1,13 @@ + + + + + + +

Hello, <%= @user.username %>

+

+ To login without a password, just follow this link: <%= @url %>. +

+

Have a great day!

+ + diff --git a/spec/rails_app/app/views/sorcery_mailer/magic_login_email.text.erb b/spec/rails_app/app/views/sorcery_mailer/magic_login_email.text.erb new file mode 100644 index 00000000..64be0dd2 --- /dev/null +++ b/spec/rails_app/app/views/sorcery_mailer/magic_login_email.text.erb @@ -0,0 +1,6 @@ +Hello, <%= @user.username %> +=============================================== + +To login without a password, just follow this link: <%= @url %>. + +Have a great day! diff --git a/spec/rails_app/config/application.rb b/spec/rails_app/config/application.rb index 44b2a9ca..0eeec0db 100644 --- a/spec/rails_app/config/application.rb +++ b/spec/rails_app/config/application.rb @@ -1,4 +1,4 @@ -require File.expand_path('../boot', __FILE__) +require File.expand_path('boot', __dir__) require 'action_controller/railtie' require 'action_mailer/railtie' @@ -6,16 +6,19 @@ Bundler.require :default, SORCERY_ORM +# rubocop:disable Lint/HandleExceptions begin require "#{SORCERY_ORM}/railtie" rescue LoadError + # TODO: Log this issue or change require scheme. end +# rubocop:enable Lint/HandleExceptions require 'sorcery' module AppRoot class Application < Rails::Application - config.autoload_paths.reject! { |p| p =~ /\/app\/(\w+)$/ && !%w(controllers helpers mailers views).include?(Regexp.last_match(1)) } + config.autoload_paths.reject! { |p| p =~ %r{/\/app\/(\w+)$/} && !%w[controllers helpers mailers views].include?(Regexp.last_match(1)) } config.autoload_paths += ["#{config.root}/app/#{SORCERY_ORM}"] # Settings in config/environments/* take precedence over those specified here. @@ -50,7 +53,9 @@ class Application < Rails::Application config.filter_parameters += [:password] config.action_mailer.delivery_method = :test - config.active_support.deprecation = :stderr + if Rails.version >= '5.1.0' && config.active_record.sqlite3.present? + config.active_record.sqlite3.represent_boolean_as_integer = true + end end end diff --git a/spec/rails_app/config/boot.rb b/spec/rails_app/config/boot.rb index eaa6ee87..e06e0ebe 100644 --- a/spec/rails_app/config/boot.rb +++ b/spec/rails_app/config/boot.rb @@ -1,4 +1,4 @@ # Set up gems listed in the Gemfile. -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__) require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) diff --git a/spec/rails_app/config/environment.rb b/spec/rails_app/config/environment.rb index 98a94c6d..37ea24a9 100644 --- a/spec/rails_app/config/environment.rb +++ b/spec/rails_app/config/environment.rb @@ -1,5 +1,5 @@ # Load the rails application -require File.expand_path('../application', __FILE__) +require File.expand_path('application', __dir__) # Initialize the rails application AppRoot::Application.initialize! diff --git a/spec/rails_app/config/initializers/secret_token.rb b/spec/rails_app/config/initializers/secret_token.rb deleted file mode 100644 index 2971ba22..00000000 --- a/spec/rails_app/config/initializers/secret_token.rb +++ /dev/null @@ -1,7 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Your secret key for verifying the integrity of signed cookies. -# If you change this key, all old signed cookies will become invalid! -# Make sure the secret is at least 30 characters and all random, -# no regular words or you'll be exposed to dictionary attacks. -AppRoot::Application.config.secret_token = 'a9789f869a0d0ac2f2b683d6e9410c530696b178bca28a7971f4a652b14ff2da89f2cf4dcbf0355f6bc41f81731aa8e46085674d1acc1980436f61cdba76ff5d' diff --git a/spec/rails_app/config/routes.rb b/spec/rails_app/config/routes.rb index 712d7f6a..8900e069 100644 --- a/spec/rails_app/config/routes.rb +++ b/spec/rails_app/config/routes.rb @@ -11,7 +11,9 @@ get :test_login_from_cookie get :test_login_from get :test_logout_with_remember + get :test_logout_with_forget_me get :test_logout_with_force_forget_me + get :test_invalidate_active_session get :test_should_be_logged_in get :test_create_from_provider get :test_add_second_provider @@ -22,6 +24,7 @@ get :test_login_from_github get :test_login_from_paypal get :test_login_from_wechat + get :test_login_from_qq get :test_login_from_microsoft get :test_login_from_google get :test_login_from_liveid @@ -29,12 +32,18 @@ get :test_login_from_jira get :test_login_from_salesforce get :test_login_from_slack + get :test_login_from_instagram + get :test_login_from_auth0 + get :test_login_from_line + get :test_login_from_discord + get :test_login_from_battlenet get :login_at_test get :login_at_test_twitter get :login_at_test_facebook get :login_at_test_github get :login_at_test_paypal get :login_at_test_wechat + get :login_at_test_qq get :login_at_test_microsoft get :login_at_test_google get :login_at_test_liveid @@ -42,12 +51,18 @@ get :login_at_test_jira get :login_at_test_salesforce get :login_at_test_slack + get :login_at_test_instagram + get :login_at_test_auth0 + get :login_at_test_line + get :login_at_test_discord + get :login_at_test_battlenet get :test_return_to_with_external get :test_return_to_with_external_twitter get :test_return_to_with_external_facebook get :test_return_to_with_external_github get :test_return_to_with_external_paypal get :test_return_to_with_external_wechat + get :test_return_to_with_external_qq get :test_return_to_with_external_microsoft get :test_return_to_with_external_google get :test_return_to_with_external_liveid @@ -55,6 +70,11 @@ get :test_return_to_with_external_jira get :test_return_to_with_external_salesforce get :test_return_to_with_external_slack + get :test_return_to_with_external_instagram + get :test_return_to_with_external_auth0 + get :test_return_to_with_external_line + get :test_return_to_with_external_discord + get :test_return_to_with_external_battlenet get :test_http_basic_auth get :some_action_making_a_non_persisted_change_to_the_user post :test_login_with_remember diff --git a/spec/rails_app/config/secrets.yml b/spec/rails_app/config/secrets.yml new file mode 100644 index 00000000..fdcc61e2 --- /dev/null +++ b/spec/rails_app/config/secrets.yml @@ -0,0 +1,4 @@ +# secrets.yml + +test: + secret_key_base: 'a9789f869a0d0ac2f2b683d6e9410c530696b178bca28a7971f4a652b14ff2da89f2cf4dcbf0355f6bc41f81731aa8e46085674d1acc1980436f61cdba76ff5d' diff --git a/spec/rails_app/db/migrate/activity_logging/20101224223624_add_activity_logging_to_users.rb b/spec/rails_app/db/migrate/activity_logging/20101224223624_add_activity_logging_to_users.rb index 52c0d9e9..ad96e090 100644 --- a/spec/rails_app/db/migrate/activity_logging/20101224223624_add_activity_logging_to_users.rb +++ b/spec/rails_app/db/migrate/activity_logging/20101224223624_add_activity_logging_to_users.rb @@ -5,11 +5,11 @@ def self.up add_column :users, :last_activity_at, :datetime, default: nil add_column :users, :last_login_from_ip_address, :string, default: nil - add_index :users, [:last_logout_at, :last_activity_at] + add_index :users, %i[last_logout_at last_activity_at] end def self.down - remove_index :users, [:last_logout_at, :last_activity_at] + remove_index :users, %i[last_logout_at last_activity_at] remove_column :users, :last_activity_at remove_column :users, :last_logout_at diff --git a/spec/rails_app/db/migrate/invalidate_active_sessions/20180221093235_add_invalidate_active_sessions_before_to_users.rb b/spec/rails_app/db/migrate/invalidate_active_sessions/20180221093235_add_invalidate_active_sessions_before_to_users.rb new file mode 100644 index 00000000..23eef7f3 --- /dev/null +++ b/spec/rails_app/db/migrate/invalidate_active_sessions/20180221093235_add_invalidate_active_sessions_before_to_users.rb @@ -0,0 +1,9 @@ +class AddInvalidateSessionsBeforeToUsers < ActiveRecord::CompatibleLegacyMigration.migration_class + def self.up + add_column :users, :invalidate_sessions_before, :datetime, default: nil + end + + def self.down + remove_column :users, :invalidate_sessions_before + end +end diff --git a/spec/rails_app/db/migrate/magic_login/20170924151831_add_magic_login_to_users.rb b/spec/rails_app/db/migrate/magic_login/20170924151831_add_magic_login_to_users.rb new file mode 100644 index 00000000..446d21fe --- /dev/null +++ b/spec/rails_app/db/migrate/magic_login/20170924151831_add_magic_login_to_users.rb @@ -0,0 +1,17 @@ +class AddMagicLoginToUsers < ActiveRecord::CompatibleLegacyMigration.migration_class + def self.up + add_column :users, :magic_login_token, :string, default: nil + add_column :users, :magic_login_token_expires_at, :datetime, default: nil + add_column :users, :magic_login_email_sent_at, :datetime, default: nil + + add_index :users, :magic_login_token + end + + def self.down + remove_index :users, :magic_login_token + + remove_column :users, :magic_login_token + remove_column :users, :magic_login_token_expires_at + remove_column :users, :magic_login_email_sent_at + end +end diff --git a/spec/rails_app/db/migrate/reset_password/20101224223622_add_reset_password_to_users.rb b/spec/rails_app/db/migrate/reset_password/20101224223622_add_reset_password_to_users.rb index e80e61f8..e4c6881d 100644 --- a/spec/rails_app/db/migrate/reset_password/20101224223622_add_reset_password_to_users.rb +++ b/spec/rails_app/db/migrate/reset_password/20101224223622_add_reset_password_to_users.rb @@ -3,11 +3,13 @@ def self.up add_column :users, :reset_password_token, :string, default: nil add_column :users, :reset_password_token_expires_at, :datetime, default: nil add_column :users, :reset_password_email_sent_at, :datetime, default: nil + add_column :users, :access_count_to_reset_password_page, :integer, default: 0 end def self.down remove_column :users, :reset_password_email_sent_at remove_column :users, :reset_password_token_expires_at remove_column :users, :reset_password_token + remove_column :users, :access_count_to_reset_password_page end end diff --git a/spec/rails_app/db/schema.rb b/spec/rails_app/db/schema.rb index 7c319ca8..ebc42033 100644 --- a/spec/rails_app/db/schema.rb +++ b/spec/rails_app/db/schema.rb @@ -10,14 +10,12 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20101224223620) do - - create_table "users", :force => true do |t| - t.string "username" - t.string "email" - t.string "crypted_password" - t.datetime "created_at" - t.datetime "updated_at" +ActiveRecord::Schema.define(version: 20_101_224_223_620) do + create_table 'users', force: true do |t| + t.string 'username' + t.string 'email' + t.string 'crypted_password' + t.datetime 'created_at' + t.datetime 'updated_at' end - end diff --git a/spec/shared_examples/user_magic_login_shared_examples.rb b/spec/shared_examples/user_magic_login_shared_examples.rb new file mode 100644 index 00000000..7a75c0fd --- /dev/null +++ b/spec/shared_examples/user_magic_login_shared_examples.rb @@ -0,0 +1,150 @@ +shared_examples_for 'magic_login_model' do + let(:user) { create_new_user } + before(:each) do + User.sorcery_adapter.delete_all + end + + context 'loaded plugin configuration' do + let(:config) { User.sorcery_config } + + before(:all) do + sorcery_reload!([:magic_login]) + end + + after(:each) do + User.sorcery_config.reset! + end + + describe 'enables configuration options' do + it do + sorcery_model_property_set(:magic_login_token_attribute_name, :test_magic_login_token) + expect(config.magic_login_token_attribute_name).to eq :test_magic_login_token + end + + it do + sorcery_model_property_set(:magic_login_token_expires_at_attribute_name, :test_magic_login_token_expires_at) + expect(config.magic_login_token_expires_at_attribute_name).to eq :test_magic_login_token_expires_at + end + + it do + sorcery_model_property_set(:magic_login_email_sent_at_attribute_name, :test_magic_login_email_sent_at) + expect(config.magic_login_email_sent_at_attribute_name).to eq :test_magic_login_email_sent_at + end + + it do + TestMailerClass = Class.new # need a mailer class to test + sorcery_model_property_set(:magic_login_mailer_class, TestMailerClass) + expect(config.magic_login_mailer_class).to eq TestMailerClass + end + + it do + sorcery_model_property_set(:magic_login_mailer_disabled, false) + expect(config.magic_login_mailer_disabled).to eq false + end + + it do + sorcery_model_property_set(:magic_login_email_method_name, :test_magic_login_email) + expect(config.magic_login_email_method_name).to eq :test_magic_login_email + end + + it do + sorcery_model_property_set(:magic_login_expiration_period, 100_000_000) + expect(config.magic_login_expiration_period).to eq 100_000_000 + end + + it do + sorcery_model_property_set(:magic_login_time_between_emails, 100_000_000) + expect(config.magic_login_time_between_emails).to eq 100_000_000 + end + end + + describe '#generate_magic_login_token!' do + context 'magic_login_token is nil' do + it "magic_login_token_expires_at and magic_login_email_sent_at aren't nil " do + user.generate_magic_login_token! + expect(user.magic_login_token_expires_at).not_to be_nil + expect(user.magic_login_email_sent_at).not_to be_nil + end + + it 'magic_login_token is different from the one before' do + token_before = user.magic_login_token + user.generate_magic_login_token! + expect(user.magic_login_token).not_to eq token_before + end + end + + context 'magic_login_token is not nil' do + it 'changes `user.magic_login_token`' do + token_before = user.magic_login_token + user.generate_magic_login_token! + expect(user.magic_login_token).not_to eq token_before + end + end + end + + describe '#deliver_magic_login_instructions!' do + context 'success' do + before do + sorcery_model_property_set(:magic_login_time_between_emails, 30 * 60) + sorcery_model_property_set(:magic_login_mailer_disabled, false) + Timecop.travel(10.days.ago) do + user.send(:"#{config.magic_login_email_sent_at_attribute_name}=", DateTime.now) + end + sorcery_model_property_set(:magic_login_mailer_class, ::SorceryMailer) + end + + it do + user.deliver_magic_login_instructions! + expect(ActionMailer::Base.deliveries.size).to eq 1 + end + + it do + expect(user.deliver_magic_login_instructions!).to eq true + end + end + + context 'failure' do + context 'magic_login_time_between_emails is nil' do + it 'returns false' do + sorcery_model_property_set(:magic_login_time_between_emails, nil) + expect(user.deliver_magic_login_instructions!).to eq false + end + end + + context 'magic_login_email_sent_at is nil' do + it 'returns false' do + user.send(:"#{config.magic_login_email_sent_at_attribute_name}=", nil) + expect(user.deliver_magic_login_instructions!).to eq false + end + end + + context 'now is before magic_login_email_sent_at plus the interval' do + it 'returns false' do + user.send(:"#{config.magic_login_email_sent_at_attribute_name}=", DateTime.now) + sorcery_model_property_set(:magic_login_time_between_emails, 30 * 60) + expect(user.deliver_magic_login_instructions!).to eq false + end + end + + context 'magic_login_mailer_disabled is true' do + it 'returns false' do + sorcery_model_property_set(:magic_login_mailer_disabled, true) + expect(user.deliver_magic_login_instructions!).to eq false + end + end + end + end + + describe '#clear_magic_login_token!' do + it 'makes magic_login_token_attribute_name and magic_login_token_expires_at_attribute_name nil' do + user.magic_login_token = 'test_token' + user.magic_login_token_expires_at = Time.now + + user.clear_magic_login_token! + + expect(user.magic_login_token).to eq nil + expect(user.magic_login_token_expires_at).to eq nil + end + end + end +end diff --git a/spec/shared_examples/user_oauth_shared_examples.rb b/spec/shared_examples/user_oauth_shared_examples.rb index f06bbab8..3e6ec302 100644 --- a/spec/shared_examples/user_oauth_shared_examples.rb +++ b/spec/shared_examples/user_oauth_shared_examples.rb @@ -27,7 +27,7 @@ it "'load_from_provider' returns nil if user doesn't exist" do external_user - expect(User.load_from_provider(:twitter, 980342)).to be_nil + expect(User.load_from_provider(:twitter, 980_342)).to be_nil end end end diff --git a/spec/shared_examples/user_remember_me_shared_examples.rb b/spec/shared_examples/user_remember_me_shared_examples.rb index e7448d31..b31c4372 100644 --- a/spec/shared_examples/user_remember_me_shared_examples.rb +++ b/spec/shared_examples/user_remember_me_shared_examples.rb @@ -42,7 +42,7 @@ user.remember_me! end - expect(user.remember_me_token_expires_at.utc.to_s).to eq (ts + 2 * 60 * 60 * 24).utc.to_s + expect(user.remember_me_token_expires_at.utc.to_s).to eq((ts + 2 * 60 * 60 * 24).utc.to_s) end context 'when not persisting globally' do diff --git a/spec/shared_examples/user_reset_password_shared_examples.rb b/spec/shared_examples/user_reset_password_shared_examples.rb index 1f43e700..e07e861c 100644 --- a/spec/shared_examples/user_reset_password_shared_examples.rb +++ b/spec/shared_examples/user_reset_password_shared_examples.rb @@ -14,6 +14,8 @@ context 'API' do specify { expect(user).to respond_to :deliver_reset_password_instructions! } + specify { expect(user).to respond_to :change_password } + specify { expect(user).to respond_to :change_password! } it 'responds to .load_from_reset_password_token' do @@ -214,6 +216,22 @@ expect(user.reset_password_token).not_to eq old_password_code end + describe '#increment_password_reset_page_access_counter' do + it 'increments reset_password_page_access_count_attribute_name' do + expected_count = user.access_count_to_reset_password_page + 1 + user.increment_password_reset_page_access_counter + expect(user.access_count_to_reset_password_page).to eq expected_count + end + end + + describe '#reset_password_reset_page_access_counter' do + it 'reset reset_password_page_access_count_attribute_name into 0' do + user.update(access_count_to_reset_password_page: 10) + user.reset_password_reset_page_access_counter + expect(user.access_count_to_reset_password_page).to eq 0 + end + end + context 'mailer is enabled' do it 'sends an email on reset' do old_size = ActionMailer::Base.deliveries.size @@ -229,7 +247,7 @@ end it 'does not send an email if time between emails has not passed since last email' do - sorcery_model_property_set(:reset_password_time_between_emails, 10000) + sorcery_model_property_set(:reset_password_time_between_emails, 10_000) old_size = ActionMailer::Base.deliveries.size user.deliver_reset_password_instructions! @@ -273,7 +291,7 @@ end it 'does not send an email if time between emails has not passed since last email' do - sorcery_model_property_set(:reset_password_time_between_emails, 10000) + sorcery_model_property_set(:reset_password_time_between_emails, 10_000) old_size = ActionMailer::Base.deliveries.size user.deliver_reset_password_instructions! @@ -298,19 +316,33 @@ end end - it 'when change_password! is called, deletes reset_password_token' do + it 'when change_password! is called, deletes reset_password_token and calls #save!' do user.deliver_reset_password_instructions! expect(user.reset_password_token).not_to be_nil + expect(user).to_not receive(:save) + expect(user).to receive(:save!) user.change_password!('blabulsdf') - user.save! + + expect(user.reset_password_token).to be_nil + end + + it 'when change_password is called, deletes reset_password_token and calls #save' do + new_password = 'blabulsdf' + + user.deliver_reset_password_instructions! + expect(user.reset_password_token).not_to be_nil + expect(user).to_not receive(:save!) + expect(user).to receive(:save) + + user.change_password(new_password) expect(user.reset_password_token).to be_nil end it 'returns false if time between emails has not passed since last email' do - sorcery_model_property_set(:reset_password_time_between_emails, 10000) + sorcery_model_property_set(:reset_password_time_between_emails, 10_000) user.deliver_reset_password_instructions! expect(user.deliver_reset_password_instructions!).to be false diff --git a/spec/shared_examples/user_shared_examples.rb b/spec/shared_examples/user_shared_examples.rb index c563cc95..c1e09620 100644 --- a/spec/shared_examples/user_shared_examples.rb +++ b/spec/shared_examples/user_shared_examples.rb @@ -54,6 +54,13 @@ expect(User.sorcery_config.custom_encryption_provider).to eq Array end + it "enables configuration option 'pepper'" do + pepper = '*$%&%*++' + sorcery_model_property_set(:pepper, pepper) + + expect(User.sorcery_config.pepper).to eq pepper + end + it "enables configuration option 'salt_join_token'" do salt_join_token = '--%%*&-' sorcery_model_property_set(:salt_join_token, salt_join_token) @@ -228,10 +235,13 @@ class Admin2 < User; end expect(user).to receive(:save) { raise RuntimeError } + # rubocop:disable Lint/HandleExceptions begin user.save - rescue + rescue RuntimeError + # Intentionally force exception during save end + # rubocop:enable Lint/HandleExceptions expect(user.password).not_to be_nil end @@ -308,12 +318,12 @@ class Admin2 < User; end describe 'generic send email' do before(:all) do - ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate/activation") + MigrationHelper.migrate("#{Rails.root}/db/migrate/activation") User.reset_column_information end after(:all) do - ActiveRecord::Migrator.rollback("#{Rails.root}/db/migrate/activation") + MigrationHelper.rollback("#{Rails.root}/db/migrate/activation") end before do @@ -323,9 +333,11 @@ class Admin2 < User; end it 'use deliver_later' do sorcery_reload!( - [ - :user_activation, :user_activation_mailer, - :activation_needed_email_method_name, :email_delivery_method + %i[ + user_activation + user_activation_mailer + activation_needed_email_method_name + email_delivery_method ], user_activation_mailer: SorceryMailer, activation_needed_email_method_name: nil, @@ -340,9 +352,10 @@ class Admin2 < User; end it 'use deliver_now if rails version 4.2+' do allow(Rails).to receive(:version).and_return('4.2.0') sorcery_reload!( - [ - :user_activation, :user_activation_mailer, - :activation_needed_email_method_name + %i[ + user_activation + user_activation_mailer + activation_needed_email_method_name ], user_activation_mailer: SorceryMailer, activation_needed_email_method_name: nil @@ -355,9 +368,10 @@ class Admin2 < User; end it 'use deliver if rails version < 4.2' do allow(Rails).to receive(:version).and_return('4.1.0') sorcery_reload!( - [ - :user_activation, :user_activation_mailer, - :activation_needed_email_method_name + %i[ + user_activation + user_activation_mailer + activation_needed_email_method_name ], user_activation_mailer: SorceryMailer, activation_needed_email_method_name: nil @@ -452,6 +466,14 @@ def self.matches?(crypted, *tokens) expect(User.encrypt(@text)).to eq Sorcery::CryptoProviders::SHA512.encrypt(@text) end + it 'if encryption algo is bcrypt it works' do + sorcery_model_property_set(:encryption_algorithm, :bcrypt) + + # comparison is done using BCrypt::Password#==(raw_token), not by String#== + expect(User.encrypt(@text)).to be_an_instance_of BCrypt::Password + expect(User.encrypt(@text)).to eq @text + end + it 'salt is random for each user and saved in db' do sorcery_model_property_set(:salt_attribute_name, :salt) @@ -481,6 +503,54 @@ def self.matches?(crypted, *tokens) expect(user.crypted_password).to eq Sorcery::CryptoProviders::SHA512.encrypt('secret', user.salt) end + + it 'if pepper is set uses it to encrypt' do + sorcery_model_property_set(:salt_attribute_name, :salt) + sorcery_model_property_set(:pepper, '++@^$') + sorcery_model_property_set(:encryption_algorithm, :bcrypt) + + # password comparison is done using BCrypt::Password#==(raw_token), not String#== + bcrypt_password = BCrypt::Password.new(user.crypted_password) + allow(::BCrypt::Password).to receive(:create) do |token, options = {}| + # need to use common BCrypt's salt when genarating BCrypt::Password objects + # so that any generated password hashes can be compared each other + ::BCrypt::Engine.hash_secret(token, bcrypt_password.salt) + end + + expect(user.crypted_password).not_to eq Sorcery::CryptoProviders::BCrypt.encrypt('secret') + + Sorcery::CryptoProviders::BCrypt.pepper = '' + + expect(user.crypted_password).not_to eq Sorcery::CryptoProviders::BCrypt.encrypt('secret', user.salt) + + Sorcery::CryptoProviders::BCrypt.pepper = User.sorcery_config.pepper + + expect(user.crypted_password).to eq Sorcery::CryptoProviders::BCrypt.encrypt('secret', user.salt) + end + + it 'if pepper is empty string (default) does not use pepper to encrypt' do + sorcery_model_property_set(:salt_attribute_name, :salt) + sorcery_model_property_set(:pepper, '') + sorcery_model_property_set(:encryption_algorithm, :bcrypt) + + # password comparison is done using BCrypt::Password#==(raw_token), not String#== + bcrypt_password = BCrypt::Password.new(user.crypted_password) + allow(::BCrypt::Password).to receive(:create) do |token, options = {}| + # need to use common BCrypt's salt when genarating BCrypt::Password objects + # so that any generated password hashes can be compared each other + ::BCrypt::Engine.hash_secret(token, bcrypt_password.salt) + end + + expect(user.crypted_password).not_to eq Sorcery::CryptoProviders::BCrypt.encrypt('secret') + + Sorcery::CryptoProviders::BCrypt.pepper = 'some_pepper' + + expect(user.crypted_password).not_to eq Sorcery::CryptoProviders::BCrypt.encrypt('secret', user.salt) + + Sorcery::CryptoProviders::BCrypt.pepper = User.sorcery_config.pepper + + expect(user.crypted_password).to eq Sorcery::CryptoProviders::BCrypt.encrypt('secret', user.salt) + end end describe 'ORM adapter' do @@ -503,7 +573,7 @@ def self.matches?(crypted, *tokens) end it 'find_by_username works as expected with multiple username attributes' do - sorcery_model_property_set(:username_attribute_names, [:username, :email]) + sorcery_model_property_set(:username_attribute_names, %i[username email]) expect(User.sorcery_adapter.find_by_username('gizmo')).to eq user end @@ -518,6 +588,21 @@ def self.matches?(crypted, *tokens) let(:user) { create_new_user } let(:external_user) { create_new_external_user :twitter } + before(:all) do + if SORCERY_ORM == :active_record + MigrationHelper.migrate("#{Rails.root}/db/migrate/external") + MigrationHelper.migrate("#{Rails.root}/db/migrate/activation") + end + sorcery_reload! + end + + after(:all) do + if SORCERY_ORM == :active_record + MigrationHelper.rollback("#{Rails.root}/db/migrate/external") + MigrationHelper.rollback("#{Rails.root}/db/migrate/activation") + end + end + before(:each) do User.sorcery_adapter.delete_all end @@ -535,24 +620,12 @@ def self.matches?(crypted, *tokens) end describe '.create_from_provider' do - before(:all) do - if SORCERY_ORM == :active_record - ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate/external") - User.reset_column_information - end - + before(:each) do sorcery_reload!([:external]) - end - - after(:all) do - if SORCERY_ORM == :active_record - ActiveRecord::Migrator.rollback("#{Rails.root}/db/migrate/external") - end + sorcery_model_property_set(:authentications_class, Authentication) end it 'supports nested attributes' do - sorcery_model_property_set(:authentications_class, Authentication) - expect do User.create_from_provider('facebook', '123', username: 'Noam Ben Ari') end.to change { User.count }.by(1) @@ -570,33 +643,21 @@ def self.matches?(crypted, *tokens) it 'does not create user when block return false' do expect do User.create_from_provider('facebook', '123', username: 'Noam Ben Ari') { false } - end.not_to change { User.count } + end.not_to(change { User.count }) end end end describe 'activation' do - before(:all) do - if SORCERY_ORM == :active_record - ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate/external") - ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate/activation") - end - - sorcery_reload!([:user_activation, :external], user_activation_mailer: ::SorceryMailer) - end - - after(:all) do - if SORCERY_ORM == :active_record - ActiveRecord::Migrator.rollback("#{Rails.root}/db/migrate/external") - ActiveRecord::Migrator.rollback("#{Rails.root}/db/migrate/activation") - end + before(:each) do + sorcery_reload!(%i[user_activation external], user_activation_mailer: ::SorceryMailer) end after(:each) do User.sorcery_adapter.delete_all end - [:facebook, :github, :google, :liveid, :slack].each do |provider| + %i[facebook github google liveid slack].each do |provider| it 'does not send activation email to external users' do old_size = ActionMailer::Base.deliveries.size create_new_external_user(provider) diff --git a/spec/sorcery_crypto_providers_spec.rb b/spec/sorcery_crypto_providers_spec.rb index 604018fb..796c8ac0 100644 --- a/spec/sorcery_crypto_providers_spec.rb +++ b/spec/sorcery_crypto_providers_spec.rb @@ -53,7 +53,7 @@ it 'matches password encrypted using salt and join token from upstream' do Sorcery::CryptoProviders::SHA1.join_token = 'test' - expect(Sorcery::CryptoProviders::SHA1.encrypt(%w(password gq18WBnJYNh2arkC1kgH))).to eq '894b5bf1643b8d0e1b2eaddb22426be7036dab70' + expect(Sorcery::CryptoProviders::SHA1.encrypt(%w[password gq18WBnJYNh2arkC1kgH])).to eq '894b5bf1643b8d0e1b2eaddb22426be7036dab70' end end @@ -148,6 +148,7 @@ before(:all) do Sorcery::CryptoProviders::BCrypt.cost = 1 @digest = BCrypt::Password.create('Noam Ben-Ari', cost: Sorcery::CryptoProviders::BCrypt.cost) + @tokens = %w[password gq18WBnJYNh2arkC1kgH] end after(:each) do @@ -181,5 +182,64 @@ # stubbed in Sorcery::TestHelpers::Internal expect(Sorcery::CryptoProviders::BCrypt.cost).to eq 1 end + + it 'matches token encrypted with salt from upstream' do + # note: actual comparison is done by BCrypt::Password#==(raw_token) + expect(Sorcery::CryptoProviders::BCrypt.encrypt(@tokens)).to eq @tokens.flatten.join + end + + it 'respond_to?(:pepper) returns true' do + expect(Sorcery::CryptoProviders::BCrypt.respond_to?(:pepper)).to be true + end + + context 'when pepper is provided' do + before(:each) do + Sorcery::CryptoProviders::BCrypt.pepper = 'pepper' + @digest = Sorcery::CryptoProviders::BCrypt.encrypt(@tokens) # a BCrypt::Password object + end + + it 'matches token encrypted with salt and pepper from upstream' do + # note: actual comparison is done by BCrypt::Password#==(raw_token) + expect(@digest).to eq @tokens.flatten.join.concat('pepper') + end + + it 'matches? returns true when matches' do + expect(Sorcery::CryptoProviders::BCrypt.matches?(@digest, *@tokens)).to be true + end + + it 'matches? returns false when pepper is replaced with empty string' do + Sorcery::CryptoProviders::BCrypt.pepper = '' + expect(Sorcery::CryptoProviders::BCrypt.matches?(@digest, *@tokens)).to be false + end + + it 'matches? returns false when no match' do + expect(Sorcery::CryptoProviders::BCrypt.matches?(@digest, 'a_random_incorrect_password')).to be false + end + end + + context "when pepper is an empty string (default)" do + before(:each) do + Sorcery::CryptoProviders::BCrypt.pepper = '' + @digest = Sorcery::CryptoProviders::BCrypt.encrypt(@tokens) # a BCrypt::Password object + end + + # make sure the default pepper '' does nothing + it 'matches token encrypted with salt only (without pepper)' do + expect(@digest).to eq @tokens.flatten.join # keep consistency with the older versions of #join_token + end + + it 'matches? returns true when matches' do + expect(Sorcery::CryptoProviders::BCrypt.matches?(@digest, *@tokens)).to be true + end + + it 'matches? returns false when pepper has changed' do + Sorcery::CryptoProviders::BCrypt.pepper = 'a new pepper' + expect(Sorcery::CryptoProviders::BCrypt.matches?(@digest, *@tokens)).to be false + end + + it 'matches? returns false when no match' do + expect(Sorcery::CryptoProviders::BCrypt.matches?(@digest, 'a_random_incorrect_password')).to be false + end + end end end diff --git a/spec/spec.opts b/spec/spec.opts index b3eb8b49..16f9cdb0 100644 --- a/spec/spec.opts +++ b/spec/spec.opts @@ -1,2 +1,2 @@ --color ---format documentation \ No newline at end of file +--format documentation diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c8d9782f..bc5c48aa 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -29,7 +29,7 @@ class TestMailer < ActionMailer::Base; end config.include RSpec::Rails::ControllerExampleGroup, file_path: /controller(.)*_spec.rb$/ config.mock_with :rspec - config.use_transactional_fixtures = true + config.use_transactional_fixtures = false config.before(:suite) { setup_orm } config.after(:suite) { teardown_orm } @@ -40,7 +40,7 @@ class TestMailer < ActionMailer::Base; end if begin Module.const_defined?('::Rails::Controller::Testing') - rescue + rescue StandardError false end config.include ::Rails::Controller::Testing::TestProcess, type: :controller diff --git a/spec/support/migration_helper.rb b/spec/support/migration_helper.rb new file mode 100644 index 00000000..5f6e9501 --- /dev/null +++ b/spec/support/migration_helper.rb @@ -0,0 +1,29 @@ +class MigrationHelper + class << self + def migrate(path) + if ActiveRecord.version >= Gem::Version.new('6.0.0') + ActiveRecord::MigrationContext.new(path, schema_migration).migrate + elsif ActiveRecord.version >= Gem::Version.new('5.2.0') + ActiveRecord::MigrationContext.new(path).migrate + else + ActiveRecord::Migrator.migrate(path) + end + end + + def rollback(path) + if ActiveRecord.version >= Gem::Version.new('6.0.0') + ActiveRecord::MigrationContext.new(path, schema_migration).rollback + elsif ActiveRecord.version >= Gem::Version.new('5.2.0') + ActiveRecord::MigrationContext.new(path).rollback + else + ActiveRecord::Migrator.rollback(path) + end + end + + private + + def schema_migration + ActiveRecord::Base.connection.schema_migration + end + end +end diff --git a/spec/support/providers/example.rb b/spec/support/providers/example.rb new file mode 100644 index 00000000..e110789e --- /dev/null +++ b/spec/support/providers/example.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'sorcery/providers/base' + +module Sorcery + module Providers + class Example < Base + include Protocols::Oauth2 + end + end +end diff --git a/spec/support/providers/example_provider.rb b/spec/support/providers/example_provider.rb new file mode 100644 index 00000000..6428ad6a --- /dev/null +++ b/spec/support/providers/example_provider.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'sorcery/providers/base' + +module Sorcery + module Providers + class ExampleProvider < Base + include Protocols::Oauth2 + end + end +end