Merge branch 'omniauth'

This commit is contained in:
José Valim
2010-11-09 22:28:05 +01:00
48 changed files with 575 additions and 948 deletions

View File

@@ -3,7 +3,8 @@
* after_update_path_for can no longer be defined in ApplicationController
* enhancements
* Added OAuth 2 support
* Added OmniAuth support
* Added ORM adapter to abstract ORM iteraction
* sign_out_via is available in the router to configure the method used for sign out (by github.com/martinrehfeld)
* Improved Ajax requests handling in failure app (by github.com/spastorino)
* Added request_keys to easily use request specific values (like subdomain) in authentication

10
Gemfile
View File

@@ -2,10 +2,11 @@ source "http://rubygems.org"
gemspec
gem "rails", "3.0.0"
gem "rails"
gem "webrat", "0.7.1"
gem "mocha", :require => false
gem "oauth2"
gem "oa-oauth", :require => "omniauth/oauth"
gem "oa-openid", :require => "omniauth/openid"
platforms :jruby do
gem 'activerecord-jdbcsqlite3-adapter'
@@ -13,9 +14,8 @@ end
platforms :ruby do
gem "sqlite3-ruby"
if RUBY_VERSION < '1.9'
gem "ruby-debug", ">= 0.10.3"
end
gem "ruby-debug", ">= 0.10.3" if RUBY_VERSION < '1.9'
group :mongoid do
gem "mongo", "1.0.7"
gem "mongoid", "2.0.0.beta.18"

View File

@@ -32,37 +32,38 @@ GEM
activesupport (= 3.0.0)
arel (~> 1.0.0)
tzinfo (~> 0.3.23)
activerecord-jdbc-adapter (0.9.7-java)
activerecord-jdbcsqlite3-adapter (0.9.7-java)
activerecord-jdbc-adapter (= 0.9.7)
jdbc-sqlite3 (>= 3.6.3.054)
activerecord-jdbc-adapter (1.0.2-java)
activerecord-jdbcsqlite3-adapter (1.0.2-java)
activerecord-jdbc-adapter (= 1.0.2)
jdbc-sqlite3 (~> 3.6.0)
activeresource (3.0.0)
activemodel (= 3.0.0)
activesupport (= 3.0.0)
activesupport (3.0.0)
addressable (2.1.2)
addressable (2.2.2)
arel (1.0.1)
activesupport (~> 3.0.0)
bcrypt-ruby (2.1.2)
bson (1.0.4)
bson_ext (1.0.7)
builder (2.1.2)
columnize (0.3.1)
columnize (0.3.2)
erubis (2.6.6)
abstract (>= 1.0.0)
faraday (0.5.0)
addressable (~> 2.1.1)
faraday (0.5.2)
addressable (~> 2.2.2)
multipart-post (~> 1.0.1)
rack (~> 1.2.1)
i18n (0.4.1)
jdbc-sqlite3 (3.6.3.054)
rack (>= 1.1.0, < 2)
i18n (0.4.2)
jdbc-sqlite3 (3.6.14.2.056-java)
linecache (0.43)
mail (2.2.6.1)
mail (2.2.9)
activesupport (>= 2.3.6)
mime-types
treetop (>= 1.4.5)
i18n (~> 0.4.1)
mime-types (~> 1.16)
treetop (~> 1.4.8)
mime-types (1.16)
mocha (0.9.8)
mocha (0.9.9)
rake
mongo (1.0.7)
bson (>= 1.0.4)
@@ -72,20 +73,36 @@ GEM
mongo (= 1.0.7)
tzinfo (~> 0.3.22)
will_paginate (~> 3.0.pre)
multi_json (0.0.4)
multi_json (0.0.5)
multipart-post (1.0.1)
nokogiri (1.4.3.1)
nokogiri (1.4.3.1-java)
weakling (>= 0.0.3)
oa-core (0.1.6)
rack (~> 1.1)
oa-oauth (0.1.6)
multi_json (~> 0.0.2)
nokogiri (~> 1.4.2)
oa-core (= 0.1.6)
oauth (~> 0.4.0)
oauth2 (~> 0.1.0)
oa-openid (0.1.6)
oa-core (= 0.1.6)
rack-openid (~> 1.2.0)
ruby-openid-apps-discovery
oauth (0.4.4)
oauth2 (0.1.0)
faraday (~> 0.5.0)
multi_json (~> 0.0.4)
orm_adapter (0.0.2)
orm_adapter (0.0.3)
polyglot (0.3.1)
rack (1.2.1)
rack-mount (0.6.13)
rack (>= 1.0.0)
rack-test (0.5.5)
rack-openid (1.2.0)
rack (>= 1.1.0)
ruby-openid (>= 2.1.8)
rack-test (0.5.6)
rack (>= 1.0)
rails (3.0.0)
actionmailer (= 3.0.0)
@@ -101,17 +118,20 @@ GEM
rake (>= 0.8.4)
thor (~> 0.14.0)
rake (0.8.7)
ruby-debug (0.10.3)
ruby-debug (0.10.4)
columnize (>= 0.1)
ruby-debug-base (~> 0.10.3.0)
ruby-debug-base (0.10.3)
ruby-debug-base (~> 0.10.4.0)
ruby-debug-base (0.10.4)
linecache (>= 0.3)
ruby-openid (2.1.8)
ruby-openid-apps-discovery (1.2.0)
ruby-openid (>= 2.1.7)
sqlite3-ruby (1.3.1)
thor (0.14.1)
thor (0.14.4)
treetop (1.4.8)
polyglot (>= 0.3.1)
tzinfo (0.3.23)
warden (1.0.0)
warden (1.0.1)
rack (>= 1.0.0)
weakling (0.0.4-java)
webrat (0.7.1)
@@ -132,9 +152,10 @@ DEPENDENCIES
mocha
mongo (= 1.0.7)
mongoid (= 2.0.0.beta.18)
oauth2
oa-oauth
oa-openid
orm_adapter (~> 0.0.2)
rails (= 3.0.0)
rails
ruby-debug (>= 0.10.3)
sqlite3-ruby
warden (~> 1.0.0)

View File

@@ -11,7 +11,7 @@ It's composed of 12 modules:
* Database Authenticatable: encrypts and stores a password in the database to validate the authenticity of an user while signing in. The authentication can be done both through POST requests or HTTP Basic Authentication.
* Token Authenticatable: signs in a user based on an authentication token (also known as "single access token"). The token can be given both through query string or HTTP Basic Authentication.
* Oauthable: adds OAuth2 support;
* Omniauthable: adds Omniauth (github.com/intridea/omniauth) support;
* Confirmable: sends emails with confirmation instructions and verifies whether an account is already confirmed during sign in.
* Recoverable: resets the user password and sends reset instructions.
* Registerable: handles signing up users through a registration process, also allowing them to edit and destroy their account.

View File

@@ -1,4 +0,0 @@
class Devise::OauthCallbacksController < ApplicationController
include Devise::Controllers::InternalHelpers
include Devise::Oauth::InternalHelpers
end

View File

@@ -0,0 +1,26 @@
class Devise::OmniauthCallbacksController < ApplicationController
include Devise::Controllers::InternalHelpers
def failure
set_flash_message :alert, :failure, :kind => failed_strategy.name.to_s.humanize, :reason => failure_message
redirect_to after_omniauth_failure_path_for(resource_name)
end
protected
def failed_strategy
env["omniauth.failed_strategy"]
end
def failure_message
exception = env["omniauth.error"]
error = exception.error_reason if exception.respond_to?(:error_reason)
error ||= exception.error if exception.respond_to?(:error)
error ||= env["omniauth.failure_key"]
error.to_s.humanize if error
end
def after_omniauth_failure_path_for(scope)
new_session_path(scope)
end
end

View File

@@ -19,6 +19,7 @@ class Devise::RegistrationsController < ApplicationController
sign_in_and_redirect(resource_name, resource)
else
set_flash_message :notice, :inactive_signed_up, :reason => resource.inactive_message.to_s
expire_session_data_after_sign_in!
redirect_to after_inactive_sign_up_path_for(resource)
end
else

View File

