diff --git a/Gemfile b/Gemfile index 887a8aef..c1855e23 100644 --- a/Gemfile +++ b/Gemfile @@ -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" diff --git a/Gemfile.lock b/Gemfile.lock index 09dbae25..0ee4dc84 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) @@ -77,6 +81,9 @@ GEM mongo (1.0.5) bson (>= 1.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 +118,7 @@ DEPENDENCIES mocha mongo mongoid! + oauth2 rails! ruby-debug (>= 0.10.3) sqlite3-ruby diff --git a/app/controllers/devise/oauth_callbacks_controller.rb b/app/controllers/devise/oauth_callbacks_controller.rb index c007117f..445fd42c 100644 --- a/app/controllers/devise/oauth_callbacks_controller.rb +++ b/app/controllers/devise/oauth_callbacks_controller.rb @@ -1,9 +1,4 @@ class Devise::OauthCallbacksController < ApplicationController include Devise::Controllers::InternalHelpers - - def twitter - end - - def github - end + include Devise::Oauth::Helpers end diff --git a/lib/devise.rb b/lib/devise.rb index b91c64fd..165513df 100644 --- a/lib/devise.rb +++ b/lib/devise.rb @@ -3,6 +3,7 @@ require 'active_support/dependencies' 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' @@ -35,6 +36,7 @@ module Devise 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'] @@ -191,7 +193,9 @@ module Devise def self.oauth(provider, *args) @@oauth_providers << provider @@oauth_providers.uniq! - @@oauth_configs[provider] = Devise::OAuth::Config.new(*args) + + Devise::Oauth::Helpers.create_action(provider) + @@oauth_configs[provider] = Devise::Oauth::Config.new(*args) end def self.use_default_scope=(*) @@ -226,7 +230,7 @@ module Devise 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: # @@ -248,21 +252,31 @@ 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] diff --git a/lib/devise/controllers/helpers.rb b/lib/devise/controllers/helpers.rb index 2129c990..608e4f2b 100644 --- a/lib/devise/controllers/helpers.rb +++ b/lib/devise/controllers/helpers.rb @@ -40,13 +40,15 @@ 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 + 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 +161,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,6 +184,10 @@ 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 diff --git a/lib/devise/controllers/internal_helpers.rb b/lib/devise/controllers/internal_helpers.rb index 7be60c94..95669d4a 100644 --- a/lib/devise/controllers/internal_helpers.rb +++ b/lib/devise/controllers/internal_helpers.rb @@ -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"] diff --git a/lib/devise/controllers/scoped_views.rb b/lib/devise/controllers/scoped_views.rb index aa0a3d42..0736a0f9 100644 --- a/lib/devise/controllers/scoped_views.rb +++ b/lib/devise/controllers/scoped_views.rb @@ -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 diff --git a/lib/devise/controllers/url_helpers.rb b/lib/devise/controllers/url_helpers.rb index c39c11e2..b1bb46da 100644 --- a/lib/devise/controllers/url_helpers.rb +++ b/lib/devise/controllers/url_helpers.rb @@ -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) diff --git a/lib/devise/models/oauthable.rb b/lib/devise/models/oauthable.rb index 03ae83dd..a48675ae 100644 --- a/lib/devise/models/oauthable.rb +++ b/lib/devise/models/oauthable.rb @@ -4,6 +4,10 @@ module Devise extend ActiveSupport::Concern module ClassMethods + def oauth_configs + Devise.oauth_configs.slice(*oauth_providers) + end + Devise::Models.config(self, :oauth_providers) end end diff --git a/lib/devise/modules.rb b/lib/devise/modules.rb index cda6aec2..cfcac57a 100644 --- a/lib/devise/modules.rb +++ b/lib/devise/modules.rb @@ -2,23 +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 # Other authentications - d.add_module :oauthable, :controller => :oauth_callbacks, :route => :oauth_callback + d.add_module :oauthable, :controller => :oauth_callbacks, :route => :oauth_callback # Misc after - d.add_module :recoverable, :controller => :passwords, :route => :password - d.add_module :registerable, :controller => :registrations, :route => :registration + routes = [nil, :new, :edit] + d.add_module :recoverable, :controller => :passwords, :route => { :password => routes } + d.add_module :registerable, :controller => :registrations, :route => { :registration => routes } 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 diff --git a/lib/devise/oauth.rb b/lib/devise/oauth.rb new file mode 100644 index 00000000..093b7de2 --- /dev/null +++ b/lib/devise/oauth.rb @@ -0,0 +1,14 @@ +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 :UrlHelpers, "devise/oauth/url_helpers" + end +end \ No newline at end of file diff --git a/lib/devise/oauth/config.rb b/lib/devise/oauth/config.rb new file mode 100644 index 00000000..bcf4f5ad --- /dev/null +++ b/lib/devise/oauth/config.rb @@ -0,0 +1,12 @@ +module Devise + module Oauth + class Config + attr_reader :scope, :client + + def initialize(app_id, app_secret, options) + @scope = Array.wrap(options.delete(:scope)).join(",") + @client = OAuth2::Client.new(app_id, app_secret, options) + end + end + end +end \ No newline at end of file diff --git a/lib/devise/oauth/helpers.rb b/lib/devise/oauth/helpers.rb new file mode 100644 index 00000000..7e712aba --- /dev/null +++ b/lib/devise/oauth/helpers.rb @@ -0,0 +1,69 @@ +module Devise + module Oauth + module Helpers + extend ActiveSupport::Concern + + def self.create_action(name) + alias_method(name, :callback_action) + public name + end + + included do + helpers = %w(oauth_callback oauth_config oauth_client) + hide_action *helpers + helper_method *helpers + before_filter :is_oauth_callback? + end + + def oauth_callback + @oauth_callback ||= action_name.to_sym + end + + def oauth_config + @oauth_client ||= resource_class.oauth_configs[oauth_callback] + end + + def oauth_client + @oauth_client ||= oauth_config.client + end + + protected + + def is_oauth_callback? + raise ActionController::UnknownAction unless oauth_config + raise ActionController::UnknownAction unless params[:code] + end + + def oauth_model_callback + "authentication_for_#{oauth_callback}_oauth" + end + + def callback_action + access_token = oauth_client.web_server.get_access_token(params[:code]) + self.resource = User.send(oauth_model_callback, access_token, signed_in_resource) + + if resource.persisted? + sign_in_and_redirect resource_name, resource, :event => :authentication + else + render_for_oauth + end + end + + def render_for_oauth + render_with_scope oauth_callback + rescue ActionView::MissingTemplate + render_with_scope :new, devise_mapping.controllers[:registrations] + end + + # The default hook used by oauth to specify the redirect url. + def after_oauth_sign_in_path_for(resource_or_scope) + after_sign_in_path_for(resource_or_scope) + end + + # Overwrite redirect_for_sign_in so it takes uses after_oauth_sign_in_path_for. + def redirect_for_sign_in(scope, resource) #:nodoc: + redirect_to stored_location_for(scope) || after_oauth_sign_in_path_for(resource) + end + end + end +end \ No newline at end of file diff --git a/lib/devise/oauth/url_helpers.rb b/lib/devise/oauth/url_helpers.rb new file mode 100644 index 00000000..c0a29a2b --- /dev/null +++ b/lib/devise/oauth/url_helpers.rb @@ -0,0 +1,7 @@ +module Devise + module Oauth + module UrlHelpers + + end + end +end \ No newline at end of file diff --git a/lib/devise/rails.rb b/lib/devise/rails.rb index 30ba6f4f..9dc7c5d2 100644 --- a/lib/devise/rails.rb +++ b/lib/devise/rails.rb @@ -1,10 +1,6 @@ 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 @@ -30,6 +26,11 @@ module Devise app.config.filter_parameters.uniq end + initializer "devise.url_helpers" do + ActiveSupport.on_load(:action_controller) { include Devise::Controllers::UrlHelpers } + ActiveSupport.on_load(:action_view) { include Devise::Controllers::UrlHelpers } + end + unless Rails.env.production? config.after_initialize do actions = [:confirmation_instructions, :reset_password_instructions, :unlock_instructions] diff --git a/test/rails_app/config/initializers/devise.rb b/test/rails_app/config/initializers/devise.rb index 5a340f43..a0bde5e9 100644 --- a/test/rails_app/config/initializers/devise.rb +++ b/test/rails_app/config/initializers/devise.rb @@ -123,6 +123,16 @@ 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 :twitter, 'APP_ID', 'APP_SECRET', + :site => 'http://twitter.com/' + # ==> 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 diff --git a/test/test_helper.rb b/test/test_helper.rb index 56d09074..de3555b6 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -16,6 +16,8 @@ Webrat.configure do |config| config.open_error_files = false end +Faraday.default_adapter = :test + # 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 } \ No newline at end of file