Skip to content

Commit b82079d

Browse files
committed
allow JWS header types both before GNAPv19 and after
1 parent 6f2e97d commit b82079d

File tree

4 files changed

+94
-6
lines changed

4 files changed

+94
-6
lines changed

src/auth_server/models/gnap.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
SupportedAlgorithms,
1313
SupportedHTTPMethods,
1414
SupportedJWSType,
15+
SupportedJWSTypeLegacy,
1516
SymmetricJWK,
1617
)
1718

@@ -273,7 +274,7 @@ class GrantResponse(GnapBaseModel):
273274
class GNAPJOSEHeader(JOSEHeader):
274275
kid: str
275276
alg: SupportedAlgorithms
276-
typ: SupportedJWSType
277+
typ: Union[SupportedJWSType, SupportedJWSTypeLegacy]
277278
htm: SupportedHTTPMethods
278279
# The HTTP URI used for this request, including all path and query components.
279280
uri: str

src/auth_server/models/jose.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,16 @@ class JWKS(BaseModel):
110110
keys: List[Union[ECJWK, RSAJWK, SymmetricJWK]]
111111

112112

113-
class SupportedJWSType(str, Enum):
113+
class SupportedJWSTypeLegacy(str, Enum):
114114
JWS = "gnap-binding+jws"
115115
JWSD = "gnap-binding+jwsd"
116116

117117

118+
class SupportedJWSType(str, Enum):
119+
JWS = "gnap-binding-jws"
120+
JWSD = "gnap-binding-jwsd"
121+
122+
118123
class JOSEHeader(BaseModel):
119124
kid: Optional[str] = None
120125
alg: SupportedAlgorithms

src/auth_server/proof/jws.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from auth_server.config import load_config
1313
from auth_server.context import ContextRequest
1414
from auth_server.models.gnap import GNAPJOSEHeader, Key
15-
from auth_server.models.jose import JWK, SupportedAlgorithms, SupportedJWSType
15+
from auth_server.models.jose import JWK, SupportedAlgorithms, SupportedJWSType, SupportedJWSTypeLegacy
1616
from auth_server.time_utils import utc_now
1717
from auth_server.utils import hash_with
1818

