comparison test/rest_common.py @ 7809:be6cb2e0d471

feat: add support for rotating jwt keys This allows jwt_secret to have multiple ',' separated secrets. The first/leftmost should be used to sign new JWTs. All of them are used (starting from left/newest) to try to verify a JWT. If the first secret is < 32 chars in length JWTs are disabled. If any of the other secrets are < 32 chars, the configuration code causes the software to exit. This prevents insecure (too short) secrets from being used. Updated doc examples and tests.
author John Rouillard <rouilj@ieee.org>
date Thu, 14 Mar 2024 19:04:19 -0400
parents 8f29e4ea05ce
children 03c1b7ae3a68
comparison
equal deleted inserted replaced
7808:6c5f8da9fca7 7809:be6cb2e0d471
138 138
139 if jwt: 139 if jwt:
140 # must be 32 chars in length minimum (I think this is at least 140 # must be 32 chars in length minimum (I think this is at least
141 # 256 bits of data) 141 # 256 bits of data)
142 142
143 secret = "TestingTheJwtSecretTestingTheJwtSecret" 143 self.old_secret = "TestingTheJwtSecretTestingTheJwtSecret"
144 self.db.config['WEB_JWT_SECRET'] = secret 144 self.new_secret = "TestingTheNEW JwtSecretTestingTheNEWJwtSecret"
145 self.db.config['WEB_JWT_SECRET'] = self.old_secret
145 146
146 # generate all timestamps in UTC. 147 # generate all timestamps in UTC.
147 base_datetime = datetime(1970, 1, 1, tzinfo=myutc) 148 base_datetime = datetime(1970, 1, 1, tzinfo=myutc)
148 149
149 # A UTC timestamp for now. 150 # A UTC timestamp for now.
179 self.claim = {} 180 self.claim = {}
180 # generate invalid claim with expired timestamp 181 # generate invalid claim with expired timestamp
181 self.claim['expired'] = copy(claim) 182 self.claim['expired'] = copy(claim)
182 self.claim['expired']['exp'] = expired_ts 183 self.claim['expired']['exp'] = expired_ts
183 self.jwt['expired'] = tostr(jwt.encode( 184 self.jwt['expired'] = tostr(jwt.encode(
184 self.claim['expired'], secret, 185 self.claim['expired'], self.old_secret,
185 algorithm='HS256')) 186 algorithm='HS256'))
186 187
187 # generate valid claim with user role 188 # generate valid claim with user role
188 self.claim['user'] = copy(claim) 189 self.claim['user'] = copy(claim)
189 self.claim['user']['exp'] = plus1min_ts 190 self.claim['user']['exp'] = plus1min_ts
190 self.jwt['user'] = tostr(jwt.encode( 191 self.jwt['user'] = tostr(jwt.encode(
191 self.claim['user'], secret, 192 self.claim['user'], self.old_secret,
192 algorithm='HS256')) 193 algorithm='HS256'))
194
195 # generate valid claim with user role and new secret
196 self.claim['user_new_secret'] = copy(claim)
197 self.claim['user_new_secret']['exp'] = plus1min_ts
198 self.jwt['user_new_secret'] = tostr(jwt.encode(
199 self.claim['user'], self.new_secret,
200 algorithm='HS256'))
201
193 # generate invalid claim bad issuer 202 # generate invalid claim bad issuer
194 self.claim['badiss'] = copy(claim) 203 self.claim['badiss'] = copy(claim)
195 self.claim['badiss']['iss'] = "http://someissuer/bugs" 204 self.claim['badiss']['iss'] = "http://someissuer/bugs"
196 self.jwt['badiss'] = tostr(jwt.encode( 205 self.jwt['badiss'] = tostr(jwt.encode(
197 self.claim['badiss'], secret, 206 self.claim['badiss'], self.old_secret,
198 algorithm='HS256')) 207 algorithm='HS256'))
199 # generate invalid claim bad aud(ience) 208 # generate invalid claim bad aud(ience)
200 self.claim['badaud'] = copy(claim) 209 self.claim['badaud'] = copy(claim)
201 self.claim['badaud']['aud'] = "http://someaudience/bugs" 210 self.claim['badaud']['aud'] = "http://someaudience/bugs"
202 self.jwt['badaud'] = tostr(jwt.encode( 211 self.jwt['badaud'] = tostr(jwt.encode(
203 self.claim['badaud'], secret, 212 self.claim['badaud'], self.old_secret,
204 algorithm='HS256')) 213 algorithm='HS256'))
205 # generate invalid claim bad sub(ject) 214 # generate invalid claim bad sub(ject)
206 self.claim['badsub'] = copy(claim) 215 self.claim['badsub'] = copy(claim)
207 self.claim['badsub']['sub'] = str("99") 216 self.claim['badsub']['sub'] = str("99")
208 self.jwt['badsub'] = tostr( 217 self.jwt['badsub'] = tostr(
209 jwt.encode(self.claim['badsub'], secret, 218 jwt.encode(self.claim['badsub'], self.old_secret,
210 algorithm='HS256')) 219 algorithm='HS256'))
211 # generate invalid claim bad roles 220 # generate invalid claim bad roles
212 self.claim['badroles'] = copy(claim) 221 self.claim['badroles'] = copy(claim)
213 self.claim['badroles']['roles'] = ["badrole1", "badrole2"] 222 self.claim['badroles']['roles'] = ["badrole1", "badrole2"]
214 self.jwt['badroles'] = tostr(jwt.encode( 223 self.jwt['badroles'] = tostr(jwt.encode(
215 self.claim['badroles'], secret, 224 self.claim['badroles'], self.old_secret,
216 algorithm='HS256')) 225 algorithm='HS256'))
217 # generate valid claim with limited user:email role 226 # generate valid claim with limited user:email role
218 self.claim['user:email'] = copy(claim) 227 self.claim['user:email'] = copy(claim)
219 self.claim['user:email']['roles'] = ["user:email"] 228 self.claim['user:email']['roles'] = ["user:email"]
220 self.jwt['user:email'] = tostr(jwt.encode( 229 self.jwt['user:email'] = tostr(jwt.encode(
221 self.claim['user:email'], secret, 230 self.claim['user:email'], self.old_secret,
222 algorithm='HS256')) 231 algorithm='HS256'))
223 232
224 # generate valid claim with limited user:emailnorest role 233 # generate valid claim with limited user:emailnorest role
225 self.claim['user:emailnorest'] = copy(claim) 234 self.claim['user:emailnorest'] = copy(claim)
226 self.claim['user:emailnorest']['roles'] = ["user:emailnorest"] 235 self.claim['user:emailnorest']['roles'] = ["user:emailnorest"]
227 self.jwt['user:emailnorest'] = tostr(jwt.encode( 236 self.jwt['user:emailnorest'] = tostr(jwt.encode(
228 self.claim['user:emailnorest'], secret, 237 self.claim['user:emailnorest'], self.old_secret,
229 algorithm='HS256')) 238 algorithm='HS256'))
230 239
231 self.db.tx_Source = 'web' 240 self.db.tx_Source = 'web'
232 241
233 self.db.issue.addprop(tx_Source=hyperdb.String()) 242 self.db.issue.addprop(tx_Source=hyperdb.String())
3627 # we need a new test with setup called for each test 3636 # we need a new test with setup called for each test
3628 out = [] 3637 out = []
3629 def wh(s): 3638 def wh(s):
3630 out.append(s) 3639 out.append(s)
3631 3640
3632 secret = self.db.config.WEB_JWT_SECRET 3641 secret = self.db.config.WEB_JWT_SECRET[0]
3633 3642
3634 # verify library and tokens are correct 3643 # verify library and tokens are correct
3635 self.assertRaises(jwt.exceptions.InvalidTokenError, 3644 self.assertRaises(jwt.exceptions.InvalidTokenError,
3636 jwt.decode, self.jwt['expired'], 3645 jwt.decode, self.jwt['expired'],
3637 secret, algorithms=['HS256'], 3646 secret, algorithms=['HS256'],
3680 # this will be the admin still as auth failed 3689 # this will be the admin still as auth failed
3681 self.assertEqual('1', self.db.getuid()) 3690 self.assertEqual('1', self.db.getuid())
3682 self.assertEqual(out[0], b'Invalid Login - Signature has expired') 3691 self.assertEqual(out[0], b'Invalid Login - Signature has expired')
3683 del(out[0]) 3692 del(out[0])
3684 3693
3694 @skip_jwt
3695 def test_user_jwt_key_rotation_mutlisig(self):
3696 # self.dummy_client.main() closes database, so
3697 # we need a new test with setup called for each test
3698 out = []
3699 def wh(s):
3700 out.append(s)
3701
3702 # verify library and tokens are correct
3703 self.assertRaises(jwt.exceptions.InvalidTokenError,
3704 jwt.decode, self.jwt['expired'],
3705 self.old_secret, algorithms=['HS256'],
3706 audience=self.db.config.TRACKER_WEB,
3707 issuer=self.db.config.TRACKER_WEB)
3708
3709 result = jwt.decode(self.jwt['user_new_secret'],
3710 self.new_secret, algorithms=['HS256'],
3711 audience=self.db.config.TRACKER_WEB,
3712 issuer=self.db.config.TRACKER_WEB)
3713 self.assertEqual(self.claim['user'],result)
3714
3715 result = jwt.decode(self.jwt['user:email'],
3716 self.old_secret, algorithms=['HS256'],
3717 audience=self.db.config.TRACKER_WEB,
3718 issuer=self.db.config.TRACKER_WEB)
3719 self.assertEqual(self.claim['user:email'],result)
3720
3721 # set environment for all jwt tests
3722 env = {
3723 'PATH_INFO': 'rest/data/user',
3724 'HTTP_HOST': 'localhost',
3725 'TRACKER_NAME': 'rounduptest',
3726 "REQUEST_METHOD": "GET"
3727 }
3728
3729 # test case where rotation key is used,
3730 # add spaces after ',' to test config system
3731 self.db.config['WEB_JWT_SECRET'] = "%s, %s, " % (
3732 self.new_secret, self.old_secret
3733 )
3734
3735 self.dummy_client = client.Client(self.instance, MockNull(), env,
3736 [], None)
3737 self.dummy_client.db = self.db
3738 self.dummy_client.request.headers.get = self.get_header
3739 self.empty_form = cgi.FieldStorage()
3740 self.terse_form = cgi.FieldStorage()
3741 self.terse_form.list = [
3742 cgi.MiniFieldStorage('@verbose', '0'),
3743 ]
3744 self.dummy_client.form = cgi.FieldStorage()
3745 self.dummy_client.form.list = [
3746 cgi.MiniFieldStorage('@fields', 'username,address'),
3747 ]
3748 # accumulate json output for further analysis
3749 self.dummy_client.write = wh
3750
3751 # set up for standard user role token
3752 env['HTTP_AUTHORIZATION'] = 'bearer %s'%self.jwt['user']
3753
3754 self.dummy_client.main()
3755 print(out[0])
3756 json_dict = json.loads(b2s(out[0]))
3757 print(json_dict)
3758 # user will be joe id 3 as auth works
3759 self.assertTrue('3', self.db.getuid())
3760 # there should be three items in the collection admin, anon, and joe
3761 self.assertEqual(3, len(json_dict['data']['collection']))
3762 # since this token has no access to email addresses, only joe
3763 # should have email addresses. Order is by id by default.
3764 self.assertFalse('address' in json_dict['data']['collection'][0])
3765 self.assertFalse('address' in json_dict['data']['collection'][1])
3766 self.assertTrue('address' in json_dict['data']['collection'][2])
3767 del(out[0])
3768 self.db.setCurrentUser('admin')
3769
3770 @skip_jwt
3771 def test_user_jwt_key_rotation_sig_failure(self):
3772 # self.dummy_client.main() closes database, so
3773 # we need a new test with setup called for each test
3774 out = []
3775 def wh(s):
3776 out.append(s)
3777
3778 # verify library and tokens are correct
3779 self.assertRaises(jwt.exceptions.InvalidTokenError,
3780 jwt.decode, self.jwt['expired'],
3781 self.old_secret, algorithms=['HS256'],
3782 audience=self.db.config.TRACKER_WEB,
3783 issuer=self.db.config.TRACKER_WEB)
3784
3785 result = jwt.decode(self.jwt['user_new_secret'],
3786 self.new_secret, algorithms=['HS256'],
3787 audience=self.db.config.TRACKER_WEB,
3788 issuer=self.db.config.TRACKER_WEB)
3789 self.assertEqual(self.claim['user'],result)
3790
3791 result = jwt.decode(self.jwt['user:email'],
3792 self.old_secret, algorithms=['HS256'],
3793 audience=self.db.config.TRACKER_WEB,
3794 issuer=self.db.config.TRACKER_WEB)
3795 self.assertEqual(self.claim['user:email'],result)
3796
3797 # set environment for all jwt tests
3798 env = {
3799 'PATH_INFO': 'rest/data/user',
3800 'HTTP_HOST': 'localhost',
3801 'TRACKER_NAME': 'rounduptest',
3802 "REQUEST_METHOD": "GET"
3803 }
3804
3805 self.dummy_client = client.Client(self.instance, MockNull(), env,
3806 [], None)
3807 self.dummy_client.db = self.db
3808 self.dummy_client.request.headers.get = self.get_header
3809 self.empty_form = cgi.FieldStorage()
3810 self.terse_form = cgi.FieldStorage()
3811 self.terse_form.list = [
3812 cgi.MiniFieldStorage('@verbose', '0'),
3813 ]
3814 self.dummy_client.form = cgi.FieldStorage()
3815 self.dummy_client.form.list = [
3816 cgi.MiniFieldStorage('@fields', 'username,address'),
3817 ]
3818 # accumulate json output for further analysis
3819 self.dummy_client.write = wh
3820
3821 # test case where new json secret is in place
3822 self.db.config['WEB_JWT_SECRET'] = self.new_secret
3823
3824 # set up for standard user role token
3825 env['HTTP_AUTHORIZATION'] = 'bearer %s'%self.jwt['user']
3826 self.dummy_client.main()
3827 print(out[0])
3828
3829 self.assertEqual(out[0],
3830 b'Invalid Login - Signature verification failed')
3831 del(out[0])
3832 self.db.setCurrentUser('admin')
3685 3833
3686 @skip_jwt 3834 @skip_jwt
3687 def test_user_jwt(self): 3835 def test_user_jwt(self):
3688 # self.dummy_client.main() closes database, so 3836 # self.dummy_client.main() closes database, so
3689 # we need a new test with setup called for each test 3837 # we need a new test with setup called for each test
3690 out = [] 3838 out = []
3691 def wh(s): 3839 def wh(s):
3692 out.append(s) 3840 out.append(s)
3693 3841
3694 secret = self.db.config.WEB_JWT_SECRET 3842 secret = self.db.config.WEB_JWT_SECRET[0]
3695 3843
3696 # verify library and tokens are correct 3844 # verify library and tokens are correct
3697 self.assertRaises(jwt.exceptions.InvalidTokenError, 3845 self.assertRaises(jwt.exceptions.InvalidTokenError,
3698 jwt.decode, self.jwt['expired'], 3846 jwt.decode, self.jwt['expired'],
3699 secret, algorithms=['HS256'], 3847 secret, algorithms=['HS256'],
3760 # we need a new test with setup called for each test 3908 # we need a new test with setup called for each test
3761 out = [] 3909 out = []
3762 def wh(s): 3910 def wh(s):
3763 out.append(s) 3911 out.append(s)
3764 3912
3765 secret = self.db.config.WEB_JWT_SECRET 3913 secret = self.db.config.WEB_JWT_SECRET[0]
3766 3914
3767 # verify library and tokens are correct 3915 # verify library and tokens are correct
3768 self.assertRaises(jwt.exceptions.InvalidTokenError, 3916 self.assertRaises(jwt.exceptions.InvalidTokenError,
3769 jwt.decode, self.jwt['expired'], 3917 jwt.decode, self.jwt['expired'],
3770 secret, algorithms=['HS256'], 3918 secret, algorithms=['HS256'],
3828 # we need a new test with setup called for each test 3976 # we need a new test with setup called for each test
3829 out = [] 3977 out = []
3830 def wh(s): 3978 def wh(s):
3831 out.append(s) 3979 out.append(s)
3832 3980
3833 secret = self.db.config.WEB_JWT_SECRET 3981 secret = self.db.config.WEB_JWT_SECRET[0]
3834 3982
3835 # verify library and tokens are correct 3983 # verify library and tokens are correct
3836 self.assertRaises(jwt.exceptions.InvalidTokenError, 3984 self.assertRaises(jwt.exceptions.InvalidTokenError,
3837 jwt.decode, self.jwt['expired'], 3985 jwt.decode, self.jwt['expired'],
3838 secret, algorithms=['HS256'], 3986 secret, algorithms=['HS256'],
3884 self.assertTrue('error' in json_dict) 4032 self.assertTrue('error' in json_dict)
3885 self.assertTrue(json_dict['error']['status'], 403) 4033 self.assertTrue(json_dict['error']['status'], 403)
3886 self.assertTrue(json_dict['error']['msg'], "Forbidden.") 4034 self.assertTrue(json_dict['error']['msg'], "Forbidden.")
3887 4035
3888 @skip_jwt 4036 @skip_jwt
3889 def test_disabled_jwt(self): 4037 def test_admin_disabled_jwt(self):
3890 # self.dummy_client.main() closes database, so 4038 # self.dummy_client.main() closes database, so
3891 # we need a new test with setup called for each test 4039 # we need a new test with setup called for each test
3892 out = [] 4040 out = []
3893 def wh(s): 4041 def wh(s):
3894 out.append(s) 4042 out.append(s)

Roundup Issue Tracker: http://roundup-tracker.org/