Chris@0
|
1 require 'uri'
|
Chris@0
|
2 require 'openid/extensions/sreg'
|
Chris@0
|
3 require 'openid/extensions/ax'
|
Chris@0
|
4 require 'openid/store/filesystem'
|
Chris@0
|
5
|
Chris@0
|
6 require File.dirname(__FILE__) + '/open_id_authentication/db_store'
|
Chris@0
|
7 require File.dirname(__FILE__) + '/open_id_authentication/mem_cache_store'
|
Chris@0
|
8 require File.dirname(__FILE__) + '/open_id_authentication/request'
|
Chris@0
|
9 require File.dirname(__FILE__) + '/open_id_authentication/timeout_fixes' if OpenID::VERSION == "2.0.4"
|
Chris@0
|
10
|
Chris@0
|
11 module OpenIdAuthentication
|
Chris@0
|
12 OPEN_ID_AUTHENTICATION_DIR = RAILS_ROOT + "/tmp/openids"
|
Chris@0
|
13
|
Chris@0
|
14 def self.store
|
Chris@0
|
15 @@store
|
Chris@0
|
16 end
|
Chris@0
|
17
|
Chris@0
|
18 def self.store=(*store_option)
|
Chris@0
|
19 store, *parameters = *([ store_option ].flatten)
|
Chris@0
|
20
|
Chris@0
|
21 @@store = case store
|
Chris@0
|
22 when :db
|
Chris@0
|
23 OpenIdAuthentication::DbStore.new
|
Chris@0
|
24 when :mem_cache
|
Chris@0
|
25 OpenIdAuthentication::MemCacheStore.new(*parameters)
|
Chris@0
|
26 when :file
|
Chris@0
|
27 OpenID::Store::Filesystem.new(OPEN_ID_AUTHENTICATION_DIR)
|
Chris@0
|
28 else
|
Chris@0
|
29 raise "Unknown store: #{store}"
|
Chris@0
|
30 end
|
Chris@0
|
31 end
|
Chris@0
|
32
|
Chris@0
|
33 self.store = :db
|
Chris@0
|
34
|
Chris@0
|
35 class InvalidOpenId < StandardError
|
Chris@0
|
36 end
|
Chris@0
|
37
|
Chris@0
|
38 class Result
|
Chris@0
|
39 ERROR_MESSAGES = {
|
Chris@0
|
40 :missing => "Sorry, the OpenID server couldn't be found",
|
Chris@0
|
41 :invalid => "Sorry, but this does not appear to be a valid OpenID",
|
Chris@0
|
42 :canceled => "OpenID verification was canceled",
|
Chris@0
|
43 :failed => "OpenID verification failed",
|
Chris@0
|
44 :setup_needed => "OpenID verification needs setup"
|
Chris@0
|
45 }
|
Chris@0
|
46
|
Chris@0
|
47 def self.[](code)
|
Chris@0
|
48 new(code)
|
Chris@0
|
49 end
|
Chris@0
|
50
|
Chris@0
|
51 def initialize(code)
|
Chris@0
|
52 @code = code
|
Chris@0
|
53 end
|
Chris@0
|
54
|
Chris@0
|
55 def status
|
Chris@0
|
56 @code
|
Chris@0
|
57 end
|
Chris@0
|
58
|
Chris@0
|
59 ERROR_MESSAGES.keys.each { |state| define_method("#{state}?") { @code == state } }
|
Chris@0
|
60
|
Chris@0
|
61 def successful?
|
Chris@0
|
62 @code == :successful
|
Chris@0
|
63 end
|
Chris@0
|
64
|
Chris@0
|
65 def unsuccessful?
|
Chris@0
|
66 ERROR_MESSAGES.keys.include?(@code)
|
Chris@0
|
67 end
|
Chris@0
|
68
|
Chris@0
|
69 def message
|
Chris@0
|
70 ERROR_MESSAGES[@code]
|
Chris@0
|
71 end
|
Chris@0
|
72 end
|
Chris@0
|
73
|
Chris@0
|
74 # normalizes an OpenID according to http://openid.net/specs/openid-authentication-2_0.html#normalization
|
Chris@0
|
75 def self.normalize_identifier(identifier)
|
Chris@0
|
76 # clean up whitespace
|
Chris@0
|
77 identifier = identifier.to_s.strip
|
Chris@0
|
78
|
Chris@0
|
79 # if an XRI has a prefix, strip it.
|
Chris@0
|
80 identifier.gsub!(/xri:\/\//i, '')
|
Chris@0
|
81
|
Chris@0
|
82 # dodge XRIs -- TODO: validate, don't just skip.
|
Chris@0
|
83 unless ['=', '@', '+', '$', '!', '('].include?(identifier.at(0))
|
Chris@0
|
84 # does it begin with http? if not, add it.
|
Chris@0
|
85 identifier = "http://#{identifier}" unless identifier =~ /^http/i
|
Chris@0
|
86
|
Chris@0
|
87 # strip any fragments
|
Chris@0
|
88 identifier.gsub!(/\#(.*)$/, '')
|
Chris@0
|
89
|
Chris@0
|
90 begin
|
Chris@0
|
91 uri = URI.parse(identifier)
|
Chris@14
|
92 uri.scheme = uri.scheme.downcase if uri.scheme # URI should do this
|
Chris@0
|
93 identifier = uri.normalize.to_s
|
Chris@0
|
94 rescue URI::InvalidURIError
|
Chris@0
|
95 raise InvalidOpenId.new("#{identifier} is not an OpenID identifier")
|
Chris@0
|
96 end
|
Chris@0
|
97 end
|
Chris@0
|
98
|
Chris@0
|
99 return identifier
|
Chris@0
|
100 end
|
Chris@0
|
101
|
Chris@0
|
102 # deprecated for OpenID 2.0, where not all OpenIDs are URLs
|
Chris@0
|
103 def self.normalize_url(url)
|
Chris@0
|
104 ActiveSupport::Deprecation.warn "normalize_url has been deprecated, use normalize_identifier instead"
|
Chris@0
|
105 self.normalize_identifier(url)
|
Chris@0
|
106 end
|
Chris@0
|
107
|
Chris@0
|
108 protected
|
Chris@0
|
109 def normalize_url(url)
|
Chris@0
|
110 OpenIdAuthentication.normalize_url(url)
|
Chris@0
|
111 end
|
Chris@0
|
112
|
Chris@0
|
113 def normalize_identifier(url)
|
Chris@0
|
114 OpenIdAuthentication.normalize_identifier(url)
|
Chris@0
|
115 end
|
Chris@0
|
116
|
Chris@0
|
117 # The parameter name of "openid_identifier" is used rather than the Rails convention "open_id_identifier"
|
Chris@0
|
118 # because that's what the specification dictates in order to get browser auto-complete working across sites
|
Chris@0
|
119 def using_open_id?(identity_url = nil) #:doc:
|
Chris@0
|
120 identity_url ||= params[:openid_identifier] || params[:openid_url]
|
Chris@0
|
121 !identity_url.blank? || params[:open_id_complete]
|
Chris@0
|
122 end
|
Chris@0
|
123
|
Chris@0
|
124 def authenticate_with_open_id(identity_url = nil, options = {}, &block) #:doc:
|
Chris@0
|
125 identity_url ||= params[:openid_identifier] || params[:openid_url]
|
Chris@0
|
126
|
Chris@0
|
127 if params[:open_id_complete].nil?
|
Chris@0
|
128 begin_open_id_authentication(identity_url, options, &block)
|
Chris@0
|
129 else
|
Chris@0
|
130 complete_open_id_authentication(&block)
|
Chris@0
|
131 end
|
Chris@0
|
132 end
|
Chris@0
|
133
|
Chris@0
|
134 private
|
Chris@0
|
135 def begin_open_id_authentication(identity_url, options = {})
|
Chris@0
|
136 identity_url = normalize_identifier(identity_url)
|
Chris@0
|
137 return_to = options.delete(:return_to)
|
Chris@0
|
138 method = options.delete(:method)
|
Chris@0
|
139
|
Chris@0
|
140 options[:required] ||= [] # reduces validation later
|
Chris@0
|
141 options[:optional] ||= []
|
Chris@0
|
142
|
Chris@0
|
143 open_id_request = open_id_consumer.begin(identity_url)
|
Chris@0
|
144 add_simple_registration_fields(open_id_request, options)
|
Chris@0
|
145 add_ax_fields(open_id_request, options)
|
Chris@0
|
146 redirect_to(open_id_redirect_url(open_id_request, return_to, method))
|
Chris@0
|
147 rescue OpenIdAuthentication::InvalidOpenId => e
|
Chris@0
|
148 yield Result[:invalid], identity_url, nil
|
Chris@0
|
149 rescue OpenID::OpenIDError, Timeout::Error => e
|
Chris@0
|
150 logger.error("[OPENID] #{e}")
|
Chris@0
|
151 yield Result[:missing], identity_url, nil
|
Chris@0
|
152 end
|
Chris@0
|
153
|
Chris@0
|
154 def complete_open_id_authentication
|
Chris@0
|
155 params_with_path = params.reject { |key, value| request.path_parameters[key] }
|
Chris@0
|
156 params_with_path.delete(:format)
|
Chris@0
|
157 open_id_response = timeout_protection_from_identity_server { open_id_consumer.complete(params_with_path, requested_url) }
|
Chris@0
|
158 identity_url = normalize_identifier(open_id_response.display_identifier) if open_id_response.display_identifier
|
Chris@0
|
159
|
Chris@0
|
160 case open_id_response.status
|
Chris@0
|
161 when OpenID::Consumer::SUCCESS
|
Chris@0
|
162 profile_data = {}
|
Chris@0
|
163
|
Chris@0
|
164 # merge the SReg data and the AX data into a single hash of profile data
|
Chris@0
|
165 [ OpenID::SReg::Response, OpenID::AX::FetchResponse ].each do |data_response|
|
Chris@0
|
166 if data_response.from_success_response( open_id_response )
|
Chris@0
|
167 profile_data.merge! data_response.from_success_response( open_id_response ).data
|
Chris@0
|
168 end
|
Chris@0
|
169 end
|
Chris@0
|
170
|
Chris@0
|
171 yield Result[:successful], identity_url, profile_data
|
Chris@0
|
172 when OpenID::Consumer::CANCEL
|
Chris@0
|
173 yield Result[:canceled], identity_url, nil
|
Chris@0
|
174 when OpenID::Consumer::FAILURE
|
Chris@0
|
175 yield Result[:failed], identity_url, nil
|
Chris@0
|
176 when OpenID::Consumer::SETUP_NEEDED
|
Chris@0
|
177 yield Result[:setup_needed], open_id_response.setup_url, nil
|
Chris@0
|
178 end
|
Chris@0
|
179 end
|
Chris@0
|
180
|
Chris@0
|
181 def open_id_consumer
|
Chris@0
|
182 OpenID::Consumer.new(session, OpenIdAuthentication.store)
|
Chris@0
|
183 end
|
Chris@0
|
184
|
Chris@0
|
185 def add_simple_registration_fields(open_id_request, fields)
|
Chris@0
|
186 sreg_request = OpenID::SReg::Request.new
|
Chris@0
|
187
|
Chris@0
|
188 # filter out AX identifiers (URIs)
|
Chris@0
|
189 required_fields = fields[:required].collect { |f| f.to_s unless f =~ /^https?:\/\// }.compact
|
Chris@0
|
190 optional_fields = fields[:optional].collect { |f| f.to_s unless f =~ /^https?:\/\// }.compact
|
Chris@0
|
191
|
Chris@0
|
192 sreg_request.request_fields(required_fields, true) unless required_fields.blank?
|
Chris@0
|
193 sreg_request.request_fields(optional_fields, false) unless optional_fields.blank?
|
Chris@0
|
194 sreg_request.policy_url = fields[:policy_url] if fields[:policy_url]
|
Chris@0
|
195 open_id_request.add_extension(sreg_request)
|
Chris@0
|
196 end
|
Chris@0
|
197
|
Chris@0
|
198 def add_ax_fields( open_id_request, fields )
|
Chris@0
|
199 ax_request = OpenID::AX::FetchRequest.new
|
Chris@0
|
200
|
Chris@0
|
201 # look through the :required and :optional fields for URIs (AX identifiers)
|
Chris@0
|
202 fields[:required].each do |f|
|
Chris@0
|
203 next unless f =~ /^https?:\/\//
|
Chris@0
|
204 ax_request.add( OpenID::AX::AttrInfo.new( f, nil, true ) )
|
Chris@0
|
205 end
|
Chris@0
|
206
|
Chris@0
|
207 fields[:optional].each do |f|
|
Chris@0
|
208 next unless f =~ /^https?:\/\//
|
Chris@0
|
209 ax_request.add( OpenID::AX::AttrInfo.new( f, nil, false ) )
|
Chris@0
|
210 end
|
Chris@0
|
211
|
Chris@0
|
212 open_id_request.add_extension( ax_request )
|
Chris@0
|
213 end
|
Chris@0
|
214
|
Chris@0
|
215 def open_id_redirect_url(open_id_request, return_to = nil, method = nil)
|
Chris@0
|
216 open_id_request.return_to_args['_method'] = (method || request.method).to_s
|
Chris@0
|
217 open_id_request.return_to_args['open_id_complete'] = '1'
|
Chris@0
|
218 open_id_request.redirect_url(root_url, return_to || requested_url)
|
Chris@0
|
219 end
|
Chris@0
|
220
|
Chris@0
|
221 def requested_url
|
Chris@0
|
222 relative_url_root = self.class.respond_to?(:relative_url_root) ?
|
Chris@0
|
223 self.class.relative_url_root.to_s :
|
Chris@0
|
224 request.relative_url_root
|
Chris@0
|
225 "#{request.protocol}#{request.host_with_port}#{relative_url_root}#{request.path}"
|
Chris@0
|
226 end
|
Chris@0
|
227
|
Chris@0
|
228 def timeout_protection_from_identity_server
|
Chris@0
|
229 yield
|
Chris@0
|
230 rescue Timeout::Error
|
Chris@0
|
231 Class.new do
|
Chris@0
|
232 def status
|
Chris@0
|
233 OpenID::FAILURE
|
Chris@0
|
234 end
|
Chris@0
|
235
|
Chris@0
|
236 def msg
|
Chris@0
|
237 "Identity server timed out"
|
Chris@0
|
238 end
|
Chris@0
|
239 end.new
|
Chris@0
|
240 end
|
Chris@0
|
241 end
|