-
Notifications
You must be signed in to change notification settings - Fork 4
/
httpauth.py
200 lines (158 loc) · 6.63 KB
/
httpauth.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
"""
Copyright (c) 2012 Jonas Haag <jonas@lophus.org>. License: ISC
This implements Digest Auth as specified in RFC 2069, i.e. without the
`qop` quality-of-protection, `cnonce` nonce count, ... options.
References to the algorithm (HA1, HA2, nonce, ...) are taken from Wikipedia:
http://en.wikipedia.org/wiki/Digest_access_authentication
"""
import os
import re
import time
import hashlib
try: # Python 3
from urllib.request import parse_http_list, parse_keqv_list
PY2 = False
except ImportError: # Python 2
from urllib2 import parse_http_list, parse_keqv_list
PY2 = True
def md5(x):
return hashlib.md5(x).hexdigest()
def md5_str(x):
return md5(x.encode('utf8'))
def sha256(x):
return hashlib.sha256(x).hexdigest()
def reconstruct_uri(environ):
"""
Reconstruct the relative part of the request URI. I.e. if the requested URL
is https://foo.bar/spam?eggs, ``reconstruct_uri`` returns ``'/spam?eggs'``.
"""
uri = environ.get('SCRIPT_NAME', '') + environ['PATH_INFO']
if environ.get('QUERY_STRING'):
uri += '?' + environ['QUERY_STRING']
return uri
def make_www_authenticate_header(realm=None):
return 'Digest realm="%s", nonce="%s"' % (realm, generate_nonce())
def generate_nonce():
return sha256(os.urandom(1000) + str(time.time()).encode())
def make_auth_response(nonce, HA1, HA2):
""" response := md5(HA1 : nonce : HA2) """
return md5_str(HA1 + ':' + nonce + ':' + HA2)
def make_HA2(http_method, uri):
""" HA2 := http_method : uri (as reconstructed by ``reconstruct_uri``) """
return md5_str(http_method + ':' + uri)
def parse_dict_header(value):
"""
Parses a HTTP dict header value -- i.e. ``"foo=bar, spam=eggs"`` is parsed
into ``{'foo': 'bar', 'spam': 'eggs'}``.
"""
return parse_keqv_list(parse_http_list(value))
class BaseHttpAuthMiddleware(object):
"""
Abstract HTTP Digest Auth middleware. Contains all the functionality
except for credential validation -- this happens using the ``make_HA1``
method which needs to be overriden by subclasses.
`wsgi_app`
The WSGI app to be secured.
`realm`
The HTTP Auth realm to be displayed in the browser.
`routes`
(optional) A list of regular expressions that specify which URLs should
be secured. If not given, all routes are secured by default.
"""
def __init__(self, wsgi_app, realm=None, routes=()):
self.wsgi_app = wsgi_app
self.realm = realm or ''
self.routes = self.compile_routes(routes)
def __call__(self, environ, start_response):
environ['httpauth.uri'] = reconstruct_uri(environ)
if (self.should_require_authentication(environ['httpauth.uri']) and
not self.authenticate(environ)):
# URL is secured and user hasn't sent authentication/wrong credentials.
return self.challenge(environ, start_response)
else:
# Wave-through to real WSGI app.
return self.wsgi_app(environ, start_response)
def compile_routes(self, routes):
return [re.compile(route) for route in routes]
def should_require_authentication(self, url):
""" Returns True if we should require authentication for the URL given """
return (not self.routes # require auth for all URLs
or any(route.match(url) for route in self.routes))
def authenticate(self, environ):
"""
Returns True if the credentials passed in the Authorization header are
valid, False otherwise.
"""
try:
hd = parse_dict_header(environ['HTTP_AUTHORIZATION'])
except (KeyError, ValueError):
return False
return self.credentials_valid(
hd['response'],
environ['REQUEST_METHOD'],
environ['httpauth.uri'],
hd['nonce'],
hd['Digest username'],
)
def credentials_valid(self, response, http_method, uri, nonce, user):
try:
HA1 = self.make_HA1(user)
except KeyError:
# Invalid user
return False
return response == make_auth_response(nonce, HA1, make_HA2(http_method, uri))
def challenge(self, environ, start_response):
start_response(
'401 Authentication Required',
[('WWW-Authenticate', make_www_authenticate_header(self.realm))],
)
html = '<h1>401 - Authentication Required</h1>'
return [html if PY2 else html.encode()]
class DigestFileHttpAuthMiddleware(BaseHttpAuthMiddleware):
"""
Reads credentials from an Apache-style .htdigest file.
`filelike`
Any file-like object that has a ``.read()`` method.
Note: Don't pass filenames, only open files/file-likes.
"""
def __init__(self, filelike, **kwargs):
realm, self.user_HA1_map = self.parse_htdigest_file(filelike)
BaseHttpAuthMiddleware.__init__(self, realm=realm, **kwargs)
def make_HA1(self, username):
return self.user_HA1_map[username]
def parse_htdigest_file(self, filelike):
"""
.htdigest files consist of lines in the following format::
username:realm:passwordhash
where both `username` and `realm` are plain-text without any colons
and `passwordhash` is the result of ``md5(username : realm : password)``
and thus `passwordhash` == HA1.
"""
realm = None
user_HA1_map = {}
for lineno, line in enumerate(filter(None, filelike.read().splitlines()), 1):
try:
username, realm2, password_hash = line.split(':')
except ValueError:
raise ValueError("Line %d invalid: %r (username/password may not contain ':')" % (lineno, line))
if realm is not None and realm != realm2:
raise ValueError("Line %d: realm may not vary (got %r and %r)" % (lineno, realm, realm2))
else:
realm = realm2
user_HA1_map[username] = password_hash
return realm, user_HA1_map
class DictHttpAuthMiddleware(BaseHttpAuthMiddleware):
"""
Reads credentials from ``user_password_map`` which is a
`username -> plaintext password` map.
"""
def __init__(self, user_password_map, **kwargs):
self.user_password_map = user_password_map
BaseHttpAuthMiddleware.__init__(self, **kwargs)
def make_HA1(self, username):
password = self.user_password_map[username]
return md5_str(username + ':' + self.realm + ':' + password)
class AlwaysFailingAuthMiddleware(BaseHttpAuthMiddleware):
""" This thing just keeps asking for credentials all the time """
def authenticate(self, environ):
return False