├── .devcontainer ├── Dockerfile ├── devcontainer.json └── postcreate.sh ├── .document ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ruby.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.md ├── MAINTAINING.md ├── README.md ├── Rakefile ├── SECURITY.md ├── gemfiles ├── rails_71.gemfile ├── rails_72.gemfile └── rails_80.gemfile ├── lib ├── generators │ └── sorcery │ │ ├── USAGE │ │ ├── helpers.rb │ │ ├── install_generator.rb │ │ └── templates │ │ ├── initializer.rb │ │ └── migration │ │ ├── activity_logging.rb │ │ ├── brute_force_protection.rb │ │ ├── core.rb │ │ ├── external.rb │ │ ├── magic_login.rb │ │ ├── remember_me.rb │ │ ├── reset_password.rb │ │ └── user_activation.rb ├── sorcery.rb └── sorcery │ ├── adapters │ ├── active_record_adapter.rb │ ├── base_adapter.rb │ └── mongoid_adapter.rb │ ├── controller.rb │ ├── controller │ ├── config.rb │ └── submodules │ │ ├── activity_logging.rb │ │ ├── brute_force_protection.rb │ │ ├── external.rb │ │ ├── http_basic_auth.rb │ │ ├── remember_me.rb │ │ └── session_timeout.rb │ ├── crypto_providers │ ├── aes256.rb │ ├── bcrypt.rb │ ├── common.rb │ ├── md5.rb │ ├── sha1.rb │ ├── sha256.rb │ └── sha512.rb │ ├── engine.rb │ ├── model.rb │ ├── model │ ├── config.rb │ ├── submodules │ │ ├── activity_logging.rb │ │ ├── brute_force_protection.rb │ │ ├── external.rb │ │ ├── magic_login.rb │ │ ├── remember_me.rb │ │ ├── reset_password.rb │ │ └── user_activation.rb │ └── temporary_token.rb │ ├── protocols │ ├── certs │ │ └── ca-bundle.crt │ ├── oauth.rb │ └── oauth2.rb │ ├── providers │ ├── auth0.rb │ ├── base.rb │ ├── battlenet.rb │ ├── discord.rb │ ├── facebook.rb │ ├── github.rb │ ├── google.rb │ ├── heroku.rb │ ├── instagram.rb │ ├── jira.rb │ ├── line.rb │ ├── linkedin.rb │ ├── liveid.rb │ ├── microsoft.rb │ ├── paypal.rb │ ├── salesforce.rb │ ├── slack.rb │ ├── twitter.rb │ ├── vk.rb │ ├── wechat.rb │ └── xing.rb │ ├── test_helpers │ ├── internal.rb │ ├── internal │ │ └── rails.rb │ └── rails │ │ ├── controller.rb │ │ ├── integration.rb │ │ └── request.rb │ └── version.rb ├── sorcery.gemspec └── spec ├── active_record ├── user_activation_spec.rb ├── user_activity_logging_spec.rb ├── user_brute_force_protection_spec.rb ├── user_magic_login_spec.rb ├── user_oauth_spec.rb ├── user_remember_me_spec.rb ├── user_reset_password_spec.rb └── user_spec.rb ├── controllers ├── controller_activity_logging_spec.rb ├── controller_brute_force_protection_spec.rb ├── controller_http_basic_auth_spec.rb ├── controller_oauth2_spec.rb ├── controller_oauth_spec.rb ├── controller_remember_me_spec.rb ├── controller_session_timeout_spec.rb └── controller_spec.rb ├── orm └── active_record.rb ├── providers ├── example_provider_spec.rb ├── example_spec.rb ├── examples_spec.rb └── vk_spec.rb ├── rails_app ├── app │ ├── active_record │ │ ├── authentication.rb │ │ ├── user.rb │ │ └── user_provider.rb │ ├── assets │ │ └── config │ │ │ └── manifest.js │ ├── controllers │ │ ├── application_controller.rb │ │ └── sorcery_controller.rb │ ├── helpers │ │ └── application_helper.rb │ ├── mailers │ │ └── sorcery_mailer.rb │ └── views │ │ ├── application │ │ └── index.html.erb │ │ ├── layouts │ │ └── application.html.erb │ │ └── sorcery_mailer │ │ ├── activation_email.html.erb │ │ ├── activation_email.text.erb │ │ ├── activation_needed_email.html.erb │ │ ├── activation_success_email.html.erb │ │ ├── activation_success_email.text.erb │ │ ├── magic_login_email.html.erb │ │ ├── magic_login_email.text.erb │ │ ├── reset_password_email.html.erb │ │ ├── reset_password_email.text.erb │ │ └── send_unlock_token_email.text.erb ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ └── test.rb │ ├── initializers │ │ ├── backtrace_silencers.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ └── session_store.rb │ ├── locales │ │ └── en.yml │ ├── routes.rb │ └── secrets.yml └── db │ ├── migrate │ ├── activation │ │ └── 20101224223622_add_activation_to_users.rb │ ├── activity_logging │ │ └── 20101224223624_add_activity_logging_to_users.rb │ ├── brute_force_protection │ │ └── 20101224223626_add_brute_force_protection_to_users.rb │ ├── core │ │ └── 20101224223620_create_users.rb │ ├── external │ │ └── 20101224223628_create_authentications_and_user_providers.rb │ ├── invalidate_active_sessions │ │ └── 20180221093235_add_invalidate_active_sessions_before_to_users.rb │ ├── magic_login │ │ └── 20170924151831_add_magic_login_to_users.rb │ ├── remember_me │ │ └── 20101224223623_add_remember_me_token_to_users.rb │ └── reset_password │ │ └── 20101224223622_add_reset_password_to_users.rb │ ├── schema.rb │ └── seeds.rb ├── shared_examples ├── user_activation_shared_examples.rb ├── user_activity_logging_shared_examples.rb ├── user_brute_force_protection_shared_examples.rb ├── user_magic_login_shared_examples.rb ├── user_oauth_shared_examples.rb ├── user_remember_me_shared_examples.rb ├── user_reset_password_shared_examples.rb └── user_shared_examples.rb ├── sorcery_crypto_providers_spec.rb ├── sorcery_temporary_token_spec.rb ├── spec.opts ├── spec_helper.rb └── support ├── migration_helper.rb └── providers ├── example.rb ├── example_provider.rb └── examples.rb /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Which Ruby version to use. You may need to use a more restrictive version, 2 | # e.g. `3.0` 3 | ARG VARIANT=3.0 4 | 5 | # Pull Microsoft's ruby devcontainer base image 6 | FROM mcr.microsoft.com/devcontainers/ruby:${VARIANT} 7 | 8 | # Ensure we're running the latest bundler, as what ships with the Ruby image may 9 | # not be current, and bundler will auto-downgrade to match the Gemfile.lock 10 | RUN gem install bundler 11 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ruby", 3 | "build": { 4 | "dockerfile": "Dockerfile" 5 | }, 6 | 7 | // Configure tool-specific properties. 8 | "customizations": { 9 | // Configure properties specific to VS Code. 10 | "vscode": { 11 | // Add the IDs of extensions you want installed when the container is created. 12 | "extensions": [ 13 | "rebornix.Ruby" 14 | ] 15 | } 16 | }, 17 | 18 | // Set the environment variables 19 | // "runArgs": ["--env-file",".env"], 20 | 21 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 22 | // "forwardPorts": [], 23 | 24 | // Use 'postCreateCommand' to run commands after the container is created. 25 | "postCreateCommand": "bash .devcontainer/postcreate.sh", 26 | 27 | // Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 28 | "remoteUser": "vscode" 29 | } 30 | -------------------------------------------------------------------------------- /.devcontainer/postcreate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | bundle config set path vendor/bundle 4 | bundle install --jobs=1 5 | -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | lib/**/*.rb 2 | bin/* 3 | - 4 | features/**/*.feature 5 | LICENSE.txt 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Configuration 4 | 5 | - Sorcery Version: `` 6 | - Ruby Version: `` 7 | - Framework: `` 8 | - Platform: `` 9 | 10 | ### Expected Behavior 11 | 12 | 13 | 14 | ### Actual Behavior 15 | 16 | 17 | 18 | ### Steps to Reproduce 19 | 20 | 21 | 22 | 1. 23 | 2. 24 | 3. 25 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please ensure your pull request includes the following: 2 | 3 | - [ ] Description of changes 4 | - [ ] Update to CHANGELOG.md with short description and link to pull request 5 | - [ ] Changes have related RSpec tests that ensure functionality does not break 6 | 7 | 8 | -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: Test Suite 2 | 3 | # Run against all commits and pull requests. 4 | on: 5 | workflow_dispatch: 6 | schedule: 7 | - cron: '0 0 * * *' 8 | push: 9 | pull_request: 10 | 11 | jobs: 12 | test_matrix: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | ruby: 20 | - '3.2' 21 | - '3.3' 22 | - '3.4' 23 | 24 | rails: 25 | - '71' 26 | - '72' 27 | - '80' 28 | env: 29 | BUNDLE_GEMFILE: gemfiles/rails_${{ matrix.rails }}.gemfile 30 | 31 | steps: 32 | - uses: actions/checkout@v4 33 | - name: Set up Ruby 34 | uses: ruby/setup-ruby@v1 35 | with: 36 | ruby-version: ${{ matrix.ruby }} 37 | bundler-cache: true 38 | - name: Run tests 39 | run: bundle exec rake spec 40 | 41 | finish: 42 | runs-on: ubuntu-latest 43 | needs: [ test_matrix ] 44 | steps: 45 | - name: Wait for status checks 46 | run: echo "All Green!" 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # rcov generated 2 | coverage 3 | 4 | # rdoc generated 5 | rdoc 6 | 7 | # yard generated 8 | doc 9 | .yardoc 10 | 11 | # bundler 12 | .bundle 13 | vendor 14 | 15 | # jeweler generated 16 | pkg 17 | 18 | # byebug generated 19 | .byebug_history 20 | 21 | # for RVM 22 | .rvmrc 23 | 24 | # for RubyMine 25 | .idea 26 | 27 | # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore: 28 | # 29 | # * Create a file at ~/.gitignore 30 | # * Include files you want ignored 31 | # * Run: git config --global core.excludesfile ~/.gitignore 32 | # 33 | # After doing this, these files will be ignored in all your git projects, 34 | # saving you from having to 'pollute' every project you touch with them 35 | # 36 | # Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line) 37 | # 38 | # For MacOS: 39 | # 40 | #.DS_Store 41 | # 42 | # For TextMate 43 | #*.tmproj 44 | tmtags 45 | # 46 | # For emacs: 47 | #*~ 48 | #\#* 49 | #.\#* 50 | # 51 | # For vim: 52 | #*.swp 53 | # 54 | spec/rails_app/log/* 55 | *.log 56 | *.sqlite3* 57 | Gemfile*.lock 58 | gemfiles/*.lock 59 | .ruby-version 60 | tags -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | AllCops: 4 | Exclude: 5 | - 'lib/generators/sorcery/templates/**/*' 6 | TargetRubyVersion: 2.6 7 | 8 | # See: https://github.com/rubocop-hq/rubocop/issues/3344 9 | Style/DoubleNegation: 10 | Enabled: false 11 | 12 | #################### 13 | ## Pre-1.0.0 Code ## 14 | #################### 15 | 16 | Metrics/AbcSize: 17 | Exclude: 18 | - 'lib/**/*' 19 | - 'spec/**/*' 20 | Metrics/BlockLength: 21 | Exclude: 22 | - 'lib/**/*' 23 | - 'spec/**/*' 24 | Layout/LineLength: 25 | Exclude: 26 | - 'lib/**/*' 27 | - 'spec/**/*' 28 | Metrics/ClassLength: 29 | Exclude: 30 | - 'lib/**/*' 31 | - 'spec/**/*' 32 | Metrics/CyclomaticComplexity: 33 | Exclude: 34 | - 'lib/**/*' 35 | - 'spec/**/*' 36 | Metrics/MethodLength: 37 | Exclude: 38 | - 'lib/**/*' 39 | - 'spec/**/*' 40 | Metrics/PerceivedComplexity: 41 | Exclude: 42 | - 'lib/**/*' 43 | - 'spec/**/*' 44 | Naming/AccessorMethodName: 45 | Exclude: 46 | - 'lib/**/*' 47 | - 'spec/**/*' 48 | Naming/PredicateName: 49 | Exclude: 50 | - 'lib/**/*' 51 | - 'spec/**/*' 52 | Style/Documentation: 53 | Exclude: 54 | - 'lib/**/*' 55 | - 'spec/**/*' 56 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2021-04-04 05:00:11 UTC using RuboCop version 0.88.0. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 1 10 | # Configuration parameters: Include. 11 | # Include: **/*.gemspec 12 | Gemspec/RequiredRubyVersion: 13 | Exclude: 14 | - 'sorcery.gemspec' 15 | 16 | # Offense count: 2 17 | # Cop supports --auto-correct. 18 | # Configuration parameters: IndentationWidth. 19 | # SupportedStyles: special_inside_parentheses, consistent, align_braces 20 | Layout/FirstHashElementIndentation: 21 | EnforcedStyle: consistent 22 | 23 | # Offense count: 83 24 | # Cop supports --auto-correct. 25 | # Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. 26 | # SupportedHashRocketStyles: key, separator, table 27 | # SupportedColonStyles: key, separator, table 28 | # SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit 29 | Layout/HashAlignment: 30 | Enabled: false 31 | 32 | # Offense count: 3 33 | # Cop supports --auto-correct. 34 | # Configuration parameters: AllowInHeredoc. 35 | Layout/TrailingWhitespace: 36 | Exclude: 37 | - 'lib/sorcery/controller/submodules/external.rb' 38 | 39 | # Offense count: 2 40 | # Configuration parameters: AllowSafeAssignment. 41 | Lint/AssignmentInCondition: 42 | Exclude: 43 | - 'spec/rails_app/app/controllers/sorcery_controller.rb' 44 | 45 | # Offense count: 1 46 | # Cop supports --auto-correct. 47 | Lint/NonDeterministicRequireOrder: 48 | Exclude: 49 | - 'spec/spec_helper.rb' 50 | 51 | # Offense count: 4 52 | # Cop supports --auto-correct. 53 | Lint/RedundantCopDisableDirective: 54 | Exclude: 55 | - 'lib/sorcery/controller.rb' 56 | - 'lib/sorcery/model.rb' 57 | - 'spec/rails_app/config/application.rb' 58 | - 'spec/shared_examples/user_shared_examples.rb' 59 | 60 | # Offense count: 4 61 | # Cop supports --auto-correct. 62 | Lint/SendWithMixinArgument: 63 | Exclude: 64 | - 'lib/sorcery.rb' 65 | - 'lib/sorcery/engine.rb' 66 | - 'lib/sorcery/test_helpers/internal/rails.rb' 67 | 68 | # Offense count: 2 69 | # Cop supports --auto-correct. 70 | # Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. 71 | Lint/UnusedBlockArgument: 72 | Exclude: 73 | - 'spec/shared_examples/user_shared_examples.rb' 74 | 75 | # Offense count: 1 76 | # Cop supports --auto-correct. 77 | # Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions. 78 | # SupportedStyles: assign_to_condition, assign_inside_condition 79 | Style/ConditionalAssignment: 80 | Exclude: 81 | - 'lib/sorcery/adapters/active_record_adapter.rb' 82 | 83 | # Offense count: 1 84 | # Cop supports --auto-correct. 85 | Style/ExpandPathArguments: 86 | Exclude: 87 | - 'spec/rails_app/config.ru' 88 | 89 | # Offense count: 1 90 | # Configuration parameters: EnforcedStyle. 91 | # SupportedStyles: annotated, template, unannotated 92 | Style/FormatStringToken: 93 | Exclude: 94 | - 'lib/generators/sorcery/install_generator.rb' 95 | 96 | # Offense count: 125 97 | # Cop supports --auto-correct. 98 | # Configuration parameters: EnforcedStyle. 99 | # SupportedStyles: always, always_true, never 100 | Style/FrozenStringLiteralComment: 101 | Enabled: false 102 | 103 | # Offense count: 3 104 | # Cop supports --auto-correct. 105 | # Configuration parameters: EnforcedStyle, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. 106 | # SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys 107 | Style/HashSyntax: 108 | Exclude: 109 | - 'lib/sorcery/adapters/active_record_adapter.rb' 110 | - 'lib/sorcery/test_helpers/rails/integration.rb' 111 | 112 | # Offense count: 34 113 | # Cop supports --auto-correct. 114 | Style/IfUnlessModifier: 115 | Enabled: false 116 | 117 | # Offense count: 1 118 | # Cop supports --auto-correct. 119 | Style/MultilineIfModifier: 120 | Exclude: 121 | - 'lib/sorcery/providers/line.rb' 122 | 123 | # Offense count: 2 124 | # Cop supports --auto-correct. 125 | Style/RedundantBegin: 126 | Exclude: 127 | - 'lib/sorcery/controller.rb' 128 | - 'lib/sorcery/model.rb' 129 | 130 | # Offense count: 4 131 | # Cop supports --auto-correct. 132 | # Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods. 133 | # AllowedMethods: present?, blank?, presence, try, try! 134 | Style/SafeNavigation: 135 | Exclude: 136 | - 'lib/sorcery/controller/config.rb' 137 | - 'lib/sorcery/controller/submodules/brute_force_protection.rb' 138 | - 'lib/sorcery/controller/submodules/remember_me.rb' 139 | - 'lib/sorcery/model.rb' 140 | 141 | # Offense count: 7 142 | # Cop supports --auto-correct. 143 | # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. 144 | # SupportedStyles: single_quotes, double_quotes 145 | Style/StringLiterals: 146 | Exclude: 147 | - 'spec/controllers/controller_oauth2_spec.rb' 148 | - 'spec/sorcery_crypto_providers_spec.rb' 149 | 150 | # Offense count: 1 151 | # Cop supports --auto-correct. 152 | # Configuration parameters: EnforcedStyle, MinSize. 153 | # SupportedStyles: percent, brackets 154 | Style/SymbolArray: 155 | Exclude: 156 | - 'Rakefile' 157 | 158 | # Offense count: 2 159 | # Cop supports --auto-correct. 160 | Style/UnpackFirst: 161 | Exclude: 162 | - 'lib/sorcery/crypto_providers/aes256.rb' 163 | - 'spec/sorcery_crypto_providers_spec.rb' 164 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # The Sorcery Community Code of Conduct 2 | 3 | This document provides a few simple community guidelines for a safe, respectful, 4 | productive, and collaborative place for any person who is willing to contribute 5 | to the Sorcery community. It applies to all "collaborative spaces", which are 6 | defined as community communications channels (such as mailing lists, submitted 7 | patches, commit comments, etc.). 8 | 9 | * Participants will be tolerant of opposing views. 10 | * Participants must ensure that their language and actions are free of personal 11 | attacks and disparaging personal remarks. 12 | * When interpreting the words and actions of others, participants should always 13 | assume good intentions. 14 | * Behaviour which can be reasonably considered harassment will not be tolerated. 15 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'pry' 4 | gem 'rails' 5 | gem 'rails-controller-testing' 6 | gemspec 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 [Noam Ben-Ari](mailto:nbenari@gmail.com) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MAINTAINING.md: -------------------------------------------------------------------------------- 1 | # Maintaining Sorcery 2 | 3 | This will eventually be fleshed out so that anyone should be able to pick up and 4 | maintain Sorcery by following this guide. It will provide step-by-step guides 5 | for common tasks such as releasing new versions, as well as explain how to 6 | triage issues and keep the CHANGELOG up-to-date. 7 | 8 | ## Table of Contents 9 | 10 | 1. [Merging Pull Requests](#merging-pull-requests) 11 | 1. [Versioning](#versioning) 12 | 1. [Version Naming](#version-naming) 13 | 1. [Releasing a New Version](#releasing-a-new-version) 14 | 15 | ## Merging Pull Requests 16 | 17 | TODO 18 | 19 | ## Versioning 20 | 21 | ### Version Naming 22 | 23 | Sorcery uses semantic versioning which can be found at: https://semver.org/ 24 | 25 | All versions of Sorcery should follow this format: `MAJOR.MINOR.PATCH` 26 | 27 | Where: 28 | 29 | * MAJOR - Includes backwards **incompatible** changes. 30 | * MINOR - Introduces new functionality but is fully backwards compatible. 31 | * PATCH - Fixes errors in existing functionality (must be backwards compatible). 32 | 33 | The changelog and git tags should use `vMAJOR.MINOR.PATCH` to indicate that the 34 | number represents a version of Sorcery. For example, `1.0.0` would become 35 | `v1.0.0`. 36 | 37 | ### Releasing a New Version 38 | 39 | When it's time to release a new version, you'll want to ensure all the changes 40 | you need are on the master branch and that there is a passing build. Then follow 41 | this checklist and prepare a release commit: 42 | 43 | NOTE: `X.Y.Z` and `vX.Y.Z` are given as examples, and should be replaced with 44 | whatever version you are releasing. See: [Version Naming](#version-naming) 45 | 46 | 1. Update CHANGELOG.md 47 | 1. Check for any changes that have been included since the last release that 48 | are not reflected in the changelog. Add any missing entries to the `HEAD` 49 | section. 50 | 1. Check the changes in `HEAD` to determine what version increment is 51 | appropriate. See [Version Naming](#version-naming) if unsure. 52 | 1. Replace `## HEAD` with `## vX.Y.Z` and create a new `## HEAD` section 53 | above the latest version. 54 | 1. Update Gem Version 55 | 1. Update `./lib/sorcery/version.rb` to 'X.Y.Z' 56 | 1. Stage your changes and create a commit 57 | 1. `git add -A` 58 | 1. `git commit -m "Release vX.Y.Z"` 59 | 1. TODO: Gem Release (WIP) 60 | 1. `cd ` 61 | 1. `gem build` 62 | 1. `gem push ` 63 | 1. TODO: Version tagging 64 | 1. Release new version via github interface 65 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | 3 | require 'rspec/core/rake_task' 4 | require 'rubocop/rake_task' 5 | RSpec::Core::RakeTask.new(:spec) 6 | RuboCop::RakeTask.new 7 | 8 | task default: [:rubocop, :spec] 9 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | --------- | ------------------ | 7 | | ~> 0.16.0 | :white_check_mark: | 8 | | ~> 0.15.0 | :white_check_mark: | 9 | | < 0.15.0 | :x: | 10 | 11 | ## Reporting a Vulnerability 12 | 13 | Email the current maintainer(s) with a description of the vulnerability. You 14 | should expect a response within 48 hours. If the vulnerability is accepted, a 15 | Github advisory will be created and eventually released with a CVE corresponding 16 | to the issue found. 17 | 18 | A list of the current maintainers can be found on the README under the contact 19 | section. See: [README.md](https://github.com/Sorcery/sorcery#contact) 20 | -------------------------------------------------------------------------------- /gemfiles/rails_71.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', '~> 7.1.0' 4 | gem 'rails-controller-testing' 5 | gem 'sqlite3', '~> 1.4' 6 | gem 'rspec-rails', '>= 6.1' 7 | gemspec path: '..' 8 | -------------------------------------------------------------------------------- /gemfiles/rails_72.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', '~> 7.2.2.1' 4 | gem 'rails-controller-testing' 5 | gem 'sqlite3', '~> 2.5.0' 6 | gem 'rspec-rails', '>= 6.1' 7 | gemspec path: '..' 8 | -------------------------------------------------------------------------------- /gemfiles/rails_80.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', '~> 8.0' 4 | gem 'rails-controller-testing' 5 | gem 'sqlite3', '~> 2.5.0' 6 | gem 'rspec-rails', '>= 6.1' 7 | gemspec path: '..' 8 | -------------------------------------------------------------------------------- /lib/generators/sorcery/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Generates the necessary files to get you up and running with Sorcery gem 3 | 4 | Examples: 5 | rails generate sorcery:install 6 | 7 | This will generate the core migration file, the initializer file and the 'User' model class. 8 | 9 | rails generate sorcery:install remember_me reset_password 10 | 11 | This will generate the migrations files for remember_me and reset_password submodules 12 | and will create the initializer file (and add submodules to it), and create the 'User' model class. 13 | 14 | rails generate sorcery:install --model Person 15 | 16 | This will generate the core migration file, the initializer and change the model class 17 | (in the initializer and migration files) to the class 'Person' (and it's pluralized version, 'people') 18 | 19 | rails generate sorcery:install http_basic_auth external remember_me --only-submodules 20 | 21 | This will generate only the migration files for the specified submodules and will 22 | add them to the initializer file. 23 | -------------------------------------------------------------------------------- /lib/generators/sorcery/helpers.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Generators 3 | module Helpers 4 | private 5 | 6 | def sorcery_config_path 7 | 'config/initializers/sorcery.rb' 8 | end 9 | 10 | # Either return the model passed in a classified form or return the default "User". 11 | def model_class_name 12 | options[:model] ? options[:model].classify : 'User' 13 | end 14 | 15 | def tableized_model_class 16 | options[:model] ? options[:model].gsub(/::/, '').tableize : 'users' 17 | end 18 | 19 | def model_path 20 | @model_path ||= File.join('app', 'models', "#{file_path}.rb") 21 | end 22 | 23 | def file_path 24 | model_name.underscore 25 | end 26 | 27 | def namespace 28 | Rails::Generators.namespace if Rails::Generators.respond_to?(:namespace) 29 | end 30 | 31 | def namespaced? 32 | !!namespace 33 | end 34 | 35 | def model_name 36 | if namespaced? 37 | [namespace.to_s] + [model_class_name] 38 | else 39 | [model_class_name] 40 | end.join('::') 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/generators/sorcery/install_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators/migration' 2 | require 'generators/sorcery/helpers' 3 | 4 | module Sorcery 5 | module Generators 6 | class InstallGenerator < Rails::Generators::Base 7 | include Rails::Generators::Migration 8 | include Sorcery::Generators::Helpers 9 | 10 | source_root File.expand_path('templates', __dir__) 11 | 12 | argument :submodules, optional: true, type: :array, banner: 'submodules' 13 | 14 | class_option :model, optional: true, type: :string, banner: 'model', 15 | desc: "Specify the model class name if you will use anything other than 'User'" 16 | 17 | class_option :migrations, optional: true, type: :boolean, banner: 'migrations', 18 | desc: '[DEPRECATED] Please use --only-submodules option instead' 19 | 20 | class_option :only_submodules, optional: true, type: :boolean, banner: 'only-submodules', 21 | 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)" 22 | 23 | def check_deprecated_options 24 | return unless options[:migrations] 25 | 26 | warn('[DEPRECATED] `--migrations` option is deprecated, please use `--only-submodules` instead') 27 | end 28 | 29 | # Copy the initializer file to config/initializers folder. 30 | def copy_initializer_file 31 | template 'initializer.rb', sorcery_config_path unless only_submodules? 32 | end 33 | 34 | def configure_initializer_file 35 | # Add submodules to the initializer file. 36 | return unless submodules 37 | 38 | submodule_names = submodules.collect { |submodule| ':' + submodule } 39 | 40 | gsub_file sorcery_config_path, /submodules = \[.*\]/ do |str| 41 | current_submodule_names = (str =~ /\[(.*)\]/ ? Regexp.last_match(1) : '').delete(' ').split(',') 42 | "submodules = [#{(current_submodule_names | submodule_names).join(', ')}]" 43 | end 44 | end 45 | 46 | def configure_model 47 | # Generate the model and add 'authenticates_with_sorcery!' unless you passed --only-submodules 48 | return if only_submodules? 49 | 50 | generate "model #{model_class_name} --skip-migration" 51 | end 52 | 53 | def inject_sorcery_to_model 54 | indents = ' ' * (namespaced? ? 2 : 1) 55 | 56 | inject_into_class(model_path, model_class_name, "#{indents}authenticates_with_sorcery!\n") 57 | end 58 | 59 | # Copy the migrations files to db/migrate folder 60 | def copy_migration_files 61 | # Copy core migration file in all cases except when you pass --only-submodules. 62 | return unless defined?(ActiveRecord) 63 | 64 | migration_template 'migration/core.rb', 'db/migrate/sorcery_core.rb', migration_class_name: migration_class_name unless only_submodules? 65 | 66 | return unless submodules 67 | 68 | submodules.each do |submodule| 69 | unless %w[http_basic_auth session_timeout core].include?(submodule) 70 | migration_template "migration/#{submodule}.rb", "db/migrate/sorcery_#{submodule}.rb", migration_class_name: migration_class_name 71 | end 72 | end 73 | end 74 | 75 | # Define the next_migration_number method (necessary for the migration_template method to work) 76 | def self.next_migration_number(dirname) 77 | if timestamped_migrations? 78 | sleep 1 # make sure each time we get a different timestamp 79 | Time.new.utc.strftime('%Y%m%d%H%M%S') 80 | else 81 | format('%.3d', (current_migration_number(dirname) + 1)) 82 | end 83 | end 84 | 85 | private 86 | 87 | def self.timestamped_migrations? 88 | if Rails::VERSION::MAJOR >= 7 89 | ActiveRecord.timestamped_migrations 90 | else 91 | ActiveRecord::Base.timestamped_migrations 92 | end 93 | end 94 | 95 | def only_submodules? 96 | options[:migrations] || options[:only_submodules] 97 | end 98 | 99 | def migration_class_name 100 | "ActiveRecord::Migration[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]" 101 | end 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /lib/generators/sorcery/templates/migration/activity_logging.rb: -------------------------------------------------------------------------------- 1 | class SorceryActivityLogging < <%= migration_class_name %> 2 | def change 3 | add_column :<%= tableized_model_class %>, :last_login_at, :datetime, default: nil 4 | add_column :<%= tableized_model_class %>, :last_logout_at, :datetime, default: nil 5 | add_column :<%= tableized_model_class %>, :last_activity_at, :datetime, default: nil 6 | add_column :<%= tableized_model_class %>, :last_login_from_ip_address, :string, default: nil 7 | 8 | add_index :<%= tableized_model_class %>, [:last_logout_at, :last_activity_at] 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/generators/sorcery/templates/migration/brute_force_protection.rb: -------------------------------------------------------------------------------- 1 | class SorceryBruteForceProtection < <%= migration_class_name %> 2 | def change 3 | add_column :<%= tableized_model_class %>, :failed_logins_count, :integer, default: 0 4 | add_column :<%= tableized_model_class %>, :lock_expires_at, :datetime, default: nil 5 | add_column :<%= tableized_model_class %>, :unlock_token, :string, default: nil 6 | 7 | add_index :<%= tableized_model_class %>, :unlock_token 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/generators/sorcery/templates/migration/core.rb: -------------------------------------------------------------------------------- 1 | class SorceryCore < <%= migration_class_name %> 2 | def change 3 | create_table :<%= tableized_model_class %> do |t| 4 | t.string :email, null: false, index: { unique: true } 5 | t.string :crypted_password 6 | t.string :salt 7 | 8 | t.timestamps null: false 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/generators/sorcery/templates/migration/external.rb: -------------------------------------------------------------------------------- 1 | class SorceryExternal < <%= migration_class_name %> 2 | def change 3 | create_table :authentications do |t| 4 | t.integer :<%= tableized_model_class.singularize %>_id, null: false 5 | t.string :provider, :uid, null: false 6 | 7 | t.timestamps null: false 8 | end 9 | 10 | add_index :authentications, [:provider, :uid] 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/generators/sorcery/templates/migration/magic_login.rb: -------------------------------------------------------------------------------- 1 | class SorceryMagicLogin < <%= migration_class_name %> 2 | def change 3 | add_column :<%= tableized_model_class %>, :magic_login_token, :string, default: nil 4 | add_column :<%= tableized_model_class %>, :magic_login_token_expires_at, :datetime, default: nil 5 | add_column :<%= tableized_model_class %>, :magic_login_email_sent_at, :datetime, default: nil 6 | 7 | add_index :<%= tableized_model_class %>, :magic_login_token 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/generators/sorcery/templates/migration/remember_me.rb: -------------------------------------------------------------------------------- 1 | class SorceryRememberMe < <%= migration_class_name %> 2 | def change 3 | add_column :<%= tableized_model_class %>, :remember_me_token, :string, default: nil 4 | add_column :<%= tableized_model_class %>, :remember_me_token_expires_at, :datetime, default: nil 5 | 6 | add_index :<%= tableized_model_class %>, :remember_me_token 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/generators/sorcery/templates/migration/reset_password.rb: -------------------------------------------------------------------------------- 1 | class SorceryResetPassword < <%= migration_class_name %> 2 | def change 3 | add_column :<%= tableized_model_class %>, :reset_password_token, :string, default: nil 4 | add_column :<%= tableized_model_class %>, :reset_password_token_expires_at, :datetime, default: nil 5 | add_column :<%= tableized_model_class %>, :reset_password_email_sent_at, :datetime, default: nil 6 | add_column :<%= tableized_model_class %>, :access_count_to_reset_password_page, :integer, default: 0 7 | 8 | add_index :<%= tableized_model_class %>, :reset_password_token 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/generators/sorcery/templates/migration/user_activation.rb: -------------------------------------------------------------------------------- 1 | class SorceryUserActivation < <%= migration_class_name %> 2 | def change 3 | add_column :<%= tableized_model_class %>, :activation_state, :string, default: nil 4 | add_column :<%= tableized_model_class %>, :activation_token, :string, default: nil 5 | add_column :<%= tableized_model_class %>, :activation_token_expires_at, :datetime, default: nil 6 | 7 | add_index :<%= tableized_model_class %>, :activation_token 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/sorcery.rb: -------------------------------------------------------------------------------- 1 | require 'sorcery/version' 2 | 3 | module Sorcery 4 | require 'sorcery/model' 5 | 6 | module Adapters 7 | require 'sorcery/adapters/base_adapter' 8 | end 9 | 10 | module Model 11 | require 'sorcery/model/temporary_token' 12 | require 'sorcery/model/config' 13 | 14 | module Submodules 15 | require 'sorcery/model/submodules/user_activation' 16 | require 'sorcery/model/submodules/reset_password' 17 | require 'sorcery/model/submodules/remember_me' 18 | require 'sorcery/model/submodules/activity_logging' 19 | require 'sorcery/model/submodules/brute_force_protection' 20 | require 'sorcery/model/submodules/external' 21 | require 'sorcery/model/submodules/magic_login' 22 | end 23 | end 24 | 25 | require 'sorcery/controller' 26 | 27 | module Controller 28 | autoload :Config, 'sorcery/controller/config' 29 | module Submodules 30 | require 'sorcery/controller/submodules/remember_me' 31 | require 'sorcery/controller/submodules/session_timeout' 32 | require 'sorcery/controller/submodules/brute_force_protection' 33 | require 'sorcery/controller/submodules/http_basic_auth' 34 | require 'sorcery/controller/submodules/activity_logging' 35 | require 'sorcery/controller/submodules/external' 36 | end 37 | end 38 | 39 | module Protocols 40 | require 'sorcery/protocols/oauth' 41 | require 'sorcery/protocols/oauth2' 42 | end 43 | 44 | module CryptoProviders 45 | require 'sorcery/crypto_providers/common' 46 | require 'sorcery/crypto_providers/aes256' 47 | require 'sorcery/crypto_providers/bcrypt' 48 | require 'sorcery/crypto_providers/md5' 49 | require 'sorcery/crypto_providers/sha1' 50 | require 'sorcery/crypto_providers/sha256' 51 | require 'sorcery/crypto_providers/sha512' 52 | end 53 | 54 | module TestHelpers 55 | require 'sorcery/test_helpers/internal' 56 | 57 | module Rails 58 | require 'sorcery/test_helpers/rails/controller' 59 | require 'sorcery/test_helpers/rails/integration' 60 | require 'sorcery/test_helpers/rails/request' 61 | end 62 | 63 | module Internal 64 | require 'sorcery/test_helpers/internal/rails' 65 | end 66 | end 67 | 68 | require 'sorcery/adapters/base_adapter' 69 | 70 | if defined?(ActiveRecord::Base) 71 | require 'sorcery/adapters/active_record_adapter' 72 | ActiveRecord::Base.extend Sorcery::Model 73 | 74 | ActiveRecord::Base.send :define_method, :sorcery_adapter do 75 | @sorcery_adapter ||= Sorcery::Adapters::ActiveRecordAdapter.new(self) 76 | end 77 | 78 | ActiveRecord::Base.send :define_singleton_method, :sorcery_adapter do 79 | Sorcery::Adapters::ActiveRecordAdapter.from(self) 80 | end 81 | end 82 | 83 | if defined?(Mongoid::Document) 84 | require 'sorcery/adapters/mongoid_adapter' 85 | Mongoid::Document::ClassMethods.send :include, Sorcery::Model 86 | 87 | Mongoid::Document.send :define_method, :sorcery_adapter do 88 | @sorcery_adapter ||= Sorcery::Adapters::MongoidAdapter.new(self) 89 | end 90 | 91 | Mongoid::Document::ClassMethods.send :define_method, :sorcery_adapter do 92 | Sorcery::Adapters::MongoidAdapter.from(self) 93 | end 94 | end 95 | 96 | require 'sorcery/engine' if defined?(Rails) 97 | end 98 | -------------------------------------------------------------------------------- /lib/sorcery/adapters/active_record_adapter.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Adapters 3 | class ActiveRecordAdapter < BaseAdapter 4 | def update_attributes(attrs) 5 | attrs.each do |name, value| 6 | @model.send(:"#{name}=", value) 7 | end 8 | primary_key = @model.class.primary_key 9 | updated_count = @model.class.where(:"#{primary_key}" => @model.send(:"#{primary_key}")).update_all(attrs) 10 | updated_count == 1 11 | end 12 | 13 | def save(options = {}) 14 | mthd = options.delete(:raise_on_failure) ? :save! : :save 15 | @model.send(mthd, **options) 16 | end 17 | 18 | def increment(field) 19 | @model.increment!(field) 20 | end 21 | 22 | def find_authentication_by_oauth_credentials(relation_name, provider, uid) 23 | @user_config ||= ::Sorcery::Controller::Config.user_class.to_s.constantize.sorcery_config 24 | conditions = { 25 | @user_config.provider_uid_attribute_name => uid, 26 | @user_config.provider_attribute_name => provider 27 | } 28 | 29 | @model.public_send(relation_name).where(conditions).first 30 | end 31 | 32 | class << self 33 | def define_field(name, type, options = {}) 34 | # AR fields are defined through migrations, only validator here 35 | end 36 | 37 | def define_callback(time, event, method_name, options = {}) 38 | @klass.send "#{time}_#{event}", method_name, **options.slice(:if, :on) 39 | end 40 | 41 | def find_by_oauth_credentials(provider, uid) 42 | @user_config ||= ::Sorcery::Controller::Config.user_class.to_s.constantize.sorcery_config 43 | conditions = { 44 | @user_config.provider_uid_attribute_name => uid, 45 | @user_config.provider_attribute_name => provider 46 | } 47 | 48 | @klass.where(conditions).first 49 | end 50 | 51 | def find_by_remember_me_token(token) 52 | @klass.where(@klass.sorcery_config.remember_me_token_attribute_name => token).first 53 | end 54 | 55 | def find_by_credentials(credentials) 56 | relation = nil 57 | 58 | @klass.sorcery_config.username_attribute_names.each do |attribute| 59 | if @klass.sorcery_config.downcase_username_before_authenticating 60 | condition = @klass.arel_table[attribute].lower.eq(@klass.arel_table.lower(credentials[0])) 61 | else 62 | condition = @klass.arel_table[attribute].eq(credentials[0]) 63 | end 64 | 65 | relation = if relation.nil? 66 | condition 67 | else 68 | relation.or(condition) 69 | end 70 | end 71 | 72 | @klass.where(relation).first 73 | end 74 | 75 | def find_by_token(token_attr_name, token) 76 | condition = @klass.arel_table[token_attr_name].eq(token) 77 | 78 | @klass.where(condition).first 79 | end 80 | 81 | def find_by_activation_token(token) 82 | @klass.where(@klass.sorcery_config.activation_token_attribute_name => token).first 83 | end 84 | 85 | def find_by_id(id) 86 | @klass.find_by_id(id) 87 | end 88 | 89 | def find_by_username(username) 90 | @klass.sorcery_config.username_attribute_names.each do |attribute| 91 | if @klass.sorcery_config.downcase_username_before_authenticating 92 | username = username.downcase 93 | end 94 | 95 | result = @klass.where(attribute => username).first 96 | return result if result 97 | end 98 | end 99 | 100 | def find_by_email(email) 101 | @klass.where(@klass.sorcery_config.email_attribute_name => email).first 102 | end 103 | 104 | def transaction(&blk) 105 | @klass.tap(&blk) 106 | end 107 | end 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/sorcery/adapters/base_adapter.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Adapters 3 | class BaseAdapter 4 | def initialize(model) 5 | @model = model 6 | end 7 | 8 | def self.from(klass) 9 | @klass = klass 10 | self 11 | end 12 | 13 | def self.delete_all 14 | @klass.delete_all 15 | end 16 | 17 | def self.find(id) 18 | find_by_id(id) 19 | end 20 | 21 | def increment(field) 22 | @model.increment(field) 23 | end 24 | 25 | def update_attribute(name, value) 26 | update_attributes(name => value) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/sorcery/adapters/mongoid_adapter.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Adapters 3 | class MongoidAdapter < BaseAdapter 4 | def increment(attr) 5 | @model.inc(attr => 1) 6 | end 7 | 8 | def update_attributes(attrs) 9 | attrs.each do |name, value| 10 | attrs[name] = value.utc if value.is_a?(ActiveSupport::TimeWithZone) 11 | @model.send(:"#{name}=", value) 12 | end 13 | @model.class.where(_id: @model.id).update_all(attrs) 14 | end 15 | 16 | def update_attribute(name, value) 17 | update_attributes(name => value) 18 | end 19 | 20 | def save(options = {}) 21 | mthd = options.delete(:raise_on_failure) ? :save! : :save 22 | @model.send(mthd, options) 23 | end 24 | 25 | 26 | class << self 27 | def define_field(name, type, options = {}) 28 | @klass.field name, options.slice(:default).merge(type: type) 29 | end 30 | 31 | def define_callback(time, event, method_name, options = {}) 32 | @klass.send callback_name(time, event, options), method_name, **options.slice(:if) 33 | end 34 | 35 | def callback_name(time, event, options) 36 | if event == :commit 37 | options[:on] == :create ? "#{time}_create" : "#{time}_save" 38 | else 39 | "#{time}_#{event}" 40 | end 41 | end 42 | 43 | def credential_regex(credential) 44 | return { :$regex => /^#{Regexp.escape(credential)}$/i } if @klass.sorcery_config.downcase_username_before_authenticating 45 | 46 | credential 47 | end 48 | 49 | def find_by_credentials(credentials) 50 | @klass.sorcery_config.username_attribute_names.each do |attribute| 51 | @user = @klass.where(attribute => credential_regex(credentials[0])).first 52 | break if @user 53 | end 54 | @user 55 | end 56 | 57 | def find_by_oauth_credentials(provider, uid) 58 | @user_config ||= ::Sorcery::Controller::Config.user_class.to_s.constantize.sorcery_config 59 | @klass.where(@user_config.provider_attribute_name => provider, @user_config.provider_uid_attribute_name => uid).first 60 | end 61 | 62 | def find_by_activation_token(token) 63 | @klass.where(@klass.sorcery_config.activation_token_attribute_name => token).first 64 | end 65 | 66 | def find_by_remember_me_token(token) 67 | @klass.where(@klass.sorcery_config.remember_me_token_attribute_name => token).first 68 | end 69 | 70 | def transaction(&blk) 71 | tap(&blk) 72 | end 73 | 74 | def find_by_id(id) 75 | @klass.find(id) 76 | rescue ::Mongoid::Errors::DocumentNotFound 77 | nil 78 | end 79 | 80 | def find_by_username(username) 81 | query = @klass.sorcery_config.username_attribute_names.map { |name| { name => username } } 82 | @klass.any_of(*query).first 83 | end 84 | 85 | def find_by_token(token_attr_name, token) 86 | @klass.where(token_attr_name => token).first 87 | end 88 | 89 | def find_by_email(email) 90 | @klass.where(@klass.sorcery_config.email_attribute_name => email).first 91 | end 92 | 93 | def get_current_users 94 | config = @klass.sorcery_config 95 | @klass.where( 96 | config.last_activity_at_attribute_name.ne => nil 97 | ).where( 98 | "this.#{config.last_logout_at_attribute_name} == null || this.#{config.last_activity_at_attribute_name} > this.#{config.last_logout_at_attribute_name}" 99 | ).where( 100 | config.last_activity_at_attribute_name.gt => config.activity_timeout.seconds.ago.utc 101 | ).order_by(%i[_id asc]) 102 | end 103 | end 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/sorcery/controller/config.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Controller 3 | module Config 4 | class << self 5 | attr_accessor :submodules 6 | # what class to use as the user class. 7 | attr_accessor :user_class 8 | # what controller action to call for non-authenticated users. 9 | attr_accessor :not_authenticated_action 10 | # when a non logged in user tries to enter a page that requires login, 11 | # save the URL he wanted to reach, and send him there after login. 12 | attr_accessor :save_return_to_url 13 | # set domain option for cookies 14 | attr_accessor :cookie_domain 15 | 16 | attr_accessor :login_sources 17 | attr_accessor :after_login 18 | attr_accessor :after_failed_login 19 | attr_accessor :before_logout 20 | attr_accessor :after_logout 21 | attr_accessor :after_remember_me 22 | 23 | def init! 24 | @defaults = { 25 | :@user_class => nil, 26 | :@submodules => [], 27 | :@not_authenticated_action => :not_authenticated, 28 | :@login_sources => Set.new, 29 | :@after_login => Set.new, 30 | :@after_failed_login => Set.new, 31 | :@before_logout => Set.new, 32 | :@after_logout => Set.new, 33 | :@after_remember_me => Set.new, 34 | :@save_return_to_url => true, 35 | :@cookie_domain => nil 36 | } 37 | end 38 | 39 | # Resets all configuration options to their default values. 40 | def reset! 41 | @defaults.each do |k, v| 42 | instance_variable_set(k, v) 43 | end 44 | end 45 | 46 | def update! 47 | @defaults.each do |k, v| 48 | instance_variable_set(k, v) unless instance_variable_defined?(k) 49 | end 50 | end 51 | 52 | def user_config(&blk) 53 | block_given? ? @user_config = blk : @user_config 54 | end 55 | 56 | def configure(&blk) 57 | @configure_blk = blk 58 | end 59 | 60 | def configure! 61 | @configure_blk.call(self) if @configure_blk 62 | end 63 | end 64 | 65 | init! 66 | reset! 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/sorcery/controller/submodules/activity_logging.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Controller 3 | module Submodules 4 | # This submodule keeps track of events such as login, logout, 5 | # and last activity time, per user. 6 | # It helps in estimating which users are active now in the site. 7 | # This cannot be determined absolutely because a user might be 8 | # reading a page without clicking anything for a while. 9 | # This is the controller part of the submodule, which adds hooks 10 | # to register user events, 11 | # and methods to collect active users data for use in the app. 12 | # see Socery::Model::Submodules::ActivityLogging for configuration 13 | # options. 14 | module ActivityLogging 15 | def self.included(base) 16 | base.send(:include, InstanceMethods) 17 | Config.module_eval do 18 | class << self 19 | attr_accessor :register_login_time 20 | attr_accessor :register_logout_time 21 | attr_accessor :register_last_activity_time 22 | attr_accessor :register_last_ip_address 23 | 24 | def merge_activity_logging_defaults! 25 | @defaults.merge!(:@register_login_time => true, 26 | :@register_logout_time => true, 27 | :@register_last_activity_time => true, 28 | :@register_last_ip_address => true) 29 | end 30 | end 31 | merge_activity_logging_defaults! 32 | end 33 | 34 | Config.after_login << :register_login_time_to_db 35 | Config.after_login << :register_last_ip_address 36 | Config.before_logout << :register_logout_time_to_db 37 | 38 | base.after_action :register_last_activity_time_to_db 39 | end 40 | 41 | module InstanceMethods 42 | protected 43 | 44 | # registers last login time on every login. 45 | # This runs as a hook just after a successful login. 46 | def register_login_time_to_db(user, _credentials) 47 | return unless Config.register_login_time 48 | 49 | user.set_last_login_at(Time.now.in_time_zone) 50 | end 51 | 52 | # registers last logout time on every logout. 53 | # This runs as a hook just before a logout. 54 | def register_logout_time_to_db 55 | return unless Config.register_logout_time 56 | 57 | current_user.set_last_logout_at(Time.now.in_time_zone) 58 | end 59 | 60 | # Updates last activity time on every request. 61 | # The only exception is logout - we do not update activity on logout 62 | def register_last_activity_time_to_db 63 | return unless Config.register_last_activity_time 64 | return unless logged_in? 65 | 66 | current_user.set_last_activity_at(Time.now.in_time_zone) 67 | end 68 | 69 | # Updates IP address on every login. 70 | # This runs as a hook just after a successful login. 71 | def register_last_ip_address(_user, _credentials) 72 | return unless Config.register_last_ip_address 73 | 74 | current_user.set_last_ip_address(request.remote_ip) 75 | end 76 | end 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/sorcery/controller/submodules/brute_force_protection.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Controller 3 | module Submodules 4 | # This module helps protect user accounts by locking them down after too 5 | # many failed attemps to login were detected. 6 | # This is the controller part of the submodule which takes care of 7 | # updating the failed logins and resetting them. 8 | # See Sorcery::Model::Submodules::BruteForceProtection for configuration 9 | # options. 10 | module BruteForceProtection 11 | def self.included(base) 12 | base.send(:include, InstanceMethods) 13 | 14 | Config.after_login << :reset_failed_logins_count! 15 | Config.after_failed_login << :update_failed_logins_count! 16 | end 17 | 18 | module InstanceMethods 19 | protected 20 | 21 | # Increments the failed logins counter on every failed login. 22 | # Runs as a hook after a failed login. 23 | def update_failed_logins_count!(credentials) 24 | user = user_class.sorcery_adapter.find_by_credentials(credentials) 25 | user.register_failed_login! if user 26 | end 27 | 28 | # Resets the failed logins counter. 29 | # Runs as a hook after a successful login. 30 | def reset_failed_logins_count!(user, _credentials) 31 | user.sorcery_adapter.update_attribute(user_class.sorcery_config.failed_logins_count_attribute_name, 0) 32 | end 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/sorcery/controller/submodules/http_basic_auth.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Controller 3 | module Submodules 4 | # This submodule integrates HTTP Basic authentication into sorcery. 5 | # You are provided with a before action, require_login_from_http_basic, 6 | # which requests the browser for authentication. 7 | # Then the rest of the submodule takes care of logging the user in 8 | # into the session, so that the next requests will keep him logged in. 9 | module HttpBasicAuth 10 | def self.included(base) 11 | base.send(:include, InstanceMethods) 12 | Config.module_eval do 13 | class << self 14 | attr_accessor :controller_to_realm_map # What realm to display for which controller name. 15 | 16 | def merge_http_basic_auth_defaults! 17 | @defaults.merge!(:@controller_to_realm_map => { 'application' => 'Application' }) 18 | end 19 | end 20 | merge_http_basic_auth_defaults! 21 | end 22 | 23 | Config.login_sources << :login_from_basic_auth 24 | end 25 | 26 | module InstanceMethods 27 | protected 28 | 29 | # to be used as a before_action. 30 | # The method sets a session when requesting the user's credentials. 31 | # This is a trick to overcome the way HTTP authentication works (explained below): 32 | # 33 | # Once the user fills the credentials once, the browser will always send it to the 34 | # server when visiting the website, until the browser is closed. 35 | # This causes wierd behaviour if the user logs out. The session is reset, yet the 36 | # user is re-logged in by the before_action calling 'login_from_basic_auth'. 37 | # To overcome this, we set a session when requesting the password, which logout will 38 | # reset, and that's how we know if we need to request for HTTP auth again. 39 | def require_login_from_http_basic 40 | (request_http_basic_authentication(realm_name_by_controller) && (session[:http_authentication_used] = true) && return) if request.authorization.nil? || session[:http_authentication_used].nil? 41 | require_login 42 | session[:http_authentication_used] = nil unless logged_in? 43 | end 44 | 45 | # given to main controller module as a login source callback 46 | def login_from_basic_auth 47 | authenticate_with_http_basic do |username, password| 48 | @current_user = (user_class.authenticate(username, password) if session[:http_authentication_used]) || false 49 | auto_login(@current_user) if @current_user 50 | @current_user 51 | end 52 | end 53 | 54 | # Sets the realm name by searching the controller name in the hash given at configuration time. 55 | def realm_name_by_controller 56 | if defined?(ActionController::Base) 57 | current_controller = self.class 58 | while current_controller != ActionController::Base 59 | result = Config.controller_to_realm_map[current_controller.controller_name] 60 | return result if result 61 | 62 | current_controller = current_controller.superclass 63 | end 64 | nil 65 | else 66 | Config.controller_to_realm_map['application'] 67 | end 68 | end 69 | end 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/sorcery/controller/submodules/remember_me.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Controller 3 | module Submodules 4 | # The Remember Me submodule takes care of setting the user's cookie so that he will 5 | # be automatically logged in to the site on every visit, 6 | # until the cookie expires. 7 | # See Sorcery::Model::Submodules::RememberMe for configuration options. 8 | module RememberMe 9 | def self.included(base) 10 | base.send(:include, InstanceMethods) 11 | Config.module_eval do 12 | class << self 13 | attr_accessor :remember_me_httponly 14 | def merge_remember_me_defaults! 15 | @defaults.merge!(:@remember_me_httponly => true) 16 | end 17 | end 18 | merge_remember_me_defaults! 19 | end 20 | 21 | Config.login_sources << :login_from_cookie 22 | Config.before_logout << :forget_me! 23 | end 24 | 25 | module InstanceMethods 26 | # This method sets the cookie and calls the user to save the token and the expiration to db. 27 | def remember_me! 28 | current_user.remember_me! 29 | set_remember_me_cookie!(current_user) 30 | end 31 | 32 | # Clears the cookie, and depending on the value of remember_me_token_persist_globally, may clear the token value. 33 | def forget_me! 34 | current_user.forget_me! 35 | cookies.delete(:remember_me_token, domain: Config.cookie_domain) 36 | end 37 | 38 | # Clears the cookie, and clears the token value. 39 | def force_forget_me! 40 | current_user.force_forget_me! 41 | cookies.delete(:remember_me_token, domain: Config.cookie_domain) 42 | end 43 | 44 | # Override. 45 | # logins a user instance, and optionally remembers him. 46 | def auto_login(user, should_remember = false) 47 | session[:user_id] = user.id.to_s 48 | @current_user = user 49 | remember_me! if should_remember 50 | end 51 | 52 | protected 53 | 54 | # Checks the cookie for a remember me token, tried to find a user with that token 55 | # and logs the user in if found. 56 | # Runs as a login source. See 'current_user' method for how it is used. 57 | def login_from_cookie 58 | user = cookies.signed[:remember_me_token] && user_class.sorcery_adapter.find_by_remember_me_token(cookies.signed[:remember_me_token]) if defined? cookies 59 | if user && user.has_remember_me_token? 60 | set_remember_me_cookie!(user) 61 | session[:user_id] = user.id.to_s 62 | after_remember_me!(user) 63 | @current_user = user 64 | else 65 | @current_user = false 66 | end 67 | end 68 | 69 | def set_remember_me_cookie!(user) 70 | cookies.signed[:remember_me_token] = { 71 | value: user.send(user.sorcery_config.remember_me_token_attribute_name), 72 | expires: user.send(user.sorcery_config.remember_me_token_expires_at_attribute_name), 73 | httponly: Config.remember_me_httponly, 74 | domain: Config.cookie_domain 75 | } 76 | end 77 | end 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/sorcery/controller/submodules/session_timeout.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Controller 3 | module Submodules 4 | # This submodule helps you set a timeout to all user sessions. 5 | # The timeout can be configured and also you can choose to reset it on every user action. 6 | module SessionTimeout 7 | def self.included(base) 8 | base.send(:include, InstanceMethods) 9 | Config.module_eval do 10 | class << self 11 | # how long in seconds to keep the session alive. 12 | attr_accessor :session_timeout 13 | # use the last action as the beginning of session timeout. 14 | attr_accessor :session_timeout_from_last_action 15 | # allow users to invalidate active sessions 16 | attr_accessor :session_timeout_invalidate_active_sessions_enabled 17 | 18 | def merge_session_timeout_defaults! 19 | @defaults.merge!(:@session_timeout => 3600, # 1.hour 20 | :@session_timeout_from_last_action => false, 21 | :@session_timeout_invalidate_active_sessions_enabled => false) 22 | end 23 | end 24 | merge_session_timeout_defaults! 25 | end 26 | 27 | Config.after_login << :register_login_time 28 | Config.after_remember_me << :register_login_time 29 | 30 | base.prepend_before_action :validate_session 31 | end 32 | 33 | module InstanceMethods 34 | def invalidate_active_sessions! 35 | return unless Config.session_timeout_invalidate_active_sessions_enabled 36 | return unless current_user.present? 37 | 38 | current_user.send(:invalidate_sessions_before=, Time.now.in_time_zone) 39 | current_user.save 40 | end 41 | 42 | protected 43 | 44 | # Registers last login to be used as the timeout starting point. 45 | # Runs as a hook after a successful login. 46 | def register_login_time(_user, _credentials = nil) 47 | session[:login_time] = session[:last_action_time] = Time.now.in_time_zone 48 | end 49 | 50 | # Checks if session timeout was reached and expires the current session if so. 51 | # To be used as a before_action, before require_login 52 | def validate_session 53 | session_to_use = Config.session_timeout_from_last_action ? session[:last_action_time] : session[:login_time] 54 | if (session_to_use && sorcery_session_expired?(session_to_use.to_time)) || sorcery_session_invalidated? 55 | reset_sorcery_session 56 | remove_instance_variable :@current_user if defined? @current_user 57 | else 58 | session[:last_action_time] = Time.now.in_time_zone 59 | end 60 | end 61 | 62 | def sorcery_session_expired?(time) 63 | Time.now.in_time_zone - time > Config.session_timeout 64 | end 65 | 66 | # Use login time if present, otherwise use last action time. 67 | def sorcery_session_invalidated? 68 | return false unless Config.session_timeout_invalidate_active_sessions_enabled 69 | return false unless current_user.present? && current_user.try(:invalidate_sessions_before).present? 70 | 71 | time = session[:login_time] || session[:last_action_time] || Time.now.in_time_zone 72 | time < current_user.invalidate_sessions_before 73 | end 74 | end 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/sorcery/crypto_providers/aes256.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | 3 | module Sorcery 4 | module CryptoProviders 5 | # This encryption method is reversible if you have the supplied key. 6 | # So in order to use this encryption method you must supply it with a key first. 7 | # In an initializer, or before your application initializes, you should do the following: 8 | # 9 | # Sorcery::Model::ConfigAES256.key = "my 32 bytes long key" 10 | # 11 | # My final comment is that this is a strong encryption method, 12 | # but its main weakness is that its reversible. If you do not need to reverse the hash 13 | # then you should consider Sha512 or BCrypt instead. 14 | # 15 | # Keep your key in a safe place, some even say the key should be stored on a separate server. 16 | # This won't hurt performance because the only time it will try and access the key on the 17 | # separate server is during initialization, which only 18 | # happens once. The reasoning behind this is if someone does compromise your server they 19 | # won't have the key also. Basically, you don't want to store the key with the lock. 20 | class AES256 21 | class << self 22 | attr_writer :key 23 | 24 | def encrypt(*tokens) 25 | aes.encrypt 26 | aes.key = @key 27 | [aes.update(tokens.join) + aes.final].pack('m').chomp 28 | end 29 | 30 | def matches?(crypted, *tokens) 31 | decrypt(crypted) == tokens.join 32 | rescue OpenSSL::Cipher::CipherError 33 | false 34 | end 35 | 36 | def decrypt(crypted) 37 | aes.decrypt 38 | aes.key = @key 39 | (aes.update(crypted.unpack('m').first) + aes.final) 40 | end 41 | 42 | private 43 | 44 | def aes 45 | raise ArgumentError, "#{name} expects a 32 bytes long key. Please use Sorcery::Model::Config.encryption_key to set it." if @key.nil? || @key == '' 46 | 47 | @aes ||= OpenSSL::Cipher.new('AES-256-ECB') 48 | end 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/sorcery/crypto_providers/bcrypt.rb: -------------------------------------------------------------------------------- 1 | require 'bcrypt' 2 | 3 | module Sorcery 4 | module CryptoProviders 5 | # For most apps Sha512 is plenty secure, but if you are building an app that stores nuclear 6 | # launch codes you might want to consier BCrypt. This is an extremely 7 | # secure hashing algorithm, mainly because it is slow. 8 | # A brute force attack on a BCrypt encrypted password would take much longer than a brute force attack on a 9 | # password encrypted with a Sha algorithm. Keep in mind you are sacrificing performance by using this, 10 | # generating a password takes exponentially longer than any 11 | # of the Sha algorithms. I did some benchmarking to save you some time with your decision: 12 | # 13 | # require "bcrypt" 14 | # require "digest" 15 | # require "benchmark" 16 | # 17 | # Benchmark.bm(18) do |x| 18 | # x.report("BCrypt (cost = 10:") { 100.times { BCrypt::Password.create("mypass", :cost => 10) } } 19 | # x.report("BCrypt (cost = 2:") { 100.times { BCrypt::Password.create("mypass", :cost => 2) } } 20 | # x.report("Sha512:") { 100.times { Digest::SHA512.hexdigest("mypass") } } 21 | # x.report("Sha1:") { 100.times { Digest::SHA1.hexdigest("mypass") } } 22 | # end 23 | # 24 | # user system total real 25 | # BCrypt (cost = 10): 10.780000 0.060000 10.840000 ( 11.100289) 26 | # BCrypt (cost = 2): 0.180000 0.000000 0.180000 ( 0.181914) 27 | # Sha512: 0.000000 0.000000 0.000000 ( 0.000829) 28 | # Sha1: 0.000000 0.000000 0.000000 ( 0.000395) 29 | # 30 | # You can play around with the cost to get that perfect balance between performance and security. 31 | # 32 | # Decided BCrypt is for you? Just insall the bcrypt gem: 33 | # 34 | # gem install bcrypt-ruby 35 | # 36 | # Update your initializer to use it: 37 | # 38 | # config.encryption_algorithm = :bcrypt 39 | # 40 | # You are good to go! 41 | class BCrypt 42 | class << self 43 | # Setting the option :pepper allows users to append an app-specific secret token. 44 | # Basically it's equivalent to :salt_join_token option, but have a different name to ensure 45 | # backward compatibility in generating/matching passwords. 46 | attr_accessor :pepper 47 | # This is the :cost option for the BCrpyt library. 48 | # The higher the cost the more secure it is and the longer is take the generate a hash. By default this is 10. 49 | # Set this to whatever you want, play around with it to get that perfect balance between 50 | # security and performance. 51 | def cost 52 | @cost ||= 10 53 | end 54 | attr_writer :cost 55 | alias stretches cost 56 | alias stretches= cost= 57 | 58 | # Creates a BCrypt hash for the password passed. 59 | def encrypt(*tokens) 60 | ::BCrypt::Password.create(join_tokens(tokens), cost: cost) 61 | end 62 | 63 | # Does the hash match the tokens? Uses the same tokens that were used to encrypt. 64 | def matches?(hash, *tokens) 65 | hash = new_from_hash(hash) 66 | return false if hash.nil? || hash == {} 67 | 68 | hash == join_tokens(tokens) 69 | end 70 | 71 | # This method is used as a flag to tell Sorcery to "resave" the password 72 | # upon a successful login, using the new cost 73 | def cost_matches?(hash) 74 | hash = new_from_hash(hash) 75 | if hash.nil? || hash == {} 76 | false 77 | else 78 | hash.cost == cost 79 | end 80 | end 81 | 82 | def reset! 83 | @cost = 10 84 | @pepper = '' 85 | end 86 | 87 | private 88 | 89 | def join_tokens(tokens) 90 | tokens.flatten.join.concat(pepper.to_s) # make sure to add pepper in case tokens have only one element 91 | end 92 | 93 | def new_from_hash(hash) 94 | ::BCrypt::Password.new(hash) 95 | rescue ::BCrypt::Errors::InvalidHash 96 | nil 97 | end 98 | end 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/sorcery/crypto_providers/common.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module CryptoProviders 3 | module Common 4 | def self.included(base) 5 | base.class_eval do 6 | class << self 7 | attr_accessor :join_token 8 | 9 | # The number of times to loop through the encryption. 10 | def stretches 11 | @stretches ||= 1 12 | end 13 | attr_writer :stretches 14 | 15 | def encrypt(*tokens) 16 | digest = tokens.flatten.compact.join(join_token) 17 | stretches.times { digest = secure_digest(digest) } 18 | digest 19 | end 20 | 21 | # Does the crypted password match the tokens? Uses the same tokens that were used to encrypt. 22 | def matches?(crypted, *tokens) 23 | encrypt(*tokens.compact) == crypted 24 | end 25 | 26 | def reset! 27 | @stretches = 1 28 | @join_token = nil 29 | end 30 | end 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/sorcery/crypto_providers/md5.rb: -------------------------------------------------------------------------------- 1 | require 'digest/md5' 2 | 3 | module Sorcery 4 | module CryptoProviders 5 | # This class was made for the users transitioning from md5 based systems. 6 | # I highly discourage using this crypto provider as it superbly inferior 7 | # to your other options. 8 | # 9 | # Please use any other provider offered by Sorcery. 10 | class MD5 11 | include Common 12 | class << self 13 | def secure_digest(digest) 14 | Digest::MD5.hexdigest(digest) 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/sorcery/crypto_providers/sha1.rb: -------------------------------------------------------------------------------- 1 | require 'digest/sha1' 2 | 3 | module Sorcery 4 | module CryptoProviders 5 | # This class was made for the users transitioning from restful_authentication. I highly discourage using this 6 | # crypto provider as it inferior to your other options. Please use any other provider offered by Sorcery. 7 | class SHA1 8 | include Common 9 | class << self 10 | def join_token 11 | @join_token ||= '--' 12 | end 13 | 14 | # Turns your raw password into a Sha1 hash. 15 | def encrypt(*tokens) 16 | tokens = tokens.flatten 17 | digest = tokens.shift 18 | stretches.times { digest = secure_digest([digest, *tokens].join(join_token)) } 19 | digest 20 | end 21 | 22 | def secure_digest(digest) 23 | Digest::SHA1.hexdigest(digest) 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/sorcery/crypto_providers/sha256.rb: -------------------------------------------------------------------------------- 1 | require 'digest/sha2' 2 | 3 | module Sorcery 4 | # The activate_sorcery method has a custom_crypto_provider configuration option. 5 | # This allows you to use any type of encryption you like. 6 | # Just create a class with a class level encrypt and matches? method. See example below. 7 | # 8 | # === Example 9 | # 10 | # class MyAwesomeEncryptionMethod 11 | # def self.encrypt(*tokens) 12 | # # the tokens passed will be an array of objects, what type of object is irrelevant, 13 | # # just do what you need to do with them and return a single encrypted string. 14 | # # for example, you will most likely join all of the objects into a single string and then encrypt that string 15 | # end 16 | # 17 | # def self.matches?(crypted, *tokens) 18 | # # return true if the crypted string matches the tokens. 19 | # # depending on your algorithm you might decrypt the string then compare it to the token, or you might 20 | # # encrypt the tokens and make sure it matches the crypted string, its up to you 21 | # end 22 | # end 23 | module CryptoProviders 24 | # = Sha256 25 | # 26 | # Uses the Sha256 hash algorithm to encrypt passwords. 27 | class SHA256 28 | include Common 29 | class << self 30 | def secure_digest(digest) 31 | Digest::SHA256.hexdigest(digest) 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/sorcery/crypto_providers/sha512.rb: -------------------------------------------------------------------------------- 1 | require 'digest/sha2' 2 | 3 | module Sorcery 4 | # The activate_sorcery method has a custom_crypto_provider configuration option. 5 | # This allows you to use any type of encryption you like. 6 | # Just create a class with a class level encrypt and matches? method. See example below. 7 | # 8 | # === Example 9 | # 10 | # class MyAwesomeEncryptionMethod 11 | # def self.encrypt(*tokens) 12 | # # the tokens passed will be an array of objects, what type of object is irrelevant, 13 | # # just do what you need to do with them and return a single encrypted string. 14 | # # for example, you will most likely join all of the objects into a single string and then encrypt that string 15 | # end 16 | # 17 | # def self.matches?(crypted, *tokens) 18 | # # return true if the crypted string matches the tokens. 19 | # # depending on your algorithm you might decrypt the string then compare it to the token, or you might 20 | # # encrypt the tokens and make sure it matches the crypted string, its up to you 21 | # end 22 | # end 23 | module CryptoProviders 24 | # = Sha512 25 | # 26 | # Uses the Sha512 hash algorithm to encrypt passwords. 27 | class SHA512 28 | include Common 29 | class << self 30 | def secure_digest(digest) 31 | Digest::SHA512.hexdigest(digest) 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/sorcery/engine.rb: -------------------------------------------------------------------------------- 1 | require 'sorcery' 2 | require 'rails' 3 | 4 | module Sorcery 5 | # The Sorcery engine takes care of extending ActiveRecord (if used) and ActionController, 6 | # With the plugin logic. 7 | class Engine < Rails::Engine 8 | config.sorcery = ::Sorcery::Controller::Config 9 | 10 | # TODO: Should this include a modified version of the helper methods? 11 | initializer 'extend Controller with sorcery' do 12 | # FIXME: on_load is needed to fix Rails 6 deprecations, but it breaks 13 | # applications due to undefined method errors. 14 | # ActiveSupport.on_load(:action_controller_api) do 15 | if defined?(ActionController::API) 16 | ActionController::API.send(:include, Sorcery::Controller) 17 | end 18 | 19 | # FIXME: on_load is needed to fix Rails 6 deprecations, but it breaks 20 | # applications due to undefined method errors. 21 | # ActiveSupport.on_load(:action_controller_base) do 22 | if defined?(ActionController::Base) 23 | ActionController::Base.send(:include, Sorcery::Controller) 24 | ActionController::Base.helper_method :current_user 25 | ActionController::Base.helper_method :logged_in? 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/sorcery/model/config.rb: -------------------------------------------------------------------------------- 1 | # Each class which calls 'activate_sorcery!' receives an instance of this class. 2 | # Every submodule which gets loaded may add accessors to this class so that all 3 | # options will be configured from a single place. 4 | module Sorcery 5 | module Model 6 | class Config 7 | # change *virtual* password attribute, the one which is used until an encrypted one is generated. 8 | attr_accessor :password_attribute_name 9 | # change default email attribute. 10 | attr_accessor :email_attribute_name 11 | # downcase the username before trying to authenticate, default is false 12 | attr_accessor :downcase_username_before_authenticating 13 | # change default crypted_password attribute. 14 | attr_accessor :crypted_password_attribute_name 15 | # application-specific secret token that is joined with the password and its salt. 16 | # Currently available with BCrypt (default crypt provider) only. 17 | attr_accessor :pepper 18 | # what pattern to use to join the password with the salt 19 | # APPLICABLE TO MD5, SHA1, SHA256, SHA512. Other crypt providers (incl. BCrypt) ignore this parameter. 20 | attr_accessor :salt_join_token 21 | # change default salt attribute. 22 | attr_accessor :salt_attribute_name 23 | # how many times to apply encryption to the password. 24 | attr_accessor :stretches 25 | # encryption key used to encrypt reversible encryptions such as AES256. 26 | attr_accessor :encryption_key 27 | # make this configuration inheritable for subclasses. Useful for ActiveRecord's STI. 28 | attr_accessor :subclasses_inherit_config 29 | # configured in config/application.rb 30 | attr_accessor :submodules 31 | # an array of method names to call before authentication completes. used internally. 32 | attr_accessor :before_authenticate 33 | # method to send email related 34 | # options: `:deliver_later`, `:deliver_now` 35 | # Default: :deliver_now 36 | # method to send email related 37 | attr_accessor :email_delivery_method 38 | # an array of method names to call after configuration by user. used internally. 39 | attr_accessor :after_config 40 | # Set token randomness 41 | attr_accessor :token_randomness 42 | 43 | # change default username attribute, for example, to use :email as the login. See 'username_attribute_names=' below. 44 | attr_reader :username_attribute_names 45 | # change default encryption_provider. 46 | attr_reader :encryption_provider 47 | # use an external encryption class. 48 | attr_reader :custom_encryption_provider 49 | # encryption algorithm name. See 'encryption_algorithm=' below for available options. 50 | attr_reader :encryption_algorithm 51 | 52 | def initialize 53 | @defaults = { 54 | :@submodules => [], 55 | :@username_attribute_names => [:email], 56 | :@password_attribute_name => :password, 57 | :@downcase_username_before_authenticating => false, 58 | :@email_attribute_name => :email, 59 | :@crypted_password_attribute_name => :crypted_password, 60 | :@encryption_algorithm => :bcrypt, 61 | :@encryption_provider => CryptoProviders::BCrypt, 62 | :@custom_encryption_provider => nil, 63 | :@encryption_key => nil, 64 | :@pepper => '', 65 | :@salt_join_token => '', 66 | :@salt_attribute_name => :salt, 67 | :@stretches => nil, 68 | :@subclasses_inherit_config => false, 69 | :@before_authenticate => [], 70 | :@after_config => [], 71 | :@email_delivery_method => :deliver_now, 72 | :@token_randomness => 15 73 | } 74 | reset! 75 | end 76 | 77 | # Resets all configuration options to their default values. 78 | def reset! 79 | @defaults.each do |k, v| 80 | instance_variable_set(k, v) 81 | end 82 | end 83 | 84 | def username_attribute_names=(fields) 85 | @username_attribute_names = fields.is_a?(Array) ? fields : [fields] 86 | end 87 | 88 | def custom_encryption_provider=(provider) 89 | @custom_encryption_provider = @encryption_provider = provider 90 | end 91 | 92 | def encryption_algorithm=(algo) 93 | @encryption_algorithm = algo 94 | @encryption_provider = case @encryption_algorithm.to_sym 95 | when :none then nil 96 | when :md5 then CryptoProviders::MD5 97 | when :sha1 then CryptoProviders::SHA1 98 | when :sha256 then CryptoProviders::SHA256 99 | when :sha512 then CryptoProviders::SHA512 100 | when :aes256 then CryptoProviders::AES256 101 | when :bcrypt then CryptoProviders::BCrypt 102 | when :custom then @custom_encryption_provider 103 | else raise ArgumentError, "Encryption algorithm supplied, #{algo}, is invalid" 104 | end 105 | end 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /lib/sorcery/model/submodules/activity_logging.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Model 3 | module Submodules 4 | # This submodule keeps track of events such as login, logout, and last activity time, per user. 5 | # It helps in estimating which users are active now in the site. 6 | # This cannot be determined absolutely because a user might be reading a page without clicking anything 7 | # for a while. 8 | # This is the model part of the submodule, which provides configuration options. 9 | module ActivityLogging 10 | def self.included(base) 11 | base.extend(ClassMethods) 12 | base.send(:include, InstanceMethods) 13 | 14 | base.sorcery_config.class_eval do 15 | # last login attribute name. 16 | attr_accessor :last_login_at_attribute_name 17 | # last logout attribute name. 18 | attr_accessor :last_logout_at_attribute_name 19 | # last activity attribute name. 20 | attr_accessor :last_activity_at_attribute_name 21 | # last activity login source 22 | attr_accessor :last_login_from_ip_address_name 23 | # how long since last activity is the user defined offline 24 | attr_accessor :activity_timeout 25 | end 26 | 27 | base.sorcery_config.instance_eval do 28 | @defaults.merge!(:@last_login_at_attribute_name => :last_login_at, 29 | :@last_logout_at_attribute_name => :last_logout_at, 30 | :@last_activity_at_attribute_name => :last_activity_at, 31 | :@last_login_from_ip_address_name => :last_login_from_ip_address, 32 | :@activity_timeout => 10 * 60) 33 | reset! 34 | end 35 | 36 | base.sorcery_config.after_config << :define_activity_logging_fields 37 | end 38 | 39 | module InstanceMethods 40 | def set_last_login_at(time) 41 | sorcery_adapter.update_attribute(sorcery_config.last_login_at_attribute_name, time) 42 | end 43 | 44 | def set_last_logout_at(time) 45 | sorcery_adapter.update_attribute(sorcery_config.last_logout_at_attribute_name, time) 46 | end 47 | 48 | def set_last_activity_at(time) 49 | sorcery_adapter.update_attribute(sorcery_config.last_activity_at_attribute_name, time) 50 | end 51 | 52 | def set_last_ip_address(ip_address) 53 | sorcery_adapter.update_attribute(sorcery_config.last_login_from_ip_address_name, ip_address) 54 | end 55 | 56 | # online method shows if user is active (logout action makes user inactive too) 57 | def online? 58 | return false if send(sorcery_config.last_activity_at_attribute_name).nil? 59 | 60 | logged_in? && send(sorcery_config.last_activity_at_attribute_name) > sorcery_config.activity_timeout.seconds.ago 61 | end 62 | 63 | # shows if user is logged in, but it not show if user is online - see online? 64 | def logged_in? 65 | return false if send(sorcery_config.last_login_at_attribute_name).nil? 66 | return true if send(sorcery_config.last_login_at_attribute_name).present? && send(sorcery_config.last_logout_at_attribute_name).nil? 67 | 68 | send(sorcery_config.last_login_at_attribute_name) > send(sorcery_config.last_logout_at_attribute_name) 69 | end 70 | 71 | def logged_out? 72 | !logged_in? 73 | end 74 | end 75 | 76 | module ClassMethods 77 | protected 78 | 79 | def define_activity_logging_fields 80 | sorcery_adapter.define_field sorcery_config.last_login_at_attribute_name, Time 81 | sorcery_adapter.define_field sorcery_config.last_logout_at_attribute_name, Time 82 | sorcery_adapter.define_field sorcery_config.last_activity_at_attribute_name, Time 83 | sorcery_adapter.define_field sorcery_config.last_login_from_ip_address_name, String 84 | end 85 | end 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/sorcery/model/submodules/external.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Model 3 | module Submodules 4 | # This submodule helps you login users from external providers such as Twitter. 5 | # This is the model part which handles finding the user using access tokens. 6 | # For the controller options see Sorcery::Controller::External. 7 | # 8 | # Socery assumes (read: requires) you will create external users in the same table where 9 | # you keep your regular users, 10 | # but that you will have a separate table for keeping their external authentication data, 11 | # and that that separate table has a few rows for each user, facebook and twitter 12 | # for example (a one-to-many relationship). 13 | # 14 | # External users will have a null crypted_password field, since we do not hold their password. 15 | # They will not be sent activation emails on creation. 16 | module External 17 | def self.included(base) 18 | base.sorcery_config.class_eval do 19 | attr_accessor :authentications_class, 20 | :authentications_user_id_attribute_name, 21 | :provider_attribute_name, 22 | :provider_uid_attribute_name 23 | end 24 | 25 | base.sorcery_config.instance_eval do 26 | @defaults.merge!(:@authentications_class => nil, 27 | :@authentications_user_id_attribute_name => :user_id, 28 | :@provider_attribute_name => :provider, 29 | :@provider_uid_attribute_name => :uid) 30 | 31 | reset! 32 | end 33 | 34 | base.send(:include, InstanceMethods) 35 | base.extend(ClassMethods) 36 | end 37 | 38 | module ClassMethods 39 | # takes a provider and uid and finds a user by them. 40 | def load_from_provider(provider, uid) 41 | config = sorcery_config 42 | authentication = config.authentications_class.sorcery_adapter.find_by_oauth_credentials(provider, uid) 43 | # Return user if matching authentication found 44 | sorcery_adapter.find_by_id(authentication.send(config.authentications_user_id_attribute_name)) if authentication 45 | end 46 | 47 | def create_and_validate_from_provider(provider, uid, attrs) 48 | user = new(attrs) 49 | user.send(sorcery_config.authentications_class.name.demodulize.underscore.pluralize).build( 50 | sorcery_config.provider_uid_attribute_name => uid, 51 | sorcery_config.provider_attribute_name => provider 52 | ) 53 | saved = user.sorcery_adapter.save 54 | [user, saved] 55 | end 56 | 57 | def create_from_provider(provider, uid, attrs) 58 | user = new 59 | attrs.each do |k, v| 60 | user.send(:"#{k}=", v) 61 | end 62 | 63 | if block_given? 64 | return false unless yield user 65 | end 66 | 67 | sorcery_adapter.transaction do 68 | user.sorcery_adapter.save(validate: false) 69 | sorcery_config.authentications_class.create!( 70 | sorcery_config.authentications_user_id_attribute_name => user.id, 71 | sorcery_config.provider_attribute_name => provider, 72 | sorcery_config.provider_uid_attribute_name => uid 73 | ) 74 | end 75 | user 76 | end 77 | 78 | # NOTE: Should this build the authentication as well and return [user, auth]? 79 | # Currently, users call this function for the user and call add_provider_to_user after saving 80 | def build_from_provider(attrs) 81 | user = new 82 | attrs.each do |k, v| 83 | user.send(:"#{k}=", v) 84 | end 85 | 86 | if block_given? 87 | return false unless yield user 88 | end 89 | 90 | user 91 | end 92 | end 93 | 94 | module InstanceMethods 95 | def add_provider_to_user(provider, uid) 96 | authentications = sorcery_config.authentications_class.name.demodulize.underscore.pluralize 97 | # first check to see if user has a particular authentication already 98 | if sorcery_adapter.find_authentication_by_oauth_credentials(authentications, provider, uid).nil? 99 | user = send(authentications).build(sorcery_config.provider_uid_attribute_name => uid, 100 | sorcery_config.provider_attribute_name => provider) 101 | user.sorcery_adapter.save(validate: false) 102 | else 103 | user = false 104 | end 105 | 106 | user 107 | end 108 | end 109 | end 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/sorcery/model/submodules/remember_me.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Model 3 | module Submodules 4 | # The Remember Me submodule takes care of setting the user's cookie so that he will 5 | # be automatically logged in to the site on every visit, 6 | # until the cookie expires. 7 | module RememberMe 8 | def self.included(base) 9 | base.sorcery_config.class_eval do 10 | attr_accessor :remember_me_token_attribute_name, # the attribute in the model class. 11 | :remember_me_token_expires_at_attribute_name, # the expires attribute in the model class. 12 | :remember_me_token_persist_globally, # persist a single token globally for all logins/logouts (supporting multiple simultaneous browsers) 13 | :remember_me_for # how long in seconds to remember. 14 | end 15 | 16 | base.sorcery_config.instance_eval do 17 | @defaults.merge!(:@remember_me_token_attribute_name => :remember_me_token, 18 | :@remember_me_token_expires_at_attribute_name => :remember_me_token_expires_at, 19 | :@remember_me_token_persist_globally => false, 20 | :@remember_me_for => 7 * 60 * 60 * 24) 21 | 22 | reset! 23 | end 24 | 25 | base.send(:include, InstanceMethods) 26 | base.sorcery_config.after_config << :define_remember_me_fields 27 | 28 | base.extend(ClassMethods) 29 | end 30 | 31 | module ClassMethods 32 | protected 33 | 34 | def define_remember_me_fields 35 | sorcery_adapter.define_field sorcery_config.remember_me_token_attribute_name, String 36 | sorcery_adapter.define_field sorcery_config.remember_me_token_expires_at_attribute_name, Time 37 | end 38 | end 39 | 40 | module InstanceMethods 41 | # You shouldn't really use this one yourself - it's called by the controller's 'remember_me!' method. 42 | def remember_me! 43 | config = sorcery_config 44 | 45 | update_options = { config.remember_me_token_expires_at_attribute_name => Time.now.in_time_zone + config.remember_me_for } 46 | 47 | unless config.remember_me_token_persist_globally && has_remember_me_token? 48 | update_options[config.remember_me_token_attribute_name] = TemporaryToken.generate_random_token 49 | end 50 | 51 | sorcery_adapter.update_attributes(update_options) 52 | end 53 | 54 | def has_remember_me_token? 55 | send(sorcery_config.remember_me_token_attribute_name).present? 56 | end 57 | 58 | # You shouldn't really use this one yourself - it's called by the controller's 'forget_me!' method. 59 | # We only clear the token value if remember_me_token_persist_globally = true. 60 | def forget_me! 61 | sorcery_config.remember_me_token_persist_globally || force_forget_me! 62 | end 63 | 64 | # You shouldn't really use this one yourself - it's called by the controller's 'force_forget_me!' method. 65 | def force_forget_me! 66 | config = sorcery_config 67 | sorcery_adapter.update_attributes(config.remember_me_token_attribute_name => nil, 68 | config.remember_me_token_expires_at_attribute_name => nil) 69 | end 70 | end 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/sorcery/model/temporary_token.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | 3 | module Sorcery 4 | module Model 5 | # This module encapsulates the logic for temporary token. 6 | # A temporary token is created to identify a user in scenarios 7 | # such as reseting password and activating the user by email. 8 | module TemporaryToken 9 | def self.included(base) 10 | # FIXME: This may not be the ideal way of passing sorcery_config to generate_random_token. 11 | @sorcery_config = base.sorcery_config 12 | base.extend(ClassMethods) 13 | end 14 | 15 | # Random code, used for salt and temporary tokens. 16 | def self.generate_random_token 17 | SecureRandom.urlsafe_base64(@sorcery_config.token_randomness).tr('lIO0', 'sxyz') 18 | end 19 | 20 | module ClassMethods 21 | def load_from_token(token, token_attr_name, token_expiration_date_attr = nil, &block) 22 | return token_response(failure: :invalid_token, &block) if token.blank? 23 | 24 | user = sorcery_adapter.find_by_token(token_attr_name, token) 25 | 26 | return token_response(failure: :user_not_found, &block) unless user 27 | 28 | unless check_expiration_date(user, token_expiration_date_attr) 29 | return token_response(user: user, failure: :token_expired, &block) 30 | end 31 | 32 | token_response(user: user, return_value: user, &block) 33 | end 34 | 35 | protected 36 | 37 | def check_expiration_date(user, token_expiration_date_attr) 38 | return true unless token_expiration_date_attr 39 | 40 | expires_at = user.send(token_expiration_date_attr) 41 | 42 | !expires_at || (Time.now.in_time_zone < expires_at) 43 | end 44 | 45 | def token_response(options = {}) 46 | yield(options[:user], options[:failure]) if block_given? 47 | 48 | options[:return_value] 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/sorcery/protocols/oauth.rb: -------------------------------------------------------------------------------- 1 | require 'oauth' 2 | 3 | module Sorcery 4 | module Protocols 5 | module Oauth 6 | def oauth_version 7 | '1.0' 8 | end 9 | 10 | def get_request_token(token = nil, secret = nil) 11 | return ::OAuth::RequestToken.new(get_consumer, token, secret) if token && secret 12 | 13 | get_consumer.get_request_token(oauth_callback: @callback_url) 14 | end 15 | 16 | def authorize_url(args) 17 | get_request_token( 18 | args[:request_token], 19 | args[:request_token_secret] 20 | ).authorize_url(oauth_callback: @callback_url) 21 | end 22 | 23 | def get_access_token(args) 24 | get_request_token( 25 | args[:request_token], 26 | args[:request_token_secret] 27 | ).get_access_token(oauth_verifier: args[:oauth_verifier]) 28 | end 29 | 30 | protected 31 | 32 | def get_consumer 33 | ::OAuth::Consumer.new(@key, @secret, site: @site) 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/sorcery/protocols/oauth2.rb: -------------------------------------------------------------------------------- 1 | require 'oauth2' 2 | 3 | module Sorcery 4 | module Protocols 5 | module Oauth2 6 | def oauth_version 7 | '2.0' 8 | end 9 | 10 | def authorize_url(options = {}) 11 | client = build_client(options) 12 | client.auth_code.authorize_url( 13 | redirect_uri: @callback_url, 14 | scope: @scope, 15 | display: @display, 16 | state: @state 17 | ) 18 | end 19 | 20 | def get_access_token(args, options = {}) 21 | client = build_client(options) 22 | client.auth_code.get_token( 23 | args[:code], 24 | { 25 | redirect_uri: @callback_url, 26 | parse: options.delete(:parse) 27 | }, 28 | options 29 | ) 30 | end 31 | 32 | def build_client(options = {}) 33 | defaults = { 34 | site: @site, 35 | auth_scheme: :request_body, 36 | ssl: { ca_file: Sorcery::Controller::Config.ca_file } 37 | } 38 | ::OAuth2::Client.new( 39 | @key, 40 | @secret, 41 | defaults.merge!(options) 42 | ) 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/sorcery/providers/auth0.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Providers 3 | # This class adds support for OAuth with Auth0.com 4 | # 5 | # config.auth0.key = 6 | # config.auth0.secret = 7 | # config.auth0.domain = 8 | # ... 9 | # 10 | class Auth0 < Base 11 | include Protocols::Oauth2 12 | 13 | attr_accessor :auth_path, :token_path, :user_info_path, :scope 14 | 15 | def initialize 16 | super 17 | 18 | @auth_path = '/authorize' 19 | @token_path = '/oauth/token' 20 | @user_info_path = '/userinfo' 21 | @scope = 'openid profile email' 22 | end 23 | 24 | def get_user_hash(access_token) 25 | response = access_token.get(user_info_path) 26 | 27 | auth_hash(access_token).tap do |h| 28 | h[:user_info] = JSON.parse(response.body) 29 | h[:uid] = h[:user_info]['sub'] 30 | end 31 | end 32 | 33 | def login_url(_params, _session) 34 | authorize_url(authorize_url: auth_path) 35 | end 36 | 37 | def process_callback(params, _session) 38 | args = {}.tap do |a| 39 | a[:code] = params[:code] if params[:code] 40 | end 41 | 42 | get_access_token(args, token_url: token_path, token_method: :post) 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/sorcery/providers/base.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Providers 3 | class Base 4 | attr_reader :access_token 5 | 6 | attr_accessor :callback_url, :key, :original_callback_url, :secret, 7 | :site, :state, :user_info_mapping 8 | 9 | def has_callback? 10 | true 11 | end 12 | 13 | def initialize 14 | @user_info_mapping = {} 15 | end 16 | 17 | def auth_hash(access_token, hash = {}) 18 | return hash if access_token.nil? 19 | 20 | token_hash = hash.dup 21 | token_hash[:token] = access_token.token if access_token.respond_to?(:token) 22 | token_hash[:refresh_token] = access_token.refresh_token if access_token.respond_to?(:refresh_token) 23 | token_hash[:expires_at] = access_token.expires_at if access_token.respond_to?(:expires_at) 24 | token_hash[:expires_in] = access_token.expires_at if access_token.respond_to?(:expires_in) 25 | token_hash 26 | end 27 | 28 | def self.name 29 | super.gsub(/Sorcery::Providers::/, '').downcase 30 | end 31 | 32 | # Ensure that all descendant classes are loaded before run this 33 | def self.descendants 34 | ObjectSpace.each_object(Class).select { |klass| klass < self } 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/sorcery/providers/battlenet.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Providers 3 | # This class adds support for OAuth with BattleNet 4 | 5 | class Battlenet < Base 6 | include Protocols::Oauth2 7 | 8 | attr_accessor :auth_path, :scope, :token_url, :user_info_path 9 | 10 | def initialize 11 | super 12 | 13 | @scope = 'openid' 14 | @site = 'https://eu.battle.net/' 15 | @auth_path = '/oauth/authorize' 16 | @token_url = '/oauth/token' 17 | @user_info_path = '/oauth/userinfo' 18 | @state = SecureRandom.hex(16) 19 | end 20 | 21 | def get_user_hash(access_token) 22 | response = access_token.get(user_info_path) 23 | body = JSON.parse(response.body) 24 | auth_hash(access_token).tap do |h| 25 | h[:user_info] = body 26 | h[:battletag] = body['battletag'] 27 | h[:uid] = body['id'] 28 | end 29 | end 30 | 31 | # calculates and returns the url to which the user should be redirected, 32 | # to get authenticated at the external provider's site. 33 | def login_url(_params, _session) 34 | authorize_url(authorize_url: auth_path) 35 | end 36 | 37 | # tries to login the user from access token 38 | def process_callback(params, _session) 39 | args = { code: params[:code] } 40 | get_access_token( 41 | args, 42 | token_url: token_url, 43 | client_id: @key, 44 | client_secret: @secret, 45 | grant_type: 'authorization_code', 46 | token_method: :post 47 | ) 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/sorcery/providers/discord.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Providers 3 | # This class adds support for OAuth with discordapp.com 4 | 5 | class Discord < Base 6 | include Protocols::Oauth2 7 | 8 | attr_accessor :auth_path, :scope, :token_url, :user_info_path 9 | 10 | def initialize 11 | super 12 | 13 | @scope = 'identify' 14 | @site = 'https://discordapp.com/' 15 | @auth_path = '/api/oauth2/authorize' 16 | @token_url = '/api/oauth2/token' 17 | @user_info_path = '/api/users/@me' 18 | @state = SecureRandom.hex(16) 19 | end 20 | 21 | def get_user_hash(access_token) 22 | response = access_token.get(user_info_path) 23 | body = JSON.parse(response.body) 24 | auth_hash(access_token).tap do |h| 25 | h[:user_info] = body 26 | h[:uid] = body['id'] 27 | end 28 | end 29 | 30 | # calculates and returns the url to which the user should be redirected, 31 | # to get authenticated at the external provider's site. 32 | def login_url(_params, _session) 33 | authorize_url(authorize_url: auth_path) 34 | end 35 | 36 | # tries to login the user from access token 37 | def process_callback(params, _session) 38 | args = {}.tap do |a| 39 | a[:code] = params[:code] if params[:code] 40 | end 41 | get_access_token( 42 | args, 43 | token_url: token_url, 44 | client_id: @key, 45 | client_secret: @secret, 46 | grant_type: 'authorization_code', 47 | token_method: :post 48 | ) 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/sorcery/providers/facebook.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Providers 3 | # This class adds support for OAuth with facebook.com. 4 | # 5 | # config.facebook.key = 6 | # config.facebook.secret = 7 | # ... 8 | # 9 | class Facebook < Base 10 | include Protocols::Oauth2 11 | 12 | attr_reader :mode, :param_name 13 | attr_accessor :access_permissions, :display, :scope, :token_url, 14 | :user_info_path, :auth_path, :api_version, :parse 15 | 16 | def initialize 17 | super 18 | 19 | @site = 'https://graph.facebook.com' 20 | @auth_site = 'https://www.facebook.com' 21 | @user_info_path = 'me' 22 | @scope = 'email' 23 | @display = 'page' 24 | @token_url = 'oauth/access_token' 25 | @auth_path = 'dialog/oauth' 26 | @mode = :query 27 | @parse = :json 28 | @param_name = 'access_token' 29 | end 30 | 31 | def get_user_hash(access_token) 32 | response = access_token.get(user_info_path) 33 | 34 | auth_hash(access_token).tap do |h| 35 | h[:user_info] = JSON.parse(response.body) 36 | h[:uid] = h[:user_info]['id'] 37 | end 38 | end 39 | 40 | # calculates and returns the url to which the user should be redirected, 41 | # to get authenticated at the external provider's site. 42 | def login_url(_params, _session) 43 | authorize_url 44 | end 45 | 46 | # overrides oauth2#authorize_url to allow customized scope. 47 | def authorize_url 48 | # Fix: replace default oauth2 options, specially to prevent the Faraday gem which 49 | # concatenates with "/", removing the Facebook api version 50 | options = { 51 | site: File.join(@site, api_version.to_s), 52 | authorize_url: File.join(@auth_site, api_version.to_s, auth_path), 53 | token_url: token_url 54 | } 55 | 56 | @scope = access_permissions.present? ? access_permissions.join(',') : scope 57 | super(options) 58 | end 59 | 60 | # tries to login the user from access token 61 | def process_callback(params, _session) 62 | args = {}.tap do |a| 63 | a[:code] = params[:code] if params[:code] 64 | end 65 | 66 | get_access_token(args, token_url: token_url, mode: mode, 67 | param_name: param_name, parse: parse) 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/sorcery/providers/github.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Providers 3 | # This class adds support for OAuth with github.com. 4 | # 5 | # config.github.key = 6 | # config.github.secret = 7 | # ... 8 | # 9 | class Github < Base 10 | include Protocols::Oauth2 11 | 12 | attr_accessor :auth_path, :scope, :token_url, :user_info_path 13 | 14 | def initialize 15 | super 16 | 17 | @scope = nil 18 | @site = 'https://github.com/' 19 | @user_info_path = 'https://api.github.com/user' 20 | @auth_path = '/login/oauth/authorize' 21 | @token_url = '/login/oauth/access_token' 22 | end 23 | 24 | def get_user_hash(access_token) 25 | response = access_token.get(user_info_path) 26 | 27 | auth_hash(access_token).tap do |h| 28 | h[:user_info] = JSON.parse(response.body).tap do |uih| 29 | uih['email'] = primary_email(access_token) if scope =~ /user/ 30 | end 31 | h[:uid] = h[:user_info]['id'] 32 | end 33 | end 34 | 35 | # calculates and returns the url to which the user should be redirected, 36 | # to get authenticated at the external provider's site. 37 | def login_url(_params, _session) 38 | authorize_url(authorize_url: auth_path) 39 | end 40 | 41 | # tries to login the user from access token 42 | def process_callback(params, _session) 43 | args = {}.tap do |a| 44 | a[:code] = params[:code] if params[:code] 45 | end 46 | 47 | get_access_token(args, token_url: token_url, token_method: :post) 48 | end 49 | 50 | def primary_email(access_token) 51 | response = access_token.get(user_info_path + '/emails') 52 | emails = JSON.parse(response.body) 53 | primary = emails.find { |i| i['primary'] } 54 | primary && primary['email'] || emails.first && emails.first['email'] 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/sorcery/providers/google.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Providers 3 | # This class adds support for OAuth with google.com. 4 | # 5 | # config.google.key = 6 | # config.google.secret = 7 | # ... 8 | # 9 | class Google < Base 10 | include Protocols::Oauth2 11 | 12 | attr_accessor :auth_url, :scope, :token_url, :user_info_url 13 | 14 | def initialize 15 | super 16 | 17 | @site = 'https://accounts.google.com' 18 | @auth_url = '/o/oauth2/auth' 19 | @token_url = '/o/oauth2/token' 20 | @user_info_url = 'https://www.googleapis.com/oauth2/v1/userinfo' 21 | @scope = 'https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile' 22 | end 23 | 24 | def get_user_hash(access_token) 25 | response = access_token.get(user_info_url) 26 | 27 | auth_hash(access_token).tap do |h| 28 | h[:user_info] = JSON.parse(response.body) 29 | h[:uid] = h[:user_info]['id'] 30 | end 31 | end 32 | 33 | # calculates and returns the url to which the user should be redirected, 34 | # to get authenticated at the external provider's site. 35 | def login_url(_params, _session) 36 | authorize_url(authorize_url: auth_url) 37 | end 38 | 39 | # tries to login the user from access token 40 | def process_callback(params, _session) 41 | args = {}.tap do |a| 42 | a[:code] = params[:code] if params[:code] 43 | end 44 | 45 | get_access_token(args, token_url: token_url, token_method: :post) 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/sorcery/providers/heroku.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Providers 3 | # This class adds support for OAuth with heroku.com. 4 | 5 | # config.heroku.key = 6 | # config.heroku.secret = 7 | # config.heroku.callback_url = "/oauth/callback?provider=heroku" 8 | # config.heroku.scope = "read" 9 | # config.heroku.user_info_mapping = {:email => "email", :name => "email" } 10 | 11 | # NOTE: 12 | # The full path must be set for OAuth Callback URL when configuring the API Client Information on Heroku. 13 | 14 | class Heroku < Base 15 | include Protocols::Oauth2 16 | 17 | attr_accessor :auth_path, :scope, :token_url, :user_info_path 18 | 19 | def initialize 20 | super 21 | 22 | @scope = nil 23 | @site = 'https://id.heroku.com' 24 | @user_info_path = 'https://api.heroku.com/account' 25 | @auth_path = '/oauth/authorize' 26 | @token_url = '/oauth/token' 27 | @user_info_path = '/account' 28 | @state = SecureRandom.hex(16) 29 | end 30 | 31 | def get_user_hash(access_token) 32 | response = access_token.get(user_info_path) 33 | body = JSON.parse(response.body) 34 | auth_hash(access_token).tap do |h| 35 | h[:user_info] = body 36 | h[:uid] = body['id'].to_s 37 | h[:email] = body['email'].to_s 38 | end 39 | end 40 | 41 | def login_url(_params, _session) 42 | authorize_url(authorize_url: auth_path) 43 | end 44 | 45 | # tries to login the user from access token 46 | def process_callback(params, _session) 47 | raise 'Invalid state. Potential Cross Site Forgery' if params[:state] != state 48 | 49 | args = {}.tap do |a| 50 | a[:code] = params[:code] if params[:code] 51 | end 52 | get_access_token(args, token_url: token_url, token_method: :post) 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/sorcery/providers/instagram.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Providers 3 | # This class adds support for OAuth with Instagram.com. 4 | class Instagram < Base 5 | include Protocols::Oauth2 6 | 7 | attr_accessor :access_permissions, :token_url, 8 | :authorization_path, :user_info_path, 9 | :scope, :user_info_fields 10 | 11 | def initialize 12 | super 13 | 14 | @site = 'https://api.instagram.com' 15 | @token_url = '/oauth/access_token' 16 | @authorization_path = '/oauth/authorize/' 17 | @user_info_path = '/v1/users/self' 18 | @scope = 'basic' 19 | end 20 | 21 | def self.included(base) 22 | base.extend Sorcery::Providers 23 | end 24 | 25 | # provider implements method to build Oauth client 26 | def login_url(_params, _session) 27 | authorize_url(token_url: @token_url) 28 | end 29 | 30 | # overrides oauth2#authorize_url to allow customized scope. 31 | def authorize_url(opts = {}) 32 | @scope = access_permissions.present? ? access_permissions.join(' ') : scope 33 | super(opts.merge(token_url: @token_url)) 34 | end 35 | 36 | # pass oauth2 param `code` provided by instgrm server 37 | def process_callback(params, _session) 38 | args = {}.tap do |a| 39 | a[:code] = params[:code] if params[:code] 40 | end 41 | get_access_token( 42 | args, 43 | token_url: @token_url, 44 | client_id: @key, 45 | client_secret: @secret 46 | ) 47 | end 48 | 49 | # see `user_info_mapping` in config/initializer, 50 | # given `user_info_mapping` to specify 51 | # {:db_attribute_name => 'instagram_attr_name'} 52 | # so that Sorcery can build AR model from attr names 53 | # 54 | # NOTE: instead of just getting the user info 55 | # from the access_token (which already returns them), 56 | # testing strategy relies on querying user_info_path 57 | def get_user_hash(access_token) 58 | call_api_params = { 59 | access_token: access_token.token, 60 | client_id: access_token[:client_id] 61 | } 62 | response = access_token.get( 63 | "#{user_info_path}?#{call_api_params.to_param}" 64 | ) 65 | 66 | user_attrs = {} 67 | user_attrs[:user_info] = JSON.parse(response.body)['data'] 68 | user_attrs[:uid] = user_attrs[:user_info]['id'] 69 | user_attrs 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/sorcery/providers/jira.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Providers 3 | # This class adds support for OAuth with Jira 4 | # 5 | # config.jira.key = 6 | # config.jira.secret = 7 | # ... 8 | # 9 | class Jira < Base 10 | include Protocols::Oauth 11 | 12 | attr_accessor :access_token_path, :authorize_path, :request_token_path, 13 | :user_info_path, :site, :signature_method, :private_key_file, :callback_url 14 | 15 | def initialize 16 | @configuration = { 17 | authorize_path: '/authorize', 18 | request_token_path: '/request-token', 19 | access_token_path: '/access-token' 20 | } 21 | @user_info_path = '/users/me' 22 | end 23 | 24 | # Override included get_consumer method to provide authorize_path 25 | # read extra configurations 26 | def get_consumer 27 | @configuration = @configuration.merge(site: site, 28 | signature_method: signature_method, 29 | consumer_key: key, 30 | private_key_file: private_key_file) 31 | ::OAuth::Consumer.new(@key, @secret, @configuration) 32 | end 33 | 34 | def get_user_hash(access_token) 35 | response = access_token.get(user_info_path) 36 | 37 | auth_hash(access_token).tap do |h| 38 | h[:user_info] = JSON.parse(response.body)['users'].first 39 | h[:uid] = user_hash[:user_info]['id'].to_s 40 | end 41 | end 42 | 43 | # calculates and returns the url to which the user should be redirected, 44 | # to get authenticated at the external provider's site. 45 | def login_url(_params, session) 46 | req_token = get_request_token 47 | session[:request_token] = req_token.token 48 | session[:request_token_secret] = req_token.secret 49 | 50 | # it was like that -> redirect_to authorize_url({ request_token: req_token.token, request_token_secret: req_token.secret }) 51 | # for some reason Jira does not need these parameters 52 | 53 | get_request_token( 54 | session[:request_token], 55 | session[:request_token_secret] 56 | ).authorize_url 57 | end 58 | 59 | # tries to login the user from access token 60 | def process_callback(params, session) 61 | args = { 62 | oauth_verifier: params[:oauth_verifier], 63 | request_token: session[:request_token], 64 | request_token_secret: session[:request_token_secret] 65 | } 66 | 67 | args[:code] = params[:code] if params[:code] 68 | get_access_token(args) 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/sorcery/providers/line.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Providers 3 | # This class adds support for OAuth with line.com. 4 | # 5 | # config.line.key = 6 | # config.line.secret = 7 | # ... 8 | # 9 | class Line < Base 10 | include Protocols::Oauth2 11 | 12 | attr_accessor :token_url, :user_info_path, :auth_path, :scope, :bot_prompt 13 | 14 | def initialize 15 | super 16 | 17 | @site = 'https://access.line.me' 18 | @user_info_path = 'https://api.line.me/v2/profile' 19 | @token_url = 'https://api.line.me/oauth2/v2.1/token' 20 | @auth_path = 'oauth2/v2.1/authorize' 21 | @scope = 'profile' 22 | end 23 | 24 | def get_user_hash(access_token) 25 | response = access_token.get(user_info_path) 26 | auth_hash(access_token).tap do |h| 27 | h[:user_info] = JSON.parse(response.body) 28 | h[:uid] = h[:user_info]['userId'].to_s 29 | end 30 | end 31 | 32 | # calculates and returns the url to which the user should be redirected, 33 | # to get authenticated at the external provider's site. 34 | def login_url(_params, _session) 35 | @state = SecureRandom.hex(16) 36 | authorize_url(authorize_url: auth_path) 37 | end 38 | 39 | # overrides oauth2#authorize_url to add bot_prompt query. 40 | def authorize_url(options = {}) 41 | options.merge!({ 42 | connection_opts: { params: { bot_prompt: bot_prompt } } 43 | }) if bot_prompt.present? 44 | 45 | super(options) 46 | end 47 | 48 | # tries to login the user from access token 49 | def process_callback(params, _session) 50 | args = {}.tap do |a| 51 | a[:code] = params[:code] if params[:code] 52 | end 53 | 54 | get_access_token( 55 | args, 56 | token_url: token_url, 57 | token_method: :post, 58 | grant_type: 'authorization_code' 59 | ) 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/sorcery/providers/linkedin.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Providers 3 | # This class adds support for OAuth with LinkedIn. 4 | # 5 | # config.linkedin.key = 6 | # config.linkedin.secret = 7 | # ... 8 | # 9 | class Linkedin < Base 10 | include Protocols::Oauth2 11 | 12 | attr_accessor :auth_url, :scope, :token_url, :user_info_url, :email_info_url 13 | 14 | def initialize 15 | super 16 | 17 | @site = 'https://api.linkedin.com' 18 | @auth_url = '/oauth/v2/authorization' 19 | @token_url = '/oauth/v2/accessToken' 20 | @user_info_url = 'https://api.linkedin.com/v2/me' 21 | @email_info_url = 'https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))' 22 | @scope = 'r_liteprofile r_emailaddress' 23 | @state = SecureRandom.hex(16) 24 | end 25 | 26 | def get_user_hash(access_token) 27 | user_info = get_user_info(access_token) 28 | 29 | auth_hash(access_token).tap do |h| 30 | h[:user_info] = user_info 31 | h[:uid] = h[:user_info]['id'] 32 | end 33 | end 34 | 35 | # calculates and returns the url to which the user should be redirected, 36 | # to get authenticated at the external provider's site. 37 | def login_url(_params, _session) 38 | authorize_url(authorize_url: auth_url) 39 | end 40 | 41 | # tries to login the user from access token 42 | def process_callback(params, _session) 43 | args = {}.tap do |a| 44 | a[:code] = params[:code] if params[:code] 45 | end 46 | 47 | get_access_token(args, token_url: token_url, token_method: :post) 48 | end 49 | 50 | def get_user_info(access_token) 51 | response = access_token.get(user_info_url) 52 | user_info = JSON.parse(response.body) 53 | 54 | if email_in_scope? 55 | email = fetch_email(access_token) 56 | 57 | return user_info.merge(email) 58 | end 59 | 60 | user_info 61 | end 62 | 63 | def email_in_scope? 64 | scope.include?('r_emailaddress') 65 | end 66 | 67 | def fetch_email(access_token) 68 | email_response = access_token.get(email_info_url) 69 | email_info = JSON.parse(email_response.body)['elements'].first 70 | 71 | email_info['handle~'] 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/sorcery/providers/liveid.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Providers 3 | # This class adds support for OAuth with microsoft liveid. 4 | # 5 | # config.liveid.key = 6 | # config.liveid.secret = 7 | # ... 8 | # 9 | class Liveid < Base 10 | include Protocols::Oauth2 11 | 12 | attr_accessor :auth_url, :token_path, :user_info_url, :scope 13 | 14 | def initialize 15 | super 16 | 17 | @site = 'https://oauth.live.com/' 18 | @auth_url = '/authorize' 19 | @token_path = '/token' 20 | @user_info_url = 'https://apis.live.net/v5.0/me' 21 | @scope = 'wl.basic wl.emails wl.offline_access' 22 | end 23 | 24 | def get_user_hash(access_token) 25 | access_token.token_param = 'access_token' 26 | response = access_token.get(user_info_url) 27 | 28 | auth_hash(access_token).tap do |h| 29 | h[:user_info] = JSON.parse(response.body) 30 | h[:uid] = h[:user_info]['id'] 31 | end 32 | end 33 | 34 | # calculates and returns the url to which the user should be redirected, 35 | # to get authenticated at the external provider's site. 36 | def login_url(_params, _session) 37 | authorize_url(authorize_url: auth_url) 38 | end 39 | 40 | # tries to login the user from access token 41 | def process_callback(params, _session) 42 | args = {}.tap do |a| 43 | a[:code] = params[:code] if params[:code] 44 | end 45 | 46 | get_access_token(args, access_token_path: token_path, access_token_method: :post) 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/sorcery/providers/microsoft.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Providers 3 | # This class adds support for OAuth with Microsoft Graph. 4 | # 5 | # config.microsoft.key = 6 | # config.microsoft.secret = 7 | # ... 8 | # 9 | class Microsoft < Base 10 | include Protocols::Oauth2 11 | 12 | attr_accessor :auth_url, :scope, :token_url, :user_info_url 13 | 14 | def initialize 15 | super 16 | 17 | @site = 'https://login.microsoftonline.com' 18 | @auth_url = '/common/oauth2/v2.0/authorize' 19 | @token_url = '/common/oauth2/v2.0/token' 20 | @user_info_url = 'https://graph.microsoft.com/v1.0/me' 21 | @scope = 'openid email https://graph.microsoft.com/User.Read' 22 | @state = SecureRandom.hex(16) 23 | end 24 | 25 | def authorize_url(options = {}) 26 | oauth_params = { 27 | client_id: @key, 28 | response_type: 'code' 29 | } 30 | options.merge!(oauth_params) 31 | super(options) 32 | end 33 | 34 | def get_user_hash(access_token) 35 | response = access_token.get(user_info_url) 36 | 37 | auth_hash(access_token).tap do |h| 38 | h[:user_info] = JSON.parse(response.body) 39 | h[:uid] = h[:user_info]['id'] 40 | end 41 | end 42 | 43 | # calculates and returns the url to which the user should be redirected, 44 | # to get authenticated at the external provider's site. 45 | def login_url(_params, _session) 46 | authorize_url(authorize_url: auth_url) 47 | end 48 | 49 | # tries to login the user from access token 50 | def process_callback(params, _session) 51 | args = {}.tap do |a| 52 | a[:code] = params[:code] if params[:code] 53 | end 54 | 55 | get_access_token(args, token_url: token_url, token_method: :post) 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/sorcery/providers/paypal.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Providers 3 | # This class adds support for OAuth with paypal.com. 4 | # 5 | # config.paypal.key = 6 | # config.paypal.secret = 7 | # ... 8 | # 9 | class Paypal < Base 10 | include Protocols::Oauth2 11 | 12 | attr_accessor :auth_url, :scope, :token_url, :user_info_url 13 | 14 | def initialize 15 | super 16 | 17 | @scope = 'openid email' 18 | @site = 'https://api.paypal.com' 19 | @auth_url = 'https://www.paypal.com/webapps/auth/protocol/openidconnect/v1/authorize' 20 | @user_info_url = 'https://api.paypal.com/v1/identity/openidconnect/userinfo?schema=openid' 21 | @token_url = 'https://api.paypal.com/v1/identity/openidconnect/tokenservice' 22 | @state = SecureRandom.hex(16) 23 | end 24 | 25 | def get_user_hash(access_token) 26 | response = access_token.get(user_info_url) 27 | body = JSON.parse(response.body) 28 | auth_hash(access_token).tap do |h| 29 | h[:user_info] = body 30 | h[:uid] = body['user_id'] 31 | h[:email] = body['email'] 32 | end 33 | end 34 | 35 | def get_access_token(args, options = {}) 36 | client = build_client(options) 37 | client.auth_code.get_token( 38 | args[:code], 39 | { 40 | redirect_uri: @callback_url, 41 | parse: options.delete(:parse) 42 | }, 43 | options 44 | ) 45 | end 46 | 47 | def login_url(_params, _session) 48 | authorize_url(authorize_url: auth_url) 49 | end 50 | 51 | def process_callback(params, _session) 52 | args = {}.tap do |a| 53 | a[:code] = params[:code] if params[:code] 54 | end 55 | 56 | get_access_token(args, token_url: token_url, token_method: :post) 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/sorcery/providers/salesforce.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Providers 3 | # This class adds support for OAuth with salesforce.com. 4 | # 5 | # config.salesforce.key = 6 | # config.salesforce.secret = 7 | # ... 8 | # 9 | class Salesforce < Base 10 | include Protocols::Oauth2 11 | 12 | attr_accessor :auth_url, :token_url, :scope 13 | 14 | def initialize 15 | super 16 | 17 | @site = 'https://login.salesforce.com' 18 | @auth_url = '/services/oauth2/authorize' 19 | @token_url = '/services/oauth2/token' 20 | end 21 | 22 | def get_user_hash(access_token) 23 | user_info_url = access_token.params['id'] 24 | response = access_token.get(user_info_url) 25 | 26 | auth_hash(access_token).tap do |h| 27 | h[:user_info] = JSON.parse(response.body) 28 | h[:uid] = h[:user_info]['user_id'] 29 | end 30 | end 31 | 32 | # calculates and returns the url to which the user should be redirected, 33 | # to get authenticated at the external provider's site. 34 | def login_url(_params, _session) 35 | authorize_url(authorize_url: auth_url) 36 | end 37 | 38 | # tries to login the user from access token 39 | def process_callback(params, _session) 40 | args = {}.tap do |a| 41 | a[:code] = params[:code] if params[:code] 42 | end 43 | 44 | get_access_token(args, token_url: token_url, token_method: :post) 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/sorcery/providers/slack.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Providers 3 | # This class adds support for OAuth with slack.com. 4 | 5 | class Slack < Base 6 | include Protocols::Oauth2 7 | 8 | attr_accessor :auth_path, :scope, :token_url, :user_info_path 9 | 10 | def initialize 11 | super 12 | 13 | @scope = 'identity.basic, identity.email' 14 | @site = 'https://slack.com/' 15 | @user_info_path = 'https://slack.com/api/users.identity' 16 | @auth_path = '/oauth/authorize' 17 | @token_url = '/api/oauth.access' 18 | end 19 | 20 | def get_user_hash(access_token) 21 | response = access_token.get(user_info_path) 22 | auth_hash(access_token).tap do |h| 23 | h[:user_info] = JSON.parse(response.body) 24 | h[:user_info]['email'] = h[:user_info]['user']['email'] 25 | h[:uid] = h[:user_info]['user']['id'] 26 | end 27 | end 28 | 29 | # calculates and returns the url to which the user should be redirected, 30 | # to get authenticated at the external provider's site. 31 | def login_url(_params, _session) 32 | authorize_url(authorize_url: auth_path) 33 | end 34 | 35 | # tries to login the user from access token 36 | def process_callback(params, _session) 37 | args = {}.tap do |a| 38 | a[:code] = params[:code] if params[:code] 39 | end 40 | 41 | get_access_token(args, token_url: token_url, token_method: :post) 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/sorcery/providers/twitter.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Providers 3 | # This class adds support for OAuth with Twitter.com. 4 | # 5 | # config.twitter.key = 6 | # config.twitter.secret = 7 | # ... 8 | # 9 | class Twitter < Base 10 | include Protocols::Oauth 11 | 12 | attr_accessor :state, :user_info_path 13 | 14 | def initialize 15 | super 16 | 17 | @site = 'https://api.twitter.com' 18 | @user_info_path = '/1.1/account/verify_credentials.json' 19 | end 20 | 21 | # Override included get_consumer method to provide authorize_path 22 | def get_consumer 23 | ::OAuth::Consumer.new(@key, secret, site: site, authorize_path: '/oauth/authenticate') 24 | end 25 | 26 | def get_user_hash(access_token) 27 | response = access_token.get(user_info_path) 28 | 29 | auth_hash(access_token).tap do |h| 30 | h[:user_info] = JSON.parse(response.body) 31 | h[:uid] = h[:user_info]['id'].to_s 32 | end 33 | end 34 | 35 | # calculates and returns the url to which the user should be redirected, 36 | # to get authenticated at the external provider's site. 37 | def login_url(_params, session) 38 | req_token = get_request_token 39 | session[:request_token] = req_token.token 40 | session[:request_token_secret] = req_token.secret 41 | authorize_url(request_token: req_token.token, request_token_secret: req_token.secret) 42 | end 43 | 44 | # tries to login the user from access token 45 | def process_callback(params, session) 46 | args = { 47 | oauth_verifier: params[:oauth_verifier], 48 | request_token: session[:request_token], 49 | request_token_secret: session[:request_token_secret] 50 | } 51 | 52 | args[:code] = params[:code] if params[:code] 53 | get_access_token(args) 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/sorcery/providers/vk.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Providers 3 | # This class adds support for OAuth with vk.com. 4 | # 5 | # config.vk.key = 6 | # config.vk.secret = 7 | # ... 8 | # 9 | class Vk < Base 10 | include Protocols::Oauth2 11 | 12 | attr_accessor :auth_path, :token_path, :user_info_url, :scope, :api_version 13 | 14 | def initialize 15 | super 16 | 17 | @site = 'https://oauth.vk.com/' 18 | @user_info_url = 'https://api.vk.com/method/getProfiles' 19 | @auth_path = '/authorize' 20 | @token_path = '/access_token' 21 | @scope = 'email' 22 | end 23 | 24 | def get_user_hash(access_token) 25 | user_hash = auth_hash(access_token) 26 | 27 | params = { 28 | access_token: access_token.token, 29 | uids: access_token.params['user_id'], 30 | fields: user_info_mapping.values.join(','), 31 | scope: scope, 32 | v: api_version.to_s 33 | } 34 | 35 | response = access_token.get(user_info_url, params: params) 36 | if (user_hash[:user_info] = JSON.parse(response.body)) 37 | user_hash[:user_info] = user_hash[:user_info]['response'][0] 38 | user_hash[:user_info]['full_name'] = [user_hash[:user_info]['first_name'], user_hash[:user_info]['last_name']].join(' ') 39 | 40 | user_hash[:uid] = user_hash[:user_info]['id'] 41 | user_hash[:user_info]['email'] = access_token.params['email'] 42 | end 43 | user_hash 44 | end 45 | 46 | # calculates and returns the url to which the user should be redirected, 47 | # to get authenticated at the external provider's site. 48 | def login_url(_params, _session) 49 | authorize_url(authorize_url: auth_path) 50 | end 51 | 52 | # tries to login the user from access token 53 | def process_callback(params, _session) 54 | args = {}.tap do |a| 55 | a[:code] = params[:code] if params[:code] 56 | end 57 | 58 | get_access_token(args, token_url: token_path, token_method: :post) 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/sorcery/providers/wechat.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Providers 3 | # This class adds support for OAuth with open.wx.qq.com. 4 | # 5 | # config.wechat.key = 6 | # config.wechat.secret = 7 | # ... 8 | # 9 | class Wechat < Base 10 | include Protocols::Oauth2 11 | 12 | attr_reader :mode, :param_name, :parse 13 | attr_accessor :auth_url, :scope, :token_url, :user_info_path 14 | 15 | def initialize 16 | super 17 | 18 | @scope = 'snsapi_login' 19 | @auth_url = 'https://open.weixin.qq.com/connect/qrconnect' 20 | @user_info_path = 'https://api.weixin.qq.com/sns/userinfo' 21 | @token_url = 'https://api.weixin.qq.com/sns/oauth2/access_token' 22 | @state = SecureRandom.hex(16) 23 | @mode = :body 24 | @parse = :json 25 | @param_name = 'access_token' 26 | end 27 | 28 | def authorize_url(options = {}) 29 | oauth_params = { 30 | appid: @key, 31 | redirect_uri: @callback_url, 32 | response_type: 'code', 33 | scope: scope, 34 | state: @state 35 | } 36 | "#{options[:authorize_url]}?#{oauth_params.to_query}#wechat_redirect" 37 | end 38 | 39 | def get_user_hash(access_token) 40 | response = access_token.get( 41 | user_info_path, 42 | params: { 43 | access_token: access_token.token, 44 | openid: access_token.params['openid'] 45 | } 46 | ) 47 | 48 | {}.tap do |h| 49 | h[:user_info] = JSON.parse(response.body) 50 | h[:uid] = h[:user_info]['unionid'] 51 | end 52 | end 53 | 54 | def get_access_token(args, options = {}) 55 | client = build_client(options) 56 | client.auth_code.get_token( 57 | args[:code], 58 | { appid: @key, secret: @secret, parse: parse }, 59 | options 60 | ) 61 | end 62 | 63 | def login_url(_params, _session) 64 | authorize_url authorize_url: auth_url 65 | end 66 | 67 | def process_callback(params, _session) 68 | args = {}.tap do |a| 69 | a[:code] = params[:code] if params[:code] 70 | end 71 | 72 | get_access_token( 73 | args, 74 | token_url: token_url, 75 | mode: mode, 76 | param_name: param_name 77 | ) 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/sorcery/providers/xing.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Providers 3 | # This class adds support for OAuth with xing.com. 4 | # 5 | # config.xing.key = 6 | # config.xing.secret = 7 | # ... 8 | # 9 | class Xing < Base 10 | include Protocols::Oauth 11 | 12 | attr_accessor :access_token_path, :authorize_path, :request_token_path, 13 | :user_info_path 14 | 15 | def initialize 16 | @configuration = { 17 | site: 'https://api.xing.com/v1', 18 | authorize_path: '/authorize', 19 | request_token_path: '/request_token', 20 | access_token_path: '/access_token' 21 | } 22 | @user_info_path = '/users/me' 23 | end 24 | 25 | # Override included get_consumer method to provide authorize_path 26 | def get_consumer 27 | ::OAuth::Consumer.new(@key, @secret, @configuration) 28 | end 29 | 30 | def get_user_hash(access_token) 31 | response = access_token.get(user_info_path) 32 | 33 | auth_hash(access_token).tap do |h| 34 | h[:user_info] = JSON.parse(response.body)['users'].first 35 | h[:uid] = h[:user_info]['id'].to_s 36 | end 37 | end 38 | 39 | # calculates and returns the url to which the user should be redirected, 40 | # to get authenticated at the external provider's site. 41 | def login_url(_params, session) 42 | req_token = get_request_token 43 | session[:request_token] = req_token.token 44 | session[:request_token_secret] = req_token.secret 45 | authorize_url(request_token: req_token.token, request_token_secret: req_token.secret) 46 | end 47 | 48 | # tries to login the user from access token 49 | def process_callback(params, session) 50 | args = { 51 | oauth_verifier: params[:oauth_verifier], 52 | request_token: session[:request_token], 53 | request_token_secret: session[:request_token_secret] 54 | } 55 | 56 | args[:code] = params[:code] if params[:code] 57 | get_access_token(args) 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/sorcery/test_helpers/internal.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module TestHelpers 3 | # Internal TestHelpers are used to test the gem, internally, and should not be used to test apps *using* sorcery. 4 | # This file will be included in the spec_helper file. 5 | module Internal 6 | def self.included(_base) 7 | # reducing default cost for specs speed 8 | CryptoProviders::BCrypt.class_eval do 9 | class << self 10 | def cost 11 | 1 12 | end 13 | end 14 | end 15 | end 16 | 17 | # a patch to fix a bug in testing that happens when you 'destroy' a session twice. 18 | # After the first destroy, the session is an ordinary hash, and then when destroy 19 | # is called again there's an exception. 20 | class ::Hash # rubocop:disable Style/ClassAndModuleChildren 21 | def destroy 22 | clear 23 | end 24 | end 25 | 26 | def build_new_user(attributes_hash = nil) 27 | user_attributes_hash = attributes_hash || { username: 'gizmo', email: 'bla@bla.com', password: 'secret' } 28 | @user = User.new(user_attributes_hash) 29 | end 30 | 31 | def create_new_user(attributes_hash = nil) 32 | @user = build_new_user(attributes_hash) 33 | @user.sorcery_adapter.save(raise_on_failure: true) 34 | @user 35 | end 36 | 37 | def create_new_external_user(provider, attributes_hash = nil) 38 | user_attributes_hash = attributes_hash || { username: 'gizmo' } 39 | @user = User.new(user_attributes_hash) 40 | @user.sorcery_adapter.save(raise_on_failure: true) 41 | @user.authentications.create!(provider: provider, uid: 123) 42 | @user 43 | end 44 | 45 | def custom_create_new_external_user(provider, authentication_class, attributes_hash = nil) 46 | authentication_association = authentication_class.name.underscore.pluralize 47 | 48 | user_attributes_hash = attributes_hash || { username: 'gizmo' } 49 | @user = User.new(user_attributes_hash) 50 | @user.sorcery_adapter.save(raise_on_failure: true) 51 | @user.send(authentication_association).create!(provider: provider, uid: 123) 52 | @user 53 | end 54 | 55 | def sorcery_model_property_set(property, *values) 56 | User.class_eval do 57 | sorcery_config.send(:"#{property}=", *values) 58 | end 59 | end 60 | 61 | def update_model(&block) 62 | User.class_exec(&block) 63 | end 64 | 65 | private 66 | 67 | # reload user class between specs 68 | # so it will be possible to test the different submodules in isolation 69 | def reload_user_class 70 | User && Object.send(:remove_const, 'User') 71 | load 'user.rb' 72 | 73 | return unless User.respond_to?(:reset_column_information) 74 | 75 | User.reset_column_information 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/sorcery/test_helpers/internal/rails.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module TestHelpers 3 | module Internal 4 | module Rails 5 | include ::Sorcery::TestHelpers::Rails::Controller 6 | 7 | SUBMODULES_AUTO_ADDED_CONTROLLER_FILTERS = %i[ 8 | register_last_activity_time_to_db 9 | deny_banned_user 10 | validate_session 11 | ].freeze 12 | 13 | def sorcery_reload!(submodules = [], options = {}) 14 | reload_user_class 15 | 16 | # return to no-module configuration 17 | ::Sorcery::Controller::Config.init! 18 | ::Sorcery::Controller::Config.reset! 19 | 20 | # remove all plugin before_actions so they won't fail other tests. 21 | # I don't like this way, but I didn't find another. 22 | chain = SorceryController._process_action_callbacks.send :chain 23 | chain.delete_if { |c| SUBMODULES_AUTO_ADDED_CONTROLLER_FILTERS.include?(c.filter) } 24 | 25 | # configure 26 | ::Sorcery::Controller::Config.submodules = submodules 27 | ::Sorcery::Controller::Config.user_class = nil 28 | ActionController::Base.send(:include, ::Sorcery::Controller) 29 | ::Sorcery::Controller::Config.user_class = 'User' 30 | 31 | ::Sorcery::Controller::Config.user_config do |user| 32 | options.each do |property, value| 33 | user.send(:"#{property}=", value) 34 | end 35 | end 36 | User.authenticates_with_sorcery! 37 | return unless defined?(DataMapper) && User.ancestors.include?(DataMapper::Resource) 38 | 39 | DataMapper.auto_migrate! 40 | User.finalize 41 | Authentication.finalize 42 | end 43 | 44 | def sorcery_controller_property_set(property, value) 45 | ::Sorcery::Controller::Config.send(:"#{property}=", value) 46 | end 47 | 48 | def sorcery_controller_external_property_set(provider, property, value) 49 | ::Sorcery::Controller::Config.send(provider).send(:"#{property}=", value) 50 | end 51 | 52 | # This helper is used to fake multiple users signing in in tests. 53 | # It does so by clearing @current_user, thus allowing a new user to login, 54 | # all this without calling the :logout action explicitly. 55 | # A dirty dirty hack. 56 | def clear_user_without_logout 57 | subject.instance_variable_set(:@current_user, nil) 58 | end 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/sorcery/test_helpers/rails/controller.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module TestHelpers 3 | module Rails 4 | module Controller 5 | def login_user(user = nil, _test_context = {}) 6 | user ||= @user 7 | @controller.send(:auto_login, user) 8 | @controller.send(:after_login!, user, [user.send(user.sorcery_config.username_attribute_names.first), 'secret']) 9 | end 10 | 11 | def logout_user 12 | @controller.send(:logout) 13 | end 14 | 15 | def logged_in? 16 | @controller.send(:logged_in?) 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/sorcery/test_helpers/rails/integration.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module TestHelpers 3 | module Rails 4 | module Integration 5 | # Accepts arguments for user to login, route to use and HTTP method 6 | # Defaults - @user, 'sessions_url' and POST 7 | def login_user(user = nil, route = nil, http_method = :post) 8 | user ||= @user 9 | route ||= sessions_url 10 | 11 | username_attr = user.sorcery_config.username_attribute_names.first 12 | username = user.send(username_attr) 13 | page.driver.send(http_method, route, :"#{username_attr}" => username, :password => 'secret') 14 | end 15 | 16 | # Accepts route and HTTP method arguments 17 | # Default - 'logout_url' and GET 18 | def logout_user(route = nil, http_method = :get) 19 | route ||= logout_url 20 | page.driver.send(http_method, route) 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/sorcery/test_helpers/rails/request.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module TestHelpers 3 | module Rails 4 | module Request 5 | # Accepts arguments for user to login, the password, route to use and HTTP method 6 | # Defaults - @user, 'secret', 'user_sessions_url' and http_method: POST 7 | def login_user(user = nil, password = 'secret', route = nil, http_method = :post) 8 | user ||= @user 9 | route ||= user_sessions_url 10 | 11 | username_attr = user.sorcery_config.username_attribute_names.first 12 | username = user.send(username_attr) 13 | password_attr = user.sorcery_config.password_attribute_name 14 | 15 | send(http_method, route, params: { "#{username_attr}": username, "#{password_attr}": password }) 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/sorcery/version.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | VERSION = '0.17.0'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /sorcery.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('lib', __dir__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'sorcery/version' 4 | 5 | # rubocop:disable Metrics/BlockLength 6 | Gem::Specification.new do |s| 7 | s.name = 'sorcery' 8 | s.version = Sorcery::VERSION 9 | s.authors = [ 10 | 'Noam Ben Ari', 11 | 'Kir Shatrov', 12 | 'Grzegorz Witek', 13 | 'Chase Gilliam', 14 | 'Josh Buker' 15 | ] 16 | s.email = [ 17 | 'crypto@joshbuker.com' 18 | ] 19 | 20 | # TODO: Cleanup formatting. 21 | # rubocop:disable Layout/LineLength 22 | s.description = 'Provides common authentication needs such as signing in/out, activating by email and resetting password.' 23 | s.summary = 'Magical authentication for Rails applications' 24 | s.homepage = 'https://github.com/Sorcery/sorcery' 25 | s.metadata = {"bug_tracker_uri" => "https://github.com/Sorcery/sorcery/issues", "changelog_uri" => "https://github.com/Sorcery/sorcery/blob/master/CHANGELOG.md"} 26 | 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" 27 | 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' 28 | # rubocop:enable Layout/LineLength 29 | 30 | s.files = Dir['lib/**/*'] + ['README.md', 'LICENSE.md', 'CHANGELOG.md'] 31 | s.require_paths = ['lib'] 32 | 33 | s.licenses = ['MIT'] 34 | 35 | s.required_ruby_version = '>= 3.2.0' 36 | 37 | s.add_dependency 'bcrypt', '~> 3.1' 38 | s.add_dependency 'oauth', '>= 0.6' 39 | s.add_dependency 'oauth2', '~> 2.0' 40 | 41 | s.add_development_dependency 'byebug', '~> 11.1.3' 42 | s.add_development_dependency 'rspec-rails' 43 | s.add_development_dependency 'rubocop' 44 | s.add_development_dependency 'test-unit', '~> 3.2.0' 45 | s.add_development_dependency 'timecop' 46 | s.add_development_dependency 'webmock', '~> 3.3.0' 47 | s.add_development_dependency 'yard', '~> 0.9.0', '>= 0.9.12' 48 | end 49 | # rubocop:enable Metrics/BlockLength 50 | -------------------------------------------------------------------------------- /spec/active_record/user_activation_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'rails_app/app/mailers/sorcery_mailer' 4 | require 'shared_examples/user_activation_shared_examples' 5 | 6 | describe User, 'with activation submodule', active_record: true do 7 | before(:all) do 8 | MigrationHelper.migrate("#{Rails.root}/db/migrate/activation") 9 | User.reset_column_information 10 | end 11 | 12 | after(:all) do 13 | MigrationHelper.rollback("#{Rails.root}/db/migrate/activation") 14 | end 15 | 16 | it_behaves_like 'rails_3_activation_model' 17 | end 18 | -------------------------------------------------------------------------------- /spec/active_record/user_activity_logging_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'shared_examples/user_activity_logging_shared_examples' 3 | 4 | describe User, 'with activity logging submodule', active_record: true do 5 | before(:all) do 6 | MigrationHelper.migrate("#{Rails.root}/db/migrate/activity_logging") 7 | User.reset_column_information 8 | end 9 | 10 | after(:all) do 11 | MigrationHelper.rollback("#{Rails.root}/db/migrate/activity_logging") 12 | end 13 | 14 | it_behaves_like 'rails_3_activity_logging_model' 15 | end 16 | -------------------------------------------------------------------------------- /spec/active_record/user_brute_force_protection_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'shared_examples/user_brute_force_protection_shared_examples' 3 | 4 | describe User, 'with brute_force_protection submodule', active_record: true do 5 | before(:all) do 6 | MigrationHelper.migrate("#{Rails.root}/db/migrate/brute_force_protection") 7 | User.reset_column_information 8 | end 9 | 10 | after(:all) do 11 | MigrationHelper.rollback("#{Rails.root}/db/migrate/brute_force_protection") 12 | end 13 | 14 | it_behaves_like 'rails_3_brute_force_protection_model' 15 | end 16 | -------------------------------------------------------------------------------- /spec/active_record/user_magic_login_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'shared_examples/user_magic_login_shared_examples' 3 | 4 | describe User, 'with magic_login submodule', active_record: true do 5 | before(:all) do 6 | MigrationHelper.migrate("#{Rails.root}/db/migrate/magic_login") 7 | User.reset_column_information 8 | end 9 | 10 | after(:all) do 11 | MigrationHelper.rollback("#{Rails.root}/db/migrate/magic_login") 12 | end 13 | 14 | it_behaves_like 'magic_login_model' 15 | end 16 | -------------------------------------------------------------------------------- /spec/active_record/user_oauth_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'shared_examples/user_oauth_shared_examples' 3 | 4 | describe User, 'with oauth submodule', active_record: true do 5 | before(:all) do 6 | MigrationHelper.migrate("#{Rails.root}/db/migrate/external") 7 | User.reset_column_information 8 | end 9 | 10 | after(:all) do 11 | MigrationHelper.rollback("#{Rails.root}/db/migrate/external") 12 | end 13 | 14 | it_behaves_like 'rails_3_oauth_model' 15 | end 16 | -------------------------------------------------------------------------------- /spec/active_record/user_remember_me_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'shared_examples/user_remember_me_shared_examples' 3 | 4 | describe User, 'with remember_me submodule', active_record: true do 5 | before(:all) do 6 | MigrationHelper.migrate("#{Rails.root}/db/migrate/remember_me") 7 | User.reset_column_information 8 | end 9 | 10 | after(:all) do 11 | MigrationHelper.rollback("#{Rails.root}/db/migrate/remember_me") 12 | end 13 | 14 | it_behaves_like 'rails_3_remember_me_model' 15 | end 16 | -------------------------------------------------------------------------------- /spec/active_record/user_reset_password_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'shared_examples/user_reset_password_shared_examples' 3 | 4 | describe User, 'with reset_password submodule', active_record: true do 5 | before(:all) do 6 | MigrationHelper.migrate("#{Rails.root}/db/migrate/reset_password") 7 | User.reset_column_information 8 | end 9 | 10 | after(:all) do 11 | MigrationHelper.rollback("#{Rails.root}/db/migrate/reset_password") 12 | end 13 | 14 | it_behaves_like 'rails_3_reset_password_model' 15 | end 16 | -------------------------------------------------------------------------------- /spec/active_record/user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rails_app/app/mailers/sorcery_mailer' 3 | require 'shared_examples/user_shared_examples' 4 | 5 | describe User, 'with no submodules (core)', active_record: true do 6 | before(:all) do 7 | sorcery_reload! 8 | end 9 | 10 | context 'when app has plugin loaded' do 11 | it 'responds to the plugin activation class method' do 12 | expect(ActiveRecord::Base).to respond_to :authenticates_with_sorcery! 13 | end 14 | 15 | it 'User responds to .authenticates_with_sorcery!' do 16 | expect(User).to respond_to :authenticates_with_sorcery! 17 | end 18 | end 19 | 20 | # ----------------- PLUGIN CONFIGURATION ----------------------- 21 | 22 | it_should_behave_like 'rails_3_core_model' 23 | 24 | describe 'external users' do 25 | it_should_behave_like 'external_user' 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/controllers/controller_activity_logging_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | # require 'shared_examples/controller_activity_logging_shared_examples' 4 | 5 | describe SorceryController, type: :controller do 6 | after(:all) do 7 | sorcery_controller_property_set(:register_login_time, true) 8 | sorcery_controller_property_set(:register_logout_time, true) 9 | sorcery_controller_property_set(:register_last_activity_time, true) 10 | # sorcery_controller_property_set(:last_login_from_ip_address_name, true) 11 | end 12 | 13 | # ----------------- ACTIVITY LOGGING ----------------------- 14 | context 'with activity logging features' do 15 | let(:adapter) { double('sorcery_adapter') } 16 | let(:user) { double('user', id: 42, sorcery_adapter: adapter) } 17 | 18 | before(:all) do 19 | sorcery_reload!([:activity_logging]) 20 | end 21 | 22 | before(:each) do 23 | allow(user).to receive(:username) 24 | allow(user).to receive_message_chain(:sorcery_config, :username_attribute_names, :first) { :username } 25 | allow(User.sorcery_config).to receive(:last_login_at_attribute_name) { :last_login_at } 26 | allow(User.sorcery_config).to receive(:last_login_from_ip_address_name) { :last_login_from_ip_address } 27 | 28 | sorcery_controller_property_set(:register_login_time, false) 29 | sorcery_controller_property_set(:register_last_ip_address, false) 30 | sorcery_controller_property_set(:register_last_activity_time, false) 31 | end 32 | 33 | it 'logs login time on login' do 34 | now = Time.now.in_time_zone 35 | Timecop.freeze(now) 36 | 37 | sorcery_controller_property_set(:register_login_time, true) 38 | expect(user).to receive(:set_last_login_at).with(be_within(0.1).of(now)) 39 | login_user(user) 40 | 41 | Timecop.return 42 | end 43 | 44 | it 'logs logout time on logout' do 45 | login_user(user) 46 | now = Time.now.in_time_zone 47 | Timecop.freeze(now) 48 | expect(user).to receive(:set_last_logout_at).with(be_within(0.1).of(now)) 49 | 50 | logout_user 51 | 52 | Timecop.return 53 | end 54 | 55 | it 'logs last activity time when logged in' do 56 | sorcery_controller_property_set(:register_last_activity_time, true) 57 | 58 | login_user(user) 59 | now = Time.now.in_time_zone 60 | Timecop.freeze(now) 61 | expect(user).to receive(:set_last_activity_at).with(be_within(0.1).of(now)) 62 | 63 | get :some_action 64 | 65 | Timecop.return 66 | end 67 | 68 | it 'logs last IP address when logged in' do 69 | sorcery_controller_property_set(:register_last_ip_address, true) 70 | expect(user).to receive(:set_last_ip_address).with('0.0.0.0') 71 | 72 | login_user(user) 73 | end 74 | 75 | it 'updates nothing but activity fields' do 76 | pending 'Move to model' 77 | original_user_name = User.last.username 78 | login_user(user) 79 | get :some_action_making_a_non_persisted_change_to_the_user 80 | 81 | expect(User.last.username).to eq original_user_name 82 | end 83 | 84 | it 'does not register login time if configured so' do 85 | sorcery_controller_property_set(:register_login_time, false) 86 | 87 | expect(user).to receive(:set_last_login_at).never 88 | login_user(user) 89 | end 90 | 91 | it 'does not register logout time if configured so' do 92 | sorcery_controller_property_set(:register_logout_time, false) 93 | login_user(user) 94 | 95 | expect(user).to receive(:set_last_logout_at).never 96 | logout_user 97 | end 98 | 99 | it 'does not register last activity time if configured so' do 100 | sorcery_controller_property_set(:register_last_activity_time, false) 101 | 102 | expect(user).to receive(:set_last_activity_at).never 103 | login_user(user) 104 | end 105 | 106 | it 'does not register last IP address if configured so' do 107 | sorcery_controller_property_set(:register_last_ip_address, false) 108 | expect(user).to receive(:set_last_ip_address).never 109 | 110 | login_user(user) 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /spec/controllers/controller_brute_force_protection_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SorceryController, type: :controller do 4 | let(:user) { double('user', id: 42, email: 'bla@bla.com') } 5 | 6 | def request_test_login 7 | get :test_login, params: { email: 'bla@bla.com', password: 'blabla' } 8 | end 9 | 10 | # ----------------- SESSION TIMEOUT ----------------------- 11 | describe 'brute force protection features' do 12 | before(:all) do 13 | sorcery_reload!([:brute_force_protection]) 14 | end 15 | 16 | after(:each) do 17 | Sorcery::Controller::Config.reset! 18 | sorcery_controller_property_set(:user_class, User) 19 | Timecop.return 20 | end 21 | 22 | it 'counts login retries' do 23 | allow(User).to receive(:authenticate) { |&block| block.call(nil, :other) } 24 | allow(User.sorcery_adapter).to receive(:find_by_credentials).with(['bla@bla.com', 'blabla']).and_return(user) 25 | 26 | expect(user).to receive(:register_failed_login!).exactly(3).times 27 | 28 | 3.times { request_test_login } 29 | end 30 | 31 | it 'resets the counter on a good login' do 32 | # dirty hack for rails 4 33 | allow(@controller).to receive(:register_last_activity_time_to_db) 34 | 35 | allow(User).to receive(:authenticate) { |&block| block.call(user, nil) } 36 | expect(user).to receive_message_chain(:sorcery_adapter, :update_attribute).with(:failed_logins_count, 0) 37 | 38 | get :test_login, params: { email: 'bla@bla.com', password: 'secret' } 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/controllers/controller_http_basic_auth_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SorceryController, type: :controller do 4 | let(:user) { double('user', id: 42, email: 'bla@bla.com') } 5 | 6 | describe 'with http basic auth features' do 7 | before(:all) do 8 | sorcery_reload!([:http_basic_auth]) 9 | 10 | sorcery_controller_property_set(:controller_to_realm_map, 'sorcery' => 'sorcery') 11 | end 12 | 13 | after(:each) do 14 | logout_user 15 | end 16 | 17 | it 'requests basic authentication when before_action is used' do 18 | get :test_http_basic_auth 19 | 20 | expect(response.status).to eq 401 21 | end 22 | 23 | it 'authenticates from http basic if credentials are sent' do 24 | # dirty hack for rails 4 25 | allow(subject).to receive(:register_last_activity_time_to_db) 26 | 27 | @request.env['HTTP_AUTHORIZATION'] = "Basic #{Base64.encode64("#{user.email}:secret")}" 28 | expect(User).to receive('authenticate').with('bla@bla.com', 'secret').and_return(user) 29 | get :test_http_basic_auth, params: {}, session: { http_authentication_used: true } 30 | 31 | expect(response).to be_successful 32 | end 33 | 34 | it 'fails authentication if credentials are wrong' do 35 | @request.env['HTTP_AUTHORIZATION'] = "Basic #{Base64.encode64("#{user.email}:wrong!")}" 36 | expect(User).to receive('authenticate').with('bla@bla.com', 'wrong!').and_return(nil) 37 | get :test_http_basic_auth, params: {}, session: { http_authentication_used: true } 38 | 39 | expect(response).to redirect_to root_url 40 | end 41 | 42 | it "allows configuration option 'controller_to_realm_map'" do 43 | sorcery_controller_property_set(:controller_to_realm_map, '1' => '2') 44 | 45 | expect(Sorcery::Controller::Config.controller_to_realm_map).to eq('1' => '2') 46 | end 47 | 48 | it 'displays the correct realm name configured for the controller' do 49 | sorcery_controller_property_set(:controller_to_realm_map, 'sorcery' => 'Salad') 50 | get :test_http_basic_auth 51 | 52 | expect(response.headers['WWW-Authenticate']).to eq 'Basic realm="Salad"' 53 | end 54 | 55 | it "signs in the user's session on successful login" do 56 | # dirty hack for rails 4 57 | allow(controller).to receive(:register_last_activity_time_to_db) 58 | 59 | @request.env['HTTP_AUTHORIZATION'] = "Basic #{Base64.encode64("#{user.email}:secret")}" 60 | expect(User).to receive('authenticate').with('bla@bla.com', 'secret').and_return(user) 61 | 62 | get :test_http_basic_auth, params: {}, session: { http_authentication_used: true } 63 | 64 | expect(session[:user_id]).to eq '42' 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/controllers/controller_remember_me_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SorceryController, type: :controller do 4 | let!(:user) { double('user', id: 42) } 5 | 6 | # ----------------- REMEMBER ME ----------------------- 7 | context 'with remember me features' do 8 | before(:all) do 9 | if SORCERY_ORM == :active_record 10 | MigrationHelper.migrate("#{Rails.root}/db/migrate/remember_me") 11 | User.reset_column_information 12 | end 13 | 14 | sorcery_reload!([:remember_me]) 15 | end 16 | 17 | after(:all) do 18 | if SORCERY_ORM == :active_record 19 | MigrationHelper.rollback("#{Rails.root}/db/migrate/remember_me") 20 | end 21 | end 22 | 23 | before(:each) do 24 | allow(user).to receive(:remember_me_token) 25 | allow(user).to receive(:remember_me_token_expires_at) 26 | allow(user).to receive_message_chain(:sorcery_config, :remember_me_token_attribute_name).and_return(:remember_me_token) 27 | allow(user).to receive_message_chain(:sorcery_config, :remember_me_token_expires_at_attribute_name).and_return(:remember_me_token_expires_at) 28 | end 29 | 30 | it 'sets cookie on remember_me!' do 31 | expect(User).to receive(:authenticate).with('bla@bla.com', 'secret') { |&block| block.call(user, nil) } 32 | expect(user).to receive(:remember_me!) 33 | 34 | post :test_login_with_remember, params: { email: 'bla@bla.com', password: 'secret' } 35 | 36 | expect(cookies.signed['remember_me_token']).to eq assigns[:current_user].remember_me_token 37 | end 38 | 39 | it 'clears cookie on forget_me!' do 40 | request.cookies[:remember_me_token] = { value: 'asd54234dsfsd43534', expires: 3600 } 41 | get :test_logout_with_forget_me 42 | 43 | expect(response.cookies[:remember_me_token]).to be_nil 44 | end 45 | 46 | it 'clears cookie on force_forget_me!' do 47 | request.cookies[:remember_me_token] = { value: 'asd54234dsfsd43534', expires: 3600 } 48 | get :test_logout_with_force_forget_me 49 | 50 | expect(response.cookies[:remember_me_token]).to be_nil 51 | end 52 | 53 | it 'login(email,password,remember_me) logs user in and remembers' do 54 | expect(User).to receive(:authenticate).with('bla@bla.com', 'secret', '1') { |&block| block.call(user, nil) } 55 | expect(user).to receive(:remember_me!) 56 | expect(user).to receive(:remember_me_token).and_return('abracadabra').twice 57 | 58 | post :test_login_with_remember_in_login, params: { email: 'bla@bla.com', password: 'secret', remember: '1' } 59 | 60 | expect(cookies.signed['remember_me_token']).not_to be_nil 61 | expect(cookies.signed['remember_me_token']).to eq assigns[:user].remember_me_token 62 | end 63 | 64 | it 'logout also calls forget_me!' do 65 | session[:user_id] = user.id.to_s 66 | expect(User.sorcery_adapter).to receive(:find_by_id).with(user.id.to_s).and_return(user) 67 | expect(user).to receive(:remember_me!) 68 | expect(user).to receive(:forget_me!) 69 | get :test_logout_with_remember 70 | 71 | expect(cookies['remember_me_token']).to be_nil 72 | end 73 | 74 | it 'logs user in from cookie' do 75 | session[:user_id] = user.id.to_s 76 | expect(User.sorcery_adapter).to receive(:find_by_id).with(user.id.to_s).and_return(user) 77 | expect(user).to receive(:remember_me!) 78 | expect(user).to receive(:remember_me_token).and_return('token').twice 79 | expect(user).to receive(:has_remember_me_token?) { true } 80 | 81 | subject.remember_me! 82 | subject.instance_eval do 83 | remove_instance_variable :@current_user 84 | end 85 | session[:user_id] = nil 86 | 87 | expect(User.sorcery_adapter).to receive(:find_by_remember_me_token).with('token').and_return(user) 88 | 89 | expect(subject).to receive(:after_remember_me!).with(user) 90 | 91 | get :test_login_from_cookie 92 | 93 | expect(assigns[:current_user]).to eq user 94 | end 95 | 96 | it 'doest not remember_me! when not asked to, even if third parameter is used' do 97 | post :test_login_with_remember_in_login, params: { email: 'bla@bla.com', password: 'secret', remember: '0' } 98 | 99 | expect(cookies['remember_me_token']).to be_nil 100 | end 101 | 102 | it 'doest not remember_me! when not asked to' do 103 | post :test_login, params: { email: 'bla@bla.com', password: 'secret' } 104 | expect(cookies['remember_me_token']).to be_nil 105 | end 106 | 107 | # --- login_user(user) --- 108 | specify { expect(@controller).to respond_to :auto_login } 109 | 110 | it 'auto_login(user) logs in an user instance without remembering' do 111 | session[:user_id] = nil 112 | subject.auto_login(user) 113 | get :test_login_from_cookie 114 | 115 | expect(assigns[:current_user]).to eq user 116 | expect(cookies['remember_me_token']).to be_nil 117 | end 118 | 119 | it 'auto_login(user, true) logs in an user instance with remembering' do 120 | session[:user_id] = nil 121 | expect(user).to receive(:remember_me!) 122 | subject.auto_login(user, true) 123 | 124 | get :test_login_from_cookie 125 | 126 | expect(assigns[:current_user]).to eq user 127 | expect(cookies['remember_me_token']).not_to be_nil 128 | end 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /spec/orm/active_record.rb: -------------------------------------------------------------------------------- 1 | require 'sorcery' 2 | 3 | ActiveRecord::Migration.verbose = false 4 | # ActiveRecord::Base.logger = Logger.new(nil) 5 | # ActiveRecord::Base.include_root_in_json = true 6 | 7 | class TestUser < ActiveRecord::Base 8 | authenticates_with_sorcery! 9 | end 10 | 11 | def setup_orm 12 | MigrationHelper.migrate(migrations_path) 13 | end 14 | 15 | def teardown_orm 16 | MigrationHelper.rollback(migrations_path) 17 | end 18 | 19 | def migrations_path 20 | Rails.root.join('db', 'migrate', 'core') 21 | end 22 | -------------------------------------------------------------------------------- /spec/providers/example_provider_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'sorcery/providers/base' 5 | 6 | describe Sorcery::Providers::ExampleProvider do 7 | before(:all) do 8 | sorcery_reload!([:external]) 9 | sorcery_controller_property_set(:external_providers, [:example_provider]) 10 | end 11 | 12 | context 'fetching a multi-word custom provider' do 13 | it 'returns the provider' do 14 | expect(Sorcery::Controller::Config.example_provider).to be_a(Sorcery::Providers::ExampleProvider) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/providers/example_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'sorcery/providers/base' 5 | 6 | describe Sorcery::Providers::Example do 7 | before(:all) do 8 | sorcery_reload!([:external]) 9 | sorcery_controller_property_set(:external_providers, [:example]) 10 | end 11 | 12 | context 'fetching a single-word custom provider' do 13 | it 'returns the provider' do 14 | expect(Sorcery::Controller::Config.example).to be_a(Sorcery::Providers::Example) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/providers/examples_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'sorcery/providers/base' 5 | 6 | describe Sorcery::Providers::Examples do 7 | before(:all) do 8 | sorcery_reload!([:external]) 9 | sorcery_controller_property_set(:external_providers, [:examples]) 10 | end 11 | 12 | context 'fetching a plural custom provider' do 13 | it 'returns the provider' do 14 | expect(Sorcery::Controller::Config.examples).to be_a(Sorcery::Providers::Examples) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/providers/vk_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'sorcery/providers/base' 3 | require 'sorcery/providers/vk' 4 | require 'webmock/rspec' 5 | 6 | describe Sorcery::Providers::Vk do 7 | include WebMock::API 8 | 9 | let(:provider) { Sorcery::Controller::Config.vk } 10 | 11 | before(:all) do 12 | sorcery_reload!([:external]) 13 | sorcery_controller_property_set(:external_providers, [:vk]) 14 | sorcery_controller_external_property_set(:vk, :key, 'KEY') 15 | sorcery_controller_external_property_set(:vk, :secret, 'SECRET') 16 | end 17 | 18 | def stub_vk_authorize 19 | stub_request(:post, %r{https\:\/\/oauth\.vk\.com\/access_token}).to_return( 20 | status: 200, 21 | body: '{"access_token":"TOKEN","expires_in":86329,"user_id":1}', 22 | headers: { 'content-type' => 'application/json' } 23 | ) 24 | end 25 | 26 | context 'getting user info hash' do 27 | it 'should provide VK API version' do 28 | stub_vk_authorize 29 | sorcery_controller_external_property_set(:vk, :api_version, '5.71') 30 | 31 | get_user = stub_request( 32 | :get, 33 | 'https://api.vk.com/method/getProfiles?access_token=TOKEN&fields=&scope=email&uids=1&v=5.71' 34 | ).to_return(body: '{"response":[{"id":1}]}') 35 | 36 | token = provider.process_callback({ code: 'CODE' }, nil) 37 | provider.get_user_hash(token) 38 | 39 | expect(get_user).to have_been_requested 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/rails_app/app/active_record/authentication.rb: -------------------------------------------------------------------------------- 1 | class Authentication < ActiveRecord::Base 2 | belongs_to :user 3 | end 4 | -------------------------------------------------------------------------------- /spec/rails_app/app/active_record/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | has_many :authentications, dependent: :destroy 3 | has_many :user_providers, dependent: :destroy 4 | accepts_nested_attributes_for :authentications 5 | end 6 | -------------------------------------------------------------------------------- /spec/rails_app/app/active_record/user_provider.rb: -------------------------------------------------------------------------------- 1 | class UserProvider < ActiveRecord::Base 2 | belongs_to :user 3 | end 4 | -------------------------------------------------------------------------------- /spec/rails_app/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /spec/rails_app/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | end 3 | -------------------------------------------------------------------------------- /spec/rails_app/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /spec/rails_app/app/mailers/sorcery_mailer.rb: -------------------------------------------------------------------------------- 1 | class SorceryMailer < ActionMailer::Base 2 | default from: 'notifications@example.com' 3 | 4 | def activation_needed_email(user) 5 | @user = user 6 | @url = 'http://example.com/login' 7 | mail(to: user.email, 8 | subject: 'Welcome to My Awesome Site') 9 | end 10 | 11 | def activation_success_email(user) 12 | @user = user 13 | @url = 'http://example.com/login' 14 | mail(to: user.email, 15 | subject: 'Your account is now activated') 16 | end 17 | 18 | def reset_password_email(user) 19 | @user = user 20 | @url = 'http://example.com/login' 21 | mail(to: user.email, 22 | subject: 'Your password has been reset') 23 | end 24 | 25 | def send_unlock_token_email(user) 26 | @user = user 27 | @url = "http://example.com/unlock/#{user.unlock_token}" 28 | mail(to: user.email, 29 | subject: 'Your account has been locked due to many wrong logins') 30 | end 31 | 32 | def magic_login_email(user) 33 | @user = user 34 | @url = 'http://example.com/login' 35 | mail(to: user.email, 36 | subject: 'Magic Login') 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/rails_app/app/views/application/index.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_tag :action => :test_login, :method => :post do %> 2 |
3 | <%= label_tag :username %>
4 | <%= text_field_tag :username %> 5 |
6 |
7 | <%= label_tag :password %>
8 | <%= password_field_tag :password %> 9 |
10 |
11 | <%= submit_tag "Login" %> 12 |
13 |
14 | <%= label_tag "keep me logged in" %>
15 | <%= check_box_tag :remember %> 16 |
17 | <% end %> -------------------------------------------------------------------------------- /spec/rails_app/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | AppRoot 5 | <%= stylesheet_link_tag :all %> 6 | <%= javascript_include_tag :defaults %> 7 | <%= csrf_meta_tag %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/rails_app/app/views/sorcery_mailer/activation_email.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Welcome to example.com, <%= @user.username %>

