Class: Rack::Auth::OpenID

Inherits:
AbstractHandler show all
Defined in:
lib/gems/rack-0.9.1/lib/rack/auth/openid.rb

Overview

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.

Defined Under Namespace

Classes: NoSession

Constant Summary collapse

OIDStore =

Required for ruby-openid

::OpenID::Store::Memory.new
HTML =
'<html><head><title>%s</title></head><body>%s</body></html>'

Instance Attribute Summary collapse

Attributes inherited from AbstractHandler

#realm

Instance Method Summary collapse

Constructor Details

#initialize(realm, options = {}) ⇒ OpenID

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']]
  }
)

Arguments

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.

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.

URL options

: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.

Response options

: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

: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.



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/gems/rack-0.9.1/lib/rack/auth/openid.rb', line 137

def initialize(realm, options={})
  @realm = realm
  realm = URI(realm)
  if realm.path.empty?
    raise ArgumentError, "Invalid realm path: '#{realm.path}'"
  elsif not realm.absolute?
    raise ArgumentError, "Realm '#{@realm}' not absolute"
  end

  [:return_to, :login_good, :login_fail, :login_quit].each do |key|
    if options.key? key and luri = URI(options[key])
      if !luri.absolute?
        raise ArgumentError, ":#{key} is not an absolute uri: '#{luri}'"
      end
    end
  end

  if options[:return_to] and ruri = URI(options[:return_to])
    if ruri.path.empty?
      raise ArgumentError, "Invalid return_to path: '#{ruri.path}'"
    elsif realm.path != ruri.path[0, realm.path.size]
      raise ArgumentError, 'return_to not within realm.' \
    end
  end

  # TODO: extension support
  if extensions = options.delete(:extensions)
    extensions.each do |ext, args|
      add_extension ext, *args
    end
  end

  @options = {
    :session_key => 'rack.session',
    :openid_param => 'openid_identifier',
    #:return_to, :login_good, :login_fail, :login_quit
    #:no_session, :auth_fail, :error
    :store => OIDStore,
    :immediate => false,
    :anonymous => false,
    :catch_errors => false
  }.merge(options)
  @extensions = {}
end

Instance Attribute Details

#extensionsObject (readonly)

Returns the value of attribute extensions.



182
183
184
# File 'lib/gems/rack-0.9.1/lib/rack/auth/openid.rb', line 182

def extensions
  @extensions
end

#optionsObject (readonly)

Returns the value of attribute options.



182
183
184
# File 'lib/gems/rack-0.9.1/lib/rack/auth/openid.rb', line 182

def options
  @options
end

Instance Method Details

#add_extension(ext, *args) ⇒ Object

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.



402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
# File 'lib/gems/rack-0.9.1/lib/rack/auth/openid.rb', line 402

def add_extension ext, *args
  if not ext.is_a? Module
    raise TypeError, "#{ext.inspect} is not a module"
  elsif !(m = %w'Request Response NS_URI' -
          ext.constants.map{ |c| c.to_s }).empty?
    raise ArgumentError, "#{ext.inspect} missing #{m*', '}"
  end

  consts = [ext::Request, ext::Response]

  if not consts.all?{|c| c.is_a? Class }
    raise TypeError, "#{ext.inspect}'s Request or Response is not a class"
  elsif not consts.all?{|c| ::OpenID::Extension > c }
    raise ArgumentError, "#{ext.inspect}'s Request or Response not a decendant of OpenID::Extension"
  end

  if not ext::NS_URI.is_a? String
    raise TypeError, "#{ext.inspect}'s NS_URI is not a string"
  elsif not uri = URI(ext::NS_URI)
    raise ArgumentError, "#{ext.inspect}'s NS_URI is not a valid uri"
  elsif not uri.scheme =~ /^https?$/
    raise ArgumentError, "#{ext.inspect}'s NS_URI is not an http uri"
  elsif not uri.absolute?
    raise ArgumentError, "#{ext.inspect}'s NS_URI is not and absolute uri"
  end
  @extensions[ext] = args
  return ext::NS_URI
end

#call(env) ⇒ Object

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.



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/gems/rack-0.9.1/lib/rack/auth/openid.rb', line 199

def call(env)
  env['rack.auth.openid'] = self
  session = env[@options[:session_key]]
  unless session and session.is_a? Hash
    raise(NoSession, 'No compatible session')
  end
  # let us work in our own namespace...
  session = (session[:openid] ||= {})
  unless session and session.is_a? Hash
    raise(NoSession, 'Incompatible session')
  end

  request = Rack::Request.new env
  consumer = ::OpenID::Consumer.new session, @options[:store]

  if request.params['openid.mode']
    finish consumer, session, request
  elsif request.params[@options[:openid_param]]
    check consumer, session, request
  else
    env['rack.errors'].puts "No valid params provided."
    bad_request
  end
rescue NoSession
  env['rack.errors'].puts($!.message, *$@)

  @options. ### Missing or incompatible session
    fetch :no_session, [ 500,
      {'Content-Type'=>'text/plain'},
      $!.message ]
