33
44import contextlib
55import json
6+ import secrets
67from urllib .parse import parse_qs , urlparse
78
89import responses
910from cryptography .hazmat .primitives import serialization
10- from cryptography .hazmat .primitives .asymmetric import rsa
11+ from cryptography .hazmat .primitives .asymmetric import ec , rsa
1112from jose import jwt
1213from jose .exceptions import JWTError
13- from jose .utils import long_to_base64
14+ from jose .utils import base64url_encode , long_to_base64
1415
1516import odoo
1617from odoo .exceptions import AccessDenied
@@ -39,11 +40,32 @@ def setUpClass(cls):
3940 cls .rsa_key_pem ,
4041 cls .rsa_key_public_pem ,
4142 cls .rsa_key_public_jwk ,
42- ) = cls ._generate_key ()
43- _ , cls .second_key_public_pem , _ = cls ._generate_key ()
43+ ) = cls ._generate_rsa_key ()
44+ _ , cls .second_key_public_pem , _ = cls ._generate_rsa_key ()
45+
46+ (
47+ cls .es256_key_pem ,
48+ cls .es256_key_public_pem ,
49+ cls .es256_key_public_jwk ,
50+ ) = cls ._generate_ec_key (curve = ec .SECP256R1 ())
51+
52+ (
53+ cls .es384_key_pem ,
54+ cls .es384_key_public_pem ,
55+ cls .es384_key_public_jwk ,
56+ ) = cls ._generate_ec_key (curve = ec .SECP384R1 ())
57+
58+ cls .hs256_key = secrets .token_bytes (32 )
59+ cls .hs256_jwk = {
60+ "kty" : "oct" ,
61+ "use" : "sig" ,
62+ "alg" : "HS256" ,
63+ "kid" : "hs256-key" ,
64+ "k" : base64url_encode (cls .hs256_key ).decode ("utf-8" ),
65+ }
4466
4567 @staticmethod
46- def _generate_key ():
68+ def _generate_rsa_key ():
4769 rsa_key = rsa .generate_private_key (
4870 public_exponent = 65537 ,
4971 key_size = 4096 ,
@@ -67,6 +89,38 @@ def _generate_key():
6789 }
6890 return rsa_key_pem , rsa_key_public_pem , jwk
6991
92+ @staticmethod
93+ def _generate_ec_key (curve ):
94+ ec_key = ec .generate_private_key (curve )
95+ ec_key_pem = ec_key .private_bytes (
96+ serialization .Encoding .PEM ,
97+ serialization .PrivateFormat .TraditionalOpenSSL ,
98+ serialization .NoEncryption (),
99+ ).decode ("utf8" )
100+ ec_key_public = ec_key .public_key ()
101+ ec_key_public_pem = ec_key_public .public_bytes (
102+ serialization .Encoding .PEM ,
103+ serialization .PublicFormat .SubjectPublicKeyInfo ,
104+ ).decode ("utf8" )
105+
106+ curve_name = "P-256" if isinstance (curve , ec .SECP256R1 ) else "P-384"
107+ alg = "ES256" if isinstance (curve , ec .SECP256R1 ) else "ES384"
108+
109+ public_numbers = ec_key_public .public_numbers ()
110+ x = long_to_base64 (public_numbers .x ).decode ("utf-8" )
111+ y = long_to_base64 (public_numbers .y ).decode ("utf-8" )
112+
113+ jwk = {
114+ "kty" : "EC" ,
115+ "use" : "sig" ,
116+ "crv" : curve_name ,
117+ "alg" : alg ,
118+ "x" : x ,
119+ "y" : y ,
120+ "kid" : "ec-key-" + alg .lower (),
121+ }
122+ return ec_key_pem , ec_key_public_pem , jwk
123+
70124 def setUp (self ):
71125 super ().setUp ()
72126 # search our test provider and bind the demo user to it
@@ -111,30 +165,70 @@ def _prepare_login_test_user(self):
111165 return user
112166
113167 def _prepare_login_test_responses (
114- self , access_token = "42" , id_token_body = None , id_token_headers = None , keys = None
168+ self ,
169+ access_token = "42" ,
170+ id_token_body = None ,
171+ id_token_headers = None ,
172+ keys = None ,
173+ algorithm = "RS256" ,
174+ private_key = None ,
175+ public_key = None ,
115176 ):
116177 if id_token_body is None :
117178 id_token_body = {}
118179 if id_token_headers is None :
119180 id_token_headers = {"kid" : "the_key_id" }
181+
182+ if private_key is None :
183+ if algorithm == "RS256" :
184+ private_key = self .rsa_key_pem
185+ public_key_pem = self .rsa_key_public_pem
186+ elif algorithm == "ES256" :
187+ private_key = self .es256_key_pem
188+ public_key_pem = self .es256_key_public_pem
189+ elif algorithm == "ES384" :
190+ private_key = self .es384_key_pem
191+ public_key_pem = self .es384_key_public_pem
192+ elif algorithm == "HS256" :
193+ private_key = self .hs256_key
194+ # For HS256, we don't use public_key_pem as it's a symmetric key
195+ public_key_pem = None
196+ else :
197+ # For asymmetric algorithms, public_key_pem is needed
198+ public_key_pem = public_key or private_key
199+
120200 responses .add (
121201 responses .POST ,
122202 "http://localhost:8080/auth/realms/master/protocol/openid-connect/token" ,
123203 json = {
124204 "access_token" : access_token ,
125205 "id_token" : jwt .encode (
126206 id_token_body ,
127- self . rsa_key_pem ,
128- algorithm = "RS256" ,
207+ private_key ,
208+ algorithm = algorithm ,
129209 headers = id_token_headers ,
130210 ),
131211 },
132212 )
213+
214+ # Handle the keys parameter based on the algorithm
133215 if keys is None :
134- if "kid" in id_token_headers :
135- keys = [{"kid" : "the_key_id" , "keys" : [self .rsa_key_public_pem ]}]
216+ if algorithm == "HS256" :
217+ # For HS256, we use the JWK directly
218+ keys = [self .hs256_jwk ]
219+ elif algorithm == "ES256" :
220+ # For ES256, we use the JWK directly
221+ keys = [self .es256_key_public_jwk ]
222+ elif algorithm == "ES384" :
223+ # For ES384, we use the JWK directly
224+ keys = [self .es384_key_public_jwk ]
136225 else :
137- keys = [{"keys" : [self .rsa_key_public_pem ]}]
226+ # For RS256, we use the traditional approach
227+ if "kid" in id_token_headers :
228+ keys = [{"kid" : id_token_headers ["kid" ], "keys" : [public_key_pem ]}]
229+ else :
230+ keys = [{"keys" : [public_key_pem ]}]
231+
138232 responses .add (
139233 responses .GET ,
140234 "http://localhost:8080/auth/realms/master/protocol/openid-connect/certs" ,
@@ -257,6 +351,28 @@ def test_login_without_any_key(self):
257351 {"state" : json .dumps ({})},
258352 )
259353
354+ @responses .activate
355+ def test_login_with_custom_keys (self ):
356+ """Test that login works with custom provided keys"""
357+ user = self ._prepare_login_test_user ()
358+ # Generate a new RSA key for this test
359+ custom_key_pem , custom_key_public_pem , _ = self ._generate_rsa_key ()
360+
361+ self ._prepare_login_test_responses (
362+ id_token_body = {"user_id" : user .login },
363+ private_key = custom_key_pem ,
364+ public_key = custom_key_public_pem ,
365+ access_token = "custom_key_token" ,
366+ )
367+
368+ with MockRequest (self .env ):
369+ db , login , token = self .env ["res.users" ].auth_oauth (
370+ self .provider_rec .id ,
371+ {"state" : json .dumps ({})},
372+ )
373+ self .assertEqual (token , "custom_key_token" )
374+ self .assertEqual (login , user .login )
375+
260376 @responses .activate
261377 def test_login_with_multiple_keys_in_jwks (self ):
262378 """Test that login works with multiple keys present in jwks"""
@@ -317,3 +433,63 @@ def test_login_with_jwk_format(self):
317433 )
318434 self .assertEqual (token , "122/3" )
319435 self .assertEqual (login , user .login )
436+
437+ @responses .activate
438+ def test_login_with_es256_algorithm (self ):
439+ """Test that login works with ES256 algorithm"""
440+ user = self ._prepare_login_test_user ()
441+
442+ self ._prepare_login_test_responses (
443+ id_token_body = {"user_id" : user .login },
444+ id_token_headers = {"kid" : self .es256_key_public_jwk ["kid" ]},
445+ algorithm = "ES256" ,
446+ access_token = "es256token" ,
447+ )
448+
449+ with MockRequest (self .env ):
450+ db , login , token = self .env ["res.users" ].auth_oauth (
451+ self .provider_rec .id ,
452+ {"state" : json .dumps ({})},
453+ )
454+ self .assertEqual (token , "es256token" )
455+ self .assertEqual (login , user .login )
456+
457+ @responses .activate
458+ def test_login_with_es384_algorithm (self ):
459+ """Test that login works with ES384 algorithm"""
460+ user = self ._prepare_login_test_user ()
461+
462+ self ._prepare_login_test_responses (
463+ id_token_body = {"user_id" : user .login },
464+ id_token_headers = {"kid" : self .es384_key_public_jwk ["kid" ]},
465+ algorithm = "ES384" ,
466+ access_token = "es384token" ,
467+ )
468+
469+ with MockRequest (self .env ):
470+ db , login , token = self .env ["res.users" ].auth_oauth (
471+ self .provider_rec .id ,
472+ {"state" : json .dumps ({})},
473+ )
474+ self .assertEqual (token , "es384token" )
475+ self .assertEqual (login , user .login )
476+
477+ @responses .activate
478+ def test_login_with_hs256_algorithm (self ):
479+ """Test that login works with HS256 algorithm"""
480+ user = self ._prepare_login_test_user ()
481+
482+ self ._prepare_login_test_responses (
483+ id_token_body = {"user_id" : user .login },
484+ id_token_headers = {"kid" : self .hs256_jwk ["kid" ]},
485+ algorithm = "HS256" ,
486+ access_token = "hs256token" ,
487+ )
488+
489+ with MockRequest (self .env ):
490+ db , login , token = self .env ["res.users" ].auth_oauth (
491+ self .provider_rec .id ,
492+ {"state" : json .dumps ({})},
493+ )
494+ self .assertEqual (token , "hs256token" )
495+ self .assertEqual (login , user .login )
0 commit comments