8 |

9 | You have successfully signed up to example.com, 10 | your username is: <%= @user.username %>.
11 |

12 |

13 | To login to the site, just follow this link: <%= @url %>. 14 |

15 |

Thanks for joining and have a great day!

16 | 17 | -------------------------------------------------------------------------------- /spec/rails_app/app/views/sorcery_mailer/activation_email.text.erb: -------------------------------------------------------------------------------- 1 | Welcome to example.com, <%= @user.username %> 2 | =============================================== 3 | 4 | You have successfully signed up to example.com, 5 | your username is: <%= @user.username %>. 6 | 7 | To login to the site, just follow this link: <%= @url %>. 8 | 9 | Thanks for joining and have a great day! -------------------------------------------------------------------------------- /spec/rails_app/app/views/sorcery_mailer/activation_needed_email.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Congratz, <%= @user.username %>

8 |

9 | You have successfully activated your example.com account, 10 | your username is: <%= @user.username %>.
11 |

12 |

13 | To login to the site, just follow this link: <%= @url %>. 14 |

15 |

Thanks for joining and have a great day!

16 | 17 | -------------------------------------------------------------------------------- /spec/rails_app/app/views/sorcery_mailer/activation_success_email.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Congratz, <%= @user.username %>

8 |

9 | You have successfully activated your example.com account, 10 | your username is: <%= @user.username %>.
11 |

