Class | Rack::Auth::OpenID |
In: |
lib/rack/auth/openid.rb
|
Parent: | AbstractHandler |
Rack::Auth::OpenID provides a simple method for permitting openid based logins. It requires the ruby-openid library from janrain to operate, as well as a rack method of session management.
The ruby-openid home page is at openidenabled.com/ruby-openid/.
The OpenID specifications can be found at openid.net/specs/openid-authentication-1_1.html and openid.net/specs/openid-authentication-2_0.html. Documentation for published OpenID extensions and related topics can be found at openid.net/developers/specs/.
It is recommended to read through the OpenID spec, as well as ruby-openid‘s documentation, to understand what exactly goes on. However a setup as simple as the presented examples is enough to provide functionality.
This library strongly intends to utilize the OpenID 2.0 features of the ruby-openid library, while maintaining OpenID 1.0 compatiblity.
All responses from this rack application will be 303 redirects unless an error occurs, with the exception of an authentication request requiring an HTML form submission.
NOTE: Extensions are not currently supported by this implimentation of the OpenID rack application due to the complexity of the current ruby-openid extension handling.
NOTE: Due to the amount of data that this library stores in the session, Rack::Session::Cookie may fault.
OIDStore | = | ::OpenID::Store::Memory.new | Required for ruby-openid | |
HTML | = | '<html><head><title>%s</title></head><body>%s</body></html>' |
extensions | [R] | |
options | [R] |
A Hash of options is taken as it‘s single initializing argument. For example:
simple_oid = OpenID.new('http://mysite.com/') return_oid = OpenID.new('http://mysite.com/', { :return_to => 'http://mysite.com/openid' }) page_oid = OpenID.new('http://mysite.com/', :login_good => 'http://mysite.com/auth_good' ) complex_oid = OpenID.new('http://mysite.com/', :return_to => 'http://mysite.com/openid', :login_good => 'http://mysite.com/user/preferences', :auth_fail => [500, {'Content-Type'=>'text/plain'}, 'Unable to negotiate with foreign server.'], :immediate => true, :extensions => { ::OpenID::SReg => [['email'],['nickname']] } )
The first argument is the realm, identifying the site they are trusting with their identity. This is required.
NOTE: In OpenID 1.x, the realm or trust_root is optional and the return_to url is required. As this library strives tward ruby-openid 2.0, and OpenID 2.0 compatibiliy, the realm is required and return_to is optional. However, this implimentation is still backwards compatible with OpenID 1.0 servers.
The optional second argument is a hash of options.
:return_to defines the url to return to after the client authenticates with the openid service provider. This url should point to where Rack::Auth::OpenID is mounted. If :return_to is not provided, :return_to will be the current url including all query parameters.
:session_key defines the key to the session hash in the env. It defaults to ‘rack.session’.
:openid_param defines at what key in the request parameters to find the identifier to resolve. As per the 2.0 spec, the default is ‘openid_identifier’.
:immediate as true will make immediate type of requests the default. See OpenID specification documentation.
:login_good is the url to go to after the authentication process has completed.
:login_fail is the url to go to after the authentication process has failed.
:login_quit is the url to go to after the authentication process has been cancelled.
:no_session should be a rack response to be returned if no or an incompatible session is found.
:auth_fail should be a rack response to be returned if an OpenID::DiscoveryFailure occurs. This is typically due to being unable to access the identity url or identity server.
:error should be a rack response to return if any other generic error would occur and options[:catch_errors] is true.
:extensions should be a hash of openid extension implementations. The key should be the extension main module, the value should be an array of arguments for extension::Request.new
The hash is iterated over and passed to add_extension for processing. Please see add_extension for further documentation.
# File lib/rack/auth/openid.rb, line 137 137: def initialize(realm, options={}) 138: @realm = realm 139: realm = URI(realm) 140: if realm.path.empty? 141: raise ArgumentError, "Invalid realm path: '#{realm.path}'" 142: elsif not realm.absolute? 143: raise ArgumentError, "Realm '#{@realm}' not absolute" 144: end 145: 146: [:return_to, :login_good, :login_fail, :login_quit].each do |key| 147: if options.key? key and luri = URI(options[key]) 148: if !luri.absolute? 149: raise ArgumentError, ":#{key} is not an absolute uri: '#{luri}'" 150: end 151: end 152: end 153: 154: if options[:return_to] and ruri = URI(options[:return_to]) 155: if ruri.path.empty? 156: raise ArgumentError, "Invalid return_to path: '#{ruri.path}'" 157: elsif realm.path != ruri.path[0, realm.path.size] 158: raise ArgumentError, 'return_to not within realm.' \ 159: end 160: end 161: 162: # TODO: extension support 163: if extensions = options.delete(:extensions) 164: extensions.each do |ext, args| 165: add_extension ext, *args 166: end 167: end 168: 169: @options = { 170: :session_key => 'rack.session', 171: :openid_param => 'openid_identifier', 172: #:return_to, :login_good, :login_fail, :login_quit 173: #:no_session, :auth_fail, :error 174: :store => OIDStore, 175: :immediate => false, 176: :anonymous => false, 177: :catch_errors => false 178: }.merge(options) 179: @extensions = {} 180: end
The first argument should be the main extension module. The extension module should contain the constants:
* class Request, with OpenID::Extension as an ancestor * class Response, with OpenID::Extension as an ancestor * string NS_URI, which defines the namespace of the extension, should be an absolute http uri
All trailing arguments will be passed to extension::Request.new in check. The openid response will be passed to extension::Response#from_success_response, get_extension_args will be called on the result to attain the gathered data.
This method returns the key at which the response data will be found in the session, which is the namespace uri by default.
# File lib/rack/auth/openid.rb, line 402 402: def add_extension ext, *args 403: if not ext.is_a? Module 404: raise TypeError, "#{ext.inspect} is not a module" 405: elsif !(m = %w'Request Response NS_URI' - 406: ext.constants.map{ |c| c.to_s }).empty? 407: raise ArgumentError, "#{ext.inspect} missing #{m*', '}" 408: end 409: 410: consts = [ext::Request, ext::Response] 411: 412: if not consts.all?{|c| c.is_a? Class } 413: raise TypeError, "#{ext.inspect}'s Request or Response is not a class" 414: elsif not consts.all?{|c| ::OpenID::Extension > c } 415: raise ArgumentError, "#{ext.inspect}'s Request or Response not a decendant of OpenID::Extension" 416: end 417: 418: if not ext::NS_URI.is_a? String 419: raise TypeError, "#{ext.inspect}'s NS_URI is not a string" 420: elsif not uri = URI(ext::NS_URI) 421: raise ArgumentError, "#{ext.inspect}'s NS_URI is not a valid uri" 422: elsif not uri.scheme =~ /^https?$/ 423: raise ArgumentError, "#{ext.inspect}'s NS_URI is not an http uri" 424: elsif not uri.absolute? 425: raise ArgumentError, "#{ext.inspect}'s NS_URI is not and absolute uri" 426: end 427: @extensions[ext] = args 428: return ext::NS_URI 429: end
It sets up and uses session data at :openid within the session. It sets up the ::OpenID::Consumer using the store specified by options[:store].
If the parameter specified by options[:openid_param] is present, processing is passed to check and the result is returned.
If the parameter ‘openid.mode’ is set, implying a followup from the openid server, processing is passed to finish and the result is returned.
If neither of these conditions are met, a 400 error is returned.
If an error is thrown and options[:catch_errors] is false, the exception will be reraised. Otherwise a 500 error is returned.
# File lib/rack/auth/openid.rb, line 199 199: def call(env) 200: env['rack.auth.openid'] = self 201: session = env[@options[:session_key]] 202: unless session and session.is_a? Hash 203: raise(NoSession, 'No compatible session') 204: end 205: # let us work in our own namespace... 206: session = (session[:openid] ||= {}) 207: unless session and session.is_a? Hash 208: raise(NoSession, 'Incompatible session') 209: end 210: 211: request = Rack::Request.new env 212: consumer = ::OpenID::Consumer.new session, @options[:store] 213: 214: if request.params['openid.mode'] 215: finish consumer, session, request 216: elsif request.params[@options[:openid_param]] 217: check consumer, session, request 218: else 219: env['rack.errors'].puts "No valid params provided." 220: bad_request 221: end 222: rescue NoSession 223: env['rack.errors'].puts($!.message, *$@) 224: 225: @options. ### Missing or incompatible session 226: fetch :no_session, [ 500, 227: {'Content-Type'=>'text/plain'}, 228: $!.message ] 229: rescue 230: env['rack.errors'].puts($!.message, *$@) 231: 232: if not @options[:catch_error] 233: raise($!) 234: end 235: @options. 236: fetch :error, [ 500, 237: {'Content-Type'=>'text/plain'}, 238: 'OpenID has encountered an error.' ] 239: end
As the first part of OpenID consumer action, check retrieves the data required for completion.
# File lib/rack/auth/openid.rb, line 250 250: def check(consumer, session, req) 251: session[:openid_param] = req.params[@options[:openid_param]] 252: oid = consumer.begin(session[:openid_param], @options[:anonymous]) 253: pp oid if $DEBUG 254: req.env['rack.auth.openid.request'] = oid 255: 256: session[:site_return] ||= req.env['HTTP_REFERER'] 257: 258: # SETUP_NEEDED check! 259: # see OpenID::Consumer::CheckIDRequest docs 260: query_args = [@realm, *@options.values_at(:return_to, :immediate)] 261: query_args[1] ||= req.url 262: query_args[2] = false if session.key? :setup_needed 263: pp query_args if $DEBUG 264: 265: ## Extension support 266: extensions.each do |ext,args| 267: oid.add_extension ext::Request.new(*args) 268: end 269: 270: if oid.send_redirect?(*query_args) 271: redirect = oid.redirect_url(*query_args) 272: if $DEBUG 273: pp redirect 274: pp Rack::Utils.parse_query(URI(redirect).query) 275: end 276: [ 303, {'Location'=>redirect}, [] ] 277: else 278: # check on 'action' option. 279: formbody = oid.form_markup(*query_args) 280: if $DEBUG 281: pp formbody 282: end 283: body = HTML % ['Confirm...', formbody] 284: [ 200, {'Content-Type'=>'text/html'}, body.to_a ] 285: end 286: rescue ::OpenID::DiscoveryFailure => e 287: # thrown from inside OpenID::Consumer#begin by yadis stuff 288: req.env['rack.errors'].puts($!.message, *$@) 289: 290: @options. ### Foreign server failed 291: fetch :auth_fail, [ 503, 292: {'Content-Type'=>'text/plain'}, 293: 'Foreign server failure.' ] 294: end
A conveniance method that returns the namespace of all current extensions used by this instance.
# File lib/rack/auth/openid.rb, line 433 433: def extension_namespaces 434: @extensions.keys.map{|e|e::NS_URI} 435: end
This is the final portion of authentication. Unless any errors outside of specification occur, a 303 redirect will be returned with Location determined by the OpenID response type. If none of the response type :login_* urls are set, the redirect will be set to session[:openid][:site_return]. If session[:openid][:site_return] is unset, the realm will be used.
Any messages from OpenID‘s response are appended to the 303 response body.
Data gathered from extensions are stored in session[:openid] with the extension‘s namespace uri as the key.
The four valid possible outcomes are:
# File lib/rack/auth/openid.rb, line 332 332: def finish(consumer, session, req) 333: oid = consumer.complete(req.params, req.url) 334: pp oid if $DEBUG 335: req.env['rack.auth.openid.response'] = oid 336: 337: goto = session.fetch :site_return, @realm 338: body = [] 339: 340: case oid.status 341: when ::OpenID::Consumer::FAILURE 342: session.clear 343: session['authenticated'] = false 344: req.env['rack.errors'].puts oid.message 345: 346: goto = @options[:login_fail] if @options.key? :login_fail 347: body << "Authentication unsuccessful.\n" 348: when ::OpenID::Consumer::SUCCESS 349: session.clear 350: 351: ## Extension support 352: extensions.each do |ext, args| 353: session[ext::NS_URI] = ext::Response. 354: from_success_response(oid). 355: get_extension_args 356: end 357: 358: session['authenticated'] = true 359: # Value for unique identification and such 360: session['identity'] = oid.identity_url 361: # Value for display and UI labels 362: session['identifier'] = oid.display_identifier 363: 364: goto = @options[:login_good] if @options.key? :login_good 365: body << "Authentication successful.\n" 366: when ::OpenID::Consumer::CANCEL 367: session.clear 368: session['authenticated'] = false 369: 370: goto = @options[:login_fail] if @options.key? :login_fail 371: body << "Authentication cancelled.\n" 372: when ::OpenID::Consumer::SETUP_NEEDED 373: session[:setup_needed] = true 374: unless o_id = session[:openid_param] 375: raise('Required values missing.') 376: end 377: 378: goto = req.script_name+ 379: '?'+@options[:openid_param]+ 380: '='+o_id 381: body << "Reauthentication required.\n" 382: end 383: body << oid.message if oid.message 384: [ 303, {'Location'=>goto}, body] 385: end