@@ -90,7 +90,7 @@ async def check_jws_proof(
9090
logger.error("Missing JWS header")
9191
raise HTTPException(status_code=400, detail=f"Missing JWS header: {e}")
9292

93-
if jws_header.typ is not SupportedJWSType.JWS:
93+
if jws_header.typ not in [SupportedJWSType.JWS, SupportedJWSTypeLegacy.JWS]:
9494
raise HTTPException(status_code=400, detail=f"typ should be {SupportedJWSType.JWS}")
9595

9696
return await verify_gnap_jws(request=request, gnap_key=gnap_key, jws_header=jws_header, access_token=access_token)
@@ -144,7 +144,7 @@ async def check_jwsd_proof(
144144
logger.error(f"Missing Detached JWS header: {e}")
145145
raise HTTPException(status_code=400, detail=str(e))
146146

147-
if jws_header.typ is not SupportedJWSType.JWSD:
147+
if jws_header.typ not in [SupportedJWSType.JWSD, SupportedJWSTypeLegacy.JWSD]:
148148
raise HTTPException(status_code=400, detail=f"typ should be {SupportedJWSType.JWSD}")
149149

150150
return await verify_gnap_jws(request=request, gnap_key=gnap_key, jws_header=jws_header, access_token=access_token)

src/auth_server/tests/test_app.py

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
ProofMethod,
3737
StartInteractionMethod,
3838
)
39-
from auth_server.models.jose import ECJWK, SupportedAlgorithms, SupportedHTTPMethods, SupportedJWSType
39+
from auth_server.models.jose import ECJWK, SupportedAlgorithms, SupportedHTTPMethods, SupportedJWSType, SupportedJWSTypeLegacy
4040
from auth_server.models.status import Status
4141
from auth_server.saml2 import AuthnInfo, NameID, SAMLAttributes, SessionInfo
4242
from auth_server.testing import MongoTemporaryInstance
@@ -321,6 +321,41 @@ def test_transaction_jws(self):
321321
claims = self._get_access_token_claims(access_token=access_token, client=self.client)
322322
assert claims["auth_source"] == AuthSource.TEST
323323

324+
def test_transaction_jws_legacy_typ(self):
325+
client_key_dict = self.client_jwk.export_public(as_dict=True)
326+
client_jwk = ECJWK(**client_key_dict)
327+
req = GrantRequest(
328+
client=Client(key=Key(proof=Proof(method=ProofMethod.JWS), jwk=client_jwk)),
329+
access_token=[AccessTokenRequest(flags=[AccessTokenFlags.BEARER])],
330+
)
331+
jws_header = {
332+
"typ": SupportedJWSTypeLegacy.JWS,
333+
"alg": SupportedAlgorithms.ES256.value,
334+
"kid": self.client_jwk.key_id,
335+
"htm": SupportedHTTPMethods.POST.value,
336+
"uri": "http://testserver/transaction",
337+
"created": int(utc_now().timestamp()),
338+
}
339+
_jws = jws.JWS(payload=req.json(exclude_unset=True))
340+
_jws.add_signature(
341+
key=self.client_jwk,
342+
protected=json.dumps(jws_header),
343+
)
344+
data = _jws.serialize(compact=True)
345+
346+
client_header = {"Content-Type": "application/jose"}
347+
response = self.client.post("/transaction", content=data, headers=client_header)
348+
349+
assert response.status_code == 200
350+
assert "access_token" in response.json()
351+
access_token = response.json()["access_token"]
352+
assert AccessTokenFlags.BEARER.value in access_token["flags"]
353+
assert access_token["value"] is not None
354+
# Verify token and check claims
355+
claims = self._get_access_token_claims(access_token=access_token, client=self.client)
356+
assert claims["auth_source"] == AuthSource.TEST
357+
358+
324359
def test_deserialize_bad_jws(self):
325360
client_header = {"Content-Type": "application/jose"}
326361
response = self.client.post("/transaction", content=b"bogus_jws", headers=client_header)
@@ -374,6 +409,53 @@ def test_transaction_jwsd(self):
374409
claims = self._get_access_token_claims(access_token=access_token, client=self.client)
375410
assert claims["auth_source"] == AuthSource.TEST
376411

412+
def test_transaction_jwsd_legacy_typ(self):
413+
client_key_dict = self.client_jwk.export_public(as_dict=True)
414+
client_jwk = ECJWK(**client_key_dict)
415+
req = GrantRequest(
416+
client=Client(key=Key(proof=Proof(method=ProofMethod.JWSD), jwk=client_jwk)),
417+
access_token=[AccessTokenRequest(flags=[AccessTokenFlags.BEARER])],
418+
)
419+
jws_header = {
420+
"typ": SupportedJWSTypeLegacy.JWSD,
421+
"alg": SupportedAlgorithms.ES256.value,
422+
"kid": self.client_jwk.key_id,
423+
"htm": SupportedHTTPMethods.POST.value,
424+
"uri": "http://testserver/transaction",
425+
"created": int(utc_now().timestamp()),
426+
}
427+
428+
payload = req.model_dump_json(exclude_unset=True)
429+
430+
# create a hash of payload to send in payload place
431+
payload_digest = hash_with(SHA256(), payload.encode())
432+
payload_hash = base64url_encode(payload_digest)
433+
434+
# create detached jws
435+
_jws = jws.JWS(payload=payload)
436+
_jws.add_signature(
437+
key=self.client_jwk,
438+
protected=json.dumps(jws_header),
439+
)
440+
data = _jws.serialize(compact=True)
441+
442+
# Remove payload from serialized jws
443+
header, _, signature = data.split(".")
444+
client_header = {"Detached-JWS": f"{header}.{payload_hash}.{signature}"}
445+
446+
response = self.client.post(
447+
"/transaction", content=req.model_dump_json(exclude_unset=True), headers=client_header
448+
)
449+
450+
assert response.status_code == 200
451+
assert "access_token" in response.json()
452+
access_token = response.json()["access_token"]
453+
assert AccessTokenFlags.BEARER.value in access_token["flags"]
454+
assert access_token["value"] is not None
455+
# Verify token and check claims
456+
claims = self._get_access_token_claims(access_token=access_token, client=self.client)
457+
assert claims["auth_source"] == AuthSource.TEST
458+
377459
@mock.patch("aiohttp.ClientSession.get", new_callable=AsyncMock)
378460
def test_mdq_flow(self, mock_mdq):
379461
self.config["auth_flows"] = json.dumps(["TestFlow", "MDQFlow"])

0 commit comments

Comments
 (0)