twitter-login 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.markdown ADDED
@@ -0,0 +1,79 @@
1
+ Drop-in login functionality for your webapp
2
+ ===========================================
3
+
4
+ Drop this Rack middleware in your web application to enable user logins through Twitter.
5
+
6
+
7
+ How to use
8
+ ----------
9
+
10
+ First, [register a new Twitter application][register] (if you haven't already). Check
11
+ the <i>"Yes, use Twitter for login"</i> option. You can put anything as <i>"Callback
12
+ URL"</i> since the real callback URL is provided dynamically, anyway. Note down your
13
+ OAuth consumer key and secret.
14
+
15
+ Next, install this library:
16
+
17
+ [sudo] gem install twitter-login
18
+
19
+ You have to require 'twitter/login' in your app. If you're using Bundler:
20
+
21
+ ## Gemfile
22
+ clear_sources
23
+ source 'http://gemcutter.org'
24
+ gem 'twitter-login', :require_as => 'twitter/login'
25
+
26
+ Now configure your app to use the middleware. This might be different across web
27
+ frameworks.
28
+
29
+ ## Sinatra
30
+ enable :sessions
31
+ use Twitter::Login, :key => 'CONSUMER KEY', :secret => 'SECRET'
32
+ helpers Twitter::Login::Helpers
33
+
34
+ ## Rails
35
+ # environment.rb:
36
+ config.middleware.use Twitter::Login, :key => 'CONSUMER KEY', :secret => 'SECRET'
37
+
38
+ # application_controller.rb
39
+ include Twitter::Login::Helpers
40
+
41
+ Fill in the `:key`, `:secret` placeholders with real values. You're done.
42
+
43
+
44
+ What it does
45
+ ------------
46
+
47
+ This middleware handles GET requests to "/login" resource of your app. Make a login
48
+ link that points to "/login" and you're all set to receive logins from Twitter.
49
+
50
+ The user will first be redirected to Twitter to approve your application. After that he
51
+ or she is redirected back to "/login" with an OAuth verifier GET parameter. The
52
+ middleware then identifies the authenticating user, saves this info to session and
53
+ redirects to the root of your website.
54
+
55
+
56
+ Configuration
57
+ -------------
58
+
59
+ Available options for `Twitter::Login` middleware are:
60
+
61
+ * `:key` -- OAuth consumer key *(required)*
62
+ * `:secret` -- OAuth secret *(required)*
63
+ * `:login_path` -- where user goes to login (default: "/login")
64
+ * `:return_to` -- where user goes after login (default: "/")
65
+
66
+
67
+ Helpers
68
+ -------
69
+
70
+ The `Twitter::Login::Helpers` module (for Sinatra, Rails) adds these methods to your app:
71
+
72
+ * `twitter_user` (Hashie::Mash) -- Info about authenticated user. Check this object to
73
+ know whether there is a currently logged-in user. Access user data like `twitter_user.screen_name`
74
+ * `twitter_logout` -- Erases info about Twitter login from session, effectively logging-out the Twitter user
75
+ * `twitter_consumer` (Twitter::Base) -- An OAuth consumer client from ["twitter" gem][gem].
76
+ With it you can query anything on behalf of authenticated user, e.g. `twitter_consumer.friends_timeline`
77
+
78
+ [register]: http://twitter.com/apps/new
79
+ [gem]: http://rdoc.info/projects/jnunemaker/twitter
@@ -0,0 +1,140 @@
1
+ require 'twitter'
2
+ require 'rack/request'
3
+ require 'hashie/mash'
4
+
5
+ class Twitter::Login
6
+ attr_reader :options
7
+
8
+ DEFAULTS = {
9
+ :login_path => '/login', :return_to => '/',
10
+ :site => 'http://twitter.com', :authorize_path => '/oauth/authenticate'
11
+ }
12
+
13
+ def initialize(app, options)
14
+ @app = app
15
+ @options = DEFAULTS.merge options
16
+ end
17
+
18
+ def call(env)
19
+ request = Request.new(env)
20
+
21
+ if request.get? and request.path == options[:login_path]
22
+ # detect if Twitter redirected back here
23
+ if request[:oauth_verifier]
24
+ handle_twitter_authorization(request) do
25
+ @app.call(env)
26
+ end
27
+ elsif request[:denied]
28
+ # user refused to log in with Twitter, so give up
29
+ redirect_to_return_path(request)
30
+ else
31
+ # user clicked to login; send them to Twitter
32
+ redirect_to_twitter(request)
33
+ end
34
+ else
35
+ @app.call(env)
36
+ end
37
+ end
38
+
39
+ module Helpers
40
+ def twitter_consumer
41
+ token = OAuth::AccessToken.new(oauth_consumer, *session[:access_token])
42
+ Twitter::Base.new token
43
+ end
44
+
45
+ def oauth_consumer
46
+ OAuth::Consumer.new(*session[:oauth_consumer])
47
+ end
48
+
49
+ def twitter_user
50
+ if session[:twitter_user]
51
+ Hashie::Mash[session[:twitter_user]]
52
+ end
53
+ end
54
+
55
+ def twitter_logout
56
+ [:oauth_consumer, :access_token, :twitter_user].each do |key|
57
+ session[key] = nil # work around a Rails 2.3.5 bug
58
+ session.delete key
59
+ end
60
+ end
61
+ end
62
+
63
+ class Request < Rack::Request
64
+ # for storing :request_token, :access_token
65
+ def session
66
+ env['rack.session'] ||= {}
67
+ end
68
+
69
+ # SUCKS: must duplicate logic from the `url` method
70
+ def url_for(path)
71
+ url = scheme + '://' + host
72
+
73
+ if scheme == 'https' && port != 443 ||
74
+ scheme == 'http' && port != 80
75
+ url << ":#{port}"
76
+ end
77
+
78
+ url << path
79
+ end
80
+ end
81
+
82
+ protected
83
+
84
+ def redirect_to_twitter(request)
85
+ # create a request token and store its parameter in session
86
+ token = oauth_consumer.get_request_token(:oauth_callback => request.url)
87
+ request.session[:request_token] = [token.token, token.secret]
88
+ # redirect to Twitter authorization page
89
+ redirect token.authorize_url
90
+ end
91
+
92
+ def handle_twitter_authorization(request)
93
+ access_token = get_access_token(request)
94
+
95
+ # get and store authenticated user's info from Twitter
96
+ twitter = Twitter::Base.new access_token
97
+ request.session[:twitter_user] = twitter.verify_credentials.to_hash
98
+
99
+ # pass the request down to the main app
100
+ response = yield
101
+
102
+ # check if the app implemented anything at :login_path
103
+ if response[0].to_i == 404
104
+ # if not, redirect to :return_to path
105
+ redirect_to_return_path(request)
106
+ else
107
+ # use the response from the app without modification
108
+ response
109
+ end
110
+ end
111
+
112
+ private
113
+
114
+ def get_access_token(request)
115
+ # replace the request token in session with access token
116
+ request_token = ::OAuth::RequestToken.new(oauth_consumer, *request.session[:request_token])
117
+ access_token = request_token.get_access_token(:oauth_verifier => request[:oauth_verifier])
118
+
119
+ # store access token and OAuth consumer parameters in session
120
+ request.session.delete(:request_token)
121
+ request.session[:access_token] = [access_token.token, access_token.secret]
122
+ consumer = access_token.consumer
123
+ request.session[:oauth_consumer] = [consumer.key, consumer.secret, consumer.options]
124
+
125
+ return access_token
126
+ end
127
+
128
+ def redirect_to_return_path(request)
129
+ redirect request.url_for(options[:return_to])
130
+ end
131
+
132
+ def redirect(url)
133
+ ["302", {'Location' => url, 'Content-type' => 'text/plain'}, []]
134
+ end
135
+
136
+ def oauth_consumer
137
+ ::OAuth::Consumer.new options[:key], options[:secret],
138
+ :site => options[:site], :authorize_path => options[:authorize_path]
139
+ end
140
+ end
@@ -0,0 +1,114 @@
1
+ require 'twitter/login'
2
+ require 'rack/mock'
3
+ require 'rack/utils'
4
+ require 'rack/session/cookie'
5
+ require 'rack/builder'
6
+
7
+ require 'fakeweb'
8
+ FakeWeb.allow_net_connect = false
9
+
10
+ describe Twitter::Login do
11
+ before(:all) do
12
+ @app ||= begin
13
+ main_app = lambda { |env|
14
+ request = Rack::Request.new(env)
15
+ if request.path == '/'
16
+ ['200 OK', {'Content-type' => 'text/plain'}, ["Hello world"]]
17
+ else
18
+ ['404 Not Found', {'Content-type' => 'text/plain'}, ["Nothing here"]]
19
+ end
20
+ }
21
+
22
+ builder = Rack::Builder.new
23
+ builder.use Rack::Session::Cookie
24
+ builder.use described_class, :key => 'abc', :secret => '123'
25
+ builder.run main_app
26
+ builder.to_app
27
+ end
28
+ end
29
+
30
+ before(:each) do
31
+ @request = Rack::MockRequest.new(@app)
32
+ end
33
+
34
+ it "should login with Twitter" do
35
+ consumer = mock_oauth_consumer('OAuth Consumer')
36
+ token = mock('Request Token', :authorize_url => 'http://disney.com/oauth', :token => 'abc', :secret => '123')
37
+ consumer.should_receive(:get_request_token).with(:oauth_callback => 'http://example.org/login').and_return(token)
38
+ # request.session[:request_token] = token
39
+ # redirect token.authorize_url
40
+
41
+ get('/login', :lint => true)
42
+ response.status.should == 302
43
+ response['Location'].should == 'http://disney.com/oauth'
44
+ response.body.should be_empty
45
+ session[:request_token].should == ['abc', '123']
46
+ end
47
+
48
+ it "should authorize with Twitter" do
49
+ consumer = mock_oauth_consumer('OAuth Consumer', :key => 'con', :secret => 'sumer', :options => {:one=>'two'})
50
+ request_token = mock('Request Token')
51
+ OAuth::RequestToken.should_receive(:new).with(consumer, 'abc', '123').and_return(request_token)
52
+ access_token = mock('Access Token', :token => 'access1', :secret => '42', :consumer => consumer)
53
+ request_token.should_receive(:get_access_token).with(:oauth_verifier => 'abc').and_return(access_token)
54
+
55
+ twitter = mock('Twitter Base')
56
+ Twitter::Base.should_receive(:new).with(access_token).and_return(twitter)
57
+ user_credentials = Hashie::Mash.new :screen_name => 'faker',
58
+ :name => 'Fake Jr.', :profile_image_url => 'http://disney.com/mickey.png',
59
+ :followers_count => '13', :friends_count => '6', :statuses_count => '52'
60
+ twitter.should_receive(:verify_credentials).and_return(user_credentials)
61
+
62
+ session_data = {:request_token => ['abc', '123']}
63
+ get('/login?oauth_verifier=abc', build_session(session_data).update(:lint => true))
64
+ response.status.should == 302
65
+ response['Location'].should == 'http://example.org/'
66
+ session[:request_token].should be_nil
67
+ session[:access_token].should == ['access1', '42']
68
+ session[:oauth_consumer].should == ['con', 'sumer', {:one => 'two'}]
69
+
70
+ current_user = session[:twitter_user]
71
+ current_user['screen_name'].should == 'faker'
72
+ end
73
+
74
+ protected
75
+
76
+ [:get, :post, :put, :delete, :head].each do |method|
77
+ class_eval("def #{method}(*args) @response = @request.#{method}(*args) end")
78
+ end
79
+
80
+ def response
81
+ @response
82
+ end
83
+
84
+ def session
85
+ @session ||= begin
86
+ escaped = response['Set-Cookie'].match(/\=(.+?);/)[1]
87
+ cookie_load Rack::Utils.unescape(escaped)
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ def build_session(data)
94
+ encoded = cookie_dump(data)
95
+ { 'HTTP_COOKIE' => Rack::Utils.build_query('rack.session' => encoded) }
96
+ end
97
+
98
+ def cookie_load(encoded)
99
+ decoded = encoded.unpack('m*').first
100
+ Marshal.load(decoded)
101
+ end
102
+
103
+ def cookie_dump(obj)
104
+ [Marshal.dump(obj)].pack('m*')
105
+ end
106
+
107
+ def mock_oauth_consumer(*args)
108
+ consumer = mock(*args)
109
+ OAuth::Consumer.should_receive(:new).and_return(consumer)
110
+ # .with(instance_of(String), instance_of(String),
111
+ # :site => 'http://twitter.com', :authorize_path => '/oauth/authenticate')
112
+ consumer
113
+ end
114
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: twitter-login
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - "Mislav Marohni\xC4\x87"
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-01-03 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: twitter
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ~>
22
+ - !ruby/object:Gem::Version
23
+ version: 0.8.0
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ type: :development
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: 1.2.9
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: fakeweb
37
+ type: :development
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: 1.2.8
44
+ version:
45
+ description: Rack middleware for Sinatra, Rails, and other web frameworks that provides user login functionality through Twitter.
46
+ email: mislav.marohnic@gmail.com
47
+ executables: []
48
+
49
+ extensions: []
50
+
51
+ extra_rdoc_files: []
52
+
53
+ files:
54
+ - lib/twitter/login.rb
55
+ - spec/login_spec.rb
56
+ - README.markdown
57
+ has_rdoc: false
58
+ homepage: http://github.com/mislav/twitter-login
59
+ licenses: []
60
+
61
+ post_install_message:
62
+ rdoc_options: []
63
+
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: "0"
71
+ version:
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: "0"
77
+ version:
78
+ requirements: []
79
+
80
+ rubyforge_project:
81
+ rubygems_version: 1.3.5
82
+ signing_key:
83
+ specification_version: 3
84
+ summary: Rack middleware to provide login functionality through Twitter
85
+ test_files: []
86
+