From e468f9b6ef21bb589b31bdbe4195a9a21cce9389 Mon Sep 17 00:00:00 2001 From: Scott Date: Tue, 15 Feb 2011 22:18:38 -0500 Subject: [PATCH 1/5] Making this work with twitter oauth 1.0a --- lib/warden_oauth/strategy.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/warden_oauth/strategy.rb b/lib/warden_oauth/strategy.rb index c5be33c..d3077b9 100644 --- a/lib/warden_oauth/strategy.rb +++ b/lib/warden_oauth/strategy.rb @@ -43,7 +43,7 @@ def valid? def authenticate! if params.include?('warden_oauth_provider') store_request_token_on_session - redirect!(request_token.authorize_url) + redirect!(request_token.authorize_url(config.options)) throw(:warden) elsif params.include?('oauth_token') load_request_token_from_session @@ -78,8 +78,7 @@ def consumer end def request_token - host_with_port = Warden::OAuth::Utils.host_with_port(request) - @request_token ||= consumer.get_request_token(:oauth_callback => host_with_port) + @request_token ||= consumer.get_request_token(:oauth_callback => config.options[:oauth_callback]) end def access_token From 324fe72fb6f3681e1085bc8d49dbf9da0a0349b3 Mon Sep 17 00:00:00 2001 From: Scott Date: Tue, 15 Feb 2011 22:23:36 -0500 Subject: [PATCH 2/5] Updating rdoc --- README.rdoc | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.rdoc b/README.rdoc index 2ceb889..04cfc65 100644 --- a/README.rdoc +++ b/README.rdoc @@ -13,10 +13,18 @@ oauth services you would like to have on the Warden::Manager middleware config.oauth(:twitter) do |twitter| twitter.consumer_secret = twitter.consumer_key = - twitter.options :site => 'http://twitter.com' + twitter.options({ + :site => 'http://twitter.com', + :oauth_callback => (BASE_URL.end_with?('/') ? BASE_URL[0..-2] : BASE_URL) + '/users', + :oauth_callback_confirmed => 'true' + }) end config.default_strategies(:twitter_oauth, :password, :other) end + +You then need to set the BASE_URL constant so that the oauth_callback will work: + +BASE_URL = "http://localhost" == Giving an Access Token fetcher From 30de11519bdbf08e1ab365bf14d0532a48e3551d Mon Sep 17 00:00:00 2001 From: Scott Date: Tue, 15 Feb 2011 22:25:20 -0500 Subject: [PATCH 3/5] Updating rdoc --- README.rdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rdoc b/README.rdoc index 04cfc65..8d44353 100644 --- a/README.rdoc +++ b/README.rdoc @@ -24,7 +24,7 @@ oauth services you would like to have on the Warden::Manager middleware You then need to set the BASE_URL constant so that the oauth_callback will work: -BASE_URL = "http://localhost" + BASE_URL = "http://localhost" == Giving an Access Token fetcher From 4927b1da09e8e3d973aa006b05a13fe7c138f78a Mon Sep 17 00:00:00 2001 From: Scott Date: Tue, 15 Feb 2011 22:26:42 -0500 Subject: [PATCH 4/5] Updating rdoc --- README.rdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rdoc b/README.rdoc index 8d44353..e502c93 100644 --- a/README.rdoc +++ b/README.rdoc @@ -22,7 +22,7 @@ oauth services you would like to have on the Warden::Manager middleware config.default_strategies(:twitter_oauth, :password, :other) end -You then need to set the BASE_URL constant so that the oauth_callback will work: +You then need to set the BASE_URL constant in your environment files so that the oauth_callback will work: BASE_URL = "http://localhost" From 522bcc1188a1a9b37202c589b6d274d4c240abc1 Mon Sep 17 00:00:00 2001 From: Scott Date: Tue, 22 Feb 2011 11:16:30 -0500 Subject: [PATCH 5/5] Adding the ability to use oauth2 --- README.rdoc | 27 +++++ lib/warden_oauth.rb | 20 ++++ lib/warden_oauth2/base.rb | 13 +++ lib/warden_oauth2/config.rb | 51 +++++++++ lib/warden_oauth2/config_extension.rb | 45 ++++++++ lib/warden_oauth2/errors.rb | 9 ++ lib/warden_oauth2/strategy.rb | 144 ++++++++++++++++++++++++++ lib/warden_oauth2/strategy_builder.rb | 97 +++++++++++++++++ lib/warden_oauth2/utils.rb | 40 +++++++ warden_oauth.gemspec | 8 ++ 10 files changed, 454 insertions(+) create mode 100644 lib/warden_oauth2/base.rb create mode 100644 lib/warden_oauth2/config.rb create mode 100644 lib/warden_oauth2/config_extension.rb create mode 100644 lib/warden_oauth2/errors.rb create mode 100644 lib/warden_oauth2/strategy.rb create mode 100644 lib/warden_oauth2/strategy_builder.rb create mode 100644 lib/warden_oauth2/utils.rb diff --git a/README.rdoc b/README.rdoc index e502c93..9b8ea0a 100644 --- a/README.rdoc +++ b/README.rdoc @@ -69,7 +69,34 @@ Note: In Rails, don't set the :warden_oauth_provider parameter as part of the login route, if you do this, rails will catch the parameter, but not the warden rack middleware, ergo, it won't work as expected. +== Oauth 2.0 +To use oauth 2.0 it is very similar to standard oauth. Here is my example for working with facebook + + + config.warden do |manager| + manager.oauth2(:facebook) do |fb| + fb.consumer_key = FACEBOOK_CONSUMER_KEY + fb.consumer_secret = FACEBOOK_CONSUMER_SECRET + fb.client_id = FACEBOOK_CLIENT_ID + fb.options({ + :authorize_url => 'https://www.facebook.com/dialog/oauth', + :access_token_url => 'https://graph.facebook.com/oauth/access_token', + :site => 'https://graph.facebook.com', + :scope => 'email,offline_access', + :redirect_uri => (BASE_URL.end_with?('/') ? BASE_URL[0..-2] : BASE_URL) + '/users' + }) + end + manager.default_strategies(:scope => :user).unshift :facebook_oauth + end + +Note: You still need to give an Access Token fetcher + + Warden::OAuth2.access_token_user_finder(:facebook) do |access_token| + end + +Note: Instead of oauth the keyword oauth2 is used in both examples above + == Examples If you want to know how to make a twitter authentication client, check examples/twitter/application.rb diff --git a/lib/warden_oauth.rb b/lib/warden_oauth.rb index 4dceb36..63adb7b 100644 --- a/lib/warden_oauth.rb +++ b/lib/warden_oauth.rb @@ -18,3 +18,23 @@ module OAuth end end + +require "oauth2" + +module Warden + module OAuth2 + + base_path = File.dirname(__FILE__) + "/warden_oauth2" + + require base_path + "/base" + require base_path + "/errors" + autoload :Utils, base_path + '/utils' + autoload :StrategyBuilder, base_path + '/strategy_builder' + autoload :Strategy, base_path + '/strategy' + autoload :Config, base_path + "/config" + require base_path + "/config_extension" + + + end +end + diff --git a/lib/warden_oauth2/base.rb b/lib/warden_oauth2/base.rb new file mode 100644 index 0000000..b38d641 --- /dev/null +++ b/lib/warden_oauth2/base.rb @@ -0,0 +1,13 @@ +module Warden + module OAuth2 + + def self.access_token_user_finder(key, &block) + Strategy.access_token_user_finders[key] = block + end + + def self.clear_access_token_user_finders + Strategy.access_token_user_finders.clear + end + + end +end diff --git a/lib/warden_oauth2/config.rb b/lib/warden_oauth2/config.rb new file mode 100644 index 0000000..4d7258c --- /dev/null +++ b/lib/warden_oauth2/config.rb @@ -0,0 +1,51 @@ +module Warden + module OAuth2 + + # + # Holds all the information of the OAuth service. + # + class Config + attr_accessor :provider_name + + def consumer_key(key = nil) + unless key.nil? + @consumer_key = key + end + @consumer_key + end + alias_method :consumer_key=, :consumer_key + + def consumer_secret(secret = nil) + unless secret.nil? + @consumer_secret = secret + end + @consumer_secret + end + alias_method :consumer_secret=, :consumer_secret + + def client_id(client_id = nil) + unless client_id.nil? + @client_id = client_id + end + @client_id + end + alias_method :client_id=, :client_id + + def options(options = nil) + unless options.nil? + @options = options + end + @options + end + alias_method :options=, :options + + def check_requirements + if @consumer_key.nil? || @consumer_secret.nil? || @client_id.nil? + raise Warden::OAuth2::ConfigError.new("You need to specify the consumer key, consumer secret, and client id") + end + end + + end + + end +end diff --git a/lib/warden_oauth2/config_extension.rb b/lib/warden_oauth2/config_extension.rb new file mode 100644 index 0000000..c838d63 --- /dev/null +++ b/lib/warden_oauth2/config_extension.rb @@ -0,0 +1,45 @@ +module Warden + module OAuth2 + + # + # Holds all the extensions made to Warden::Config in order to create OAuth + # consumers. + # + module ConfigExtension + + # + # Helps to setup a new OAuth client authentication, to get started you need to define + # a service name, and then on the block assign the different values required in order + # to boot the OAuth process. + # @param [Symbol] service An identifier of the OAuth service + # + # @example + # use Warden::Manager do |config| + # config.oauth(:twitter) do + # consumer_key "" + # consumer_secret "" + # options :site => 'http://twitter.com' + # end + # end + # + def oauth2(service, &block) + config = Warden::OAuth2::Config.new + if block_given? + if block.arity == 1 + yield config + else + config.instance_eval(&block) + end + end + config.check_requirements + config.provider_name = service + Warden::OAuth2::Strategy.build(service, config) + end + + end + + end +end + +Warden::Config.send(:include, Warden::OAuth2::ConfigExtension) + diff --git a/lib/warden_oauth2/errors.rb b/lib/warden_oauth2/errors.rb new file mode 100644 index 0000000..736761f --- /dev/null +++ b/lib/warden_oauth2/errors.rb @@ -0,0 +1,9 @@ +module Warden + module OAuth2 + + class ConfigError < ArgumentError; end + class ServiceAlreadyRegistered < StandardError; end + class AccessTokenFinderMissing < StandardError; end + + end +end diff --git a/lib/warden_oauth2/strategy.rb b/lib/warden_oauth2/strategy.rb new file mode 100644 index 0000000..71c33ce --- /dev/null +++ b/lib/warden_oauth2/strategy.rb @@ -0,0 +1,144 @@ +module Warden + module OAuth2 + + # + # Holds all the main logic of the OAuth authentication, all the generated + # OAuth classes will extend from this class + # + class Strategy < Warden::Strategies::Base + extend StrategyBuilder + + ###################### + ### Strategy Logic ### + ###################### + + + def self.access_token_user_finders + (@_user_token_finders ||= {}) + end + + # + # An OAuth strategy will be valid to execute if: + # * A 'warden_oauth_provider' parameter is given, with the name of the OAuth service + # * A 'oauth_token' is being receive on the request (response from an OAuth provider) + # + def valid? + (params.include?('warden_oauth2_provider') && params['warden_oauth2_provider'] == config.provider_name.to_s) || + params.include?('code') + end + + + # + # Manages the OAuth authentication process, there can be 3 outcomes from this Strategy: + # 1. The OAuth credentials are invalid and the FailureApp is called + # 2. The OAuth credentials are valid, but there is no user associated to them. In this case + # the FailureApp is called, but the env['warden.options'][:oauth][:access_token] will be + # available. + # 3. The OAuth credentials are valid, and the user is authenticated successfuly + # + # @note + # If you want to signup users with the twitter credentials, you can manage the creation of a new + # user in the FailureApp with the given access_token + # + def authenticate! + puts "Inside OAUTH2 authenticate!****^^^^^^^^^^^^^^^^^^^" + if params.include?('warden_oauth2_provider') + #store_request_token_url_on_session + redirect!(consumer.authorize_url(:redirect_uri => config.options[:redirect_uri],:client_id => config.client_id,:scope => config.options[:scope])) + throw(:warden) + elsif params.include?('code') + #load_request_token_from_session + user = find_user_by_access_token(access_token) + if user.nil? + puts "got bad user" + fail!("User with access token not found") + throw_error_with_oauth_info + else + puts "Got success" + success!(user) + end + end + end + + def fail!(msg) #:nodoc: + self.errors.add(service_param_name.to_sym, msg) + super + end + + ################### + ### OAuth Logic ### + ################### + + def consumer + puts 'options - ' + config.options.inspect + @consumer ||= ::OAuth2::Client.new(config.client_id, config.consumer_secret, config.options) + end + + # def request_token + # #@request_token_url ||= consumer.get_request_token(:redirect_uri => config.options[:redirect_uri],:scope => config.options[:scope]) + # @request_token_url ||= consumer.authorize_url + # end + # + def access_token + puts 'access_token_url = ' + consumer.access_token_url.inspect + @access_token ||= consumer.web_server.get_access_token(params['code'],:redirect_uri => config.options[:redirect_uri]) + end + + protected + + def find_user_by_access_token(access_token) + raise RuntimeError.new(<<-ERROR_MESSAGE) unless self.respond_to?(:_find_user_by_access_token) + +You need to define a finder by access_token for this strategy. +Write on the warden initializer the following code: +Warden::OAuth2.access_token_user_finder(:#{config.provider_name}) do |access_token| + # Logic to get your user from an access_token +end + +ERROR_MESSAGE + self._find_user_by_access_token(access_token) + end + + def throw_error_with_oauth_info + throw(:warden, :oauth => { + self.config.provider_name => { + :provider => config.provider_name, + :access_token => access_token, + :consumer_key => config.consumer_key, + :consumer_secret => config.consumer_secret + } + }) + end + + # def store_request_token_on_session + # puts 'request_token - ' + request_token.inspect + # session[:request_token] = request_token.token + # session[:request_secret] = request_token.secret + # end + # + # def load_request_token_from_session + # token = session.delete(:request_token) + # secret = session.delete(:request_secret) + # @request_token = ::OAuth2::RequestToken.new(consumer, token, secret) + # end + + # def missing_stored_token? + # !request_token + # end + # + # def stored_token_match_recieved_token? + # request_token.token == params['oauth_token'] + # end + + def service_param_name + '%s_oauth2' % config.provider_name + end + + def config + self.class::CONFIG + end + + end + + end +end diff --git a/lib/warden_oauth2/strategy_builder.rb b/lib/warden_oauth2/strategy_builder.rb new file mode 100644 index 0000000..8fe7745 --- /dev/null +++ b/lib/warden_oauth2/strategy_builder.rb @@ -0,0 +1,97 @@ +module Warden + module OAuth2 + + # + # Handles the creation an registration of OAuth strategies based on configuration parameters + # via the Warden::Manager.oauth method + # + module StrategyBuilder + extend self + + + # + # Defines the user finder from the access_token for the strategy, receives a block + # that will be invoked each time you want to find an user via an access_token in your + # system. + # + # @param blk Block that recieves the access_token as a parameter and will return a user or nil + # + def access_token_user_finder(&blk) + define_method(:_find_user_by_access_token, &blk) + end + + # + # Manages the creation and registration of the OAuth strategy specified + # on the keyword + # + # @param [Symbol] name of the oauth service + # @param [Walruz::Config] configuration specified on the declaration of the oauth service + # + def build(keyword, config) + strategy_class = self.create_oauth_strategy_class(keyword) + self.register_oauth_strategy_class(keyword, strategy_class) + self.set_oauth_service_info(strategy_class, config) + # adding the access_token_user_finder to the strategy + if self.access_token_user_finders.include?(keyword) + strategy_class.access_token_user_finder(&self.access_token_user_finders[keyword]) + end + end + + # + # Creates the OAuth Strategy class from the keyword specified on the declaration of the + # oauth service. This class will be namespaced inside Warden::OAuth2::Strategy + # + # @param [Symbol] name of the OAuth service + # @return [Class] The class representing the Warden strategy + # + # @example + # + # self.create_oauth_strategy_class(:twitter) #=> Warden::OAuth2::Strategy::Twitter + # # will create a class Warden::OAuth2::Strategy::Twitter that extends from + # # Warden::OAuth2::Strategy + # + def create_oauth_strategy_class(keyword) + puts "making strategy class - " + keyword.inspect + class_name = Warden::OAuth2::Utils.camelize(keyword.to_s) + if self.const_defined?(class_name) + self.const_get(class_name) + else + self.const_set(class_name, Class.new(self)) + end + end + + # + # Registers the generated OAuth Strategy in the Warden::Strategies collection, the label + # of the strategy will be the given oauth service name plus an '_oauth' postfix + # + # @param [Symbol] name of the OAuth service + # + # @example + # manager.oauth(:twitter) { |twitter| ... } # will register a strategy :twitter_oauth + # + def register_oauth_strategy_class(keyword, strategy_class) + keyword_name = "%s_oauth" % keyword.to_s + if Warden::Strategies[keyword_name.to_sym].nil? + Warden::Strategies.add(keyword_name.to_sym, strategy_class) + end + end + + # + # Defines a CONFIG constant in the generated class that will hold the configuration information + # (consumer_key, consumer_secret and options) of the oauth service. + # + # @param [Class] strategy class that will hold the configuration info + # @param [Warden::OAuth2::Config] configuration info of the oauth service + # + def set_oauth_service_info(strategy_class, config) + strategy_class.const_set("CONFIG", config) unless strategy_class.const_defined?("CONFIG") + end + + protected :create_oauth_strategy_class, + :register_oauth_strategy_class, + :set_oauth_service_info + + end + + end +end diff --git a/lib/warden_oauth2/utils.rb b/lib/warden_oauth2/utils.rb new file mode 100644 index 0000000..6208c7c --- /dev/null +++ b/lib/warden_oauth2/utils.rb @@ -0,0 +1,40 @@ +module Warden + module OAuth2 + + # + # Contains methods from Rails to avoid unnecessary dependencies + # + module Utils + + # + # Fetched from ActiveSupport::Inflector.camelize to avoid dependencies + # + def camelize(lower_case_and_underscored_word, first_letter_in_uppercase = true) + if first_letter_in_uppercase + lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase } + else + lower_case_and_underscored_word.first.downcase + camelize(lower_case_and_underscored_word)[1..-1] + end + end + + # + # Fetched from ActionController::Request to avoid dependencies + # + def host_with_port(request) + url = request.scheme + "://" + url << request.host + + if request.scheme == "https" && request.port != 443 || + request.scheme == "http" && request.port != 80 + url << ":#{request.port}" + end + + url + end + + module_function :camelize, :host_with_port + + end + + end +end diff --git a/warden_oauth.gemspec b/warden_oauth.gemspec index 334c330..f8e08a3 100644 --- a/warden_oauth.gemspec +++ b/warden_oauth.gemspec @@ -36,6 +36,13 @@ Gem::Specification.new do |s| "lib/warden_oauth/strategy.rb", "lib/warden_oauth/strategy_builder.rb", "lib/warden_oauth/utils.rb", + "lib/warden_oauth2/base.rb", + "lib/warden_oauth2/config.rb", + "lib/warden_oauth2/config_extension.rb", + "lib/warden_oauth2/errors.rb", + "lib/warden_oauth2/strategy.rb", + "lib/warden_oauth2/strategy_builder.rb", + "lib/warden_oauth2/utils.rb", "spec/application_runner.rb", "spec/application_scenario.rb", "spec/fixtures/authorize_request_token.txt", @@ -68,6 +75,7 @@ Gem::Specification.new do |s| if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then s.add_runtime_dependency(%q, [">= 0.8.1"]) s.add_runtime_dependency(%q, [">= 0"]) + s.add_runtime_dependency(%q, [">= 0"]) s.add_development_dependency(%q, [">= 0"]) s.add_development_dependency(%q, [">= 0"]) s.add_development_dependency(%q, [">= 0"])