Merge branch 'oauth'. Welcome to the future.

This commit is contained in:
José Valim 2010-07-26 20:33:41 +02:00
commit 33ce94363e
68 changed files with 1670 additions and 527 deletions

View File

@ -11,6 +11,7 @@ gem "sqlite3-ruby"
gem "webrat", "0.7.0"
gem "mocha", :require => false
gem "bcrypt-ruby", :require => "bcrypt"
gem "oauth2"
if RUBY_VERSION < '1.9'
gem "ruby-debug", ">= 0.10.3"

View File

@ -56,6 +56,7 @@ GEM
remote: http://rubygems.org/
specs:
abstract (1.0.0)
addressable (2.1.2)
arel (0.4.0)
activesupport (>= 3.0.0.beta)
bcrypt-ruby (2.1.2)
@ -65,6 +66,9 @@ GEM
columnize (0.3.1)
erubis (2.6.6)
abstract (>= 1.0.0)
faraday (0.4.6)
addressable (>= 2.1.1)
rack (>= 1.0.1)
i18n (0.4.1)
linecache (0.43)
mail (2.2.5)
@ -76,7 +80,11 @@ GEM
rake
mongo (1.0.5)
bson (>= 1.0.4)
multi_json (0.0.4)
nokogiri (1.4.2)
oauth2 (0.0.10)
faraday (~> 0.4.1)
multi_json (>= 0.0.4)
polyglot (0.3.1)
rack (1.2.1)
rack-mount (0.6.9)
@ -111,6 +119,7 @@ DEPENDENCIES
mocha
mongo
mongoid!
oauth2
rails!
ruby-debug (>= 0.10.3)
sqlite3-ruby

View File

