Skip to content

Commit 0abeb0f

Browse files
committed
Merge PR OCA#797 into 16.0
Signed-off-by sbidoul
2 parents d39fdff + 479301d commit 0abeb0f

3 files changed

Lines changed: 189 additions & 12 deletions

File tree

auth_oidc/models/auth_oauth_provider.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def _decode_id_token(self, access_token, id_token, kid):
9797
values = jwt.decode(
9898
id_token,
9999
key,
100-
algorithms=["RS256"],
100+
algorithms=["RS256", "ES256", "ES384", "HS256"],
101101
audience=self.client_id,
102102
access_token=access_token,
103103
)

auth_oidc/tests/test_auth_oidc_auth_code.py

Lines changed: 187 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33

44
import contextlib
55
import json
6+
import secrets
67
from urllib.parse import parse_qs, urlparse
78

89
import responses
910
from cryptography.hazmat.primitives import serialization
10-
from cryptography.hazmat.primitives.asymmetric import rsa
11+
from cryptography.hazmat.primitives.asymmetric import ec, rsa
1112
from jose import jwt
1213
from jose.exceptions import JWTError
13-
from jose.utils import long_to_base64
14+
from jose.utils import base64url_encode, long_to_base64
1415

1516
import odoo
1617
from 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)

readme/newsfragment/797.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
auth_oidc: add support for ES256, ES384, and HS256 when decoding OpenID Connect ID tokens.

0 commit comments

Comments
 (0)