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 +79 -0
- data/lib/twitter/login.rb +140 -0
- data/spec/login_spec.rb +114 -0
- metadata +86 -0
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
|
data/spec/login_spec.rb
ADDED
@@ -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
|
+
|