├── .document ├── .gitignore ├── .rspec ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── gemfiles ├── active_record-rails40.gemfile ├── active_record-rails41.gemfile └── active_record-rails42.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 │ │ ├── remember_me.rb │ │ ├── reset_password.rb │ │ └── user_activation.rb ├── sorcery.rb └── sorcery │ ├── adapters │ ├── active_record_adapter.rb │ └── base_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 │ │ ├── remember_me.rb │ │ ├── reset_password.rb │ │ └── user_activation.rb │ └── temporary_token.rb │ ├── protocols │ ├── certs │ │ └── ca-bundle.crt │ ├── oauth.rb │ └── oauth2.rb │ ├── providers │ ├── base.rb │ ├── facebook.rb │ ├── github.rb │ ├── google.rb │ ├── heroku.rb │ ├── jira.rb │ ├── linkedin.rb │ ├── liveid.rb │ ├── paypal.rb │ ├── salesforce.rb │ ├── twitter.rb │ ├── vk.rb │ └── xing.rb │ ├── test_helpers │ ├── internal.rb │ ├── internal │ │ └── rails.rb │ └── rails │ │ ├── controller.rb │ │ └── integration.rb │ └── version.rb ├── sorcery.gemspec └── spec ├── active_record ├── user_activation_spec.rb ├── user_activity_logging_spec.rb ├── user_brute_force_protection_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 ├── rails_app ├── app │ ├── active_record │ │ ├── authentication.rb │ │ ├── user.rb │ │ └── user_provider.rb │ ├── controllers │ │ └── 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 │ │ ├── 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 │ │ ├── secret_token.rb │ │ └── session_store.rb │ ├── locales │ │ └── en.yml │ └── routes.rb └── 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 │ ├── 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_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 ├── spec.opts └── spec_helper.rb /.document: -------------------------------------------------------------------------------- 1 | lib/**/*.rb 2 | bin/* 3 | - 4 | features/**/*.feature 5 | LICENSE.txt 6 | -------------------------------------------------------------------------------- /.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 | 14 | # jeweler generated 15 | pkg 16 | 17 | # for RVM 18 | .rvmrc 19 | 20 | # for RubyMine 21 | .idea 22 | 23 | # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore: 24 | # 25 | # * Create a file at ~/.gitignore 26 | # * Include files you want ignored 27 | # * Run: git config --global core.excludesfile ~/.gitignore 28 | # 29 | # After doing this, these files will be ignored in all your git projects, 30 | # saving you from having to 'pollute' every project you touch with them 31 | # 32 | # 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) 33 | # 34 | # For MacOS: 35 | # 36 | #.DS_Store 37 | # 38 | # For TextMate 39 | #*.tmproj 40 | tmtags 41 | # 42 | # For emacs: 43 | #*~ 44 | #\#* 45 | #.\#* 46 | # 47 | # For vim: 48 | #*.swp 49 | # 50 | spec/rails_app/log/* 51 | *.log 52 | *.sqlite3 53 | Gemfile*.lock 54 | gemfiles/*.lock 55 | .ruby-version 56 | tags -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - jruby 4 | - 2.0.0 5 | - 2.1 6 | - 2.2 7 | - 2.3.0 8 | 9 | env: 10 | global: 11 | - JRUBY_OPTS="--2.0" 12 | 13 | gemfile: 14 | - Gemfile 15 | - gemfiles/active_record-rails40.gemfile 16 | - gemfiles/active_record-rails41.gemfile 17 | - gemfiles/active_record-rails42.gemfile 18 | 19 | before_script: 20 | - mysql -e 'create database sorcery_test;' 21 | 22 | before_install: 23 | - rvm get stable --auto-dotfiles 24 | - gem update bundler 25 | 26 | matrix: 27 | allow_failures: 28 | - rvm: jruby 29 | 30 | exclude: 31 | - rvm: 2.0.0 32 | gemfile: gemfiles/active_record-rails42.gemfile 33 | 34 | - rvm: jruby 35 | gemfile: Gemfile 36 | 37 | - rvm: 2.2 38 | gemfile: gemfiles/active_record-rails40.gemfile 39 | 40 | - rvm: 2.3.0 41 | gemfile: gemfiles/active_record-rails40.gemfile 42 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', '~> 4.0' 4 | gem 'sqlite3' 5 | gem 'pry' 6 | 7 | gemspec 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Noam Ben-Ari 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Sorcery has moved! 2 | 3 | You can find sorcery at it's new home: [Sorcery/sorcery](https://github.com/Sorcery/sorcery) 4 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | require 'rspec/core/rake_task' 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /gemfiles/active_record-rails40.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'sqlite3', platform: :mri 4 | gem 'activerecord-jdbcsqlite3-adapter', platform: :jruby 5 | gem 'rails', '~> 4.0.1' 6 | 7 | gemspec path: '..' 8 | -------------------------------------------------------------------------------- /gemfiles/active_record-rails41.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'sqlite3', platform: :mri 4 | gem 'activerecord-jdbcsqlite3-adapter', platform: :jruby 5 | gem 'rails', '~> 4.1.0' 6 | 7 | gemspec path: '..' 8 | -------------------------------------------------------------------------------- /gemfiles/active_record-rails42.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'sqlite3', platform: :mri 4 | gem 'activerecord-jdbcsqlite3-adapter', platform: :jruby 5 | gem 'rails', '~> 4.2.0' 6 | 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 model_path 16 | @model_path ||= File.join("app", "models", "#{file_path}.rb") 17 | end 18 | 19 | def file_path 20 | model_name.underscore 21 | end 22 | 23 | def namespace 24 | Rails::Generators.namespace if Rails::Generators.respond_to?(:namespace) 25 | end 26 | 27 | def namespaced? 28 | !!namespace 29 | end 30 | 31 | def model_name 32 | if namespaced? 33 | [namespace.to_s] + [model_class_name] 34 | else 35 | [model_class_name] 36 | end.join("::") 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /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', __FILE__) 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 | 24 | def check_deprecated_options 25 | if options[:migrations] 26 | warn("[DEPRECATED] `--migrations` option is deprecated, please use `--only-submodules` instead") 27 | end 28 | end 29 | 30 | # Copy the initializer file to config/initializers folder. 31 | def copy_initializer_file 32 | template "initializer.rb", sorcery_config_path unless only_submodules? 33 | end 34 | 35 | def configure_initializer_file 36 | # Add submodules to the initializer file. 37 | if submodules 38 | submodule_names = submodules.collect{ |submodule| ':' + submodule } 39 | 40 | gsub_file sorcery_config_path, /submodules = \[.*\]/ do |str| 41 | current_submodule_names = (str =~ /\[(.*)\]/ ? $1 : '').delete(' ').split(',') 42 | "submodules = [#{(current_submodule_names | submodule_names).join(', ')}]" 43 | end 44 | end 45 | end 46 | 47 | def configure_model 48 | # Generate the model and add 'authenticates_with_sorcery!' unless you passed --only-submodules 49 | unless only_submodules? 50 | generate "model #{model_class_name} --skip-migration" 51 | 52 | inject_sorcery_to_model 53 | end 54 | end 55 | 56 | def inject_sorcery_to_model 57 | indents = " " * (namespaced? ? 2 : 1) 58 | 59 | inject_into_class(model_path, model_class_name, "#{indents}authenticates_with_sorcery!\n") 60 | end 61 | 62 | # Copy the migrations files to db/migrate folder 63 | def copy_migration_files 64 | # Copy core migration file in all cases except when you pass --only-submodules. 65 | return unless defined?(Sorcery::Generators::InstallGenerator::ActiveRecord) 66 | migration_template "migration/core.rb", "db/migrate/sorcery_core.rb", migration_class_name: migration_class_name unless only_submodules? 67 | 68 | if submodules 69 | submodules.each do |submodule| 70 | unless submodule == "http_basic_auth" || submodule == "session_timeout" || submodule == "core" 71 | migration_template "migration/#{submodule}.rb", "db/migrate/sorcery_#{submodule}.rb", migration_class_name: migration_class_name 72 | end 73 | end 74 | end 75 | 76 | end 77 | 78 | # Define the next_migration_number method (necessary for the migration_template method to work) 79 | def self.next_migration_number(dirname) 80 | if ActiveRecord::Base.timestamped_migrations 81 | sleep 1 # make sure each time we get a different timestamp 82 | Time.new.utc.strftime("%Y%m%d%H%M%S") 83 | else 84 | "%.3d" % (current_migration_number(dirname) + 1) 85 | end 86 | end 87 | 88 | private 89 | def only_submodules? 90 | options[:migrations] || options[:only_submodules] 91 | end 92 | 93 | def migration_class_name 94 | if Rails::VERSION::MAJOR >= 5 95 | "ActiveRecord::Migration[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]" 96 | else 97 | "ActiveRecord::Migration" 98 | end 99 | end 100 | 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/generators/sorcery/templates/migration/activity_logging.rb: -------------------------------------------------------------------------------- 1 | class SorceryActivityLogging < <%= migration_class_name %> 2 | def change 3 | add_column :<%= model_class_name.tableize %>, :last_login_at, :datetime, :default => nil 4 | add_column :<%= model_class_name.tableize %>, :last_logout_at, :datetime, :default => nil 5 | add_column :<%= model_class_name.tableize %>, :last_activity_at, :datetime, :default => nil 6 | add_column :<%= model_class_name.tableize %>, :last_login_from_ip_address, :string, :default => nil 7 | 8 | add_index :<%= model_class_name.tableize %>, [: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 :<%= model_class_name.tableize %>, :failed_logins_count, :integer, :default => 0 4 | add_column :<%= model_class_name.tableize %>, :lock_expires_at, :datetime, :default => nil 5 | add_column :<%= model_class_name.tableize %>, :unlock_token, :string, :default => nil 6 | 7 | add_index :<%= model_class_name.tableize %>, :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 :<%= model_class_name.tableize %> do |t| 4 | t.string :email, :null => false 5 | t.string :crypted_password 6 | t.string :salt 7 | 8 | t.timestamps :null => false 9 | end 10 | 11 | add_index :<%= model_class_name.tableize %>, :email, unique: true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /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 :<%= model_class_name.tableize.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/remember_me.rb: -------------------------------------------------------------------------------- 1 | class SorceryRememberMe < <%= migration_class_name %> 2 | def change 3 | add_column :<%= model_class_name.tableize %>, :remember_me_token, :string, :default => nil 4 | add_column :<%= model_class_name.tableize %>, :remember_me_token_expires_at, :datetime, :default => nil 5 | 6 | add_index :<%= model_class_name.tableize %>, :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 :<%= model_class_name.tableize %>, :reset_password_token, :string, :default => nil 4 | add_column :<%= model_class_name.tableize %>, :reset_password_token_expires_at, :datetime, :default => nil 5 | add_column :<%= model_class_name.tableize %>, :reset_password_email_sent_at, :datetime, :default => nil 6 | 7 | add_index :<%= model_class_name.tableize %>, :reset_password_token 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/generators/sorcery/templates/migration/user_activation.rb: -------------------------------------------------------------------------------- 1 | class SorceryUserActivation < <%= migration_class_name %> 2 | def change 3 | add_column :<%= model_class_name.tableize %>, :activation_state, :string, :default => nil 4 | add_column :<%= model_class_name.tableize %>, :activation_token, :string, :default => nil 5 | add_column :<%= model_class_name.tableize %>, :activation_token_expires_at, :datetime, :default => nil 6 | 7 | add_index :<%= model_class_name.tableize %>, :activation_token 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/sorcery.rb: -------------------------------------------------------------------------------- 1 | require 'sorcery/version' 2 | 3 | module Sorcery 4 | 5 | require 'sorcery/model' 6 | 7 | module Adapters 8 | require 'sorcery/adapters/base_adapter' 9 | end 10 | 11 | module Model 12 | require 'sorcery/model/temporary_token' 13 | require 'sorcery/model/config' 14 | 15 | 16 | module Submodules 17 | require 'sorcery/model/submodules/user_activation' 18 | require 'sorcery/model/submodules/reset_password' 19 | require 'sorcery/model/submodules/remember_me' 20 | require 'sorcery/model/submodules/activity_logging' 21 | require 'sorcery/model/submodules/brute_force_protection' 22 | require 'sorcery/model/submodules/external' 23 | end 24 | end 25 | 26 | require 'sorcery/controller' 27 | 28 | module Controller 29 | autoload :Config, 'sorcery/controller/config' 30 | module Submodules 31 | require 'sorcery/controller/submodules/remember_me' 32 | require 'sorcery/controller/submodules/session_timeout' 33 | require 'sorcery/controller/submodules/brute_force_protection' 34 | require 'sorcery/controller/submodules/http_basic_auth' 35 | require 'sorcery/controller/submodules/activity_logging' 36 | require 'sorcery/controller/submodules/external' 37 | end 38 | end 39 | 40 | module Protocols 41 | require 'sorcery/protocols/oauth' 42 | require 'sorcery/protocols/oauth2' 43 | end 44 | 45 | module CryptoProviders 46 | require 'sorcery/crypto_providers/common' 47 | require 'sorcery/crypto_providers/aes256' 48 | require 'sorcery/crypto_providers/bcrypt' 49 | require 'sorcery/crypto_providers/md5' 50 | require 'sorcery/crypto_providers/sha1' 51 | require 'sorcery/crypto_providers/sha256' 52 | require 'sorcery/crypto_providers/sha512' 53 | end 54 | 55 | module TestHelpers 56 | require 'sorcery/test_helpers/internal' 57 | 58 | module Rails 59 | require 'sorcery/test_helpers/rails/controller' 60 | require 'sorcery/test_helpers/rails/integration' 61 | end 62 | 63 | module Internal 64 | require 'sorcery/test_helpers/internal/rails' 65 | end 66 | 67 | end 68 | 69 | require 'sorcery/adapters/base_adapter' 70 | 71 | if defined?(ActiveRecord) 72 | require 'sorcery/adapters/active_record_adapter' 73 | ActiveRecord::Base.extend Sorcery::Model 74 | 75 | ActiveRecord::Base.send :define_method, :sorcery_adapter do 76 | @sorcery_adapter ||= Sorcery::Adapters::ActiveRecordAdapter.new(self) 77 | end 78 | 79 | ActiveRecord::Base.send :define_singleton_method, :sorcery_adapter do 80 | Sorcery::Adapters::ActiveRecordAdapter.from(self) 81 | end 82 | end 83 | 84 | require 'sorcery/engine' if defined?(Rails) 85 | end 86 | -------------------------------------------------------------------------------- /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 | @model.class.where(:"#{primary_key}" => @model.send(:"#{primary_key}")).update_all(attrs) 10 | end 11 | 12 | def save(options = {}) 13 | mthd = options.delete(:raise_on_failure) ? :save! : :save 14 | @model.send(mthd, options) 15 | end 16 | 17 | def increment(field) 18 | @model.increment!(field) 19 | end 20 | 21 | def find_authentication_by_oauth_credentials(relation_name, provider, uid) 22 | @user_config ||= ::Sorcery::Controller::Config.user_class.to_s.constantize.sorcery_config 23 | conditions = { 24 | @user_config.provider_uid_attribute_name => uid, 25 | @user_config.provider_attribute_name => provider 26 | } 27 | 28 | @model.public_send(relation_name).where(conditions).first 29 | end 30 | 31 | class << self 32 | def define_field(name, type, options={}) 33 | # AR fields are defined through migrations, only validator here 34 | end 35 | 36 | def define_callback(time, event, method_name, options={}) 37 | @klass.send "#{time}_#{event}", method_name, options.slice(:if) 38 | end 39 | 40 | def find_by_oauth_credentials(provider, uid) 41 | @user_config ||= ::Sorcery::Controller::Config.user_class.to_s.constantize.sorcery_config 42 | conditions = { 43 | @user_config.provider_uid_attribute_name => uid, 44 | @user_config.provider_attribute_name => provider 45 | } 46 | 47 | @klass.where(conditions).first 48 | end 49 | 50 | def find_by_remember_me_token(token) 51 | @klass.where(@klass.sorcery_config.remember_me_token_attribute_name => token).first 52 | end 53 | 54 | def find_by_credentials(credentials) 55 | relation = nil 56 | 57 | @klass.sorcery_config.username_attribute_names.each do |attribute| 58 | if @klass.sorcery_config.downcase_username_before_authenticating 59 | condition = @klass.arel_table[attribute].lower.eq(@klass.arel_table.lower(credentials[0])) 60 | else 61 | condition = @klass.arel_table[attribute].eq(credentials[0]) 62 | end 63 | 64 | if relation.nil? 65 | relation = condition 66 | else 67 | relation = relation.or(condition) 68 | end 69 | end 70 | 71 | @klass.where(relation).first 72 | end 73 | 74 | def find_by_token(token_attr_name, token) 75 | condition = @klass.arel_table[token_attr_name].eq(token) 76 | 77 | @klass.where(condition).first 78 | end 79 | 80 | def find_by_activation_token(token) 81 | @klass.where(@klass.sorcery_config.activation_token_attribute_name => token).first 82 | end 83 | 84 | def find_by_id(id) 85 | @klass.find_by_id(id) 86 | end 87 | 88 | def find_by_username(username) 89 | @klass.sorcery_config.username_attribute_names.each do |attribute| 90 | if @klass.sorcery_config.downcase_username_before_authenticating 91 | username = username.downcase 92 | end 93 | 94 | result = @klass.where(attribute => username).first 95 | return result if result 96 | end 97 | end 98 | 99 | def find_by_email(email) 100 | @klass.where(@klass.sorcery_config.email_attribute_name => email).first 101 | end 102 | 103 | def transaction(&blk) 104 | @klass.tap(&blk) 105 | end 106 | end 107 | end 108 | 109 | 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /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/controller.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Controller 3 | def self.included(klass) 4 | klass.class_eval do 5 | include InstanceMethods 6 | Config.submodules.each do |mod| 7 | begin 8 | include Submodules.const_get(mod.to_s.split('_').map { |p| p.capitalize }.join) 9 | rescue NameError 10 | # don't stop on a missing submodule. 11 | end 12 | end 13 | end 14 | Config.update! 15 | Config.configure! 16 | end 17 | 18 | module InstanceMethods 19 | # To be used as before_action. 20 | # Will trigger auto-login attempts via the call to logged_in? 21 | # If all attempts to auto-login fail, the failure callback will be called. 22 | def require_login 23 | if !logged_in? 24 | session[:return_to_url] = request.url if Config.save_return_to_url && request.get? && !request.xhr? 25 | self.send(Config.not_authenticated_action) 26 | end 27 | end 28 | 29 | # Takes credentials and returns a user on successful authentication. 30 | # Runs hooks after login or failed login. 31 | def login(*credentials) 32 | @current_user = nil 33 | user = user_class.authenticate(*credentials) 34 | if user 35 | old_session = session.dup.to_hash 36 | reset_sorcery_session 37 | old_session.each_pair do |k,v| 38 | session[k.to_sym] = v 39 | end 40 | form_authenticity_token 41 | 42 | auto_login(user) 43 | after_login!(user, credentials) 44 | current_user 45 | else 46 | after_failed_login!(credentials) 47 | nil 48 | end 49 | end 50 | 51 | # put this into the catch block to rescue undefined method `destroy_session' 52 | # hotfix for https://github.com/NoamB/sorcery/issues/464 53 | # can be removed when Rails 4.1 is out 54 | def reset_sorcery_session 55 | reset_session # protect from session fixation attacks 56 | rescue NoMethodError 57 | end 58 | 59 | # Resets the session and runs hooks before and after. 60 | def logout 61 | if logged_in? 62 | user = current_user 63 | before_logout! 64 | @current_user = nil 65 | reset_sorcery_session 66 | after_logout!(user) 67 | end 68 | end 69 | 70 | def logged_in? 71 | !!current_user 72 | end 73 | 74 | # attempts to auto-login from the sources defined (session, basic_auth, cookie, etc.) 75 | # returns the logged in user if found, nil if not 76 | def current_user 77 | unless defined?(@current_user) 78 | @current_user = login_from_session || login_from_other_sources || nil 79 | end 80 | @current_user 81 | end 82 | 83 | def current_user=(user) 84 | @current_user = user 85 | end 86 | 87 | # used when a user tries to access a page while logged out, is asked to login, 88 | # and we want to return him back to the page he originally wanted. 89 | def redirect_back_or_to(url, flash_hash = {}) 90 | redirect_to(session[:return_to_url] || url, :flash => flash_hash) 91 | session[:return_to_url] = nil 92 | end 93 | 94 | # The default action for denying non-authenticated users. 95 | # You can override this method in your controllers, 96 | # or provide a different method in the configuration. 97 | def not_authenticated 98 | redirect_to root_path 99 | end 100 | 101 | # login a user instance 102 | # 103 | # @param [] user the user instance. 104 | # @return - do not depend on the return value. 105 | def auto_login(user, should_remember = false) 106 | session[:user_id] = user.id.to_s 107 | @current_user = user 108 | end 109 | 110 | # Overwrite Rails' handle unverified request 111 | def handle_unverified_request 112 | cookies[:remember_me_token] = nil 113 | @current_user = nil 114 | super # call the default behaviour which resets the session 115 | end 116 | 117 | protected 118 | 119 | # Tries all available sources (methods) until one doesn't return false. 120 | def login_from_other_sources 121 | result = nil 122 | Config.login_sources.find do |source| 123 | result = send(source) 124 | end 125 | result || false 126 | end 127 | 128 | def login_from_session 129 | @current_user = if session[:user_id] 130 | user_class.sorcery_adapter.find_by_id(session[:user_id]) 131 | end 132 | end 133 | 134 | def after_login!(user, credentials = []) 135 | Config.after_login.each {|c| self.send(c, user, credentials)} 136 | end 137 | 138 | def after_failed_login!(credentials) 139 | Config.after_failed_login.each {|c| self.send(c, credentials)} 140 | end 141 | 142 | def before_logout! 143 | Config.before_logout.each {|c| self.send(c)} 144 | end 145 | 146 | def after_logout!(user) 147 | Config.after_logout.each {|c| self.send(c, user)} 148 | end 149 | 150 | def user_class 151 | @user_class ||= Config.user_class.to_s.constantize 152 | end 153 | 154 | end 155 | 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /lib/sorcery/controller/config.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Controller 3 | module Config 4 | class << self 5 | attr_accessor :submodules, 6 | :user_class, # what class to use as the user class. 7 | :not_authenticated_action, # what controller action to call for non-authenticated users. 8 | 9 | :save_return_to_url, # when a non logged in user tries to enter a page that requires 10 | # login, save the URL he wanted to reach, 11 | # and send him there after login. 12 | 13 | :cookie_domain, # set domain option for cookies 14 | 15 | :login_sources, 16 | :after_login, 17 | :after_failed_login, 18 | :before_logout, 19 | :after_logout 20 | 21 | def init! 22 | @defaults = { 23 | :@user_class => nil, 24 | :@submodules => [], 25 | :@not_authenticated_action => :not_authenticated, 26 | :@login_sources => [], 27 | :@after_login => [], 28 | :@after_failed_login => [], 29 | :@before_logout => [], 30 | :@after_logout => [], 31 | :@save_return_to_url => true, 32 | :@cookie_domain => nil 33 | } 34 | end 35 | 36 | # Resets all configuration options to their default values. 37 | def reset! 38 | @defaults.each do |k,v| 39 | instance_variable_set(k,v) 40 | end 41 | end 42 | 43 | def update! 44 | @defaults.each do |k,v| 45 | instance_variable_set(k,v) if !instance_variable_defined?(k) 46 | end 47 | end 48 | 49 | def user_config(&blk) 50 | block_given? ? @user_config = blk : @user_config 51 | end 52 | 53 | def configure(&blk) 54 | @configure_blk = blk 55 | end 56 | 57 | def configure! 58 | @configure_blk.call(self) if @configure_blk 59 | end 60 | end 61 | init! 62 | reset! 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /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 | ) 30 | end 31 | end 32 | merge_activity_logging_defaults! 33 | end 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 | base.after_action :register_last_activity_time_to_db 38 | end 39 | 40 | module InstanceMethods 41 | protected 42 | 43 | # registers last login time on every login. 44 | # This runs as a hook just after a successful login. 45 | def register_login_time_to_db(user, credentials) 46 | return unless Config.register_login_time 47 | user.set_last_login_at(Time.now.in_time_zone) 48 | end 49 | 50 | # registers last logout time on every logout. 51 | # This runs as a hook just before a logout. 52 | def register_logout_time_to_db 53 | return unless Config.register_logout_time 54 | current_user.set_last_logout_at(Time.now.in_time_zone) 55 | end 56 | 57 | # Updates last activity time on every request. 58 | # The only exception is logout - we do not update activity on logout 59 | def register_last_activity_time_to_db 60 | return unless Config.register_last_activity_time 61 | return unless logged_in? 62 | current_user.set_last_activity_at(Time.now.in_time_zone) 63 | end 64 | 65 | # Updates IP address on every login. 66 | # This runs as a hook just after a successful login. 67 | def register_last_ip_address(user, credentials) 68 | return unless Config.register_last_ip_address 69 | current_user.set_last_ip_address(request.remote_ip) 70 | end 71 | end 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /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 | 20 | protected 21 | 22 | # Increments the failed logins counter on every failed login. 23 | # Runs as a hook after a failed login. 24 | def update_failed_logins_count!(credentials) 25 | user = user_class.sorcery_adapter.find_by_credentials(credentials) 26 | user.register_failed_login! if user 27 | end 28 | 29 | # Resets the failed logins counter. 30 | # Runs as a hook after a successful login. 31 | def reset_failed_logins_count!(user, credentials) 32 | user.sorcery_adapter.update_attribute(user_class.sorcery_config.failed_logins_count_attribute_name, 0) 33 | end 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /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 | Config.login_sources << :login_from_basic_auth 23 | end 24 | 25 | module InstanceMethods 26 | 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) and (session[:http_authentication_used] = true) and 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 | current_controller = current_controller.superclass 62 | end 63 | nil 64 | else 65 | Config.controller_to_realm_map["application"] 66 | end 67 | end 68 | 69 | end 70 | 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /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 | Config.login_sources << :login_from_cookie 21 | Config.after_login << :remember_me_if_asked_to 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 | # calls remember_me! if a third credential was passed to the login method. 55 | # Runs as a hook after login. 56 | def remember_me_if_asked_to(user, credentials) 57 | remember_me! if ( credentials.size == 3 && credentials[2] && credentials[2] != "0" ) 58 | end 59 | 60 | # Checks the cookie for a remember me token, tried to find a user with that token 61 | # and logs the user in if found. 62 | # Runs as a login source. See 'current_user' method for how it is used. 63 | def login_from_cookie 64 | user = cookies.signed[:remember_me_token] && user_class.sorcery_adapter.find_by_remember_me_token(cookies.signed[:remember_me_token]) 65 | if user && user.has_remember_me_token? 66 | set_remember_me_cookie!(user) 67 | session[:user_id] = user.id.to_s 68 | @current_user = user 69 | else 70 | @current_user = false 71 | end 72 | end 73 | 74 | def set_remember_me_cookie!(user) 75 | cookies.signed[:remember_me_token] = { 76 | :value => user.send(user.sorcery_config.remember_me_token_attribute_name), 77 | :expires => user.send(user.sorcery_config.remember_me_token_expires_at_attribute_name), 78 | :httponly => Config.remember_me_httponly, 79 | :domain => Config.cookie_domain 80 | } 81 | end 82 | end 83 | 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /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 | attr_accessor :session_timeout, # how long in seconds to keep the session alive. 12 | 13 | :session_timeout_from_last_action # use the last action as the beginning of session 14 | # timeout. 15 | 16 | def merge_session_timeout_defaults! 17 | @defaults.merge!(:@session_timeout => 3600, # 1.hour 18 | :@session_timeout_from_last_action => false) 19 | end 20 | end 21 | merge_session_timeout_defaults! 22 | end 23 | Config.after_login << :register_login_time 24 | base.prepend_before_action :validate_session 25 | end 26 | 27 | module InstanceMethods 28 | protected 29 | 30 | # Registers last login to be used as the timeout starting point. 31 | # Runs as a hook after a successful login. 32 | def register_login_time(user, credentials) 33 | session[:login_time] = session[:last_action_time] = Time.now.in_time_zone 34 | end 35 | 36 | # Checks if session timeout was reached and expires the current session if so. 37 | # To be used as a before_action, before require_login 38 | def validate_session 39 | session_to_use = Config.session_timeout_from_last_action ? session[:last_action_time] : session[:login_time] 40 | if session_to_use && sorcery_session_expired?(session_to_use.to_time) 41 | reset_sorcery_session 42 | @current_user = nil 43 | else 44 | session[:last_action_time] = Time.now.in_time_zone 45 | end 46 | end 47 | 48 | def sorcery_session_expired?(time) 49 | Time.now.in_time_zone - time > Config.session_timeout 50 | end 51 | 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /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::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.new("#{name} expects a 32 bytes long key. Please use Sorcery::Model::Config.encryption_key to set it.") if ( @key.nil? || @key == "" ) 46 | @aes ||= OpenSSL::Cipher::Cipher.new("AES-256-ECB") 47 | end 48 | end 49 | end 50 | end 51 | end -------------------------------------------------------------------------------- /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 | # This is the :cost option for the BCrpyt library. 44 | # The higher the cost the more secure it is and the longer is take the generate a hash. By default this is 10. 45 | # Set this to whatever you want, play around with it to get that perfect balance between 46 | # security and performance. 47 | def cost 48 | @cost ||= 10 49 | end 50 | attr_writer :cost 51 | alias :stretches :cost 52 | alias :stretches= :cost= 53 | 54 | # Creates a BCrypt hash for the password passed. 55 | def encrypt(*tokens) 56 | ::BCrypt::Password.create(join_tokens(tokens), :cost => cost) 57 | end 58 | 59 | # Does the hash match the tokens? Uses the same tokens that were used to encrypt. 60 | def matches?(hash, *tokens) 61 | hash = new_from_hash(hash) 62 | return false if hash.nil? || hash == {} 63 | hash == join_tokens(tokens) 64 | end 65 | 66 | # This method is used as a flag to tell Sorcery to "resave" the password 67 | # upon a successful login, using the new cost 68 | def cost_matches?(hash) 69 | hash = new_from_hash(hash) 70 | if hash.nil? || hash == {} 71 | false 72 | else 73 | hash.cost == cost 74 | end 75 | end 76 | 77 | def reset! 78 | @cost = 10 79 | end 80 | 81 | private 82 | 83 | def join_tokens(tokens) 84 | tokens.flatten.join 85 | end 86 | 87 | def new_from_hash(hash) 88 | begin 89 | ::BCrypt::Password.new(hash) 90 | rescue ::BCrypt::Errors::InvalidHash 91 | return nil 92 | end 93 | end 94 | end 95 | end 96 | end 97 | end -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | initializer "extend Controller with sorcery" do |app| 11 | ActionController::Base.send(:include, Sorcery::Controller) 12 | ActionController::Base.helper_method :current_user 13 | ActionController::Base.helper_method :logged_in? 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/sorcery/model.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | # This module handles all plugin operations which are related to the Model layer in the MVC pattern. 3 | # It should be included into the ORM base class. 4 | # In the case of Rails this is usually ActiveRecord (actually, in that case, the plugin does this automatically). 5 | # 6 | # When included it defines a single method: 'authenticates_with_sorcery!' 7 | # which when called adds the other capabilities to the class. 8 | # This method is also the place to configure the plugin in the Model layer. 9 | module Model 10 | def authenticates_with_sorcery! 11 | @sorcery_config = Config.new 12 | 13 | extend ClassMethods # included here, before submodules, so they can be overriden by them. 14 | include InstanceMethods 15 | include TemporaryToken 16 | 17 | include_required_submodules! 18 | 19 | # This runs the options block set in the initializer on the model class. 20 | ::Sorcery::Controller::Config.user_config.tap{|blk| blk.call(@sorcery_config) if blk} 21 | 22 | define_base_fields 23 | init_orm_hooks! 24 | 25 | @sorcery_config.after_config << :add_config_inheritance if @sorcery_config.subclasses_inherit_config 26 | @sorcery_config.after_config.each { |c| send(c) } 27 | end 28 | 29 | private 30 | 31 | def define_base_fields 32 | self.class_eval do 33 | sorcery_config.username_attribute_names.each do |username| 34 | sorcery_adapter.define_field username, String, length: 255 35 | end 36 | unless sorcery_config.username_attribute_names.include?(sorcery_config.email_attribute_name) 37 | sorcery_adapter.define_field sorcery_config.email_attribute_name, String, length: 255 38 | end 39 | sorcery_adapter.define_field sorcery_config.crypted_password_attribute_name, String, length: 255 40 | sorcery_adapter.define_field sorcery_config.salt_attribute_name, String, length: 255 41 | end 42 | 43 | end 44 | 45 | # includes required submodules into the model class, 46 | # which usually is called User. 47 | def include_required_submodules! 48 | self.class_eval do 49 | @sorcery_config.submodules = ::Sorcery::Controller::Config.submodules 50 | @sorcery_config.submodules.each do |mod| 51 | begin 52 | include Submodules.const_get(mod.to_s.split('_').map {|p| p.capitalize}.join) 53 | rescue NameError 54 | # don't stop on a missing submodule. Needed because some submodules are only defined 55 | # in the controller side. 56 | end 57 | end 58 | end 59 | end 60 | 61 | # add virtual password accessor and ORM callbacks. 62 | def init_orm_hooks! 63 | sorcery_adapter.define_callback :before, :validation, :encrypt_password, if: Proc.new {|record| 64 | record.send(sorcery_config.password_attribute_name).present? 65 | } 66 | 67 | sorcery_adapter.define_callback :after, :save, :clear_virtual_password, if: Proc.new {|record| 68 | record.send(sorcery_config.password_attribute_name).present? 69 | } 70 | 71 | attr_accessor sorcery_config.password_attribute_name 72 | end 73 | 74 | module ClassMethods 75 | # Returns the class instance variable for configuration, when called by the class itself. 76 | def sorcery_config 77 | @sorcery_config 78 | end 79 | 80 | # The default authentication method. 81 | # Takes a username and password, 82 | # Finds the user by the username and compares the user's password to the one supplied to the method. 83 | # returns the user if success, nil otherwise. 84 | def authenticate(*credentials) 85 | raise ArgumentError, "at least 2 arguments required" if credentials.size < 2 86 | 87 | return false if credentials[0].blank? 88 | 89 | if @sorcery_config.downcase_username_before_authenticating 90 | credentials[0].downcase! 91 | end 92 | 93 | user = sorcery_adapter.find_by_credentials(credentials) 94 | 95 | if user.respond_to?(:active_for_authentication?) 96 | return nil if !user.active_for_authentication? 97 | end 98 | 99 | set_encryption_attributes 100 | 101 | user if user && @sorcery_config.before_authenticate.all? {|c| user.send(c)} && user.valid_password?(credentials[1]) 102 | end 103 | 104 | # encrypt tokens using current encryption_provider. 105 | def encrypt(*tokens) 106 | return tokens.first if @sorcery_config.encryption_provider.nil? 107 | 108 | set_encryption_attributes() 109 | 110 | CryptoProviders::AES256.key = @sorcery_config.encryption_key 111 | @sorcery_config.encryption_provider.encrypt(*tokens) 112 | end 113 | 114 | protected 115 | 116 | def set_encryption_attributes() 117 | @sorcery_config.encryption_provider.stretches = @sorcery_config.stretches if @sorcery_config.encryption_provider.respond_to?(:stretches) && @sorcery_config.stretches 118 | @sorcery_config.encryption_provider.join_token = @sorcery_config.salt_join_token if @sorcery_config.encryption_provider.respond_to?(:join_token) && @sorcery_config.salt_join_token 119 | end 120 | 121 | def add_config_inheritance 122 | self.class_eval do 123 | def self.inherited(subclass) 124 | subclass.class_eval do 125 | class << self 126 | attr_accessor :sorcery_config 127 | end 128 | end 129 | subclass.sorcery_config = sorcery_config 130 | super 131 | end 132 | end 133 | end 134 | 135 | end 136 | 137 | module InstanceMethods 138 | # Returns the class instance variable for configuration, when called by an instance. 139 | def sorcery_config 140 | self.class.sorcery_config 141 | end 142 | 143 | # identifies whether this user is regular, i.e. we hold his credentials in our db, 144 | # or that he is external, and his credentials are saved elsewhere (twitter/facebook etc.). 145 | def external? 146 | send(sorcery_config.crypted_password_attribute_name).nil? 147 | end 148 | 149 | # Calls the configured encryption provider to compare the supplied password with the encrypted one. 150 | def valid_password?(pass) 151 | _crypted = self.send(sorcery_config.crypted_password_attribute_name) 152 | return _crypted == pass if sorcery_config.encryption_provider.nil? 153 | 154 | _salt = self.send(sorcery_config.salt_attribute_name) unless sorcery_config.salt_attribute_name.nil? 155 | 156 | sorcery_config.encryption_provider.matches?(_crypted, pass, _salt) 157 | end 158 | 159 | protected 160 | 161 | # creates new salt and saves it. 162 | # encrypts password with salt and saves it. 163 | def encrypt_password 164 | config = sorcery_config 165 | self.send(:"#{config.salt_attribute_name}=", new_salt = TemporaryToken.generate_random_token) if !config.salt_attribute_name.nil? 166 | self.send(:"#{config.crypted_password_attribute_name}=", self.class.encrypt(self.send(config.password_attribute_name),new_salt)) 167 | end 168 | 169 | def clear_virtual_password 170 | config = sorcery_config 171 | self.send(:"#{config.password_attribute_name}=", nil) 172 | 173 | if respond_to?(:"#{config.password_attribute_name}_confirmation=") 174 | self.send(:"#{config.password_attribute_name}_confirmation=", nil) 175 | end 176 | end 177 | 178 | # calls the requested email method on the configured mailer 179 | # supports both the ActionMailer 3 way of calling, and the plain old Ruby object way. 180 | def generic_send_email(method, mailer) 181 | config = sorcery_config 182 | mail = config.send(mailer).send(config.send(method),self) 183 | if defined?(ActionMailer) and config.send(mailer).kind_of?(Class) and config.send(mailer) < ActionMailer::Base 184 | mail.send(config.email_delivery_method) 185 | end 186 | end 187 | end 188 | 189 | end 190 | end 191 | -------------------------------------------------------------------------------- /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 | 8 | attr_accessor :username_attribute_names, # change default username attribute, for example, to use :email 9 | # as the login. 10 | 11 | :password_attribute_name, # change *virtual* password attribute, the one which is used 12 | # until an encrypted one is generated. 13 | 14 | :email_attribute_name, # change default email attribute. 15 | 16 | :downcase_username_before_authenticating, # downcase the username before trying to authenticate, default is false 17 | 18 | :crypted_password_attribute_name, # change default crypted_password attribute. 19 | :salt_join_token, # what pattern to use to join the password with the salt 20 | :salt_attribute_name, # change default salt attribute. 21 | :stretches, # how many times to apply encryption to the password. 22 | :encryption_key, # encryption key used to encrypt reversible encryptions such as 23 | # AES256. 24 | 25 | :subclasses_inherit_config, # make this configuration inheritable for subclasses. Useful for 26 | # ActiveRecord's STI. 27 | 28 | :submodules, # configured in config/application.rb 29 | :before_authenticate, # an array of method names to call before authentication 30 | # completes. used internally. 31 | 32 | :email_delivery_method, # method to send email related 33 | # options: `:deliver_later`, `:deliver_now`, `:deliver` 34 | # Default: :deliver (Rails version < 4.2) or :deliver_now (Rails version 4.2+) 35 | # method to send email related 36 | 37 | 38 | :after_config # an array of method names to call after configuration by user. 39 | # used internally. 40 | 41 | attr_reader :encryption_provider, # change default encryption_provider. 42 | :custom_encryption_provider, # use an external encryption class. 43 | :encryption_algorithm # encryption algorithm name. See 'encryption_algorithm=' below 44 | # for available options. 45 | def initialize 46 | @defaults = { 47 | :@submodules => [], 48 | :@username_attribute_names => [:email], 49 | :@password_attribute_name => :password, 50 | :@downcase_username_before_authenticating => false, 51 | :@email_attribute_name => :email, 52 | :@crypted_password_attribute_name => :crypted_password, 53 | :@encryption_algorithm => :bcrypt, 54 | :@encryption_provider => CryptoProviders::BCrypt, 55 | :@custom_encryption_provider => nil, 56 | :@encryption_key => nil, 57 | :@salt_join_token => "", 58 | :@salt_attribute_name => :salt, 59 | :@stretches => nil, 60 | :@subclasses_inherit_config => false, 61 | :@before_authenticate => [], 62 | :@after_config => [], 63 | :@email_delivery_method => default_email_delivery_method, 64 | } 65 | reset! 66 | end 67 | 68 | # Resets all configuration options to their default values. 69 | def reset! 70 | @defaults.each do |k,v| 71 | instance_variable_set(k,v) 72 | end 73 | end 74 | 75 | def username_attribute_names=(fields) 76 | @username_attribute_names = fields.kind_of?(Array) ? fields : [fields] 77 | end 78 | 79 | def custom_encryption_provider=(provider) 80 | @custom_encryption_provider = @encryption_provider = provider 81 | end 82 | 83 | def encryption_algorithm=(algo) 84 | @encryption_algorithm = algo 85 | @encryption_provider = case @encryption_algorithm.to_sym 86 | when :none then nil 87 | when :md5 then CryptoProviders::MD5 88 | when :sha1 then CryptoProviders::SHA1 89 | when :sha256 then CryptoProviders::SHA256 90 | when :sha512 then CryptoProviders::SHA512 91 | when :aes256 then CryptoProviders::AES256 92 | when :bcrypt then CryptoProviders::BCrypt 93 | when :custom then @custom_encryption_provider 94 | else raise ArgumentError.new("Encryption algorithm supplied, #{algo}, is invalid") 95 | end 96 | end 97 | 98 | private 99 | def default_email_delivery_method 100 | # Rails 4.2 deprecates #deliver 101 | rails_version_bigger_than_or_equal?('4.2.0') ? :deliver_now : :deliver 102 | end 103 | 104 | def rails_version_bigger_than_or_equal?(version) 105 | Gem::Version.new(version) <= Gem::Version.new(Rails.version) 106 | end 107 | 108 | end 109 | 110 | end 111 | end 112 | 113 | -------------------------------------------------------------------------------- /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 | attr_accessor :last_login_at_attribute_name, # last login attribute name. 16 | :last_logout_at_attribute_name, # last logout attribute name. 17 | :last_activity_at_attribute_name, # last activity attribute name. 18 | :last_login_from_ip_address_name, # last activity login source 19 | :activity_timeout # how long since last activity is 20 | # the user defined offline 21 | end 22 | 23 | base.sorcery_config.instance_eval do 24 | @defaults.merge!(:@last_login_at_attribute_name => :last_login_at, 25 | :@last_logout_at_attribute_name => :last_logout_at, 26 | :@last_activity_at_attribute_name => :last_activity_at, 27 | :@last_login_from_ip_address_name => :last_login_from_ip_address, 28 | :@activity_timeout => 10 * 60) 29 | reset! 30 | end 31 | 32 | base.sorcery_config.after_config << :define_activity_logging_fields 33 | end 34 | 35 | module InstanceMethods 36 | def set_last_login_at(time) 37 | sorcery_adapter.update_attribute(sorcery_config.last_login_at_attribute_name, time) 38 | end 39 | 40 | def set_last_logout_at(time) 41 | sorcery_adapter.update_attribute(sorcery_config.last_logout_at_attribute_name, time) 42 | end 43 | 44 | def set_last_activity_at(time) 45 | sorcery_adapter.update_attribute(sorcery_config.last_activity_at_attribute_name, time) 46 | end 47 | 48 | def set_last_ip_address(ip_address) 49 | sorcery_adapter.update_attribute(sorcery_config.last_login_from_ip_address_name, ip_address) 50 | end 51 | 52 | # online method shows if user is active (logout action makes user inactive too) 53 | def online? 54 | return false if self.send(sorcery_config.last_activity_at_attribute_name).nil? 55 | 56 | logged_in? and self.send(sorcery_config.last_activity_at_attribute_name) > sorcery_config.activity_timeout.seconds.ago 57 | end 58 | 59 | # shows if user is logged in, but it not show if user is online - see online? 60 | def logged_in? 61 | return false if self.send(sorcery_config.last_login_at_attribute_name).nil? 62 | return true if self.send(sorcery_config.last_login_at_attribute_name).present? and self.send(sorcery_config.last_logout_at_attribute_name).nil? 63 | 64 | self.send(sorcery_config.last_login_at_attribute_name) > self.send(sorcery_config.last_logout_at_attribute_name) 65 | end 66 | 67 | def logged_out? 68 | not logged_in? 69 | end 70 | 71 | end 72 | 73 | module ClassMethods 74 | 75 | protected 76 | def define_activity_logging_fields 77 | sorcery_adapter.define_field sorcery_config.last_login_at_attribute_name, Time 78 | sorcery_adapter.define_field sorcery_config.last_logout_at_attribute_name, Time 79 | sorcery_adapter.define_field sorcery_config.last_activity_at_attribute_name, Time 80 | sorcery_adapter.define_field sorcery_config.last_login_from_ip_address_name, String 81 | end 82 | end 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/sorcery/model/submodules/brute_force_protection.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Model 3 | module Submodules 4 | # This module helps protect user accounts by locking them down after too many failed attemps 5 | # to login were detected. 6 | # This is the model part of the submodule which provides configuration options and methods 7 | # for locking and unlocking the user. 8 | module BruteForceProtection 9 | def self.included(base) 10 | base.sorcery_config.class_eval do 11 | attr_accessor :failed_logins_count_attribute_name, # failed logins attribute name. 12 | :lock_expires_at_attribute_name, # this field indicates whether user 13 | # is banned and when it will be active again. 14 | :consecutive_login_retries_amount_limit, # how many failed logins allowed. 15 | :login_lock_time_period, # how long the user should be banned. 16 | # in seconds. 0 for permanent. 17 | 18 | :unlock_token_attribute_name, # Unlock token attribute name 19 | :unlock_token_email_method_name, # Mailer method name 20 | :unlock_token_mailer_disabled, # When true, dont send unlock token via email 21 | :unlock_token_mailer # Mailer class 22 | end 23 | 24 | base.sorcery_config.instance_eval do 25 | @defaults.merge!(:@failed_logins_count_attribute_name => :failed_logins_count, 26 | :@lock_expires_at_attribute_name => :lock_expires_at, 27 | :@consecutive_login_retries_amount_limit => 50, 28 | :@login_lock_time_period => 60 * 60, 29 | 30 | :@unlock_token_attribute_name => :unlock_token, 31 | :@unlock_token_email_method_name => :send_unlock_token_email, 32 | :@unlock_token_mailer_disabled => false, 33 | :@unlock_token_mailer => nil) 34 | reset! 35 | end 36 | 37 | base.sorcery_config.before_authenticate << :prevent_locked_user_login 38 | base.sorcery_config.after_config << :define_brute_force_protection_fields 39 | base.extend(ClassMethods) 40 | base.send(:include, InstanceMethods) 41 | end 42 | 43 | module ClassMethods 44 | def load_from_unlock_token(token) 45 | return nil if token.blank? 46 | user = sorcery_adapter.find_by_token(sorcery_config.unlock_token_attribute_name,token) 47 | user 48 | end 49 | 50 | protected 51 | 52 | def define_brute_force_protection_fields 53 | sorcery_adapter.define_field sorcery_config.failed_logins_count_attribute_name, Integer, :default => 0 54 | sorcery_adapter.define_field sorcery_config.lock_expires_at_attribute_name, Time 55 | sorcery_adapter.define_field sorcery_config.unlock_token_attribute_name, String 56 | end 57 | end 58 | 59 | module InstanceMethods 60 | # Called by the controller to increment the failed logins counter. 61 | # Calls 'lock!' if login retries limit was reached. 62 | def register_failed_login! 63 | config = sorcery_config 64 | return if !unlocked? 65 | 66 | sorcery_adapter.increment(config.failed_logins_count_attribute_name) 67 | 68 | if self.send(config.failed_logins_count_attribute_name) >= config.consecutive_login_retries_amount_limit 69 | lock! 70 | end 71 | end 72 | 73 | # /!\ 74 | # Moved out of protected for use like activate! in controller 75 | # /!\ 76 | def unlock! 77 | config = sorcery_config 78 | attributes = {config.lock_expires_at_attribute_name => nil, 79 | config.failed_logins_count_attribute_name => 0, 80 | config.unlock_token_attribute_name => nil} 81 | sorcery_adapter.update_attributes(attributes) 82 | end 83 | 84 | def locked? 85 | !unlocked? 86 | end 87 | 88 | protected 89 | 90 | def lock! 91 | config = sorcery_config 92 | attributes = {config.lock_expires_at_attribute_name => Time.now.in_time_zone + config.login_lock_time_period, 93 | config.unlock_token_attribute_name => TemporaryToken.generate_random_token} 94 | sorcery_adapter.update_attributes(attributes) 95 | 96 | unless config.unlock_token_mailer_disabled || config.unlock_token_mailer.nil? 97 | send_unlock_token_email! 98 | end 99 | end 100 | 101 | def unlocked? 102 | config = sorcery_config 103 | self.send(config.lock_expires_at_attribute_name).nil? 104 | end 105 | 106 | def send_unlock_token_email! 107 | return if sorcery_config.unlock_token_email_method_name.nil? 108 | 109 | generic_send_email(:unlock_token_email_method_name, :unlock_token_mailer) 110 | end 111 | 112 | # Prevents a locked user from logging in, and unlocks users that expired their lock time. 113 | # Runs as a hook before authenticate. 114 | def prevent_locked_user_login 115 | config = sorcery_config 116 | if !self.unlocked? && config.login_lock_time_period != 0 117 | self.unlock! if self.send(config.lock_expires_at_attribute_name) <= Time.now.in_time_zone 118 | end 119 | unlocked? 120 | end 121 | end 122 | end 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /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 | 24 | end 25 | 26 | base.sorcery_config.instance_eval do 27 | @defaults.merge!(:@authentications_class => nil, 28 | :@authentications_user_id_attribute_name => :user_id, 29 | :@provider_attribute_name => :provider, 30 | :@provider_uid_attribute_name => :uid) 31 | 32 | reset! 33 | end 34 | 35 | base.send(:include, InstanceMethods) 36 | base.extend(ClassMethods) 37 | 38 | end 39 | 40 | module ClassMethods 41 | # takes a provider and uid and finds a user by them. 42 | def load_from_provider(provider,uid) 43 | config = sorcery_config 44 | authentication = config.authentications_class.sorcery_adapter.find_by_oauth_credentials(provider, uid) 45 | user = sorcery_adapter.find_by_id(authentication.send(config.authentications_user_id_attribute_name)) if authentication 46 | end 47 | 48 | def create_and_validate_from_provider(provider, uid, attrs) 49 | user = new(attrs) 50 | user.send(sorcery_config.authentications_class.to_s.downcase.pluralize).build( 51 | sorcery_config.provider_uid_attribute_name => uid, 52 | sorcery_config.provider_attribute_name => provider 53 | ) 54 | saved = user.sorcery_adapter.save 55 | [user, saved] 56 | end 57 | 58 | def create_from_provider(provider, uid, attrs) 59 | user = new 60 | attrs.each do |k,v| 61 | user.send(:"#{k}=", v) 62 | end 63 | 64 | if block_given? 65 | return false unless yield user 66 | end 67 | 68 | sorcery_adapter.transaction do 69 | user.sorcery_adapter.save(:validate => false) 70 | sorcery_config.authentications_class.create!( 71 | sorcery_config.authentications_user_id_attribute_name => user.id, 72 | sorcery_config.provider_attribute_name => provider, 73 | sorcery_config.provider_uid_attribute_name => uid 74 | ) 75 | end 76 | user 77 | end 78 | end 79 | 80 | module InstanceMethods 81 | def add_provider_to_user(provider, uid) 82 | authentications = sorcery_config.authentications_class.name.underscore.pluralize 83 | # first check to see if user has a particular authentication already 84 | if sorcery_adapter.find_authentication_by_oauth_credentials(authentications, provider, uid).nil? 85 | user = send(authentications).build(sorcery_config.provider_uid_attribute_name => uid, 86 | sorcery_config.provider_attribute_name => provider) 87 | user.sorcery_adapter.save(validate: false) 88 | else 89 | user = false 90 | end 91 | 92 | user 93 | end 94 | 95 | end 96 | 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /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 | 15 | end 16 | 17 | base.sorcery_config.instance_eval do 18 | @defaults.merge!(:@remember_me_token_attribute_name => :remember_me_token, 19 | :@remember_me_token_expires_at_attribute_name => :remember_me_token_expires_at, 20 | :@remember_me_token_persist_globally => false, 21 | :@remember_me_for => 7 * 60 * 60 * 24) 22 | 23 | reset! 24 | end 25 | 26 | base.send(:include, InstanceMethods) 27 | base.sorcery_config.after_config << :define_remember_me_fields 28 | 29 | base.extend(ClassMethods) 30 | end 31 | 32 | module ClassMethods 33 | protected 34 | 35 | def define_remember_me_fields 36 | sorcery_adapter.define_field sorcery_config.remember_me_token_attribute_name, String 37 | sorcery_adapter.define_field sorcery_config.remember_me_token_expires_at_attribute_name, Time 38 | end 39 | 40 | end 41 | 42 | module InstanceMethods 43 | # You shouldn't really use this one yourself - it's called by the controller's 'remember_me!' method. 44 | def remember_me! 45 | config = sorcery_config 46 | 47 | update_options = { config.remember_me_token_expires_at_attribute_name => Time.now.in_time_zone + config.remember_me_for } 48 | 49 | unless config.remember_me_token_persist_globally and has_remember_me_token? 50 | update_options.merge!(config.remember_me_token_attribute_name => TemporaryToken.generate_random_token) 51 | end 52 | 53 | self.sorcery_adapter.update_attributes(update_options) 54 | end 55 | 56 | def has_remember_me_token? 57 | self.send(sorcery_config.remember_me_token_attribute_name).present? 58 | end 59 | 60 | # You shouldn't really use this one yourself - it's called by the controller's 'forget_me!' method. 61 | # We only clear the token value if remember_me_token_persist_globally = true. 62 | def forget_me! 63 | sorcery_config.remember_me_token_persist_globally or force_forget_me! 64 | end 65 | 66 | # You shouldn't really use this one yourself - it's called by the controller's 'force_forget_me!' method. 67 | def force_forget_me! 68 | config = sorcery_config 69 | self.sorcery_adapter.update_attributes(config.remember_me_token_attribute_name => nil, 70 | config.remember_me_token_expires_at_attribute_name => nil) 71 | end 72 | end 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/sorcery/model/submodules/reset_password.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Model 3 | module Submodules 4 | # This submodule adds the ability to reset password via email confirmation. 5 | # When the user requests an email is sent to him with a url. 6 | # The url includes a token, which is also saved with the user's record in the db. 7 | # The token has configurable expiration. 8 | # When the user clicks the url in the email, providing the token has not yet expired, 9 | # he will be able to reset his password via a form. 10 | # 11 | # When using this submodule, supplying a mailer is mandatory. 12 | module ResetPassword 13 | def self.included(base) 14 | base.sorcery_config.class_eval do 15 | attr_accessor :reset_password_token_attribute_name, # reset password code attribute name. 16 | :reset_password_token_expires_at_attribute_name, # expires at attribute name. 17 | :reset_password_email_sent_at_attribute_name, # when was email sent, used for hammering 18 | # protection. 19 | 20 | :reset_password_mailer, # mailer class. Needed. 21 | 22 | :reset_password_mailer_disabled, # when true sorcery will not automatically 23 | # email password reset details and allow you to 24 | # manually handle how and when email is sent 25 | 26 | :reset_password_email_method_name, # reset password email method on your 27 | # mailer class. 28 | 29 | :reset_password_expiration_period, # how many seconds before the reset request 30 | # expires. nil for never expires. 31 | 32 | :reset_password_time_between_emails # hammering protection, how long to wait 33 | # before allowing another email to be sent. 34 | 35 | end 36 | 37 | base.sorcery_config.instance_eval do 38 | @defaults.merge!(:@reset_password_token_attribute_name => :reset_password_token, 39 | :@reset_password_token_expires_at_attribute_name => :reset_password_token_expires_at, 40 | :@reset_password_email_sent_at_attribute_name => :reset_password_email_sent_at, 41 | :@reset_password_mailer => nil, 42 | :@reset_password_mailer_disabled => false, 43 | :@reset_password_email_method_name => :reset_password_email, 44 | :@reset_password_expiration_period => nil, 45 | :@reset_password_time_between_emails => 5 * 60 ) 46 | 47 | reset! 48 | end 49 | 50 | base.extend(ClassMethods) 51 | 52 | base.sorcery_config.after_config << :validate_mailer_defined 53 | base.sorcery_config.after_config << :define_reset_password_fields 54 | 55 | base.send(:include, InstanceMethods) 56 | 57 | end 58 | 59 | module ClassMethods 60 | # Find user by token, also checks for expiration. 61 | # Returns the user if token found and is valid. 62 | def load_from_reset_password_token(token) 63 | token_attr_name = @sorcery_config.reset_password_token_attribute_name 64 | token_expiration_date_attr = @sorcery_config.reset_password_token_expires_at_attribute_name 65 | load_from_token(token, token_attr_name, token_expiration_date_attr) 66 | end 67 | 68 | protected 69 | 70 | # This submodule requires the developer to define his own mailer class to be used by it 71 | # when reset_password_mailer_disabled is false 72 | def validate_mailer_defined 73 | msg = "To use reset_password submodule, you must define a mailer (config.reset_password_mailer = YourMailerClass)." 74 | raise ArgumentError, msg if @sorcery_config.reset_password_mailer == nil and @sorcery_config.reset_password_mailer_disabled == false 75 | end 76 | 77 | def define_reset_password_fields 78 | sorcery_adapter.define_field sorcery_config.reset_password_token_attribute_name, String 79 | sorcery_adapter.define_field sorcery_config.reset_password_token_expires_at_attribute_name, Time 80 | sorcery_adapter.define_field sorcery_config.reset_password_email_sent_at_attribute_name, Time 81 | end 82 | 83 | end 84 | 85 | module InstanceMethods 86 | # generates a reset code with expiration 87 | def generate_reset_password_token! 88 | config = sorcery_config 89 | attributes = {config.reset_password_token_attribute_name => TemporaryToken.generate_random_token, 90 | config.reset_password_email_sent_at_attribute_name => Time.now.in_time_zone} 91 | attributes[config.reset_password_token_expires_at_attribute_name] = Time.now.in_time_zone + config.reset_password_expiration_period if config.reset_password_expiration_period 92 | 93 | self.sorcery_adapter.update_attributes(attributes) 94 | end 95 | 96 | # generates a reset code with expiration and sends an email to the user. 97 | def deliver_reset_password_instructions! 98 | mail = false 99 | config = sorcery_config 100 | # hammering protection 101 | return false if config.reset_password_time_between_emails.present? && self.send(config.reset_password_email_sent_at_attribute_name) && self.send(config.reset_password_email_sent_at_attribute_name) > config.reset_password_time_between_emails.seconds.ago.utc 102 | self.class.sorcery_adapter.transaction do 103 | generate_reset_password_token! 104 | mail = send_reset_password_email! unless config.reset_password_mailer_disabled 105 | end 106 | mail 107 | end 108 | 109 | # Clears token and tries to update the new password for the user. 110 | def change_password!(new_password) 111 | clear_reset_password_token 112 | self.send(:"#{sorcery_config.password_attribute_name}=", new_password) 113 | sorcery_adapter.save 114 | end 115 | 116 | protected 117 | 118 | def send_reset_password_email! 119 | generic_send_email(:reset_password_email_method_name, :reset_password_mailer) 120 | end 121 | 122 | # Clears the token. 123 | def clear_reset_password_token 124 | config = sorcery_config 125 | self.send(:"#{config.reset_password_token_attribute_name}=", nil) 126 | self.send(:"#{config.reset_password_token_expires_at_attribute_name}=", nil) if config.reset_password_expiration_period 127 | end 128 | end 129 | 130 | end 131 | end 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /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 | base.extend(ClassMethods) 11 | end 12 | 13 | # Random code, used for salt and temporary tokens. 14 | def self.generate_random_token 15 | SecureRandom.base64(15).tr('+/=lIO0', 'pqrsxyz') 16 | end 17 | 18 | module ClassMethods 19 | def load_from_token(token, token_attr_name, token_expiration_date_attr) 20 | return nil if token.blank? 21 | user = sorcery_adapter.find_by_token(token_attr_name,token) 22 | if !user.blank? && !user.send(token_expiration_date_attr).nil? 23 | return Time.now.in_time_zone < user.send(token_expiration_date_attr) ? user : nil 24 | end 25 | user 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/sorcery/protocols/oauth.rb: -------------------------------------------------------------------------------- 1 | require 'oauth' 2 | 3 | module Sorcery 4 | module Protocols 5 | module Oauth 6 | 7 | def oauth_version 8 | '1.0' 9 | end 10 | 11 | def get_request_token(token=nil,secret=nil) 12 | return ::OAuth::RequestToken.new(get_consumer, token, secret) if token && secret 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({ 21 | oauth_callback: @callback_url 22 | }) 23 | end 24 | 25 | def get_access_token(args) 26 | get_request_token( 27 | args[:request_token], 28 | args[:request_token_secret] 29 | ).get_access_token({ 30 | oauth_verifier: args[:oauth_verifier] 31 | }) 32 | end 33 | 34 | protected 35 | 36 | def get_consumer 37 | ::OAuth::Consumer.new(@key, @secret, site: @site) 38 | end 39 | 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/sorcery/protocols/oauth2.rb: -------------------------------------------------------------------------------- 1 | require 'oauth2' 2 | 3 | module Sorcery 4 | module Protocols 5 | module Oauth2 6 | 7 | def oauth_version 8 | '2.0' 9 | end 10 | 11 | def authorize_url(options = {}) 12 | client = build_client(options) 13 | client.auth_code.authorize_url( 14 | redirect_uri: @callback_url, 15 | scope: @scope, 16 | display: @display, 17 | state: @state 18 | ) 19 | end 20 | 21 | def get_access_token(args, options = {}) 22 | client = build_client(options) 23 | client.auth_code.get_token( 24 | args[:code], 25 | { 26 | redirect_uri: @callback_url, 27 | parse: options.delete(:parse) 28 | }, 29 | options 30 | ) 31 | end 32 | 33 | def build_client(options = {}) 34 | defaults = { 35 | site: @site, 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 | 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/sorcery/providers/base.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Providers 3 | class Base 4 | 5 | attr_reader :access_token 6 | 7 | attr_accessor :callback_url, :key, :original_callback_url, :secret, 8 | :site, :state, :user_info_mapping 9 | 10 | def has_callback?; true; end 11 | 12 | def initialize 13 | @user_info_mapping = {} 14 | end 15 | 16 | def auth_hash(access_token, hash={}) 17 | return hash if access_token.nil? 18 | 19 | token_hash = hash.dup 20 | token_hash[:token] = access_token.token if access_token.respond_to?(:token) 21 | token_hash[:refresh_token] = access_token.refresh_token if access_token.respond_to?(:refresh_token) 22 | token_hash[:expires_at] = access_token.expires_at if access_token.respond_to?(:expires_at) 23 | token_hash[:expires_in] = access_token.expires_at if access_token.respond_to?(:expires_in) 24 | token_hash 25 | end 26 | 27 | def self.name 28 | super.gsub(/Sorcery::Providers::/, '').downcase 29 | end 30 | 31 | # Ensure that all descendant classes are loaded before run this 32 | def self.descendants 33 | ObjectSpace.each_object(Class).select { |klass| klass < self } 34 | end 35 | 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /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 | 11 | include Protocols::Oauth2 12 | 13 | attr_reader :mode, :param_name, :parse 14 | attr_accessor :access_permissions, :display, :scope, :token_url, 15 | :user_info_path, :auth_path, :api_version 16 | 17 | def initialize 18 | super 19 | 20 | @site = 'https://graph.facebook.com' 21 | @auth_site = 'https://www.facebook.com' 22 | @user_info_path = 'me' 23 | @scope = 'email' 24 | @display = 'page' 25 | @token_url = 'oauth/access_token' 26 | @auth_path = 'dialog/oauth' 27 | @mode = :query 28 | @parse = :query 29 | @param_name = 'access_token' 30 | end 31 | 32 | def get_user_hash(access_token) 33 | response = access_token.get(user_info_path) 34 | 35 | auth_hash(access_token).tap do |h| 36 | h[:user_info] = JSON.parse(response.body) 37 | h[:uid] = h[:user_info]['id'] 38 | end 39 | end 40 | 41 | # calculates and returns the url to which the user should be redirected, 42 | # to get authenticated at the external provider's site. 43 | def login_url(params, session) 44 | authorize_url 45 | end 46 | 47 | # overrides oauth2#authorize_url to allow customized scope. 48 | def authorize_url 49 | 50 | # Fix: replace default oauth2 options, specially to prevent the Faraday gem which 51 | # concatenates with "/", removing the Facebook api version 52 | options = { 53 | site: File::join(@site, api_version.to_s), 54 | authorize_url: File::join(@auth_site, api_version.to_s, auth_path), 55 | token_url: token_url 56 | } 57 | 58 | @scope = access_permissions.present? ? access_permissions.join(',') : scope 59 | super(options) 60 | end 61 | 62 | # tries to login the user from access token 63 | def process_callback(params, session) 64 | args = {}.tap do |a| 65 | a[:code] = params[:code] if params[:code] 66 | end 67 | 68 | get_access_token(args, token_url: token_url, mode: mode, 69 | param_name: param_name, parse: parse) 70 | end 71 | 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /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 | 11 | include Protocols::Oauth2 12 | 13 | attr_accessor :auth_path, :scope, :token_url, :user_info_path 14 | 15 | def initialize 16 | super 17 | 18 | @scope = nil 19 | @site = 'https://github.com/' 20 | @user_info_path = 'https://api.github.com/user' 21 | @auth_path = '/login/oauth/authorize' 22 | @token_url = '/login/oauth/access_token' 23 | end 24 | 25 | def get_user_hash(access_token) 26 | response = access_token.get(user_info_path) 27 | 28 | auth_hash(access_token).tap do |h| 29 | h[:user_info] = JSON.parse(response.body).tap do |uih| 30 | uih['email'] = primary_email(access_token) if scope =~ /user/ 31 | end 32 | h[:uid] = h[:user_info]['id'] 33 | end 34 | end 35 | 36 | # calculates and returns the url to which the user should be redirected, 37 | # to get authenticated at the external provider's site. 38 | def login_url(params, session) 39 | authorize_url({ authorize_url: auth_path }) 40 | end 41 | 42 | # tries to login the user from access token 43 | def process_callback(params, session) 44 | args = {}.tap do |a| 45 | a[:code] = params[:code] if params[:code] 46 | end 47 | 48 | get_access_token(args, token_url: token_url, token_method: :post) 49 | end 50 | 51 | def primary_email(access_token) 52 | response = access_token.get(user_info_path + "/emails") 53 | emails = JSON.parse(response.body) 54 | primary = emails.find{|i| i['primary'] } 55 | primary && primary['email'] || emails.first && emails.first['email'] 56 | end 57 | 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /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 | 11 | include Protocols::Oauth2 12 | 13 | attr_accessor :auth_url, :scope, :token_url, :user_info_url 14 | 15 | def initialize 16 | super 17 | 18 | @site = 'https://accounts.google.com' 19 | @auth_url = '/o/oauth2/auth' 20 | @token_url = '/o/oauth2/token' 21 | @user_info_url = 'https://www.googleapis.com/oauth2/v1/userinfo' 22 | @scope = 'https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile' 23 | end 24 | 25 | def get_user_hash(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, token_url: token_url, token_method: :post) 47 | end 48 | 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/sorcery/providers/heroku.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Providers 3 | 4 | # This class adds support for OAuth with heroku.com. 5 | 6 | # config.heroku.key = 7 | # config.heroku.secret = 8 | # config.heroku.callback_url = "/oauth/callback?provider=heroku" 9 | # config.heroku.scope = "read" 10 | # config.heroku.user_info_mapping = {:email => "email", :name => "email" } 11 | 12 | # NOTE: 13 | # The full path must be set for OAuth Callback URL when configuring the API Client Information on Heroku. 14 | 15 | class Heroku < Base 16 | 17 | include Protocols::Oauth2 18 | 19 | attr_accessor :auth_path, :scope, :token_url, :user_info_path 20 | 21 | def initialize 22 | super 23 | 24 | @scope = nil 25 | @site = 'https://id.heroku.com' 26 | @user_info_path = 'https://api.heroku.com/account' 27 | @auth_path = '/oauth/authorize' 28 | @token_url = '/oauth/token' 29 | @user_info_path = '/account' 30 | @state = SecureRandom.hex(16) 31 | end 32 | 33 | def get_user_hash(access_token) 34 | response = access_token.get(user_info_path) 35 | body = JSON.parse(response.body) 36 | auth_hash(access_token).tap do |h| 37 | h[:user_info] = body 38 | h[:uid] = body['id'].to_s 39 | h[:email] = body['email'].to_s 40 | end 41 | end 42 | 43 | def login_url(params, session) 44 | authorize_url({ authorize_url: auth_path }) 45 | end 46 | 47 | # tries to login the user from access token 48 | def process_callback(params, session) 49 | raise "Invalid state. Potential Cross Site Forgery" if params[:state] != state 50 | args = { }.tap do |a| 51 | a[:code] = params[:code] if params[:code] 52 | end 53 | get_access_token(args, token_url: token_url, token_method: :post) 54 | end 55 | end 56 | end 57 | end -------------------------------------------------------------------------------- /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 | 11 | include Protocols::Oauth 12 | 13 | attr_accessor :access_token_path, :authorize_path, :request_token_path, 14 | :user_info_path, :site, :signature_method, :private_key_file, :callback_url 15 | 16 | 17 | def initialize 18 | @configuration = { 19 | authorize_path: '/authorize', 20 | request_token_path: '/request-token', 21 | access_token_path: '/access-token' 22 | } 23 | @user_info_path = '/users/me' 24 | end 25 | 26 | # Override included get_consumer method to provide authorize_path 27 | #read extra configurations 28 | def get_consumer 29 | @configuration = @configuration.merge({ 30 | site: site, 31 | signature_method: signature_method, 32 | consumer_key: key, 33 | private_key_file: private_key_file 34 | }) 35 | ::OAuth::Consumer.new(@key, @secret, @configuration) 36 | end 37 | 38 | def get_user_hash(access_token) 39 | response = access_token.get(user_info_path) 40 | 41 | auth_hash(access_token).tap do |h| 42 | h[:user_info] = JSON.parse(response.body)['users'].first 43 | h[:uid] = user_hash[:user_info]['id'].to_s 44 | end 45 | end 46 | 47 | # calculates and returns the url to which the user should be redirected, 48 | # to get authenticated at the external provider's site. 49 | def login_url(params, session) 50 | req_token = get_request_token 51 | session[:request_token] = req_token.token 52 | session[:request_token_secret] = req_token.secret 53 | 54 | #it was like that -> redirect_to authorize_url({ request_token: req_token.token, request_token_secret: req_token.secret }) 55 | #for some reason Jira does not need these parameters 56 | 57 | get_request_token( 58 | session[:request_token], 59 | session[:request_token_secret] 60 | ).authorize_url 61 | end 62 | 63 | # tries to login the user from access token 64 | def process_callback(params, session) 65 | args = { 66 | oauth_verifier: params[:oauth_verifier], 67 | request_token: session[:request_token], 68 | request_token_secret: session[:request_token_secret] 69 | } 70 | 71 | args.merge!({ code: params[:code] }) if params[:code] 72 | get_access_token(args) 73 | end 74 | 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/sorcery/providers/linkedin.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | module Providers 3 | # This class adds support for OAuth with Linkedin.com. 4 | # 5 | # config.linkedin.key = 6 | # config.linkedin.secret = 7 | # ... 8 | # 9 | class Linkedin < Base 10 | 11 | include Protocols::Oauth 12 | 13 | attr_accessor :authorize_path, :access_permissions, :access_token_path, 14 | :request_token_path, :user_info_fields, :user_info_path 15 | 16 | def initialize 17 | @configuration = { 18 | site: 'https://api.linkedin.com', 19 | authorize_path: '/uas/oauth/authenticate', 20 | request_token_path: '/uas/oauth/requestToken', 21 | access_token_path: '/uas/oauth/accessToken' 22 | } 23 | @user_info_path = '/v1/people/~' 24 | end 25 | 26 | # Override included get_consumer method to provide authorize_path 27 | def get_consumer 28 | # Add access permissions to request token path 29 | @configuration[:request_token_path] += '?scope=' + access_permissions.join('+') unless access_permissions.blank? or @configuration[:request_token_path].include? '?scope=' 30 | ::OAuth::Consumer.new(@key, @secret, @configuration) 31 | end 32 | 33 | def get_user_hash(access_token) 34 | fields = self.user_info_fields.join(',') 35 | response = access_token.get("#{@user_info_path}:(id,#{fields})", 'x-li-format' => 'json') 36 | 37 | auth_hash(access_token).tap do |h| 38 | h[:user_info] = JSON.parse(response.body) 39 | h[:uid] = h[: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 | authorize_url({ request_token: req_token.token, request_token_secret: req_token.secret }) 50 | end 51 | 52 | # tries to login the user from access token 53 | def process_callback(params, session) 54 | args = { 55 | oauth_verifier: params[:oauth_verifier], 56 | request_token: session[:request_token], 57 | request_token_secret: session[:request_token_secret] 58 | } 59 | 60 | args.merge!({ code: params[:code] }) if params[:code] 61 | get_access_token(args) 62 | end 63 | 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /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 | 11 | include Protocols::Oauth2 12 | 13 | attr_accessor :auth_url, :token_path, :user_info_url, :scope 14 | 15 | def initialize 16 | super 17 | 18 | @site = 'https://oauth.live.com/' 19 | @auth_url = '/authorize' 20 | @token_path = '/token' 21 | @user_info_url = 'https://apis.live.net/v5.0/me' 22 | @scope = 'wl.basic wl.emails wl.offline_access' 23 | end 24 | 25 | def get_user_hash(access_token) 26 | access_token.token_param = 'access_token' 27 | response = access_token.get(user_info_url) 28 | 29 | auth_hash(access_token).tap do |h| 30 | h[:user_info] = JSON.parse(response.body) 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 | self.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, access_token_path: token_path, 48 | access_token_method: :post) 49 | end 50 | 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /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 | 11 | include Protocols::Oauth2 12 | 13 | attr_accessor :auth_url, :scope, :token_url, :user_info_url 14 | 15 | def initialize 16 | super 17 | 18 | @scope = 'openid email' 19 | @site = 'https://api.paypal.com' 20 | @auth_url = 'https://www.paypal.com/webapps/auth/protocol/openidconnect/v1/authorize' 21 | @user_info_url = 'https://api.paypal.com/v1/identity/openidconnect/userinfo?schema=openid' 22 | @token_url = 'https://api.paypal.com/v1/identity/openidconnect/tokenservice' 23 | @state = SecureRandom.hex(16) 24 | end 25 | 26 | def get_user_hash(access_token) 27 | response = access_token.get(user_info_url) 28 | body = JSON.parse(response.body) 29 | auth_hash(access_token).tap do |h| 30 | h[:user_info] = body 31 | h[:uid] = body['user_id'] 32 | h[:email] = body['email'] 33 | end 34 | end 35 | 36 | def get_access_token(args, options = {}) 37 | client = build_client(options) 38 | client.auth_code.get_token( 39 | args[:code], 40 | { 41 | redirect_uri: @callback_url, 42 | parse: options.delete(:parse) 43 | }, 44 | options 45 | ) 46 | end 47 | 48 | def login_url(params, session) 49 | authorize_url({ authorize_url: auth_url }) 50 | end 51 | 52 | def process_callback(params, session) 53 | args = {}.tap do |a| 54 | a[:code] = params[:code] if params[:code] 55 | end 56 | 57 | get_access_token(args, token_url: token_url, token_method: :post) 58 | end 59 | 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /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 | 11 | include Protocols::Oauth2 12 | 13 | attr_accessor :auth_url, :token_url, :scope 14 | 15 | def initialize 16 | super 17 | 18 | @site = 'https://login.salesforce.com' 19 | @auth_url = '/services/oauth2/authorize' 20 | @token_url = '/services/oauth2/token' 21 | end 22 | 23 | def get_user_hash(access_token) 24 | user_info_url = access_token.params['id'] 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]['user_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 | 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /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 | 11 | include Protocols::Oauth 12 | 13 | attr_accessor :state, :user_info_path 14 | 15 | def initialize 16 | super 17 | 18 | @site = 'https://api.twitter.com' 19 | @user_info_path = '/1.1/account/verify_credentials.json' 20 | end 21 | 22 | # Override included get_consumer method to provide authorize_path 23 | def get_consumer 24 | ::OAuth::Consumer.new(@key, secret, site: site, authorize_path: '/oauth/authenticate') 25 | end 26 | 27 | def get_user_hash(access_token) 28 | response = access_token.get(user_info_path) 29 | 30 | auth_hash(access_token).tap do |h| 31 | h[:user_info] = JSON.parse(response.body) 32 | h[:uid] = h[:user_info]['id'].to_s 33 | end 34 | end 35 | 36 | # calculates and returns the url to which the user should be redirected, 37 | # to get authenticated at the external provider's site. 38 | def login_url(params, session) 39 | req_token = self.get_request_token 40 | session[:request_token] = req_token.token 41 | session[:request_token_secret] = req_token.secret 42 | self.authorize_url({ request_token: req_token.token, request_token_secret: req_token.secret }) 43 | end 44 | 45 | # tries to login the user from access token 46 | def process_callback(params, session) 47 | args = { 48 | oauth_verifier: params[:oauth_verifier], 49 | request_token: session[:request_token], 50 | request_token_secret: session[:request_token_secret] 51 | } 52 | 53 | args.merge!({ code: params[:code] }) if params[:code] 54 | get_access_token(args) 55 | end 56 | 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /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 | 11 | include Protocols::Oauth2 12 | 13 | attr_accessor :auth_path, :token_path, :user_info_url, :scope 14 | 15 | def initialize 16 | super 17 | 18 | @site = 'https://oauth.vk.com/' 19 | @user_info_url = 'https://api.vk.com/method/getProfiles' 20 | @auth_path = '/authorize' 21 | @token_path = '/access_token' 22 | @scope = 'email' 23 | end 24 | 25 | def get_user_hash(access_token) 26 | user_hash = auth_hash(access_token) 27 | 28 | params = { 29 | access_token: access_token.token, 30 | uids: access_token.params['user_id'], 31 | fields: user_info_mapping.values.join(','), 32 | scope: scope 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]['uid'] 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 | self.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 | 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /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 | 11 | include Protocols::Oauth 12 | 13 | attr_accessor :access_token_path, :authorize_path, :request_token_path, 14 | :user_info_path 15 | 16 | 17 | def initialize 18 | @configuration = { 19 | site: 'https://api.xing.com/v1', 20 | authorize_path: '/authorize', 21 | request_token_path: '/request_token', 22 | access_token_path: '/access_token' 23 | } 24 | @user_info_path = '/users/me' 25 | end 26 | 27 | # Override included get_consumer method to provide authorize_path 28 | def get_consumer 29 | ::OAuth::Consumer.new(@key, @secret, @configuration) 30 | end 31 | 32 | def get_user_hash(access_token) 33 | response = access_token.get(user_info_path) 34 | 35 | auth_hash(access_token).tap do |h| 36 | h[:user_info] = JSON.parse(response.body)['users'].first 37 | h[:uid] = h[:user_info]['id'].to_s 38 | end 39 | end 40 | 41 | # calculates and returns the url to which the user should be redirected, 42 | # to get authenticated at the external provider's site. 43 | def login_url(params, session) 44 | req_token = get_request_token 45 | session[:request_token] = req_token.token 46 | session[:request_token_secret] = req_token.secret 47 | authorize_url({ request_token: req_token.token, request_token_secret: req_token.secret }) 48 | end 49 | 50 | # tries to login the user from access token 51 | def process_callback(params, session) 52 | args = { 53 | oauth_verifier: params[:oauth_verifier], 54 | request_token: session[:request_token], 55 | request_token_secret: session[:request_token_secret] 56 | } 57 | 58 | args.merge!({ code: params[:code] }) if params[:code] 59 | get_access_token(args) 60 | end 61 | 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /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 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 | Object.send(:remove_const, "User") 71 | load 'user.rb' 72 | if User.respond_to?(:reset_column_information) 73 | User.reset_column_information 74 | end 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /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 = [ 8 | :register_last_activity_time_to_db, 9 | :deny_banned_user, 10 | :validate_session 11 | ] 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 | # hopefully it won't break until Rails 4. 23 | chain = if Gem::Version.new(::Rails::VERSION::STRING) >= Gem::Version.new("4.1.0") 24 | SorceryController._process_action_callbacks.send :chain 25 | else 26 | SorceryController._process_action_callbacks 27 | end 28 | 29 | chain.delete_if {|c| SUBMODULES_AUTO_ADDED_CONTROLLER_FILTERS.include?(c.filter) } 30 | 31 | # configure 32 | ::Sorcery::Controller::Config.submodules = submodules 33 | ::Sorcery::Controller::Config.user_class = nil 34 | ActionController::Base.send(:include,::Sorcery::Controller) 35 | ::Sorcery::Controller::Config.user_class = "User" 36 | 37 | ::Sorcery::Controller::Config.user_config do |user| 38 | options.each do |property,value| 39 | user.send(:"#{property}=", value) 40 | end 41 | end 42 | User.authenticates_with_sorcery! 43 | if defined?(DataMapper) and User.ancestors.include?(DataMapper::Resource) 44 | DataMapper.auto_migrate! 45 | User.finalize 46 | Authentication.finalize 47 | end 48 | end 49 | 50 | def sorcery_controller_property_set(property, value) 51 | ::Sorcery::Controller::Config.send(:"#{property}=", value) 52 | end 53 | 54 | def sorcery_controller_external_property_set(provider, property, value) 55 | ::Sorcery::Controller::Config.send(provider).send(:"#{property}=", value) 56 | end 57 | 58 | # This helper is used to fake multiple users signing in in tests. 59 | # It does so by clearing @current_user, thus allowing a new user to login, 60 | # all this without calling the :logout action explicitly. 61 | # A dirty dirty hack. 62 | def clear_user_without_logout 63 | subject.instance_variable_set(:@current_user,nil) 64 | end 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /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 | 6 | #Accepts arguments for user to login, route to use and HTTP method 7 | #Defaults - @user, 'sessions_url' and POST 8 | def login_user(user = nil, route = nil, http_method = :post) 9 | user ||= @user 10 | route ||= sessions_url 11 | 12 | username_attr = user.sorcery_config.username_attribute_names.first 13 | username = user.send(username_attr) 14 | page.driver.send(http_method, route, { :"#{username_attr}" => username, :password => 'secret' }) 15 | end 16 | 17 | #Accepts route and HTTP method arguments 18 | #Default - 'logout_url' and GET 19 | def logout_user(route = nil, http_method = :get) 20 | route ||= logout_url 21 | page.driver.send(http_method, route) 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/sorcery/version.rb: -------------------------------------------------------------------------------- 1 | module Sorcery 2 | VERSION = "0.9.1" 3 | end 4 | -------------------------------------------------------------------------------- /sorcery.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib', __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'sorcery/version' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "sorcery" 7 | s.version = Sorcery::VERSION 8 | s.authors = ["Noam Ben Ari", "Kir Shatrov", "Grzegorz Witek"] 9 | s.email = "nbenari@gmail.com" 10 | s.description = "Provides common authentication needs such as signing in/out, activating by email and resetting password." 11 | s.summary = "Magical authentication for Rails 3 & 4 applications" 12 | s.homepage = "http://github.com/NoamB/sorcery" 13 | s.post_install_message = "As of version 1.0 oauth/oauth2 won't be automatically bundled\n" 14 | s.post_install_message += "you need to add those dependencies to your Gemfile" 15 | 16 | s.files = `git ls-files`.split($/) 17 | s.require_paths = ["lib"] 18 | 19 | s.licenses = ["MIT"] 20 | 21 | s.required_ruby_version = '>= 2.0.0' 22 | 23 | s.add_dependency "oauth", "~> 0.4", ">= 0.4.4" 24 | s.add_dependency "oauth2", ">= 0.8.0" 25 | s.add_dependency "bcrypt", "~> 3.1" 26 | 27 | s.add_development_dependency "abstract", ">= 1.0.0" 28 | s.add_development_dependency "json", ">= 1.7.7" 29 | s.add_development_dependency "yard", "~> 0.6.0" 30 | 31 | s.add_development_dependency "timecop" 32 | s.add_development_dependency "simplecov", ">= 0.3.8" 33 | s.add_development_dependency "rspec", "~> 3.1.0" 34 | s.add_development_dependency "rspec-rails", "~> 3.1.0" 35 | s.add_development_dependency "test-unit", "~> 3.1.0" 36 | end 37 | 38 | -------------------------------------------------------------------------------- /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 | ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate/activation") 9 | User.reset_column_information 10 | end 11 | 12 | after(:all) do 13 | ActiveRecord::Migrator.rollback("#{Rails.root}/db/migrate/activation") 14 | end 15 | 16 | it_behaves_like "rails_3_activation_model" 17 | 18 | end 19 | -------------------------------------------------------------------------------- /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 | 6 | before(:all) do 7 | ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate/activity_logging") 8 | User.reset_column_information 9 | end 10 | 11 | after(:all) do 12 | ActiveRecord::Migrator.rollback("#{Rails.root}/db/migrate/activity_logging") 13 | end 14 | 15 | it_behaves_like "rails_3_activity_logging_model" 16 | 17 | end 18 | -------------------------------------------------------------------------------- /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 | ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate/brute_force_protection") 7 | User.reset_column_information 8 | end 9 | 10 | after(:all) do 11 | ActiveRecord::Migrator.rollback("#{Rails.root}/db/migrate/brute_force_protection") 12 | end 13 | 14 | it_behaves_like "rails_3_brute_force_protection_model" 15 | 16 | end -------------------------------------------------------------------------------- /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 | ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate/external") 7 | User.reset_column_information 8 | end 9 | 10 | after(:all) do 11 | ActiveRecord::Migrator.rollback("#{Rails.root}/db/migrate/external") 12 | end 13 | 14 | it_behaves_like "rails_3_oauth_model" 15 | 16 | end -------------------------------------------------------------------------------- /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 | ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate/remember_me") 7 | User.reset_column_information 8 | end 9 | 10 | after(:all) do 11 | ActiveRecord::Migrator.rollback("#{Rails.root}/db/migrate/remember_me") 12 | end 13 | 14 | it_behaves_like "rails_3_remember_me_model" 15 | 16 | end -------------------------------------------------------------------------------- /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 | ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate/reset_password") 7 | User.reset_column_information 8 | end 9 | 10 | after(:all) do 11 | ActiveRecord::Migrator.rollback("#{Rails.root}/db/migrate/reset_password") 12 | end 13 | 14 | it_behaves_like "rails_3_reset_password_model" 15 | 16 | end 17 | -------------------------------------------------------------------------------- /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 | before(:all) do 26 | ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate/external") 27 | User.reset_column_information 28 | sorcery_reload! 29 | end 30 | 31 | after(:all) do 32 | ActiveRecord::Migrator.rollback("#{Rails.root}/db/migrate/external") 33 | end 34 | 35 | it_should_behave_like "external_user" 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /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 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 | 16 | let(:adapter) { double('sorcery_adapter') } 17 | let(:user) { double('user', id: 42, sorcery_adapter: adapter) } 18 | 19 | before(:all) do 20 | sorcery_reload!([:activity_logging]) 21 | end 22 | 23 | before(:each) do 24 | allow(user).to receive(:username) 25 | allow(user).to receive_message_chain(:sorcery_config, :username_attribute_names, :first) { :username } 26 | allow(User.sorcery_config).to receive(:last_login_at_attribute_name) { :last_login_at } 27 | allow(User.sorcery_config).to receive(:last_login_from_ip_address_name) { :last_login_from_ip_address } 28 | 29 | sorcery_controller_property_set(:register_login_time, false) 30 | sorcery_controller_property_set(:register_last_ip_address, false) 31 | sorcery_controller_property_set(:register_last_activity_time, false) 32 | end 33 | 34 | 35 | it "logs login time on login" do 36 | now = Time.now.in_time_zone 37 | Timecop.freeze(now) 38 | 39 | sorcery_controller_property_set(:register_login_time, true) 40 | expect(user).to receive(:set_last_login_at).with(be_within(0.1).of(now)) 41 | login_user(user) 42 | 43 | Timecop.return 44 | end 45 | 46 | it "logs logout time on logout" do 47 | login_user(user) 48 | now = Time.now.in_time_zone 49 | Timecop.freeze(now) 50 | expect(user).to receive(:set_last_logout_at).with(be_within(0.1).of(now)) 51 | 52 | logout_user 53 | 54 | Timecop.return 55 | end 56 | 57 | it "logs last activity time when logged in" do 58 | sorcery_controller_property_set(:register_last_activity_time, true) 59 | 60 | login_user(user) 61 | now = Time.now.in_time_zone 62 | Timecop.freeze(now) 63 | expect(user).to receive(:set_last_activity_at).with(be_within(0.1).of(now)) 64 | 65 | get :some_action 66 | 67 | Timecop.return 68 | end 69 | 70 | it "logs last IP address when logged in" do 71 | sorcery_controller_property_set(:register_last_ip_address, true) 72 | expect(user).to receive(:set_last_ip_address).with('0.0.0.0') 73 | 74 | login_user(user) 75 | end 76 | 77 | it "updates nothing but activity fields" do 78 | pending 'Move to model' 79 | original_user_name = User.last.username 80 | login_user(user) 81 | get :some_action_making_a_non_persisted_change_to_the_user 82 | 83 | expect(User.last.username).to eq original_user_name 84 | end 85 | 86 | it "does not register login time if configured so" do 87 | sorcery_controller_property_set(:register_login_time, false) 88 | 89 | expect(user).to receive(:set_last_login_at).never 90 | login_user(user) 91 | end 92 | 93 | it "does not register logout time if configured so" do 94 | sorcery_controller_property_set(:register_logout_time, false) 95 | login_user(user) 96 | 97 | expect(user).to receive(:set_last_logout_at).never 98 | logout_user 99 | end 100 | 101 | it "does not register last activity time if configured so" do 102 | sorcery_controller_property_set(:register_last_activity_time, false) 103 | 104 | expect(user).to receive(:set_last_activity_at).never 105 | login_user(user) 106 | end 107 | 108 | it "does not register last IP address if configured so" do 109 | sorcery_controller_property_set(:register_last_ip_address, false) 110 | expect(user).to receive(:set_last_ip_address).never 111 | 112 | login_user(user) 113 | end 114 | 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /spec/controllers/controller_brute_force_protection_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SorceryController do 4 | 5 | let(:user) { double('user', id: 42, email: 'bla@bla.com') } 6 | 7 | def request_test_login 8 | get :test_login, email: 'bla@bla.com', password: 'blabla' 9 | end 10 | 11 | # ----------------- SESSION TIMEOUT ----------------------- 12 | describe "brute force protection features" do 13 | 14 | before(:all) do 15 | sorcery_reload!([:brute_force_protection]) 16 | end 17 | 18 | after(:each) do 19 | Sorcery::Controller::Config.reset! 20 | sorcery_controller_property_set(:user_class, User) 21 | Timecop.return 22 | end 23 | 24 | it "counts login retries" do 25 | allow(User).to receive(:authenticate) 26 | allow(User.sorcery_adapter).to receive(:find_by_credentials).with(['bla@bla.com', 'blabla']).and_return(user) 27 | 28 | expect(user).to receive(:register_failed_login!).exactly(3).times 29 | 30 | 3.times { request_test_login } 31 | end 32 | 33 | it "resets the counter on a good login" do 34 | # dirty hack for rails 4 35 | allow(@controller).to receive(:register_last_activity_time_to_db) 36 | 37 | allow(User).to receive(:authenticate).and_return(user) 38 | expect(user).to receive_message_chain(:sorcery_adapter, :update_attribute).with(:failed_logins_count, 0) 39 | 40 | get :test_login, email: 'bla@bla.com', password: 'secret' 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/controllers/controller_http_basic_auth_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SorceryController do 4 | 5 | let(:user) { double("user", id: 42, email: 'bla@bla.com') } 6 | 7 | describe "with http basic auth features" do 8 | before(:all) do 9 | sorcery_reload!([:http_basic_auth]) 10 | 11 | sorcery_controller_property_set(:controller_to_realm_map, {"sorcery" => "sorcery"}) 12 | end 13 | 14 | after(:each) do 15 | logout_user 16 | end 17 | 18 | it "requests basic authentication when before_action is used" do 19 | get :test_http_basic_auth 20 | 21 | expect(response.status).to eq 401 22 | end 23 | 24 | it "authenticates from http basic if credentials are sent" do 25 | # dirty hack for rails 4 26 | allow(subject).to receive(:register_last_activity_time_to_db) 27 | 28 | @request.env["HTTP_AUTHORIZATION"] = "Basic #{Base64::encode64("#{user.email}:secret")}" 29 | expect(User).to receive('authenticate').with('bla@bla.com', 'secret').and_return(user) 30 | get :test_http_basic_auth, nil, http_authentication_used: true 31 | 32 | expect(response).to be_a_success 33 | end 34 | 35 | it "fails authentication if credentials are wrong" do 36 | @request.env["HTTP_AUTHORIZATION"] = "Basic #{Base64::encode64("#{user.email}:wrong!")}" 37 | expect(User).to receive('authenticate').with('bla@bla.com', 'wrong!').and_return(nil) 38 | get :test_http_basic_auth, nil, http_authentication_used: true 39 | 40 | expect(response).to redirect_to root_url 41 | end 42 | 43 | it "allows configuration option 'controller_to_realm_map'" do 44 | sorcery_controller_property_set(:controller_to_realm_map, {"1" => "2"}) 45 | 46 | expect(Sorcery::Controller::Config.controller_to_realm_map).to eq({"1" => "2"}) 47 | end 48 | 49 | it "displays the correct realm name configured for the controller" do 50 | sorcery_controller_property_set(:controller_to_realm_map, {"sorcery" => "Salad"}) 51 | get :test_http_basic_auth 52 | 53 | expect(response.headers["WWW-Authenticate"]).to eq "Basic realm=\"Salad\"" 54 | end 55 | 56 | it "signs in the user's session on successful login" do 57 | # dirty hack for rails 4 58 | allow(controller).to receive(:register_last_activity_time_to_db) 59 | 60 | @request.env["HTTP_AUTHORIZATION"] = "Basic #{Base64::encode64("#{user.email}:secret")}" 61 | expect(User).to receive('authenticate').with('bla@bla.com', 'secret').and_return(user) 62 | 63 | get :test_http_basic_auth, nil, http_authentication_used: true 64 | 65 | expect(session[:user_id]).to eq "42" 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /spec/controllers/controller_remember_me_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SorceryController do 4 | 5 | let!(:user) { double('user', id: 42) } 6 | 7 | # ----------------- REMEMBER ME ----------------------- 8 | context "with remember me features" do 9 | 10 | before(:all) do 11 | sorcery_reload!([:remember_me]) 12 | end 13 | 14 | after(:each) do 15 | session = nil 16 | cookies = nil 17 | end 18 | 19 | before(:each) do 20 | allow(user).to receive(:remember_me_token) 21 | allow(user).to receive(:remember_me_token_expires_at) 22 | allow(user).to receive_message_chain(:sorcery_config, :remember_me_token_attribute_name).and_return(:remember_me_token) 23 | allow(user).to receive_message_chain(:sorcery_config, :remember_me_token_expires_at_attribute_name).and_return(:remember_me_token_expires_at) 24 | end 25 | 26 | it "sets cookie on remember_me!" do 27 | expect(User).to receive(:authenticate).with('bla@bla.com', 'secret').and_return(user) 28 | expect(user).to receive(:remember_me!) 29 | 30 | post :test_login_with_remember, :email => 'bla@bla.com', :password => 'secret' 31 | 32 | expect(cookies.signed["remember_me_token"]).to eq assigns[:current_user].remember_me_token 33 | end 34 | 35 | it "clears cookie on forget_me!" do 36 | cookies["remember_me_token"] == {:value => 'asd54234dsfsd43534', :expires => 3600} 37 | get :test_logout 38 | 39 | expect(cookies["remember_me_token"]).to be_nil 40 | end 41 | 42 | it "clears cookie on force_forget_me!" do 43 | cookies["remember_me_token"] == {:value => 'asd54234dsfsd43534', :expires => 3600} 44 | get :test_logout_with_force_forget_me 45 | 46 | expect(cookies["remember_me_token"]).to be_nil 47 | end 48 | 49 | it "login(email,password,remember_me) logs user in and remembers" do 50 | expect(User).to receive(:authenticate).with('bla@bla.com', 'secret', '1').and_return(user) 51 | expect(user).to receive(:remember_me!) 52 | expect(user).to receive(:remember_me_token).and_return('abracadabra').twice 53 | 54 | post :test_login_with_remember_in_login, :email => 'bla@bla.com', :password => 'secret', :remember => "1" 55 | 56 | expect(cookies.signed["remember_me_token"]).not_to be_nil 57 | expect(cookies.signed["remember_me_token"]).to eq assigns[:user].remember_me_token 58 | end 59 | 60 | it "logout also calls forget_me!" do 61 | session[:user_id] = user.id.to_s 62 | expect(User.sorcery_adapter).to receive(:find_by_id).with(user.id.to_s).and_return(user) 63 | expect(user).to receive(:remember_me!) 64 | expect(user).to receive(:forget_me!) 65 | get :test_logout_with_remember 66 | 67 | expect(cookies["remember_me_token"]).to be_nil 68 | end 69 | 70 | it "logs user in from cookie" do 71 | session[:user_id] = user.id.to_s 72 | expect(User.sorcery_adapter).to receive(:find_by_id).with(user.id.to_s).and_return(user) 73 | expect(user).to receive(:remember_me!) 74 | expect(user).to receive(:remember_me_token).and_return('token').twice 75 | expect(user).to receive(:has_remember_me_token?) { true } 76 | 77 | subject.remember_me! 78 | subject.instance_eval do 79 | remove_instance_variable :@current_user 80 | end 81 | session[:user_id] = nil 82 | 83 | expect(User.sorcery_adapter).to receive(:find_by_remember_me_token).with('token').and_return(user) 84 | 85 | get :test_login_from_cookie 86 | 87 | expect(assigns[:current_user]).to eq user 88 | end 89 | 90 | it "doest not remember_me! when not asked to, even if third parameter is used" do 91 | post :test_login_with_remember_in_login, :email => 'bla@bla.com', :password => 'secret', :remember => "0" 92 | 93 | expect(cookies["remember_me_token"]).to be_nil 94 | end 95 | 96 | it "doest not remember_me! when not asked to" do 97 | post :test_login, :email => 'bla@bla.com', :password => 'secret' 98 | expect(cookies["remember_me_token"]).to be_nil 99 | end 100 | 101 | # --- login_user(user) --- 102 | specify { expect(@controller).to respond_to :auto_login } 103 | 104 | it "auto_login(user) logs in an user instance without remembering" do 105 | session[:user_id] = nil 106 | subject.auto_login(user) 107 | get :test_login_from_cookie 108 | 109 | expect(assigns[:current_user]).to eq user 110 | expect(cookies["remember_me_token"]).to be_nil 111 | end 112 | 113 | it "auto_login(user, true) logs in an user instance with remembering" do 114 | session[:user_id] = nil 115 | expect(user).to receive(:remember_me!) 116 | subject.auto_login(user, true) 117 | 118 | get :test_login_from_cookie 119 | 120 | expect(assigns[:current_user]).to eq user 121 | expect(cookies["remember_me_token"]).not_to be_nil 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /spec/controllers/controller_session_timeout_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SorceryController do 4 | 5 | let!(:user) { double('user', id: 42) } 6 | 7 | # ----------------- SESSION TIMEOUT ----------------------- 8 | context "with session timeout features" do 9 | before(:all) do 10 | sorcery_reload!([:session_timeout]) 11 | sorcery_controller_property_set(:session_timeout,0.5) 12 | end 13 | 14 | after(:each) do 15 | Timecop.return 16 | end 17 | 18 | before(:each) do 19 | allow(user).to receive(:username) 20 | allow(user).to receive_message_chain(:sorcery_config, :username_attribute_names, :first) { :username } 21 | end 22 | 23 | it "does not reset session before session timeout" do 24 | login_user user 25 | get :test_should_be_logged_in 26 | 27 | expect(session[:user_id]).not_to be_nil 28 | expect(response).to be_a_success 29 | end 30 | 31 | it "resets session after session timeout" do 32 | login_user user 33 | Timecop.travel(Time.now.in_time_zone+0.6) 34 | get :test_should_be_logged_in 35 | 36 | expect(session[:user_id]).to be_nil 37 | expect(response).to be_a_redirect 38 | end 39 | 40 | it "works if the session is stored as a string or a Time" do 41 | session[:login_time] = Time.now.to_s 42 | # TODO: ??? 43 | expect(User).to receive(:authenticate).with('bla@bla.com', 'secret').and_return(user) 44 | 45 | get :test_login, :email => 'bla@bla.com', :password => 'secret' 46 | 47 | expect(session[:user_id]).not_to be_nil 48 | expect(response).to be_a_success 49 | end 50 | 51 | context "with 'session_timeout_from_last_action'" do 52 | it "does not logout if there was activity" do 53 | sorcery_controller_property_set(:session_timeout_from_last_action, true) 54 | expect(User).to receive(:authenticate).with('bla@bla.com', 'secret').and_return(user) 55 | 56 | get :test_login, :email => 'bla@bla.com', :password => 'secret' 57 | Timecop.travel(Time.now.in_time_zone+0.3) 58 | get :test_should_be_logged_in 59 | 60 | expect(session[:user_id]).not_to be_nil 61 | 62 | Timecop.travel(Time.now.in_time_zone+0.3) 63 | get :test_should_be_logged_in 64 | 65 | expect(session[:user_id]).not_to be_nil 66 | expect(response).to be_a_success 67 | end 68 | 69 | it "with 'session_timeout_from_last_action' logs out if there was no activity" do 70 | sorcery_controller_property_set(:session_timeout_from_last_action, true) 71 | get :test_login, :email => 'bla@bla.com', :password => 'secret' 72 | Timecop.travel(Time.now.in_time_zone+0.6) 73 | get :test_should_be_logged_in 74 | 75 | expect(session[:user_id]).to be_nil 76 | expect(response).to be_a_redirect 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/controllers/controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SorceryController do 4 | describe "plugin configuration" do 5 | before(:all) do 6 | sorcery_reload! 7 | end 8 | 9 | after(:each) do 10 | Sorcery::Controller::Config.reset! 11 | sorcery_reload! 12 | end 13 | 14 | it "enables configuration option 'user_class'" do 15 | sorcery_controller_property_set(:user_class, "TestUser") 16 | 17 | expect(Sorcery::Controller::Config.user_class).to eq "TestUser" 18 | end 19 | 20 | it "enables configuration option 'not_authenticated_action'" do 21 | sorcery_controller_property_set(:not_authenticated_action, :my_action) 22 | 23 | expect(Sorcery::Controller::Config.not_authenticated_action).to eq :my_action 24 | end 25 | 26 | end 27 | 28 | # ----------------- PLUGIN ACTIVATED ----------------------- 29 | context "when activated with sorcery" do 30 | let(:user) { double('user', id: 42) } 31 | 32 | before(:all) do 33 | sorcery_reload! 34 | end 35 | 36 | after(:each) do 37 | Sorcery::Controller::Config.reset! 38 | sorcery_reload! 39 | sorcery_controller_property_set(:user_class, User) 40 | sorcery_model_property_set(:username_attribute_names, [:email]) 41 | end 42 | 43 | specify { should respond_to(:login) } 44 | 45 | specify { should respond_to(:logout) } 46 | 47 | specify { should respond_to(:logged_in?) } 48 | 49 | specify { should respond_to(:current_user) } 50 | 51 | specify { should respond_to(:require_login) } 52 | 53 | describe "#login" do 54 | 55 | context "when succeeds" do 56 | before do 57 | expect(User).to receive(:authenticate).with('bla@bla.com', 'secret').and_return(user) 58 | get :test_login, :email => 'bla@bla.com', :password => 'secret' 59 | end 60 | 61 | it "assigns user to @user variable" do 62 | expect(assigns[:user]).to eq user 63 | end 64 | 65 | it "writes user id in session" do 66 | expect(session[:user_id]).to eq user.id.to_s 67 | end 68 | 69 | it "sets csrf token in session" do 70 | expect(session[:_csrf_token]).not_to be_nil 71 | end 72 | 73 | end 74 | 75 | context "when fails" do 76 | before do 77 | expect(User).to receive(:authenticate).with('bla@bla.com', 'opensesame!').and_return(nil) 78 | get :test_login, :email => 'bla@bla.com', :password => 'opensesame!' 79 | end 80 | 81 | it "sets @user variable to nil" do 82 | expect(assigns[:user]).to be_nil 83 | end 84 | 85 | it "sets user_id in session to nil" do 86 | expect(session[:user_id]).to be_nil 87 | end 88 | end 89 | end 90 | 91 | describe "#logout" do 92 | it "clears the session" do 93 | cookies[:remember_me_token] = nil 94 | session[:user_id] = user.id.to_s 95 | expect(User.sorcery_adapter).to receive(:find_by_id).with("42") { user } 96 | get :test_logout 97 | 98 | expect(session[:user_id]).to be_nil 99 | end 100 | end 101 | 102 | describe "#logged_in?" do 103 | it "returns true when user is logged in" do 104 | session[:user_id] = user.id.to_s 105 | expect(User.sorcery_adapter).to receive(:find_by_id).with("42") { user } 106 | 107 | expect(subject.logged_in?).to be true 108 | end 109 | 110 | it "returns false when user is not logged in" do 111 | session[:user_id] = nil 112 | 113 | expect(subject.logged_in?).to be false 114 | end 115 | end 116 | 117 | describe "#current_user" do 118 | it "current_user returns the user instance if logged in" do 119 | session[:user_id] = user.id.to_s 120 | expect(User.sorcery_adapter).to receive(:find_by_id).once.with("42") { user } 121 | 122 | 2.times { expect(subject.current_user).to eq user } # memoized! 123 | end 124 | 125 | it "current_user returns false if not logged in" do 126 | session[:user_id] = nil 127 | expect(User.sorcery_adapter).to_not receive(:find_by_id) 128 | 129 | 2.times { expect(subject.current_user).to be_nil } # memoized! 130 | end 131 | end 132 | 133 | 134 | it "calls the configured 'not_authenticated_action' when authenticate before_action fails" do 135 | session[:user_id] = nil 136 | sorcery_controller_property_set(:not_authenticated_action, :test_not_authenticated_action) 137 | get :test_logout 138 | 139 | expect(response.body).to eq "test_not_authenticated_action" 140 | end 141 | 142 | it "require_login before_action saves the url that the user originally wanted" do 143 | get :some_action 144 | 145 | expect(session[:return_to_url]).to eq "http://test.host/some_action" 146 | expect(response).to redirect_to("http://test.host/") 147 | end 148 | 149 | it "require_login before_action does not save the url that the user originally wanted upon all non-get http methods" do 150 | [:post, :put, :delete].each do |m| 151 | self.send(m, :some_action) 152 | 153 | expect(session[:return_to_url]).to be_nil 154 | end 155 | end 156 | 157 | it "on successful login the user is redirected to the url he originally wanted" do 158 | session[:return_to_url] = "http://test.host/some_action" 159 | post :test_return_to, :email => 'bla@bla.com', :password => 'secret' 160 | 161 | expect(response).to redirect_to("http://test.host/some_action") 162 | expect(flash[:notice]).to eq "haha!" 163 | end 164 | 165 | 166 | # --- auto_login(user) --- 167 | specify { should respond_to(:auto_login) } 168 | 169 | it "auto_login(user) los in a user instance" do 170 | session[:user_id] = nil 171 | subject.auto_login(user) 172 | 173 | expect(subject.logged_in?).to be true 174 | end 175 | 176 | it "auto_login(user) works even if current_user was already set to false" do 177 | get :test_logout 178 | 179 | expect(session[:user_id]).to be_nil 180 | expect(subject.current_user).to be_nil 181 | 182 | expect(User).to receive(:first) { user } 183 | 184 | get :test_auto_login 185 | 186 | expect(assigns[:result]).to eq user 187 | end 188 | end 189 | 190 | end 191 | -------------------------------------------------------------------------------- /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 | ActiveRecord::Migrator.migrate(migrations_path) 13 | end 14 | 15 | def teardown_orm 16 | ActiveRecord::Migrator.rollback(migrations_path) 17 | end 18 | 19 | def migrations_path 20 | Rails.root.join("db", "migrate", "core") 21 | end 22 | -------------------------------------------------------------------------------- /spec/rails_app/app/active_record/authentication.rb: -------------------------------------------------------------------------------- 1 | class Authentication < ActiveRecord::Base 2 | belongs_to :user 3 | end -------------------------------------------------------------------------------- /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/controllers/sorcery_controller.rb: -------------------------------------------------------------------------------- 1 | require 'oauth' 2 | 3 | class SorceryController < ActionController::Base 4 | protect_from_forgery 5 | 6 | before_action :require_login_from_http_basic, only: [:test_http_basic_auth] 7 | before_action :require_login, only: [:test_logout, :test_logout_with_force_forget_me, :test_should_be_logged_in, :some_action] 8 | 9 | def index 10 | end 11 | 12 | def some_action 13 | render nothing: true 14 | end 15 | 16 | def some_action_making_a_non_persisted_change_to_the_user 17 | current_user.email = 'to_be_ignored' 18 | render nothing: true 19 | end 20 | 21 | def test_login 22 | @user = login(params[:email], params[:password]) 23 | render nothing: true 24 | end 25 | 26 | def test_auto_login 27 | @user = User.first 28 | auto_login(@user) 29 | @result = current_user 30 | render nothing: true 31 | end 32 | 33 | def test_return_to 34 | @user = login(params[:email], params[:password]) 35 | redirect_back_or_to(:index, notice: 'haha!') 36 | end 37 | 38 | def test_logout 39 | logout 40 | render nothing: true 41 | end 42 | 43 | def test_logout_with_remember 44 | remember_me! 45 | logout 46 | render nothing: true 47 | end 48 | 49 | def test_logout_with_force_forget_me 50 | remember_me! 51 | force_forget_me! 52 | logout 53 | render nothing: true 54 | end 55 | 56 | def test_login_with_remember 57 | @user = login(params[:email], params[:password]) 58 | remember_me! 59 | 60 | render nothing: true 61 | end 62 | 63 | def test_login_with_remember_in_login 64 | @user = login(params[:email], params[:password], params[:remember]) 65 | 66 | render nothing: true 67 | end 68 | 69 | def test_login_from_cookie 70 | @user = current_user 71 | render nothing: true 72 | end 73 | 74 | def test_not_authenticated_action 75 | render text: 'test_not_authenticated_action' 76 | end 77 | 78 | def test_should_be_logged_in 79 | render nothing: true 80 | end 81 | 82 | def test_http_basic_auth 83 | render text: 'HTTP Basic Auth' 84 | end 85 | 86 | def login_at_test_twitter 87 | login_at(:twitter) 88 | end 89 | 90 | alias :login_at_test :login_at_test_twitter 91 | 92 | def login_at_test_facebook 93 | login_at(:facebook) 94 | end 95 | 96 | def login_at_test_github 97 | login_at(:github) 98 | end 99 | 100 | def login_at_test_paypal 101 | login_at(:paypal) 102 | end 103 | 104 | def login_at_test_google 105 | login_at(:google) 106 | end 107 | 108 | def login_at_test_liveid 109 | login_at(:liveid) 110 | end 111 | 112 | def login_at_test_jira 113 | login_at(:jira) 114 | end 115 | 116 | def login_at_test_vk 117 | login_at(:vk) 118 | end 119 | 120 | def login_at_test_salesforce 121 | login_at(:salesforce) 122 | end 123 | 124 | def login_at_test_with_state 125 | login_at(:facebook, {state: 'bla'}) 126 | end 127 | 128 | def test_login_from_twitter 129 | if @user = login_from(:twitter) 130 | redirect_to 'bla', notice: 'Success!' 131 | else 132 | redirect_to 'blu', alert: 'Failed!' 133 | end 134 | end 135 | 136 | alias :test_login_from :test_login_from_twitter 137 | 138 | def test_login_from_facebook 139 | if @user = login_from(:facebook) 140 | redirect_to 'bla', notice: 'Success!' 141 | else 142 | redirect_to 'blu', alert: 'Failed!' 143 | end 144 | end 145 | 146 | def test_login_from_github 147 | if @user = login_from(:github) 148 | redirect_to 'bla', notice: 'Success!' 149 | else 150 | redirect_to 'blu', alert: 'Failed!' 151 | end 152 | end 153 | 154 | def test_login_from_paypal 155 | if @user = login_from(:paypal) 156 | redirect_to 'bla', notice: 'Success!' 157 | else 158 | redirect_to 'blu', alert: 'Failed!' 159 | end 160 | end 161 | 162 | def test_login_from_google 163 | if @user = login_from(:google) 164 | redirect_to 'bla', notice: 'Success!' 165 | else 166 | redirect_to 'blu', alert: 'Failed!' 167 | end 168 | end 169 | 170 | def test_login_from_liveid 171 | if @user = login_from(:liveid) 172 | redirect_to 'bla', notice: 'Success!' 173 | else 174 | redirect_to 'blu', alert: 'Failed!' 175 | end 176 | end 177 | 178 | def test_login_from_vk 179 | if @user = login_from(:vk) 180 | redirect_to 'bla', notice: 'Success!' 181 | else 182 | redirect_to 'blu', alert: 'Failed!' 183 | end 184 | end 185 | 186 | def test_login_from_jira 187 | if @user = login_from(:jira) 188 | redirect_to 'bla', notice: 'Success!' 189 | else 190 | redirect_to 'blu', alert: 'Failed!' 191 | end 192 | end 193 | 194 | def test_login_from_salesforce 195 | if @user = login_from(:salesforce) 196 | redirect_to 'bla', notice: 'Success!' 197 | else 198 | redirect_to 'blu', alert: 'Failed!' 199 | end 200 | end 201 | 202 | def test_return_to_with_external_twitter 203 | if @user = login_from(:twitter) 204 | redirect_back_or_to 'bla', notice: 'Success!' 205 | else 206 | redirect_to 'blu', alert: 'Failed!' 207 | end 208 | end 209 | 210 | def test_return_to_with_external_jira 211 | if @user = login_from(:jira) 212 | redirect_back_or_to 'bla', notice: 'Success!' 213 | else 214 | redirect_to 'blu', alert: 'Failed!' 215 | end 216 | end 217 | 218 | alias :test_return_to_with_external :test_return_to_with_external_twitter 219 | 220 | def test_return_to_with_external_facebook 221 | if @user = login_from(:facebook) 222 | redirect_back_or_to 'bla', notice: 'Success!' 223 | else 224 | redirect_to 'blu', alert: 'Failed!' 225 | end 226 | end 227 | 228 | def test_return_to_with_external_github 229 | if @user = login_from(:github) 230 | redirect_back_or_to 'bla', notice: 'Success!' 231 | else 232 | redirect_to 'blu', alert: 'Failed!' 233 | end 234 | end 235 | 236 | def test_return_to_with_external_paypal 237 | if @user = login_from(:paypal) 238 | redirect_back_or_to 'bla', notice: 'Success!' 239 | else 240 | redirect_to 'blu', alert: 'Failed!' 241 | end 242 | end 243 | 244 | def test_return_to_with_external_google 245 | if @user = login_from(:google) 246 | redirect_back_or_to 'bla', notice: 'Success!' 247 | else 248 | redirect_to 'blu', alert: 'Failed!' 249 | end 250 | end 251 | 252 | def test_return_to_with_external_liveid 253 | if @user = login_from(:liveid) 254 | redirect_back_or_to 'bla', notice: 'Success!' 255 | else 256 | redirect_to 'blu', alert: 'Failed!' 257 | end 258 | end 259 | 260 | def test_return_to_with_external_vk 261 | if @user = login_from(:vk) 262 | redirect_back_or_to 'bla', notice: 'Success!' 263 | else 264 | redirect_to 'blu', alert: 'Failed!' 265 | end 266 | end 267 | 268 | def test_return_to_with_external_salesforce 269 | if @user = login_from(:salesforce) 270 | redirect_back_or_to 'bla', notice: 'Success!' 271 | else 272 | redirect_to 'blu', alert: 'Failed!' 273 | end 274 | end 275 | 276 | def test_create_from_provider 277 | provider = params[:provider] 278 | login_from(provider) 279 | if @user = create_from(provider) 280 | redirect_to 'bla', notice: 'Success!' 281 | else 282 | redirect_to 'blu', alert: 'Failed!' 283 | end 284 | end 285 | 286 | def test_add_second_provider 287 | provider = params[:provider] 288 | if logged_in? 289 | if @user = add_provider_to_user(provider) 290 | redirect_to "bla", :notice => "Success!" 291 | else 292 | redirect_to "blu", :alert => "Failed!" 293 | end 294 | end 295 | end 296 | 297 | def test_create_from_provider_with_block 298 | provider = params[:provider] 299 | login_from(provider) 300 | @user = create_from(provider) do |user| 301 | # check uniqueness of email 302 | # User.where(email: user.email).empty? 303 | false 304 | end 305 | if @user 306 | redirect_to 'bla', notice: 'Success!' 307 | else 308 | redirect_to 'blu', alert: 'Failed!' 309 | end 310 | end 311 | 312 | end 313 | -------------------------------------------------------------------------------- /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 | 3 | default :from => "notifications@example.com" 4 | 5 | def activation_needed_email(user) 6 | @user = user 7 | @url = "http://example.com/login" 8 | mail(:to => user.email, 9 | :subject => "Welcome to My Awesome Site") 10 | end 11 | 12 | def activation_success_email(user) 13 | @user = user 14 | @url = "http://example.com/login" 15 | mail(:to => user.email, 16 | :subject => "Your account is now activated") 17 | end 18 | 19 | def reset_password_email(user) 20 | @user = user 21 | @url = "http://example.com/login" 22 | mail(:to => user.email, 23 | :subject => "Your password has been reset") 24 | end 25 | 26 | def send_unlock_token_email(user) 27 | @user = user 28 | @url = "http://example.com/unlock/#{user.unlock_token}" 29 | mail(:to => user.email, 30 | :subject => "Your account has been locked due to many wrong logins") 31 | end 32 | end -------------------------------------------------------------------------------- /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/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', __FILE__) 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 | begin 10 | require "#{SORCERY_ORM}/railtie" 11 | rescue LoadError 12 | end 13 | 14 | require "sorcery" 15 | 16 | module AppRoot 17 | class Application < Rails::Application 18 | config.autoload_paths.reject!{ |p| p =~ /\/app\/(\w+)$/ && !%w(controllers helpers mailers views).include?($1) } 19 | config.autoload_paths += [ "#{config.root}/app/#{SORCERY_ORM}" ] 20 | 21 | # Settings in config/environments/* take precedence over those specified here. 22 | # Application configuration should go into files in config/initializers 23 | # -- all .rb files in that directory are automatically loaded. 24 | 25 | # Custom directories with classes and modules you want to be autoloadable. 26 | # config.autoload_paths += %W(#{config.root}/extras) 27 | 28 | # Only load the plugins named here, in the order given (default is alphabetical). 29 | # :all can be used as a placeholder for all plugins not explicitly named. 30 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 31 | 32 | # Activate observers that should always be running. 33 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 34 | 35 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 36 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 37 | # config.time_zone = 'Central Time (US & Canada)' 38 | 39 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 40 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 41 | # config.i18n.default_locale = :de 42 | 43 | # JavaScript files you want as :defaults (application.js is always included). 44 | # config.action_view.javascript_expansions[:defaults] = %w(jquery rails) 45 | 46 | # Configure the default encoding used in templates for Ruby 1.9. 47 | config.encoding = "utf-8" 48 | 49 | # Configure sensitive parameters which will be filtered from the log file. 50 | config.filter_parameters += [:password] 51 | 52 | config.action_mailer.delivery_method = :test 53 | 54 | config.active_support.deprecation = :stderr 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/rails_app/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) 3 | 4 | require 'bundler/setup' if File.exists?(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', __FILE__) 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/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | AppRoot::Application.config.secret_token = 'a9789f869a0d0ac2f2b683d6e9410c530696b178bca28a7971f4a652b14ff2da89f2cf4dcbf0355f6bc41f81731aa8e46085674d1acc1980436f61cdba76ff5d' 8 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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_force_forget_me 15 | get :test_should_be_logged_in 16 | get :test_create_from_provider 17 | get :test_add_second_provider 18 | get :test_return_to_with_external 19 | get :test_login_from 20 | get :test_login_from_twitter 21 | get :test_login_from_facebook 22 | get :test_login_from_github 23 | get :test_login_from_paypal 24 | get :test_login_from_google 25 | get :test_login_from_liveid 26 | get :test_login_from_vk 27 | get :test_login_from_jira 28 | get :test_login_from_salesforce 29 | get :login_at_test 30 | get :login_at_test_twitter 31 | get :login_at_test_facebook 32 | get :login_at_test_github 33 | get :login_at_test_paypal 34 | get :login_at_test_google 35 | get :login_at_test_liveid 36 | get :login_at_test_vk 37 | get :login_at_test_jira 38 | get :login_at_test_salesforce 39 | get :test_return_to_with_external 40 | get :test_return_to_with_external_twitter 41 | get :test_return_to_with_external_facebook 42 | get :test_return_to_with_external_github 43 | get :test_return_to_with_external_paypal 44 | get :test_return_to_with_external_google 45 | get :test_return_to_with_external_liveid 46 | get :test_return_to_with_external_vk 47 | get :test_return_to_with_external_jira 48 | get :test_return_to_with_external_salesforce 49 | get :test_http_basic_auth 50 | get :some_action_making_a_non_persisted_change_to_the_user 51 | post :test_login_with_remember 52 | get :test_create_from_provider_with_block 53 | get :login_at_test_with_state 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/rails_app/db/migrate/activation/20101224223622_add_activation_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddActivationToUsers < ActiveRecord::Migration 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 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, [:last_logout_at, :last_activity_at] 9 | end 10 | 11 | def self.down 12 | remove_index :users, [: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 -------------------------------------------------------------------------------- /spec/rails_app/db/migrate/brute_force_protection/20101224223626_add_brute_force_protection_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddBruteForceProtectionToUsers < ActiveRecord::Migration 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 -------------------------------------------------------------------------------- /spec/rails_app/db/migrate/core/20101224223620_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration 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 -------------------------------------------------------------------------------- /spec/rails_app/db/migrate/external/20101224223628_create_authentications_and_user_providers.rb: -------------------------------------------------------------------------------- 1 | class CreateAuthenticationsAndUserProviders < ActiveRecord::Migration 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/remember_me/20101224223623_add_remember_me_token_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddRememberMeTokenToUsers < ActiveRecord::Migration 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 -------------------------------------------------------------------------------- /spec/rails_app/db/migrate/reset_password/20101224223622_add_reset_password_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddResetPasswordToUsers < ActiveRecord::Migration 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 | end 7 | 8 | def self.down 9 | remove_column :users, :reset_password_email_sent_at 10 | remove_column :users, :reset_password_token_expires_at 11 | remove_column :users, :reset_password_token 12 | end 13 | end -------------------------------------------------------------------------------- /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 => 20101224223620) do 14 | 15 | create_table "users", :force => true do |t| 16 | t.string "username" 17 | t.string "email" 18 | t.string "crypted_password" 19 | t.datetime "created_at" 20 | t.datetime "updated_at" 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /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 | 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /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 | 8 | context "loaded plugin configuration" do 9 | 10 | let(:config) { User.sorcery_config } 11 | 12 | before(:all) do 13 | sorcery_reload!([:brute_force_protection]) 14 | end 15 | 16 | after(:each) do 17 | User.sorcery_config.reset! 18 | end 19 | 20 | specify { expect(user).to respond_to(:failed_logins_count) } 21 | specify { expect(user).to respond_to(:lock_expires_at) } 22 | 23 | it "enables configuration option 'failed_logins_count_attribute_name'" do 24 | sorcery_model_property_set(:failed_logins_count_attribute_name, :my_count) 25 | expect(config.failed_logins_count_attribute_name).to eq :my_count 26 | end 27 | 28 | it "enables configuration option 'lock_expires_at_attribute_name'" do 29 | sorcery_model_property_set(:lock_expires_at_attribute_name, :expires) 30 | expect(config.lock_expires_at_attribute_name).to eq :expires 31 | end 32 | 33 | it "enables configuration option 'consecutive_login_retries_amount_allowed'" do 34 | sorcery_model_property_set(:consecutive_login_retries_amount_limit, 34) 35 | expect(config.consecutive_login_retries_amount_limit).to eq 34 36 | end 37 | 38 | it "enables configuration option 'login_lock_time_period'" do 39 | sorcery_model_property_set(:login_lock_time_period, 2.hours) 40 | expect(config.login_lock_time_period).to eq 2.hours 41 | end 42 | 43 | describe "#locked?" do 44 | it "is locked" do 45 | user.send("#{config.lock_expires_at_attribute_name}=", Time.now + 5.days) 46 | expect(user).to be_locked 47 | end 48 | 49 | it "isn't locked" do 50 | user.send("#{config.lock_expires_at_attribute_name}=", nil) 51 | expect(user).not_to be_locked 52 | end 53 | end 54 | end 55 | 56 | describe "#register_failed_login!" do 57 | it "locks user when number of retries reached the limit" do 58 | expect(user.lock_expires_at).to be_nil 59 | 60 | sorcery_model_property_set(:consecutive_login_retries_amount_limit, 1) 61 | user.register_failed_login! 62 | lock_expires_at = User.sorcery_adapter.find_by_id(user.id).lock_expires_at 63 | 64 | expect(lock_expires_at).not_to be_nil 65 | end 66 | 67 | context "unlock_token_mailer_disabled is true" do 68 | it "does not automatically send unlock email" do 69 | sorcery_model_property_set(:unlock_token_mailer_disabled, true) 70 | sorcery_model_property_set(:consecutive_login_retries_amount_limit, 2) 71 | sorcery_model_property_set(:login_lock_time_period, 0) 72 | sorcery_model_property_set(:unlock_token_mailer, SorceryMailer) 73 | 74 | 3.times { user.register_failed_login! } 75 | 76 | expect(ActionMailer::Base.deliveries.size).to eq 0 77 | 78 | end 79 | end 80 | 81 | context "unlock_token_mailer_disabled is false" do 82 | before do 83 | sorcery_model_property_set(:unlock_token_mailer_disabled, false) 84 | sorcery_model_property_set(:consecutive_login_retries_amount_limit, 2) 85 | sorcery_model_property_set(:login_lock_time_period, 0) 86 | sorcery_model_property_set(:unlock_token_mailer, SorceryMailer) 87 | end 88 | 89 | it "does not automatically send unlock email" do 90 | 3.times { user.register_failed_login! } 91 | 92 | expect(ActionMailer::Base.deliveries.size).to eq 1 93 | end 94 | 95 | it "generates unlock token before mail is sent" do 96 | 3.times { user.register_failed_login! } 97 | 98 | expect(ActionMailer::Base.deliveries.last.body.to_s.match(user.unlock_token)).not_to be_nil 99 | end 100 | end 101 | end 102 | 103 | context ".authenticate" do 104 | 105 | it "unlocks after lock time period passes" do 106 | sorcery_model_property_set(:consecutive_login_retries_amount_limit, 2) 107 | sorcery_model_property_set(:login_lock_time_period, 0.2) 108 | 2.times { user.register_failed_login! } 109 | 110 | lock_expires_at = User.sorcery_adapter.find_by_id(user.id).lock_expires_at 111 | expect(lock_expires_at).not_to be_nil 112 | 113 | Timecop.travel(Time.now.in_time_zone + 0.3) 114 | User.authenticate('bla@bla.com', 'secret') 115 | 116 | lock_expires_at = User.sorcery_adapter.find_by_id(user.id).lock_expires_at 117 | expect(lock_expires_at).to be_nil 118 | Timecop.return 119 | end 120 | 121 | it "doest not unlock if time period is 0 (permanent lock)" do 122 | sorcery_model_property_set(:consecutive_login_retries_amount_limit, 2) 123 | sorcery_model_property_set(:login_lock_time_period, 0) 124 | 125 | 2.times { user.register_failed_login! } 126 | 127 | unlock_date = user.lock_expires_at 128 | Timecop.travel(Time.now.in_time_zone + 1) 129 | 130 | user.register_failed_login! 131 | 132 | expect(user.lock_expires_at.to_s).to eq unlock_date.to_s 133 | Timecop.return 134 | end 135 | 136 | end 137 | 138 | describe "#unlock!" do 139 | it "unlocks after entering unlock token" do 140 | sorcery_model_property_set(:consecutive_login_retries_amount_limit, 2) 141 | sorcery_model_property_set(:login_lock_time_period, 0) 142 | sorcery_model_property_set(:unlock_token_mailer, SorceryMailer) 143 | 3.times { user.register_failed_login! } 144 | 145 | expect(user.unlock_token).not_to be_nil 146 | 147 | token = user.unlock_token 148 | user = User.load_from_unlock_token(token) 149 | 150 | expect(user).not_to be_nil 151 | 152 | user.unlock! 153 | expect(User.load_from_unlock_token(user.unlock_token)).to be_nil 154 | end 155 | end 156 | end 157 | -------------------------------------------------------------------------------- /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 | 8 | before(:all) do 9 | Authentication.sorcery_adapter.delete_all 10 | User.sorcery_adapter.delete_all 11 | 12 | sorcery_reload!([:external]) 13 | sorcery_controller_property_set(:external_providers, [:twitter]) 14 | sorcery_model_property_set(:authentications_class, Authentication) 15 | sorcery_controller_external_property_set(:twitter, :key, "eYVNBjBDi33aa9GkA3w") 16 | sorcery_controller_external_property_set(:twitter, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") 17 | sorcery_controller_external_property_set(:twitter, :callback_url, "http://blabla.com") 18 | end 19 | 20 | it "responds to 'load_from_provider'" do 21 | expect(User).to respond_to(:load_from_provider) 22 | end 23 | 24 | it "'load_from_provider' loads user if exists" do 25 | external_user 26 | expect(User.load_from_provider :twitter, 123).to eq external_user 27 | end 28 | 29 | it "'load_from_provider' returns nil if user doesn't exist" do 30 | external_user 31 | expect(User.load_from_provider :twitter, 980342).to be_nil 32 | end 33 | 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /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 | 6 | before(:all) do 7 | sorcery_reload!([:remember_me]) 8 | end 9 | 10 | after(:each) do 11 | User.sorcery_config.reset! 12 | end 13 | 14 | it "allows configuration option 'remember_me_token_attribute_name'" do 15 | sorcery_model_property_set(:remember_me_token_attribute_name, :my_token) 16 | 17 | expect(User.sorcery_config.remember_me_token_attribute_name).to eq :my_token 18 | end 19 | 20 | it "allows configuration option 'remember_me_token_expires_at_attribute_name'" do 21 | sorcery_model_property_set(:remember_me_token_expires_at_attribute_name, :my_expires) 22 | 23 | expect(User.sorcery_config.remember_me_token_expires_at_attribute_name).to eq :my_expires 24 | end 25 | 26 | it "allows configuration option 'remember_me_token_persist_globally'" do 27 | sorcery_model_property_set(:remember_me_token_persist_globally, true) 28 | 29 | expect(User.sorcery_config.remember_me_token_persist_globally).to eq true 30 | end 31 | 32 | specify { expect(user).to respond_to :remember_me! } 33 | 34 | specify { expect(user).to respond_to :forget_me! } 35 | 36 | specify { expect(user).to respond_to :force_forget_me! } 37 | 38 | it "sets an expiration based on 'remember_me_for' attribute" do 39 | sorcery_model_property_set(:remember_me_for, 2 * 60 * 60 * 24) 40 | 41 | ts = Time.now.in_time_zone 42 | Timecop.freeze(ts) do 43 | user.remember_me! 44 | end 45 | 46 | expect(user.remember_me_token_expires_at.utc.to_s).to eq (ts + 2 * 60 * 60 * 24).utc.to_s 47 | end 48 | 49 | context "when not persisting globally" do 50 | before { sorcery_model_property_set(:remember_me_token_persist_globally, false) } 51 | 52 | it "generates a new token on 'remember_me!' when a token doesn't exist" do 53 | expect(user.remember_me_token).to be_nil 54 | user.remember_me! 55 | 56 | expect(user.remember_me_token).not_to be_nil 57 | end 58 | 59 | it "generates a new token on 'remember_me!' when a token exists" do 60 | user.remember_me_token = "abc123" 61 | user.remember_me! 62 | 63 | expect(user.remember_me_token).not_to be_nil 64 | expect(user.remember_me_token).not_to eq("abc123") 65 | end 66 | 67 | it "deletes the token and expiration on 'forget_me!'" do 68 | user.remember_me! 69 | 70 | expect(user.remember_me_token).not_to be_nil 71 | 72 | user.forget_me! 73 | 74 | expect(user.remember_me_token).to be_nil 75 | expect(user.remember_me_token_expires_at).to be_nil 76 | end 77 | 78 | it "deletes the token and expiration on 'force_forget_me!'" do 79 | user.remember_me! 80 | 81 | expect(user.remember_me_token).not_to be_nil 82 | 83 | user.force_forget_me! 84 | 85 | expect(user.remember_me_token).to be_nil 86 | expect(user.remember_me_token_expires_at).to be_nil 87 | end 88 | end 89 | 90 | context "when persisting globally" do 91 | before { sorcery_model_property_set(:remember_me_token_persist_globally, true) } 92 | 93 | it "generates a new token on 'remember_me!' when a token doesn't exist" do 94 | expect(user.remember_me_token).to be_nil 95 | user.remember_me! 96 | 97 | expect(user.remember_me_token).not_to be_nil 98 | end 99 | 100 | it "keeps existing token on 'remember_me!' when a token exists" do 101 | user.remember_me_token = "abc123" 102 | user.remember_me! 103 | 104 | expect(user.remember_me_token).to eq("abc123") 105 | end 106 | 107 | it "keeps the token and expiration on 'forget_me!'" do 108 | user.remember_me! 109 | 110 | expect(user.remember_me_token).not_to be_nil 111 | 112 | user.forget_me! 113 | 114 | expect(user.remember_me_token).to_not be_nil 115 | expect(user.remember_me_token_expires_at).to_not be_nil 116 | end 117 | 118 | it "deletes the token and expiration on 'force_forget_me!'" do 119 | user.remember_me! 120 | 121 | expect(user.remember_me_token).not_to be_nil 122 | 123 | user.force_forget_me! 124 | 125 | expect(user.remember_me_token).to be_nil 126 | expect(user.remember_me_token_expires_at).to be_nil 127 | end 128 | end 129 | 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /spec/sorcery_crypto_providers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Crypto Providers wrappers" do 4 | 5 | describe Sorcery::CryptoProviders::MD5 do 6 | 7 | after(:each) do 8 | Sorcery::CryptoProviders::MD5.reset! 9 | end 10 | 11 | it "encrypt works via wrapper like normal lib" do 12 | expect(Sorcery::CryptoProviders::MD5.encrypt 'Noam Ben-Ari').to eq Digest::MD5.hexdigest('Noam Ben-Ari') 13 | end 14 | 15 | it "works with multiple stretches" do 16 | Sorcery::CryptoProviders::MD5.stretches = 3 17 | expect(Sorcery::CryptoProviders::MD5.encrypt 'Noam Ben-Ari').to eq Digest::MD5.hexdigest(Digest::MD5.hexdigest(Digest::MD5.hexdigest('Noam Ben-Ari'))) 18 | end 19 | 20 | it "matches? returns true when matches" do 21 | expect(Sorcery::CryptoProviders::MD5.matches? Digest::MD5.hexdigest('Noam Ben-Ari'), 'Noam Ben-Ari').to be true 22 | end 23 | 24 | it "matches? returns false when no match" do 25 | expect(Sorcery::CryptoProviders::MD5.matches? Digest::MD5.hexdigest('Noam Ben-Ari'), 'Some Dude').to be false 26 | end 27 | 28 | end 29 | 30 | describe Sorcery::CryptoProviders::SHA1 do 31 | 32 | before(:all) do 33 | @digest = 'Noam Ben-Ari' 34 | Sorcery::CryptoProviders::SHA1.stretches.times {@digest = Digest::SHA1.hexdigest(@digest)} 35 | end 36 | 37 | after(:each) do 38 | Sorcery::CryptoProviders::SHA1.reset! 39 | end 40 | 41 | it "encrypt works via wrapper like normal lib" do 42 | expect(Sorcery::CryptoProviders::SHA1.encrypt 'Noam Ben-Ari').to eq @digest 43 | end 44 | 45 | it "works with multiple stretches" do 46 | Sorcery::CryptoProviders::SHA1.stretches = 3 47 | expect(Sorcery::CryptoProviders::SHA1.encrypt 'Noam Ben-Ari').to eq Digest::SHA1.hexdigest(Digest::SHA1.hexdigest(Digest::SHA1.hexdigest('Noam Ben-Ari'))) 48 | end 49 | 50 | it "matches? returns true when matches" do 51 | expect(Sorcery::CryptoProviders::SHA1.matches? @digest, 'Noam Ben-Ari').to be true 52 | end 53 | 54 | it "matches? returns false when no match" do 55 | expect(Sorcery::CryptoProviders::SHA1.matches? @digest, 'Some Dude').to be false 56 | end 57 | 58 | it "matches password encrypted using salt and join token from upstream" do 59 | Sorcery::CryptoProviders::SHA1.join_token = "test" 60 | expect(Sorcery::CryptoProviders::SHA1.encrypt ['password', 'gq18WBnJYNh2arkC1kgH']).to eq '894b5bf1643b8d0e1b2eaddb22426be7036dab70' 61 | end 62 | end 63 | 64 | describe Sorcery::CryptoProviders::SHA256 do 65 | 66 | before(:all) do 67 | @digest = 'Noam Ben-Ari' 68 | Sorcery::CryptoProviders::SHA256.stretches.times {@digest = Digest::SHA256.hexdigest(@digest)} 69 | end 70 | 71 | after(:each) do 72 | Sorcery::CryptoProviders::SHA256.reset! 73 | end 74 | 75 | it "encrypt works via wrapper like normal lib" do 76 | expect(Sorcery::CryptoProviders::SHA256.encrypt 'Noam Ben-Ari').to eq @digest 77 | end 78 | 79 | it "works with multiple stretches" do 80 | Sorcery::CryptoProviders::SHA256.stretches = 3 81 | expect(Sorcery::CryptoProviders::SHA256.encrypt 'Noam Ben-Ari').to eq Digest::SHA256.hexdigest(Digest::SHA256.hexdigest(Digest::SHA256.hexdigest('Noam Ben-Ari'))) 82 | end 83 | 84 | it "matches? returns true when matches" do 85 | expect(Sorcery::CryptoProviders::SHA256.matches? @digest, 'Noam Ben-Ari').to be true 86 | end 87 | 88 | it "matches? returns false when no match" do 89 | expect(Sorcery::CryptoProviders::SHA256.matches? @digest, 'Some Dude').to be false 90 | end 91 | 92 | end 93 | 94 | describe Sorcery::CryptoProviders::SHA512 do 95 | 96 | before(:all) do 97 | @digest = 'Noam Ben-Ari' 98 | Sorcery::CryptoProviders::SHA512.stretches.times {@digest = Digest::SHA512.hexdigest(@digest)} 99 | end 100 | 101 | after(:each) do 102 | Sorcery::CryptoProviders::SHA512.reset! 103 | end 104 | 105 | it "encrypt works via wrapper like normal lib" do 106 | expect(Sorcery::CryptoProviders::SHA512.encrypt 'Noam Ben-Ari').to eq @digest 107 | end 108 | 109 | it "works with multiple stretches" do 110 | Sorcery::CryptoProviders::SHA512.stretches = 3 111 | expect(Sorcery::CryptoProviders::SHA512.encrypt 'Noam Ben-Ari').to eq Digest::SHA512.hexdigest(Digest::SHA512.hexdigest(Digest::SHA512.hexdigest('Noam Ben-Ari'))) 112 | end 113 | 114 | it "matches? returns true when matches" do 115 | expect(Sorcery::CryptoProviders::SHA512.matches? @digest, 'Noam Ben-Ari').to be true 116 | end 117 | 118 | it "matches? returns false when no match" do 119 | expect(Sorcery::CryptoProviders::SHA512.matches? @digest, 'Some Dude').to be false 120 | end 121 | 122 | end 123 | 124 | describe Sorcery::CryptoProviders::AES256 do 125 | 126 | before(:all) do 127 | aes = OpenSSL::Cipher::Cipher.new("AES-256-ECB") 128 | aes.encrypt 129 | @key = "asd234dfs423fddsmndsflktsdf32343" 130 | aes.key = @key 131 | @digest = 'Noam Ben-Ari' 132 | @digest = [aes.update(@digest) + aes.final].pack("m").chomp 133 | Sorcery::CryptoProviders::AES256.key = @key 134 | end 135 | 136 | it "encrypt works via wrapper like normal lib" do 137 | expect(Sorcery::CryptoProviders::AES256.encrypt 'Noam Ben-Ari').to eq @digest 138 | end 139 | 140 | it "matches? returns true when matches" do 141 | expect(Sorcery::CryptoProviders::AES256.matches? @digest, 'Noam Ben-Ari').to be true 142 | end 143 | 144 | it "matches? returns false when no match" do 145 | expect(Sorcery::CryptoProviders::AES256.matches? @digest, 'Some Dude').to be false 146 | end 147 | 148 | it "can be decrypted" do 149 | aes = OpenSSL::Cipher::Cipher.new("AES-256-ECB") 150 | aes.decrypt 151 | aes.key = @key 152 | expect(aes.update(@digest.unpack("m").first) + aes.final).to eq "Noam Ben-Ari" 153 | end 154 | 155 | end 156 | 157 | describe Sorcery::CryptoProviders::BCrypt do 158 | 159 | before(:all) do 160 | Sorcery::CryptoProviders::BCrypt.cost = 1 161 | @digest = BCrypt::Password.create('Noam Ben-Ari', :cost => Sorcery::CryptoProviders::BCrypt.cost) 162 | end 163 | 164 | after(:each) do 165 | Sorcery::CryptoProviders::BCrypt.reset! 166 | end 167 | 168 | it "is comparable with original secret" do 169 | expect(BCrypt::Password.new Sorcery::CryptoProviders::BCrypt.encrypt('Noam Ben-Ari')).to eq 'Noam Ben-Ari' 170 | end 171 | 172 | it "works with multiple costs" do 173 | Sorcery::CryptoProviders::BCrypt.cost = 3 174 | expect(BCrypt::Password.new(Sorcery::CryptoProviders::BCrypt.encrypt 'Noam Ben-Ari')).to eq 'Noam Ben-Ari' 175 | end 176 | 177 | it "matches? returns true when matches" do 178 | expect(Sorcery::CryptoProviders::BCrypt.matches? @digest, 'Noam Ben-Ari').to be true 179 | end 180 | 181 | it "matches? returns false when no match" do 182 | expect(Sorcery::CryptoProviders::BCrypt.matches? @digest, 'Some Dude').to be false 183 | end 184 | 185 | it "respond_to?(:stretches) returns true" do 186 | expect(Sorcery::CryptoProviders::BCrypt.respond_to? :stretches).to be true 187 | end 188 | 189 | it "sets cost when stretches is set" do 190 | Sorcery::CryptoProviders::BCrypt.stretches = 4 191 | 192 | # stubbed in Sorcery::TestHelpers::Internal 193 | expect(Sorcery::CryptoProviders::BCrypt.cost).to eq 1 194 | end 195 | 196 | end 197 | 198 | end 199 | -------------------------------------------------------------------------------- /spec/spec.opts: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation -------------------------------------------------------------------------------- /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 'simplecov' 9 | # SimpleCov.root File.join(File.dirname(__FILE__), '..', 'lib') 10 | # SimpleCov.start 11 | 12 | require 'rspec' 13 | 14 | require 'rails/all' 15 | require 'rspec/rails' 16 | require 'timecop' 17 | 18 | def setup_orm; end 19 | def teardown_orm; end 20 | 21 | require "orm/#{SORCERY_ORM}" 22 | 23 | require "rails_app/config/environment" 24 | 25 | class TestMailer < ActionMailer::Base;end 26 | 27 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} 28 | 29 | RSpec.configure do |config| 30 | config.include RSpec::Rails::ControllerExampleGroup, :file_path => /controller(.)*_spec.rb$/ 31 | config.mock_with :rspec 32 | 33 | config.use_transactional_fixtures = true 34 | 35 | config.before(:suite) { setup_orm } 36 | config.after(:suite) { teardown_orm } 37 | config.before(:each) { ActionMailer::Base.deliveries.clear } 38 | 39 | config.include ::Sorcery::TestHelpers::Internal 40 | config.include ::Sorcery::TestHelpers::Internal::Rails 41 | end 42 | --------------------------------------------------------------------------------