diff --git a/README.md b/README.md index 08d22480..d350a37d 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,14 @@ -[![Build Status](https://travis-ci.org/NoamB/sorcery.svg?branch=master)](https://travis-ci.org/NoamB/sorcery) -[![Code Climate](https://codeclimate.com/github/NoamB/sorcery.png)](https://codeclimate.com/github/NoamB/sorcery) -[![Inline docs](http://inch-ci.org/github/NoamB/sorcery.png?branch=master)](http://inch-ci.org/github/NoamB/sorcery) +# Sorcery: Magical Authentication -# sorcery - -## MAINTAINER NEEDED - -Currently Sorcery project is not actively maintained. I (@arnvald) try to keep looking at issues and help with any problems, but I don't develop the library anymore. -Therefore if you are interested in taking over the project, please check this issue: [https://github.com/NoamB/sorcery/issues/777](https://github.com/NoamB/sorcery/issues/777) +[![Gem Version](https://badge.fury.io/rb/sorcery.svg)](https://rubygems.org/gems/sorcery) +[![Gem Downloads](https://img.shields.io/gem/dt/sorcery.svg)](https://rubygems.org/gems/sorcery) +[![Build Status](https://travis-ci.org/Sorcery/sorcery.svg?branch=master)](https://travis-ci.org/Sorcery/sorcery) +[![Code Climate](https://codeclimate.com/github/Sorcery/sorcery.svg)](https://codeclimate.com/github/Sorcery/sorcery) +[![Inline docs](http://inch-ci.org/github/Sorcery/sorcery.svg?branch=master)](http://inch-ci.org/github/Sorcery/sorcery) +[![Join the chat at https://gitter.im/Sorcery/sorcery](https://badges.gitter.im/join_chat.svg)](https://gitter.im/Sorcery/sorcery?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ## Project -[![Join the chat at https://gitter.im/NoamB/sorcery](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/NoamB/sorcery?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) Magical Authentication for Rails. Supports ActiveRecord, DataMapper, Mongoid and MongoMapper. @@ -19,11 +16,13 @@ Inspired by restful_authentication, Authlogic and Devise. Crypto code taken almost unchanged from Authlogic. OAuth code inspired by OmniAuth and Ryan Bates's railscasts about it. -**Rails 5 status:** [Sorcery 0.9.1](http://rubygems.org/gems/sorcery/versions/0.9.0) should work fine with Rails 5. We can't guarantee it at this point, though, so please open an issue for any problem with Rails 5. +**Rails 5 status:** [Sorcery 0.9.1](http://rubygems.org/gems/sorcery/versions/0.9.1) should work fine with Rails 5. We can't guarantee it at this point, though, so please open an issue for any problem with Rails 5. + **Rails 4 status:** [Sorcery 0.9.0](http://rubygems.org/gems/sorcery/versions/0.9.0) is fully tested and ready for Rails 4.0, 4.1 and 4.2. + **Mongoid status:** Version 0.9.0 works with Mongoid 4. -https://github.com/NoamB/sorcery/wiki/Simple-Password-Authentication +https://github.com/Sorcery/sorcery/wiki/Simple-Password-Authentication ## Philosophy @@ -44,10 +43,11 @@ can write your own authentication flow. It was built with a few goals in mind: Hopefully, I've achieved this. If not, let me know. ## Useful Links + [Documentation](http://rubydoc.info/gems/sorcery) | -[Railscast](http://railscasts.com/episodes/283-authentication-with-sorcery) | [Simple tutorial](https://github.com/NoamB/sorcery/wiki/Simple-Password-Authentication) | [Example Rails 4 app](https://github.com/NoamB/sorcery-example-app) +[Railscast](http://railscasts.com/episodes/283-authentication-with-sorcery) | [Simple tutorial](https://github.com/Sorcery/sorcery/wiki/Simple-Password-Authentication) | [Example Rails 4 app](https://github.com/Sorcery/sorcery-example-app) -Check out the tutorials in the [Wiki](https://github.com/NoamB/sorcery/wiki) for more! +Check out the tutorials in the [Wiki](https://github.com/Sorcery/sorcery/wiki) for more! ## API Summary @@ -56,6 +56,7 @@ explaining and the rest are commented: ### core + ```ruby require_login # this is a before action login(email, password, remember_me = false) @@ -71,11 +72,13 @@ User.authenticates_with_sorcery! ``` ### http basic auth + ```ruby require_login_from_http_basic # this is a before action ``` ### external + ```ruby login_at(provider) # sends the user to an external service (twitter etc.) to authenticate. login_from(provider) # tries to login from the external provider's callback. @@ -83,6 +86,7 @@ create_from(provider) # create the user in the local app db. ``` ### remember me + ```ruby auto_login(user, should_remember=false) # login without credentials, optional remember_me remember_me! @@ -91,6 +95,7 @@ force_forget_me! # completely forgets all sessions by clearing the token, eve ``` ### reset password + ```ruby User.load_from_reset_password_token(token) @user.generate_reset_password_token! # if you want to send the email by youself @@ -99,6 +104,7 @@ User.load_from_reset_password_token(token) ``` ### user activation + ```ruby User.load_from_activation_token(token) @user.setup_activation @@ -186,12 +192,13 @@ end Sidekiq and Resque integrations are coming soon. ## Single Table Inheritance (STI) Support + STI is supported via the `user.subclasses_inherit_config` setting in config/initializers/sorcery.rb. ## Full Features List by module -**Core** (see [lib/sorcery/model.rb](https://github.com/NoamB/sorcery/blob/master/lib/sorcery/model.rb) and -[lib/sorcery/controller.rb](https://github.com/NoamB/sorcery/blob/master/lib/sorcery/controller.rb)): +**Core** (see [lib/sorcery/model.rb](https://github.com/Sorcery/sorcery/blob/master/lib/sorcery/model.rb) and +[lib/sorcery/controller.rb](https://github.com/Sorcery/sorcery/blob/master/lib/sorcery/controller.rb)): * login/logout, optional return user to requested url on login, configurable redirect for non-logged-in users. @@ -201,7 +208,7 @@ STI is supported via the `user.subclasses_inherit_config` setting in config/init * allow multiple fields to serve as username. -**User Activation** (see [lib/sorcery/model/submodules/user_activation.rb](https://github.com/NoamB/sorcery/blob/master/lib/sorcery/model/submodules/user_activation.rb)): +**User Activation** (see [lib/sorcery/model/submodules/user_activation.rb](https://github.com/Sorcery/sorcery/blob/master/lib/sorcery/model/submodules/user_activation.rb)): * User activation by email with optional success email. * configurable attribute names. @@ -210,7 +217,7 @@ STI is supported via the `user.subclasses_inherit_config` setting in config/init * Optionally prevent non-active users to login. -**Reset Password** (see [lib/sorcery/model/submodules/reset_password.rb](https://github.com/NoamB/sorcery/blob/master/lib/sorcery/model/submodules/reset_password.rb)): +**Reset Password** (see [lib/sorcery/model/submodules/reset_password.rb](https://github.com/Sorcery/sorcery/blob/master/lib/sorcery/model/submodules/reset_password.rb)): * Reset password with email verification. * configurable mailer, method name, and attribute name. @@ -218,33 +225,33 @@ STI is supported via the `user.subclasses_inherit_config` setting in config/init * configurable time between emails (hammering protection). -**Remember Me** (see [lib/sorcery/model/submodules/remember_me.rb](https://github.com/NoamB/sorcery/blob/master/lib/sorcery/model/submodules/remember_me.rb)): +**Remember Me** (see [lib/sorcery/model/submodules/remember_me.rb](https://github.com/Sorcery/sorcery/blob/master/lib/sorcery/model/submodules/remember_me.rb)): * Remember me with configurable expiration. * configurable attribute names. * configurable to persist globally (supporting multiple browsers at the same time), or starting anew after each login -**Session Timeout** (see [lib/sorcery/controller/submodules/session_timeout.rb](https://github.com/NoamB/sorcery/blob/master/lib/sorcery/controller/submodules/session_timeout.rb)): +**Session Timeout** (see [lib/sorcery/controller/submodules/session_timeout.rb](https://github.com/Sorcery/sorcery/blob/master/lib/sorcery/controller/submodules/session_timeout.rb)): * Configurable session timeout. * Optionally session timeout will be calculated from last user action. -**Brute Force Protection** (see [lib/sorcery/model/submodules/brute_force_protection.rb](https://github.com/NoamB/sorcery/blob/master/lib/sorcery/model/submodules/brute_force_protection.rb)): +**Brute Force Protection** (see [lib/sorcery/model/submodules/brute_force_protection.rb](https://github.com/Sorcery/sorcery/blob/master/lib/sorcery/model/submodules/brute_force_protection.rb)): * Brute force login hammering protection. * configurable logins before lock and lock duration. -**Basic HTTP Authentication** (see [lib/sorcery/controller/submodules/http_basic_auth.rb](https://github.com/NoamB/sorcery/blob/master/lib/sorcery/controller/submodules/http_basic_auth.rb)): +**Basic HTTP Authentication** (see [lib/sorcery/controller/submodules/http_basic_auth.rb](https://github.com/Sorcery/sorcery/blob/master/lib/sorcery/controller/submodules/http_basic_auth.rb)): * A before action for requesting authentication with HTTP Basic. * automatic login from HTTP Basic. * automatic login is disabled if session key changed. -**Activity Logging** (see [lib/sorcery/model/submodules/activity_logging.rb](https://github.com/NoamB/sorcery/blob/master/lib/sorcery/model/submodules/activity_logging.rb)): +**Activity Logging** (see [lib/sorcery/model/submodules/activity_logging.rb](https://github.com/Sorcery/sorcery/blob/master/lib/sorcery/model/submodules/activity_logging.rb)): * automatic logging of last login, last logout, last activity time and IP address for last login. @@ -252,7 +259,7 @@ STI is supported via the `user.subclasses_inherit_config` setting in config/init list of logged in users. -**External** (see [lib/sorcery/controller/submodules/external.rb](https://github.com/NoamB/sorcery/blob/master/lib/sorcery/controller/submodules/external.rb)): +**External** (see [lib/sorcery/controller/submodules/external.rb](https://github.com/Sorcery/sorcery/blob/master/lib/sorcery/controller/submodules/external.rb)): * OAuth1 and OAuth2 support (currently: Twitter, Facebook, Github, Google, Heroku, LinkedIn, VK, LiveID, Xing, and Salesforce) diff --git a/lib/generators/sorcery/templates/initializer.rb b/lib/generators/sorcery/templates/initializer.rb index 8a736c3f..36623bb3 100644 --- a/lib/generators/sorcery/templates/initializer.rb +++ b/lib/generators/sorcery/templates/initializer.rb @@ -73,7 +73,7 @@ # -- external -- - # What providers are supported by this app, i.e. [:twitter, :facebook, :github, :linkedin, :xing, :google, :liveid, :salesforce] . + # What providers are supported by this app, i.e. [:twitter, :facebook, :github, :linkedin, :xing, :google, :liveid, :salesforce, :slack] . # Default: `[]` # # config.external_providers = @@ -144,6 +144,11 @@ # config.vk.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=vk" # config.vk.user_info_mapping = {:login => "domain", :name => "full_name"} # + #config.slack.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=slack" + #config.slack.key = '' + #config.slack.secret = '' + #config.slack.user_info_mapping = {email: 'email'} + # # To use liveid in development mode you have to replace mydomain.com with # a valid domain even in development. To use a valid domain in development # simply add your domain in your /etc/hosts file in front of 127.0.0.1 diff --git a/lib/sorcery/controller/submodules/external.rb b/lib/sorcery/controller/submodules/external.rb index e7988dfd..8688039e 100644 --- a/lib/sorcery/controller/submodules/external.rb +++ b/lib/sorcery/controller/submodules/external.rb @@ -20,6 +20,7 @@ def self.included(base) require 'sorcery/providers/jira' require 'sorcery/providers/salesforce' require 'sorcery/providers/paypal' + require 'sorcery/providers/slack' Config.module_eval do class << self diff --git a/lib/sorcery/providers/slack.rb b/lib/sorcery/providers/slack.rb new file mode 100644 index 00000000..129cf5c3 --- /dev/null +++ b/lib/sorcery/providers/slack.rb @@ -0,0 +1,46 @@ +module Sorcery + module Providers + # This class adds support for OAuth with slack.com. + + class Slack < Base + + include Protocols::Oauth2 + + attr_accessor :auth_path, :scope, :token_url, :user_info_path + + def initialize + super + + @scope = 'identity.basic, identity.email' + @site = 'https://slack.com/' + @user_info_path = 'https://slack.com/api/users.identity' + @auth_path = '/oauth/authorize' + @token_url = '/api/oauth.access' + end + + def get_user_hash(access_token) + response = access_token.get(user_info_path, params: { token: access_token.token }) + auth_hash(access_token).tap do |h| + h[:user_info] = JSON.parse(response.body) + h[:user_info]['email'] = h[:user_info]['user']['email'] + h[:uid] = h[:user_info]['user']['id'] + end + end + + # calculates and returns the url to which the user should be redirected, + # to get authenticated at the external provider's site. + def login_url(params, session) + authorize_url({ authorize_url: auth_path }) + end + + # tries to login the user from access token + def process_callback(params, session) + args = {}.tap do |a| + a[:code] = params[:code] if params[:code] + end + + get_access_token(args, token_url: token_url, token_method: :post) + end + end + end +end diff --git a/sorcery.gemspec b/sorcery.gemspec index 638094ef..cc4d4fb7 100644 --- a/sorcery.gemspec +++ b/sorcery.gemspec @@ -34,4 +34,3 @@ Gem::Specification.new do |s| s.add_development_dependency "rspec-rails", "~> 3.1.0" s.add_development_dependency "test-unit", "~> 3.1.0" end - diff --git a/spec/controllers/controller_oauth2_spec.rb b/spec/controllers/controller_oauth2_spec.rb index 73b09ee7..c238219a 100644 --- a/spec/controllers/controller_oauth2_spec.rb +++ b/spec/controllers/controller_oauth2_spec.rb @@ -152,7 +152,7 @@ expect(flash[:notice]).to eq "Success!" end - [:github, :google, :liveid, :vk, :salesforce, :paypal].each do |provider| + [:github, :google, :liveid, :vk, :salesforce, :paypal, :slack].each do |provider| describe "with #{provider}" do @@ -205,7 +205,7 @@ end sorcery_reload!([:user_activation,:external], :user_activation_mailer => ::SorceryMailer) - sorcery_controller_property_set(:external_providers, [:facebook, :github, :google, :liveid, :vk, :salesforce, :paypal]) + sorcery_controller_property_set(:external_providers, [:facebook, :github, :google, :liveid, :vk, :salesforce, :paypal, :slack]) sorcery_controller_external_property_set(:facebook, :key, "eYVNBjBDi33aa9GkA3w") sorcery_controller_external_property_set(:facebook, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") @@ -228,8 +228,13 @@ sorcery_controller_external_property_set(:paypal, :key, "eYVNBjBDi33aa9GkA3w") sorcery_controller_external_property_set(:paypal, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") sorcery_controller_external_property_set(:paypal, :callback_url, "http://blabla.com") + sorcery_controller_external_property_set(:slack, :key, "eYVNBjBDi33aa9GkA3w") + sorcery_controller_external_property_set(:slack, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") + sorcery_controller_external_property_set(:slack, :callback_url, "http://blabla.com") end + + after(:all) do if SORCERY_ORM == :active_record ActiveRecord::Migrator.rollback("#{Rails.root}/db/migrate/activation") @@ -287,7 +292,7 @@ end end - %w(facebook github google liveid vk salesforce).each do |provider| + %w(facebook github google liveid vk salesforce slack).each do |provider| context "when #{provider}" do before(:each) do sorcery_controller_property_set(:register_login_time, true) @@ -327,7 +332,7 @@ let(:user) { double('user', id: 42) } - %w(facebook github google liveid vk salesforce).each do |provider| + %w(facebook github google liveid vk salesforce slack).each do |provider| context "when #{provider}" do before(:each) do sorcery_model_property_set(:authentications_class, Authentication) @@ -389,7 +394,13 @@ def stub_all_oauth2_requests! "first_name"=>"Noam", "last_name"=>"Ben Ari" } - ]}.to_json } + ], + "user"=> { + "name"=>"Sonny Whether", + "id"=>"123", + "email"=>"bobby@example.com" + } + }.to_json } allow(access_token).to receive(:get) { response } allow(access_token).to receive(:token) { "187041a618229fdaf16613e96e1caabc1e86e46bbfad228de41520e63fe45873684c365a14417289599f3" } # access_token params for VK auth @@ -398,7 +409,7 @@ def stub_all_oauth2_requests! end def set_external_property - sorcery_controller_property_set(:external_providers, [:facebook, :github, :google, :liveid, :vk, :salesforce, :paypal]) + sorcery_controller_property_set(:external_providers, [:facebook, :github, :google, :liveid, :vk, :salesforce, :paypal, :slack]) sorcery_controller_external_property_set(:facebook, :key, "eYVNBjBDi33aa9GkA3w") sorcery_controller_external_property_set(:facebook, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") sorcery_controller_external_property_set(:facebook, :callback_url, "http://blabla.com") @@ -420,6 +431,9 @@ def set_external_property sorcery_controller_external_property_set(:paypal, :key, "eYVNBjBDi33aa9GkA3w") sorcery_controller_external_property_set(:paypal, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") sorcery_controller_external_property_set(:paypal, :callback_url, "http://blabla.com") + sorcery_controller_external_property_set(:slack, :key, "eYVNBjBDi33aa9GkA3w") + sorcery_controller_external_property_set(:slack, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") + sorcery_controller_external_property_set(:slack, :callback_url, "http://blabla.com") end def provider_url(provider) @@ -429,7 +443,8 @@ def provider_url(provider) google: "https://accounts.google.com/o/oauth2/auth?client_id=#{::Sorcery::Controller::Config.google.key}&display&redirect_uri=http%3A%2F%2Fblabla.com&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile&state", liveid: "https://oauth.live.com/authorize?client_id=#{::Sorcery::Controller::Config.liveid.key}&display&redirect_uri=http%3A%2F%2Fblabla.com&response_type=code&scope=wl.basic+wl.emails+wl.offline_access&state", vk: "https://oauth.vk.com/authorize?client_id=#{::Sorcery::Controller::Config.vk.key}&display&redirect_uri=http%3A%2F%2Fblabla.com&response_type=code&scope=#{::Sorcery::Controller::Config.vk.scope}&state", - salesforce: "https://login.salesforce.com/services/oauth2/authorize?client_id=#{::Sorcery::Controller::Config.salesforce.key}&display&redirect_uri=http%3A%2F%2Fblabla.com&response_type=code&scope#{'=' + ::Sorcery::Controller::Config.salesforce.scope unless ::Sorcery::Controller::Config.salesforce.scope.nil?}&state" + salesforce: "https://login.salesforce.com/services/oauth2/authorize?client_id=#{::Sorcery::Controller::Config.salesforce.key}&display&redirect_uri=http%3A%2F%2Fblabla.com&response_type=code&scope#{'=' + ::Sorcery::Controller::Config.salesforce.scope unless ::Sorcery::Controller::Config.salesforce.scope.nil?}&state", + slack: "https://slack.com/oauth/authorize?client_id=#{::Sorcery::Controller::Config.slack.key}&display&redirect_uri=http%3A%2F%2Fblabla.com&response_type=code&scope=identity.basic%2C+identity.email&state" }[provider] end end diff --git a/spec/rails_app/app/controllers/sorcery_controller.rb b/spec/rails_app/app/controllers/sorcery_controller.rb index d5857871..8f1f3c78 100644 --- a/spec/rails_app/app/controllers/sorcery_controller.rb +++ b/spec/rails_app/app/controllers/sorcery_controller.rb @@ -121,6 +121,10 @@ def login_at_test_salesforce login_at(:salesforce) end + def login_at_test_slack + login_at(:slack) + end + def login_at_test_with_state login_at(:facebook, {state: 'bla'}) end @@ -199,6 +203,14 @@ def test_login_from_salesforce end end + def test_login_from_slack + if @user = login_from(:slack) + redirect_to 'bla', notice: 'Success!' + else + redirect_to 'blu', alert: 'Failed!' + end + end + def test_return_to_with_external_twitter if @user = login_from(:twitter) redirect_back_or_to 'bla', notice: 'Success!' @@ -273,6 +285,14 @@ def test_return_to_with_external_salesforce end end + def test_return_to_with_external_slack + if @user = login_from(:slack) + redirect_back_or_to 'bla', notice: 'Success!' + else + redirect_to 'blu', alert: 'Failed!' + end + end + def test_create_from_provider provider = params[:provider] login_from(provider) diff --git a/spec/rails_app/config/routes.rb b/spec/rails_app/config/routes.rb index 20a56290..c75eb535 100644 --- a/spec/rails_app/config/routes.rb +++ b/spec/rails_app/config/routes.rb @@ -26,6 +26,7 @@ get :test_login_from_vk get :test_login_from_jira get :test_login_from_salesforce + get :test_login_from_slack get :login_at_test get :login_at_test_twitter get :login_at_test_facebook @@ -36,6 +37,7 @@ get :login_at_test_vk get :login_at_test_jira get :login_at_test_salesforce + get :login_at_test_slack get :test_return_to_with_external get :test_return_to_with_external_twitter get :test_return_to_with_external_facebook @@ -46,6 +48,7 @@ get :test_return_to_with_external_vk get :test_return_to_with_external_jira get :test_return_to_with_external_salesforce + get :test_return_to_with_external_slack get :test_http_basic_auth get :some_action_making_a_non_persisted_change_to_the_user post :test_login_with_remember diff --git a/spec/shared_examples/user_shared_examples.rb b/spec/shared_examples/user_shared_examples.rb index 242aef7d..27522b3e 100644 --- a/spec/shared_examples/user_shared_examples.rb +++ b/spec/shared_examples/user_shared_examples.rb @@ -262,11 +262,11 @@ class Admin2 < User; end let(:user_with_pass) { create_new_user({:username => 'foo_bar', :email => "foo@bar.com", :password => 'foobar'})} specify { expect(user_with_pass).to respond_to :valid_password? } - + it "returns true if password is correct" do expect(user_with_pass.valid_password?("foobar")).to be true end - + it "returns false if password is incorrect" do expect(user_with_pass.valid_password?("foobug")).to be false end @@ -541,7 +541,7 @@ def self.matches?(crypted,*tokens) User.sorcery_adapter.delete_all end - [:facebook, :github, :google, :liveid].each do |provider| + [:facebook, :github, :google, :liveid, :slack].each do |provider| it "does not send activation email to external users" do old_size = ActionMailer::Base.deliveries.size