~michaelforrest/use-case-mapper/trunk

« back to all changes in this revision

Viewing changes to vendor/rails/actionpack/lib/action_controller/http_authentication.rb

  • Committer: Richard Lee (Canonical)
  • Date: 2010-10-15 15:17:58 UTC
  • mfrom: (190.1.3 use-case-mapper)
  • Revision ID: richard.lee@canonical.com-20101015151758-wcvmfxrexsongf9d
Merge

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
module ActionController
2
 
  module HttpAuthentication
3
 
    # Makes it dead easy to do HTTP Basic authentication.
4
 
    #
5
 
    # Simple Basic example:
6
 
    #
7
 
    #   class PostsController < ApplicationController
8
 
    #     USER_NAME, PASSWORD = "dhh", "secret"
9
 
    #
10
 
    #     before_filter :authenticate, :except => [ :index ]
11
 
    #
12
 
    #     def index
13
 
    #       render :text => "Everyone can see me!"
14
 
    #     end
15
 
    #
16
 
    #     def edit
17
 
    #       render :text => "I'm only accessible if you know the password"
18
 
    #     end
19
 
    #
20
 
    #     private
21
 
    #       def authenticate
22
 
    #         authenticate_or_request_with_http_basic do |user_name, password|
23
 
    #           user_name == USER_NAME && password == PASSWORD
24
 
    #         end
25
 
    #       end
26
 
    #   end
27
 
    #
28
 
    #
29
 
    # Here is a more advanced Basic example where only Atom feeds and the XML API is protected by HTTP authentication,
30
 
    # the regular HTML interface is protected by a session approach:
31
 
    #
32
 
    #   class ApplicationController < ActionController::Base
33
 
    #     before_filter :set_account, :authenticate
34
 
    #
35
 
    #     protected
36
 
    #       def set_account
37
 
    #         @account = Account.find_by_url_name(request.subdomains.first)
38
 
    #       end
39
 
    #
40
 
    #       def authenticate
41
 
    #         case request.format
42
 
    #         when Mime::XML, Mime::ATOM
43
 
    #           if user = authenticate_with_http_basic { |u, p| @account.users.authenticate(u, p) }
44
 
    #             @current_user = user
45
 
    #           else
46
 
    #             request_http_basic_authentication
47
 
    #           end
48
 
    #         else
49
 
    #           if session_authenticated?
50
 
    #             @current_user = @account.users.find(session[:authenticated][:user_id])
51
 
    #           else
52
 
    #             redirect_to(login_url) and return false
53
 
    #           end
54
 
    #         end
55
 
    #       end
56
 
    #   end
57
 
    #
58
 
    # In your integration tests, you can do something like this:
59
 
    #
60
 
    #   def test_access_granted_from_xml
61
 
    #     get(
62
 
    #       "/notes/1.xml", nil,
63
 
    #       :authorization => ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password)
64
 
    #     )
65
 
    #
66
 
    #     assert_equal 200, status
67
 
    #   end
68
 
    #
69
 
    # Simple Digest example:
70
 
    #
71
 
    #   require 'digest/md5'
72
 
    #   class PostsController < ApplicationController
73
 
    #     REALM = "SuperSecret"
74
 
    #     USERS = {"dhh" => "secret", #plain text password
75
 
    #              "dap" => Digest:MD5::hexdigest(["dap",REALM,"secret"].join(":"))  #ha1 digest password
76
 
    #
77
 
    #     before_filter :authenticate, :except => [:index]
78
 
    #
79
 
    #     def index
80
 
    #       render :text => "Everyone can see me!"
81
 
    #     end
82
 
    #
83
 
    #     def edit
84
 
    #       render :text => "I'm only accessible if you know the password"
85
 
    #     end
86
 
    #
87
 
    #     private
88
 
    #       def authenticate
89
 
    #         authenticate_or_request_with_http_digest(REALM) do |username|
90
 
    #           USERS[username]
91
 
    #         end
92
 
    #       end
93
 
    #   end
94
 
    #
95
 
    # NOTE: The +authenticate_or_request_with_http_digest+ block must return the user's password or the ha1 digest hash so the framework can appropriately
96
 
    #       hash to check the user's credentials. Returning +nil+ will cause authentication to fail.
97
 
    #       Storing the ha1 hash: MD5(username:realm:password), is better than storing a plain password. If
98
 
    #       the password file or database is compromised, the attacker would be able to use the ha1 hash to
99
 
    #       authenticate as the user at this +realm+, but would not have the user's password to try using at
100
 
    #       other sites.
101
 
    #
102
 
    # On shared hosts, Apache sometimes doesn't pass authentication headers to
103
 
    # FCGI instances. If your environment matches this description and you cannot
104
 
    # authenticate, try this rule in your Apache setup:
105
 
    #
106
 
    #   RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L]
107
 
    module Basic
108
 
      extend self
109
 
 
110
 
      module ControllerMethods
111
 
        def authenticate_or_request_with_http_basic(realm = "Application", &login_procedure)
