# Copyright (c) 2007, Kundan Singh. All rights reserved. See LICENSE for details.This file implements RFC2617 (HTTP auth)''' The HTTP basic and digest access authentication as per RFC 2617. ''' import random, hashlib, base64, timeFrom RFC2617 p.3_quote = lambda s: '"' + s + '"' if not s or s[0] != '"' != s[-1] else s _unquote = lambda s: s[1:-1] if s and s[0] == '"' == s[-1] else s def createAuthenticate(authMethod='Digest', **kwargs): '''Build the WWW-Authenticate header's value. >>> print createAuthenticate('Basic', realm='iptel.org') Basic realm="iptel.org" >>> print createAuthenticate('Digest', realm='iptel.org', domain='sip:iptel.org', nonce='somenonce') Digest realm="iptel.org", domain="sip:iptel.org", qop="auth", nonce="somenonce", opaque="", stale=FALSE, algorithm=MD5 ''' if authMethod.lower() == 'basic': return 'Basic realm=%s'%(_quote(kwargs.get('realm', ''))) elif authMethod.lower() == 'digest': predef = ('realm', 'domain', 'qop', 'nonce', 'opaque', 'stale', 'algorithm') unquoted = ('stale', 'algorithm') now = time.time(); nonce = kwargs.get('nonce', base64.b64encode('%d %s'%(now, hashlib.md5('%d:%d'%(now, id(createAuthenticate)))))) default = dict(realm='', domain='', opaque='', stale='FALSE', algorithm='MD5', qop='auth', nonce=nonce) kv = map(lambda x: (x, kwargs.get(x, default[x])), predef) + filter(lambda x: x[0] not in predef, kwargs.items()) # put predef attributes in order before non predef attributes return 'Digest ' + ', '.join(map(lambda y: '%s=%s'%(y[0], _quote(y[1]) if y[0] not in unquoted else y[1]), kv)) else: raise ValueError, 'invalid authMethod%s'%(authMethod)HTTP provides a simple challenge-response authentication mechanism that MAY be used by a server to challenge a client request and by a client to provide authentication information. It uses an extensible, case-insensitive token to identify the authentication scheme, followed by a comma-separated list of attribute-value pairs which carry the parameters necessary for achieving authentication via that scheme. auth-scheme = token auth-param = token "=" ( token | quoted-string )From RFC2617 p.3The 401 (Unauthorized) response message is used by an origin server to challenge the authorization of a user agent. This response MUST include a WWW-Authenticate header field containing at least one challenge applicable to the requested resource. The 407 (Proxy Authentication Required) response message is used by a proxy to challenge the authorization of a client and MUST include a Proxy- Authenticate header field containing at least one challenge applicable to the proxy for the requested resource. challenge = auth-scheme 1*SP 1#auth-paramFrom RFC2617 p.4def createAuthorization(challenge, username, password, uri=None, method=None, entityBody=None, context=None): '''Build the Authorization header for this challenge. The challenge represents the WWW-Authenticate header's value and the function returns the Authorization header's value. The context (dict) is used to save cnonce and nonceCount if available. The uri represents the request URI str, and method the request method. The result contains the properties in alphabetical order of property name. >>> context = {'cnonce':'0a4f113b', 'nc': 0} >>> print createAuthorization('Digest realm="testrealm@host.com", qop="auth", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", opaque="5ccc069c403ebaf9f0171e9517f40e41"', 'Mufasa', 'Circle Of Life', '/dir/index.html', 'GET', None, context) Digest cnonce="0a4f113b",nc=00000001,nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",opaque="5ccc069c403ebaf9f0171e9517f40e41",qop=auth,realm="testrealm@host.com",response="6629fae49393a05397450978507c4ef1",uri="/dir/index.html",username="Mufasa" >>> print createAuthorization('Basic realm="WallyWorld"', 'Aladdin', 'open sesame') Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== ''' authMethod, sep, rest = challenge.strip().partition(' ') ch, cr = dict(), dict() # challenge and credentials cr['password'] = password cr['username'] = usernameA user agent that wishes to authenticate itself with an origin server--usually, but not necessarily, after receiving a 401 (Unauthorized)--MAY do so by including an Authorization header field with the request. A client that wishes to authenticate itself with a proxy--usually, but not necessarily, after receiving a 407 (Proxy Authentication Required)--MAY do so by including a Proxy- Authorization header field with the request. Both the Authorization field value and the Proxy-Authorization field value consist of credentials containing the authentication information of the client for the realm of the resource being requested. The user agent MUST choose to use one of the challenges with the strongest auth-scheme it understands and request credentials from the user based upon that challenge. credentials = auth-scheme #auth-paramFrom RFC2617 p.5if authMethod.lower() == 'basic': return authMethod + ' ' + basic(cr)The "basic" authentication scheme is based on the model that the client must authenticate itself with a user-ID and a password for each realm. The realm value should be considered an opaque string which can only be compared for equality with other realms on that server. The server will service the request only if it can validate the user-ID and password for the protection space of the Request-URI. There are no optional authentication parameters. For Basic, the framework above is utilized as follows: challenge = "Basic" realm credentials = "Basic" basic-credentials Upon receipt of an unauthorized request for a URI within the protection space, the origin server MAY respond with a challenge like the following: WWW-Authenticate: Basic realm="WallyWorld" where "WallyWorld" is the string assigned by the server to identify the protection space of the Request-URI. A proxy may respond with the same challenge using the Proxy-Authenticate header field.From RFC2617 p.6elif authMethod.lower() == 'digest': for n,v in map(lambda x: x.strip().split('='), rest.split(',') if rest else []): ch[n.lower().strip()] = _unquote(v.strip()) # TODO: doesn't work if embedded ',' in value, e.g., qop="auth,auth-int"Like Basic Access Authentication, the Digest scheme is based on a simple challenge-response paradigm. The Digest scheme challenges using a nonce value. A valid response contains a checksum (by default, the MD5 checksum) of the username, the password, the given nonce value, the HTTP method, and the requested URI. In this way, the password is never sent in the clear. Just as with the Basic scheme, the username and password must be prearranged in some fashion not addressed by this document.From RFC2617 p.8for y in filter(lambda x: x in ch, ['username', 'realm', 'nonce', 'opaque', 'algorithm']): cr[y] = ch[y] cr['uri'] = uri cr['httpMethod'] = method if 'qop' in ch: if context and 'cnonce' in context: cnonce, nc = context['cnonce'], context['nc'] + 1 else: cnonce, nc = H(str(random.randint(0, 2**31))), 1 if context: context['cnonce'], context['nc'] = cnonce, nc cr['qop'], cr['cnonce'], cr['nc'] = 'auth', cnonce, '%08x'% ncIf a server receives a request for an access-protected object, and an acceptable Authorization header is not sent, the server responds with a "401 Unauthorized" status code, and a WWW-Authenticate header as per the framework defined above, which for the digest scheme is utilized as follows: challenge = "Digest" digest-challenge digest-challenge = 1#( realm | [ domain ] | nonce | [ opaque ] |[ stale ] | [ algorithm ] | [ qop-options ] | [auth-param] ) domain = "domain" "=" <"> URI ( 1*SP URI ) <"> URI = absoluteURI | abs_path nonce = "nonce" "=" nonce-value nonce-value = quoted-string opaque = "opaque" "=" quoted-string stale = "stale" "=" ( "true" | "false" ) algorithm = "algorithm" "=" ( "MD5" | "MD5-sess" | token ) qop-options = "qop" "=" <"> 1#qop-value <"> qop-value = "auth" | "auth-int" | tokenFrom RFC2617 p.11cr['response'] = digest(cr) items = sorted(filter(lambda x: x not in ['name', 'authMethod', 'value', 'httpMethod', 'entityBody', 'password'], cr)) return authMethod + ' ' + ','.join(map(lambda y: '%s=%s'%(y, (cr[y] if y == 'qop' or y == 'nc' else _quote(cr[y]))), items)) else: raise ValueError, 'Invalid auth method -- ' + authMethodThe client is expected to retry the request, passing an Authorization header line, which is defined according to the framework above, utilized as follows. credentials = "Digest" digest-response digest-response = 1#( username | realm | nonce | digest-uri | response | [ algorithm ] | [cnonce] | [opaque] | [message-qop] | [nonce-count] | [auth-param] ) username = "username" "=" username-value username-value = quoted-string digest-uri = "uri" "=" digest-uri-value digest-uri-value = request-uri ; As specified by HTTP/1.1 message-qop = "qop" "=" qop-value cnonce = "cnonce" "=" cnonce-value cnonce-value = nonce-value nonce-count = "nc" "=" nc-value nc-value = 8LHEX response = "response" "=" request-digestFrom RFC2617 p.10H = lambda d: hashlib.md5(d).hexdigest() KD = lambda s, d: H(s + ':' + d)In this document the string obtained by applying the digest algorithm to the data "data" with secret "secret" will be denoted by KD(secret, data), and the string obtained by applying the checksum algorithm to the data "data" will be denoted H(data). The notation unq(X) means the value of the quoted-string X without the surrounding quotes. For the "MD5" and "MD5-sess" algorithms H(data) = MD5(data) and KD(secret, data) = H(concat(secret, ":", data))From RFC2617 p.18def digest(cr): '''Create a digest response for the credentials. >>> input = {'httpMethod':'GET', 'username':'Mufasa', 'password': 'Circle Of Life', 'realm':'testrealm@host.com', 'algorithm':'md5', 'nonce':'dcd98b7102dd2f0e8b11d0f600bfb0c093', 'uri':'/dir/index.html', 'qop':'auth', 'nc': '00000001', 'cnonce':'0a4f113b', 'opaque':'5ccc069c403ebaf9f0171e9517f40e41'} >>> print digest(input) "6629fae49393a05397450978507c4ef1" ''' algorithm, username, realm, password, nonce, cnonce, nc, qop, httpMethod, uri, entityBody \ = map(lambda x: cr[x] if x in cr else None, ['algorithm', 'username', 'realm', 'password', 'nonce', 'cnonce', 'nc', 'qop', 'httpMethod', 'uri', 'entityBody'])The first time the client requests the document, no Authorization header is sent, so the server responds with: HTTP/1.1 401 Unauthorized WWW-Authenticate: Digest realm="testrealm@host.com", qop="auth,auth-int", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", opaque="5ccc069c403ebaf9f0171e9517f40e41" The client may prompt the user for the username and password, after which it will respond with a new request, including the following Authorization header: Authorization: Digest username="Mufasa", realm="testrealm@host.com", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", uri="/dir/index.html", qop=auth, nc=00000001, cnonce="0a4f113b", response="6629fae49393a05397450978507c4ef1", opaque="5ccc069c403ebaf9f0171e9517f40e41"From RFC2617 p.13if algorithm and algorithm.lower() == 'md5-sess': A1 = H(username + ':' + realm + ':' + password) + ':' + nonce + ':' + cnonce else: A1 = username + ':' + realm + ':' + passwordIf the "algorithm" directive's value is "MD5" or is unspecified, then A1 is: A1 = unq(username-value) ":" unq(realm-value) ":" passwd where passwd = < user's password > If the "algorithm" directive's value is "MD5-sess", then A1 is calculated only once - on the first request by the client following receipt of a WWW-Authenticate challenge from the server. It uses the server nonce from that challenge, and the first client nonce value to construct A1 as follows: A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd ) ":" unq(nonce-value) ":" unq(cnonce-value) This creates a 'session key' for the authentication of subsequentFrom RFC2617 p.14if not qop or qop == 'auth': A2 = httpMethod + ':' + str(uri) else: A2 = httpMethod + ':' + str(uri) + ':' + H(str(entityBody))If the "qop" directive's value is "auth" or is unspecified, then A2 is: A2 = Method ":" digest-uri-value If the "qop" value is "auth-int", then A2 is: A2 = Method ":" digest-uri-value ":" H(entity-body)From RFC2617 p.13if qop and (qop == 'auth' or qop == 'auth-int'): return _quote(KD(H(A1), nonce + ':' + str(nc) + ':' + cnonce + ':' + qop + ':' + H(A2))) else: return _quote(KD(H(A1), nonce + ':' + H(A2)))If the "qop" value is "auth" or "auth-int": request-digest = <"> < KD ( H(A1), unq(nonce-value) ":" nc-value ":" unq(cnonce-value) ":" unq(qop-value) ":" H(A2) ) <"> If the "qop" directive is not present (this construction is for compatibility with RFC 2069): request-digest = <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) > <">From RFC2617 p.6def basic(cr): '''Create a basic response for the credentials. >>> print basic({'username':'Aladdin', 'password':'open sesame'}) QWxhZGRpbjpvcGVuIHNlc2FtZQ== '''If the user agent wishes to send the userid "Aladdin" and password "open sesame", it would use the following header field: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==From RFC2617 p.5return base64.b64encode(cr['username'] + ':' + cr['password']) if __name__ == '__main__': import doctest doctest.testmod()To receive authorization, the client sends the userid and password, separated by a single colon (":") character, within a base64 [7] encoded string in the credentials. basic-credentials = base64-user-pass base64-user-pass =user-pass = userid ":" password userid = * password = *TEXT Userids might be case sensitive.