xaviershay-twitter-auth 0.1.19
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +119 -0
- data/Rakefile +31 -0
- data/VERSION.yml +4 -0
- data/app/controllers/sessions_controller.rb +66 -0
- data/app/models/twitter_auth/basic_user.rb +63 -0
- data/app/models/twitter_auth/generic_user.rb +93 -0
- data/app/models/twitter_auth/oauth_user.rb +44 -0
- data/app/views/sessions/_login_form.html.erb +17 -0
- data/app/views/sessions/new.html.erb +5 -0
- data/config/routes.rb +6 -0
- data/generators/twitter_auth/USAGE +12 -0
- data/generators/twitter_auth/templates/migration.rb +48 -0
- data/generators/twitter_auth/templates/twitter_auth.yml +47 -0
- data/generators/twitter_auth/templates/user.rb +5 -0
- data/generators/twitter_auth/twitter_auth_generator.rb +34 -0
- data/lib/twitter_auth.rb +99 -0
- data/lib/twitter_auth/controller_extensions.rb +69 -0
- data/lib/twitter_auth/cryptify.rb +30 -0
- data/lib/twitter_auth/dispatcher/basic.rb +46 -0
- data/lib/twitter_auth/dispatcher/oauth.rb +26 -0
- data/lib/twitter_auth/dispatcher/shared.rb +40 -0
- data/rails/init.rb +8 -0
- data/spec/controllers/controller_extensions_spec.rb +162 -0
- data/spec/controllers/sessions_controller_spec.rb +250 -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 +142 -0
- data/spec/models/twitter_auth/oauth_user_spec.rb +101 -0
- data/spec/schema.rb +41 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +53 -0
- data/spec/twitter_auth/cryptify_spec.rb +51 -0
- data/spec/twitter_auth/dispatcher/basic_spec.rb +83 -0
- data/spec/twitter_auth/dispatcher/oauth_spec.rb +72 -0
- data/spec/twitter_auth/dispatcher/shared_spec.rb +26 -0
- data/spec/twitter_auth_spec.rb +160 -0
- metadata +127 -0
data/config/routes.rb
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
ActionController::Routing::Routes.draw do |map|
|
2
|
+
map.login '/login', :controller => 'sessions', :action => 'new'
|
3
|
+
map.logout '/logout', :controller => 'sessions', :action => 'destroy'
|
4
|
+
map.resource :session
|
5
|
+
map.oauth_callback '/oauth_callback', :controller => 'sessions', :action => 'oauth_callback'
|
6
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
TwitterAuth Generator
|
2
|
+
=====================
|
3
|
+
|
4
|
+
The TwitterAuth generator allows you to generate the components necessary to implement Twitter as a Single Sign-On provider for your site.
|
5
|
+
|
6
|
+
To run it, you simply need to call it:
|
7
|
+
|
8
|
+
script/generate twitter_auth
|
9
|
+
|
10
|
+
This will generate the migration necessary for the users table as well as generate a User model that extends the appropriate TwitterAuth model template and a config/twitter.yml that allows you to set your OAuth consumer key and secret.
|
11
|
+
|
12
|
+
By default, TwitterAuth uses OAuth as its authentication strategy. If you wish to use HTTP Basic you can pass in the --basic option.
|
@@ -0,0 +1,48 @@
|
|
1
|
+
class TwitterAuthMigration < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :users do |t|
|
4
|
+
t.string :login
|
5
|
+
<% if options[:oauth] -%>
|
6
|
+
t.string :access_token
|
7
|
+
t.string :access_secret
|
8
|
+
<% elsif options[:basic] -%>
|
9
|
+
t.binary :crypted_password
|
10
|
+
t.string :salt
|
11
|
+
<% end -%>
|
12
|
+
|
13
|
+
t.string :remember_token
|
14
|
+
t.datetime :remember_token_expires_at
|
15
|
+
|
16
|
+
# This information is automatically kept
|
17
|
+
# in-sync at each login of the user. You
|
18
|
+
# may remove any/all of these columns.
|
19
|
+
t.string :name
|
20
|
+
t.string :location
|
21
|
+
t.string :description
|
22
|
+
t.string :profile_image_url
|
23
|
+
t.string :url
|
24
|
+
t.boolean :protected
|
25
|
+
t.string :profile_background_color
|
26
|
+
t.string :profile_sidebar_fill_color
|
27
|
+
t.string :profile_link_color
|
28
|
+
t.string :profile_sidebar_border_color
|
29
|
+
t.string :profile_text_color
|
30
|
+
t.string :profile_background_image_url
|
31
|
+
t.boolean :profile_background_tiled
|
32
|
+
t.integer :friends_count
|
33
|
+
t.integer :statuses_count
|
34
|
+
t.integer :followers_count
|
35
|
+
t.integer :favourites_count
|
36
|
+
|
37
|
+
# Probably don't need both, but they're here.
|
38
|
+
t.integer :utc_offset
|
39
|
+
t.string :time_zone
|
40
|
+
|
41
|
+
t.timestamps
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.down
|
46
|
+
drop_table :users
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
<% if options[:oauth] -%>
|
2
|
+
development:
|
3
|
+
strategy: oauth
|
4
|
+
oauth_consumer_key: devkey
|
5
|
+
oauth_consumer_secret: devsecret
|
6
|
+
base_url: "https://twitter.com"
|
7
|
+
authorize_path: "/oauth/authenticate"
|
8
|
+
api_timeout: 10
|
9
|
+
remember_for: 14 # days
|
10
|
+
oauth_callback: "http://localhost:3000/oauth_callback"
|
11
|
+
test:
|
12
|
+
strategy: oauth
|
13
|
+
oauth_consumer_key: testkey
|
14
|
+
oauth_consumer_secret: testsecret
|
15
|
+
base_url: "https://twitter.com"
|
16
|
+
authorize_path: "/oauth/authenticate"
|
17
|
+
api_timeout: 10
|
18
|
+
remember_for: 14 # days
|
19
|
+
oauth_callback: "http://localhost:3000/oauth_callback"
|
20
|
+
production:
|
21
|
+
strategy: oauth
|
22
|
+
oauth_consumer_key: prodkey
|
23
|
+
oauth_consumer_secret: prodsecret
|
24
|
+
authorize_path: "/oauth/authenticate"
|
25
|
+
base_url: "https://twitter.com"
|
26
|
+
api_timeout: 10
|
27
|
+
remember_for: 14 # days
|
28
|
+
<% else -%>
|
29
|
+
development:
|
30
|
+
strategy: basic
|
31
|
+
api_timeout: 10
|
32
|
+
base_url: "https://twitter.com"
|
33
|
+
# randomly generated key for encrypting Twitter passwords
|
34
|
+
encryption_key: "<%= key = ActiveSupport::SecureRandom.hex(12) %>"
|
35
|
+
remember_for: 14 # days
|
36
|
+
test:
|
37
|
+
strategy: basic
|
38
|
+
api_timeout: 10
|
39
|
+
base_url: "https://twitter.com"
|
40
|
+
encryption_key: "<%= key %>"
|
41
|
+
remember_for: 14 # days
|
42
|
+
production:
|
43
|
+
strategy: basic
|
44
|
+
api_timeout: 10
|
45
|
+
encryption_key: "<%= key %>"
|
46
|
+
remember_for: 14 # days
|
47
|
+
<% end %>
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class TwitterAuthGenerator < Rails::Generator::Base
|
2
|
+
default_options :oauth => true, :basic => false
|
3
|
+
|
4
|
+
def manifest
|
5
|
+
record do |m|
|
6
|
+
m.class_collisions 'User'
|
7
|
+
|
8
|
+
m.migration_template 'migration.rb', 'db/migrate', :migration_file_name => 'twitter_auth_migration'
|
9
|
+
m.template 'user.rb', File.join('app','models','user.rb')
|
10
|
+
m.template 'twitter_auth.yml', File.join('config','twitter_auth.yml')
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
protected
|
15
|
+
|
16
|
+
def banner
|
17
|
+
"Usage: #{$0} twitter_auth"
|
18
|
+
end
|
19
|
+
|
20
|
+
def add_options!(opt)
|
21
|
+
opt.separator ''
|
22
|
+
opt.separator 'Options:'
|
23
|
+
|
24
|
+
opt.on('-O', '--oauth', 'Use the OAuth authentication strategy to connect to Twitter. (default)') { |v|
|
25
|
+
options[:oauth] = v
|
26
|
+
options[:basic] = !v
|
27
|
+
}
|
28
|
+
|
29
|
+
opt.on('-B', '--basic', 'Use the HTTP Basic authentication strategy to connect to Twitter.') { |v|
|
30
|
+
options[:basic] = v
|
31
|
+
options[:oauth] = !v
|
32
|
+
}
|
33
|
+
end
|
34
|
+
end
|
data/lib/twitter_auth.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
module TwitterAuth
|
2
|
+
class Error < StandardError; end
|
3
|
+
|
4
|
+
def self.config(environment=RAILS_ENV)
|
5
|
+
@config ||= {}
|
6
|
+
@config[environment] ||= YAML.load(File.open(RAILS_ROOT + '/config/twitter_auth.yml').read)[environment]
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.base_url
|
10
|
+
config['base_url'] || 'https://twitter.com'
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.path_prefix
|
14
|
+
URI.parse(base_url).path
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.api_timeout
|
18
|
+
config['api_timeout'] || 10
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.encryption_key
|
22
|
+
raise TwitterAuth::Cryptify::Error, 'You must specify an encryption_key in config/twitter_auth.yml' if config['encryption_key'].blank?
|
23
|
+
config['encryption_key']
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.oauth_callback?
|
27
|
+
config.key?('oauth_callback')
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.oauth_callback
|
31
|
+
config['oauth_callback']
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.remember_for
|
35
|
+
(config['remember_for'] || 14).to_i
|
36
|
+
end
|
37
|
+
|
38
|
+
# The authentication strategy employed by this
|
39
|
+
# application. Set in +config/twitter.yml+ as
|
40
|
+
# strategy; valid options are oauth or basic.
|
41
|
+
def self.strategy
|
42
|
+
strat = config['strategy']
|
43
|
+
raise ArgumentError, 'Invalid TwitterAuth Strategy: Valid strategies are oauth and basic.' unless %w(oauth basic).include?(strat)
|
44
|
+
strat.to_sym
|
45
|
+
rescue Errno::ENOENT
|
46
|
+
:oauth
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.oauth?
|
50
|
+
strategy == :oauth
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.basic?
|
54
|
+
strategy == :basic
|
55
|
+
end
|
56
|
+
|
57
|
+
# The OAuth consumer used by TwitterAuth for authentication. The consumer key and secret are set in your application's +config/twitter.yml+
|
58
|
+
def self.consumer
|
59
|
+
options = {:site => TwitterAuth.base_url}
|
60
|
+
[ :authorize_path,
|
61
|
+
:request_token_path,
|
62
|
+
:access_token_path,
|
63
|
+
:scheme ].each do |oauth_option|
|
64
|
+
options[oauth_option] = TwitterAuth.config[oauth_option.to_s] if TwitterAuth.config[oauth_option.to_s]
|
65
|
+
end
|
66
|
+
|
67
|
+
OAuth::Consumer.new(
|
68
|
+
config['oauth_consumer_key'],
|
69
|
+
config['oauth_consumer_secret'],
|
70
|
+
options
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.net
|
75
|
+
uri = URI.parse(TwitterAuth.base_url)
|
76
|
+
net = Net::HTTP.new(uri.host, uri.port)
|
77
|
+
net.use_ssl = uri.scheme == 'https'
|
78
|
+
net.read_timeout = TwitterAuth.api_timeout
|
79
|
+
net
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.authorize_path
|
83
|
+
config['authorize_path'] || '/oauth/authorize'
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
require 'twitter_auth/controller_extensions'
|
88
|
+
require 'twitter_auth/cryptify'
|
89
|
+
require 'twitter_auth/dispatcher/oauth'
|
90
|
+
require 'twitter_auth/dispatcher/basic'
|
91
|
+
require 'twitter_auth/dispatcher/shared'
|
92
|
+
|
93
|
+
module TwitterAuth
|
94
|
+
module Dispatcher
|
95
|
+
class Error < StandardError; end
|
96
|
+
class Unauthorized < Error; end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module TwitterAuth
|
2
|
+
# These methods borrow HEAVILY from Rick Olsen's
|
3
|
+
# Restful Authentication. All cleverness props
|
4
|
+
# go to him, not me.
|
5
|
+
module ControllerExtensions
|
6
|
+
def self.included(base)
|
7
|
+
base.send :helper_method, :current_user, :logged_in?, :authorized?
|
8
|
+
end
|
9
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
def authentication_failed(message, destination='/')
|
13
|
+
flash[:error] = message
|
14
|
+
redirect_to destination
|
15
|
+
end
|
16
|
+
|
17
|
+
def authentication_succeeded(message = 'You have logged in successfully.', destination = '/')
|
18
|
+
flash[:notice] = message
|
19
|
+
redirect_to destination
|
20
|
+
end
|
21
|
+
|
22
|
+
def current_user
|
23
|
+
@current_user ||= User.find_by_id(session[:user_id]) || User.from_remember_token(cookies[:remember_token])
|
24
|
+
end
|
25
|
+
|
26
|
+
def current_user=(new_user)
|
27
|
+
session[:user_id] = new_user.id
|
28
|
+
@current_user = new_user
|
29
|
+
end
|
30
|
+
|
31
|
+
def authorized?
|
32
|
+
!!current_user
|
33
|
+
end
|
34
|
+
|
35
|
+
def login_required
|
36
|
+
authorized? || access_denied
|
37
|
+
end
|
38
|
+
|
39
|
+
def access_denied
|
40
|
+
store_location
|
41
|
+
redirect_to login_path
|
42
|
+
end
|
43
|
+
|
44
|
+
def store_location
|
45
|
+
session[:return_to] = request.request_uri
|
46
|
+
end
|
47
|
+
|
48
|
+
def redirect_back_or_default(default)
|
49
|
+
redirect_to(session[:return_to] || default)
|
50
|
+
session[:return_to] = nil
|
51
|
+
end
|
52
|
+
|
53
|
+
def logged_in?
|
54
|
+
!!current_user
|
55
|
+
end
|
56
|
+
|
57
|
+
def logout_keeping_session!
|
58
|
+
@current_user = nil
|
59
|
+
session[:user_id] = nil
|
60
|
+
cookies.delete(:remember_token)
|
61
|
+
end
|
62
|
+
|
63
|
+
def build_user(user)
|
64
|
+
# Hook for applications to set additional user attributes on create
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
ActionController::Base.send(:include, TwitterAuth::ControllerExtensions)
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module TwitterAuth
|
2
|
+
module Cryptify
|
3
|
+
class Error < StandardError; end
|
4
|
+
|
5
|
+
def self.encrypt(data)
|
6
|
+
salt = generate_salt
|
7
|
+
{:encrypted_data => EzCrypto::Key.encrypt_with_password(TwitterAuth.encryption_key, salt, data), :salt => salt}
|
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
|
21
|
+
|
22
|
+
EzCrypto::Key.decrypt_with_password(TwitterAuth.encryption_key, salt, encrypted_data)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.generate_salt
|
26
|
+
ActiveSupport::SecureRandom.hex(4)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
module TwitterAuth
|
4
|
+
module Dispatcher
|
5
|
+
class Basic
|
6
|
+
include TwitterAuth::Dispatcher::Shared
|
7
|
+
|
8
|
+
attr_accessor :user
|
9
|
+
|
10
|
+
def initialize(user)
|
11
|
+
raise TwitterAuth::Error, 'Dispatcher must be initialized with a User.' unless user.is_a?(TwitterAuth::BasicUser)
|
12
|
+
self.user = user
|
13
|
+
end
|
14
|
+
|
15
|
+
def request(http_method, path, body=nil, *arguments)
|
16
|
+
path = TwitterAuth.path_prefix + path
|
17
|
+
path = append_extension_to(path)
|
18
|
+
|
19
|
+
response = TwitterAuth.net.start{ |http|
|
20
|
+
req = "Net::HTTP::#{http_method.to_s.capitalize}".constantize.new(path, *arguments)
|
21
|
+
req.basic_auth user.login, user.password
|
22
|
+
req.set_form_data(body) unless body.nil?
|
23
|
+
http.request(req)
|
24
|
+
}
|
25
|
+
|
26
|
+
handle_response(response)
|
27
|
+
end
|
28
|
+
|
29
|
+
def get(path, *arguments)
|
30
|
+
request(:get, path, *arguments)
|
31
|
+
end
|
32
|
+
|
33
|
+
def post(path, body='', *arguments)
|
34
|
+
request(:post, path, body, *arguments)
|
35
|
+
end
|
36
|
+
|
37
|
+
def put(path, body='', *arguments)
|
38
|
+
request(:put, path, body, *arguments)
|
39
|
+
end
|
40
|
+
|
41
|
+
def delete(path, *arguments)
|
42
|
+
request(:delete, path, *arguments)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'oauth'
|
2
|
+
|
3
|
+
module TwitterAuth
|
4
|
+
module Dispatcher
|
5
|
+
class Oauth < OAuth::AccessToken
|
6
|
+
include TwitterAuth::Dispatcher::Shared
|
7
|
+
|
8
|
+
attr_accessor :user
|
9
|
+
|
10
|
+
def initialize(user)
|
11
|
+
raise TwitterAuth::Error, 'Dispatcher must be initialized with a User.' unless user.is_a?(TwitterAuth::OauthUser)
|
12
|
+
self.user = user
|
13
|
+
super(TwitterAuth.consumer, user.access_token, user.access_secret)
|
14
|
+
end
|
15
|
+
|
16
|
+
def request(http_method, path, *arguments)
|
17
|
+
path = TwitterAuth.path_prefix + path
|
18
|
+
path = append_extension_to(path)
|
19
|
+
|
20
|
+
response = super
|
21
|
+
|
22
|
+
handle_response(response)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module TwitterAuth
|
2
|
+
module Dispatcher
|
3
|
+
module Shared
|
4
|
+
def post!(status)
|
5
|
+
self.post('/statuses/update.json', :status => status)
|
6
|
+
end
|
7
|
+
|
8
|
+
def append_extension_to(path)
|
9
|
+
path, query_string = *(path.split("?"))
|
10
|
+
path << '.json' unless path.match(/\.(:?xml|json)\z/i)
|
11
|
+
"#{path}#{"?#{query_string}" if query_string}"
|
12
|
+
end
|
13
|
+
|
14
|
+
def handle_response(response)
|
15
|
+
case response
|
16
|
+
when Net::HTTPOK
|
17
|
+
begin
|
18
|
+
JSON.parse(response.body)
|
19
|
+
rescue JSON::ParserError
|
20
|
+
response.body
|
21
|
+
end
|
22
|
+
when Net::HTTPUnauthorized
|
23
|
+
raise TwitterAuth::Dispatcher::Unauthorized, 'The credentials provided did not authorize the user.'
|
24
|
+
else
|
25
|
+
message = begin
|
26
|
+
JSON.parse(response.body)['error']
|
27
|
+
rescue JSON::ParserError
|
28
|
+
if match = response.body.match(/<error>(.*)<\/error>/)
|
29
|
+
match[1]
|
30
|
+
else
|
31
|
+
'An error occurred processing your Twitter request.'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
raise TwitterAuth::Dispatcher::Error, message
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|