@@ -85,12 +85,13 @@ def _mint_jwt(
8585def _make_local_auth (
8686 public_key : dict [str , object ],
8787 * ,
88- issuer : str = "https://auth.example.com" ,
88+ issuer : str | tuple [ str , ...] = "https://auth.example.com" ,
8989 audience : str | tuple [str , ...] = "https://api.example.com/vgi" ,
9090 principal_claim : str = "sub" ,
9191 domain : str = "jwt" ,
9292) -> Callable [[falcon .Request ], AuthContext ]:
9393 """Create a local JWT authenticate callback (no JWKS endpoint needed)."""
94+ issuers = (issuer ,) if isinstance (issuer , str ) else issuer
9495 audiences = (audience ,) if isinstance (audience , str ) else audience
9596
9697 def authenticate (req : falcon .Request ) -> AuthContext :
@@ -103,7 +104,7 @@ def authenticate(req: falcon.Request) -> AuthContext:
103104 raw_token ,
104105 public_key ,
105106 claims_options = {
106- "iss" : {"essential" : True , "value " : issuer },
107+ "iss" : {"essential" : True , "values " : list ( issuers ) },
107108 "aud" : {"essential" : True , "values" : list (audiences )},
108109 },
109110 )
@@ -816,6 +817,37 @@ def test_wrong_issuer_raises_value_error(self) -> None:
816817 with pytest .raises (ValueError ):
817818 auth_fn (req )
818819
820+ def test_multiple_issuers_first_matches (self ) -> None :
821+ """A JWT matching the first of multiple issuers is accepted."""
822+ priv , pub = _make_rsa_key ()
823+ iss1 = "https://auth.example.com"
824+ iss2 = "https://auth2.example.com"
825+ token = _mint_jwt (priv , iss = iss1 )
826+ auth_fn = _make_local_auth (pub , issuer = (iss1 , iss2 ))
827+ req = falcon .testing .helpers .create_req (headers = {"Authorization" : f"Bearer { token } " })
828+ auth = auth_fn (req )
829+ assert auth .authenticated is True
830+
831+ def test_multiple_issuers_second_matches (self ) -> None :
832+ """A JWT matching the second of multiple issuers is accepted."""
833+ priv , pub = _make_rsa_key ()
834+ iss1 = "https://auth.example.com"
835+ iss2 = "https://auth2.example.com"
836+ token = _mint_jwt (priv , iss = iss2 )
837+ auth_fn = _make_local_auth (pub , issuer = (iss1 , iss2 ))
838+ req = falcon .testing .helpers .create_req (headers = {"Authorization" : f"Bearer { token } " })
839+ auth = auth_fn (req )
840+ assert auth .authenticated is True
841+
842+ def test_multiple_issuers_none_match (self ) -> None :
843+ """A JWT with an unrecognized issuer is rejected."""
844+ priv , pub = _make_rsa_key ()
845+ token = _mint_jwt (priv , iss = "https://wrong.example.com" )
846+ auth_fn = _make_local_auth (pub , issuer = ("https://a.example.com" , "https://b.example.com" ))
847+ req = falcon .testing .helpers .create_req (headers = {"Authorization" : f"Bearer { token } " })
848+ with pytest .raises (ValueError ):
849+ auth_fn (req )
850+
819851 def test_missing_bearer_raises_value_error (self ) -> None :
820852 """Missing Authorization header raises ValueError."""
821853 _priv , pub = _make_rsa_key ()
@@ -909,3 +941,30 @@ def test_empty_audience_tuple_raises(self) -> None:
909941 audience = (),
910942 jwks_uri = "https://auth.example.com/.well-known/jwks.json" ,
911943 )
944+
945+ def test_empty_issuer_tuple_raises (self ) -> None :
946+ """Passing an empty issuer tuple raises ValueError eagerly."""
947+ with pytest .raises (ValueError , match = "issuer must not be empty" ):
948+ jwt_authenticate (
949+ issuer = (),
950+ audience = "https://api.example.com/vgi" ,
951+ jwks_uri = "https://auth.example.com/.well-known/jwks.json" ,
952+ )
953+
954+ def test_jwt_authenticate_factory_multiple_issuers (self ) -> None :
955+ """jwt_authenticate() accepts a tuple of issuers."""
956+ auth_fn = jwt_authenticate (
957+ issuer = ("https://auth.example.com" , "https://auth2.example.com" ),
958+ audience = "https://api.example.com/vgi" ,
959+ jwks_uri = "https://auth.example.com/.well-known/jwks.json" ,
960+ )
961+ assert callable (auth_fn )
962+
963+ def test_single_issuer_string_still_works (self ) -> None :
964+ """Passing issuer as a plain string still works (backwards compat)."""
965+ priv , pub = _make_rsa_key ()
966+ token = _mint_jwt (priv )
967+ auth_fn = _make_local_auth (pub , issuer = "https://auth.example.com" )
968+ req = falcon .testing .helpers .create_req (headers = {"Authorization" : f"Bearer { token } " })
969+ auth = auth_fn (req )
970+ assert auth .authenticated is True
0 commit comments