12 |

13 | To login to the site, just follow this link: <%= @url %>. 14 |

15 |

Thanks for joining and have a great day!

16 | 17 | -------------------------------------------------------------------------------- /spec/rails_app/app/views/sorcery_mailer/activation_success_email.text.erb: -------------------------------------------------------------------------------- 1 | Congratz, <%= @user.username %> 2 | =============================================== 3 | 4 | You have successfully activated your example.com account, 5 | your username is: <%= @user.username %>. 6 | 7 | To login to the site, just follow this link: <%= @url %>. 8 | 9 | Thanks for joining and have a great day! -------------------------------------------------------------------------------- /spec/rails_app/app/views/sorcery_mailer/magic_login_email.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Hello, <%= @user.username %>

8 |

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

11 |

Have a great day!

12 | 13 | 14 | -------------------------------------------------------------------------------- /spec/rails_app/app/views/sorcery_mailer/magic_login_email.text.erb: -------------------------------------------------------------------------------- 1 | Hello, <%= @user.username %> 2 | =============================================== 3 | 4 | To login without a password, just follow this link: <%= @url %>. 5 | 6 | Have a great day! 7 | -------------------------------------------------------------------------------- /spec/rails_app/app/views/sorcery_mailer/reset_password_email.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Hello, <%= @user.username %>