112
 
          authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm)
113
 
        end
114
 
 
115
 
        def authenticate_with_http_basic(&login_procedure)
116
 
          HttpAuthentication::Basic.authenticate(self, &login_procedure)
117
 
        end
118
 
 
119
 
        def request_http_basic_authentication(realm = "Application")
120
 
          HttpAuthentication::Basic.authentication_request(self, realm)
121
 
        end
122
 
      end
123
 
 
124
 
      def authenticate(controller, &login_procedure)
125
 
        unless authorization(controller.request).blank?
126
 
          login_procedure.call(*user_name_and_password(controller.request))
127
 
        end
128
 
      end
129
 
 
130
 
      def user_name_and_password(request)
131
 
        decode_credentials(request).split(/:/, 2)
132
 
      end
133
 
 
134
 
      def authorization(request)
135
 
        request.env['HTTP_AUTHORIZATION']   ||
136
 
        request.env['X-HTTP_AUTHORIZATION'] ||
137
 
        request.env['X_HTTP_AUTHORIZATION'] ||
138
 
        request.env['REDIRECT_X_HTTP_AUTHORIZATION']
139
 
      end
140
 
 
141
 
      def decode_credentials(request)
142
 
        ActiveSupport::Base64.decode64(authorization(request).split(' ', 2).last || '')
143
 
      end
144
 
 
145
 
      def encode_credentials(user_name, password)
146
 
        "Basic #{ActiveSupport::Base64.encode64("#{user_name}:#{password}")}"
147
 
      end
148
 
 
149
 
      def authentication_request(controller, realm)
150
 
        controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.gsub(/"/, "")}")
151
 
        controller.__send__ :render, :text => "HTTP Basic: Access denied.\n", :status => :unauthorized
152
 
      end
153
 
    end
154
 
 
155
 
    module Digest
156
 
      extend self
157
 
 
158
 
      module ControllerMethods
159
 
        def authenticate_or_request_with_http_digest(realm = "Application", &password_procedure)
160
 
          authenticate_with_http_digest(realm, &password_procedure) || request_http_digest_authentication(realm)
161
 
        end
162
 
 
163
 
        # Authenticate with HTTP Digest, returns true or false
164
 
        def authenticate_with_http_digest(realm = "Application", &password_procedure)
165
 
          HttpAuthentication::Digest.authenticate(self, realm, &password_procedure)
166
 
        end
167
 
 
168
 
        # Render output including the HTTP Digest authentication header
169
 
        def request_http_digest_authentication(realm = "Application", message = nil)
170
 
          HttpAuthentication::Digest.authentication_request(self, realm, message)
171
 
        end
172
 
      end
173
 
 
174
 
      # Returns false on a valid response, true otherwise
175
 
      def authenticate(controller, realm, &password_procedure)
176
 
        authorization(controller.request) && validate_digest_response(controller.request, realm, &password_procedure)
177
 
      end
178
 
 
179
 
      def authorization(request)
180
 
        request.env['HTTP_AUTHORIZATION']   ||
181
 
        request.env['X-HTTP_AUTHORIZATION'] ||
182
 
        request.env['X_HTTP_AUTHORIZATION'] ||
183
 
        request.env['REDIRECT_X_HTTP_AUTHORIZATION']
184
 
      end
185
 
 
186
 
      # Returns false unless the request credentials response value matches the expected value.
187
 
      # First try the password as a ha1 digest password. If this fails, then try it as a plain
188
 
      # text password.
189
 
      def validate_digest_response(request, realm, &password_procedure)
190
 
        credentials = decode_credentials_header(request)
191
 
        valid_nonce = validate_nonce(request, credentials[:nonce])
192
 
 
193
 
        if valid_nonce && realm == credentials[:realm] && opaque == credentials[:opaque]
194
 
          password = password_procedure.call(credentials[:username])
195
 
          return false unless password
196
 
 
197
 
          method = request.env['rack.methodoverride.original_method'] || request.env['REQUEST_METHOD']
198
 
          uri    = credentials[:uri][0,1] == '/' ? request.request_uri : request.url
199
 
 
200
 
         [true, false].any? do |password_is_ha1|
201
 
           expected = expected_response(method, uri, credentials, password, password_is_ha1)
202
 
           expected == credentials[:response]
203
 
         end
204
 
        end
205
 
      end
206
 
 
207
 
      # Returns the expected response for a request of +http_method+ to +uri+ with the decoded +credentials+ and the expected +password+
208
 
      # Optional parameter +password_is_ha1+ is set to +true+ by default, since best practice is to store ha1 digest instead
209
 
      # of a plain-text password.
210
 
      def expected_response(http_method, uri, credentials, password, password_is_ha1=true)
211
 
        ha1 = password_is_ha1 ? password : ha1(credentials, password)
212
 
        ha2 = ::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(':'))
213
 
        ::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(':'))
214
 
      end
215
 
 
216
 
      def ha1(credentials, password)
