103
class DigestedCredentials(object):
105
Yet Another Simple HTTP Digest authentication scheme.
107
implements(IUsernameHashedPassword, IUsernameDigestHash)
109
def __init__(self, username, method, realm, fields):
110
self.username = username
116
def checkPassword(self, password):
118
Verify that the credentials represented by this object agree with the
119
given plaintext C{password} by hashing C{password} in the same way the
120
response hash represented by this object was generated and comparing
123
response = self.fields.get('response')
124
uri = self.fields.get('uri')
125
nonce = self.fields.get('nonce')
126
cnonce = self.fields.get('cnonce')
127
nc = self.fields.get('nc')
128
algo = self.fields.get('algorithm', 'md5').lower()
129
qop = self.fields.get('qop', 'auth')
131
expected = calcResponse(
132
calcHA1(algo, self.username, self.realm, password, nonce, cnonce),
133
calcHA2(algo, self.method, uri, qop, None),
134
algo, nonce, nc, cnonce, qop)
136
return expected == response
139
def checkHash(self, digestHash):
141
Verify that the credentials represented by this object agree with the
142
credentials represented by the I{H(A1)} given in C{digestHash}.
144
@param digestHash: A precomputed H(A1) value based on the username,
145
realm, and password associate with this credentials object.
147
response = self.fields.get('response')
148
uri = self.fields.get('uri')
149
nonce = self.fields.get('nonce')
150
cnonce = self.fields.get('cnonce')
151
nc = self.fields.get('nc')
152
algo = self.fields.get('algorithm', 'md5').lower()
153
qop = self.fields.get('qop', 'auth')
155
expected = calcResponse(
156
calcHA1(algo, None, None, None, nonce, cnonce, preHA1=digestHash),
157
calcHA2(algo, self.method, uri, qop, None),
158
algo, nonce, nc, cnonce, qop)
160
return expected == response
164
class DigestCredentialFactory(object):
166
Support for RFC2617 HTTP Digest Authentication
168
@cvar CHALLENGE_LIFETIME_SECS: The number of seconds for which an
169
opaque should be valid.
171
@type privateKey: C{str}
172
@ivar privateKey: A random string used for generating the secure opaque.
174
@type algorithm: C{str}
175
@param algorithm: Case insensitive string specifying the hash algorithm to
176
use. Must be either C{'md5'} or C{'sha'}. C{'md5-sess'} is B{not}
179
@type authenticationRealm: C{str}
180
@param authenticationRealm: case sensitive string that specifies the realm
181
portion of the challenge
184
CHALLENGE_LIFETIME_SECS = 15 * 60 # 15 minutes
188
def __init__(self, algorithm, authenticationRealm):
189
self.algorithm = algorithm
190
self.authenticationRealm = authenticationRealm
191
self.privateKey = secureRandom(12)
194
def getChallenge(self, address):
196
Generate the challenge for use in the WWW-Authenticate header.
198
@param address: The client address to which this challenge is being
201
@return: The C{dict} that can be used to generate a WWW-Authenticate
204
c = self._generateNonce()
205
o = self._generateOpaque(c, address)
210
'algorithm': self.algorithm,
211
'realm': self.authenticationRealm}
214
def _generateNonce(self):
216
Create a random value suitable for use as the nonce parameter of a
217
WWW-Authenticate challenge.
221
return secureRandom(12).encode('hex')
226
Parameterize the time based seed used in C{_generateOpaque}
227
so we can deterministically unittest it's behavior.
232
def _generateOpaque(self, nonce, clientip):
234
Generate an opaque to be returned to the client. This is a unique
235
string that can be returned to us and verified.
237
# Now, what we do is encode the nonce, client ip and a timestamp in the
238
# opaque value with a suitable digest.
239
now = str(int(self._getTime()))
242
key = "%s,%s,%s" % (nonce, clientip, now)
243
digest = md5(key + self.privateKey).hexdigest()
244
ekey = key.encode('base64')
245
return "%s-%s" % (digest, ekey.strip('\n'))
248
def _verifyOpaque(self, opaque, nonce, clientip):
250
Given the opaque and nonce from the request, as well as the client IP
251
that made the request, verify that the opaque was generated by us.
252
And that it's not too old.
254
@param opaque: The opaque value from the Digest response
255
@param nonce: The nonce value from the Digest response
256
@param clientip: The remote IP address of the client making the request
257
or C{None} if the request was submitted over a channel where this
260
@return: C{True} if the opaque was successfully verified.
262
@raise error.LoginFailed: if C{opaque} could not be parsed or
263
contained the wrong values.
265
# First split the digest from the key
266
opaqueParts = opaque.split('-')
267
if len(opaqueParts) != 2:
268
raise error.LoginFailed('Invalid response, invalid opaque value')
274
key = opaqueParts[1].decode('base64')
275
keyParts = key.split(',')
277
if len(keyParts) != 3:
278
raise error.LoginFailed('Invalid response, invalid opaque value')
280
if keyParts[0] != nonce:
281
raise error.LoginFailed(
282
'Invalid response, incompatible opaque/nonce values')
284
if keyParts[1] != clientip:
285
raise error.LoginFailed(
286
'Invalid response, incompatible opaque/client values')
289
when = int(keyParts[2])
291
raise error.LoginFailed(
292
'Invalid response, invalid opaque/time values')
294
if (int(self._getTime()) - when >
295
DigestCredentialFactory.CHALLENGE_LIFETIME_SECS):
297
raise error.LoginFailed(
298
'Invalid response, incompatible opaque/nonce too old')
301
digest = md5(key + self.privateKey).hexdigest()
302
if digest != opaqueParts[0]:
303
raise error.LoginFailed('Invalid response, invalid opaque value')
308
def decode(self, response, method, host):
310
Decode the given response and attempt to generate a
311
L{DigestedCredentials} from it.
313
@type response: C{str}
314
@param response: A string of comma seperated key=value pairs
317
@param method: The action requested to which this response is addressed
318
(GET, POST, INVITE, OPTIONS, etc).
321
@param host: The address the request was sent from.
323
@raise error.LoginFailed: If the response does not contain a username,
324
a nonce, an opaque, or if the opaque is invalid.
326
@return: L{DigestedCredentials}
329
if s[0] == s[-1] == '"':
332
response = ' '.join(response.splitlines())
333
parts = response.split(',')
337
for (k, v) in [p.split('=', 1) for p in parts]:
338
auth[k.strip()] = unq(v.strip())
340
username = auth.get('username')
342
raise error.LoginFailed('Invalid response, no username given.')
344
if 'opaque' not in auth:
345
raise error.LoginFailed('Invalid response, no opaque given.')
347
if 'nonce' not in auth:
348
raise error.LoginFailed('Invalid response, no nonce given.')
350
# Now verify the nonce/opaque values for this client
351
if self._verifyOpaque(auth.get('opaque'), auth.get('nonce'), host):
352
return DigestedCredentials(username,
354
self.authenticationRealm,
87
359
class CramMD5Credentials:
88
360
implements(IUsernameHashedPassword)