Class WEBrick::HTTPAuth::DigestAuth
In: lib/webrick/httpauth/digestauth.rb
Parent: Object

Methods

Included Modules

Authenticator

Constants

AuthScheme = "Digest"
OpaqueInfo = Struct.new(:time, :nonce, :nc)
MustParams = ['username','realm','nonce','uri','response']
MustParamsAuth = ['cnonce','nc']

Attributes

algorithm  [R] 
qop  [R] 

Public Class methods

[Source]

    # File lib/webrick/httpauth/digestauth.rb, line 29
29:       def self.make_passwd(realm, user, pass)
30:         pass ||= ""
31:         Digest::MD5::hexdigest([user, realm, pass].join(":"))
32:       end

[Source]

    # File lib/webrick/httpauth/digestauth.rb, line 34
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

Public Instance methods

[Source]

    # File lib/webrick/httpauth/digestauth.rb, line 65
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

[Source]

    # File lib/webrick/httpauth/digestauth.rb, line 75
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

Private Instance methods

[Source]

     # File lib/webrick/httpauth/digestauth.rb, line 102
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

[Source]

     # File lib/webrick/httpauth/digestauth.rb, line 260
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

[Source]

     # File lib/webrick/httpauth/digestauth.rb, line 303
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

[Source]

     # File lib/webrick/httpauth/digestauth.rb, line 319
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

[Source]

     # File lib/webrick/httpauth/digestauth.rb, line 253
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

[Source]

     # File lib/webrick/httpauth/digestauth.rb, line 286
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

[Source]

     # File lib/webrick/httpauth/digestauth.rb, line 330
330:       def hexdigest(*args)
331:         @h.hexdigest(args.join(":"))
332:       end

[Source]

     # File lib/webrick/httpauth/digestauth.rb, line 230
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

[Validate]