pelle-open_id_authentication 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1,38 @@
1
+ * Gemified it [Pelle]
2
+ * Added support for OpenID OAuth extension. [Pelle]
3
+
4
+ * Fake HTTP method from OpenID server since they only support a GET. Eliminates the need to set an extra route to match the server's reply. [Josh Peek]
5
+
6
+ * OpenID 2.0 recommends that forms should use the field name "openid_identifier" rather than "openid_url" [Josh Peek]
7
+
8
+ * Return open_id_response.display_identifier to the application instead of .endpoints.claimed_id. [nbibler]
9
+
10
+ * Add Timeout protection [Rick]
11
+
12
+ * An invalid identity url passed through authenticate_with_open_id will no longer raise an InvalidOpenId exception. Instead it will return Result[:missing] to the completion block.
13
+
14
+ * Allow a return_to option to be used instead of the requested url [Josh Peek]
15
+
16
+ * Updated plugin to use Ruby OpenID 2.x.x [Josh Peek]
17
+
18
+ * Tied plugin to ruby-openid 1.1.4 gem until we can make it compatible with 2.x [DHH]
19
+
20
+ * Use URI instead of regexps to normalize the URL and gain free, better matching #8136 [dkubb]
21
+
22
+ * Allow -'s in #normalize_url [Rick]
23
+
24
+ * remove instance of mattr_accessor, it was breaking tests since they don't load ActiveSupport. Fix Timeout test [Rick]
25
+
26
+ * Throw a InvalidOpenId exception instead of just a RuntimeError when the URL can't be normalized [DHH]
27
+
28
+ * Just use the path for the return URL, so extra query parameters don't interfere [DHH]
29
+
30
+ * Added a new default database-backed store after experiencing trouble with the filestore on NFS. The file store is still available as an option [DHH]
31
+
32
+ * Added normalize_url and applied it to all operations going through the plugin [DHH]
33
+
34
+ * Removed open_id? as the idea of using the same input box for both OpenID and username has died -- use using_open_id? instead (which checks for the presence of params[:openid_url] by default) [DHH]
35
+
36
+ * Added OpenIdAuthentication::Result to make it easier to deal with default situations where you don't care to do something particular for each error state [DHH]
37
+
38
+ * Stop relying on root_url being defined, we can just grab the current url instead [DHH]
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2004-2008 David Heinemeier Hansson
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
data/README ADDED
@@ -0,0 +1,231 @@
1
+ OpenIdAuthentication
2
+ ====================
3
+
4
+ Provides a thin wrapper around the excellent ruby-openid gem from JanRan. Be sure to install that first:
5
+
6
+ gem install ruby-openid
7
+
8
+ To understand what OpenID is about and how it works, it helps to read the documentation for lib/openid/consumer.rb
9
+ from that gem.
10
+
11
+ The specification used is http://openid.net/specs/openid-authentication-2_0.html.
12
+
13
+
14
+ Prerequisites
15
+ =============
16
+
17
+ OpenID authentication uses the session, so be sure that you haven't turned that off. It also relies on a number of
18
+ database tables to store the authentication keys. So you'll have to run the migration to create these before you get started:
19
+
20
+ rake open_id_authentication:db:create
21
+
22
+ Or, use the included generators to install or upgrade:
23
+
24
+ ./script/generate open_id_authentication_tables MigrationName
25
+ ./script/generate upgrade_open_id_authentication_tables MigrationName
26
+
27
+ Alternatively, you can use the file-based store, which just relies on on tmp/openids being present in RAILS_ROOT. But be aware that this store only works if you have a single application server. And it's not safe to use across NFS. It's recommended that you use the database store if at all possible. To use the file-based store, you'll also have to add this line to your config/environment.rb:
28
+
29
+ OpenIdAuthentication.store = :file
30
+
31
+ This particular plugin also relies on the fact that the authentication action allows for both POST and GET operations.
32
+ If you're using RESTful authentication, you'll need to explicitly allow for this in your routes.rb.
33
+
34
+ The plugin also expects to find a root_url method that points to the home page of your site. You can accomplish this by using a root route in config/routes.rb:
35
+
36
+ map.root :controller => 'articles'
37
+
38
+ This plugin relies on Rails Edge revision 6317 or newer.
39
+
40
+
41
+ Example
42
+ =======
43
+
44
+ This example is just to meant to demonstrate how you could use OpenID authentication. You might well want to add
45
+ salted hash logins instead of plain text passwords and other requirements on top of this. Treat it as a starting point,
46
+ not a destination.
47
+
48
+ Note that the User model referenced in the simple example below has an 'identity_url' attribute. You will want to add the same or similar field to whatever
49
+ model you are using for authentication.
50
+
51
+ Also of note is the following code block used in the example below:
52
+
53
+ authenticate_with_open_id do |result, identity_url|
54
+ ...
55
+ end
56
+
57
+ In the above code block, 'identity_url' will need to match user.identity_url exactly. 'identity_url' will be a string in the form of 'http://example.com' -
58
+ If you are storing just 'example.com' with your user, the lookup will fail.
59
+
60
+ There is a handy method in this plugin called 'normalize_url' that will help with validating OpenID URLs.
61
+
62
+ OpenIdAuthentication.normalize_url(user.identity_url)
63
+
64
+ The above will return a standardized version of the OpenID URL - the above called with 'example.com' will return 'http://example.com/'
65
+ It will also raise an InvalidOpenId exception if the URL is determined to not be valid.
66
+ Use the above code in your User model and validate OpenID URLs before saving them.
67
+
68
+ config/routes.rb
69
+
70
+ map.root :controller => 'articles'
71
+ map.resource :session
72
+
73
+
74
+ app/views/sessions/new.erb
75
+
76
+ <% form_tag(session_url) do %>
77
+ <p>
78
+ <label for="name">Username:</label>
79
+ <%= text_field_tag "name" %>
80
+ </p>
81
+
82
+ <p>
83
+ <label for="password">Password:</label>
84
+ <%= password_field_tag %>
85
+ </p>
86
+
87
+ <p>
88
+ ...or use:
89
+ </p>
90
+
91
+ <p>
92
+ <label for="openid_identifier">OpenID:</label>
93
+ <%= text_field_tag "openid_identifier" %>
94
+ </p>
95
+
96
+ <p>
97
+ <%= submit_tag 'Sign in', :disable_with => "Signing in&hellip;" %>
98
+ </p>
99
+ <% end %>
100
+
101
+ app/controllers/sessions_controller.rb
102
+ class SessionsController < ApplicationController
103
+ def create
104
+ if using_open_id?
105
+ open_id_authentication
106
+ else
107
+ password_authentication(params[:name], params[:password])
108
+ end
109
+ end
110
+
111
+
112
+ protected
113
+ def password_authentication(name, password)
114
+ if @current_user = @account.users.authenticate(params[:name], params[:password])
115
+ successful_login
116
+ else
117
+ failed_login "Sorry, that username/password doesn't work"
118
+ end
119
+ end
120
+
121
+ def open_id_authentication
122
+ authenticate_with_open_id do |result, identity_url|
123
+ if result.successful?
124
+ if @current_user = @account.users.find_by_identity_url(identity_url)
125
+ successful_login
126
+ else
127
+ failed_login "Sorry, no user by that identity URL exists (#{identity_url})"
128
+ end
129
+ else
130
+ failed_login result.message
131
+ end
132
+ end
133
+ end
134
+
135
+
136
+ private
137
+ def successful_login
138
+ session[:user_id] = @current_user.id
139
+ redirect_to(root_url)
140
+ end
141
+
142
+ def failed_login(message)
143
+ flash[:error] = message
144
+ redirect_to(new_session_url)
145
+ end
146
+ end
147
+
148
+
149
+
150
+ If you're fine with the result messages above and don't need individual logic on a per-failure basis,
151
+ you can collapse the case into a mere boolean:
152
+
153
+ def open_id_authentication
154
+ authenticate_with_open_id do |result, identity_url|
155
+ if result.successful? && @current_user = @account.users.find_by_identity_url(identity_url)
156
+ successful_login
157
+ else
158
+ failed_login(result.message || "Sorry, no user by that identity URL exists (#{identity_url})")
159
+ end
160
+ end
161
+ end
162
+
163
+
164
+ Simple Registration OpenID Extension
165
+ ====================================
166
+
167
+ Some OpenID Providers support this lightweight profile exchange protocol. See more: http://www.openidenabled.com/openid/simple-registration-extension
168
+
169
+ You can support it in your app by changing #open_id_authentication
170
+
171
+ def open_id_authentication(identity_url)
172
+ # Pass optional :required and :optional keys to specify what sreg fields you want.
173
+ # Be sure to yield registration, a third argument in the #authenticate_with_open_id block.
174
+ authenticate_with_open_id(identity_url,
175
+ :required => [ :nickname, :email ],
176
+ :optional => :fullname) do |result, identity_url, registration|
177
+ case result.status
178
+ when :missing
179
+ failed_login "Sorry, the OpenID server couldn't be found"
180
+ when :invalid
181
+ failed_login "Sorry, but this does not appear to be a valid OpenID"
182
+ when :canceled
183
+ failed_login "OpenID verification was canceled"
184
+ when :failed
185
+ failed_login "Sorry, the OpenID verification failed"
186
+ when :successful
187
+ if @current_user = @account.users.find_by_identity_url(identity_url)
188
+ assign_registration_attributes!(registration)
189
+
190
+ if current_user.save
191
+ successful_login
192
+ else
193
+ failed_login "Your OpenID profile registration failed: " +
194
+ @current_user.errors.full_messages.to_sentence
195
+ end
196
+ else
197
+ failed_login "Sorry, no user by that identity URL exists"
198
+ end
199
+ end
200
+ end
201
+ end
202
+
203
+ # registration is a hash containing the valid sreg keys given above
204
+ # use this to map them to fields of your user model
205
+ def assign_registration_attributes!(registration)
206
+ model_to_registration_mapping.each do |model_attribute, registration_attribute|
207
+ unless registration[registration_attribute].blank?
208
+ @current_user.send("#{model_attribute}=", registration[registration_attribute])
209
+ end
210
+ end
211
+ end
212
+
213
+ def model_to_registration_mapping
214
+ { :login => 'nickname', :email => 'email', :display_name => 'fullname' }
215
+ end
216
+
217
+ Attribute Exchange OpenID Extension
218
+ ===================================
219
+
220
+ Some OpenID providers also support the OpenID AX (attribute exchange) protocol for exchanging identity information between endpoints. See more: http://openid.net/specs/openid-attribute-exchange-1_0.html
221
+
222
+ Accessing AX data is very similar to the Simple Registration process, described above -- just add the URI identifier for the AX field to your :optional or :required parameters. For example:
223
+
224
+ authenticate_with_open_id(identity_url,
225
+ :required => [ :email, 'http://schema.openid.net/birthDate' ]) do |result, identity_url, registration|
226
+
227
+ This would provide the sreg data for :email, and the AX data for 'http://schema.openid.net/birthDate'
228
+
229
+
230
+
231
+ Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,38 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test the open_id_authentication plugin.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.pattern = 'test/**/*_test.rb'
12
+ t.verbose = true
13
+ end
14
+
15
+ desc 'Generate documentation for the open_id_authentication plugin.'
16
+ Rake::RDocTask.new(:rdoc) do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = 'OpenIdAuthentication'
19
+ rdoc.options << '--line-numbers' << '--inline-source'
20
+ rdoc.rdoc_files.include('README')
21
+ rdoc.rdoc_files.include('lib/**/*.rb')
22
+ end
23
+
24
+ begin
25
+ require 'jeweler'
26
+ Jeweler::Tasks.new do |gem|
27
+ gem.name = 'open_id_authentication'
28
+ gem.author = "David Heinemeier Hansson"
29
+ gem.email = 'david@loudthinking.com'
30
+ gem.homepage = 'http://www.rubyonrails.org'
31
+ gem.summary = 'Provides a thin wrapper around the excellent ruby-openid gem from JanRan.'
32
+ gem.extra_rdoc_files = ["MIT-LICENSE", "README", "CHANGELOG"]
33
+ gem.add_dependency('pelle-ruby-openid', '>= 2.1.8')
34
+
35
+ end
36
+ rescue LoadError
37
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
38
+ end
@@ -0,0 +1,11 @@
1
+ class OpenIdAuthenticationTablesGenerator < Rails::Generator::NamedBase
2
+ def initialize(runtime_args, runtime_options = {})
3
+ super
4
+ end
5
+
6
+ def manifest
7
+ record do |m|
8
+ m.migration_template 'migration.rb', 'db/migrate'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,20 @@
1
+ class <%= class_name %> < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :open_id_authentication_associations, :force => true do |t|
4
+ t.integer :issued, :lifetime
5
+ t.string :handle, :assoc_type
6
+ t.binary :server_url, :secret
7
+ end
8
+
9
+ create_table :open_id_authentication_nonces, :force => true do |t|
10
+ t.integer :timestamp, :null => false
11
+ t.string :server_url, :null => true
12
+ t.string :salt, :null => false
13
+ end
14
+ end
15
+
16
+ def self.down
17
+ drop_table :open_id_authentication_associations
18
+ drop_table :open_id_authentication_nonces
19
+ end
20
+ end
@@ -0,0 +1,26 @@
1
+ class <%= class_name %> < ActiveRecord::Migration
2
+ def self.up
3
+ drop_table :open_id_authentication_settings
4
+ drop_table :open_id_authentication_nonces
5
+
6
+ create_table :open_id_authentication_nonces, :force => true do |t|
7
+ t.integer :timestamp, :null => false
8
+ t.string :server_url, :null => true
9
+ t.string :salt, :null => false
10
+ end
11
+ end
12
+
13
+ def self.down
14
+ drop_table :open_id_authentication_nonces
15
+
16
+ create_table :open_id_authentication_nonces, :force => true do |t|
17
+ t.integer :created
18
+ t.string :nonce
19
+ end
20
+
21
+ create_table :open_id_authentication_settings, :force => true do |t|
22
+ t.string :setting
23
+ t.binary :value
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,11 @@
1
+ class UpgradeOpenIdAuthenticationTablesGenerator < Rails::Generator::NamedBase
2
+ def initialize(runtime_args, runtime_options = {})
3
+ super
4
+ end
5
+
6
+ def manifest
7
+ record do |m|
8
+ m.migration_template 'migration.rb', 'db/migrate'
9
+ end
10
+ end
11
+ end
data/init.rb ADDED
@@ -0,0 +1,18 @@
1
+ if config.respond_to?(:gems)
2
+ config.gem 'pelle-ruby-openid', :lib => 'openid', :version => '>=2.1.8', :source => 'http://gems.github.com'
3
+ else
4
+ begin
5
+ require 'openid'
6
+ rescue LoadError
7
+ begin
8
+ gem 'pelle-ruby-openid', '>=2.1.8', :lib => 'openid'
9
+ rescue Gem::LoadError
10
+ puts "Install the ruby-openid gem to enable OpenID support"
11
+ end
12
+ end
13
+ end
14
+
15
+ config.to_prepare do
16
+ OpenID::Util.logger = Rails.logger
17
+ ActionController::Base.send :include, OpenIdAuthentication
18
+ end
@@ -0,0 +1,9 @@
1
+ module OpenIdAuthentication
2
+ class Association < ActiveRecord::Base
3
+ set_table_name :open_id_authentication_associations
4
+
5
+ def from_record
6
+ OpenID::Association.new(handle, secret, issued, lifetime, assoc_type)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,55 @@
1
+ require 'openid/store/interface'
2
+
3
+ module OpenIdAuthentication
4
+ class DbStore < OpenID::Store::Interface
5
+ def self.cleanup_nonces
6
+ now = Time.now.to_i
7
+ Nonce.delete_all(["timestamp > ? OR timestamp < ?", now + OpenID::Nonce.skew, now - OpenID::Nonce.skew])
8
+ end
9
+
10
+ def self.cleanup_associations
11
+ now = Time.now.to_i
12
+ Association.delete_all(['issued + lifetime > ?',now])
13
+ end
14
+
15
+ def store_association(server_url, assoc)
16
+ remove_association(server_url, assoc.handle)
17
+ Association.create(:server_url => server_url,
18
+ :handle => assoc.handle,
19
+ :secret => assoc.secret,
20
+ :issued => assoc.issued,
21
+ :lifetime => assoc.lifetime,
22
+ :assoc_type => assoc.assoc_type)
23
+ end
24
+
25
+ def get_association(server_url, handle = nil)
26
+ assocs = if handle.blank?
27
+ Association.find_all_by_server_url(server_url)
28
+ else
29
+ Association.find_all_by_server_url_and_handle(server_url, handle)
30
+ end
31
+
32
+ assocs.reverse.each do |assoc|
33
+ a = assoc.from_record
34
+ if a.expires_in == 0
35
+ assoc.destroy
36
+ else
37
+ return a
38
+ end
39
+ end if assocs.any?
40
+
41
+ return nil
42
+ end
43
+
44
+ def remove_association(server_url, handle)
45
+ Association.delete_all(['server_url = ? AND handle = ?', server_url, handle]) > 0
46
+ end
47
+
48
+ def use_nonce(server_url, timestamp, salt)
49
+ return false if Nonce.find_by_server_url_and_timestamp_and_salt(server_url, timestamp, salt)
50
+ return false if (timestamp - Time.now.to_i).abs > OpenID::Nonce.skew
51
+ Nonce.create(:server_url => server_url, :timestamp => timestamp, :salt => salt)
52
+ return true
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,5 @@
1
+ module OpenIdAuthentication
2
+ class Nonce < ActiveRecord::Base
3
+ set_table_name :open_id_authentication_nonces
4
+ end
5
+ end
@@ -0,0 +1,23 @@
1
+ module OpenIdAuthentication
2
+ module Request
3
+ def self.included(base)
4
+ base.alias_method_chain :request_method, :openid
5
+ end
6
+
7
+ def request_method_with_openid
8
+ if !parameters[:_method].blank? && parameters[:open_id_complete] == '1'
9
+ parameters[:_method].to_sym
10
+ else
11
+ request_method_without_openid
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ # In Rails 2.3, the request object has been renamed
18
+ # from AbstractRequest to Request
19
+ if defined? ActionController::Request
20
+ ActionController::Request.send :include, OpenIdAuthentication::Request
21
+ else
22
+ ActionController::AbstractRequest.send :include, OpenIdAuthentication::Request
23
+ end
@@ -0,0 +1,20 @@
1
+ # http://trac.openidenabled.com/trac/ticket/156
2
+ module OpenID
3
+ @@timeout_threshold = 20
4
+
5
+ def self.timeout_threshold
6
+ @@timeout_threshold
7
+ end
8
+
9
+ def self.timeout_threshold=(value)
10
+ @@timeout_threshold = value
11
+ end
12
+
13
+ class StandardFetcher
14
+ def make_http(uri)
15
+ http = @proxy.new(uri.host, uri.port)
16
+ http.read_timeout = http.open_timeout = OpenID.timeout_threshold
17
+ http
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,257 @@
1
+ require 'uri'
2
+ require 'openid/extensions/sreg'
3
+ require 'openid/extensions/ax'
4
+ require 'openid/extensions/oauth'
5
+ require 'openid/store/filesystem'
6
+
7
+ require File.dirname(__FILE__) + '/open_id_authentication/association'
8
+ require File.dirname(__FILE__) + '/open_id_authentication/nonce'
9
+ require File.dirname(__FILE__) + '/open_id_authentication/db_store'
10
+ require File.dirname(__FILE__) + '/open_id_authentication/request'
11
+ require File.dirname(__FILE__) + '/open_id_authentication/timeout_fixes' if OpenID::VERSION == "2.0.4"
12
+
13
+ module OpenIdAuthentication
14
+ OPEN_ID_AUTHENTICATION_DIR = RAILS_ROOT + "/tmp/openids"
15
+
16
+ def self.store
17
+ @@store
18
+ end
19
+
20
+ def self.store=(*store_option)
21
+ store, *parameters = *([ store_option ].flatten)
22
+
23
+ @@store = case store
24
+ when :db
25
+ OpenIdAuthentication::DbStore.new
26
+ when :file
27
+ OpenID::Store::Filesystem.new(OPEN_ID_AUTHENTICATION_DIR)
28
+ else
29
+ store
30
+ end
31
+ end
32
+
33
+ self.store = :db
34
+
35
+ class InvalidOpenId < StandardError
36
+ end
37
+
38
+ class Result
39
+ ERROR_MESSAGES = {
40
+ :missing => "Sorry, the OpenID server couldn't be found",
41
+ :invalid => "Sorry, but this does not appear to be a valid OpenID",
42
+ :canceled => "OpenID verification was canceled",
43
+ :failed => "OpenID verification failed",
44
+ :setup_needed => "OpenID verification needs setup"
45
+ }
46
+
47
+ def self.[](code)
48
+ new(code)
49
+ end
50
+
51
+ def initialize(code)
52
+ @code = code
53
+ end
54
+
55
+ def status
56
+ @code
57
+ end
58
+
59
+ ERROR_MESSAGES.keys.each { |state| define_method("#{state}?") { @code == state } }
60
+
61
+ def successful?
62
+ @code == :successful
63
+ end
64
+
65
+ def unsuccessful?
66
+ ERROR_MESSAGES.keys.include?(@code)
67
+ end
68
+
69
+ def message
70
+ ERROR_MESSAGES[@code]
71
+ end
72
+ end
73
+
74
+ # normalizes an OpenID according to http://openid.net/specs/openid-authentication-2_0.html#normalization
75
+ def self.normalize_identifier(identifier)
76
+ # clean up whitespace
77
+ identifier = identifier.to_s.strip
78
+
79
+ # if an XRI has a prefix, strip it.
80
+ identifier.gsub!(/xri:\/\//i, '')
81
+
82
+ # dodge XRIs -- TODO: validate, don't just skip.
83
+ unless ['=', '@', '+', '$', '!', '('].include?(identifier.at(0))
84
+ # does it begin with http? if not, add it.
85
+ identifier = "http://#{identifier}" unless identifier =~ /^http/i
86
+
87
+ # strip any fragments
88
+ identifier.gsub!(/\#(.*)$/, '')
89
+
90
+ begin
91
+ uri = URI.parse(identifier)
92
+ uri.scheme = uri.scheme.downcase # URI should do this
93
+ identifier = uri.normalize.to_s
94
+ rescue URI::InvalidURIError
95
+ raise InvalidOpenId.new("#{identifier} is not an OpenID identifier")
96
+ end
97
+ end
98
+
99
+ return identifier
100
+ end
101
+
102
+ # deprecated for OpenID 2.0, where not all OpenIDs are URLs
103
+ def self.normalize_url(url)
104
+ ActiveSupport::Deprecation.warn "normalize_url has been deprecated, use normalize_identifier instead"
105
+ self.normalize_identifier(url)
106
+ end
107
+
108
+ protected
109
+ def normalize_url(url)
110
+ OpenIdAuthentication.normalize_url(url)
111
+ end
112
+
113
+ def normalize_identifier(url)
114
+ OpenIdAuthentication.normalize_identifier(url)
115
+ end
116
+
117
+ # The parameter name of "openid_identifier" is used rather than the Rails convention "open_id_identifier"
118
+ # because that's what the specification dictates in order to get browser auto-complete working across sites
119
+ def using_open_id?(identity_url = nil) #:doc:
120
+ identity_url ||= params[:openid_identifier] || params[:openid_url]
121
+ !identity_url.blank? || params[:open_id_complete]
122
+ end
123
+
124
+ def authenticate_with_open_id(identity_url = nil, options = {}, &block) #:doc:
125
+ identity_url ||= params[:openid_identifier] || params[:openid_url]
126
+
127
+ if params[:open_id_complete].nil?
128
+ begin_open_id_authentication(identity_url, options, &block)
129
+ else
130
+ complete_open_id_authentication(&block)
131
+ end
132
+ end
133
+
134
+ private
135
+ def begin_open_id_authentication(identity_url, options = {})
136
+ identity_url = normalize_identifier(identity_url)
137
+ return_to = options.delete(:return_to)
138
+ method = options.delete(:method)
139
+
140
+ options[:required] ||= [] # reduces validation later
141
+ options[:optional] ||= []
142
+
143
+ open_id_request = open_id_consumer.begin(identity_url)
144
+ add_simple_registration_fields(open_id_request, options)
145
+ add_ax_fields(open_id_request, options)
146
+ add_oauth_fields(open_id_request, options )
147
+
148
+ redirect_to(open_id_redirect_url(open_id_request, return_to, method))
149
+ rescue OpenIdAuthentication::InvalidOpenId => e
150
+ yield Result[:invalid], identity_url, nil
151
+ rescue OpenID::OpenIDError, Timeout::Error => e
152
+ logger.error("[OPENID] #{e}")
153
+ yield Result[:missing], identity_url, nil
154
+ end
155
+
156
+ def complete_open_id_authentication
157
+ params_with_path = params.reject { |key, value| request.path_parameters[key] }
158
+ params_with_path.delete(:format)
159
+ open_id_response = timeout_protection_from_identity_server { open_id_consumer.complete(params_with_path, requested_url) }
160
+ identity_url = normalize_identifier(open_id_response.display_identifier) if open_id_response.display_identifier
161
+
162
+ case open_id_response.status
163
+ when OpenID::Consumer::SUCCESS
164
+ profile_data = {}
165
+
166
+ # merge the SReg data and the AX data into a single hash of profile data
167
+ [ OpenID::SReg::Response, OpenID::AX::FetchResponse ].each do |data_response|
168
+ if data_response.from_success_response( open_id_response )
169
+ profile_data.merge! data_response.from_success_response( open_id_response ).data
170
+ end
171
+ end
172
+
173
+ # If applicable add OAuth request_token and scope
174
+ oauth_response = OpenID::OAuth::Response.from_success_response( open_id_response )
175
+ if oauth_response
176
+ profile_data[:request_token]=oauth_response.request_token
177
+ profile_data[:oauth_scope]=oauth_response.scope if oauth_response.scope
178
+ end
179
+
180
+ yield Result[:successful], identity_url, profile_data
181
+ when OpenID::Consumer::CANCEL
182
+ yield Result[:canceled], identity_url, nil
183
+ when OpenID::Consumer::FAILURE
184
+ yield Result[:failed], identity_url, nil
185
+ when OpenID::Consumer::SETUP_NEEDED
186
+ yield Result[:setup_needed], open_id_response.setup_url, nil
187
+ end
188
+ end
189
+
190
+ def open_id_consumer
191
+ OpenID::Consumer.new(session, OpenIdAuthentication.store)
192
+ end
193
+
194
+ def add_simple_registration_fields(open_id_request, fields)
195
+ sreg_request = OpenID::SReg::Request.new
196
+
197
+ # filter out AX identifiers (URIs)
198
+ required_fields = fields[:required].collect { |f| f.to_s unless f =~ /^https?:\/\// }.compact
199
+ optional_fields = fields[:optional].collect { |f| f.to_s unless f =~ /^https?:\/\// }.compact
200
+
201
+ sreg_request.request_fields(required_fields, true) unless required_fields.blank?
202
+ sreg_request.request_fields(optional_fields, false) unless optional_fields.blank?
203
+ sreg_request.policy_url = fields[:policy_url] if fields[:policy_url]
204
+ open_id_request.add_extension(sreg_request)
205
+ end
206
+
207
+ def add_oauth_fields(open_id_request, options={})
208
+ return unless options[:oauth] && options[:oauth][:consumer]
209
+ oauth_request = OpenID::OAuth::Request.new options[:oauth][:consumer], options[:oauth][:scope]
210
+
211
+ open_id_request.add_extension(oauth_request)
212
+ end
213
+
214
+ def add_ax_fields( open_id_request, fields )
215
+ ax_request = OpenID::AX::FetchRequest.new
216
+
217
+ # look through the :required and :optional fields for URIs (AX identifiers)
218
+ fields[:required].each do |f|
219
+ next unless f =~ /^https?:\/\//
220
+ ax_request.add( OpenID::AX::AttrInfo.new( f, nil, true ) )
221
+ end
222
+
223
+ fields[:optional].each do |f|
224
+ next unless f =~ /^https?:\/\//
225
+ ax_request.add( OpenID::AX::AttrInfo.new( f, nil, false ) )
226
+ end
227
+
228
+ open_id_request.add_extension( ax_request )
229
+ end
230
+
231
+ def open_id_redirect_url(open_id_request, return_to = nil, method = nil)
232
+ open_id_request.return_to_args['_method'] = (method || request.method).to_s
233
+ open_id_request.return_to_args['open_id_complete'] = '1'
234
+ open_id_request.redirect_url(root_url, return_to || requested_url)
235
+ end
236
+
237
+ def requested_url
238
+ relative_url_root = self.class.respond_to?(:relative_url_root) ?
239
+ self.class.relative_url_root.to_s :
240
+ request.relative_url_root
241
+ "#{request.protocol}#{request.host_with_port}#{ActionController::Base.relative_url_root}#{request.path}"
242
+ end
243
+
244
+ def timeout_protection_from_identity_server
245
+ yield
246
+ rescue Timeout::Error
247
+ Class.new do
248
+ def status
249
+ OpenID::FAILURE
250
+ end
251
+
252
+ def msg
253
+ "Identity server timed out"
254
+ end
255
+ end.new
256
+ end
257
+ end
@@ -0,0 +1,30 @@
1
+ namespace :open_id_authentication do
2
+ namespace :db do
3
+ desc "Creates authentication tables for use with OpenIdAuthentication"
4
+ task :create => :environment do
5
+ generate_migration(["open_id_authentication_tables", "add_open_id_authentication_tables"])
6
+ end
7
+
8
+ desc "Upgrade authentication tables from ruby-openid 1.x.x to 2.x.x"
9
+ task :upgrade => :environment do
10
+ generate_migration(["upgrade_open_id_authentication_tables", "upgrade_open_id_authentication_tables"])
11
+ end
12
+
13
+ def generate_migration(args)
14
+ require 'rails_generator'
15
+ require 'rails_generator/scripts/generate'
16
+
17
+ if ActiveRecord::Base.connection.supports_migrations?
18
+ Rails::Generator::Scripts::Generate.new.run(args)
19
+ else
20
+ raise "Task unavailable to this database (no migration support)"
21
+ end
22
+ end
23
+
24
+ desc "Clear the authentication tables"
25
+ task :clear => :environment do
26
+ OpenIdAuthentication::DbStore.cleanup_nonces
27
+ OpenIdAuthentication::DbStore.cleanup_associations
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,32 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class NormalizeTest < Test::Unit::TestCase
4
+ include OpenIdAuthentication
5
+
6
+ NORMALIZATIONS = {
7
+ "openid.aol.com/nextangler" => "http://openid.aol.com/nextangler",
8
+ "http://openid.aol.com/nextangler" => "http://openid.aol.com/nextangler",
9
+ "https://openid.aol.com/nextangler" => "https://openid.aol.com/nextangler",
10
+ "HTTP://OPENID.AOL.COM/NEXTANGLER" => "http://openid.aol.com/NEXTANGLER",
11
+ "HTTPS://OPENID.AOL.COM/NEXTANGLER" => "https://openid.aol.com/NEXTANGLER",
12
+ "loudthinking.com" => "http://loudthinking.com/",
13
+ "http://loudthinking.com" => "http://loudthinking.com/",
14
+ "http://loudthinking.com:80" => "http://loudthinking.com/",
15
+ "https://loudthinking.com:443" => "https://loudthinking.com/",
16
+ "http://loudthinking.com:8080" => "http://loudthinking.com:8080/",
17
+ "techno-weenie.net" => "http://techno-weenie.net/",
18
+ "http://techno-weenie.net" => "http://techno-weenie.net/",
19
+ "http://techno-weenie.net " => "http://techno-weenie.net/",
20
+ "=name" => "=name"
21
+ }
22
+
23
+ def test_normalizations
24
+ NORMALIZATIONS.each do |from, to|
25
+ assert_equal to, normalize_identifier(from)
26
+ end
27
+ end
28
+
29
+ def test_broken_open_id
30
+ assert_raises(InvalidOpenId) { normalize_identifier(nil) }
31
+ end
32
+ end
@@ -0,0 +1,46 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class OpenIdAuthenticationTest < Test::Unit::TestCase
4
+ def setup
5
+ @controller = Class.new do
6
+ include OpenIdAuthentication
7
+ def params() {} end
8
+ end.new
9
+ end
10
+
11
+ def test_authentication_should_fail_when_the_identity_server_is_missing
12
+ open_id_consumer = mock()
13
+ open_id_consumer.expects(:begin).raises(OpenID::OpenIDError)
14
+ @controller.expects(:open_id_consumer).returns(open_id_consumer)
15
+ @controller.expects(:logger).returns(mock(:error => true))
16
+
17
+ @controller.send(:authenticate_with_open_id, "http://someone.example.com") do |result, identity_url|
18
+ assert result.missing?
19
+ assert_equal "Sorry, the OpenID server couldn't be found", result.message
20
+ end
21
+ end
22
+
23
+ def test_authentication_should_be_invalid_when_the_identity_url_is_invalid
24
+ @controller.send(:authenticate_with_open_id, "!") do |result, identity_url|
25
+ assert result.invalid?, "Result expected to be invalid but was not"
26
+ assert_equal "Sorry, but this does not appear to be a valid OpenID", result.message
27
+ end
28
+ end
29
+
30
+ def test_authentication_should_fail_when_the_identity_server_times_out
31
+ open_id_consumer = mock()
32
+ open_id_consumer.expects(:begin).raises(Timeout::Error, "Identity Server took too long.")
33
+ @controller.expects(:open_id_consumer).returns(open_id_consumer)
34
+ @controller.expects(:logger).returns(mock(:error => true))
35
+
36
+ @controller.send(:authenticate_with_open_id, "http://someone.example.com") do |result, identity_url|
37
+ assert result.missing?
38
+ assert_equal "Sorry, the OpenID server couldn't be found", result.message
39
+ end
40
+ end
41
+
42
+ def test_authentication_should_begin_when_the_identity_server_is_present
43
+ @controller.expects(:begin_open_id_authentication)
44
+ @controller.send(:authenticate_with_open_id, "http://someone.example.com")
45
+ end
46
+ end
@@ -0,0 +1,14 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class StatusTest < Test::Unit::TestCase
4
+ include OpenIdAuthentication
5
+
6
+ def test_state_conditional
7
+ assert Result[:missing].missing?
8
+ assert Result[:missing].unsuccessful?
9
+ assert !Result[:missing].successful?
10
+
11
+ assert Result[:successful].successful?
12
+ assert !Result[:successful].unsuccessful?
13
+ end
14
+ end
@@ -0,0 +1,17 @@
1
+ require 'test/unit'
2
+ require 'rubygems'
3
+
4
+ gem 'activesupport'
5
+ require 'active_support'
6
+
7
+ gem 'actionpack'
8
+ require 'action_controller'
9
+
10
+ gem 'mocha'
11
+ require 'mocha'
12
+
13
+ gem 'ruby-openid'
14
+ require 'openid'
15
+
16
+ RAILS_ROOT = File.dirname(__FILE__) unless defined? RAILS_ROOT
17
+ require File.dirname(__FILE__) + "/../lib/open_id_authentication"
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pelle-open_id_authentication
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - David Heinemeier Hansson
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-09-27 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: pelle-ruby-openid
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 2.1.8
24
+ version:
25
+ description:
26
+ email: david@loudthinking.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - CHANGELOG
33
+ - MIT-LICENSE
34
+ - README
35
+ files:
36
+ - CHANGELOG
37
+ - README
38
+ - Rakefile
39
+ - generators/open_id_authentication_tables/open_id_authentication_tables_generator.rb
40
+ - generators/open_id_authentication_tables/templates/migration.rb
41
+ - generators/upgrade_open_id_authentication_tables/templates/migration.rb
42
+ - generators/upgrade_open_id_authentication_tables/upgrade_open_id_authentication_tables_generator.rb
43
+ - init.rb
44
+ - lib/open_id_authentication.rb
45
+ - lib/open_id_authentication/association.rb
46
+ - lib/open_id_authentication/db_store.rb
47
+ - lib/open_id_authentication/nonce.rb
48
+ - lib/open_id_authentication/request.rb
49
+ - lib/open_id_authentication/timeout_fixes.rb
50
+ - tasks/open_id_authentication_tasks.rake
51
+ - test/normalize_test.rb
52
+ - test/open_id_authentication_test.rb
53
+ - test/status_test.rb
54
+ - test/test_helper.rb
55
+ - MIT-LICENSE
56
+ has_rdoc: false
57
+ homepage: http://www.rubyonrails.org
58
+ licenses:
59
+ post_install_message:
60
+ rdoc_options:
61
+ - --charset=UTF-8
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: "0"
69
+ version:
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: "0"
75
+ version:
76
+ requirements: []
77
+
78
+ rubyforge_project:
79
+ rubygems_version: 1.3.5
80
+ signing_key:
81
+ specification_version: 3
82
+ summary: Provides a thin wrapper around the excellent ruby-openid gem from JanRan.
83
+ test_files:
84
+ - test/normalize_test.rb
85
+ - test/open_id_authentication_test.rb
86
+ - test/status_test.rb
87
+ - test/test_helper.rb