@@ -18,8 +18,8 @@
<%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %><br />
<% end -%>
<%- if devise_mapping.oauthable? %>
<%- resource_class.oauth_providers.each do |provider| %>
<%= link_to "Sign in with #{provider.to_s.titleize}", oauth_authorize_url(resource_name, provider) %><br />
<%- if devise_mapping.omniauthable? %>
<%- resource_class.omniauth_providers.each do |provider| %>
<%= link_to "Sign in with #{provider.to_s.titleize}", omniauth_authorize_path(resource_name, provider) %><br />
<% end -%>
<% end -%>

View File

@@ -34,7 +34,7 @@ en:
unlocks:
send_instructions: 'You will receive an email with instructions about how to unlock your account in a few minutes.'
unlocked: 'Your account was successfully unlocked. You are now signed in.'
oauth_callbacks:
omniauth_callbacks:
success: 'Successfully authorized from %{kind} account.'
failure: 'Could not authorize you from %{kind} because "%{reason}".'
mailer:

View File

@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
s.required_rubygems_version = Gem::Requirement.new("> 1.3.1") if s.respond_to? :required_rubygems_version=
s.authors = ["Jos\303\251 Valim", "Carlos Ant\303\264nio"]
s.date = %q{2010-10-10}
s.date = %q{2010-10-15}
s.description = %q{Flexible authentication solution for Rails with Warden}
s.email = %q{contact@plataformatec.com.br}
s.extra_rdoc_files = [
@@ -22,7 +22,7 @@ Gem::Specification.new do |s|
"MIT-LICENSE",
"README.rdoc",
"app/controllers/devise/confirmations_controller.rb",
"app/controllers/devise/oauth_callbacks_controller.rb",
"app/controllers/devise/omniauth_callbacks_controller.rb",
"app/controllers/devise/passwords_controller.rb",
"app/controllers/devise/registrations_controller.rb",
"app/controllers/devise/sessions_controller.rb",
@@ -65,7 +65,7 @@ Gem::Specification.new do |s|
"lib/devise/models/database_authenticatable.rb",
"lib/devise/models/encryptable.rb",
"lib/devise/models/lockable.rb",
"lib/devise/models/oauthable.rb",
"lib/devise/models/omniauthable.rb",
"lib/devise/models/recoverable.rb",
"lib/devise/models/registerable.rb",
"lib/devise/models/rememberable.rb",
@@ -74,12 +74,10 @@ Gem::Specification.new do |s|
"lib/devise/models/trackable.rb",
"lib/devise/models/validatable.rb",
"lib/devise/modules.rb",
"lib/devise/oauth.rb",
"lib/devise/oauth/config.rb",
"lib/devise/oauth/helpers.rb",
"lib/devise/oauth/internal_helpers.rb",
"lib/devise/oauth/test_helpers.rb",
"lib/devise/oauth/url_helpers.rb",
"lib/devise/omniauth.rb",
"lib/devise/omniauth/config.rb",
"lib/devise/omniauth/test_helpers.rb",
"lib/devise/omniauth/url_helpers.rb",
"lib/devise/orm/active_record.rb",
"lib/devise/orm/mongoid.rb",
"lib/devise/path_checker.rb",
@@ -121,7 +119,7 @@ Gem::Specification.new do |s|
"test/integration/database_authenticatable_test.rb",
"test/integration/http_authenticatable_test.rb",
"test/integration/lockable_test.rb",
"test/integration/oauthable_test.rb",
"test/integration/omniauthable_test.rb",
"test/integration/recoverable_test.rb",
"test/integration/registerable_test.rb",
"test/integration/rememberable_test.rb",
@@ -136,7 +134,6 @@ Gem::Specification.new do |s|
"test/models/database_authenticatable_test.rb",
"test/models/encryptable_test.rb",
"test/models/lockable_test.rb",
"test/models/oauthable_test.rb",
"test/models/recoverable_test.rb",
"test/models/rememberable_test.rb",
"test/models/timeoutable_test.rb",
@@ -144,8 +141,7 @@ Gem::Specification.new do |s|
"test/models/trackable_test.rb",
"test/models/validatable_test.rb",
"test/models_test.rb",
"test/oauth/config_test.rb",
"test/oauth/url_helpers_test.rb",
"test/omniauth/url_helpers_test.rb",
"test/orm/active_record.rb",
"test/orm/mongoid.rb",
"test/rails_app/app/active_record/admin.rb",
@@ -157,6 +153,7 @@ Gem::Specification.new do |s|
"test/rails_app/app/controllers/home_controller.rb",
"test/rails_app/app/controllers/publisher/registrations_controller.rb",
"test/rails_app/app/controllers/publisher/sessions_controller.rb",
"test/rails_app/app/controllers/users/omniauth_callbacks_controller.rb",
"test/rails_app/app/controllers/users_controller.rb",
"test/rails_app/app/helpers/application_helper.rb",
"test/rails_app/app/mongoid/admin.rb",

View File

@@ -5,7 +5,7 @@ require 'set'
module Devise
autoload :FailureApp, 'devise/failure_app'
autoload :Oauth, 'devise/oauth'
autoload :OmniAuth, 'devise/omniauth'
autoload :PathChecker, 'devise/path_checker'
autoload :Schema, 'devise/schema'
autoload :TestHelpers, 'devise/test_helpers'
@@ -170,7 +170,7 @@ module Devise
# Which formats should be treated as navigational.
mattr_accessor :navigational_formats
@@navigational_formats = [:html]
@@navigational_formats = [:"*/*", :html]
# When set to true, signing out an user signs out all other scopes.
mattr_accessor :sign_out_all_scopes
@@ -180,29 +180,21 @@ module Devise
mattr_accessor :sign_out_via
@@sign_out_via = :get
# Oauth providers
mattr_accessor :oauth_providers
@@oauth_providers = []
# PRIVATE CONFIGURATION
# Store scopes mappings.
mattr_reader :mappings
@@mappings = ActiveSupport::OrderedHash.new
# Oauth configurations.
mattr_reader :oauth_configs
@@oauth_configs = ActiveSupport::OrderedHash.new
# Omniauth configurations.
mattr_reader :omniauth_configs
@@omniauth_configs = ActiveSupport::OrderedHash.new
# Define a set of modules that are called when a mapping is added.
mattr_reader :helpers
@@helpers = Set.new
@@helpers << Devise::Controllers::Helpers
# Define a set of modules that are called when a provider is added.
mattr_reader :oauth_helpers
@@oauth_helpers = Set.new
# Private methods to interface with Warden.
mattr_accessor :warden_config
@@warden_config = nil
@@ -214,6 +206,10 @@ module Devise
yield self
end
def self.omniauth_providers
omniauth_configs.keys
end
def self.cookie_domain=(value)
ActiveSupport::Deprecation.warn "Devise.cookie_domain=(value) is deprecated. "
"Please use Devise.cookie_options = { :domain => value } instead."
@@ -312,29 +308,19 @@ module Devise
@@warden_config_block = block
end
# Specify an oauth provider.
# Specify an omniauth provider.
#
# config.oauth :github, APP_ID, APP_SECRET,
# :site => 'https://github.com/',
# :authorize_path => '/login/oauth/authorize',
# :access_token_path => '/login/oauth/access_token',
# :scope => %w(user public_repo)
# config.omniauth :github, APP_ID, APP_SECRET
#
def self.oauth(provider, *args)
@@helpers << Devise::Oauth::UrlHelpers
@@oauth_helpers << Devise::Oauth::InternalHelpers
@@oauth_providers << provider
@@oauth_providers.uniq!
@@oauth_helpers.each { |h| h.define_oauth_helpers(provider) }
@@oauth_configs[provider] = Devise::Oauth::Config.new(*args)
def self.omniauth(provider, *args)
@@helpers << Devise::OmniAuth::UrlHelpers
@@omniauth_configs[provider] = Devise::OmniAuth::Config.new(provider, args)
end
# Include helpers in the given scope to AC and AV.
def self.include_helpers(scope)
ActiveSupport.on_load(:action_controller) do
include scope::Helpers
include scope::Helpers if defined?(scope::Helpers)
include scope::UrlHelpers
end

View File

@@ -104,10 +104,11 @@ module Devise
scope = Devise::Mapping.find_scope!(resource_or_scope)
resource = args.last || resource_or_scope
expire_session_data_after_sign_in!
if options[:bypass]
warden.session_serializer.store(resource, scope)
else
expire_session_data_after_sign_in!
warden.set_user(resource, options.merge!(:scope => scope))
end
end
@@ -195,7 +196,13 @@ module Devise
options = args.extract_options!
scope = Devise::Mapping.find_scope!(resource_or_scope)
resource = args.last || resource_or_scope
sign_in(scope, resource, options) unless warden.user(scope) == resource
if warden.user(scope) == resource
expire_session_data_after_sign_in!
else
sign_in(scope, resource, options)
end
redirect_for_sign_in(scope, resource)
end
@@ -219,9 +226,10 @@ module Devise
redirect_to after_sign_out_path_for(scope)
end
# A hook called to expire session data after sign up/in. This is used
# by a few extensions, like oauth, to expire tokens stored in session.
# A hook called to expire session data after sign up/in. All keys
# stored under "devise." namespace are removed after sign in.
def expire_session_data_after_sign_in!
session.keys.grep(/^devise\./).each { |k| session.delete(k) }
end
end
end

View File

@@ -40,6 +40,11 @@ module Devise
raise "Could not find a valid mapping for #{duck}"
end
def self.find_by_path!(path, path_type=:fullpath)
Devise.mappings.each_value { |m| return m if path.include?(m.send(path_type)) }
raise "Could not find a valid mapping for path #{path}"
end
def initialize(name, options) #:nodoc:
@plural = (options[:as] ? "#{options[:as]}_#{name}" : name).to_sym
@singular = (options[:singular] || @plural.to_s.singularize).to_sym
@@ -84,7 +89,7 @@ module Devise
end
def fullpath
"#{@path_prefix}/#{@path}".squeeze("/")
"/#{@path_prefix}/#{@path}".squeeze("/")
end
# Create magic predicates for verifying what module is activated by this map.

View File

@@ -1,49 +0,0 @@
module Devise
module Models
# Adds OAuth support to your model. The whole workflow is deeply discussed in the
# README. This module adds just a class +oauth_access_token+ helper to your model
# which assists you on creating an access token. All the other OAuth hooks in
# Devise must be implemented by yourself in your application.
#
# == Options
#
# Oauthable adds the following options to devise_for:
#
# * +oauth_providers+: Which providers are avaialble to this model. It expects an array:
#
# devise_for :database_authenticatable, :oauthable, :oauth_providers => [:twitter]
#
module Oauthable
extend ActiveSupport::Concern
module ClassMethods
def oauth_configs #:nodoc:
Devise.oauth_configs.slice(*oauth_providers)
end
# Pass a token stored in the database to this object to get an OAuth2::AccessToken
# object back, as the one received in your model hook.
#
# For each provider you add, you may want to add a hook to retrieve the token based
# on the column you stored the token in the database. For example, you may want to
# the following for twitter:
#
# def oauth_twitter_token
# @oauth_twitter_token ||= self.class.oauth_access_token(:twitter, twitter_token)
# end
#
# You can call get, post, put and delete in this object to access Twitter's API.
def oauth_access_token(provider, token)
oauth_configs[provider].access_token_by_token(token)
end
# TODO Implement this method in the future.
# def refresh_oauth_token(provider, refresh_token)
# returns access_token
# end
Devise::Models.config(self, :oauth_providers)
end
end
end
end

View File

@@ -0,0 +1,23 @@
require 'devise/omniauth'
module Devise
module Models
# Adds OmniAuth support to your model.
#
# == Options
#
# Oauthable adds the following options to devise_for:
#
# * +omniauth_providers+: Which providers are avaialble to this model. It expects an array:
#
# devise_for :database_authenticatable, :omniauthable, :omniauth_providers => [:twitter]
#
module Omniauthable
extend ActiveSupport::Concern
module ClassMethods
Devise::Models.config(self, :omniauth_providers)
end
end
end
end

View File

@@ -11,7 +11,7 @@ Devise.with_options :model => true do |d|
# Other authentications
d.add_module :encryptable
d.add_module :oauthable, :controller => :oauth_callbacks, :route => :oauth_callback
d.add_module :omniauthable, :controller => :omniauth_callbacks, :route => :omniauth_callback
# Misc after
routes = [nil, :new, :edit]

View File

@@ -1,41 +0,0 @@
begin
require "oauth2"
rescue LoadError => e
warn "Could not load 'oauth2'. Please ensure you have the gem installed and listed in your Gemfile."
raise
end
module Devise
module Oauth
autoload :Config, "devise/oauth/config"
autoload :Helpers, "devise/oauth/helpers"
autoload :InternalHelpers, "devise/oauth/internal_helpers"
autoload :UrlHelpers, "devise/oauth/url_helpers"
autoload :TestHelpers, "devise/oauth/test_helpers"
class << self
delegate :short_circuit_authorizers!, :unshort_circuit_authorizers!, :to => "Devise::Oauth::TestHelpers"
def test_mode!
Faraday.default_adapter = :test
ActiveSupport.on_load(:action_controller) { include Devise::Oauth::TestHelpers }
ActiveSupport.on_load(:action_view) { include Devise::Oauth::TestHelpers }
end
def stub!(provider, stubs=nil, &block)
raise "You either need to pass stubs as a block or as a parameter" unless block_given? || stubs
stubs ||= Faraday::Adapter::Test::Stubs.new(&block)
Devise.oauth_configs[provider].build_connection do |b|
b.adapter :test, stubs
end
end
def reset_stubs!(*providers)
target = providers.any? ? Devise.oauth_configs.slice(*providers) : Devise.oauth_configs
target.each_value do |v|
v.build_connection { |b| b.adapter Faraday.default_adapter }
end
end
end
end
end

View File

@@ -1,33 +0,0 @@
require 'active_support/core_ext/array/wrap'
module Devise
module Oauth
# A configuration object that holds the OAuth2::Client object
# and all configuration values given config.oauth.
class Config
attr_reader :scope, :client
def initialize(app_id, app_secret, options)
@scope = Array.wrap(options.delete(:scope))
@client = OAuth2::Client.new(app_id, app_secret, options)
end
def authorize_url(options)
options[:scope] ||= @scope.join(',')
client.web_server.authorize_url(options)
end
def access_token_by_code(code, redirect_uri=nil)
client.web_server.get_access_token(code, :redirect_uri => redirect_uri)
end
def access_token_by_token(token)
OAuth2::AccessToken.new(client, token)
end
def build_connection(&block)
client.connection.build(&block)
end
end
end
end

View File

@@ -1,18 +0,0 @@
module Devise
module Oauth
# Provides a few helpers that are included in ActionController::Base
# for convenience.
module Helpers
protected
# Overwrite expire_session_data_after_sign_in! so it removes all
# oauth tokens from session ensuring registrations done in a row
# do not try to store the same token in the database.
def expire_session_data_after_sign_in!
super
session.keys.grep(/_oauth_token$/).each { |k| session.delete(k) }
end
end
end
end

View File

@@ -1,182 +0,0 @@
module Devise
module Oauth
module InternalHelpers
extend ActiveSupport::Concern
def self.define_oauth_helpers(name) #:nodoc:
alias_method(name, :callback_action)
public name
end
included do
helpers = %w(oauth_callback oauth_provider oauth_config)
hide_action *helpers
helper_method *helpers
before_filter :valid_oauth_callback?, :oauth_error_happened?
end
# Returns the oauth_callback (also aliased as oauth_provider) as a symbol.
# For example: :github.
def oauth_callback
@oauth_callback ||= action_name.to_sym
end
alias :oauth_provider :oauth_callback
# Returns the configuration object for this oauth callback.
def oauth_config
@oauth_client ||= resource_class.oauth_configs[oauth_callback]
end
protected
# This method checks three things:
#
# * If the URL being accessed is a valid provider for the given scope;
# * If code or error was streamed back from the server;
# * If the resource class implements the required hook;
#
def valid_oauth_callback?
unless oauth_config
unknown_action! "Skipping #{oauth_callback} OAuth because configuration " <<
"could not be found for model #{resource_name}."
end
unless params[:code] || params[:error] || params[:error_reason]
unknown_action! "Skipping #{oauth_callback} OAuth because code nor error were sent."
end
unless resource_class.respond_to?(oauth_model_callback)
raise "#{resource_class.name} does not respond to #{oauth_model_callback}. " <<
"Check the OAuth section in the README for more information."
end
end
# Check if an error was sent by the authorizer. If it happened, we redirect
# to url specified by after_oauth_failure_path_for, which defaults to new_session_path.
#
# By default, Devise shows a custom message from I18n saying the user could
# not be authenticated and the reason:
#
# en:
# devise:
# oauth_callbacks:
# failure: 'Could not authorize you from %{kind} because "%{reason}".'
#
# Let's suppose the reason returned by a Github was "access_denied". It will show:
#
# Could not authorize you from Github because "Access denied"
#
# And it will also be logged on console:
#
# github oauth failed: "access_denied".
#
# However, each specific error message can be customized using I18n:
#
# en:
# devise:
# oauth_callbacks:
# access_denied: 'You did not give access to our application on %{kind}.'
#
# Note "access_denied" follows the same lookup rule described in set_oauth_flash_message
# method. Besides, is important to remember most errors are specified by OAuth 2
# specification. But a few providers do not use them yet.
#
# TODO: Currently, Facebook is returning error_reason=user_denied when
# the user denies, but the specification defines error=access_denied instead.
def oauth_error_happened?
if error = params[:error] || params[:error_reason]
# Some providers returns access-denied instead of access_denied.
error = error.to_s.gsub("-", "_")
logger.warn "[Devise] #{oauth_callback} oauth failed: #{error.inspect}."
set_oauth_flash_message :alert, :failure, :reason => error.humanize
redirect_to after_oauth_failure_path_for(resource_name)
end
end
# The model method used as hook.
def oauth_model_callback #:nodoc:
"find_for_#{oauth_callback}_oauth"
end
# The session key to store the token.
def oauth_session_key #:nodoc:
"#{resource_name}_#{oauth_callback}_oauth_token"
end
# The callback redirect uri. Used to request the access token.
def oauth_redirect_uri #:nodoc:
oauth_callback_url(resource_name, oauth_callback)
end
# This is the implementation for all OAuth actions.
def callback_action
access_token = oauth_config.access_token_by_code(params[:code], oauth_redirect_uri)
self.resource = resource_class.send(oauth_model_callback, access_token, signed_in_resource)
if resource.persisted? && resource.errors.empty?
set_oauth_flash_message :notice, :success
sign_in_and_redirect resource_name, resource, :event => :authentication
else
session[oauth_session_key] = access_token.token
clean_up_passwords(resource)
render_for_oauth
end
end
# Handles oauth flash messages by adding a cascade. The default messages
# are always in the controller namespace:
#
# en:
# devise:
# oauth_callbacks:
# success: 'Successfully authorized from %{kind} account.'
# failure: 'Could not authorize you from %{kind} because "%{reason}".'
# skipped: 'Skipped Oauth authorization for %{kind}.'
#
# But they can also be nested according to the oauth provider:
#
# en:
# devise:
# oauth_callbacks:
# github:
# success: 'Hello coder! Welcome to our app!'
#
# And finally by Devise scope:
#
# en:
# devise:
# oauth_callbacks:
# admin:
# github:
# success: 'Hello coder with high permissions! Can I get a raise?'
#
def set_oauth_flash_message(key, type, options={})
options[:kind] = oauth_callback.to_s.titleize
options[:default] = type.to_sym
set_flash_message(key, "#{oauth_callback}.#{type}", options)
end
# Choose which template to render when a not persisted resource is
# returned in the find_for_x_oauth. By default, it renders registrations/new.
def render_for_oauth
render_with_scope :new, devise_mapping.controllers[:registrations]
end
# The default hook used by oauth to specify the redirect url for success.
def after_oauth_success_path_for(resource)
after_sign_in_path_for(resource)
end
# The default hook used by oauth to specify the redirect url for failure.
def after_oauth_failure_path_for(scope)
new_session_path(scope)
end
# Overwrite redirect_for_sign_in so it takes uses after_oauth_success_path_for.
def redirect_for_sign_in(scope, resource) #:nodoc:
redirect_to stored_location_for(scope) || after_oauth_success_path_for(resource)
end
end
end
end

View File

@@ -1,29 +0,0 @@
module Devise
module Oauth
module TestHelpers #:nodoc:
def self.short_circuit_authorizers!
module_eval <<-ALIASES, __FILE__, __LINE__ + 1
def oauth_authorize_url(scope, provider)
oauth_callback_path(scope, provider, :code => "12345")
end
ALIASES
Devise.mappings.each_value do |m|
next unless m.oauthable?
module_eval <<-ALIASES, __FILE__, __LINE__ + 1
def #{m.name}_oauth_authorize_url(provider)
#{m.name}_oauth_callback_path(provider, :code => "12345")
end
ALIASES
end
end
def self.unshort_circuit_authorizers!
module_eval do
instance_methods.each { |m| remove_method(m) }
end
end
end
end
end

View File

@@ -1,35 +0,0 @@
module Devise
module Oauth
module UrlHelpers
def self.define_helpers(mapping)
return unless mapping.oauthable?
class_eval <<-URL_HELPERS, __FILE__, __LINE__ + 1
def #{mapping.name}_oauth_authorize_url(provider, options={})
if config = Devise.oauth_configs[provider.to_sym]
options[:redirect_uri] ||= #{mapping.name}_oauth_callback_url(provider.to_s)
config.authorize_url(options)
else
raise ArgumentError, "Could not find oauth provider \#{provider.inspect}"
end
end
URL_HELPERS
end
def oauth_authorize_url(resource_or_scope, *args)
scope = Devise::Mapping.find_scope!(resource_or_scope)
send("#{scope}_oauth_authorize_url", *args)
end
def oauth_callback_url(resource_or_scope, *args)
scope = Devise::Mapping.find_scope!(resource_or_scope)
send("#{scope}_oauth_callback_url", *args)
end
def oauth_callback_path(resource_or_scope, *args)
scope = Devise::Mapping.find_scope!(resource_or_scope)
send("#{scope}_oauth_callback_path", *args)
end
end
end
end

47
lib/devise/omniauth.rb Normal file
View File

@@ -0,0 +1,47 @@
begin
require "omniauth/core"
rescue LoadError => e
warn "Could not load 'omniauth/core'. Please ensure you have the oa-core gem installed and listed in your Gemfile."
raise
end
module OmniAuth
# TODO HAXES Backport to OmniAuth
module Strategy #:nodoc:
def initialize(app, name, *args)
@app = app
@name = name.to_sym
@options = args.last.is_a?(Hash) ? args.pop : {}
yield self if block_given?
end
def fail!(message_key, exception = nil)
self.env['omniauth.error'] = exception
self.env['omniauth.failure_key'] = message_key
self.env['omniauth.failed_strategy'] = self
OmniAuth.config.on_failure.call(self.env, message_key.to_sym)
end
end
end
# Clean up the default path_prefix. It will be automatically set by Devise.
OmniAuth.config.path_prefix = nil
OmniAuth.config.on_failure = Proc.new do |env, key|
env['devise.mapping'] = Devise::Mapping.find_by_path!(env['PATH_INFO'], :path)
controller_klass = "#{env['devise.mapping'].controllers[:omniauth_callbacks].camelize}Controller"
controller_klass.constantize.action(:failure).call(env)
end
module Devise
module OmniAuth
autoload :Config, "devise/omniauth/config"
autoload :UrlHelpers, "devise/omniauth/url_helpers"
autoload :TestHelpers, "devise/omniauth/test_helpers"
class << self
delegate :short_circuit_authorizers!, :unshort_circuit_authorizers!,
:test_mode!, :stub!, :reset_stubs!, :to => "Devise::OmniAuth::TestHelpers"
end
end
end

View File

@@ -0,0 +1,30 @@
module Devise
module OmniAuth
class Config
attr_accessor :strategy
attr_reader :args
def initialize(provider, args)
@provider = provider
@args = args
@strategy = nil
end
def strategy_class
::OmniAuth::Strategies.const_get("#{::OmniAuth::Utils.camelize(@provider.to_s)}")
end
def check_if_allow_stubs!
raise "OmniAuth strategy for #{@provider} does not allow stubs, only OAuth2 ones do." unless allow_stubs?
end
def allow_stubs?
defined?(::OmniAuth::Strategies::OAuth2) && strategy.is_a?(::OmniAuth::Strategies::OAuth2)
end
def build_connection(&block)
strategy.client.connection.build(&block)
end
end
end
end

View File

@@ -0,0 +1,57 @@
module Devise
module OmniAuth
module TestHelpers
def self.test_mode!
Faraday.default_adapter = :test if defined?(Faraday)
ActiveSupport.on_load(:action_controller) { include Devise::OmniAuth::TestHelpers }
ActiveSupport.on_load(:action_view) { include Devise::OmniAuth::TestHelpers }
end
def self.stub!(provider, stubs=nil, &block)
raise "You either need to pass stubs as a block or as a parameter" unless block_given? || stubs
config = Devise.omniauth_configs[provider]
raise "Could not find configuration for #{provider.to_s} omniauth provider" unless config
config.check_if_allow_stubs!
stubs ||= Faraday::Adapter::Test::Stubs.new(&block)
config.build_connection do |b|
b.adapter :test, stubs
end
end
def self.reset_stubs!(*providers)
target = providers.any? ? Devise.omniauth_configs.slice(*providers) : Devise.omniauth_configs
target.each_value do |config|
next unless config.allow_stubs?
config.build_connection { |b| b.adapter Faraday.default_adapter }
end
end
def self.short_circuit_authorizers!
module_eval <<-ALIASES, __FILE__, __LINE__ + 1
def omniauth_authorize_path(*args)
omniauth_callback_path(*args)
end
ALIASES
Devise.mappings.each_value do |m|
next unless m.omniauthable?
module_eval <<-ALIASES, __FILE__, __LINE__ + 1
def #{m.name}_omniauth_authorize_path(provider)
#{m.name}_omniauth_callback_path(provider)
end
ALIASES
end
end
def self.unshort_circuit_authorizers!
module_eval do
instance_methods.each { |m| remove_method(m) }
end
end
end
end
end

View File

@@ -0,0 +1,29 @@
module Devise
module OmniAuth
module UrlHelpers
def self.define_helpers(mapping)
return unless mapping.omniauthable?
class_eval <<-URL_HELPERS, __FILE__, __LINE__ + 1
def #{mapping.name}_omniauth_authorize_path(provider, params = {})
if Devise.omniauth_configs[provider.to_sym]
"/#{mapping.path}/auth/\#{provider}\#{'?'+params.to_param if params.present?}"
else
raise ArgumentError, "Could not find omniauth provider \#{provider.inspect}"
end
end
URL_HELPERS
end
def omniauth_authorize_path(resource_or_scope, *args)
scope = Devise::Mapping.find_scope!(resource_or_scope)
send("#{scope}_omniauth_authorize_path", *args)
end
def omniauth_callback_path(resource_or_scope, *args)
scope = Devise::Mapping.find_scope!(resource_or_scope)
send("#{scope}_omniauth_callback_path", *args)
end
end
end
end

View File

@@ -5,12 +5,6 @@ module Devise
class Engine < ::Rails::Engine
config.devise = Devise
# Skip eager load of controllers because it is handled by Devise
# to avoid loading unused controllers.
target = paths.is_a?(Hash) ? paths["app/controllers"] : paths.app.controllers
target.autoload!
target.skip_eager_load!
# Initialize Warden and copy its configurations.
config.app_middleware.use Warden::Manager do |config|
Devise.warden_config = config
@@ -19,18 +13,19 @@ module Devise
# Force routes to be loaded if we are doing any eager load.
config.before_eager_load { |app| app.reload_routes! }
initializer "devise.add_filters" do |app|
app.config.filter_parameters += [:password, :password_confirmation]
app.config.filter_parameters.uniq
end
initializer "devise.url_helpers" do
Devise.include_helpers(Devise::Controllers)
end
initializer "devise.oauth_url_helpers" do
if Devise.oauth_providers.any?
Devise.include_helpers(Devise::Oauth)
initializer "devise.omniauth" do |app|
Devise.omniauth_configs.each do |provider, config|
app.middleware.use config.strategy_class, *config.args do |strategy|
config.strategy = strategy
end
end
if Devise.omniauth_configs.any?
Devise.include_helpers(Devise::OmniAuth)
end
end
@@ -50,19 +45,5 @@ module Devise
"to your models and comment the current value in the config/initializers/devise.rb"
end
end
# Check all available mappings and only load related controllers.
def eager_load!
mappings = Devise.mappings.values.map(&:modules).flatten.uniq
controllers = Devise::CONTROLLERS.values_at(*mappings)
path = paths.is_a?(Hash) ? paths["app/controllers"].first : paths.app.controllers.first
matcher = /\A#{Regexp.escape(path)}\/(.*)\.rb\Z/
Dir.glob("#{path}/devise/{#{controllers.join(',')}}_controller.rb").sort.each do |file|
require_dependency file.sub(matcher, '\1')
end
super
end
end
end

View File

@@ -123,6 +123,30 @@ module ActionDispatch::Routing
# end
# end
#
# ==== Adding custom actions to override controllers
#
# You can pass a block to devise_for that will add any routes defined in the block to Devise's
# list of known actions. This is important if you add a custom action to a controller that
# overrides an out of the box Devise controller.
# For example:
#
# class RegistrationsController < Devise::RegistrationsController
# def update
# # do something different here
# end
#
# def deactivate
# # not a standard action
# # deactivate code here
# end
# end
#
# In order to get Devise to recognize the deactivate action, your devise_for entry should look like this,
#
# devise_for :owners, :controllers => { :registrations => "registrations" } do
# post "deactivate", :to => "registrations#deactivate", :as => "deactivate_registration"
# end
#
def devise_for(*resources)
options = resources.extract_options!
@@ -238,9 +262,17 @@ module ActionDispatch::Routing
end
end
def devise_oauth_callback(mapping, controllers) #:nodoc:
get "/oauth/:action/callback", :action => Regexp.union(mapping.to.oauth_providers.map(&:to_s)),
:to => controllers[:oauth_callbacks], :as => :oauth_callback
def devise_omniauth_callback(mapping, controllers) #:nodoc:
path_prefix = "/#{mapping.path}/auth"
if ::OmniAuth.config.path_prefix && ::OmniAuth.config.path_prefix != path_prefix
warn "[DEVISE] You can only add :omniauthable behavior to one model."
else
::OmniAuth.config.path_prefix = path_prefix
end
match "/auth/:action/callback", :action => Regexp.union(mapping.to.omniauth_providers.map(&:to_s)),
:to => controllers[:omniauth_callbacks], :as => :omniauth_callback
end
def with_devise_exclusive_scope(new_path, new_as) #:nodoc:

View File

@@ -4,7 +4,7 @@ module Devise
def model_contents
<<-CONTENT
# Include default devise modules. Others available are:
# :token_authenticatable, :encryptable, :confirmable, :lockable, :timeoutable and :oauthable
# :token_authenticatable, :encryptable, :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable

View File

@@ -141,21 +141,20 @@ Devise.setup do |config|
# Lists the formats that should be treated as navigational. Formats like
# :html, should redirect to the sign in page when the user does not have
# access, but formats like :xml or :json, should return 401.
#
# If you have any extra navigational formats, like :iphone or :mobile, you
# should add them to the navigational formats lists. Default is [:html]
# config.navigational_formats = [:html, :iphone]
# should add them to the navigational formats lists.
#
# The :"*/*" format below is required to match Internet Explorer requests.
# config.navigational_formats = [:"*/*", :html]
# The default HTTP method used to sign out a resource. Default is :get.
# config.sign_out_via = :get
# ==> OAuth2
# Add a new OAuth2 provider. Check the README for more information on setting
# up on your models and hooks. By default this is not set.
# config.oauth :github, 'APP_ID', 'APP_SECRET',
# :site => 'https://github.com/',
# :authorize_path => '/login/oauth/authorize',
# :access_token_path => '/login/oauth/access_token',
# :scope => %w(user public_repo)
# ==> OmniAuth
# Add a new OmniAuth provider. Check the wiki for more information on setting
# up on your models and hooks.
# config.omniauth :github, 'APP_ID', 'APP_SECRET', :scope => 'user,public_repo'
# ==> Warden configuration
# If you want to use other strategies, that are not supported by Devise, or

View File

@@ -28,6 +28,11 @@ class FailureTest < ActiveSupport::TestCase
assert_equal 302, @response.first
end
test 'return 302 status for wildcard requests' do
call_failure 'action_dispatch.request.formats' => nil, 'HTTP_ACCEPT' => '*/*'
assert_equal 302, @response.first
end
test 'return to the default redirect location' do
call_failure
assert_equal 'You need to sign in or sign up before continuing.', @request.flash[:alert]

View File

@@ -376,7 +376,7 @@ end
class AuthenticationSignOutViaTest < ActionController::IntegrationTest
def sign_in!(scope)
sign_in_as_user(:visit => send("new_#{scope}_session_path"))
sign_in_as_admin(:visit => send("new_#{scope}_session_path"))
assert warden.authenticated?(scope)
end

View File

@@ -1,244 +0,0 @@
require 'test_helper'
class OAuthableIntegrationTest < ActionController::IntegrationTest
FACEBOOK_INFO = {
:username => 'usertest',
:email => 'user@test.com'
}
ACCESS_TOKEN = {
:access_token => "plataformatec"
}
setup do
Devise::Oauth.short_circuit_authorizers!
end
teardown do
Devise::Oauth.unshort_circuit_authorizers!
Devise::Oauth.reset_stubs!
User.singleton_class.remove_possible_method(:find_for_github_oauth)
end
def stub_github!
def User.find_for_github_oauth(*); end
Devise::Oauth.stub!(:github) do |b|
b.post('/login/oauth/access_token') { [200, {}, ACCESS_TOKEN.to_json] }
end
end
def stub_facebook!(valid=true)
data = valid ? FACEBOOK_INFO : FACEBOOK_INFO.except(:email)
Devise::Oauth.stub!(:facebook) do |b|
b.post('/oauth/access_token') { [200, {}, ACCESS_TOKEN.to_json] }
b.get('/me?access_token=plataformatec') { [200, {}, data.to_json] }
end
end
test "[BASIC] setup with persisted user" do
stub_facebook!
assert_difference "User.count", 1 do
visit "/users/sign_in"
click_link "Sign in with Facebook"
end
assert_current_url "/"
assert_contain "Successfully authorized from Facebook account."
assert warden.authenticated?(:user)
assert_not warden.authenticated?(:admin)
assert "plataformatec", warden.user(:user).facebook_token
end
test "[BASIC] setup with not persisted user and follow up" do
stub_facebook!(false)
assert_no_difference "User.count" do
visit "/users/sign_in"
click_link "Sign in with Facebook"
end
assert_contain "1 error prohibited this user from being saved"
assert_contain "Email can't be blank"
assert_not warden.authenticated?(:user)
assert_not warden.authenticated?(:admin)
fill_in "Email", :with => "user.form@test.com"
click_button "Sign up"
assert_current_url "/"
assert_contain "You have signed up successfully."
assert_contain "Hello User user.form@test.com"
assert warden.authenticated?(:user)
assert_not warden.authenticated?(:admin)
assert "plataformatec", warden.user(:user).facebook_token
end
test "[BASIC] setup updating an existing user in database" do
stub_facebook!
user = create_user
assert_no_difference "User.count" do
visit "/users/sign_in"
click_link "Sign in with Facebook"
end
assert_current_url "/"
assert_contain "Successfully authorized from Facebook account."
assert_equal user, warden.user(:user)
assert_equal "plataformatec", user.reload.facebook_token
end
test "[BASIC] setup updating an existing user in session" do
stub_facebook!
# Create an user and change his e-mail
user = sign_in_as_user
user.email = "another@test.com"
user.save!
assert_no_difference "User.count" do
visit "/"
click_link "Sign in with Facebook"
end
assert_current_url "/"
assert_contain "Successfully authorized from Facebook account."
assert_equal user, warden.user(:user)
assert_equal "another@test.com", warden.user(:user).email
assert_equal "plataformatec", user.reload.facebook_token
end
test "[SESSION CLEANUP] ensures session is cleaned up after sign up" do
stub_facebook!(false)
assert_no_difference "User.count" do
visit "/users/sign_in"
click_link "Sign in with Facebook"
end
assert_contain "1 error prohibited this user from being saved"
fill_in "Email", :with => "user.form@test.com"
click_button "Sign up"
assert_contain "You have signed up successfully."
visit "/users/sign_out"
user = sign_in_as_user
assert_nil warden.user(:user).facebook_token
assert_equal user, warden.user(:user)
end
test "[SESSION CLEANUP] ensures session is cleaned up on cancel" do
stub_facebook!(false)
assert_no_difference "User.count" do
visit "/users/sign_in"
click_link "Sign in with Facebook"
end
assert_contain "1 error prohibited this user from being saved"
visit "/users/cancel"
user = sign_in_as_user
assert_nil warden.user(:user).facebook_token
assert_equal user, warden.user(:user)
end
test "[SESSION CLEANUP] ensures session is cleaned up on sign in" do
stub_facebook!(false)
assert_no_difference "User.count" do
visit "/users/sign_in"
click_link "Sign in with Facebook"
end
assert_contain "1 error prohibited this user from being saved"
user = sign_in_as_user
assert_nil warden.user(:user).facebook_token
assert_equal user, warden.user(:user)
end
test "[I18N] scopes messages based on oauth callback for success" do
stub_facebook!
store_translations :en, :devise => { :oauth_callbacks => {
:facebook => { :success => "Welcome facebooker" } } } do
visit "/users/sign_in"
click_link "Sign in with Facebook"
assert_contain "Welcome facebooker"
end
end
test "[I18N] scopes messages based on oauth callback and resource name for success" do
stub_facebook!
store_translations :en, :devise => { :oauth_callbacks => {
:user => { :facebook => { :success => "Welcome facebooker user" } },
:facebook => { :success => "Welcome facebooker" } } } do
visit "/users/sign_in"
click_link "Sign in with Facebook"
assert_contain "Welcome facebooker user"
end
end
test "[FAILURE] shows 404 if no code or error are given as params" do
assert_raise AbstractController::ActionNotFound do
visit "/users/oauth/facebook/callback"
end
end
test "[FAILURE] raises an error if model does not implement a hook" do
begin
visit "/users/oauth/github/callback?code=123456"
raise "Expected visit to raise an error"
rescue Exception => e
assert_match "User does not respond to find_for_github_oauth", e.message
end
end
test "[FAILURE] handles callback error parameter according to the specification" do
visit "/users/oauth/facebook/callback?error=access_denied"
assert_current_url "/users/sign_in"
assert_contain 'Could not authorize you from Facebook because "Access denied".'
end
test "[FAILURE] handles callback error_reason just for Facebook compatibility" do
visit "/users/oauth/facebook/callback?error_reason=access_denied"
assert_current_url "/users/sign_in"
assert_contain 'Could not authorize you from Facebook because "Access denied".'
end
test "[FAILURE][I18N] uses I18n for custom messages" do
visit "/users/oauth/facebook/callback?error=access_denied"
assert_current_url "/users/sign_in"
assert_contain 'Could not authorize you from Facebook because "Access denied"'
end
test "[FAILURE][I18N] uses I18n with oauth callback scope for custom messages" do
store_translations :en, :devise => { :oauth_callbacks => {
:facebook => { :failure => "Access denied bro" } } } do
visit "/users/oauth/facebook/callback?error=access_denied"
assert_current_url "/users/sign_in"
assert_contain "Access denied bro"
end
end
test "[FAILURE][I18N] uses I18n with oauth callback scope and resource name for custom messages" do
store_translations :en, :devise => { :oauth_callbacks => {
:user => { :facebook => { :failure => "Access denied user" } },
:facebook => { :failure => "Access denied bro" } } } do
visit "/users/oauth/facebook/callback?error=access_denied"
assert_current_url "/users/sign_in"
assert_contain "Access denied user"
end
end
end

View File

@@ -0,0 +1,107 @@
require 'test_helper'
class OmniauthableIntegrationTest < ActionController::IntegrationTest
FACEBOOK_INFO = {
:id => '12345',
:link => 'http://facebook.com/josevalim',
:email => 'user@example.com',
:first_name => 'Jose',
:last_name => 'Valim',
:website => 'http://blog.plataformatec.com.br'
}
ACCESS_TOKEN = {
:access_token => "plataformatec"
}
setup do
stub_facebook!
Devise::OmniAuth.short_circuit_authorizers!
end
teardown do
Devise::OmniAuth.unshort_circuit_authorizers!
Devise::OmniAuth.reset_stubs!
end
def stub_facebook!
Devise::OmniAuth.stub!(:facebook) do |b|
b.post('/oauth/access_token') { [200, {}, ACCESS_TOKEN.to_json] }
b.get('/me?access_token=plataformatec') { [200, {}, FACEBOOK_INFO.to_json] }
end
end
test "can access omniauth.auth in the env hash" do
visit "/users/sign_in"
click_link "Sign in with Facebook"
json = ActiveSupport::JSON.decode(response.body)
assert_equal "12345", json["uid"]
assert_equal "facebook", json["provider"]
assert_equal "josevalim", json["user_info"]["nickname"]
assert_equal FACEBOOK_INFO, json["extra"]["user_hash"].symbolize_keys
assert_equal "plataformatec", json["credentials"]["token"]
end
test "cleans up session on sign up" do
assert_no_difference "User.count" do
visit "/users/sign_in"
click_link "Sign in with Facebook"
end
assert session["devise.facebook_data"]
assert_difference "User.count" do
visit "/users/sign_up"
fill_in "Password", :with => "123456"
fill_in "Password confirmation", :with => "123456"
click_button "Sign up"
end
assert_current_url "/"
assert_contain "You have signed up successfully."
assert_contain "Hello User user@example.com"
assert_not session["devise.facebook_data"]
end
test "cleans up session on cancel" do
assert_no_difference "User.count" do
visit "/users/sign_in"
click_link "Sign in with Facebook"
end
assert session["devise.facebook_data"]
visit "/users/cancel"
assert !session["devise.facebook_data"]
end
test "cleans up session on sign in" do
assert_no_difference "User.count" do
visit "/users/sign_in"
click_link "Sign in with Facebook"
end
assert session["devise.facebook_data"]
user = sign_in_as_user
assert !session["devise.facebook_data"]
end
test "handles callback error parameter according to the specification" do
visit "/users/auth/facebook/callback?error=access_denied"
assert_current_url "/users/sign_in"
assert_contain 'Could not authorize you from Facebook because "Access denied".'
end
test "handles other exceptions from omniauth" do
Devise::OmniAuth.stub!(:facebook) do |b|
b.post('/oauth/access_token') { [401, {}, {}.to_json] }
end
visit "/users/sign_in"
click_link "Sign in with facebook"
assert_current_url "/users/sign_in"
assert_contain 'Could not authorize you from Facebook because "Invalid credentials".'
end
end

View File

@@ -29,7 +29,6 @@ class RegistrationTest < ActionController::IntegrationTest
click_button 'Sign up'
assert_contain 'You have signed up successfully. However, we could not sign you in because your account is unconfirmed.'
assert_contain 'Sign in'
assert_not_contain 'You have to confirm your account before continuing'
assert_not warden.authenticated?(:user)
@@ -168,13 +167,13 @@ class RegistrationTest < ActionController::IntegrationTest
test 'a user should be able to cancel sign up by deleting data in the session' do
get "/set"
assert_equal "something", @request.session["user_provider_oauth_token"]
assert_equal "something", @request.session["devise.foo_bar"]
get "/users/sign_up"
assert_equal "something", @request.session["user_provider_oauth_token"]
assert_equal "something", @request.session["devise.foo_bar"]
get "/users/cancel"
assert_nil @request.session["user_provider_oauth_token"]
assert_nil @request.session["devise.foo_bar"]
assert_redirected_to new_user_registration_path
end
end

View File

@@ -90,6 +90,20 @@ class MappingTest < ActiveSupport::TestCase
assert mapping.recoverable?
assert mapping.lockable?
assert_not mapping.confirmable?
assert_not mapping.oauthable?
assert_not mapping.omniauthable?
end
test 'find mapping by path' do
assert_raise RuntimeError do
Devise::Mapping.find_by_path!('/accounts/facebook/callback')
end
assert_nothing_raised do
Devise::Mapping.find_by_path!('/:locale/accounts/login')
end
assert_nothing_raised do
Devise::Mapping.find_by_path!('/accounts/facebook/callback', :path)
end
end
end

View File

@@ -1,21 +0,0 @@
require 'test_helper'
class OauthableTest < ActiveSupport::TestCase
teardown { Devise::Oauth.reset_stubs! }
test "oauth_configs returns all configurations relative to that model" do
swap User, :oauth_providers => [:github] do
assert_equal User.oauth_configs, Devise.oauth_configs.slice(:github)
end
end
test "oauth_access_token returns the token object for the given provider" do
Devise::Oauth.stub!(:facebook) do |b|
b.get('/me?access_token=plataformatec') { [200, {}, {}.to_json] }
end
access_token = User.oauth_access_token(:facebook, "plataformatec")
assert_kind_of OAuth2::AccessToken, access_token
assert_equal "{}", access_token.get("/me")
end
end

View File

@@ -1,44 +0,0 @@
require 'test_helper'
class OauthConfigTest < ActiveSupport::TestCase
ACCESS_TOKEN = {
:access_token => "plataformatec"
}
setup { @config = Devise.oauth_configs[:facebook] }
teardown { Devise::Oauth.reset_stubs! }
test "stored OAuth2::Client" do
assert_kind_of OAuth2::Client, @config.client
end
test "build authorize url" do
url = @config.authorize_url(:redirect_uri => "foo")
assert_match "https://graph.facebook.com/oauth/authorize?", url
assert_match "scope=email%2Coffline_access", url
assert_match "client_id=APP_ID", url
assert_match "type=web_server", url
assert_match "redirect_uri=foo", url
end
test "retrieves access token object by code" do
Devise::Oauth.stub!(:facebook) do |b|
b.post('/oauth/access_token') { [200, {}, ACCESS_TOKEN.to_json] }
b.get('/me?access_token=plataformatec') { [200, {}, {}.to_json] }
end
access_token = @config.access_token_by_code("12345")
assert_kind_of OAuth2::AccessToken, access_token
assert_equal "{}", access_token.get("/me")
end
test "retrieves access token object by token" do
Devise::Oauth.stub!(:facebook) do |b|
b.get('/me?access_token=plataformatec') { [200, {}, {}.to_json] }
end
access_token = @config.access_token_by_token("plataformatec")
assert_kind_of OAuth2::AccessToken, access_token
assert_equal "{}", access_token.get("/me")
end
end

View File

@@ -1,47 +0,0 @@
require 'test_helper'
class OauthRoutesTest < ActionController::TestCase
tests ApplicationController
def assert_path_and_url(action, provider)
# Resource param
assert_equal @controller.send(action, :user, provider),
@controller.send("user_#{action}", provider)
# Default url params
assert_equal @controller.send(action, :user, provider, :param => 123),
@controller.send("user_#{action}", provider, :param => 123)
# With an object
assert_equal @controller.send(action, User.new, provider, :param => 123),
@controller.send("user_#{action}", provider, :param => 123)
end
test 'should alias oauth_callback to mapped user auth_callback' do
assert_path_and_url :oauth_callback_path, :github
assert_path_and_url :oauth_callback_url, :github
assert_path_and_url :oauth_callback_path, :facebook
assert_path_and_url :oauth_callback_url, :facebook
end
test 'should alias oauth_authorize to mapped user auth_authorize' do
assert_path_and_url :oauth_authorize_url, :github
assert_path_and_url :oauth_authorize_url, :facebook
end
test 'should adds scope, provider and redirect_uri to authorize urls' do
url = @controller.oauth_authorize_url(:user, :github)
assert_match "https://github.com/login/oauth/authorize?", url
assert_match "scope=user%2Cpublic_repo", url
assert_match "client_id=APP_ID", url
assert_match "type=web_server", url
assert_match "redirect_uri=http%3A%2F%2Ftest.host%2Fusers%2Foauth%2Fgithub%2Fcallback", url
url = @controller.oauth_authorize_url(:user, :facebook)
assert_match "https://graph.facebook.com/oauth/authorize?", url
assert_match "scope=email%2Coffline_access", url
assert_match "client_id=APP_ID", url
assert_match "type=web_server", url
assert_match "redirect_uri=http%3A%2F%2Ftest.host%2Fusers%2Foauth%2Ffacebook%2Fcallback", url
end
end

View File

@@ -0,0 +1,47 @@
require 'test_helper'
class OmniAuthRoutesTest < ActionController::TestCase
tests ApplicationController
def assert_path(action, provider, with_param=true)
# Resource param
assert_equal @controller.send(action, :user, provider),
@controller.send("user_#{action}", provider)
# With an object
assert_equal @controller.send(action, User.new, provider),
@controller.send("user_#{action}", provider)
if with_param
# Default url params
assert_equal @controller.send(action, :user, provider, :param => 123),
@controller.send("user_#{action}", provider, :param => 123)
end
end
test 'should alias omniauth_callback to mapped user auth_callback' do
assert_path :omniauth_callback_path, :facebook
end
test 'should alias omniauth_authorize to mapped user auth_authorize' do
assert_path :omniauth_authorize_path, :facebook, false
end
test 'should generate authorization path' do
assert_match "/users/auth/facebook", @controller.omniauth_authorize_path(:user, :facebook)
assert_raise ArgumentError do
@controller.omniauth_authorize_path(:user, :github)
end
end
test 'should generate authorization path with params' do
assert_match "/users/auth/open_id?openid_url=http%3A%2F%2Fyahoo.com",
@controller.omniauth_authorize_path(:user, :open_id, :openid_url => "http://yahoo.com")
end
test 'should not add a "?" if no param was sent' do
assert_equal "/users/auth/open_id",
@controller.omniauth_authorize_path(:user, :open_id)
end
end

View File

@@ -6,7 +6,7 @@ class HomeController < ApplicationController
end
def set
session["user_provider_oauth_token"] = "something"
session["devise.foo_bar"] = "something"
head :ok
end
end

View File

@@ -0,0 +1,7 @@
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def facebook
data = env["omniauth.auth"]
session["devise.facebook_data"] = data["extra"]["user_hash"]
render :json => data
end
end

View File

@@ -1,5 +1 @@
Home!
<%- User.oauth_providers.each do |provider| %>
<%= link_to "Sign in with #{provider.to_s.titleize}", user_oauth_authorize_url(provider) %><br />
<% end -%>
Home!

View File

@@ -148,18 +148,9 @@ Devise.setup do |config|
# The default HTTP method used to sign out a resource. Default is :get.
# config.sign_out_via = :get
# ==> OAuth2
# Add a new OAuth2 provider. Check the README for more information on setting
# up on your models and hooks. By default this is not set.
config.oauth :github, 'APP_ID', 'APP_SECRET',
:site => 'https://github.com/',
:authorize_path => '/login/oauth/authorize',
:access_token_path => '/login/oauth/access_token',
:scope => 'user,public_repo'
config.oauth :facebook, 'APP_ID', 'APP_SECRET',
:site => 'https://graph.facebook.com/',
:scope => %w(email offline_access)
# ==> OmniAuth
config.omniauth :facebook, 'APP_ID', 'APP_SECRET', :scope => 'email,offline_access'
config.omniauth :open_id
# ==> Warden configuration
# If you want to use other strategies, that are not supported by Devise, or

View File

@@ -8,7 +8,7 @@ Rails.application.routes.draw do
resources :admins, :only => [:index]
# Users scope
devise_for :users do
devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks" } do
match "/devise_for/sign_in", :to => "devise/sessions#new"
end
@@ -30,11 +30,11 @@ Rails.application.routes.draw do
# Other routes for routing_test.rb
namespace :publisher, :path_names => { :sign_in => "i_dont_care", :sign_out => "get_out" } do
devise_for :accounts, :class_name => "User", :path_names => { :sign_in => "get_in" }
devise_for :accounts, :class_name => "Admin", :path_names => { :sign_in => "get_in" }
end
scope ":locale" do
devise_for :accounts, :singular => "manager", :class_name => "User",
devise_for :accounts, :singular => "manager", :class_name => "Admin",
:path_names => {
:sign_in => "login", :sign_out => "logout",
:password => "secret", :confirmation => "verification",
@@ -44,9 +44,9 @@ Rails.application.routes.draw do
end
namespace :sign_out_via, :module => "devise" do
devise_for :deletes, :sign_out_via => :delete, :class_name => "User"
devise_for :posts, :sign_out_via => :post, :class_name => "User"
devise_for :delete_or_posts, :sign_out_via => [:delete, :post], :class_name => "User"
devise_for :deletes, :sign_out_via => :delete, :class_name => "Admin"
devise_for :posts, :sign_out_via => :post, :class_name => "Admin"
devise_for :delete_or_posts, :sign_out_via => [:delete, :post], :class_name => "Admin"
end
match "/set", :to => "home#set"

View File

@@ -4,45 +4,20 @@ module SharedUser
included do
devise :database_authenticatable, :confirmable, :lockable, :recoverable,
:registerable, :rememberable, :timeoutable, :token_authenticatable,
:trackable, :validatable, :oauthable
:trackable, :validatable, :omniauthable
# They need to be included after Devise is called.
extend ExtendMethods
end
module ExtendMethods
def find_for_facebook_oauth(access_token, signed_in_resource=nil)
data = ActiveSupport::JSON.decode(access_token.get('/me'))
user = signed_in_resource || User.find_by_email(data["email"]) || User.new
user.update_with_facebook_oauth(access_token, data)
user.save
user
end
def new_with_session(params, session)
super.tap do |user|
if session[:user_facebook_oauth_token]
access_token = oauth_access_token(:facebook, session[:user_facebook_oauth_token])
user.update_with_facebook_oauth(access_token)
if data = session["devise.facebook_data"]
user.email = data["email"]
user.confirmed_at = Time.now
end
end
end
end
def update_with_facebook_oauth(access_token, data=nil)
data ||= ActiveSupport::JSON.decode(access_token.get('/me'))
self.username = data["username"] unless username.present?
self.email = data["email"] unless email.present?
self.confirmed_at ||= Time.now
self.facebook_token = access_token.token
unless encrypted_password.present?
self.password = Devise.friendly_token[0, 10]
self.password_confirmation = nil
end
yield self if block_given?
end
end

View File

@@ -91,15 +91,13 @@ class DefaultRoutingTest < ActionController::TestCase
assert_named_route "/users/cancel", :cancel_user_registration_path
end
test 'map oauth callbacks' do
assert_recognizes({:controller => 'devise/oauth_callbacks', :action => 'facebook'}, {:path => 'users/oauth/facebook/callback', :method => :get})
assert_named_route "/users/oauth/facebook/callback", :user_oauth_callback_path, :facebook
assert_recognizes({:controller => 'devise/oauth_callbacks', :action => 'github'}, {:path => 'users/oauth/github/callback', :method => :get})
assert_named_route "/users/oauth/github/callback", :user_oauth_callback_path, :github
test 'map omniauth callbacks' do
assert_recognizes({:controller => 'users/omniauth_callbacks', :action => 'facebook'}, {:path => 'users/auth/facebook/callback', :method => :get})
assert_recognizes({:controller => 'users/omniauth_callbacks', :action => 'facebook'}, {:path => 'users/auth/facebook/callback', :method => :post})
assert_named_route "/users/auth/facebook/callback", :user_omniauth_callback_path, :facebook
assert_raise ActionController::RoutingError do
assert_recognizes({:controller => 'devise/oauth_callbacks', :action => 'twitter'}, {:path => 'users/oauth/twitter/callback', :method => :get})
assert_recognizes({:controller => 'ysers/omniauth_callbacks', :action => 'twitter'}, {:path => 'users/auth/twitter/callback', :method => :get})
end
end
@@ -137,14 +135,6 @@ class CustomizedRoutingTest < ActionController::TestCase
assert_recognizes({:controller => 'devise/passwords', :action => 'new', :locale => 'en'}, '/en/accounts/secret/new')
end
test 'map account with custom path name for confirmation' do
assert_recognizes({:controller => 'devise/confirmations', :action => 'new', :locale => 'en'}, '/en/accounts/verification/new')
end
test 'map account with custom path name for unlock' do
assert_recognizes({:controller => 'devise/unlocks', :action => 'new', :locale => 'en'}, '/en/accounts/unblock/new')
end
test 'map account with custom path name for registration' do
assert_recognizes({:controller => 'devise/registrations', :action => 'new', :locale => 'en'}, '/en/accounts/management/register')
end

View File

@@ -16,7 +16,7 @@ Webrat.configure do |config|
config.open_error_files = false
end
Devise::Oauth.test_mode!
Devise::OmniAuth.test_mode!
# Add support to load paths so we can overwrite broken webrat setup
$:.unshift File.expand_path('../support', __FILE__)