Parent

Rack::Auth::OpenID

Rack::Auth::OpenID provides a simple method for setting up an OpenID Consumer. 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 Consumer functionality.

This library strongly intends to utilize the OpenID 2.0 features of the ruby-openid library, which provides OpenID 1.0 compatiblity.

NOTE: Due to the amount of data that this library stores in the session, Rack::Session::Cookie may fault.

Constants

ValidStatus

Required for ruby-openid

Attributes

realm[R]

(Not documented)

return_to[R]

(Not documented)

session_key[R]

(Not documented)

openid_param[R]

(Not documented)

store[R]

(Not documented)

immediate[R]

(Not documented)

extensions[R]

(Not documented)

Public Class Methods

new(realm, options={}) click to toggle source

Arguments

The first argument is the realm, identifying the site they are trusting with their identity. This is required, also treated as the trust_root in OpenID 1.x exchanges.

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 which allows flexibility with caveats.

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

:store defined what OpenID Store to use for persistant information. By default a Store::Memory will be used.

:immediate as true will make initial requests to be of an immediate type. This is false by default. See OpenID specification documentation.

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

Examples

  simple_oid = OpenID.new('http://mysite.com/')

  return_oid = OpenID.new('http://mysite.com/', {
    :return_to => 'http://mysite.com/openid'
  })

  complex_oid = OpenID.new('http://mysite.com/',
    :immediate => true,
    :extensions => {
      ::OpenID::SReg => [['email'],['nickname']]
    }
  )

Advanced

Most of the functionality of this library is encapsulated such that expansion and overriding functions isn’t difficult nor tricky. Alternately, to avoid opening up singleton objects or subclassing, a wrapper rack middleware can be composed to act upon Auth::OpenID’s responses. See check and finish for locations of pertinent data.

Responses

To change the responses that Auth::OpenID returns, override the methods redirect, bad_request, unauthorized, access_denied, and foreign_server_failure.

Additionally confirm_post_params is used when the URI would exceed length limits on a GET request when doing the initial verification request.

Processing

To change methods of processing completed transactions, override the methods success, setup_needed, cancel, and failure. Please ensure the returned object is a rack compatible response.

The first argument is an OpenID::Response, the second is a Rack::Request of the current request, the last is the hash used in ruby-openid handling, which can be found manually at env[:openid].

This is useful if you wanted to expand the processing done, such as setting up user accounts.

  oid_app = Rack::Auth::OpenID.new realm, :return_to => return_to
  def oid_app.success oid, request, session
    user = Models::User[oid.identity_url]
    user ||= Models::User.create_from_openid oid
    request['rack.session'][:user] = user.id
    redirect MyApp.site_home
  end

  site_map['/openid'] = oid_app
  map = Rack::URLMap.new site_map
  ...
     # File lib/rack/auth/openid.rb, line 151
151:       def initialize(realm, options={})
152:         realm = URI(realm)
153:         raise ArgumentError, "Invalid realm: #{realm}" \
154:           unless realm.absolute? \
155:           and realm.fragment.nil? \
156:           and realm.scheme =~ /^https?$/ \
157:           and realm.host =~ /^(\*\.)?#{URI::REGEXP::PATTERN::URIC_NO_SLASH}+/
158:         realm.path = '/' if realm.path.empty?
159:         @realm = realm.to_s
160: 
161:         if ruri = options[:return_to]
162:           ruri = URI(ruri)
163:           raise ArgumentError, "Invalid return_to: #{ruri}" \
164:             unless ruri.absolute? \
165:             and ruri.scheme  =~ /^https?$/ \
166:             and ruri.fragment.nil?
167:           raise ArgumentError, "return_to #{ruri} not within realm #{realm}" \
168:             unless self.within_realm?(ruri)
169:           @return_to = ruri.to_s
170:         end
171: 
172:         @session_key  = options[:session_key]   || 'rack.session'
173:         @openid_param = options[:openid_param]  || 'openid_identifier'
174:         @store        = options[:store]         || ::OpenID::Store::Memory.new
175:         @immediate    = !!options[:immediate]
176: 
177:         @extensions = {}
178:         if extensions = options.delete(:extensions)
179:           extensions.each do |ext, args|
180:             add_extension ext, *args
181:           end
182:         end
183: 
184:         # Undocumented, semi-experimental
185:         @anonymous    = !!options[:anonymous]
186:       end

Public Instance Methods

add_extension(ext, *args) click to toggle source

The first argument should be the main extension module. The extension module should contain the constants:

  * class Request, should have OpenID::Extension as an ancestor
  * class Response, should have OpenID::Extension as an ancestor
  * string NS_URI, which defining the namespace of the extension

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 312
312:       def add_extension(ext, *args)
313:         raise BadExtension unless valid_extension?(ext)
314:         extensions[ext] = args
315:         return ext::NS_URI
316:       end
call(env) click to toggle source

Sets up and uses session data at :openid within the session. Errors in this setup will raise a NoSession exception.

If the parameter ‘openid.mode’ is set, which implies a followup from the openid server, processing is passed to finish and the result is returned. However, if there is no appropriate openid information in the session, a 400 error is returned.

If the parameter specified by options[:openid_param] is present, processing is passed to check and the result is returned.

If neither of these conditions are met, unauthorized is called.

     # File lib/rack/auth/openid.rb, line 204
204:       def call(env)
205:         env['rack.auth.openid'] = self
206:         env_session = env[@session_key]
207:         unless env_session and env_session.is_a?(Hash)
208:           raise NoSession, 'No compatible session'
209:         end
210:         # let us work in our own namespace...
211:         session = (env_session[:openid] ||= {})
212:         unless session and session.is_a?(Hash)
213:           raise NoSession, 'Incompatible openid session'
214:         end
215: 
216:         request = Rack::Request.new(env)
217:         consumer = ::OpenID::Consumer.new(session, @store)
218: 
219:         if mode = request.GET['openid.mode']
220:           if session.key?(:openid_param)
221:             finish(consumer, session, request)
222:           else
223:             bad_request
224:           end
225:         elsif request.GET[@openid_param]
226:           check(consumer, session, request)
227:         else
228:           unauthorized
229:         end
230:       end
check(consumer, session, req) click to toggle source

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

If all parameters fit within the max length of a URI, a 303 redirect will be returned. Otherwise confirm_post_params will be called.

Any messages from OpenID’s request are logged to env

env is the openid checkid request instance.

session[:openid_param] is set to the openid identifier provided by the user.

session[:return_to] is set to the return_to uri given to the identity provider.

     # File lib/rack/auth/openid.rb, line 249
249:       def check(consumer, session, req)
250:         oid = consumer.begin(req.GET[@openid_param], @anonymous)
251:         req.env['rack.auth.openid.request'] = oid
252:         req.env['rack.errors'].puts(oid.message)
253:         p oid if $DEBUG
254: 
255:         ## Extension support
256:         extensions.each do |ext,args|
257:           oid.add_extension(ext::Request.new(*args))
258:         end
259: 
260:         session[:openid_param] = req.GET[openid_param]
261:         return_to_uri = return_to ? return_to : req.url
262:         session[:return_to] = return_to_uri
263:         immediate = session.key?(:setup_needed) ? false : immediate
264: 
265:         if oid.send_redirect?(realm, return_to_uri, immediate)
266:           uri = oid.redirect_url(realm, return_to_uri, immediate)
267:           redirect(uri)
268:         else
269:           confirm_post_params(oid, realm, return_to_uri, immediate)
270:         end
271:       rescue ::OpenID::DiscoveryFailure => e
272:         # thrown from inside OpenID::Consumer#begin by yadis stuff
273:         req.env['rack.errors'].puts([e.message, *e.backtrace]*"\n")
274:         return foreign_server_failure
275:       end
finish(consumer, session, req) click to toggle source

This is the final portion of authentication. If successful, a redirect to the realm is be returned. Data gathered from extensions are stored in session[:openid] with the extension’s namespace uri as the key.

Any messages from OpenID’s response are logged to env

env will contain the openid response.

     # File lib/rack/auth/openid.rb, line 287
287:       def finish(consumer, session, req)
288:         oid = consumer.complete(req.GET, req.url)
289:         req.env['rack.auth.openid.response'] = oid
290:         req.env['rack.errors'].puts(oid.message)
291:         p oid if $DEBUG
292: 
293:         raise unless ValidStatus.include?(oid.status)
294:         __send__(oid.status, oid, req, session)
295:       end
include?(uri) click to toggle source

Alias for within_realm?

valid_extension?(ext) click to toggle source

Checks the validitity, in the context of usage, of a submitted extension.

     # File lib/rack/auth/openid.rb, line 321
321:       def valid_extension?(ext)
322:         if not %w[NS_URI Request Response].all?{|c| ext.const_defined?(c) }
323:           raise ArgumentError, 'Extension is missing constants.'
324:         elsif not ext::Response.respond_to?(:from_success_response)
325:           raise ArgumentError, 'Response is missing required method.'
326:         end
327:         return true
328:       rescue
329:         return false
330:       end
within_realm?(uri) click to toggle source

Checks the provided uri to ensure it’d be considered within the realm. is currently not compatible with wildcard realms.

     # File lib/rack/auth/openid.rb, line 335
335:       def within_realm? uri
336:         uri = URI.parse(uri.to_s)
337:         realm = URI.parse(self.realm)
338:         return false unless uri.absolute?
339:         return false unless uri.path[0, realm.path.size] == realm.path
340:         return false unless uri.host == realm.host or realm.host[/^\*\./]
341:         # for wildcard support, is awkward with URI limitations
342:         realm_match = Regexp.escape(realm.host).
343:           sub(/^\*\./,"^#{URI::REGEXP::PATTERN::URIC_NO_SLASH}+.")+'$'
344:         return false unless uri.host.match(realm_match)
345:         return true
346:       end
Also aliased as: include?

Protected Instance Methods

access_denied() click to toggle source

Returns a basic access denied 403 response.

     # File lib/rack/auth/openid.rb, line 389
389:       def access_denied
390:         [ 403, {'Content-Type' => 'text/plain', 'Content-Length' => '14'},
391:           ['Access denied.'] ]
392:       end
bad_request() click to toggle source

Returns an empty 400 response.

     # File lib/rack/auth/openid.rb, line 375
375:       def bad_request
376:         [ 400, {'Content-Type'=>'text/plain', 'Content-Length'=>'0'},
377:           [''] ]
378:       end
confirm_post_params(oid, realm, return_to, immediate) click to toggle source

Returns an html form page for posting to an Identity Provider if the GET request would exceed the upper URI length limit.

     # File lib/rack/auth/openid.rb, line 356
356:       def confirm_post_params(oid, realm, return_to, immediate)
357:         Rack::Response.new.finish do |r|
358:           r.write '<html><head><title>Confirm...</title></head><body>'
359:           r.write oid.form_markup(realm, return_to, immediate)
360:           r.write '</body></html>'
361:         end
362:       end
foreign_server_failure() click to toggle source

Returns a 503 response to be used if communication with the remote OpenID server fails.

     # File lib/rack/auth/openid.rb, line 397
397:       def foreign_server_failure
398:         [ 503, {'Content-Type'=>'text/plain', 'Content-Length' => '23'},
399:           ['Foreign server failure.'] ]
400:       end
redirect(uri) click to toggle source

Returns a 303 redirect with the destination of that provided by the argument.

     # File lib/rack/auth/openid.rb, line 367
367:       def redirect(uri)
368:         [ 303, {'Content-Length'=>'0', 'Content-Type'=>'text/plain',
369:           'Location' => uri},
370:           [] ]
371:       end
unauthorized() click to toggle source

Returns a basic unauthorized 401 response.

     # File lib/rack/auth/openid.rb, line 382
382:       def unauthorized
383:         [ 401, {'Content-Type' => 'text/plain', 'Content-Length' => '13'},
384:           ['Unauthorized.'] ]
385:       end

Private Instance Methods

cancel(oid, request, session) click to toggle source

Called if the user indicates they wish to cancel identification. Data within openid session is cleared.

     # File lib/rack/auth/openid.rb, line 439
439:       def cancel(oid, request, session)
440:         session.clear
441:         access_denied
442:       end
failure(oid, request, session) click to toggle source

Called if the Identity Provider indicates the user is unable to confirm their identity. Data within the openid session is left alone, in case of swarm auth attacks.

     # File lib/rack/auth/openid.rb, line 448
448:       def failure(oid, request, session)
449:         unauthorized
450:       end
setup_needed(oid, request, session) click to toggle source

Called if the Identity Provider indicates further setup by the user is required. The identifier is retrived from the openid session at :openid_param. And :setup_needed is set to true to prevent looping.

     # File lib/rack/auth/openid.rb, line 430
430:       def setup_needed(oid, request, session)
431:         identifier = session[:openid_param]
432:         session[:setup_needed] = true
433:         redirect req.script_name + '?' + openid_param + '=' + identifier
434:       end
success(oid, request, session) click to toggle source

Called to complete processing on a successful transaction. Within the openid session, :openid_identity and :openid_identifier are set to the user friendly and the standard representation of the validated identity. All other data in the openid session is cleared.

     # File lib/rack/auth/openid.rb, line 413
413:       def success(oid, request, session)
414:         session.clear
415:         session[:openid_identity]   = oid.display_identifier
416:         session[:openid_identifier] = oid.identity_url
417:         extensions.keys.each do |ext|
418:           label     = ext.name[/[^:]+$/].downcase
419:           response  = ext::Response.from_success_response(oid)
420:           session[label] = response.data
421:         end
422:         redirect(realm)
423:       end

Disabled; run with --debug to generate this.

[Validate]

Generated with the Darkfish Rdoc Generator 1.1.6.