Repository: jpadilla/pyjwt
Pull request #913 (2023-08-23 17:01:22)

Update jwt-api to accept either a string or list of strings for issuer validation
This diff updates jwt issuer validation to accept either a string (as it always has) or a `List[str]`, and consideres the jwt `iss` claim valid if the single string matches or if `jwt[iss]` is found in the list.  Method signatures should be unchanged, though types were updated.

mattpollak: If there is anything else I can do to get this merged, please let me know!  And thank you for reviewing.  

Patch:
diff --git a/jwt/api_jwt.py b/jwt/api_jwt.py
index 48d739a..87883bc 100644
--- a/jwt/api_jwt.py
+++ b/jwt/api_jwt.py
@@ -5,7 +5,7 @@ import warnings
 from calendar import timegm
 from collections.abc import Iterable
 from datetime import datetime, timedelta, timezone
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, List
 
 from . import api_jws
 from .exceptions import (
@@ -110,7 +110,7 @@ class PyJWT:
         # passthrough arguments to _validate_claims
         # consider putting in options
         audience: str | Iterable[str] | None = None,
-        issuer: str | None = None,
+        issuer: str | List[str] | None = None,
         leeway: float | timedelta = 0,
         # kwargs
         **kwargs: Any,
@@ -195,7 +195,7 @@ class PyJWT:
         # passthrough arguments to _validate_claims
         # consider putting in options
         audience: str | Iterable[str] | None = None,
-        issuer: str | None = None,
+        issuer: str | List[str] | None = None,
         leeway: float | timedelta = 0,
         # kwargs
         **kwargs: Any,
@@ -300,7 +300,7 @@ class PyJWT:
         try:
             exp = int(payload["exp"])
         except ValueError:
-            raise DecodeError("Expiration Time claim (exp) must be an" " integer.")
+            raise DecodeError("Expiration Time claim (exp) must be an integer.")
 
         if exp <= (now - leeway):
             raise ExpiredSignatureError("Signature has expired")
@@ -362,8 +362,12 @@ class PyJWT:
         if "iss" not in payload:
             raise MissingRequiredClaimError("iss")
 
-        if payload["iss"] != issuer:
-            raise InvalidIssuerError("Invalid issuer")
+        if isinstance(issuer, list):
+            if payload["iss"] not in issuer:
+                raise InvalidIssuerError("Invalid issuer")
+        else:
+            if payload["iss"] != issuer:
+                raise InvalidIssuerError("Invalid issuer")
 
 
 _jwt_global_obj = PyJWT()


Test patch:
diff --git a/tests/test_api_jwt.py b/tests/test_api_jwt.py
index b51170b..509f788 100644
--- a/tests/test_api_jwt.py
+++ b/tests/test_api_jwt.py
@@ -486,6 +486,12 @@ class TestJWT:
         token = jwt.encode(payload, "secret")
         jwt.decode(token, "secret", issuer=issuer, algorithms=["HS256"])
 
+    def test_check_issuer_list_when_valid(self, jwt):
+        issuer = ["urn:foo", "urn:bar"]
+        payload = {"some": "payload", "iss": "urn:foo"}
+        token = jwt.encode(payload, "secret")
+        jwt.decode(token, "secret", issuer=issuer, algorithms=["HS256"])
+
     def test_raise_exception_invalid_issuer(self, jwt):
         issuer = "urn:wrong"
 
@@ -496,6 +502,16 @@ class TestJWT:
         with pytest.raises(InvalidIssuerError):
             jwt.decode(token, "secret", issuer=issuer, algorithms=["HS256"])
 
+    def test_raise_exception_invalid_issuer_list(self, jwt):
+        issuer = ["urn:wrong", "urn:bar", "urn:baz"]
+
+        payload = {"some": "payload", "iss": "urn:foo"}
+
+        token = jwt.encode(payload, "secret")
+
+        with pytest.raises(InvalidIssuerError):
+            jwt.decode(token, "secret", issuer=issuer, algorithms=["HS256"])
+
     def test_skip_check_audience(self, jwt):
         payload = {"some": "payload", "aud": "urn:me"}
         token = jwt.encode(payload, "secret")