@ -7,10 +7,11 @@ Devise is a flexible authentication solution for Rails based on Warden. It:
* Allows you to have multiple roles (or models/scopes) signed in at the same time;
* Is based on a modularity concept: use just what you really need.
Right now it's composed of 11 modules:
Right now 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 an 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;
* 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.
@ -99,15 +100,9 @@ Configure your routes after setting up your model. Open your config/routes.rb fi
devise_for :users
This will use your User model to create a set of needed routes (you can see them by running `rake routes`).
This will use your User model to create a set of needed routes (you can see them by running `rake routes`). If you invoked the devise generator, you noticed that this is exactly what the generator produces for us: model, routes and migrations.
Options for configuring your routes include :class_name (to set the class for that route), :path_prefix, :path and :path_names, where the last two have the same meaning as in common routes. The available :path_names are:
devise_for :users, :path => "usuarios", :path_names => { :sign_in => 'login', :sign_out => 'logout', :password => 'secret', :confirmation => 'verification', :unlock => 'unblock', :registration => 'register', :sign_up => 'cmon_let_me_in' }
Be sure to check devise_for documentation for details.
This exactly what the devise generator produces for you: model, routes and migrations. Don't forget to run rake db:migrate and you are ready to go! But don't stop reading here, we still have a lot to tell you.
Don't forget to run rake db:migrate and you are ready to go! But don't stop reading here, we still have a lot to tell you.
== Controller filters and helpers
@ -179,9 +174,13 @@ Since Devise is an engine, all its views are packaged inside the gem. These view
rails generate devise:views
However, if you have more than one role in your application (such as "User" and "Admin"), you will notice that Devise uses the same views for all roles. Fortunately, Devise offers an easy way to customize views. All you need to do is set "config.scoped_views = true" inside "config/initializers/devise.rb".
If you are using HAML, you will need hpricot installed to convert Devise views to HAML.
After doing so, you will be able to have views based on the role like "users/sessions/new" and "admins/sessions/new". If no view is found within the scope, Devise will use the default view at "devise/sessions/new".
If you have more than one role in your application (such as "User" and "Admin"), you will notice that Devise uses the same views for all roles. Fortunately, Devise offers an easy way to customize views. All you need to do is set "config.scoped_views = true" inside "config/initializers/devise.rb".
After doing so, you will be able to have views based on the role like "users/sessions/new" and "admins/sessions/new". If no view is found within the scope, Devise will use the default view at "devise/sessions/new". You can also use the generator to generate scoped views:
rails generate devise:views users
== Configuring controllers
@ -194,12 +193,28 @@ If the customization at the views level is not enough, you can customize each co
2) Tell the router to use this controller:
devise_for :admins, :controllers => { :sessions => "admin/sessions" }
devise_for :admins, :controllers => { :sessions => "admins/sessions" }
3) And since we changed the controller, it won't use the "devise/sessions" views, so remember to copy "devise/sessions" to "admin/sessions".
Remember that Devise uses flash messages to let users know if sign in was successful or failed. Devise expects your application to call "flash[:notice]" and "flash[:alert]" as appropriate.
== Configuring routes
Devise also ships with default routes. If you need to customize them, you should probably be able to do it through the devise_for method. It accepts several options like :class_name, :path_prefix and so on, including the possibility to change path names for I18n:
devise_for :users, :path => "usuarios", :path_names => { :sign_in => 'login', :sign_out => 'logout', :password => 'secret', :confirmation => 'verification', :unlock => 'unblock', :registration => 'register', :sign_up => 'cmon_let_me_in' }
Be sure to check devise_for documentation for details.
If you have the need for more deep customization, for instance to also allow "/sign_in" besides "/users/sign_in", all you need to do is to create your routes normally and wrap them in a +devise_scope+ block in the router:
devise_scope :user do
get "sign_in", :to => "devise/sessions#new"
end
This way you tell devise to use the scope :user when "/sign_in" is accessed. Notice +devise_scope+ is also aliased as +as+, feel free to choose the one you prefer.
== I18n
Devise uses flash messages with I18n with the flash keys :success and :failure. To customize your app, you can set up your locale file:
@ -219,15 +234,16 @@ You can also create distinct messages based on the resource you've configured us
admin:
signed_in: 'Hello admin!'
The Devise mailer uses the same pattern to create subject messages:
The Devise mailer uses a similar pattern to create subject messages:
en:
devise:
mailer:
confirmation_instructions: 'Hello everybody!'
user:
confirmation_instructions: 'Hello User! Please confirm your email'
reset_password_instructions: 'Reset instructions'
confirmation_instructions:
subject: 'Hello everybody!'
user_subject: 'Hello User! Please confirm your email'
reset_password_instructions:
subject: 'Reset instructions'
Take a look at our locale file to check all available messages.
@ -249,13 +265,179 @@ You can include the Devise Test Helpers in all of your tests by adding the follo
Do not use such helpers for integration tests such as Cucumber or Webrat. Instead, fill in the form or explicitly set the user in session. For more tips, check the wiki (http://wiki.github.com/plataformatec/devise).
== OAuth2
Since version 1.2, Devise comes with OAuth support out of the box. To create OAuth support from Github for example, first you need to register your app in Github and add it as provider in your devise initializer:
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)
Then, you need to mark your model as oauthable:
class User < ActiveRecord::Base
devise :database_authenticatable, :oauthable
end
And add a link to your views/sign up form:
<%= link_to "Sign in as User with Github", user_oauth_authorize_url(:github) %>
This link will send the user straight to Github. After the user authorizes your application, Github will redirect the user back to your application at "/users/oauth/github/callback". This URL will be handled *internally* and *automatically* by +Devise::OauthCallbacksController#github+ action, which looks like this:
def github
access_token = github_config.access_token_by_code(params[:code])
@user = User.find_for_github_oauth(access_token, signed_in_resource)
if @user && @user.persisted?
sign_in @user
set_oauth_flash_message :notice, :success
redirect_to after_oauth_success_path_for(@user) #=> redirects to user_root_path or root_path
elsif @user
session[:user_github_oauth_token] = access_token.token
render_for_auth #=> renders sign up view by default
else
set_oauth_flash_message :alert, :skipped
redirect_to after_oauth_skipped_path_for(:user) #=> redirects to user_new_session_path
end
end
In other words, Devise does all the work for you but it expects you to implement the +find_for_github_oauth+ method in your model that receives two arguments: the first is an +access_token+ object from OAuth2 library (http://github.com/intridea/oauth2) and the second is the signed in resource which we will ignore for this while. Depending on what this method returns, Devise act in a different way as seen above.
A basic implementation for +find_for_github_oauth+ would be:
def self.find_for_github_oauth(access_token, signed_in_resource=nil)
# Get the user email info from Github for sign up
data = ActiveSupport::JSON.decode(access_token.get('/api/v2/json/user/show'))["user"]
if user = User.find_by_email(data["email"])
user
else
# Create an user with a stub password.
User.create!(:name => user["name"], :email => user["email"], :password => Devise.friendly_token)
end
end
First, notice the +access_token+ object allows you to make requests to the provider using get/post/put/delete methods to retrieve user information. Next, our method above has two branches and both of them returns a persisted user. So, if we go back to our github action above, we will see that after returning a persisted record, it will sign in the returned user and redirect to the configured +after_oauth_success_path_for+ with a flash message. This flash message is retrieved from I18n and looks like this:
en:
devise:
oauth_callbacks:
# Higher priority message:
user:
github:
success: 'Hello dear user! Welcome to our app!'
# With medium priority
github:
success: 'Hello coder! Welcome to our app!'
# With lower priority
success: 'Successfully authorized from %{kind} account.'
Our basic implementation assumes that all information retrieved from Github is enough for us to create an user, however this may not be true for all providers. That said, Devise allows +find_for_github_oauth+ to have different outcomes. For instance, if it returns a record which was not persisted (usually a new record with errors), it will render the sign up views from the registrations controller and show all error messages. On the other hand, if you decide to return nil from +find_for_github_oauth+, Devise will consider that you decided to skip the authentication and will redirect to +after_oauth_skipped_path_for+ (defaults to the sign in page) with the skipped flash message.
All these methods +after_oauth_skipped_path_for+, +render_for_oauth+ and so on can be customized and overwritten in your application by inheriting from Devise::OauthCallbacksController as we have seen above in the "Configuring controllers" section.
For last but not least, Devise also supports linking accounts. The setup discussed above only uses Github for sign up and assumes that after the user signs up, there will not have any interaction with Github at all. However, this is not true for some applications.
If you need to interact with Github after sign up, the first step is to create a +github_token+ in the database and store in it in the +access_token+ given to +find_for_github_oauth+. You may also want to allow an already signed in user to link his account to a Github account without a need to sign up again. This is where the +signed_in_resource+ we discussed earlier takes place. If +find_for_github_oauth+ receives a signed in resource as parameter, you can link the github account to it like below:
def self.find_for_github_oauth(access_token, signed_in_resource=nil)
data = ActiveSupport::JSON.decode(access_token.get('/api/v2/json/user/show'))["user"]
# Link the account if an e-mail already exists in the database
# or a signed_in_resource, which is already in session was given.
if user = signed_in_resource || User.find_by_email(data["email"])
user.update_attribute(:github_token, access_token.token)
user
else
User.create!(:name => user["name"], :email => user["email"],
:password => Devise.friendly_token){ |u| u.github_token = access_token.token }
end
end
Since the access token is stored as string in the database, you can create another +access_token+ object to do get/post/put/delete requests like this:
def oauth_github_token
@oauth_github_token ||= self.class.oauth_access_token(:github, github_token)
end
Or use a composition pattern through ActiveRecord's composed_of.
For github, the access token never expires. For facebook, you need to ask for offline access to get a token that won't expire. However, some providers like 37 Signals may expire the token and you need to store both access_token and refresh token in your database. This mechanism is not yet supported by Devise by default and you should check OAuth2 documentation for more information.
Finally, notice in cases a resource is returned by +find_for_github_oauth+ but is not persisted, we store the access token in the session before rendering the registrations form. This allows you to recover your token later by overwriting +new_with_session+ class method in your model:
def self.new_with_session(params, session)
super.tap { |u| u.github_token = session[:user_github_oauth_token] }
end
This method is called automatically by Devise::RegistrationsController before building/creating a new resource. All oauth tokens in sessions are removed after the user signs in/up.
=== Testing OAuth
Devise provides a few helpers to aid testing. Since the +user_oauth_authorize_url(:github)+ link added to our views points to Github, we certainly don't want our integration tests to send users to Github. That said, Devise provides a way to short circuit these url helpers and make them point straight to the oauth callback url with a fake code bypassing Github.
All you need to do is to call the following helpers:
# Inside our (test|spec)_helper.rb
Devise::Oauth.test_mode!
# Inside our integration tests for Oauth
setup { Devise::Oauth.short_circuit_authorizers! }
teardown { Devise::Oauth.unshort_circuit_authorizers! }
Since we are now passing a fake code to Devise OAuth callback, if we try to retrieve an access token from Github, it will obviously fail. That said, all following requests to the provider needs to be stubbed. Luckily, Devise provides a method called +Devise::Oauth.stub!+ that yields a block to help us build our stubs. All in all, our integration test would look like this:
# Inside our (test|spec)_helper.rb
Devise::Oauth.test_mode!
# Inside our integration tests for Oauth
ACCESS_TOKEN = {
:access_token => "plataformatec"
}
GITHUB_INFO = {
:user => {
:name => 'User Example',
:email => 'user@example.com'
}
}
setup do
Devise::Oauth.short_circuit_authorizers!
Devise::Oauth.stub!(:github) do |b|
b.post('/login/oauth/access_token') { [200, {}, ACCESS_TOKEN.to_json] }
b.post('/api/v2/json/user/show') { [200, {}, GITHUB_INFO.to_json] }
end
end
teardown do
Devise::Oauth.unshort_circuit_authorizers!
Devise::Oauth.reset_stubs!
end
test "auth from Github" do
assert_difference "User.count", 1 do
visit "/users/sign_in"
click_link "Sign in with Github"
end
assert_contain "Successfully authorized from Github account."
end
Enjoy!
== Migrating from other solutions
Devise implements encryption strategies for Clearance, Authlogic and Restful-Authentication. To make use of these strategies, set the desired encryptor in the encryptor initializer config option. You might also need to rename your encrypted password and salt columns to match Devise's fields (encrypted_password and password_salt).
== Other ORMs
Devise supports ActiveRecord (by default) and Mongoid. We offer experimental Datamapper support (with the limitation that the Devise test suite does not run completely with Datamapper). To choose other ORM, you just need to configure it in the initializer file.
Devise supports ActiveRecord (default) and Mongoid. To choose other ORM, you just need to require it in the initializer file.
== Extensions
@ -263,32 +445,29 @@ Devise also has extensions created by the community:
* http://github.com/scambra/devise_invitable adds support to Devise for sending invitations by email.
* http://github.com/grimen/devise_facebook_connectable adds support for Facebook Connect authentication, and optionally fetching user info from Facebook in the same step.
* http://github.com/joshk/devise_imapable adds support for imap based authentication, excellent for internal apps when an LDAP server isn't available.
* http://github.com/cschiewek/devise_ldap_authenticatable adds support for LDAP authentication via simple bind.
Please consult their respective documentation for more information and requirements.
== TODO
== Bugs and Feedback
Please refer to TODO file.
If you discover any bugs, we would like to know about it. Be sure to include as much relevant information as possible, as Devise and Rails versions. If possible, please try to go through the following steps:
== Security
1) Look at the source code a bit to find out whether your assumptions are correct. We try to keep it as clean and documented as possible;
Needless to say, security is extremely important to Devise. If you find yourself in a possible security issue with Devise, please go through the following steps, trying to reproduce the bug:
2) Provide a simple way to reproduce the bug: a small test case to Devise test suite or a simple app on Github;
1) Look at the source code a bit to find out whether your assumptions are correct;
2) If possible, provide a way to reproduce the bug: a small app on Github or a step-by-step to reproduce;
3) E-mail us or send a Github private message instead of using the normal issues;
Our Issues Tracker is available at:
Being able to reproduce the bug is the first step to fix it. Thanks for your understanding.
http://github.com/plataformatec/devise/issues
== Maintainers
If you found a security bug, we ask you to *NOT* use the Issues Tracker and instead send us a private message through Github or send an e-mail to the developers.
* José Valim (http://github.com/josevalim)
* Carlos Antônio da Silva (http://github.com/carlosantoniodasilva)
Finally, if you have questions or would like to give some feedback, please use the Mailing List instead of Issues Tracker:
http://groups.google.com/group/plataformatec-devise
== Contributors
@ -296,15 +475,12 @@ We have a long list of valued contributors. Check them all at:
http://github.com/plataformatec/devise/contributors
== Bugs and Feedback
If you want to add new features, let us know in the Issues Tracker! If you want to scratch our itches, feel free to check the TODO file in the repository. :)
If you discover any bugs, please create an issue on GitHub.
== Maintainers
http://github.com/plataformatec/devise/issues
For support, send an e-mail to the mailing list.
http://groups.google.com/group/plataformatec-devise
* José Valim (http://github.com/josevalim)
* Carlos Antônio da Silva (http://github.com/carlosantoniodasilva)
== License

2
TODO
View File

@ -1,3 +1,5 @@
* Move integration tests to Capybara
* Better ORM integration
* Extract activatable models tests from confirmable
* Add support to automatically refresh the access token for OAuth
* Add test to generators using the new Rails::Generators::TestCase

View File

@ -3,7 +3,7 @@ class Devise::ConfirmationsController < ApplicationController
# GET /resource/confirmation/new
def new
build_resource
build_resource({})
render_with_scope :new
end

View File

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

View File

@ -4,7 +4,7 @@ class Devise::PasswordsController < ApplicationController
# GET /resource/password/new
def new
build_resource
build_resource({})
render_with_scope :new
end

View File

@ -1,5 +1,5 @@
class Devise::RegistrationsController < ApplicationController
prepend_before_filter :require_no_authentication, :only => [ :new, :create ]
prepend_before_filter :require_no_authentication, :only => [ :new, :create, :cancel ]
prepend_before_filter :authenticate_scope!, :only => [:edit, :update, :destroy]
include Devise::Controllers::InternalHelpers
@ -45,8 +45,23 @@ class Devise::RegistrationsController < ApplicationController
sign_out_and_redirect(self.resource)
end
# GET /resource/cancel
# Forces the session data which is usually expired after sign
# in to be expired now.
def cancel
expire_session_data_after_sign_in!
redirect_to new_registration_path(resource_name)
end
protected
# Build a devise resource passing in the session. Useful to move
# temporary session data to the newly created user.
def build_resource(hash=nil)
hash ||= params[resource_name] || {}
self.resource = resource_class.new_with_session(hash, session)
end
# Authenticates the current scope and gets a copy of the current resource.
# We need to use a copy because we don't want actions like update changing
# the current user in place.

View File

@ -4,7 +4,7 @@ class Devise::UnlocksController < ApplicationController
# GET /resource/unlock/new
def new
build_resource
build_resource({})
render_with_scope :new
end

View File

@ -17,3 +17,9 @@
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
<%= 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 />
<% end =%>
<% end -%>

View File

@ -30,6 +30,10 @@ 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:
success: 'Successfully authorized from %{kind} account.'
failure: 'Could not authorize you from %{kind} because "%{reason}".'
skipped: 'Skipped Oauth authorization for %{kind}.'
mailer:
confirmation_instructions:
subject: 'Confirmation instructions'

View File

@ -1,8 +1,10 @@
require 'active_support/core_ext/numeric/time'
require 'active_support/dependencies'
require 'set'
module Devise
autoload :FailureApp, 'devise/failure_app'
autoload :Oauth, 'devise/oauth'
autoload :PathChecker, 'devise/path_checker'
autoload :Schema, 'devise/schema'
autoload :TestHelpers, 'devise/test_helpers'
@ -30,11 +32,12 @@ module Devise
end
# Constants which holds devise configuration for extensions. Those should
# not be modified by the "end user".
# not be modified by the "end user" (this is why they are constants).
ALL = []
CONTROLLERS = ActiveSupport::OrderedHash.new
ROUTES = ActiveSupport::OrderedHash.new
STRATEGIES = ActiveSupport::OrderedHash.new
URL_HELPERS = ActiveSupport::OrderedHash.new
# True values used to check params
TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE']
@ -69,7 +72,7 @@ module Devise
mattr_accessor :http_authenticatable
@@http_authenticatable = true
# If http authentication is used for ajax requests. True by default.
# If http authentication is used for ajax requests. True by default.
mattr_accessor :http_authenticatable_on_xhr
@@http_authenticatable_on_xhr = true
@ -111,11 +114,7 @@ module Devise
# Used to define the password encryption algorithm.
mattr_accessor :encryptor
@@encryptor = nil
# Store scopes mappings.
mattr_accessor :mappings
@@mappings = ActiveSupport::OrderedHash.new
@@encryptor = :bcrypt
# Tells if devise should apply the schema in ORMs where devise declaration
# and schema belongs to the same class (as Datamapper and Mongoid).
@ -161,21 +160,37 @@ module Devise
mattr_accessor :navigational_formats
@@navigational_formats = [:html]
# Private methods to interface with Warden.
mattr_accessor :warden_config
@@warden_config = nil
@@warden_config_block = nil
# When set to true, signing out an user signs out all other scopes.
mattr_accessor :sign_out_all_scopes
@@sign_out_all_scopes = false
def self.use_default_scope=(*)
ActiveSupport::Deprecation.warn "config.use_default_scope is deprecated and removed from Devise. " <<
"If you are using non conventional routes in Devise, all you need to do is to pass the devise " <<
"scope in the router DSL:\n\n as :user do\n get \"sign_in\", :to => \"devise/sessions\"\n end\n\n" <<
"The method :as is also aliased to :devise_scope. Choose the one you prefer.", caller
end
# 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
# 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
@@warden_config_block = nil
# Default way to setup Devise. Run rails generate devise_install to create
# a fresh initializer with all configuration values.
@ -197,19 +212,19 @@ module Devise
# Small method that adds a mapping to Devise.
def self.add_mapping(resource, options)
mapping = Devise::Mapping.new(resource, options)
self.mappings[mapping.name] = mapping
self.default_scope ||= mapping.name
@@mappings[mapping.name] = mapping
@@default_scope ||= mapping.name
@@helpers.each { |h| h.define_helpers(mapping) }
mapping
end
# Make Devise aware of an 3rd party Devise-module. For convenience.
# Make Devise aware of an 3rd party Devise-module (like invitable). For convenience.
#
# == Options:
#
# +model+ - String representing the load path to a custom *model* for this module (to autoload.)
# +controller+ - Symbol representing the name of an exisiting or custom *controller* for this module.
# +route+ - Symbol representing the named *route* helper for this module.
# +flash+ - Symbol representing the *flash messages* used by this helper.
# +strategy+ - Symbol representing if this module got a custom *strategy*.
#
# All values, except :model, accept also a boolean and will have the same name as the given module
@ -225,26 +240,36 @@ module Devise
ALL << module_name
options.assert_valid_keys(:strategy, :model, :controller, :route)
config = {
:strategy => STRATEGIES,
:route => ROUTES,
:controller => CONTROLLERS
}
if strategy = options[:strategy]
STRATEGIES[module_name] = (strategy == true ? module_name : strategy)
end
config.each do |key, value|
next unless options[key]
name = (options[key] == true ? module_name : options[key])
if controller = options[:controller]
CONTROLLERS[module_name] = (controller == true ? module_name : controller)
end
if value.is_a?(Hash)
value[module_name] = name
if route = options[:route]
case route
when TrueClass
key, value = module_name, []
when Symbol
key, value = route, []
when Hash
key, value = route.keys.first, route.values.flatten
else
value << name unless value.include?(name)
raise ArgumentError, ":route should be true, a Symbol or a Hash"
end
URL_HELPERS[key] ||= []
URL_HELPERS[key].concat(value)
URL_HELPERS[key].uniq!
ROUTES[module_name] = key
end
if options[:model]
model_path = (options[:model] == true ? "devise/models/#{module_name}" : options[:model])
Devise::Models.send(:autoload, module_name.to_s.camelize.to_sym, model_path)
path = (options[:model] == true ? "devise/models/#{module_name}" : options[:model])
Devise::Models.send(:autoload, module_name.to_s.camelize.to_sym, path)
end
Devise::Mapping.add_module module_name
@ -265,6 +290,37 @@ module Devise
@@warden_config_block = block
end
# Specify an oauth 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)
#
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)
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::UrlHelpers
end
ActiveSupport.on_load(:action_view) do
include scope::UrlHelpers
end
end
# A method used internally to setup warden manager from the Rails initialize
# block.
def self.configure_warden! #:nodoc:

View File

@ -9,6 +9,52 @@ module Devise
*Devise.mappings.keys.map { |m| [:"current_#{m}", :"#{m}_signed_in?", :"#{m}_session"] }.flatten
end
# Define authentication filters and accessor helpers based on mappings.
# These filters should be used inside the controllers as before_filters,
# so you can control the scope of the user who should be signed in to
# access that specific controller/action.
# Example:
#
# Roles:
# User
# Admin
#
# Generated methods:
# authenticate_user! # Signs user in or redirect
# authenticate_admin! # Signs admin in or redirect
# user_signed_in? # Checks whether there is an user signed in or not
# admin_signed_in? # Checks whether there is an admin signed in or not
# current_user # Current signed in user
# current_admin # Currend signed in admin
# user_session # Session data available only to the user scope
# admin_session # Session data available only to the admin scope
#
# Use:
# before_filter :authenticate_user! # Tell devise to use :user map
# before_filter :authenticate_admin! # Tell devise to use :admin map
#
def self.define_helpers(mapping) #:nodoc:
mapping = mapping.name
class_eval <<-METHODS, __FILE__, __LINE__ + 1
def authenticate_#{mapping}!
warden.authenticate!(:scope => :#{mapping})
end
def #{mapping}_signed_in?
!!current_#{mapping}
end
def current_#{mapping}
@current_#{mapping} ||= warden.authenticate(:scope => :#{mapping})
end
def #{mapping}_session
current_#{mapping} && warden.session(:#{mapping})
end
METHODS
end
# The main accessor for the warden proxy instance
def warden
request.env['warden']
@ -40,13 +86,16 @@ module Devise
#
# Examples:
#
# sign_in :user, @user # sign_in(scope, resource)
# sign_in @user # sign_in(resource)
# sign_in :user, @user # sign_in(scope, resource)
# sign_in @user # sign_in(resource)
# sign_in @user, :event => :authentication # sign_in(resource, options)
#
def sign_in(resource_or_scope, resource=nil)
scope = Devise::Mapping.find_scope!(resource_or_scope)
resource ||= resource_or_scope
warden.set_user(resource, :scope => scope)
def sign_in(resource_or_scope, *args)
options = args.extract_options!
scope = Devise::Mapping.find_scope!(resource_or_scope)
resource = args.last || resource_or_scope
expire_session_data_after_sign_in!
warden.set_user(resource, options.merge!(:scope => scope))
end
# Sign out a given user or scope. This helper is useful for signing out an user
@ -159,14 +208,17 @@ module Devise
end
# Sign in an user and tries to redirect first to the stored location and
# then to the url specified by after_sign_in_path_for.
#
# If just a symbol is given, consider that the user was already signed in
# through other means and just perform the redirection.
def sign_in_and_redirect(resource_or_scope, resource=nil)
scope = Devise::Mapping.find_scope!(resource_or_scope)
resource ||= resource_or_scope
sign_in(scope, resource) unless warden.user(scope) == resource
# then to the url specified by after_sign_in_path_for. It accepts the same
# parameters as the sign_in method.
def sign_in_and_redirect(resource_or_scope, *args)
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
redirect_for_sign_in(scope, resource)
end
def redirect_for_sign_in(scope, resource) #:nodoc:
redirect_to stored_location_for(scope) || after_sign_in_path_for(resource)
end
@ -179,53 +231,17 @@ module Devise
else
sign_out(scope)
end
redirect_for_sign_out(scope)
end
def redirect_for_sign_out(scope) #:nodoc:
redirect_to after_sign_out_path_for(scope)
end
# Define authentication filters and accessor helpers based on mappings.
# These filters should be used inside the controllers as before_filters,
# so you can control the scope of the user who should be signed in to
# access that specific controller/action.
# Example:
#
# Roles:
# User
# Admin
#
# Generated methods:
# authenticate_user! # Signs user in or redirect
# authenticate_admin! # Signs admin in or redirect
# user_signed_in? # Checks whether there is an user signed in or not
# admin_signed_in? # Checks whether there is an admin signed in or not
# current_user # Current signed in user
# current_admin # Currend signed in admin
# user_session # Session data available only to the user scope
# admin_session # Session data available only to the admin scope
#
# Use:
# before_filter :authenticate_user! # Tell devise to use :user map
# before_filter :authenticate_admin! # Tell devise to use :admin map
#
Devise.mappings.each_key do |mapping|
class_eval <<-METHODS, __FILE__, __LINE__ + 1
def authenticate_#{mapping}!
warden.authenticate!(:scope => :#{mapping})
end
def #{mapping}_signed_in?
warden.authenticate?(:scope => :#{mapping})
end
def current_#{mapping}
@current_#{mapping} ||= warden.authenticate(:scope => :#{mapping})
end
def #{mapping}_session
current_#{mapping} && warden.session(:#{mapping})
end
METHODS
# 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.
def expire_session_data_after_sign_in!
end
end
end
end

View File

@ -10,7 +10,7 @@ module Devise
included do
helper DeviseHelper
helpers = %w(resource scope_name resource_name
helpers = %w(resource scope_name resource_name signed_in_resource
resource_class devise_mapping devise_controller?)
hide_action *helpers
helper_method *helpers
@ -35,6 +35,11 @@ module Devise
devise_mapping.to
end
# Returns a signed in resource from session (if one exists)
def signed_in_resource
warden.authenticate(:scope => resource_name)
end
# Attempt to find the mapped route for devise based on request path
def devise_mapping
@devise_mapping ||= request.env["devise.mapping"]
@ -49,7 +54,12 @@ module Devise
# Checks whether it's a devise mapped resource or not.
def is_devise_resource? #:nodoc:
raise ActionController::UnknownAction unless devise_mapping
unknown_action!("Could not find devise mapping for #{request.fullpath}.") unless devise_mapping
end
def unknown_action!(msg)
logger.debug "[Devise] #{msg}" if logger
raise ActionController::UnknownAction, msg
end
# Sets the resource creating an instance variable
@ -85,12 +95,14 @@ module Devise
#
# Please refer to README or en.yml locale file to check what messages are
# available.
def set_flash_message(key, kind)
flash[key] = I18n.t(:"#{resource_name}.#{kind}", :resource_name => resource_name,
:scope => [:devise, controller_name.to_sym], :default => kind)
def set_flash_message(key, kind, options={}) #:nodoc:
options[:scope] = "devise.#{controller_name}"
options[:default] = Array(options[:default]).unshift(kind.to_sym)
options[:resource_name] = resource_name
flash[key] = I18n.t("#{resource_name}.#{kind}", options)
end
def clean_up_passwords(object)
def clean_up_passwords(object) #:nodoc:
object.clean_up_passwords if object.respond_to?(:clean_up_passwords)
end
end

View File

@ -17,17 +17,15 @@ module Devise
# Render a view for the specified scope. Turned off by default.
# Accepts just :controller as option.
def render_with_scope(action, options={})
controller_name = options.delete(:controller) || self.controller_name
def render_with_scope(action, path=self.controller_path)
if self.class.scoped_views?
begin
render :template => "#{devise_mapping.plural}/#{controller_name}/#{action}"
render :template => "#{devise_mapping.plural}/#{path.split("/").last}/#{action}"
rescue ActionView::MissingTemplate
render :template => "#{controller_path}/#{action}"
render :template => "#{path}/#{action}"
end
else
render :template => "#{controller_path}/#{action}"
render :template => "#{path}/#{action}"
end
end
end

View File

@ -19,13 +19,11 @@ module Devise
# Those helpers are added to your ApplicationController.
module UrlHelpers
Devise::ROUTES.values.uniq.each do |module_name|
Devise::URL_HELPERS.each do |module_name, actions|
[:path, :url].each do |path_or_url|
actions = [ nil, :new_ ]
actions << :edit_ if [:password, :registration].include?(module_name)
actions << :destroy_ if [:session].include?(module_name)
actions.each do |action|
action = action ? "#{action}_" : ""
class_eval <<-URL_HELPERS, __FILE__, __LINE__ + 1
def #{action}#{module_name}_#{path_or_url}(resource_or_scope, *args)
scope = Devise::Mapping.find_scope!(resource_or_scope)

View File

@ -27,6 +27,7 @@ module Devise
elsif warden_options[:recall]
recall
else
debug!
redirect
end
end
@ -52,6 +53,11 @@ module Devise
protected
def debug!
return unless Rails.logger.try(:debug?)
Rails.logger.debug "[Devise] Could not sign in #{scope}: #{i18n_message.inspect}."
end
def i18n_message(default = nil)
message = warden.message || warden_options[:message] || default || :unauthenticated
@ -89,7 +95,7 @@ module Devise
end
def scope
@scope ||= warden_options[:scope]
@scope ||= warden_options[:scope] || Devise.default_scope
end
def attempted_path
@ -101,7 +107,7 @@ module Devise
# yet, but we still need to store the uri based on scope, so different scopes
# would never use the same uri to redirect.
def store_location!
session[:"#{scope}_return_to"] = attempted_path if request.get? && !http_auth?
session["#{scope}_return_to"] = attempted_path if request.get? && !http_auth?
end
end
end

View File

@ -47,20 +47,6 @@ module Devise
def devise(*modules)
include Devise::Models::Authenticatable
options = modules.extract_options!
if modules.delete(:authenticatable)
ActiveSupport::Deprecation.warn ":authenticatable as module is deprecated. Please give :database_authenticatable instead.", caller
modules << :database_authenticatable
end
if modules.delete(:activatable)
ActiveSupport::Deprecation.warn ":activatable as module is deprecated. It's included in your model by default.", caller
end
if modules.delete(:http_authenticatable)
ActiveSupport::Deprecation.warn ":http_authenticatable as module is deprecated and is on by default. Revert by setting :http_authenticatable => false.", caller
end
self.devise_modules += Devise::ALL & modules.map(&:to_sym).uniq
devise_modules_hook! do

View File

@ -4,18 +4,17 @@ module Devise
module Models
# Authenticable module. Holds common settings for authentication.
#
# == Configuration:
# == Options
#
# You can overwrite configuration values by setting in globally in Devise,
# using devise method or overwriting the respective instance method.
# Authenticatable adds the following options to devise_for:
#
# authentication_keys: parameters used for authentication. By default [:email].
# * +authentication_keys+: parameters used for authentication. By default [:email].
#
# http_authenticatable: if this model allows http authentication. By default true.
# It also accepts an array specifying the strategies that should allow http.
# * +http_authenticatable+: if this model allows http authentication. By default true.
# It also accepts an array specifying the strategies that should allow http.
#
# params_authenticatable: if this model allows authentication through request params. By default true.
# It also accepts an array specifying the strategies that should allow params authentication.
# * +params_authenticatable+: if this model allows authentication through request params. By default true.
# It also accepts an array specifying the strategies that should allow params authentication.
#
# == Active?
#

View File

@ -9,22 +9,22 @@ module Devise
# it means it won't be able to sign in again without confirming the account
# again through the email that was sent.
#
# Configuration:
# == Options
#
# confirm_within: the time you want the user will have to confirm it's account
# without blocking his access. When confirm_within is zero, the
# user won't be able to sign in without confirming. You can
# use this to let your user access some features of your
# application without confirming the account, but blocking it
# after a certain period (ie 7 days). By default confirm_within is
# zero, it means users always have to confirm to sign in.
# Confirmable adds the following options to devise_for:
#
# Examples:
# * +confirm_within+: the time you want to allow the user to access his account
# before confirming it. After this period, the user access is denied. You can
# use this to let your user access some features of your application without
# confirming the account, but blocking it after a certain period (ie 7 days).
# By default confirm_within is zero, it means users always have to confirm to sign in.
#
# == Examples
#
# User.find(1).confirm! # returns true unless it's already confirmed
# User.find(1).confirmed? # true/false
# User.find(1).send_confirmation_instructions # manually send instructions
# User.find(1).resend_confirmation! # generates a new token and resent it
#
module Confirmable
extend ActiveSupport::Concern

View File

@ -5,21 +5,20 @@ module Devise
# Authenticable Module, responsible for encrypting password and validating
# authenticity of a user while signing in.
#
# Configuration:
# == Options
#
# You can overwrite configuration values by setting in globally in Devise,
# using devise method or overwriting the respective instance method.
# DatabaseAuthenticable adds the following options to devise_for:
#
# pepper: encryption key used for creating encrypted password. Each time
# password changes, it's gonna be encrypted again, and this key
# is added to the password and salt to create a secure hash.
# Always use `rake secret' to generate a new key.
# * +pepper+: encryption key used for creating encrypted password. Each time
# password changes, it's gonna be encrypted again, and this key is added
# to the password and salt to create a secure hash. Always use `rake secret'
# to generate a new key.
#
# stretches: defines how many times the password will be encrypted.
# * +stretches+: defines how many times the password will be encrypted.
#
# encryptor: the encryptor going to be used. By default :sha1.
# * +encryptor+: the encryptor going to be used. By default :sha1.
#
# Examples:
# == Examples
#
# User.find(1).valid_password?('password123') # returns true/false
#

View File

@ -7,13 +7,14 @@ module Devise
# will unlock the user automatically after some configured time (ie 2.hours).
# It's also possible to setup lockable to use both email and time strategies.
#
# Configuration:
# == Options
#
# maximum_attempts: how many attempts should be accepted before blocking the user.
# lock_strategy: lock the user account by :failed_attempts or :none.
# unlock_strategy: unlock the user account by :time, :email, :both or :none.
# unlock_in: the time you want to lock the user after to lock happens. Only
# available when unlock_strategy is :time or :both.
# Lockable adds the following options to devise_for:
#
# * +maximum_attempts+: how many attempts should be accepted before blocking the user.
# * +lock_strategy+: lock the user account by :failed_attempts or :none.
# * +unlock_strategy+: unlock the user account by :time, :email, :both or :none.
# * +unlock_in+: the time you want to lock the user after to lock happens. Only available when unlock_strategy is :time or :both.
#
module Lockable
extend ActiveSupport::Concern

View File

@ -0,0 +1,49 @@
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

@ -1,8 +1,9 @@
module Devise
module Models
# Recoverable takes care of reseting the user password and send reset instructions
# Examples:
# Recoverable takes care of reseting the user password and send reset instructions.
#
# == Examples
#
# # resets the user password and save the record, true if valid passwords are given, otherwise false
# User.find(1).reset_password!('password123', 'password123')
@ -13,6 +14,7 @@ module Devise
#
# # creates a new token and send it with instructions about how to reset the password
# User.find(1).send_reset_password_instructions
#
module Recoverable
extend ActiveSupport::Concern

View File

@ -3,6 +3,19 @@ module Devise
# Registerable is responsible for everything related to registering a new
# resource (ie user sign up).
module Registerable
extend ActiveSupport::Concern
module ClassMethods
# A convenience method that receives both parameters and session to
# initialize an user. This can be used by OAuth, for example, to send
# in the user token and be stored on initialization.
#
# By default discards all information sent by the session by calling
# new with params.
def new_with_session(params, session)
new(params)
end
end
end
end
end

View File

@ -11,24 +11,23 @@ module Devise
# You probably wouldn't use rememberable methods directly, they are used
# mostly internally for handling the remember token.
#
# Configuration:
# == Options
#
# remember_for: the time you want the user will be remembered without
# asking for credentials. After this time the user will be
# blocked and will have to enter his credentials again.
# This configuration is also used to calculate the expires
# time for the cookie created to remember the user.
# 2.weeks by default.
# Rememberable adds the following options in devise_for:
#
# remember_across_browsers: if true, a valid remember token can be
# re-used between multiple browsers.
# True by default.
# * +remember_for+: the time you want the user will be remembered without
# asking for credentials. After this time the user will be blocked and
# will have to enter his credentials again. This configuration is als
# used to calculate the expires time for the cookie created to remember
# the user. By default remember_for is 2.weeks.
#
# extend_remember_period: if true, extends the user's remember period
# when remembered via cookie.
# False by default.
# * +remember_across_browsers+: if a valid remember token can be re-used
# between multiple browsers. By default remember_across_browsers is true.
#
# Examples:
# * +extend_remember_period+: if true, extends the user's remember period
# when remembered via cookie. False by default.
#
# == Examples
#
# User.find(1).remember_me! # regenerating the token
# User.find(1).forget_me! # clearing the token

View File

@ -7,9 +7,16 @@ module Devise
# will be asked for credentials again, it means, he/she will be redirected
# to the sign in page.
#
# Configuration:
# == Options
#
# Timeoutable adds the following options to devise_for:
#
# * +timeout_in+: the interval to timeout the user session without activity.
#
# == Examples
#
# user.timedout?(30.minutes.ago)
#
# timeout_in: the time you want to timeout the user session without activity.
module Timeoutable
extend ActiveSupport::Concern

View File

@ -8,12 +8,11 @@ module Devise
# This module only provides a few helpers to help you manage the token. Creating and resetting
# the token is your responsibility.
#
# == Configuration:
# == Options
#
# You can overwrite configuration values by setting in globally in Devise (+Devise.setup+),
# using devise method, or overwriting the respective instance method.
# TokenAuthenticable adds the following options to devise_for:
#
# +token_authentication_key+ - Defines name of the authentication token params key. E.g. /users/sign_in?some_key=...
# * +token_authentication_key+: Defines name of the authentication token params key. E.g. /users/sign_in?some_key=...
#
module TokenAuthenticatable
extend ActiveSupport::Concern

View File

@ -1,10 +1,17 @@
module Devise
module Models
# Validatable creates all needed validations for a user email and password.
# It's optional, given you may want to create the validations by yourself.
# Automatically validate if the email is present, unique and it's format is
# valid. Also tests presence of password, confirmation and length
# valid. Also tests presence of password, confirmation and length.
#
# == Options
#
# Validatable adds the following options to devise_for:
#
# * +email_regexp+: the regular expression used to validate e-mails;
# * +password_length+: a range expressing password length. Defaults to 6..20.
#
module Validatable
# All validations used by this module.
VALIDATIONS = [ :validates_presence_of, :validates_uniqueness_of, :validates_format_of,

View File

@ -2,20 +2,26 @@ require 'active_support/core_ext/object/with_options'
Devise.with_options :model => true do |d|
# Strategies first
d.with_options :strategy => true do |s|
s.add_module :database_authenticatable, :controller => :sessions, :route => :session
s.add_module :token_authenticatable, :controller => :sessions, :route => :session
d.with_options :strategy => true do |s|
routes = [nil, :new, :destroy]
s.add_module :database_authenticatable, :controller => :sessions, :route => { :session => routes }
s.add_module :token_authenticatable, :controller => :sessions, :route => { :session => routes }
s.add_module :rememberable
end
# Misc after
d.add_module :recoverable, :controller => :passwords, :route => :password
d.add_module :registerable, :controller => :registrations, :route => :registration
# Other authentications
d.add_module :oauthable, :controller => :oauth_callbacks, :route => :oauth_callback
# Misc after
routes = [nil, :new, :edit]
d.add_module :recoverable, :controller => :passwords, :route => { :password => routes }
d.add_module :registerable, :controller => :registrations, :route => { :registration => (routes << :cancel) }
d.add_module :validatable
# The ones which can sign out after
d.add_module :confirmable, :controller => :confirmations, :route => :confirmation
d.add_module :lockable, :controller => :unlocks, :route => :unlock
routes = [nil, :new]
d.add_module :confirmable, :controller => :confirmations, :route => { :confirmation => routes }
d.add_module :lockable, :controller => :unlocks, :route => { :unlock => routes }
d.add_module :timeoutable
# Stats for last, so we make sure the user is really signed in

41
lib/devise/oauth.rb Normal file
View File

@ -0,0 +1,41 @@
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

@ -0,0 +1,33 @@
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

@ -0,0 +1,28 @@
module Devise
module Oauth
# Provides a few helpers that are included in ActionController::Base
# for convenience.
module Helpers
extend ActiveSupport::Concern
included do
helper_method :oauth_callback
end
def oauth_callback #:nodoc:
nil
end
alias :oauth_provider :oauth_callback
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

@ -0,0 +1,190 @@
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_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, error[0,25], :default => :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 && resource.persisted? && resource.errors.empty?
set_oauth_flash_message :notice, :success
sign_in_and_redirect resource_name, resource, :event => :authentication
elsif resource
session[oauth_session_key] = access_token.token
clean_up_passwords(resource)
render_for_oauth
else
set_oauth_flash_message :alert, :skipped
redirect_to after_oauth_skipped_path_for(resource_name)
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] = Array(options[:default]).unshift(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_or_scope)
after_sign_in_path_for(resource_or_scope)
end
# The default hook used by oauth to specify the redirect url for skip.
def after_oauth_skipped_path_for(scope)
new_session_path(scope)
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

@ -0,0 +1,29 @@
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_url(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_url(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

@ -0,0 +1,35 @@
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

View File

@ -1,14 +1,16 @@
require 'devise/rails/routes'
require 'devise/rails/warden_compat'
# Include UrlHelpers in ActionController and ActionView as soon as they are loaded.
ActiveSupport.on_load(:action_controller) { include Devise::Controllers::UrlHelpers }
ActiveSupport.on_load(:action_view) { include Devise::Controllers::UrlHelpers }
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.
config.paths.app.controllers.autoload!
config.paths.app.controllers.skip_eager_load!
# Initialize Warden and copy its configurations.
config.app_middleware.use Warden::Manager do |config|
Devise.warden_config = config
end
@ -16,54 +18,33 @@ module Devise
# Force routes to be loaded if we are doing any eager load.
config.before_eager_load { |app| app.reload_routes! }
config.after_initialize do
Devise.encryptor ||= begin
warn "[WARNING] config.encryptor is not set in your config/initializers/devise.rb. " \
"Devise will then set it to :bcrypt. If you were using the previous default " \
"encryptor, please add config.encryptor = :sha1 to your configuration file." if Devise.mailer_sender
:bcrypt
end
end
initializer "devise.add_filters" do |app|
app.config.filter_parameters += [:password, :password_confirmation]
app.config.filter_parameters.uniq
end
unless Rails.env.production?
config.after_initialize do
actions = [:confirmation_instructions, :reset_password_instructions, :unlock_instructions]
initializer "devise.url_helpers" do
Devise.include_helpers(Devise::Controllers)
end
translations = begin
I18n.t("devise.mailer", :raise => true).map { |k, v| k if v.is_a?(String) }.compact
rescue Exception => e # Do not care if something fails
[]
end
initializer "devise.oauth_url_helpers" do
if Devise.oauth_providers.any?
Devise.include_helpers(Devise::Oauth)
end
end
keys = actions & translations
# Check all available mapings and only load related controllers.
def eager_load!
mappings = Devise.mappings.values.map(&:modules).flatten.uniq
controllers = Devise::CONTROLLERS.values_at(*mappings)
path = paths.app.controllers.to_a.first
matcher = /\A#{Regexp.escape(path)}\/(.*)\.rb\Z/
keys.each do |key|
ActiveSupport::Deprecation.warn "The I18n message 'devise.mailer.#{key}' is deprecated. " \
"Please use 'devise.mailer.#{key}.subject' instead."
end
Dir.glob("#{path}/devise/{#{controllers.join(',')}}_controller.rb").sort.each do |file|
require_dependency file.sub(matcher, '\1')
end
config.after_initialize do
flash = [:unauthenticated, :unconfirmed, :invalid, :invalid_token, :timeout, :inactive, :locked]
translations = begin
I18n.t("devise.sessions", :raise => true).keys
rescue Exception => e # Do not care if something fails
[]
end
keys = flash & translations
if keys.any?
ActiveSupport::Deprecation.warn "The following I18n messages in 'devise.sessions' " \
"are deprecated: #{keys.to_sentence}. Please move them to 'devise.failure' instead."
end
end
super
end
end
end

View File

@ -5,7 +5,6 @@ module ActionDispatch::Routing
def finalize_with_devise!
finalize_without_devise!
Devise.configure_warden!
ActionController::Base.send :include, Devise::Controllers::Helpers
end
alias_method_chain :finalize!, :devise
end
@ -116,16 +115,6 @@ module ActionDispatch::Routing
def devise_for(*resources)
options = resources.extract_options!
if as = options.delete(:as)
ActiveSupport::Deprecation.warn ":as is deprecated, please use :path instead."
options[:path] ||= as
end
if scope = options.delete(:scope)
ActiveSupport::Deprecation.warn ":scope is deprecated, please use :singular instead."
options[:singular] ||= scope
end
options[:as] ||= @scope[:as] if @scope[:as].present?
options[:module] ||= @scope[:module] if @scope[:module].present?
options[:path_prefix] ||= @scope[:path] if @scope[:path].present?
@ -227,8 +216,20 @@ module ActionDispatch::Routing
end
def devise_registration(mapping, controllers) #:nodoc:
resource :registration, :only => [:new, :create, :edit, :update, :destroy], :path => mapping.path_names[:registration],
:path_names => { :new => mapping.path_names[:sign_up] }, :controller => controllers[:registrations]
path_names = {
:new => mapping.path_names[:sign_up],
:cancel => mapping.path_names[:cancel]
}
resource :registration, :except => :show, :path => mapping.path_names[:registration],
:path_names => path_names, :controller => controllers[:registrations] do
get :cancel
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
end
def with_devise_exclusive_scope(new_path, new_as) #:nodoc:

View File

@ -3,11 +3,6 @@ module Devise
# and overwrite the apply_schema method.
module Schema
def authenticatable(*args)
ActiveSupport::Deprecation.warn "t.authenticatable in migrations is deprecated. Please use t.database_authenticatable instead.", caller
database_authenticatable(*args)
end
# Creates email, encrypted_password and password_salt.
#
# == Options
@ -21,10 +16,6 @@ module Devise
null = options[:null] || false
default = options[:default] || ""
if options.delete(:encryptor)
ActiveSupport::Deprecation.warn ":encryptor as option is deprecated, simply remove it."
end
apply_devise_schema :email, String, :null => null, :default => default
apply_devise_schema :encrypted_password, String, :null => null, :default => default, :limit => 128
apply_devise_schema :password_salt, String, :null => null, :default => default

View File

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

View File

@ -35,16 +35,16 @@ Devise.setup do |config|
# config.http_authentication_realm = "Application"
# ==> Configuration for :database_authenticatable
# For bcrypt, this is the cost for hashing the password and defaults to 10. If
# using other encryptors, it sets how many times you want the password re-encrypted.
config.stretches = 10
# Define which will be the encryption algorithm. Devise also supports encryptors
# from others authentication tools as :clearance_sha1, :authlogic_sha512 (then
# you should set stretches above to 20 for default behavior) and :restful_authentication_sha1
# (then you should set stretches to 10, and copy REST_AUTH_SITE_KEY to pepper)
config.encryptor = :bcrypt
# For bcrypt, this is the cost for hashing the password and defaults to 10. If
# using other encryptors, it sets how many times you want the password re-encrypted.
config.stretches = 10
# Setup a pepper to generate the encrypted password.
config.pepper = <%= ActiveSupport::SecureRandom.hex(64).inspect %>
@ -126,17 +126,21 @@ Devise.setup do |config|
# should add them to the navigational formats lists. Default is [:html]
# config.navigational_formats = [:html, :iphone]
# ==> OAuth2
# Add a new OAuth2 provider. Check the README for more information on setting
# up on your models and hooks.
# 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)
# ==> Warden configuration
# If you want to use other strategies, that are not (yet) supported by Devise,
# you can configure them inside the config.warden block. The example below
# allows you to setup OAuth, using http://github.com/roman/warden_oauth
# If you want to use other strategies, that are not supported by Devise, or
# change the failure app, you can configure them inside the config.warden block.
#
# config.warden do |manager|
# manager.oauth(:twitter) do |twitter|
# twitter.consumer_secret = <YOUR CONSUMER SECRET>
# twitter.consumer_key = <YOUR CONSUMER KEY>
# twitter.options :site => 'http://twitter.com'
# end
# manager.default_strategies(:scope => :user).unshift :twitter_oauth
# manager.failure_app = AnotherApp
# manager.default_strategies(:scope => :user).unshift :some_external_strategy
# end
end

View File

@ -1,4 +0,0 @@
# Remove this file after deprecation
if caller.none? { |l| l =~ %r{lib/rails/generators\.rb:(\d+):in `lookup!'$} }
warn "[WARNING] `rails g devise_install` is deprecated, please use `rails g devise:install` instead."
end

View File

@ -1,4 +0,0 @@
# Remove this file after deprecation
if caller.none? { |l| l =~ %r{lib/rails/generators\.rb:(\d+):in `lookup!'$} }
warn "[WARNING] `rails g devise_views` is deprecated, please use `rails g devise:views` instead."
end

View File

@ -1,51 +1,16 @@
require 'test_helper'
require 'ostruct'
class MockController < ApplicationController
attr_accessor :env
def request
self
end
def path
''
end
def index
end
def host_with_port
"test.host:3000"
end
def protocol
"http"
end
def script_name
""
end
def symbolized_path_parameters
{}
end
end
class ControllerAuthenticableTest < ActionController::TestCase
tests MockController
tests ApplicationController
def setup
@mock_warden = OpenStruct.new
@controller.env = { 'warden' => @mock_warden }
end
test 'setup warden' do
assert_not_nil @controller.warden
@controller.request.env['warden'] = @mock_warden
end
test 'provide access to warden instance' do
assert_equal @controller.warden, @controller.env['warden']
assert_equal @mock_warden, @controller.warden
end
test 'proxy signed_in? to authenticated' do
@ -81,13 +46,13 @@ class ControllerAuthenticableTest < ActionController::TestCase
end
test 'proxy user_signed_in? to authenticate? with user scope' do
@mock_warden.expects(:authenticate?).with(:scope => :user)
@controller.user_signed_in?
@mock_warden.expects(:authenticate).with(:scope => :user).returns("user")
assert @controller.user_signed_in?
end
test 'proxy admin_signed_in? to authenticate? with admin scope' do
@mock_warden.expects(:authenticate?).with(:scope => :admin)
@controller.admin_signed_in?
@mock_warden.expects(:authenticate).with(:scope => :admin)
assert_not @controller.admin_signed_in?
end
test 'proxy user_session to session scope in warden' do

View File

@ -22,16 +22,16 @@ class HelpersTest < ActionController::TestCase
end
test 'get resource instance variable from env' do
@controller.instance_variable_set(:@user, admin = Admin.new)
assert_equal admin, @controller.resource
@controller.instance_variable_set(:@user, user = User.new)
assert_equal user, @controller.resource
end
test 'set resource instance variable from env' do
admin = @controller.send(:resource_class).new
@controller.send(:resource=, admin)
user = @controller.send(:resource_class).new
@controller.send(:resource=, user)
assert_equal admin, @controller.send(:resource)
assert_equal admin, @controller.instance_variable_get(:@user)
assert_equal user, @controller.send(:resource)
assert_equal user, @controller.instance_variable_get(:@user)
end
test 'resources methods are not controller actions' do
@ -39,11 +39,15 @@ class HelpersTest < ActionController::TestCase
end
test 'require no authentication tests current mapping' do
@controller.expects(:resource_name).returns(:user).twice
@mock_warden.expects(:authenticated?).with(:user).returns(true)
@controller.expects(:redirect_to).with(root_path)
@controller.send :require_no_authentication
end
test 'signed in resource returns signed in resource for current scope' do
@mock_warden.expects(:authenticate).with(:scope => :user).returns(User.new)
assert_kind_of User, @controller.signed_in_resource
end
test 'is a devise controller' do
assert @controller.devise_controller?

View File

@ -20,7 +20,7 @@ class RoutesTest < ActionController::TestCase
send(:"#{prepend_path}user_#{name}_url", :param => 123)
@request.path = nil
# With an AR object
# With an object
assert_equal @controller.send(:"#{prepend_path}#{name}_path", User.new),
send(:"#{prepend_path}user_#{name}_path")
assert_equal @controller.send(:"#{prepend_path}#{name}_url", User.new),
@ -54,5 +54,6 @@ class RoutesTest < ActionController::TestCase
assert_path_and_url :registration
assert_path_and_url :registration, :new
assert_path_and_url :registration, :edit
assert_path_and_url :registration, :cancel
end
end

View File

@ -1,15 +1,6 @@
require 'test_helper'
class AuthenticationSanityTest < ActionController::IntegrationTest
def setup
Devise.sign_out_all_scopes = false
end
def teardown
Devise.sign_out_all_scopes = false
end
test 'home should be accessible without sign in' do
visit '/'
assert_response :success
@ -18,14 +9,12 @@ class AuthenticationSanityTest < ActionController::IntegrationTest
test 'sign in as user should not authenticate admin scope' do
sign_in_as_user
assert warden.authenticated?(:user)
assert_not warden.authenticated?(:admin)
end
test 'sign in as admin should not authenticate user scope' do
sign_in_as_admin
assert warden.authenticated?(:admin)
assert_not warden.authenticated?(:user)
end
@ -33,59 +22,61 @@ class AuthenticationSanityTest < ActionController::IntegrationTest
test 'sign in as both user and admin at same time' do
sign_in_as_user
sign_in_as_admin
assert warden.authenticated?(:user)
assert warden.authenticated?(:admin)
end
test 'sign out as user should not touch admin authentication if sign_out_all_scopes is false' do
sign_in_as_user
sign_in_as_admin
get destroy_user_session_path
assert_not warden.authenticated?(:user)
assert warden.authenticated?(:admin)
swap Devise, :sign_out_all_scopes => false do
sign_in_as_user
sign_in_as_admin
get destroy_user_session_path
assert_not warden.authenticated?(:user)
assert warden.authenticated?(:admin)
end
end
test 'sign out as admin should not touch user authentication if sign_out_all_scopes is false' do
sign_in_as_user
sign_in_as_admin
swap Devise, :sign_out_all_scopes => false do
sign_in_as_user
sign_in_as_admin
get destroy_admin_session_path
assert_not warden.authenticated?(:admin)
assert warden.authenticated?(:user)
get destroy_admin_session_path
assert_not warden.authenticated?(:admin)
assert warden.authenticated?(:user)
end
end
test 'sign out as user should also sign out admin if sign_out_all_scopes is true' do
Devise.sign_out_all_scopes = true
sign_in_as_user
sign_in_as_admin
swap Devise, :sign_out_all_scopes => true do
sign_in_as_user
sign_in_as_admin
get destroy_user_session_path
assert_not warden.authenticated?(:user)
assert_not warden.authenticated?(:admin)
get destroy_user_session_path
assert_not warden.authenticated?(:user)
assert_not warden.authenticated?(:admin)
end
end
test 'sign out as admin should also sign out user if sign_out_all_scopes is true' do
Devise.sign_out_all_scopes = true
sign_in_as_user
sign_in_as_admin
swap Devise, :sign_out_all_scopes => true do
sign_in_as_user
sign_in_as_admin
get destroy_admin_session_path
assert_not warden.authenticated?(:admin)
assert_not warden.authenticated?(:user)
get destroy_admin_session_path
assert_not warden.authenticated?(:admin)
assert_not warden.authenticated?(:user)
end
end
test 'not signed in as admin should not be able to access admins actions' do
get admins_path
assert_redirected_to new_admin_session_path
assert_not warden.authenticated?(:admin)
end
test 'not signed in as admin should not be able to access private route restricted to admins' do
get private_path
assert_redirected_to new_admin_session_path
assert_not warden.authenticated?(:admin)
end
@ -94,7 +85,6 @@ class AuthenticationSanityTest < ActionController::IntegrationTest
sign_in_as_user
assert warden.authenticated?(:user)
assert_not warden.authenticated?(:admin)
get private_path
assert_redirected_to new_admin_session_path
end
@ -236,6 +226,25 @@ class AuthenticationSessionTest < ActionController::IntegrationTest
get '/users'
assert_equal "Cart", @controller.user_session[:cart]
end
test 'does not explode when invalid user class is stored in session' do
klass = User
paths = ActiveSupport::Dependencies.autoload_paths.dup
begin
sign_in_as_user
assert warden.authenticated?(:user)
Object.send :remove_const, :User
ActiveSupport::Dependencies.autoload_paths.clear
visit "/users"
assert_not warden.authenticated?(:user)
ensure
Object.const_set(:User, klass)
ActiveSupport::Dependencies.autoload_paths.replace(paths)
end
end
end
class AuthenticationWithScopesTest < ActionController::IntegrationTest
@ -277,18 +286,6 @@ class AuthenticationWithScopesTest < ActionController::IntegrationTest
end
end
end
test 'uses the mapping from router' do
sign_in_as_user :visit => "/as/sign_in"
assert warden.authenticated?(:user)
assert_not warden.authenticated?(:admin)
end
test 'uses the mapping from nested devise_for call' do
sign_in_as_user :visit => "/devise_for/sign_in"
assert warden.authenticated?(:user)
assert_not warden.authenticated?(:admin)
end
end
class AuthenticationOthersTest < ActionController::IntegrationTest
@ -317,28 +314,21 @@ class AuthenticationOthersTest < ActionController::IntegrationTest
end
end
test 'registration in xml format' do
test 'registration in xml format works when recognizing path' do
assert_nothing_raised do
post user_registration_path(:format => 'xml', :user => {:email => "test@example.com", :password => "invalid"} )
end
end
test 'does not explode when invalid user class is stored in session' do
klass = User
paths = ActiveSupport::Dependencies.autoload_paths.dup
test 'uses the mapping from router' do
sign_in_as_user :visit => "/as/sign_in"
assert warden.authenticated?(:user)
assert_not warden.authenticated?(:admin)
end
begin
sign_in_as_user
assert warden.authenticated?(:user)
Object.send :remove_const, :User
ActiveSupport::Dependencies.autoload_paths.clear
visit "/users"
assert_not warden.authenticated?(:user)
ensure
Object.const_set(:User, klass)
ActiveSupport::Dependencies.autoload_paths.replace(paths)
end
test 'uses the mapping from nested devise_for call' do
sign_in_as_user :visit => "/devise_for/sign_in"
assert warden.authenticated?(:user)
assert_not warden.authenticated?(:admin)
end
end

View File

@ -0,0 +1,295 @@
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!(times=1)
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!(times=1)
# If times != 1, use invalid data
data = (times != 1) ? FACEBOOK_INFO.except(:email) : FACEBOOK_INFO
Devise::Oauth.stub!(:facebook) do |b|
b.post('/oauth/access_token') { [200, {}, ACCESS_TOKEN.to_json] }
times.times {
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!(2)
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.update_attribute(:email, "another@test.com")
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 "[BASIC] setup skipping oauth callback" do
stub_github!
assert_no_difference "User.count" do
visit "/users/sign_in"
click_link "Sign in with Github"
end
assert_current_url "/users/sign_in"
assert_contain "Skipped Oauth authorization for Github."
assert_not warden.authenticated?(:user)
assert_not warden.authenticated?(:admin)
end
test "[SESSION CLEANUP] ensures session is cleaned up after sign up" do
stub_facebook!(2)
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!(2)
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!(2)
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 "[I18N] scopes messages based on oauth callback for skipped" do
stub_github!
store_translations :en, :devise => { :oauth_callbacks => {
:github => { :skipped => "Skipped github" } } } do
visit "/users/sign_in"
click_link "Sign in with Github"
assert_contain "Skipped github"
end
end
test "[I18N] scopes messages based on oauth callback and resource name for skipped" do
stub_github!
store_translations :en, :devise => { :oauth_callbacks => {
:user => { :github => { :skipped => "Skipped github user" } },
:github => { :skipped => "Skipped github" } } } do
visit "/users/sign_in"
click_link "Sign in with Github"
assert_contain "Skipped github 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
store_translations :en, :devise => { :oauth_callbacks => { :access_denied => "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 for custom messages" do
store_translations :en, :devise => { :oauth_callbacks => {
:facebook => { :access_denied => "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 => { :access_denied => "Access denied user" } },
:facebook => { :access_denied => "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
test "[FAILURE][I18N] trim messages to avoid long symbols lookups" do
store_translations :en, :devise => { :oauth_callbacks => {
:facebook => { ("a"*25) => "Access denied bro" } } } do
visit "/users/oauth/facebook/callback?error=#{"a"*100}"
assert_current_url "/users/sign_in"
assert_contain "Access denied bro"
end
end
end

View File

@ -150,4 +150,16 @@ class RegistrationTest < ActionController::IntegrationTest
assert User.all.empty?
end
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"]
get "/users/sign_up"
assert_equal "something", @request.session["user_provider_oauth_token"]
get "/users/cancel"
assert_nil @request.session["user_provider_oauth_token"]
assert_redirected_to new_user_registration_path
end
end

View File

@ -0,0 +1,21 @@
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

44
test/oauth/config_test.rb Normal file
View File

@ -0,0 +1,44 @@
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

@ -0,0 +1,47 @@
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

@ -1,3 +1,6 @@
require 'shared_admin'
class Admin < ActiveRecord::Base
devise :database_authenticatable, :registerable, :timeoutable, :recoverable, :lockable, :unlock_strategy => :time
include Shim
include SharedAdmin
end

View File

@ -1,7 +1,8 @@
class User < ActiveRecord::Base
devise :database_authenticatable, :confirmable, :lockable, :recoverable,
:registerable, :rememberable, :timeoutable, :token_authenticatable,
:trackable, :validatable
require 'shared_user'
attr_accessible :username, :email, :password, :password_confirmation
class User < ActiveRecord::Base
include Shim
include SharedUser
attr_accessible :username, :email, :password, :password_confirmation, :remember_me
end

View File

@ -4,4 +4,9 @@ class HomeController < ApplicationController
def private
end
def set
session["user_provider_oauth_token"] = "something"
head :ok
end
end

View File

@ -1,6 +1,7 @@
require 'shared_admin'
class Admin
include Mongoid::Document
include Shim
devise :database_authenticatable, :timeoutable, :registerable, :recoverable, :lockable, :unlock_strategy => :time
include SharedAdmin
end

View File

@ -1,6 +1,10 @@
module Shim
extend ::ActiveSupport::Concern
include ::Mongoid::Timestamps
included do
include ::Mongoid::Timestamps
field :created_at, :type => DateTime
end
module ClassMethods
def last(options={})

View File

@ -1,10 +1,7 @@
require 'shared_user'
class User
include Mongoid::Document
include Shim
field :created_at, :type => DateTime
devise :database_authenticatable, :confirmable, :lockable, :recoverable,
:registerable, :rememberable, :timeoutable, :token_authenticatable,
:trackable, :validatable
include SharedUser
end

View File

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

View File

@ -123,6 +123,17 @@ Devise.setup do |config|
# should add them to the navigational formats lists. Default is [:html]
# config.navigational_formats = [:html, :iphone]
# ==> OAuth
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)
# ==> Warden configuration
# If you want to use other strategies, that are not (yet) supported by Devise,
# you can configure them inside the config.warden block. The example below

View File

@ -39,9 +39,10 @@ Rails.application.routes.draw do
:sign_in => "login", :sign_out => "logout",
:password => "secret", :confirmation => "verification",
:unlock => "unblock", :sign_up => "register",
:registration => "management"
:registration => "management", :cancel => "giveup"
}
end
match "/set", :to => "home#set"
root :to => "home#index"
end

View File

@ -1,27 +1,29 @@
class CreateTables < ActiveRecord::Migration
def self.up
[:users, :admins, :accounts].each do |table|
create_table table do |t|
t.database_authenticatable :null => (table == :admins)
create_table :users do |t|
t.string :username
t.string :facebook_token
if table != :admin
t.string :username
t.confirmable
t.recoverable
t.rememberable
t.trackable
t.lockable
t.token_authenticatable
end
t.database_authenticatable :null => false
t.confirmable
t.recoverable
t.rememberable
t.trackable
t.lockable
t.token_authenticatable
t.timestamps
end
t.timestamps
end
create_table :admins do |t|
t.database_authenticatable :null => true, :encryptor => :bcrypt
t.recoverable
t.lockable
t.timestamps
end
end
def self.down
[:users, :admins, :accounts].each do |table|
drop_table table
end
drop_table :users
drop_table :admins
end
end

View File

@ -1,81 +1,47 @@
# This file is auto-generated from the current state of the database. Instead of editing this file,
# please use the migrations feature of Active Record to incrementally modify your database, and
# then regenerate this schema definition.
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# Note that this schema.rb definition is the authoritative source for your database schema. If you need
# to create the application database on another system, you should be using db:schema:load, not running
# all the migrations from scratch. The latter is a flawed and unsustainable approach (the more migrations
# Note that this schema.rb definition is the authoritative source for your
# database schema. If you need to create the application database on another
# system, you should be using db:schema:load, not running all the migrations
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
# you'll amass, the slower it'll run and the greater likelihood for issues).
#
# It's strongly recommended to check this file into your version control system.
ActiveRecord::Schema.define(:version => 20100401102949) do
create_table "accounts", :force => true do |t|
t.string "email", :default => "", :null => false
t.string "encrypted_password", :default => "", :null => false
t.string "password_salt", :default => "", :null => false
t.string "username"
t.string "confirmation_token"
t.datetime "confirmed_at"
t.datetime "confirmation_sent_at"
t.string "reset_password_token"
t.string "remember_token"
t.datetime "remember_created_at"
t.integer "sign_in_count", :default => 0
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.integer "failed_attempts", :default => 0
t.string "unlock_token"
t.datetime "locked_at"
t.string "authentication_token"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "admins", :force => true do |t|
t.string "email", :default => ""
t.string "encrypted_password", :default => ""
t.string "password_salt", :default => ""
t.string "username"
t.string "confirmation_token"
t.datetime "confirmed_at"
t.datetime "confirmation_sent_at"
t.string "email", :default => ""
t.string "encrypted_password", :limit => 128, :default => ""
t.string "password_salt", :default => ""
t.string "reset_password_token"
t.string "remember_token"
t.datetime "remember_created_at"
t.integer "sign_in_count", :default => 0
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.integer "failed_attempts", :default => 0
t.integer "failed_attempts", :default => 0
t.string "unlock_token"
t.datetime "locked_at"
t.string "authentication_token"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "users", :force => true do |t|
t.string "email", :default => "", :null => false
t.string "encrypted_password", :default => "", :null => false
t.string "password_salt", :default => "", :null => false
t.string "username"
t.string "facebook_token"
t.string "email", :default => "", :null => false
t.string "encrypted_password", :limit => 128, :default => "", :null => false
t.string "password_salt", :default => "", :null => false
t.string "confirmation_token"
t.datetime "confirmed_at"
t.datetime "confirmation_sent_at"
t.string "reset_password_token"
t.string "remember_token"
t.datetime "remember_created_at"
t.integer "sign_in_count", :default => 0
t.integer "sign_in_count", :default => 0
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.integer "failed_attempts", :default => 0
t.integer "failed_attempts", :default => 0
t.string "unlock_token"
t.datetime "locked_at"
t.string "authentication_token"

View File

@ -0,0 +1,7 @@
module SharedAdmin
extend ActiveSupport::Concern
included do
devise :database_authenticatable, :registerable, :timeoutable, :recoverable, :lockable, :unlock_strategy => :time
end
end

View File

@ -0,0 +1,48 @@
module SharedUser
extend ActiveSupport::Concern
included do
devise :database_authenticatable, :confirmable, :lockable, :recoverable,
:registerable, :rememberable, :timeoutable, :token_authenticatable,
:trackable, :validatable, :oauthable
# 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)
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
self.password_confirmation = nil
end
yield self if block_given?
end
end

View File

@ -86,10 +86,27 @@ class DefaultRoutingTest < ActionController::TestCase
assert_recognizes({:controller => 'devise/registrations', :action => 'destroy'}, {:path => 'users', :method => :delete})
end
test 'map cancel user registration' do
assert_recognizes({:controller => 'devise/registrations', :action => 'cancel'}, {:path => 'users/cancel', :method => :get})
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
assert_raise ActionController::RoutingError do
assert_recognizes({:controller => 'devise/oauth_callbacks', :action => 'twitter'}, {:path => 'users/oauth/twitter/callback', :method => :get})
end
end
protected
def assert_named_route(result, name)
assert_equal result, @routes.url_helpers.send(name)
def assert_named_route(result, *args)
assert_equal result, @routes.url_helpers.send(*args)
end
end
@ -131,6 +148,11 @@ class CustomizedRoutingTest < ActionController::TestCase
test 'map account with custom path name for registration' do
assert_recognizes({:controller => 'devise/registrations', :action => 'new', :locale => 'en'}, '/en/accounts/management/register')
end
test 'map account with custom path name for cancel registration' do
assert_recognizes({:controller => 'devise/registrations', :action => 'cancel', :locale => 'en'}, '/en/accounts/management/giveup')
end
end
class ScopedRoutingTest < ActionController::TestCase

View File

@ -1,5 +0,0 @@
module Devise
module TestSilencer
def test(*args, &block); end
end
end

View File

@ -16,6 +16,8 @@ Webrat.configure do |config|
config.open_error_files = false
end
Devise::Oauth.test_mode!
# Add support to load paths so we can overwrite broken webrat setup
$:.unshift File.expand_path('../support', __FILE__)
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }