Class: Rack::Auth::OpenID

Inherits:
Object
  • Object
show all
Defined in:
lib/rack/auth/openid.rb

Overview

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.

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

Defined Under Namespace

Classes: BadExtension, NoSession

Constant Summary collapse

ValidStatus =

Possible statuses returned from consumer responses. See definitions in the ruby-openid library.

[
  ::OpenID::Consumer::SUCCESS,
  ::OpenID::Consumer::FAILURE,
  ::OpenID::Consumer::CANCEL,
  ::OpenID::Consumer::SETUP_NEEDED
]

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

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

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 lits of acceptable options include :return_to, :session_key, :openid_param, :store, :immediate, :extensions.

: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 unprovided, the url of the current request is used.

:session_key defines the key to the session hash in the env. The default is ‘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 is 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 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.

Raises:

  • (ArgumentError)


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
181
182
183
184
185
186
187
188
189
190
# File 'lib/rack/auth/openid.rb', line 155

def initialize(realm, options={})
  realm = URI(realm)
  raise ArgumentError, "Invalid realm: #{realm}" \
    unless realm.absolute? \
    and realm.fragment.nil? \
    and realm.scheme =~ /^https?$/ \
    and realm.host =~ /^(\*\.)?#{URI::REGEXP::PATTERN::URIC_NO_SLASH}+/
  realm.path = '/' if realm.path.empty?
  @realm = realm.to_s

  if ruri = options[:return_to]
    ruri = URI(ruri)
    raise ArgumentError, "Invalid return_to: #{ruri}" \
      unless ruri.absolute? \
      and ruri.scheme =~ /^https?$/ \
      and ruri.fragment.nil?
    raise ArgumentError, "return_to #{ruri} not within realm #{realm}" \
      unless self.within_realm?(ruri)
    @return_to = ruri.to_s
  end

  @session_key  = options[:session_key]   || 'rack.session'
  @openid_param = options[:openid_param]  || 'openid_identifier'
  @store        = options[:store]         || ::OpenID::Store::Memory.new
  @immediate    = !!options[:immediate]

  @extensions   = {}
  if extensions = options[:extensions]
    extensions.each do |ext, args|
      add_extension(ext, *args)
    end
  end

  # Undocumented, semi-experimental
  @anonymous    = !!options[:anonymous]
end

Instance Attribute Details

#extensionsObject (readonly)

Returns the value of attribute extensions.



192
193
194
# File 'lib/rack/auth/openid.rb', line 192

def extensions
  @extensions
end

#immediateObject (readonly)

Returns the value of attribute immediate.



192
193
194
# File 'lib/rack/auth/openid.rb', line 192

def immediate
  @immediate
end

#openid_paramObject (readonly)

Returns the value of attribute openid_param.



192
193
194
# File 'lib/rack/auth/openid.rb', line 192

def openid_param
  @openid_param
end

#realmObject (readonly)

Returns the value of attribute realm.



192
193
194
# File 'lib/rack/auth/openid.rb', line 192

def realm
  @realm
end

#return_toObject (readonly)

Returns the value of attribute return_to.



192
193
194
# File 'lib/rack/auth/openid.rb', line 192

def return_to
  @return_to
end

#session_keyObject (readonly)

Returns the value of attribute session_key.



192
193
194
# File 'lib/rack/auth/openid.rb', line 192

def session_key
  @session_key
end

#storeObject (readonly)

Returns the value of attribute store.



192
193
194
# File 'lib/rack/auth/openid.rb', line 192

def store
  @store
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, 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, oid#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.

Raises:



314
315
316
317
318
# File 'lib/rack/auth/openid.rb', line 314

def add_extension(ext, *args)
  raise BadExtension unless valid_extension?(ext)
  extensions[ext] = args
  return ext::NS_URI
end

#call(env) ⇒ Object

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, #bad_request is called.



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/rack/auth/openid.rb', line 208

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

  request = Rack::Request.new(env)
  consumer = ::OpenID::Consumer.new(session, @store)

  if mode = request.GET['openid.mode']
    finish(consumer, session, request)
  elsif request.GET[@openid_param]
    check(consumer, session, request)
  else
    bad_request
  end
end

#check(consumer, session, req) ⇒ Object

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['rack.auth.openid.request'] 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.



249
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
# File 'lib/rack/auth/openid.rb', line 249

def check(consumer, session, req)
  oid = consumer.begin(req.GET[@openid_param], @anonymous)
  req.env['rack.auth.openid.request'] = oid
  req.env['rack.errors'].puts(oid.message)
  p oid if $DEBUG

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

  session[:openid_param] = req.GET[openid_param]
  return_to_uri = return_to ? return_to : req.url
  session[:return_to] = return_to_uri
  immediate = session.key?(:setup_needed) ? false : immediate

  if oid.send_redirect?(realm, return_to_uri, immediate)
    redirect(oid.redirect_url(realm, return_to_uri, immediate))
  else
    confirm_post_params(oid, realm, return_to_uri, immediate)
  end
rescue ::OpenID::DiscoveryFailure => e
  # thrown from inside OpenID::Consumer#begin by yadis stuff
  req.env['rack.errors'].puts( [e.message, *e.backtrace]*"\n" )
  return foreign_server_failure
end

#finish(consumer, session, req) ⇒ Object

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

Any messages from OpenID’s response are logged to env

env['rack.auth.openid.response'] will contain the openid response.



286
287
288
289
290
291
292
293
294
295
296
297
# File 'lib/rack/auth/openid.rb', line 286

def finish(consumer, session, req)
  oid = consumer.complete(req.GET, req.url)
  req.env['rack.auth.openid.response'] = oid
  req.env['rack.errors'].puts(oid.message)
  p oid if $DEBUG

  if ValidStatus.include?(oid.status)
    __send__(oid.status, oid, req, session)
  else
    invalid_status(oid, req, session)
  end
end

#valid_extension?(ext) ⇒ Boolean

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

Returns:

  • (Boolean)


323
324
325
326
327
328
329
330
331
332
# File 'lib/rack/auth/openid.rb', line 323

def valid_extension?(ext)
  if not %w[NS_URI Request Response].all?{|c| ext.const_defined?(c) }
    raise ArgumentError, 'Extension is missing constants.'
  elsif not ext::Response.respond_to?(:from_success_response)
    raise ArgumentError, 'Response is missing required method.'
  end
  return true
rescue
  return false
end

#within_realm?(uri) ⇒ Boolean Also known as: include?

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

Returns:

  • (Boolean)


337
338
339
340
341
342
343
344
345
346
347
348
# File 'lib/rack/auth/openid.rb', line 337

def within_realm? uri
  uri = URI.parse(uri.to_s)
  realm = URI.parse(self.realm)
  return false unless uri.absolute?
  return false unless uri.path[0, realm.path.size] == realm.path
  return false unless uri.host == realm.host or realm.host[/^\*\./]
  # for wildcard support, is awkward with URI limitations
  realm_match = Regexp.escape(realm.host).
    sub(/^\*\./,"^#{URI::REGEXP::PATTERN::URIC_NO_SLASH}+.")+'$'
  return false unless uri.host.match(realm_match)
  return true
end