rescue
  env['rack.errors'].puts($!.message, *$@)

  if not @options[:catch_error]
    raise($!)
  end
  @options.
    fetch :error, [ 500,
      {'Content-Type'=>'text/plain'},
      'OpenID has encountered an error.' ]
end

#check(consumer, session, req) ⇒ Object

As the first part of OpenID consumer action, #check retrieves the data required for completion.

  • session[:openid][:openid_param] is set to the submitted identifier to be authenticated.

  • session[:openid][:site_return] is set as the request’s HTTP_REFERER, unless already set.

  • env['rack.auth.openid.request'] is the openid checkid request instance.



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'lib/gems/rack-0.9.1/lib/rack/auth/openid.rb', line 250

def check(consumer, session, req)
  session[:openid_param]  = req.params[@options[:openid_param]]
  oid = consumer.begin(session[:openid_param], @options[:anonymous])
  pp oid if $DEBUG
  req.env['rack.auth.openid.request'] = oid

  session[:site_return] ||= req.env['HTTP_REFERER']

  # SETUP_NEEDED check!
  # see OpenID::Consumer::CheckIDRequest docs
  query_args = [@realm, *@options.values_at(:return_to, :immediate)]
  query_args[1] ||= req.url
  query_args[2] = false if session.key? :setup_needed
  pp query_args if $DEBUG

  ## Extension support
  extensions.each do |ext,args|
    oid.add_extension ext::Request.new(*args)
  end

  if oid.send_redirect?(*query_args)
    redirect = oid.redirect_url(*query_args)
    if $DEBUG
      pp redirect
      pp Rack::Utils.parse_query(URI(redirect).query)
    end
    [ 303, {'Location'=>redirect}, [] ]
  else
    # check on 'action' option.
    formbody = oid.form_markup(*query_args)
    if $DEBUG
      pp formbody
    end
    body = HTML % ['Confirm...', formbody]
    [ 200, {'Content-Type'=>'text/html'}, body.to_a ]
  end
rescue ::OpenID::DiscoveryFailure => e
  # thrown from inside OpenID::Consumer#begin by yadis stuff
  req.env['rack.errors'].puts($!.message, *$@)

  @options. ### Foreign server failed
    fetch :auth_fail, [ 503,
      {'Content-Type'=>'text/plain'},
      'Foreign server failure.' ]
end

#extension_namespacesObject

A conveniance method that returns the namespace of all current extensions used by this instance.



433
434
435
# File 'lib/gems/rack-0.9.1/lib/rack/auth/openid.rb', line 433

def extension_namespaces
  @extensions.keys.map{|e|e::NS_URI}
end

#finish(consumer, session, req) ⇒ Object

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 with the extension’s namespace uri as the key.

  • env['rack.auth.openid.response'] is the openid response.

The four valid possible outcomes are:

  • failure: options[:login_fail] or session[:site_return] or the realm

    • session[:openid] is cleared and any messages are send to rack.errors

    • session[:openid]['authenticated'] is false

  • success: options[:login_good] or session[:site_return] or the realm

    • session[:openid] is cleared

    • session[:openid]['authenticated'] is true

    • session[:openid]['identity'] is the actual identifier

    • session[:openid]['identifier'] is the pretty identifier

  • cancel: options[:login_good] or session[:site_return] or the realm

    • session[:openid] is cleared

    • session[:openid]['authenticated'] is false

  • setup_needed: resubmits the authentication request. A flag is set for non-immediate handling.

    • session[:openid][:setup_needed] is set to true, which will prevent immediate style openid authentication.



332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
# File 'lib/gems/rack-0.9.1/lib/rack/auth/openid.rb', line 332

def finish(consumer, session, req)
  oid = consumer.complete(req.params, req.url)
  pp oid if $DEBUG
  req.env['rack.auth.openid.response'] = oid

  goto = session.fetch :site_return, @realm
  body = []

  case oid.status
  when ::OpenID::Consumer::FAILURE
    session.clear
    session['authenticated'] = false
    req.env['rack.errors'].puts oid.message

    goto = @options[:login_fail] if @options.key? :login_fail
    body << "Authentication unsuccessful.\n"
  when ::OpenID::Consumer::SUCCESS
    session.clear

    ## Extension support
    extensions.each do |ext, args|
      session[ext::NS_URI] = ext::Response.
        from_success_response(oid).
        get_extension_args
    end

    session['authenticated'] = true
    # Value for unique identification and such
    session['identity'] = oid.identity_url
    # Value for display and UI labels
    session['identifier'] = oid.display_identifier

    goto = @options[:login_good] if @options.key? :login_good
    body << "Authentication successful.\n"
  when ::OpenID::Consumer::CANCEL
    session.clear
    session['authenticated'] = false

    goto = @options[:login_fail] if @options.key? :login_fail
    body << "Authentication cancelled.\n"
  when ::OpenID::Consumer::SETUP_NEEDED
    session[:setup_needed] = true
    unless o_id = session[:openid_param]
      raise('Required values missing.')
    end

    goto = req.script_name+
      '?'+@options[:openid_param]+
      '='+o_id
    body << "Reauthentication required.\n"
  end
  body << oid.message if oid.message
  [ 303, {'Location'=>goto}, body]
end