8 |

9 | You have requested to reset your password. 10 |

11 |

12 | To choose a new password, just follow this link: <%= @url %>. 13 |

14 |

Have a great day!

15 | 16 | -------------------------------------------------------------------------------- /spec/rails_app/app/views/sorcery_mailer/reset_password_email.text.erb: -------------------------------------------------------------------------------- 1 | Hello, <%= @user.username %> 2 | =============================================== 3 | 4 | You have requested to reset your password. 5 | 6 | To choose a new password, just follow this link: <%= @url %>. 7 | 8 | Have a great day! -------------------------------------------------------------------------------- /spec/rails_app/app/views/sorcery_mailer/send_unlock_token_email.text.erb: -------------------------------------------------------------------------------- 1 | Please visit <%= @url %> for unlock your account. -------------------------------------------------------------------------------- /spec/rails_app/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run RailsApp::Application 5 | -------------------------------------------------------------------------------- /spec/rails_app/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('boot', __dir__) 2 | 3 | require 'action_controller/railtie' 4 | require 'action_mailer/railtie' 5 | require 'rails/test_unit/railtie' 6 | 7 | Bundler.require :default, SORCERY_ORM 8 | 9 | # rubocop:disable Lint/HandleExceptions 10 | begin 11 | require "#{SORCERY_ORM}/railtie" 12 | rescue LoadError 13 | # TODO: Log this issue or change require scheme. 14 | end 15 | # rubocop:enable Lint/HandleExceptions 16 | 17 | require 'sorcery' 18 | 19 | module AppRoot 20 | class Application < Rails::Application 21 | config.autoload_paths.reject! { |p| p =~ %r{/\/app\/(\w+)$/} && !%w[controllers helpers mailers views].include?(Regexp.last_match(1)) } 22 | config.autoload_paths += ["#{config.root}/app/#{SORCERY_ORM}"] 23 | 24 | # Settings in config/environments/* take precedence over those specified here. 25 | # Application configuration should go into files in config/initializers 26 | # -- all .rb files in that directory are automatically loaded. 27 | 28 | # Custom directories with classes and modules you want to be autoloadable. 29 | # config.autoload_paths += %W(#{config.root}/extras) 30 | 31 | # Only load the plugins named here, in the order given (default is alphabetical). 32 | # :all can be used as a placeholder for all plugins not explicitly named. 33 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 34 | 35 | # Activate observers that should always be running. 36 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 37 | 38 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 39 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 40 | # config.time_zone = 'Central Time (US & Canada)' 41 | 42 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 43 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 44 | # config.i18n.default_locale = :de 45 | 46 | # JavaScript files you want as :defaults (application.js is always included). 47 | # config.action_view.javascript_expansions[:defaults] = %w(jquery rails) 48 | 49 | # Configure the default encoding used in templates for Ruby 1.9. 50 | config.encoding = 'utf-8' 51 | 52 | # Configure sensitive parameters which will be filtered from the log file. 53 | config.filter_parameters += [:password] 54 | 55 | config.action_mailer.delivery_method = :test 56 | config.active_support.deprecation = :stderr 57 | if config.active_record.sqlite3.present? 58 | config.active_record.sqlite3.represent_boolean_as_integer = true 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/rails_app/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__) 3 | 4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 5 | -------------------------------------------------------------------------------- /spec/rails_app/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3-ruby (not necessary on OS X Leopard) 3 | development: 4 | adapter: sqlite3 5 | database: db/development.sqlite3 6 | pool: 5 7 | timeout: 5000 8 | 9 | # Warning: The database defined as "test" will be erased and 10 | # re-generated from your development database when you run "rake". 11 | # Do not set this db to the same as development or production. 12 | test: 13 | adapter: sqlite3 14 | database: db/test.sqlite3 15 | pool: 5 16 | timeout: 5000 17 | # adapter: sqlite3 18 | # database: ":memory:" 19 | 20 | production: 21 | adapter: sqlite3 22 | database: ":memory:" 23 | -------------------------------------------------------------------------------- /spec/rails_app/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('application', __dir__) 3 | 4 | # Initialize the rails application 5 | AppRoot::Application.initialize! 6 | -------------------------------------------------------------------------------- /spec/rails_app/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | AppRoot::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Log error messages when you accidentally call methods on nil. 11 | config.whiny_nils = true 12 | 13 | # Show full error reports and disable caching 14 | config.consider_all_requests_local = true 15 | config.action_controller.perform_caching = false 16 | 17 | # Raise exceptions instead of rendering exception templates 18 | config.action_dispatch.show_exceptions = false 19 | 20 | # Disable request forgery protection in test environment 21 | config.action_controller.allow_forgery_protection = false 22 | 23 | # Tell Action Mailer not to deliver emails to the real world. 24 | # The :test delivery method accumulates sent emails in the 25 | # ActionMailer::Base.deliveries array. 26 | config.action_mailer.delivery_method = :test 27 | 28 | # Use SQL instead of Active Record's schema dumper when creating the test database. 29 | # This is necessary if your schema can't be completely dumped by the schema dumper, 30 | # like if you have constraints or database-specific column types 31 | # config.active_record.schema_format = :sql 32 | 33 | # Print deprecation notices to the stderr 34 | config.active_support.deprecation = :stderr 35 | 36 | config.eager_load = false 37 | end 38 | -------------------------------------------------------------------------------- /spec/rails_app/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /spec/rails_app/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | -------------------------------------------------------------------------------- /spec/rails_app/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /spec/rails_app/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | AppRoot::Application.config.session_store :cookie_store, key: '_app_root_session' 4 | 5 | # Use the database for sessions instead of the cookie-based default, 6 | # which shouldn't be used to store highly confidential information 7 | # (create the session table with "rails generate session_migration") 8 | # AppRoot::Application.config.session_store :active_record_store 9 | 10 | if AppRoot::Application.config.respond_to?(:secret_key_base=) 11 | AppRoot::Application.config.secret_key_base = 'foobar' 12 | end 13 | -------------------------------------------------------------------------------- /spec/rails_app/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" 6 | -------------------------------------------------------------------------------- /spec/rails_app/config/routes.rb: -------------------------------------------------------------------------------- 1 | AppRoot::Application.routes.draw do 2 | root to: 'application#index' 3 | 4 | controller :sorcery do 5 | get :test_login 6 | get :test_logout 7 | get :some_action 8 | post :test_return_to 9 | get :test_auto_login 10 | post :test_login_with_remember_in_login 11 | get :test_login_from_cookie 12 | get :test_login_from 13 | get :test_logout_with_remember 14 | get :test_logout_with_forget_me 15 | get :test_logout_with_force_forget_me 16 | get :test_invalidate_active_session 17 | get :test_should_be_logged_in 18 | get :test_create_from_provider 19 | get :test_add_second_provider 20 | get :test_return_to_with_external 21 | get :test_login_from 22 | get :test_login_from_twitter 23 | get :test_login_from_facebook 24 | get :test_login_from_github 25 | get :test_login_from_paypal 26 | get :test_login_from_wechat 27 | get :test_login_from_microsoft 28 | get :test_login_from_google 29 | get :test_login_from_liveid 30 | get :test_login_from_vk 31 | get :test_login_from_jira 32 | get :test_login_from_salesforce 33 | get :test_login_from_slack 34 | get :test_login_from_instagram 35 | get :test_login_from_auth0 36 | get :test_login_from_line 37 | get :test_login_from_discord 38 | get :test_login_from_battlenet 39 | get :login_at_test 40 | get :login_at_test_twitter 41 | get :login_at_test_facebook 42 | get :login_at_test_github 43 | get :login_at_test_paypal 44 | get :login_at_test_wechat 45 | get :login_at_test_microsoft 46 | get :login_at_test_google 47 | get :login_at_test_liveid 48 | get :login_at_test_vk 49 | get :login_at_test_jira 50 | get :login_at_test_salesforce 51 | get :login_at_test_slack 52 | get :login_at_test_instagram 53 | get :login_at_test_auth0 54 | get :login_at_test_line 55 | get :login_at_test_discord 56 | get :login_at_test_battlenet 57 | get :test_return_to_with_external 58 | get :test_return_to_with_external_twitter 59 | get :test_return_to_with_external_facebook 60 | get :test_return_to_with_external_github 61 | get :test_return_to_with_external_paypal 62 | get :test_return_to_with_external_wechat 63 | get :test_return_to_with_external_microsoft 64 | get :test_return_to_with_external_google 65 | get :test_return_to_with_external_liveid 66 | get :test_return_to_with_external_vk 67 | get :test_return_to_with_external_jira 68 | get :test_return_to_with_external_salesforce 69 | get :test_return_to_with_external_slack 70 | get :test_return_to_with_external_instagram 71 | get :test_return_to_with_external_auth0 72 | get :test_return_to_with_external_line 73 | get :test_return_to_with_external_discord 74 | get :test_return_to_with_external_battlenet 75 | get :test_http_basic_auth 76 | get :some_action_making_a_non_persisted_change_to_the_user 77 | post :test_login_with_remember 78 | get :test_create_from_provider_with_block 79 | get :login_at_test_with_state 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /spec/rails_app/config/secrets.yml: -------------------------------------------------------------------------------- 1 | # secrets.yml 2 | 3 | test: 4 | secret_key_base: 'a9789f869a0d0ac2f2b683d6e9410c530696b178bca28a7971f4a652b14ff2da89f2cf4dcbf0355f6bc41f81731aa8e46085674d1acc1980436f61cdba76ff5d' 5 | -------------------------------------------------------------------------------- /spec/rails_app/db/migrate/activation/20101224223622_add_activation_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddActivationToUsers < ActiveRecord::Migration::Current 2 | def self.up 3 | add_column :users, :activation_state, :string, default: nil 4 | add_column :users, :activation_token, :string, default: nil 5 | add_column :users, :activation_token_expires_at, :datetime, default: nil 6 | 7 | add_index :users, :activation_token 8 | end 9 | 10 | def self.down 11 | remove_index :users, :activation_token 12 | 13 | remove_column :users, :activation_token_expires_at 14 | remove_column :users, :activation_token 15 | remove_column :users, :activation_state 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/rails_app/db/migrate/activity_logging/20101224223624_add_activity_logging_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddActivityLoggingToUsers < ActiveRecord::Migration::Current 2 | def self.up 3 | add_column :users, :last_login_at, :datetime, default: nil 4 | add_column :users, :last_logout_at, :datetime, default: nil 5 | add_column :users, :last_activity_at, :datetime, default: nil 6 | add_column :users, :last_login_from_ip_address, :string, default: nil 7 | 8 | add_index :users, %i[last_logout_at last_activity_at] 9 | end 10 | 11 | def self.down 12 | remove_index :users, %i[last_logout_at last_activity_at] 13 | 14 | remove_column :users, :last_activity_at 15 | remove_column :users, :last_logout_at 16 | remove_column :users, :last_login_at 17 | remove_column :users, :last_login_from_ip_address 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/rails_app/db/migrate/brute_force_protection/20101224223626_add_brute_force_protection_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddBruteForceProtectionToUsers < ActiveRecord::Migration::Current 2 | def self.up 3 | add_column :users, :failed_logins_count, :integer, default: 0 4 | add_column :users, :lock_expires_at, :datetime, default: nil 5 | add_column :users, :unlock_token, :string, default: nil 6 | end 7 | 8 | def self.down 9 | remove_column :users, :unlock_token 10 | remove_column :users, :lock_expires_at 11 | remove_column :users, :failed_logins_count 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/rails_app/db/migrate/core/20101224223620_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration::Current 2 | def self.up 3 | create_table :users do |t| 4 | t.string :username, null: false 5 | t.string :email, default: nil 6 | t.string :crypted_password 7 | t.string :salt 8 | 9 | t.timestamps null: false 10 | end 11 | end 12 | 13 | def self.down 14 | drop_table :users 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/rails_app/db/migrate/external/20101224223628_create_authentications_and_user_providers.rb: -------------------------------------------------------------------------------- 1 | class CreateAuthenticationsAndUserProviders < ActiveRecord::Migration::Current 2 | def self.up 3 | create_table :authentications do |t| 4 | t.integer :user_id, null: false 5 | t.string :provider, :uid, null: false 6 | 7 | t.timestamps null: false 8 | end 9 | 10 | create_table :user_providers do |t| 11 | t.integer :user_id, null: false 12 | t.string :provider, :uid, null: false 13 | 14 | t.timestamps null: false 15 | end 16 | end 17 | 18 | def self.down 19 | drop_table :authentications 20 | drop_table :user_providers 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/rails_app/db/migrate/invalidate_active_sessions/20180221093235_add_invalidate_active_sessions_before_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddInvalidateSessionsBeforeToUsers < ActiveRecord::Migration::Current 2 | def self.up 3 | add_column :users, :invalidate_sessions_before, :datetime, default: nil 4 | end 5 | 6 | def self.down 7 | remove_column :users, :invalidate_sessions_before 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/rails_app/db/migrate/magic_login/20170924151831_add_magic_login_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddMagicLoginToUsers < ActiveRecord::Migration::Current 2 | def self.up 3 | add_column :users, :magic_login_token, :string, default: nil 4 | add_column :users, :magic_login_token_expires_at, :datetime, default: nil 5 | add_column :users, :magic_login_email_sent_at, :datetime, default: nil 6 | 7 | add_index :users, :magic_login_token 8 | end 9 | 10 | def self.down 11 | remove_index :users, :magic_login_token 12 | 13 | remove_column :users, :magic_login_token 14 | remove_column :users, :magic_login_token_expires_at 15 | remove_column :users, :magic_login_email_sent_at 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/rails_app/db/migrate/remember_me/20101224223623_add_remember_me_token_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddRememberMeTokenToUsers < ActiveRecord::Migration::Current 2 | def self.up 3 | add_column :users, :remember_me_token, :string, default: nil 4 | add_column :users, :remember_me_token_expires_at, :datetime, default: nil 5 | 6 | add_index :users, :remember_me_token 7 | end 8 | 9 | def self.down 10 | remove_index :users, :remember_me_token 11 | 12 | remove_column :users, :remember_me_token_expires_at 13 | remove_column :users, :remember_me_token 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/rails_app/db/migrate/reset_password/20101224223622_add_reset_password_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddResetPasswordToUsers < ActiveRecord::Migration::Current 2 | def self.up 3 | add_column :users, :reset_password_token, :string, default: nil 4 | add_column :users, :reset_password_token_expires_at, :datetime, default: nil 5 | add_column :users, :reset_password_email_sent_at, :datetime, default: nil 6 | add_column :users, :access_count_to_reset_password_page, :integer, default: 0 7 | end 8 | 9 | def self.down 10 | remove_column :users, :reset_password_email_sent_at 11 | remove_column :users, :reset_password_token_expires_at 12 | remove_column :users, :reset_password_token 13 | remove_column :users, :access_count_to_reset_password_page 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/rails_app/db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # Note that this schema.rb definition is the authoritative source for your 6 | # database schema. If you need to create the application database on another 7 | # system, you should be using db:schema:load, not running all the migrations 8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 9 | # you'll amass, the slower it'll run and the greater likelihood for issues). 10 | # 11 | # It's strongly recommended to check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 20_101_224_223_620) do 14 | create_table 'users', force: true do |t| 15 | t.string 'username' 16 | t.string 'email' 17 | t.string 'crypted_password' 18 | t.datetime 'created_at' 19 | t.datetime 'updated_at' 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/rails_app/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ :name => 'Chicago' }, { :name => 'Copenhagen' }]) 7 | # Mayor.create(:name => 'Daley', :city => cities.first) 8 | -------------------------------------------------------------------------------- /spec/shared_examples/user_activity_logging_shared_examples.rb: -------------------------------------------------------------------------------- 1 | shared_examples_for 'rails_3_activity_logging_model' do 2 | context 'loaded plugin configuration' do 3 | before(:all) do 4 | sorcery_reload!([:activity_logging]) 5 | end 6 | 7 | after(:each) do 8 | User.sorcery_config.reset! 9 | end 10 | 11 | it "allows configuration option 'last_login_at_attribute_name'" do 12 | sorcery_model_property_set(:last_login_at_attribute_name, :login_time) 13 | 14 | expect(User.sorcery_config.last_login_at_attribute_name).to eq :login_time 15 | end 16 | 17 | it "allows configuration option 'last_logout_at_attribute_name'" do 18 | sorcery_model_property_set(:last_logout_at_attribute_name, :logout_time) 19 | expect(User.sorcery_config.last_logout_at_attribute_name).to eq :logout_time 20 | end 21 | 22 | it "allows configuration option 'last_activity_at_attribute_name'" do 23 | sorcery_model_property_set(:last_activity_at_attribute_name, :activity_time) 24 | expect(User.sorcery_config.last_activity_at_attribute_name).to eq :activity_time 25 | end 26 | 27 | it "allows configuration option 'last_login_from_ip_adress'" do 28 | sorcery_model_property_set(:last_login_from_ip_address_name, :ip_address) 29 | expect(User.sorcery_config.last_login_from_ip_address_name).to eq :ip_address 30 | end 31 | 32 | it '.set_last_login_at update last_login_at' do 33 | user = create_new_user 34 | now = Time.now.in_time_zone 35 | expect(user.sorcery_adapter).to receive(:update_attribute).with(:last_login_at, now) 36 | 37 | user.set_last_login_at(now) 38 | end 39 | 40 | it '.set_last_logout_at update last_logout_at' do 41 | user = create_new_user 42 | now = Time.now.in_time_zone 43 | expect(user.sorcery_adapter).to receive(:update_attribute).with(:last_logout_at, now) 44 | 45 | user.set_last_logout_at(now) 46 | end 47 | 48 | it '.set_last_activity_at update last_activity_at' do 49 | user = create_new_user 50 | now = Time.now.in_time_zone 51 | expect(user.sorcery_adapter).to receive(:update_attribute).with(:last_activity_at, now) 52 | 53 | user.set_last_activity_at(now) 54 | end 55 | 56 | it '.set_last_ip_address update last_login_from_ip_address' do 57 | user = create_new_user 58 | expect(user.sorcery_adapter).to receive(:update_attribute).with(:last_login_from_ip_address, '0.0.0.0') 59 | 60 | user.set_last_ip_address('0.0.0.0') 61 | end 62 | 63 | it 'show if user logged in' do 64 | user = create_new_user 65 | expect(user.logged_in?).to eq(false) 66 | 67 | now = Time.now.in_time_zone 68 | user.set_last_login_at(now) 69 | expect(user.logged_in?).to eq(true) 70 | 71 | now = Time.now.in_time_zone 72 | user.set_last_logout_at(now) 73 | expect(user.logged_in?).to eq(false) 74 | end 75 | 76 | it 'show if user logged out' do 77 | user = create_new_user 78 | expect(user.logged_out?).to eq(true) 79 | 80 | now = Time.now.in_time_zone 81 | user.set_last_login_at(now) 82 | expect(user.logged_out?).to eq(false) 83 | 84 | now = Time.now.in_time_zone 85 | user.set_last_logout_at(now) 86 | expect(user.logged_out?).to eq(true) 87 | end 88 | 89 | it 'show online status of user' do 90 | user = create_new_user 91 | expect(user.online?).to eq(false) 92 | 93 | now = Time.now.in_time_zone 94 | user.set_last_login_at(now) 95 | user.set_last_activity_at(now) 96 | expect(user.online?).to eq(true) 97 | 98 | user.set_last_activity_at(now - 1.day) 99 | expect(user.online?).to eq(false) 100 | 101 | now = Time.now.in_time_zone 102 | user.set_last_logout_at(now) 103 | expect(user.online?).to eq(false) 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /spec/shared_examples/user_brute_force_protection_shared_examples.rb: -------------------------------------------------------------------------------- 1 | shared_examples_for 'rails_3_brute_force_protection_model' do 2 | let(:user) { create_new_user } 3 | before(:each) do 4 | User.sorcery_adapter.delete_all 5 | end 6 | 7 | context 'loaded plugin configuration' do 8 | let(:config) { User.sorcery_config } 9 | 10 | before(:all) do 11 | sorcery_reload!([:brute_force_protection]) 12 | end 13 | 14 | after(:each) do 15 | User.sorcery_config.reset! 16 | end 17 | 18 | specify { expect(user).to respond_to(:failed_logins_count) } 19 | specify { expect(user).to respond_to(:lock_expires_at) } 20 | 21 | it "enables configuration option 'failed_logins_count_attribute_name'" do 22 | sorcery_model_property_set(:failed_logins_count_attribute_name, :my_count) 23 | expect(config.failed_logins_count_attribute_name).to eq :my_count 24 | end 25 | 26 | it "enables configuration option 'lock_expires_at_attribute_name'" do 27 | sorcery_model_property_set(:lock_expires_at_attribute_name, :expires) 28 | expect(config.lock_expires_at_attribute_name).to eq :expires 29 | end 30 | 31 | it "enables configuration option 'consecutive_login_retries_amount_allowed'" do 32 | sorcery_model_property_set(:consecutive_login_retries_amount_limit, 34) 33 | expect(config.consecutive_login_retries_amount_limit).to eq 34 34 | end 35 | 36 | it "enables configuration option 'login_lock_time_period'" do 37 | sorcery_model_property_set(:login_lock_time_period, 2.hours) 38 | expect(config.login_lock_time_period).to eq 2.hours 39 | end 40 | 41 | describe '#login_locked?' do 42 | it 'is locked' do 43 | user.send("#{config.lock_expires_at_attribute_name}=", Time.now + 5.days) 44 | expect(user).to be_login_locked 45 | end 46 | 47 | it "isn't locked" do 48 | user.send("#{config.lock_expires_at_attribute_name}=", nil) 49 | expect(user).not_to be_login_locked 50 | end 51 | end 52 | end 53 | 54 | describe '#register_failed_login!' do 55 | it 'locks user when number of retries reached the limit' do 56 | expect(user.lock_expires_at).to be_nil 57 | 58 | sorcery_model_property_set(:consecutive_login_retries_amount_limit, 1) 59 | user.register_failed_login! 60 | lock_expires_at = User.sorcery_adapter.find_by_id(user.id).lock_expires_at 61 | 62 | expect(lock_expires_at).not_to be_nil 63 | end 64 | 65 | context 'unlock_token_mailer_disabled is true' do 66 | it 'does not automatically send unlock email' do 67 | sorcery_model_property_set(:unlock_token_mailer_disabled, true) 68 | sorcery_model_property_set(:consecutive_login_retries_amount_limit, 2) 69 | sorcery_model_property_set(:login_lock_time_period, 0) 70 | sorcery_model_property_set(:unlock_token_mailer, SorceryMailer) 71 | 72 | 3.times { user.register_failed_login! } 73 | 74 | expect(ActionMailer::Base.deliveries.size).to eq 0 75 | end 76 | end 77 | 78 | context 'unlock_token_mailer_disabled is false' do 79 | before do 80 | sorcery_model_property_set(:unlock_token_mailer_disabled, false) 81 | sorcery_model_property_set(:consecutive_login_retries_amount_limit, 2) 82 | sorcery_model_property_set(:login_lock_time_period, 0) 83 | sorcery_model_property_set(:unlock_token_mailer, SorceryMailer) 84 | end 85 | 86 | it 'does not automatically send unlock email' do 87 | 3.times { user.register_failed_login! } 88 | 89 | expect(ActionMailer::Base.deliveries.size).to eq 1 90 | end 91 | 92 | it 'generates unlock token before mail is sent' do 93 | 3.times { user.register_failed_login! } 94 | 95 | expect(ActionMailer::Base.deliveries.last.body.to_s.match(user.unlock_token)).not_to be_nil 96 | end 97 | end 98 | end 99 | 100 | context '.authenticate' do 101 | it 'unlocks after lock time period passes' do 102 | sorcery_model_property_set(:consecutive_login_retries_amount_limit, 2) 103 | sorcery_model_property_set(:login_lock_time_period, 0.2) 104 | 2.times { user.register_failed_login! } 105 | 106 | lock_expires_at = User.sorcery_adapter.find_by_id(user.id).lock_expires_at 107 | expect(lock_expires_at).not_to be_nil 108 | 109 | Timecop.travel(Time.now.in_time_zone + 0.3) 110 | User.authenticate('bla@bla.com', 'secret') 111 | 112 | lock_expires_at = User.sorcery_adapter.find_by_id(user.id).lock_expires_at 113 | expect(lock_expires_at).to be_nil 114 | Timecop.return 115 | end 116 | 117 | it 'doest not unlock if time period is 0 (permanent lock)' do 118 | sorcery_model_property_set(:consecutive_login_retries_amount_limit, 2) 119 | sorcery_model_property_set(:login_lock_time_period, 0) 120 | 121 | 2.times { user.register_failed_login! } 122 | 123 | unlock_date = user.lock_expires_at 124 | Timecop.travel(Time.now.in_time_zone + 1) 125 | 126 | user.register_failed_login! 127 | 128 | expect(user.lock_expires_at.to_s).to eq unlock_date.to_s 129 | Timecop.return 130 | end 131 | end 132 | 133 | describe '#login_unlock!' do 134 | it 'unlocks after entering unlock token' do 135 | sorcery_model_property_set(:consecutive_login_retries_amount_limit, 2) 136 | sorcery_model_property_set(:login_lock_time_period, 0) 137 | sorcery_model_property_set(:unlock_token_mailer, SorceryMailer) 138 | 3.times { user.register_failed_login! } 139 | 140 | expect(user.unlock_token).not_to be_nil 141 | 142 | token = user.unlock_token 143 | user = User.load_from_unlock_token(token) 144 | 145 | expect(user).not_to be_nil 146 | 147 | user.login_unlock! 148 | expect(User.load_from_unlock_token(user.unlock_token)).to be_nil 149 | end 150 | end 151 | end 152 | -------------------------------------------------------------------------------- /spec/shared_examples/user_oauth_shared_examples.rb: -------------------------------------------------------------------------------- 1 | shared_examples_for 'rails_3_oauth_model' do 2 | # ----------------- PLUGIN CONFIGURATION ----------------------- 3 | 4 | let(:external_user) { create_new_external_user :twitter } 5 | 6 | describe 'loaded plugin configuration' do 7 | before(:all) do 8 | Authentication.sorcery_adapter.delete_all 9 | User.sorcery_adapter.delete_all 10 | 11 | sorcery_reload!([:external]) 12 | sorcery_controller_property_set(:external_providers, [:twitter]) 13 | sorcery_model_property_set(:authentications_class, Authentication) 14 | sorcery_controller_external_property_set(:twitter, :key, 'eYVNBjBDi33aa9GkA3w') 15 | sorcery_controller_external_property_set(:twitter, :secret, 'XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8') 16 | sorcery_controller_external_property_set(:twitter, :callback_url, 'http://blabla.com') 17 | end 18 | 19 | it "responds to 'load_from_provider'" do 20 | expect(User).to respond_to(:load_from_provider) 21 | end 22 | 23 | it "'load_from_provider' loads user if exists" do 24 | external_user 25 | expect(User.load_from_provider(:twitter, 123)).to eq external_user 26 | end 27 | 28 | it "'load_from_provider' returns nil if user doesn't exist" do 29 | external_user 30 | expect(User.load_from_provider(:twitter, 980_342)).to be_nil 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/shared_examples/user_remember_me_shared_examples.rb: -------------------------------------------------------------------------------- 1 | shared_examples_for 'rails_3_remember_me_model' do 2 | let(:user) { create_new_user } 3 | 4 | describe 'loaded plugin configuration' do 5 | before(:all) do 6 | sorcery_reload!([:remember_me]) 7 | end 8 | 9 | after(:each) do 10 | User.sorcery_config.reset! 11 | end 12 | 13 | it "allows configuration option 'remember_me_token_attribute_name'" do 14 | sorcery_model_property_set(:remember_me_token_attribute_name, :my_token) 15 | 16 | expect(User.sorcery_config.remember_me_token_attribute_name).to eq :my_token 17 | end 18 | 19 | it "allows configuration option 'remember_me_token_expires_at_attribute_name'" do 20 | sorcery_model_property_set(:remember_me_token_expires_at_attribute_name, :my_expires) 21 | 22 | expect(User.sorcery_config.remember_me_token_expires_at_attribute_name).to eq :my_expires 23 | end 24 | 25 | it "allows configuration option 'remember_me_token_persist_globally'" do 26 | sorcery_model_property_set(:remember_me_token_persist_globally, true) 27 | 28 | expect(User.sorcery_config.remember_me_token_persist_globally).to eq true 29 | end 30 | 31 | specify { expect(user).to respond_to :remember_me! } 32 | 33 | specify { expect(user).to respond_to :forget_me! } 34 | 35 | specify { expect(user).to respond_to :force_forget_me! } 36 | 37 | it "sets an expiration based on 'remember_me_for' attribute" do 38 | sorcery_model_property_set(:remember_me_for, 2 * 60 * 60 * 24) 39 | 40 | ts = Time.now.in_time_zone 41 | Timecop.freeze(ts) do 42 | user.remember_me! 43 | end 44 | 45 | expect(user.remember_me_token_expires_at.utc.to_s).to eq((ts + 2 * 60 * 60 * 24).utc.to_s) 46 | end 47 | 48 | context 'when not persisting globally' do 49 | before { sorcery_model_property_set(:remember_me_token_persist_globally, false) } 50 | 51 | it "generates a new token on 'remember_me!' when a token doesn't exist" do 52 | expect(user.remember_me_token).to be_nil 53 | user.remember_me! 54 | 55 | expect(user.remember_me_token).not_to be_nil 56 | end 57 | 58 | it "generates a new token on 'remember_me!' when a token exists" do 59 | user.remember_me_token = 'abc123' 60 | user.remember_me! 61 | 62 | expect(user.remember_me_token).not_to be_nil 63 | expect(user.remember_me_token).not_to eq('abc123') 64 | end 65 | 66 | it "deletes the token and expiration on 'forget_me!'" do 67 | user.remember_me! 68 | 69 | expect(user.remember_me_token).not_to be_nil 70 | 71 | user.forget_me! 72 | 73 | expect(user.remember_me_token).to be_nil 74 | expect(user.remember_me_token_expires_at).to be_nil 75 | end 76 | 77 | it "deletes the token and expiration on 'force_forget_me!'" do 78 | user.remember_me! 79 | 80 | expect(user.remember_me_token).not_to be_nil 81 | 82 | user.force_forget_me! 83 | 84 | expect(user.remember_me_token).to be_nil 85 | expect(user.remember_me_token_expires_at).to be_nil 86 | end 87 | end 88 | 89 | context 'when persisting globally' do 90 | before { sorcery_model_property_set(:remember_me_token_persist_globally, true) } 91 | 92 | it "generates a new token on 'remember_me!' when a token doesn't exist" do 93 | expect(user.remember_me_token).to be_nil 94 | user.remember_me! 95 | 96 | expect(user.remember_me_token).not_to be_nil 97 | end 98 | 99 | it "keeps existing token on 'remember_me!' when a token exists" do 100 | user.remember_me_token = 'abc123' 101 | user.remember_me! 102 | 103 | expect(user.remember_me_token).to eq('abc123') 104 | end 105 | 106 | it "keeps the token and expiration on 'forget_me!'" do 107 | user.remember_me! 108 | 109 | expect(user.remember_me_token).not_to be_nil 110 | 111 | user.forget_me! 112 | 113 | expect(user.remember_me_token).to_not be_nil 114 | expect(user.remember_me_token_expires_at).to_not be_nil 115 | end 116 | 117 | it "deletes the token and expiration on 'force_forget_me!'" do 118 | user.remember_me! 119 | 120 | expect(user.remember_me_token).not_to be_nil 121 | 122 | user.force_forget_me! 123 | 124 | expect(user.remember_me_token).to be_nil 125 | expect(user.remember_me_token_expires_at).to be_nil 126 | end 127 | end 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /spec/sorcery_temporary_token_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Sorcery::Model::TemporaryToken do 4 | describe '.generate_random_token' do 5 | before do 6 | sorcery_reload! 7 | end 8 | 9 | subject { Sorcery::Model::TemporaryToken.generate_random_token.length } 10 | 11 | context 'token_randomness is 3' do 12 | before do 13 | sorcery_model_property_set(:token_randomness, 3) 14 | end 15 | 16 | it { is_expected.to eq 4 } 17 | end 18 | 19 | context 'token_randomness is 15' do 20 | before do 21 | sorcery_model_property_set(:token_randomness, 15) 22 | end 23 | 24 | it { is_expected.to eq 20 } 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/spec.opts: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation 3 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 3 | 4 | ENV['RAILS_ENV'] ||= 'test' 5 | 6 | SORCERY_ORM = :active_record 7 | 8 | require 'rails/all' 9 | require 'rspec/rails' 10 | require 'timecop' 11 | require 'byebug' 12 | 13 | def setup_orm; end 14 | 15 | def teardown_orm; end 16 | 17 | require "orm/#{SORCERY_ORM}" 18 | 19 | require 'rails_app/config/environment' 20 | 21 | class TestMailer < ActionMailer::Base; end 22 | 23 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 24 | 25 | RSpec.configure do |config| 26 | config.include RSpec::Rails::ControllerExampleGroup, file_path: /controller(.)*_spec.rb$/ 27 | config.mock_with :rspec 28 | 29 | config.use_transactional_fixtures = false 30 | 31 | config.before(:suite) { setup_orm } 32 | config.after(:suite) { teardown_orm } 33 | config.before(:each) { ActionMailer::Base.deliveries.clear } 34 | 35 | config.include ::Sorcery::TestHelpers::Internal 36 | config.include ::Sorcery::TestHelpers::Internal::Rails 37 | 38 | config.include ::Rails::Controller::Testing::TestProcess, type: :controller 39 | config.include ::Rails::Controller::Testing::TemplateAssertions, type: :controller 40 | config.include ::Rails::Controller::Testing::Integration, type: :controller 41 | end 42 | -------------------------------------------------------------------------------- /spec/support/migration_helper.rb: -------------------------------------------------------------------------------- 1 | class MigrationHelper 2 | class << self 3 | def migrate(path) 4 | ActiveRecord::MigrationContext.new(path).migrate 5 | end 6 | 7 | def rollback(path) 8 | ActiveRecord::MigrationContext.new(path).rollback 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/support/providers/example.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'sorcery/providers/base' 4 | 5 | module Sorcery 6 | module Providers 7 | class Example < Base 8 | include Protocols::Oauth2 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/support/providers/example_provider.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'sorcery/providers/base' 4 | 5 | module Sorcery 6 | module Providers 7 | class ExampleProvider < Base 8 | include Protocols::Oauth2 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/support/providers/examples.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'sorcery/providers/base' 4 | 5 | module Sorcery 6 | module Providers 7 | class Examples < Base 8 | include Protocols::Oauth2 9 | end 10 | end 11 | end 12 | --------------------------------------------------------------------------------