1 | #
|
---|
2 | # httpauth/digestauth.rb -- HTTP digest access authentication
|
---|
3 | #
|
---|
4 | # Author: IPR -- Internet Programming with Ruby -- writers
|
---|
5 | # Copyright (c) 2003 Internet Programming with Ruby writers.
|
---|
6 | # Copyright (c) 2003 H.M.
|
---|
7 | #
|
---|
8 | # The original implementation is provided by H.M.
|
---|
9 | # URL: http://rwiki.jin.gr.jp/cgi-bin/rw-cgi.rb?cmd=view;name=
|
---|
10 | # %C7%A7%BE%DA%B5%A1%C7%BD%A4%F2%B2%FE%C2%A4%A4%B7%A4%C6%A4%DF%A4%EB
|
---|
11 | #
|
---|
12 | # $IPR: digestauth.rb,v 1.5 2003/02/20 07:15:47 gotoyuzo Exp $
|
---|
13 |
|
---|
14 | require 'webrick/config'
|
---|
15 | require 'webrick/httpstatus'
|
---|
16 | require 'webrick/httpauth/authenticator'
|
---|
17 | require 'digest/md5'
|
---|
18 | require 'digest/sha1'
|
---|
19 |
|
---|
20 | module WEBrick
|
---|
21 | module HTTPAuth
|
---|
22 | class DigestAuth
|
---|
23 | include Authenticator
|
---|
24 |
|
---|
25 | AuthScheme = "Digest"
|
---|
26 | OpaqueInfo = Struct.new(:time, :nonce, :nc)
|
---|
27 | attr_reader :algorithm, :qop
|
---|
28 |
|
---|
29 | def self.make_passwd(realm, user, pass)
|
---|
30 | pass ||= ""
|
---|
31 | Digest::MD5::hexdigest([user, realm, pass].join(":"))
|
---|
32 | end
|
---|
33 |
|
---|
34 | def initialize(config, default=Config::DigestAuth)
|
---|
35 | check_init(config)
|
---|
36 | @config = default.dup.update(config)
|
---|
37 | @algorithm = @config[:Algorithm]
|
---|
38 | @domain = @config[:Domain]
|
---|
39 | @qop = @config[:Qop]
|
---|
40 | @use_opaque = @config[:UseOpaque]
|
---|
41 | @use_next_nonce = @config[:UseNextNonce]
|
---|
42 | @check_nc = @config[:CheckNc]
|
---|
43 | @use_auth_info_header = @config[:UseAuthenticationInfoHeader]
|
---|
44 | @nonce_expire_period = @config[:NonceExpirePeriod]
|
---|
45 | @nonce_expire_delta = @config[:NonceExpireDelta]
|
---|
46 | @internet_explorer_hack = @config[:InternetExplorerHack]
|
---|
47 | @opera_hack = @config[:OperaHack]
|
---|
48 |
|
---|
49 | case @algorithm
|
---|
50 | when 'MD5','MD5-sess'
|
---|
51 | @h = Digest::MD5
|
---|
52 | when 'SHA1','SHA1-sess' # it is a bonus feature :-)
|
---|
53 | @h = Digest::SHA1
|
---|
54 | else
|
---|
55 | msg = format('Alogrithm "%s" is not supported.', @algorithm)
|
---|
56 | raise ArgumentError.new(msg)
|
---|
57 | end
|
---|
58 |
|
---|
59 | @instance_key = hexdigest(self.__id__, Time.now.to_i, Process.pid)
|
---|
60 | @opaques = {}
|
---|
61 | @last_nonce_expire = Time.now
|
---|
62 | @mutex = Mutex.new
|
---|
63 | end
|
---|
64 |
|
---|
65 | def authenticate(req, res)
|
---|
66 | unless result = @mutex.synchronize{ _authenticate(req, res) }
|
---|
67 | challenge(req, res)
|
---|
68 | end
|
---|
69 | if result == :nonce_is_stale
|
---|
70 | challenge(req, res, true)
|
---|
71 | end
|
---|
72 | return true
|
---|
73 | end
|
---|
74 |
|
---|
75 | def challenge(req, res, stale=false)
|
---|
76 | nonce = generate_next_nonce(req)
|
---|
77 | if @use_opaque
|
---|
78 | opaque = generate_opaque(req)
|
---|
79 | @opaques[opaque].nonce = nonce
|
---|
80 | end
|
---|
81 |
|
---|
82 | param = Hash.new
|
---|
83 | param["realm"] = HTTPUtils::quote(@realm)
|
---|
84 | param["domain"] = HTTPUtils::quote(@domain.to_a.join(" ")) if @domain
|
---|
85 | param["nonce"] = HTTPUtils::quote(nonce)
|
---|
86 | param["opaque"] = HTTPUtils::quote(opaque) if opaque
|
---|
87 | param["stale"] = stale.to_s
|
---|
88 | param["algorithm"] = @algorithm
|
---|
89 | param["qop"] = HTTPUtils::quote(@qop.to_a.join(",")) if @qop
|
---|
90 |
|
---|
91 | res[@response_field] =
|
---|
92 | "#{@auth_scheme} " + param.map{|k,v| "#{k}=#{v}" }.join(", ")
|
---|
93 | info("%s: %s", @response_field, res[@response_field]) if $DEBUG
|
---|
94 | raise @auth_exception
|
---|
95 | end
|
---|
96 |
|
---|
97 | private
|
---|
98 |
|
---|
99 | MustParams = ['username','realm','nonce','uri','response']
|
---|
100 | MustParamsAuth = ['cnonce','nc']
|
---|
101 |
|
---|
102 | def _authenticate(req, res)
|
---|
103 | unless digest_credentials = check_scheme(req)
|
---|
104 | return false
|
---|
105 | end
|
---|
106 |
|
---|
107 | auth_req = split_param_value(digest_credentials)
|
---|
108 | if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int"
|
---|
109 | req_params = MustParams + MustParamsAuth
|
---|
110 | else
|
---|
111 | req_params = MustParams
|
---|
112 | end
|
---|
113 | req_params.each{|key|
|
---|
114 | unless auth_req.has_key?(key)
|
---|
115 | error('%s: parameter missing. "%s"', auth_req['username'], key)
|
---|
116 | raise HTTPStatus::BadRequest
|
---|
117 | end
|
---|
118 | }
|
---|
119 |
|
---|
120 | if !check_uri(req, auth_req)
|
---|
121 | raise HTTPStatus::BadRequest
|
---|
122 | end
|
---|
123 |
|
---|
124 | if auth_req['realm'] != @realm
|
---|
125 | error('%s: realm unmatch. "%s" for "%s"',
|
---|
126 | auth_req['username'], auth_req['realm'], @realm)
|
---|
127 | return false
|
---|
128 | end
|
---|
129 |
|
---|
130 | auth_req['algorithm'] ||= 'MD5'
|
---|
131 | if auth_req['algorithm'] != @algorithm &&
|
---|
132 | (@opera_hack && auth_req['algorithm'] != @algorithm.upcase)
|
---|
133 | error('%s: algorithm unmatch. "%s" for "%s"',
|
---|
134 | auth_req['username'], auth_req['algorithm'], @algorithm)
|
---|
135 | return false
|
---|
136 | end
|
---|
137 |
|
---|
138 | if (@qop.nil? && auth_req.has_key?('qop')) ||
|
---|
139 | (@qop && (! @qop.member?(auth_req['qop'])))
|
---|
140 | error('%s: the qop is not allowed. "%s"',
|
---|
141 | auth_req['username'], auth_req['qop'])
|
---|
142 | return false
|
---|
143 | end
|
---|
144 |
|
---|
145 | password = @userdb.get_passwd(@realm, auth_req['username'], @reload_db)
|
---|
146 | unless password
|
---|
147 | error('%s: the user is not allowd.', auth_req['username'])
|
---|
148 | return false
|
---|
149 | end
|
---|
150 |
|
---|
151 | nonce_is_invalid = false
|
---|
152 | if @use_opaque
|
---|
153 | info("@opaque = %s", @opaque.inspect) if $DEBUG
|
---|
154 | if !(opaque = auth_req['opaque'])
|
---|
155 | error('%s: opaque is not given.', auth_req['username'])
|
---|
156 | nonce_is_invalid = true
|
---|
157 | elsif !(opaque_struct = @opaques[opaque])
|
---|
158 | error('%s: invalid opaque is given.', auth_req['username'])
|
---|
159 | nonce_is_invalid = true
|
---|
160 | elsif !check_opaque(opaque_struct, req, auth_req)
|
---|
161 | @opaques.delete(auth_req['opaque'])
|
---|
162 | nonce_is_invalid = true
|
---|
163 | end
|
---|
164 | elsif !check_nonce(req, auth_req)
|
---|
165 | nonce_is_invalid = true
|
---|
166 | end
|
---|
167 |
|
---|
168 | if /-sess$/ =~ auth_req['algorithm'] ||
|
---|
169 | (@opera_hack && /-SESS$/ =~ auth_req['algorithm'])
|
---|
170 | ha1 = hexdigest(password, auth_req['nonce'], auth_req['cnonce'])
|
---|
171 | else
|
---|
172 | ha1 = password
|
---|
173 | end
|
---|
174 |
|
---|
175 | if auth_req['qop'] == "auth" || auth_req['qop'] == nil
|
---|
176 | ha2 = hexdigest(req.request_method, auth_req['uri'])
|
---|
177 | ha2_res = hexdigest("", auth_req['uri'])
|
---|
178 | elsif auth_req['qop'] == "auth-int"
|
---|
179 | ha2 = hexdigest(req.request_method, auth_req['uri'],
|
---|
180 | hexdigest(req.body))
|
---|
181 | ha2_res = hexdigest("", auth_req['uri'], hexdigest(res.body))
|
---|
182 | end
|
---|
183 |
|
---|
184 | if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int"
|
---|
185 | param2 = ['nonce', 'nc', 'cnonce', 'qop'].map{|key|
|
---|
186 | auth_req[key]
|
---|
187 | }.join(':')
|
---|
188 | digest = hexdigest(ha1, param2, ha2)
|
---|
189 | digest_res = hexdigest(ha1, param2, ha2_res)
|
---|
190 | else
|
---|
191 | digest = hexdigest(ha1, auth_req['nonce'], ha2)
|
---|
192 | digest_res = hexdigest(ha1, auth_req['nonce'], ha2_res)
|
---|
193 | end
|
---|
194 |
|
---|
195 | if digest != auth_req['response']
|
---|
196 | error("%s: digest unmatch.", auth_req['username'])
|
---|
197 | return false
|
---|
198 | elsif nonce_is_invalid
|
---|
199 | error('%s: digest is valid, but nonce is not valid.',
|
---|
200 | auth_req['username'])
|
---|
201 | return :nonce_is_stale
|
---|
202 | elsif @use_auth_info_header
|
---|
203 | auth_info = {
|
---|
204 | 'nextnonce' => generate_next_nonce(req),
|
---|
205 | 'rspauth' => digest_res
|
---|
206 | }
|
---|
207 | if @use_opaque
|
---|
208 | opaque_struct.time = req.request_time
|
---|
209 | opaque_struct.nonce = auth_info['nextnonce']
|
---|
210 | opaque_struct.nc = "%08x" % (auth_req['nc'].hex + 1)
|
---|
211 | end
|
---|
212 | if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int"
|
---|
213 | ['qop','cnonce','nc'].each{|key|
|
---|
214 | auth_info[key] = auth_req[key]
|
---|
215 | }
|
---|
216 | end
|
---|
217 | res[@resp_info_field] = auth_info.keys.map{|key|
|
---|
218 | if key == 'nc'
|
---|
219 | key + '=' + auth_info[key]
|
---|
220 | else
|
---|
221 | key + "=" + HTTPUtils::quote(auth_info[key])
|
---|
222 | end
|
---|
223 | }.join(', ')
|
---|
224 | end
|
---|
225 | info('%s: authentication scceeded.', auth_req['username'])
|
---|
226 | req.user = auth_req['username']
|
---|
227 | return true
|
---|
228 | end
|
---|
229 |
|
---|
230 | def split_param_value(string)
|
---|
231 | ret = {}
|
---|
232 | while string.size != 0
|
---|
233 | case string
|
---|
234 | when /^\s*([\w\-\.\*\%\!]+)=\s*\"((\\.|[^\"])*)\"\s*,?/
|
---|
235 | key = $1
|
---|
236 | matched = $2
|
---|
237 | string = $'
|
---|
238 | ret[key] = matched.gsub(/\\(.)/, "\\1")
|
---|
239 | when /^\s*([\w\-\.\*\%\!]+)=\s*([^,\"]*),?/
|
---|
240 | key = $1
|
---|
241 | matched = $2
|
---|
242 | string = $'
|
---|
243 | ret[key] = matched.clone
|
---|
244 | when /^s*^,/
|
---|
245 | string = $'
|
---|
246 | else
|
---|
247 | break
|
---|
248 | end
|
---|
249 | end
|
---|
250 | ret
|
---|
251 | end
|
---|
252 |
|
---|
253 | def generate_next_nonce(req)
|
---|
254 | now = "%012d" % req.request_time.to_i
|
---|
255 | pk = hexdigest(now, @instance_key)[0,32]
|
---|
256 | nonce = [now + ":" + pk].pack("m*").chop # it has 60 length of chars.
|
---|
257 | nonce
|
---|
258 | end
|
---|
259 |
|
---|
260 | def check_nonce(req, auth_req)
|
---|
261 | username = auth_req['username']
|
---|
262 | nonce = auth_req['nonce']
|
---|
263 |
|
---|
264 | pub_time, pk = nonce.unpack("m*")[0].split(":", 2)
|
---|
265 | if (!pub_time || !pk)
|
---|
266 | error("%s: empty nonce is given", username)
|
---|
267 | return false
|
---|
268 | elsif (hexdigest(pub_time, @instance_key)[0,32] != pk)
|
---|
269 | error("%s: invalid private-key: %s for %s",
|
---|
270 | username, hexdigest(pub_time, @instance_key)[0,32], pk)
|
---|
271 | return false
|
---|
272 | end
|
---|
273 |
|
---|
274 | diff_time = req.request_time.to_i - pub_time.to_i
|
---|
275 | if (diff_time < 0)
|
---|
276 | error("%s: difference of time-stamp is negative.", username)
|
---|
277 | return false
|
---|
278 | elsif diff_time > @nonce_expire_period
|
---|
279 | error("%s: nonce is expired.", username)
|
---|
280 | return false
|
---|
281 | end
|
---|
282 |
|
---|
283 | return true
|
---|
284 | end
|
---|
285 |
|
---|
286 | def generate_opaque(req)
|
---|
287 | @mutex.synchronize{
|
---|
288 | now = req.request_time
|
---|
289 | if now - @last_nonce_expire > @nonce_expire_delta
|
---|
290 | @opaques.delete_if{|key,val|
|
---|
291 | (now - val.time) > @nonce_expire_period
|
---|
292 | }
|
---|
293 | @last_nonce_expire = now
|
---|
294 | end
|
---|
295 | begin
|
---|
296 | opaque = Utils::random_string(16)
|
---|
297 | end while @opaques[opaque]
|
---|
298 | @opaques[opaque] = OpaqueInfo.new(now, nil, '00000001')
|
---|
299 | opaque
|
---|
300 | }
|
---|
301 | end
|
---|
302 |
|
---|
303 | def check_opaque(opaque_struct, req, auth_req)
|
---|
304 | if (@use_next_nonce && auth_req['nonce'] != opaque_struct.nonce)
|
---|
305 | error('%s: nonce unmatched. "%s" for "%s"',
|
---|
306 | auth_req['username'], auth_req['nonce'], opaque_struct.nonce)
|
---|
307 | return false
|
---|
308 | elsif !check_nonce(req, auth_req)
|
---|
309 | return false
|
---|
310 | end
|
---|
311 | if (@check_nc && auth_req['nc'] != opaque_struct.nc)
|
---|
312 | error('%s: nc unmatched."%s" for "%s"',
|
---|
313 | auth_req['username'], auth_req['nc'], opaque_struct.nc)
|
---|
314 | return false
|
---|
315 | end
|
---|
316 | true
|
---|
317 | end
|
---|
318 |
|
---|
319 | def check_uri(req, auth_req)
|
---|
320 | uri = auth_req['uri']
|
---|
321 | if uri != req.request_uri.to_s && uri != req.unparsed_uri &&
|
---|
322 | (@internet_explorer_hack && uri != req.path)
|
---|
323 | error('%s: uri unmatch. "%s" for "%s"', auth_req['username'],
|
---|
324 | auth_req['uri'], req.request_uri.to_s)
|
---|
325 | return false
|
---|
326 | end
|
---|
327 | true
|
---|
328 | end
|
---|
329 |
|
---|
330 | def hexdigest(*args)
|
---|
331 | @h.hexdigest(args.join(":"))
|
---|
332 | end
|
---|
333 | end
|
---|
334 |
|
---|
335 | class ProxyDigestAuth < DigestAuth
|
---|
336 | include ProxyAuthenticator
|
---|
337 |
|
---|
338 | def check_uri(req, auth_req)
|
---|
339 | return true
|
---|
340 | end
|
---|
341 | end
|
---|
342 | end
|
---|
343 | end
|
---|