mbleigh-twitter-auth 0.0.2 → 0.1.1
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 +66 -35
- data/VERSION.yml +2 -2
- data/generators/twitter_auth/USAGE +12 -0
- data/generators/twitter_auth/templates/migration.rb +29 -20
- data/generators/twitter_auth/templates/twitter_auth.yml +38 -0
- data/generators/twitter_auth/templates/user.rb +4 -6
- data/generators/twitter_auth/twitter_auth_generator.rb +31 -7
- data/lib/twitter_auth.rb +65 -1
- data/lib/twitter_auth/controller_extensions.rb +32 -168
- data/lib/twitter_auth/cryptify.rb +18 -7
- data/lib/twitter_auth/dispatcher/basic.rb +45 -0
- data/lib/twitter_auth/dispatcher/oauth.rb +23 -0
- data/spec/controllers/controller_extensions_spec.rb +146 -0
- data/spec/controllers/sessions_controller_spec.rb +206 -0
- data/spec/fixtures/config/twitter_auth.yml +17 -0
- data/spec/fixtures/factories.rb +18 -0
- data/spec/fixtures/fakeweb.rb +18 -0
- data/spec/fixtures/twitter.rb +5 -0
- data/spec/models/twitter_auth/basic_user_spec.rb +122 -0
- data/spec/models/twitter_auth/generic_user_spec.rb +48 -0
- data/spec/models/twitter_auth/oauth_user_spec.rb +85 -0
- data/spec/schema.rb +37 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +41 -0
- data/spec/twitter_auth/cryptify_spec.rb +51 -0
- data/spec/twitter_auth/dispatcher/basic_spec.rb +63 -0
- data/spec/twitter_auth/dispatcher/oauth_spec.rb +52 -0
- data/spec/twitter_auth_spec.rb +129 -0
- metadata +57 -8
@@ -1,19 +1,30 @@
|
|
1
1
|
module TwitterAuth
|
2
2
|
module Cryptify
|
3
3
|
class Error < StandardError; end
|
4
|
-
mattr_accessor :crypt_password
|
5
|
-
@@crypt_password = '--TwitterAuth-!##@--2ef'
|
6
4
|
|
7
|
-
def self.encrypt(data
|
8
|
-
|
5
|
+
def self.encrypt(data)
|
6
|
+
salt = generate_salt
|
7
|
+
{:encrypted_data => EzCrypto::Key.encrypt_with_password(TwitterAuth.encryption_key, salt, data), :salt => salt}
|
9
8
|
end
|
9
|
+
|
10
|
+
def self.decrypt(encrypted_data_or_hash, salt=nil)
|
11
|
+
case encrypted_data_or_hash
|
12
|
+
when String
|
13
|
+
encrypted_data = encrypted_data_or_hash
|
14
|
+
raise ArgumentError, 'Must provide a salt to decrypt.' unless salt
|
15
|
+
when Hash
|
16
|
+
encrypted_data = encrypted_data_or_hash[:encrypted_data]
|
17
|
+
salt = encrypted_data_or_hash[:salt]
|
18
|
+
else
|
19
|
+
raise ArgumentError, 'Must provide either an encrypted hash result or encrypted string and salt.'
|
20
|
+
end
|
10
21
|
|
11
|
-
|
12
|
-
EzCrypto::Key.decrypt_with_password(crypt_password, salt, encrypted_data)
|
22
|
+
EzCrypto::Key.decrypt_with_password(TwitterAuth.encryption_key, salt, encrypted_data)
|
13
23
|
end
|
14
24
|
|
15
25
|
def self.generate_salt
|
16
26
|
ActiveSupport::SecureRandom.hex(4)
|
17
27
|
end
|
18
28
|
end
|
19
|
-
end
|
29
|
+
end
|
30
|
+
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
module TwitterAuth
|
4
|
+
module Dispatcher
|
5
|
+
class Basic
|
6
|
+
attr_accessor :user
|
7
|
+
|
8
|
+
def initialize(user)
|
9
|
+
raise TwitterAuth::Error, 'Dispatcher must be initialized with a User.' unless user.is_a?(TwitterAuth::BasicUser)
|
10
|
+
self.user = user
|
11
|
+
end
|
12
|
+
|
13
|
+
def request(http_method, path, body=nil, *arguments)
|
14
|
+
path << '.json' unless path.match(/\.(:?xml|json)\z/i)
|
15
|
+
|
16
|
+
response = TwitterAuth.net.start{ |http|
|
17
|
+
req = "Net::HTTP::#{http_method.to_s.capitalize}".constantize.new(path, *arguments)
|
18
|
+
req.basic_auth user.login, user.password
|
19
|
+
req.set_form_data(body) unless body.nil?
|
20
|
+
http.request(req)
|
21
|
+
}
|
22
|
+
|
23
|
+
JSON.parse(response.body)
|
24
|
+
rescue JSON::ParserError
|
25
|
+
response.body
|
26
|
+
end
|
27
|
+
|
28
|
+
def get(path, *arguments)
|
29
|
+
request(:get, path, *arguments)
|
30
|
+
end
|
31
|
+
|
32
|
+
def post(path, body='', *arguments)
|
33
|
+
request(:post, path, body, *arguments)
|
34
|
+
end
|
35
|
+
|
36
|
+
def put(path, body='', *arguments)
|
37
|
+
request(:put, path, body, *arguments)
|
38
|
+
end
|
39
|
+
|
40
|
+
def delete(path, *arguments)
|
41
|
+
request(:delete, path, *arguments)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'oauth'
|
2
|
+
|
3
|
+
module TwitterAuth
|
4
|
+
module Dispatcher
|
5
|
+
class Oauth < OAuth::AccessToken
|
6
|
+
attr_accessor :user
|
7
|
+
|
8
|
+
def initialize(user)
|
9
|
+
raise TwitterAuth::Error, 'Dispatcher must be initialized with a User.' unless user.is_a?(TwitterAuth::OauthUser)
|
10
|
+
self.user = user
|
11
|
+
super(TwitterAuth.consumer, user.access_token, user.access_secret)
|
12
|
+
end
|
13
|
+
|
14
|
+
def request(http_method, path, *arguments)
|
15
|
+
path << '.json' unless path.match(/\.(:?xml|json)\z/i)
|
16
|
+
response = super
|
17
|
+
JSON.parse(response.body)
|
18
|
+
rescue JSON::ParserError
|
19
|
+
response.body
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
ActionController::Routing::Routes.draw do |map|
|
4
|
+
map.connect ':controller/:action/:id'
|
5
|
+
end
|
6
|
+
|
7
|
+
class TwitterAuthTestController < ApplicationController
|
8
|
+
before_filter :login_required, :only => [:login_required_action]
|
9
|
+
|
10
|
+
def login_required_action
|
11
|
+
render :text => "You are logged in!"
|
12
|
+
end
|
13
|
+
|
14
|
+
def fail_auth
|
15
|
+
authentication_failed('Auth FAIL.')
|
16
|
+
end
|
17
|
+
|
18
|
+
def pass_auth
|
19
|
+
if params[:message]
|
20
|
+
authentication_succeeded(params[:message])
|
21
|
+
else
|
22
|
+
authentication_succeeded
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def access_denied_action
|
27
|
+
access_denied
|
28
|
+
end
|
29
|
+
|
30
|
+
def redirect_back_action
|
31
|
+
redirect_back_or_default(params[:to] || '/')
|
32
|
+
end
|
33
|
+
|
34
|
+
def logout_keeping_session_action
|
35
|
+
logout_keeping_session!
|
36
|
+
redirect_back_or_default('/')
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe TwitterAuthTestController do
|
41
|
+
%w(authentication_failed authentication_succeeded current_user authorized? login_required access_denied store_location redirect_back_or_default logout_keeping_session!).each do |m|
|
42
|
+
it "should respond to the extension method '#{m}'" do
|
43
|
+
controller.should respond_to(m)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "#authentication_failed" do
|
48
|
+
it 'should set the flash[:error] to the message passed in' do
|
49
|
+
get :fail_auth
|
50
|
+
flash[:error].should == 'Auth FAIL.'
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'should redirect to the root' do
|
54
|
+
get :fail_auth
|
55
|
+
should redirect_to('/')
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "#authentication_succeeded" do
|
60
|
+
it 'should set the flash[:notice] to a default success message' do
|
61
|
+
get :pass_auth
|
62
|
+
flash[:notice].should == 'You have logged in successfully.'
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'should be able ot receive a custom message' do
|
66
|
+
get :pass_auth, :message => 'Eat at Joes.'
|
67
|
+
flash[:notice].should == 'Eat at Joes.'
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe '#current_user' do
|
72
|
+
it 'should find the user based on the session user_id' do
|
73
|
+
user = Factory.create(:twitter_oauth_user)
|
74
|
+
request.session[:user_id] = user.id
|
75
|
+
controller.send(:current_user).should == user
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'should return nil if there is no user matching that id' do
|
79
|
+
request.session[:user_id] = 2345
|
80
|
+
controller.send(:current_user).should be_nil
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'should memoize the result (and not do a double find)' do
|
84
|
+
user = Factory.create(:twitter_oauth_user)
|
85
|
+
User.should_receive(:find_by_id).once.and_return(user)
|
86
|
+
controller.send(:current_user).should == user
|
87
|
+
controller.send(:current_user).should == user
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe "#authorized?" do
|
92
|
+
it 'should be true if there is a current_user' do
|
93
|
+
user = Factory.create(:twitter_oauth_user)
|
94
|
+
controller.stub!(:current_user).and_return(user)
|
95
|
+
controller.send(:authorized?).should be_true
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'should be false if there is not current_user' do
|
99
|
+
controller.stub!(:current_user).and_return(nil)
|
100
|
+
controller.send(:authorized?).should be_false
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe '#access_denied' do
|
105
|
+
it 'should redirect to the login path' do
|
106
|
+
get :access_denied_action
|
107
|
+
should redirect_to(login_path)
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'should store the location first' do
|
111
|
+
controller.should_receive(:store_location).once
|
112
|
+
get :access_denied_action
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
describe '#redirect_back_or_default' do
|
117
|
+
it 'should redirect if there is a session[:return_to]' do
|
118
|
+
request.session[:return_to] = '/'
|
119
|
+
get :redirect_back_action, :to => '/notroot'
|
120
|
+
should redirect_to('/')
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'should redirect to the default provided otherwise' do
|
124
|
+
get :redirect_back_action, :to => '/someurl'
|
125
|
+
should redirect_to('/someurl')
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
describe 'logout_keeping_session!' do
|
130
|
+
before do
|
131
|
+
@user = Factory.create(:twitter_oauth_user)
|
132
|
+
request.session[:user_id] = @user.id
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'should unset session[:user_id]' do
|
136
|
+
get :logout_keeping_session_action
|
137
|
+
request.session[:user_id].should be_nil
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'should unset current_user' do
|
141
|
+
controller.send(:current_user).should == @user
|
142
|
+
get :logout_keeping_session_action
|
143
|
+
controller.send(:current_user).should be_nil
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,206 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
describe SessionsController do
|
4
|
+
integrate_views
|
5
|
+
|
6
|
+
describe 'routes' do
|
7
|
+
it 'should route /session/new to SessionsController#new' do
|
8
|
+
params_from(:get, '/session/new').should == {:controller => 'sessions', :action => 'new'}
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'should route /login to SessionsController#new' do
|
12
|
+
params_from(:get, '/login').should == {:controller => 'sessions', :action => 'new'}
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should route /logout to SessionsController#destroy' do
|
16
|
+
params_from(:get, '/logout').should == {:controller => 'sessions', :action => 'destroy'}
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should route DELETE /session to SessionsController#destroy' do
|
20
|
+
params_from(:delete, '/session').should == {:controller => 'sessions', :action => 'destroy'}
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should route /oauth_callback to SessionsController#oauth_callback' do
|
24
|
+
params_from(:get, '/oauth_callback').should == {:controller => 'sessions', :action => 'oauth_callback'}
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'should route POST /session to SessionsController#create' do
|
28
|
+
params_from(:post, '/session').should == {:controller => 'sessions', :action => 'create'}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe 'with OAuth strategy' do
|
33
|
+
before do
|
34
|
+
stub_oauth!
|
35
|
+
end
|
36
|
+
|
37
|
+
describe '#new' do
|
38
|
+
it 'should retrieve a request token' do
|
39
|
+
get :new
|
40
|
+
assigns[:request_token].token.should == 'faketoken'
|
41
|
+
assigns[:request_token].secret.should == 'faketokensecret'
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'should set session variables for the request token' do
|
45
|
+
get :new
|
46
|
+
session[:request_token].should == 'faketoken'
|
47
|
+
session[:request_token_secret].should == 'faketokensecret'
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'should redirect to the oauth authorization url' do
|
51
|
+
get :new
|
52
|
+
response.should redirect_to('https://twitter.com/oauth/authorize?oauth_token=faketoken')
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should redirect to the oauth_callback if one is specified' do
|
56
|
+
TwitterAuth.stub!(:oauth_callback).and_return('http://localhost:3000/development')
|
57
|
+
TwitterAuth.stub!(:oauth_callback?).and_return(true)
|
58
|
+
|
59
|
+
get :new
|
60
|
+
response.should redirect_to('https://twitter.com/oauth/authorize?oauth_token=faketoken&oauth_callback=' + CGI.escape(TwitterAuth.oauth_callback))
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe '#oauth_callback' do
|
65
|
+
describe 'with no session info' do
|
66
|
+
it 'should set the flash[:error]' do
|
67
|
+
get :oauth_callback, :oauth_token => 'faketoken'
|
68
|
+
flash[:error].should == 'No authentication information was found in the session. Please try again.'
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'should redirect to "/" by default' do
|
72
|
+
get :oauth_callback, :oauth_token => 'faketoken'
|
73
|
+
response.should redirect_to('/')
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'should call authentication_failed' do
|
77
|
+
controller.should_receive(:authentication_failed).any_number_of_times
|
78
|
+
get :oauth_callback, :oauth_token => 'faketoken'
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe 'with proper info' do
|
83
|
+
before do
|
84
|
+
@user = Factory.create(:twitter_oauth_user)
|
85
|
+
request.session[:request_token] = 'faketoken'
|
86
|
+
request.session[:request_token_secret] = 'faketokensecret'
|
87
|
+
get :oauth_callback, :oauth_token => 'faketoken'
|
88
|
+
end
|
89
|
+
|
90
|
+
describe 'building the access token' do
|
91
|
+
it 'should rebuild the request token' do
|
92
|
+
correct_token = OAuth::RequestToken.new(TwitterAuth.consumer,'faketoken','faketokensecret')
|
93
|
+
|
94
|
+
%w(token secret).each do |att|
|
95
|
+
assigns[:request_token].send(att).should == correct_token.send(att)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'should exchange the request token for an access token' do
|
100
|
+
assigns[:access_token].should be_a(OAuth::AccessToken)
|
101
|
+
assigns[:access_token].token.should == 'fakeaccesstoken'
|
102
|
+
assigns[:access_token].secret.should == 'fakeaccesstokensecret'
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'should wipe the request token after exchange' do
|
106
|
+
session[:request_token].should be_nil
|
107
|
+
session[:request_token_secret].should be_nil
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
describe 'identifying the user' do
|
112
|
+
it "should find the user" do
|
113
|
+
assigns[:user].should == @user
|
114
|
+
end
|
115
|
+
|
116
|
+
it "should assign the user id to the session" do
|
117
|
+
session[:user_id].should == @user.id
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe "when OAuth doesn't work" do
|
122
|
+
before do
|
123
|
+
request.session[:request_token] = 'faketoken'
|
124
|
+
request.session[:request_token_secret] = 'faketokensecret'
|
125
|
+
@request_token = OAuth::RequestToken.new(TwitterAuth.consumer, session[:request_token], session[:request_token_secret])
|
126
|
+
OAuth::RequestToken.stub!(:new).and_return(@request_token)
|
127
|
+
end
|
128
|
+
|
129
|
+
it 'should call authentication_failed when it gets a 401 from OAuth' do
|
130
|
+
@request_token.stub!(:get_access_token).and_raise(Net::HTTPServerException.new('401 "Unauthorized"', '401 "Unauthorized"'))
|
131
|
+
controller.should_receive(:authentication_failed).with('This authentication request is no longer valid. Please try again.')
|
132
|
+
# the should raise_error is hacky because of the expectation
|
133
|
+
# stubbing the proper behavior :-(
|
134
|
+
lambda{get :oauth_callback, :oauth_token => 'faketoken'}.should raise_error(ActionView::MissingTemplate)
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'should call authentication_failed when it gets a different HTTPServerException' do
|
138
|
+
@request_token.stub!(:get_access_token).and_raise(Net::HTTPServerException.new('404 "Not Found"', '404 "Not Found"'))
|
139
|
+
controller.should_receive(:authentication_failed).with('There was a problem trying to authenticate you. Please try again.')
|
140
|
+
lambda{get :oauth_callback, :oauth_token => 'faketoken'}.should raise_error(ActionView::MissingTemplate)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
describe 'with Basic strategy' do
|
148
|
+
before do
|
149
|
+
stub_basic!
|
150
|
+
end
|
151
|
+
|
152
|
+
describe '#new' do
|
153
|
+
it 'should render the new action' do
|
154
|
+
get :new
|
155
|
+
response.should render_template('sessions/new')
|
156
|
+
end
|
157
|
+
|
158
|
+
it 'should render the login form' do
|
159
|
+
get :new
|
160
|
+
response.should have_tag('form[action=/session][id=login_form][method=post]')
|
161
|
+
end
|
162
|
+
|
163
|
+
describe '#create' do
|
164
|
+
before do
|
165
|
+
@user = Factory.create(:twitter_basic_user)
|
166
|
+
end
|
167
|
+
|
168
|
+
it 'should call logout_keeping_session! to remove session info' do
|
169
|
+
controller.should_receive(:logout_keeping_session!)
|
170
|
+
post :create
|
171
|
+
end
|
172
|
+
|
173
|
+
it 'should try to authenticate the user' do
|
174
|
+
User.should_receive(:authenticate)
|
175
|
+
post :create
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'should call authentication_failed on authenticate failure' do
|
179
|
+
User.should_receive(:authenticate).and_return(nil)
|
180
|
+
post :create, :login => 'wrong', :password => 'false'
|
181
|
+
response.should redirect_to('/login')
|
182
|
+
end
|
183
|
+
|
184
|
+
it 'should call authentication_succeeded on authentication success' do
|
185
|
+
User.should_receive(:authenticate).and_return(@user)
|
186
|
+
post :create, :login => 'twitterman', :password => 'cool'
|
187
|
+
response.should redirect_to('/')
|
188
|
+
flash[:notice].should_not be_blank
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|
194
|
+
|
195
|
+
describe '#destroy' do
|
196
|
+
it 'should call logout_keeping_session!' do
|
197
|
+
controller.should_receive(:logout_keeping_session!).once
|
198
|
+
get :destroy
|
199
|
+
end
|
200
|
+
|
201
|
+
it 'should redirect to the root' do
|
202
|
+
get :destroy
|
203
|
+
response.should redirect_to('/')
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|