From a9ef69754de57e9fa32304b29c269e959092be4f Mon Sep 17 00:00:00 2001 From: Michael Bleigh Date: Sun, 4 Apr 2010 13:56:26 -0400 Subject: [PATCH] OAuth, Twitter, LinkedIn, and OpenID strategies are all up and running. --- .gitignore | 1 + Rakefile | 2 + lib/omni_auth.rb | 39 +++++++++++- lib/omni_auth/builder.rb | 23 +++++++ lib/omni_auth/strategies/gowalla.rb | 26 ++++---- lib/omni_auth/strategies/http_basic.rb | 9 ++- lib/omni_auth/strategies/linked_in.rb | 31 +++++++++ lib/omni_auth/strategies/oauth.rb | 28 ++++----- lib/omni_auth/strategies/open_id.rb | 63 +++++++++++++++++++ lib/omni_auth/strategies/twitter.rb | 29 ++++++--- lib/omni_auth/strategy.rb | 13 ++++ .../strategies}/oauth_spec.rb | 23 +++---- spec/spec_helper.rb | 2 +- 13 files changed, 234 insertions(+), 55 deletions(-) create mode 100644 lib/omni_auth/builder.rb create mode 100644 lib/omni_auth/strategies/open_id.rb rename spec/{auth_elsewhere => omni_auth/strategies}/oauth_spec.rb (69%) diff --git a/.gitignore b/.gitignore index c3fea9d..f2afccf 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .DS_Store /live +.rvmrc ## TEXTMATE *.tmproj diff --git a/Rakefile b/Rakefile index 9cf252b..60831be 100644 --- a/Rakefile +++ b/Rakefile @@ -13,6 +13,8 @@ begin gem.add_dependency 'rack' gem.add_dependency 'rest-client' gem.add_dependency 'oauth' + gem.add_dependency 'nokogiri' + gem.add_dependency 'json' gem.add_development_dependency "rspec", ">= 1.2.9" # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings end diff --git a/lib/omni_auth.rb b/lib/omni_auth.rb index b185c2e..0a220bc 100644 --- a/lib/omni_auth.rb +++ b/lib/omni_auth.rb @@ -24,6 +24,10 @@ module OmniAuth attr_accessor :path_prefix end + def self.build(stack, &block) + OmniAuth::Builder.new(stack, &block) + end + def self.config Configuration.instance end @@ -31,9 +35,40 @@ module OmniAuth def self.configure yield config end + + module Utils + extend self + + def deep_merge(hash, other_hash) + target = hash.dup + + other_hash.keys.each do |key| + if other_hash[key].is_a? ::Hash and hash[key].is_a? ::Hash + target[key] = deep_merge(target[key],other_hash[key]) + next + end + + target[key] = other_hash[key] + end + + target + end + + def camelize(lower_case_and_underscored_word, first_letter_in_uppercase = true) + return "OAuth" if lower_case_and_underscored_word.to_s == 'oauth' + return "OpenID" if lower_case_and_underscored_word.to_s == 'open_id' + + if first_letter_in_uppercase + lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase } + else + lower_case_and_underscored_word.first + camelize(lower_case_and_underscored_word)[1..-1] + end + end + end end require 'omni_auth/strategy' -%w(oauth http_basic linked_in gowalla twitter).each do |s| +%w(oauth http_basic linked_in gowalla twitter open_id).each do |s| require "omni_auth/strategies/#{s}" -end \ No newline at end of file +end +require 'omni_auth/builder' \ No newline at end of file diff --git a/lib/omni_auth/builder.rb b/lib/omni_auth/builder.rb new file mode 100644 index 0000000..a1e1d4d --- /dev/null +++ b/lib/omni_auth/builder.rb @@ -0,0 +1,23 @@ +module OmniAuth + class Builder < Rack::Builder + def initialize(app, &block) + @app = app + super(&block) + end + + def provider(klass, *args, &block) + if klass.is_a?(Class) + middleware = klass + else + middleware = OmniAuth::Strategies.const_get("#{OmniAuth::Utils.camelize(klass.to_s)}") + end + + use middleware, *args, &block + end + + def call(env) + @ins << @app unless @ins.include?(@app) + to_app.call(env) + end + end +end \ No newline at end of file diff --git a/lib/omni_auth/strategies/gowalla.rb b/lib/omni_auth/strategies/gowalla.rb index 60bdaf7..9ec5a07 100644 --- a/lib/omni_auth/strategies/gowalla.rb +++ b/lib/omni_auth/strategies/gowalla.rb @@ -2,16 +2,16 @@ # so this won't actually work at all it # turns out. -module OmniAuth - module Strategies - class Gowalla < OmniAuth::Strategies::HttpBasic #:nodoc: - def initialize(app, api_key) - super(app, :gowalla, nil, {'X-Gowalla-API-Key' => api_key, 'Accept' => 'application/json'}) - end - - def endpoint - "http://#{request[:username]}:#{request[:password]}@api.gowalla.com/users/#{request[:username]}" - end - end - end -end \ No newline at end of file +# module OmniAuth +# module Strategies +# class Gowalla < OmniAuth::Strategies::HttpBasic #:nodoc: +# def initialize(app, api_key) +# super(app, :gowalla, nil, {'X-Gowalla-API-Key' => api_key, 'Accept' => 'application/json'}) +# end +# +# def endpoint +# "http://#{request[:username]}:#{request[:password]}@api.gowalla.com/users/#{request[:username]}" +# end +# end +# end +# end \ No newline at end of file diff --git a/lib/omni_auth/strategies/http_basic.rb b/lib/omni_auth/strategies/http_basic.rb index 59ccdc5..b9acac4 100644 --- a/lib/omni_auth/strategies/http_basic.rb +++ b/lib/omni_auth/strategies/http_basic.rb @@ -14,13 +14,18 @@ module OmniAuth attr_reader :endpoint, :request_headers def request_phase - resp = RestClient.get(endpoint, request_headers) + @response = RestClient.get(endpoint, request_headers) + request.POST['auth'] = auth_hash + @env['HTTP_METHOD'] = 'GET' + @env['PATH_INFO'] = "#{OmniAuth.config.path_prefix}/#{name}/callback" + + @app.call(@env) rescue RestClient::Request::Unauthorized fail!(:invalid_credentials) end def callback_phase - + [401, {}, 'Unauthorized'] end end end diff --git a/lib/omni_auth/strategies/linked_in.rb b/lib/omni_auth/strategies/linked_in.rb index 47e6690..dcf58cb 100644 --- a/lib/omni_auth/strategies/linked_in.rb +++ b/lib/omni_auth/strategies/linked_in.rb @@ -1,3 +1,5 @@ +require 'nokogiri' + module OmniAuth module Strategies class LinkedIn < OmniAuth::Strategies::OAuth @@ -9,6 +11,35 @@ module OmniAuth :authorize_path => '/uas/oauth/authorize', :scheme => :header) end + + def auth_hash + hash = user_hash(@access_token) + + OmniAuth::Utils.deep_merge(super, { + 'uid' => hash.delete('id'), + 'user_info' => hash + }) + end + + def user_hash(access_token) + person = Nokogiri::XML::Document.parse(@access_token.get('/v1/people/~:(id,first-name,last-name,headline,member-url-resources,picture-url,location)').body).xpath('person') + + hash = { + 'id' => person.xpath('id').text, + 'first_name' => person.xpath('first-name').text, + 'last_name' => person.xpath('last-name').text, + 'location' => person.xpath('location/name').text, + 'image' => person.xpath('picture-url').text, + 'description' => person.xpath('headline').text, + 'urls' => person.css('member-url-resources member-url').inject({}) do |hash,element| + hash[element.xpath('name').text] = element.xpath('url').text + hash + end + } + + hash[:name] = "#{hash['first_name']} #{hash['last_name']}" + hash + end end end end \ No newline at end of file diff --git a/lib/omni_auth/strategies/oauth.rb b/lib/omni_auth/strategies/oauth.rb index c84f504..588af4d 100644 --- a/lib/omni_auth/strategies/oauth.rb +++ b/lib/omni_auth/strategies/oauth.rb @@ -22,31 +22,27 @@ module OmniAuth request_token = ::OAuth::RequestToken.new(consumer, session[:oauth][name.to_sym].delete(:request_token), session[:oauth][name.to_sym].delete(:request_secret)) @access_token = request_token.get_access_token(:oauth_verifier => request.params['oauth_verifier']) - request[:auth] = { - :provider => name.to_sym, - :uid => unique_id, - :credentials => { - :token => @access_token.token, - :secret => @access_token.secret - }, :extra => { - :access_token => @access_token - } - } + request['auth'] = self.auth_hash @app.call(self.env) rescue ::OAuth::Unauthorized fail!(:invalid_credentials) end + def auth_hash + OmniAuth::Utils.deep_merge(super, { + 'credentials' => { + 'token' => @access_token.token, + 'secret' => @access_token.secret + }, 'extra' => { + 'access_token' => @access_token + } + }) + end + def unique_id nil end - - def full_host - uri = URI.parse(request.url) - uri.path = '' - uri.to_s - end end end end \ No newline at end of file diff --git a/lib/omni_auth/strategies/open_id.rb b/lib/omni_auth/strategies/open_id.rb new file mode 100644 index 0000000..c815b49 --- /dev/null +++ b/lib/omni_auth/strategies/open_id.rb @@ -0,0 +1,63 @@ +require 'rack/openid' + +module OmniAuth + module Strategies + class OpenID + include OmniAuth::Strategy + + def initialize(app, store = nil, options = {}) + super(app, :open_id) + @options = options + @options[:required] ||= %w(email fullname) + @options[:optional] ||= %w(nickname dob gender postcode country language timezone) + @store = store + end + + def dummy_app + lambda{|env| [401, {"WWW-Authenticate" => Rack::OpenID.build_header( + :identifier => request[:identifier], + :return_to => request.url + '/callback', + :required => @options[:required], + :optional => @options[:optional] + )}, []]} + end + + def request_phase + return fail!(:missing_information) unless request[:identifier] + openid = Rack::OpenID.new(dummy_app, @store) + openid.call(env) + end + + def callback_phase + openid = Rack::OpenID.new(lambda{|env| [200,{},[]]}, @store) + openid.call(env) + resp = env.delete('rack.openid.response') + + case resp.status + when :failure + fail!(:invalid_credentials) + when :success + request['auth'] = auth_hash(resp) + @app.call(env) + end + end + + def auth_hash(response) + { + 'uid' => response.display_identifier, + 'user_info' => user_info(response.display_identifier, ::OpenID::SReg::Response.from_success_response(response)) + } + end + + def user_info(identifier, sreg) + { + 'email' => sreg['email'], + 'name' => sreg['fullname'], + 'location' => sreg['postcode'], + 'nickname' => sreg['nickname'], + 'urls' => {'Profile' => identifier} + }.reject{|k,v| v.nil? || v == ''} + end + end + end +end \ No newline at end of file diff --git a/lib/omni_auth/strategies/twitter.rb b/lib/omni_auth/strategies/twitter.rb index 1207be4..280f735 100644 --- a/lib/omni_auth/strategies/twitter.rb +++ b/lib/omni_auth/strategies/twitter.rb @@ -15,22 +15,31 @@ module OmniAuth :site => 'https://api.twitter.com') end - def user_hash - @user_hash ||= JSON.parse(@access_token.get('/1/account/verify_credentials.json'), :symbolize_keys => true) - end - - def unique_id - @access_token.params[:user_id] + def auth_hash + OmniAuth::Utils.deep_merge(super, { + 'uid' => @access_token.params[:user_id], + 'user_info' => user_info, + 'extra' => {'user_hash' => user_hash} + }) end def user_info + user_hash = self.user_hash + { - :name => user_hash[:name], - :image => user_hash[:profile_image_url], - :screen_name => user_hash[:screen_name], - :description => user_hash[:description] + 'nickname' => user_hash['screen_name'], + 'name' => user_hash['name'], + 'location' => user_hash['location'], + 'image' => user_hash['profile_image_url'], + 'screen_name' => user_hash['screen_name'], + 'description' => user_hash['description'], + 'urls' => {'Website' => user_hash['url']} } end + + def user_hash + @user_hash ||= JSON.parse(@access_token.get('/1/account/verify_credentials.json').body) + end end end end \ No newline at end of file diff --git a/lib/omni_auth/strategy.rb b/lib/omni_auth/strategy.rb index 363b833..d10a40c 100644 --- a/lib/omni_auth/strategy.rb +++ b/lib/omni_auth/strategy.rb @@ -34,6 +34,19 @@ module OmniAuth raise NotImplementedError end + def auth_hash + { + 'provider' => name.to_s, + 'uid' => nil + } + end + + def full_host + uri = URI.parse(request.url) + uri.path = '' + uri.to_s + end + def session @env['rack.session'] end diff --git a/spec/auth_elsewhere/oauth_spec.rb b/spec/omni_auth/strategies/oauth_spec.rb similarity index 69% rename from spec/auth_elsewhere/oauth_spec.rb rename to spec/omni_auth/strategies/oauth_spec.rb index 1f04003..195815e 100644 --- a/spec/auth_elsewhere/oauth_spec.rb +++ b/spec/omni_auth/strategies/oauth_spec.rb @@ -1,5 +1,4 @@ -require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') -require 'auth_elsewhere' +require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper') class Rack::Session::Phony def initialize(app); @app = app end @@ -12,9 +11,11 @@ end def app Rack::Builder.new { use Rack::Session::Phony - use AuthElsewhere::OAuth, :twitter, 'abc', 'def', :site => 'https://api.twitter.com' - use AuthElsewhere::OAuth, :linked_in, 'abc', 'def', :site => 'https://api.linkedin.com' - run lambda { |env| [200, {'Content-Type' => 'text/plain'}, env.key?(:oauth).to_s] } + use OmniAuth::Builder do + provider :oauth, :twitter, 'abc', 'def', :site => 'https://api.twitter.com' + provider :oauth, :linked_in, 'abc', 'def', :site => 'https://api.linkedin.com' + end + run lambda { |env| [200, {'Content-Type' => 'text/plain'}, Rack::Request.new(env).params.key?('auth').to_s] } }.to_app end @@ -22,7 +23,7 @@ def session last_request.env['rack.session'] end -describe "AuthElsewhere::OAuth" do +describe "OmniAuth::Strategies::OAuth" do before do stub_request(:post, 'https://api.twitter.com/oauth/request_token'). to_return(:body => "oauth_token=yourtoken&oauth_token_secret=yoursecret&oauth_callback_confirmed=true") @@ -50,8 +51,8 @@ describe "AuthElsewhere::OAuth" do end it 'should exchange the request token for an access token' do - last_request.env[:oauth][:provider].should == :twitter - last_request.env[:oauth][:access_token].should be_kind_of(OAuth::AccessToken) + last_request['auth']['provider'].should == 'twitter' + last_request['auth']['extra']['access_token'].should be_kind_of(OAuth::AccessToken) end it 'should call through to the master app' do @@ -60,12 +61,12 @@ describe "AuthElsewhere::OAuth" do end end -describe 'AuthElsewhere::Twitter' do +describe 'OmniAuth::Strategies::Twitter' do it 'should subclass OAuth' do - AuthElsewhere::Twitter.should < AuthElsewhere::OAuth + OmniAuth::Strategies::Twitter.should < OmniAuth::Strategies::OAuth end it 'should initialize with just consumer key and secret' do - AuthElsewhere::Twitter.new({},'abc','def') + OmniAuth::Strategies::Twitter.new({},'abc','def') end end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 1ed3ede..cd3191d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,7 +2,7 @@ $LOAD_PATH.unshift(File.dirname(__FILE__)) $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) require 'rubygems' -require 'auth_elsewhere' +require 'omni_auth' require 'spec' require 'spec/autorun' require 'rack/test'