This repository has been archived by the owner on Oct 12, 2017. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 15
/
RestfulAuth
812 lines (626 loc) · 32.1 KB
/
RestfulAuth
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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
'''RESTfulAuth'''
I wrote this tool, based on the !DigestAuth tool shipped with CherryPy, because standard digest auth per RFC2617 does not offer any way
of logging a user out or of time-limiting the use of the authentication credentials. I wanted an authentication scheme which would work well with my app which is designed on RESTful lines - don't add new protocols if HTTP will do the job. In particular I wanted to avoid
the use (and added complexity) of cookies and sessions.
The RFC does suggest the use of expiring nonces as a way of mitigating replay attacks. Expiring nonces also allow all the extra
features I wanted to be included in digest authentication.
So this tool extends the standard !DigestAuth tool by:
1. Allowing the time for which a nonce is valid to be specified so that a user is automatically logged out when the nonce expires.
2. Allowing a nonce refresh period to be specified to provide some protection against replay attacks.
3. Allowing the user to log out by visiting a link which expires all her nonces.
4. Providing a simple role based authorisation scheme.
5. Allows authorisation to be specified per-method and per URI.
6. Facilitates the use of this tool in an suexec CGI or mod_wsgi environment by allowing the HTTP Authorization header to be passed in with a different name.
All the above are configured on a per-URI basis in the CherryPy config.
This scheme is susceptible to all the attacks described in the RFC with the exception that it allows replay attacks to be mitigated.
But for any serious application SSL will mitigate the remaining attacks.
Server side security (eg of the password database) is not addressed by this tool.
'''Changes from DigestAuth'''
The nonce storage mechanism is added. This uses the filesystem as a database. The nonce files created are never read - the
tool relies entirely on the nonce file metadata.
The nonces are generated by the tool - the standard !DigestAuth tool will frequently produce identical nonces since the timestamp it uses has only a 1 second granularity.
'''Caveats'''
This tool was designed for use in a low traffic environment with a total user count in the hundreds.
I'm not a security expert, but I believe that this tool is at least as secure as the built-in !DigestAuth tool.
The test cases (after the code) are not terribly comprehensive.
{{{
""" Authentication and authorisation tool for RESTful apps.
Authentication is performed by expiring-nonce HTTP digest authentication,
providing a means of time-limiting logins and a means of logging users out.
Authorisation uses role-based access control.
Tool parameters are:
realm: a string containing the authentication realm.
users: a dict of the form: {username: (password, role1, ...)} or a
callable returning the password/roles tuple when passed the
username.
refresh: an integer giving the refresh time for nonces in seconds.
expires: an integer giving the maximum life of a nonce (from the last
refresh if refreshed). Zero indicates a one-time nonce. If less
than 'refresh' (but not zero) 'refresh' will be used as the
expiry time.
logout: a boolean value. True indicates that all nonces originating from
the client making the request to this resource should be expired
(regardless of realm). This allows a 'logout' function to be
implemented without the use of javascript, cookies or .htaccess
magic. A 'logout' link should point to a non-cacheable resource
that is protected by restful_auth with this flag set.
Default is False.
roles: a dict of the form {method1: (role1, role2, ...), ....}
The roles for the current method are compared to the user's
roles as returned from the 'users' parameter. If no match is
found the authentication fails. The default is an empty dict,
in which case this tool does not check roles - any authenticated
user will be authorised by this tool. The roles are available to
the page handler in cherrypy.resource.loginroles.
If the 'expires' parameter is greater than 'refresh', then when an nonce
has been issued for the time given by the 'refresh' parameter a new nonce
will be sent to the client together with the 'stale=true' flag, avoiding
the need for the user to re-enter her credentials. The 'refresh' timer is
then restarted. If both the 'refresh' and the 'expires' times have been
exceeded then the 'stale' flag will not be sent and the user must
re-authenticate.
Some examples of using the restful-auth tool:
The root of the application tree. Here we set up the default parameters for
the application tree. The nonce will be refreshed every minute and will
expire if there is no activity for 10 minutes.
app = main.AppRoot(config, {'tools.restful_auth.on' : False,
'tools.restful_auth.realm': 'Application Realm',
'tools.restful_auth.users': lib.get_users,
'tools.restful_auth.refresh': 60,
'tools.restful_auth.expires': 600})
This URI expires all the nonces from this host.
app.logout = main.Logout(config, {'tools.restful_auth.on': True,
'tools.restful_auth.logout': True})
Here we set up the access rights for this branch of the tree. Guests and
users can read the resource, users can modify it, and administrators can
create new resources. In the users dict administrators would have both the
'admin' and the 'user' roles.
app.branch = uri.branch(config,
{'tools.restful_auth.roles': {'GET': ('guest', 'user'),
'PUT': ('user'),
'POST': ('admins')}})
Anyone can access a this leaf - restful_auth is off.
app.branch.leaf1 = uri.Leaf1(config)
Only users with the appropriate roles can access this leaf.
app.branch.leaf2 = uri.Leaf2(config, {'tools.restful_auth.on' : True})
This leaf does something serious like modify application settings, so we
specify a new realm, a one-time nonce so that every access must be
authenticated, and only allow access to a user with the superadmin role.
app.branch.leaf3 = uri.Leaf3(config,
{'tools.restful_auth.on':True,
'tools.restful_auth.realm':'Administrator realm',
'tools.restful_auth.users': lib.get_users,
'tools.restful_auth.refresh': 15,
'tools.restful_auth.expires': 0,
'tools.restful_auth.roles': {'GET': ('superadmin',),
'POST': ('superadmin',)}})
Copyright (c) PR Hardman 2007.
This module is free software, and you may redistribute it and/or modify
it under the same terms as Python itself, so long as this copyright message
and disclaimer are retained in their original form.
IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.
THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
Created 5 December 2007
"""
import time
import os
import glob
import md5
import random
import cherrypy
import cherrypy.lib.httpauth as httpauth
#------------------------------- restful_auth tool ---------------------#
NONCE_DIR = os.path.join(os.path.dirname(__file__), ".nonce")
N_EXPIRED = 1
N_REFRESH = 2
N_VALID = 3
STALE_WWW_AUTH = 'Digest realm="%s", nonce="%s", algorithm="MD5", qop="auth", stale=true'
STD_WWW_AUTH = 'Digest realm="%s", nonce="%s", algorithm="MD5", qop="auth"'
def nonce_state(nonce, expires):
""" Report the nonce state.
A nonce may be: 1) Expired: Doesn't exist or both 'refresh' and
'expires' exceeded
2) Refresh due: 'refresh' exceeded but 'expires' not
exceeded
3) One-time: 'expires' is 0. Expire forcibly (delete).
3) Valid: none of the above
"""
fn = '%s-%s' % (nonce, cherrypy.request.remote.ip)
filename = os.path.join(NONCE_DIR, fn)
if not os.path.exists(filename):
# Already expired
return N_EXPIRED
stats = os.stat(filename)
if stats.st_mtime < time.time():
# Expire the nonce
os.remove(filename)
return N_EXPIRED
elif stats.st_atime < time.time():
os.remove(filename)
return N_REFRESH
if expires == 0:
os.remove(filename)
return N_VALID
return N_VALID
def record_nonce(nonce, refresh, expires):
""" Record the nonce together with it's expiry time and the client IP """
if not os.path.exists(NONCE_DIR):
os.mkdir(NONCE_DIR, 0700)
# The nonce is a hex digest so it can be used as
# a filename with the client ip appended.
fn = '%s-%s' % (nonce, cherrypy.request.remote.ip)
filename = os.path.join(NONCE_DIR, fn)
# expire any stale nonces
for name in glob.glob(os.path.join(NONCE_DIR,'*')):
if os.stat(name).st_mtime < time.time():
os.remove(name)
# Record this nonce/IP
try:
fd = os.open(filename, os.O_RDONLY | os.O_CREAT | os.O_EXCL)
except:
raise
os.close(fd)
# Mark when this nonce will expire
now = int(time.time())
if expires > refresh:
os.utime(filename, (now + refresh, now + expires))
else:
os.utime(filename, (now + refresh, now + refresh))
return
def do_logout():
""" Expire all nonces from the current client """
client_ip = cherrypy.request.remote.ip
for name in glob.glob(os.path.join(NONCE_DIR,'*')):
if name not in ('.', '..'):
if name[name.rfind('-') + 1:] == client_ip:
os.remove(name)
return
def check_roles(resource_roles, user_roles):
""" Check that one of the resource roles matches the list of user roles """
for role in resource_roles:
if role in user_roles:
return True
return False
def check_auth(users, realm, expires, roles):
"""If an authorization header contains valid credentials return True,
else return False. If the nonce has expired but the expires time has not
been exceeded return the 'stale' flag.
If the application is running under CGI/suexec or mod_wsgi without the
WSGIPassAuthorization directive then the authorization
header (which will be removed by suexec/mod_wsgi) must be written to the
x-dbappauth header by mod-rewrite and mod_headers in the application's
.htaccess file as follows:
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ /cgi-bin/run_cgi.cgi/$1 [E=MY_HTTP_AUTH:%{HTTP:Authorization},QSA,PT,L]
RequestHeader set x-myappauth "%{MY_HTTP_AUTH}e" env=MY_HTTP_AUTH
Based on cherrypy.lib.auth.check_auth()
"""
auth = False
stale = False
if 'authorization' in cherrypy.request.headers:
auth_hdr = cherrypy.request.headers['authorization']
elif 'x-myappauth' in cherrypy.request.headers:
auth_hdr = cherrypy.request.headers['x-myappauth']
if not auth_hdr:
return auth, stale
else:
return auth, stale
# make sure the provided credentials are correctly set
ah = httpauth.parseAuthorization(auth_hdr)
if ah is None:
raise cherrypy.HTTPError(400, 'Bad Request')
# These lines only required for CherryPy pre 3.1.0rc1
if ah['realm'] != realm:
return auth, stale
nstate = nonce_state(ah['nonce'], expires)
if nstate == N_EXPIRED:
# No need to validate the authorization
return auth, stale
elif nstate == N_REFRESH:
stale = True
encrypt = httpauth.DIGEST_AUTH_ENCODERS[httpauth.MD5]
if callable(users):
# A callable must return a tuple
userinfo = users(ah["username"])
else:
if not isinstance(users, dict):
raise ValueError, "Authentication users must be a dict of tuples"
# fetch the user password
userinfo = users.get(ah["username"], None)
if not isinstance(userinfo, tuple):
raise ValueError, "Authentication users must be a dict of tuples"
if userinfo:
# Check that the user's role matches the role allowed
if not roles or check_roles(roles.get(cherrypy.request.method),
userinfo[1:]):
# Validate the authorization by re-computing it here
# and compare it with what the user-agent provided
if httpauth.checkResponse(ah, userinfo[0],
method=cherrypy.request.method,
encrypt=encrypt, realm=realm):
cherrypy.request.login = ah["username"]
cherrypy.request.loginroles = userinfo[1:]
return True, stale
cherrypy.request.login = False
cherrypy.request.loginroles = ()
return auth, stale
def restful_auth(realm, users, refresh, expires, logout=False, roles=None):
""" Authentication and authorisation tool for RESTful apps.
Authentication is performed by nonce-expiring HTTP digest authentication
providing a means of time-limiting logins an a means of logging users out.
Authorisation uses role-based access control.
Tool parameters are:
realm: a string containing the authentication realm.
users: a dict of the form: {username: (password, role1, ...)} or a
callable returning the password/roles tuple when passed the
username.
refresh: an integer giving the refresh time for nonces in seconds.
expires: an integer giving the maximum life of a nonce (from the last
refresh if refreshed). Zero indicates a one-time nonce. If less
than 'refresh' (but not zero) 'refresh' will be used as the
expiry time.
logout: a boolean value. True indicates that all nonces originating from
the client making the request to this resource should be expired
(regardless of realm). This allows a 'logout' function to be
implemented without the use of javascript, cookies or .htaccess
magic. A 'logout' link should point to a non-cacheable resource
that is protected by restful_auth with this flag set.
Default is False.
roles: a dict of the form {method1: (role1, role2, ...), ....}
The roles for the current method are compared to the user's
roles as returned from the 'users' parameter. If no match is
found the authentication fails. The default is an empty dict,
in which case this tool does not check roles - any authenticated
user will be authorised by this tool. The roles are available to
the page handler in cherrypy.resource.loginroles.
If the 'expires' parameter is greater than 'refresh', then when an nonce
has been issued for the time given by the 'refresh' parameter a new nonce
will be sent to the client together with the 'stale=true' flag, avoiding
the need for the user to re-enter her credentials. The 'refresh' timer is
then restarted. If both the 'refresh' and the 'expires' times have been
exceeded then the 'stale' flag will not be sent and the user must
re-authenticate.
"""
if logout:
do_logout()
return
auth, stale = check_auth(users, realm, expires, roles)
# Calculate the nonce and build the www-authenticate header here
# rather than use the function in httpauth because that function may give
# duplicate nonces since it only uses the seconds from time.time().
if auth:
if not stale:
return
else:
nonce = md5.new("%d:%s:%.6f" %
(time.time(), realm, random.random())).hexdigest()
www_auth = STALE_WWW_AUTH % (realm, nonce)
else:
nonce = md5.new("%d:%s:%.6f" %
(time.time(), realm, random.random())).hexdigest()
www_auth = STD_WWW_AUTH % (realm, nonce)
cherrypy.response.headers['www-authenticate'] = www_auth
# Record the new nonce
record_nonce(nonce, refresh, expires)
raise cherrypy.HTTPError(401,
"You are not authorized to access that resource")
cherrypy.tools.restful_auth = cherrypy.Tool('on_start_resource',
restful_auth)
}}}
'''Test Cases'''
{{{
""" Test cases for the restful_auth tool """
from cherrypy.test import test
test.prefer_parent_path()
import os
import time
import cherrypy
from cherrypy.lib import httpauth
import tools
def setup_server():
class Root:
def index(self):
return "This is public."
index.exposed = True
class Protected:
def index(self):
return "Hello %s, you've been authorized." % cherrypy.request.login
index.exposed = True
class Roles:
def index(self, *args, **kwargs):
if cherrypy.request.method == 'GET':
return "Hello %s, you've been authorized to GET." \
% cherrypy.request.login
elif cherrypy.request.method == 'POST':
return "Hello %s, you've been authorized to POST." \
% cherrypy.request.login
else:
raise cherrypy.HTTPError(405)
index.exposed = True
class Protected3:
def index(self):
return "Hello %s, you've been authorized." % cherrypy.request.login
index.exposed = True
class Protected4:
def index(self):
return "Hello %s, you've been authorized." % cherrypy.request.login
index.exposed = True
class Logout:
def index(self):
return "You are now logged out."
index.exposed = True
test_roles = {'test': ('test#', 'role1', 'role3'),
'postman': ('postman#', 'role2', 'role3')}
def fetch_user(user):
""" Return the user's password and login roles """
return test_roles.get(user)
conf = {'/': {'tools.restful_auth.on': False,
'tools.restful_auth.realm': 'localhost',
'tools.restful_auth.users': fetch_user,
'tools.restful_auth.refresh': 30,
'tools.restful_auth.expires': 60},
'/digest': { 'tools.restful_auth.on': True,},
'/refresh': { 'tools.restful_auth.on': True,
'tools.restful_auth.refresh': 1,
'tools.restful_auth.expires': 3},
'/onetime': { 'tools.restful_auth.on': True,
'tools.restful_auth.expires': 0},
'/logout': {'tools.restful_auth.on': True,
'tools.restful_auth.logout': True},
'/roles': {'tools.restful_auth.on': True,
'tools.restful_auth.roles': {'GET': ('role1', 'role3'),
'POST': ('role2',)}},
}
root = Root()
root.digest = Protected()
root.refresh = Protected()
root.onetime = Protected()
root.roles = Roles()
root.logout = Logout()
cherrypy.tree.mount(root, config=conf)
cherrypy.config.update({'environment': 'test_suite',
'log.error_file': os.path.join(os.path.dirname(__file__),
'cp_error_log'),
})
from cherrypy.test import helper
class RESTWebCase(helper.CPWebCase):
""" Add extra methods for our test cases """
def get_nonce(self, msg = None):
""" Extract the nonce from the www-authenticate request header """
www_auth = None
for k, v in self.headers:
if k.lower() == "www-authenticate":
www_auth = v
break
if www_auth is None:
self._handlewebError(
"Check nonce: www-authenticate header not found: %s" % msg)
return
nonce = None
www_auth = www_auth[7:]
items = www_auth.split(', ')
for item in items:
key, value = item.split('=')
if key.lower() == 'nonce':
nonce = value.strip('"')
break
if nonce is None:
self._handlewebError(
"Nonce was not found in www-authenticate header: %s" % msg)
return nonce
def calc_auth_hdr(self, nonce, path, user, password, method='GET'):
""" Return the authenticate header string """
base_auth = 'Digest username="%s", realm="localhost", nonce="%s", uri=%s, algorithm=MD5, response="%s", qop=auth, nc=%s, cnonce="1522e61005789929"'
auth = base_auth % (user, nonce, path, '', '00000001')
params = httpauth.parseAuthorization(auth)
response = httpauth._computeDigestResponse(params, password, method)
return base_auth % (user, nonce, path, response, '00000001')
def assertStale(self, msg = None):
""" Check that the stale flag is set in the www-authenticate header """
www_auth = None
for k, v in self.headers:
if k.lower() == "www-authenticate":
www_auth = v
break
if www_auth is None:
self._handlewebError(
"Checking stale flag on: : www-authenticate header not found", msg)
stale = None
www_auth = www_auth[7:]
items = www_auth.split(', ')
for item in items:
key, value = item.split('=')
if key.lower() == 'stale':
stale = value.strip('"')
break
if stale is None:
self._handlewebError(
"Stale flag not found in www-authenticate header", msg)
if stale != 'true':
self._handlewebError(
"Stale flag not set to 'true' in www-authenticate header")
return
def assertNotStale(self, msg = None):
""" Check that the stale flag is not set in the www-authenticate header """
www_auth = None
for k, v in self.headers:
if k.lower() == "www-authenticate":
www_auth = v
break
if www_auth is None:
self._handlewebError(
"Checking stale flag off: : www-authenticate header not found", msg)
stale = None
www_auth = www_auth[7:]
items = www_auth.split(', ')
for item in items:
key, value = item.split('=')
if key.lower() == 'stale':
stale = value.strip('"')
break
if stale:
self._handlewebError(
"Stale flag was found in www-authenticate header", msg)
return
class RESTAuthTest(RESTWebCase):
def testBasicRestfulDigest(self):
""" Basic restful auth test including test of digest auth """
path = "/digest/"
self.getPage(path)
self.assertStatus(401, 'Basic test first request')
value = None
for k, v in self.headers:
if k.lower() == "www-authenticate":
if v.startswith("Digest"):
value = v
break
if value is None:
self._handlewebError("Digest authentification scheme was not found")
value = value[7:]
items = value.split(', ')
tokens = {}
for item in items:
key, value = item.split('=')
tokens[key.lower()] = value
missing_msg = "%s is missing"
bad_value_msg = "'%s' was expecting '%s' but found '%s'"
nonce = None
if 'realm' not in tokens:
self._handlewebError(missing_msg % 'realm')
elif tokens['realm'] != '"localhost"':
self._handlewebError(bad_value_msg % ('realm', '"localhost"', tokens['realm']))
if 'nonce' not in tokens:
self._handlewebError(missing_msg % 'nonce')
else:
nonce = tokens['nonce'].strip('"')
if 'algorithm' not in tokens:
self._handlewebError(missing_msg % 'algorithm')
elif tokens['algorithm'] != '"MD5"':
self._handlewebError(bad_value_msg % ('algorithm', '"MD5"', tokens['algorithm']))
if 'qop' not in tokens:
self._handlewebError(missing_msg % 'qop')
elif tokens['qop'] != '"auth"':
self._handlewebError(bad_value_msg % ('qop', '"auth"', tokens['qop']))
auth = self.calc_auth_hdr(nonce, path, 'test', 'test#')
msg = 'Basic test after authentication'
self.getPage(path, [('Authorization', auth)])
self.assertStatus('200 OK', msg)
self.assertBody("Hello test, you've been authorized.", msg)
def testPublic(self):
""" Public access """
self.getPage("/")
msg = 'Public test'
self.assertStatus('200 OK', msg)
self.assertHeader('Content-Type', 'text/html', msg)
self.assertBody('This is public.', msg)
def testRefresh(self):
""" Test refreshing nonce """
path = "/refresh/"
self.getPage(path)
self.assertStatus(401, 'Refresh test first request')
nonce1 = self.get_nonce('Refresh test first nonce')
auth = self.calc_auth_hdr(nonce1, path, 'test', 'test#')
msg = 'Refresh test first authenticated request'
self.getPage(path, [('Authorization', auth)])
self.assertStatus('200 OK', msg)
self.assertBody("Hello test, you've been authorized.", msg)
# Wait two seconds, then check we get a new nonce with the stale flag
time.sleep(2)
self.getPage(path, [('Authorization', auth)])
msg = 'Refresh test - nonce requires refresh request'
self.assertStatus(401, msg)
self.assertStale(msg)
nonce2 = self.get_nonce('Refresh test second nonce')
self.assertNotEqual(nonce1, nonce2)
auth = self.calc_auth_hdr(nonce2, path, 'test', 'test#')
msg = 'Refresh test - refreshed nonce request'
self.getPage(path, [('Authorization', auth)])
self.assertStatus('200 OK', msg)
self.assertBody("Hello test, you've been authorized.", msg)
# Wait four seconds, then check we get a 401 without the stale flag
# and with a new nonce
time.sleep(4)
self.getPage(path, [('Authorization', auth)])
msg = 'Refresh test - expired nonce request'
self.assertStatus(401, msg)
self.assertNotStale(msg)
nonce3 = self.get_nonce('Refresh test third nonce')
self.assertNotEqual(nonce3, nonce2)
def testOneTime(self):
""" Test one-time nonce """
path = "/onetime/"
self.getPage(path)
self.assertStatus(401, 'One-time test first request')
nonce = self.get_nonce('One-time test nonce')
auth = self.calc_auth_hdr(nonce, path, 'test', 'test#')
self.getPage(path, [('Authorization', auth)])
msg = 'One-time test - THE request'
self.assertStatus('200 OK', msg)
self.assertBody("Hello test, you've been authorized.", msg)
# Now check the nonce has expired
self.getPage(path, [('Authorization', auth)])
self.assertStatus(401, 'One-time request on expired nonce')
def testLogout(self):
""" Test logout """
# First authenticate using the /digest path
path = "/digest/"
self.getPage(path)
self.assertStatus(401, 'Logout test first request')
nonce = self.get_nonce('Logout test nonce')
auth = self.calc_auth_hdr(nonce, path, 'test', 'test#')
self.getPage(path, [('Authorization', auth)])
msg = 'Logout test after authentication'
self.assertStatus('200 OK', msg)
self.assertBody("Hello test, you've been authorized.", msg)
# Now logout and check we get a 401 on a subsequent access to /digest
path = '/logout/'
msg = 'Logout test - request to Logout page'
self.getPage('/logout/', [('Authorization', auth)])
self.assertStatus('200 OK', msg)
self.assertBody("You are now logged out.", msg)
self.getPage('/digest/', [('Authorization', auth)])
self.assertStatus(401, 'Logout test - request after logout')
def testRoles(self):
""" Test login roles """
# GET a page as 'test'
path = "/roles/"
self.getPage(path)
self.assertStatus(401, "Role test first request as 'test'")
nonce = self.get_nonce("'Role test nonce for 'test'")
auth = self.calc_auth_hdr(nonce, path, 'test', 'test#')
msg = "Role test authorised GET as 'test'"
self.getPage(path, [('Authorization', auth)])
self.assertStatus('200 OK', msg)
self.assertBody("Hello test, you've been authorized to GET.", msg)
# POST some data as 'test'
msg = "Role test un-authorised POST as 'test'"
self.getPage(path, [('Authorization', auth)], 'POST', 'some data')
self.assertStatus(401, msg)
# Try again with the correct method in the response
nonce = self.get_nonce("'Role test POST nonce for 'test'")
auth = self.calc_auth_hdr(nonce, path, 'test', 'test#', 'POST')
msg = "Role test 'authorised' POST as 'test'"
self.getPage(path, [('Authorization', auth)], 'POST', 'some data')
self.assertStatus(401, msg)
# POST some data as 'postman'
auth = self.calc_auth_hdr(nonce, path, 'postman', 'postman#')
self.getPage(path, [('Authorization', auth)], 'POST', 'some data')
self.assertStatus(401, "Role test first POST as 'postman'")
nonce = self.get_nonce("Role test nonce for 'postman'")
auth = self.calc_auth_hdr(nonce, path, 'postman', 'postman#', 'POST')
self.getPage(path, [('Authorization', auth)], 'POST', 'some data')
msg = "Role test authorised POST as 'postman'"
self.assertStatus('200 OK', msg)
self.assertBody("Hello postman, you've been authorized to POST.", msg)
if __name__ == "__main__":
setup_server()
helper.testmain()
}}}