twitter-login 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+