217
 
        ::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(':'))
218
 
      end
219
 
 
220
 
      def encode_credentials(http_method, credentials, password, password_is_ha1)
221
 
        credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password, password_is_ha1)
222
 
        "Digest " + credentials.sort_by {|x| x[0].to_s }.inject([]) {|a, v| a << "#{v[0]}='#{v[1]}'" }.join(', ')
223
 
      end
224
 
 
225
 
      def decode_credentials_header(request)
226
 
        decode_credentials(authorization(request))
227
 
      end
228
 
 
229
 
      def decode_credentials(header)
230
 
        header.to_s.gsub(/^Digest\s+/,'').split(',').inject({}.with_indifferent_access) do |hash, pair|
231
 
          key, value = pair.split('=', 2)
232
 
          hash[key.strip] = value.to_s.gsub(/^"|"$/,'').gsub(/'/, '')
233
 
          hash
234
 
        end
235
 
      end
236
 
 
237
 
      def authentication_header(controller, realm)
238
 
        controller.headers["WWW-Authenticate"] = %(Digest realm="#{realm}", qop="auth", algorithm=MD5, nonce="#{nonce}", opaque="#{opaque}")
239
 
      end
240
 
 
241
 
      def authentication_request(controller, realm, message = nil)
242
 
        message ||= "HTTP Digest: Access denied.\n"
243
 
        authentication_header(controller, realm)
244
 
        controller.__send__ :render, :text => message, :status => :unauthorized
245
 
      end
246
 
 
247
 
      # Uses an MD5 digest based on time to generate a value to be used only once.
248
 
      #
249
 
      # A server-specified data string which should be uniquely generated each time a 401 response is made.
250
 
      # It is recommended that this string be base64 or hexadecimal data.
251
 
      # Specifically, since the string is passed in the header lines as a quoted string, the double-quote character is not allowed.
252
 
      #
253
 
      # The contents of the nonce are implementation dependent.
254
 
      # The quality of the implementation depends on a good choice.
255
 
      # A nonce might, for example, be constructed as the base 64 encoding of
256
 
      #
257
 
      # => time-stamp H(time-stamp ":" ETag ":" private-key)
258
 
      #
259
 
      # where time-stamp is a server-generated time or other non-repeating value,
260
 
      # ETag is the value of the HTTP ETag header associated with the requested entity,
261
 
      # and private-key is data known only to the server.
262
 
      # With a nonce of this form a server would recalculate the hash portion after receiving the client authentication header and
263
 
      # reject the request if it did not match the nonce from that header or
264
 
      # if the time-stamp value is not recent enough. In this way the server can limit the time of the nonce's validity.
265
 
      # The inclusion of the ETag prevents a replay request for an updated version of the resource.
266
 
      # (Note: including the IP address of the client in the nonce would appear to offer the server the ability
267
 
      # to limit the reuse of the nonce to the same client that originally got it.
268
 
      # However, that would break proxy farms, where requests from a single user often go through different proxies in the farm.
269
 
      # Also, IP address spoofing is not that hard.)
270
 
      #
271
 
      # An implementation might choose not to accept a previously used nonce or a previously used digest, in order to
272
 
      # protect against a replay attack. Or, an implementation might choose to use one-time nonces or digests for
273
 
      # POST or PUT requests and a time-stamp for GET requests. For more details on the issues involved see Section 4
274
 
      # of this document.
275
 
      #
276
 
      # The nonce is opaque to the client. Composed of Time, and hash of Time with secret
277
 
      # key from the Rails session secret generated upon creation of project. Ensures
278
 
      # the time cannot be modifed by client.
279
 
      def nonce(time = Time.now)
280
 
        t = time.to_i
281
 
        hashed = [t, secret_key]
282
 
        digest = ::Digest::MD5.hexdigest(hashed.join(":"))
283
 
        Base64.encode64("#{t}:#{digest}").gsub("\n", '')
284
 
      end
285
 
 
286
 
      # Might want a shorter timeout depending on whether the request
287
 
      # is a PUT or POST, and if client is browser or web service.
288
 
      # Can be much shorter if the Stale directive is implemented. This would
289
 
      # allow a user to use new nonce without prompting user again for their
290
 
      # username and password.
291
 
      def validate_nonce(request, value, seconds_to_timeout=5*60)
292
 
        return false if value.nil?
293
 
        t = Base64.decode64(value).split(":").first.to_i
294
 
        nonce(t) == value && (t - Time.now.to_i).abs <= seconds_to_timeout
295
 
      end
296
 
 
297
 
      # Opaque based on random generation - but changing each request?
298
 
      def opaque()
299
 
        ::Digest::MD5.hexdigest(secret_key)
300
 
      end
301
 
 
302
 
      # Set in /initializers/session_store.rb, and loaded even if sessions are not in use.
303
 
      def secret_key
304
 
        ActionController::Base.session_options[:secret]
305
 
      end
306
 
 
307
 
    end
308
 
  end
309
 
end