├── .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 |
--------------------------------------------------------------------------------