aboutsummaryrefslogblamecommitdiffstats
path: root/actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth/openid.rb
blob: 2bd064ea4a5f736dc1f95d203e7a2b7454ddb79a (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
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
240
241
242
243
244
245
246
247
248
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
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
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
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
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
430
431
432
433
434
435
436
437




















































































































































































































































































































































































































































                                                                                                          
# AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net

gem 'ruby-openid', '~> 2' if defined? Gem
require 'rack/auth/abstract/handler' #rack
require 'uri' #std
require 'pp' #std
require 'openid' #gem
require 'openid/extension' #gem
require 'openid/store/memory' #gem

module Rack
  module Auth
    # 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 http://openidenabled.com/ruby-openid/.
    #
    # The OpenID specifications can be found at
    # http://openid.net/specs/openid-authentication-1_1.html
    # and
    # http://openid.net/specs/openid-authentication-2_0.html. Documentation
    # for published OpenID extensions and related topics can be found at
    # http://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.
    class OpenID < AbstractHandler
      class NoSession < RuntimeError; end
      # Required for ruby-openid
      OIDStore = ::OpenID::Store::Memory.new
      HTML = '<html><head><title>%s</title></head><body>%s</body></html>'

      # 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
      #
      # <tt>:return_to</tt> 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 <tt>:return_to</tt> is not
      # provided, :return_to will be the current url including all query
      # parameters.
      #
      # <tt>:session_key</tt> defines the key to the session hash in the env.
      # It defaults to 'rack.session'.
      #
      # <tt>:openid_param</tt> 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'.
      #
      # <tt>:immediate</tt> as true will make immediate type of requests the
      # default. See OpenID specification documentation.
      #
      # === URL options
      #
      # <tt>:login_good</tt> is the url to go to after the authentication
      # process has completed.
      #
      # <tt>:login_fail</tt> is the url to go to after the authentication
      # process has failed.
      #
      # <tt>:login_quit</tt> is the url to go to after the authentication
      # process
      # has been cancelled.
      #
      # === Response options
      #
      # <tt>:no_session</tt> should be a rack response to be returned if no or
      # an incompatible session is found.
      #
      # <tt>:auth_fail</tt> 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.
      #
      # <tt>:error</tt> should be a rack response to return if any other
      # generic error would occur and <tt>options[:catch_errors]</tt> is true.
      #
      # === Extensions
      #
      # <tt>:extensions</tt> 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.
      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

      attr_reader :options, :extensions

      # It sets up and uses session data at <tt>:openid</tt> within the
      # session. It sets up the ::OpenID::Consumer using the store specified by
      # <tt>options[:store]</tt>.
      #
      # If the parameter specified by <tt>options[:openid_param]</tt> 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 <tt>options[:catch_errors]</tt> is false, the
      # exception will be reraised. Otherwise a 500 error is returned.
      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

      # As the first part of OpenID consumer action, #check retrieves the data
      # required for completion.
      #
      # * <tt>session[:openid][:openid_param]</tt> is set to the submitted
      #   identifier to be authenticated.
      # * <tt>session[:openid][:site_return]</tt> is set as the request's
      #   HTTP_REFERER, unless already set.
      # * <tt>env['rack.auth.openid.request']</tt> is the openid checkid
      #   request instance.
      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

      # 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
      # <tt>session[:openid][:site_return]</tt>. If
      # <tt>session[:openid][:site_return]</tt> 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.
      #
      # * <tt>env['rack.auth.openid.response']</tt> is the openid response.
      #
      # The four valid possible outcomes are:
      # * failure: <tt>options[:login_fail]</tt> or
      #   <tt>session[:site_return]</tt> or the realm
      #   * <tt>session[:openid]</tt> is cleared and any messages are send to
      #     rack.errors
      #   * <tt>session[:openid]['authenticated']</tt> is <tt>false</tt>
      # * success: <tt>options[:login_good]</tt> or
      #   <tt>session[:site_return]</tt> or the realm
      #   * <tt>session[:openid]</tt> is cleared
      #   * <tt>session[:openid]['authenticated']</tt> is <tt>true</tt>
      #   * <tt>session[:openid]['identity']</tt> is the actual identifier
      #   * <tt>session[:openid]['identifier']</tt> is the pretty identifier
      # * cancel: <tt>options[:login_good]</tt> or
      #   <tt>session[:site_return]</tt> or the realm
      #   * <tt>session[:openid]</tt> is cleared
      #   * <tt>session[:openid]['authenticated']</tt> is <tt>false</tt>
      # * setup_needed: resubmits the authentication request. A flag is set for
      #   non-immediate handling.
      #   * <tt>session[:openid][:setup_needed]</tt> is set to <tt>true</tt>,
      #     which will prevent immediate style openid authentication.
      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 @option.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 @option.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

      # 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.
      def add_extension ext, *args
        if not ext.is_a? Module
          raise TypeError, "#{ext.inspect} is not a module"
        elsif not (m = %w'Request Response NS_URI' - ext.constants).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

      # A conveniance method that returns the namespace of all current
      # extensions used by this instance.
      def extension_namespaces
        @extensions.keys.map{|e|e::NS_URI}
      end
    end
  end
end