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 +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
|
+
|