Repository: auth0/auth0-python
Pull request #507 (2023-07-25 11:59:10)

[SDK-4394] Add organization name validation
### Changes

Validate the `org_name` claim when `organization` config isn't an org id (doesn't include the `org_` prefix)



Patch:
diff --git a/auth0/authentication/async_token_verifier.py b/auth0/authentication/async_token_verifier.py
index 058e493..367e167 100644
--- a/auth0/authentication/async_token_verifier.py
+++ b/auth0/authentication/async_token_verifier.py
@@ -176,7 +176,7 @@ class AsyncTokenVerifier(TokenVerifier):
             token (str): The JWT to verify.
             nonce (str, optional): The nonce value sent during authentication.
             max_age (int, optional): The max_age value sent during authentication.
-            organization (str, optional): The expected organization ID (org_id) claim value. This should be specified
+            organization (str, optional): The expected organization ID (org_id) or orgnization name (org_name) claim value. This should be specified
             when logging in to an organization.
 
         Returns:
diff --git a/auth0/authentication/token_verifier.py b/auth0/authentication/token_verifier.py
index 030eda2..10fafee 100644
--- a/auth0/authentication/token_verifier.py
+++ b/auth0/authentication/token_verifier.py
@@ -299,7 +299,7 @@ class TokenVerifier:
             token (str): The JWT to verify.
             nonce (str, optional): The nonce value sent during authentication.
             max_age (int, optional): The max_age value sent during authentication.
-            organization (str, optional): The expected organization ID (org_id) claim value. This should be specified
+            organization (str, optional): The expected organization ID (org_id) or orgnization name (org_name) claim value. This should be specified
             when logging in to an organization.
 
         Returns:
@@ -402,16 +402,30 @@ class TokenVerifier:
 
         # Organization
         if organization:
-            if "org_id" not in payload or not isinstance(payload["org_id"], str):
-                raise TokenValidationError(
-                    "Organization (org_id) claim must be a string present in the ID"
-                    " token"
-                )
-            if payload["org_id"] != organization:
-                raise TokenValidationError(
-                    "Organization (org_id) claim mismatch in the ID token; expected"
-                    ' "{}", found "{}"'.format(organization, payload["org_id"])
-                )
+            if organization.startswith("org_"):
+                if "org_id" not in payload or not isinstance(payload["org_id"], str):
+                    raise TokenValidationError(
+                        "Organization (org_id) claim must be a string present in the ID"
+                        " token"
+                    )
+                if payload["org_id"] != organization:
+                    raise TokenValidationError(
+                        "Organization (org_id) claim mismatch in the ID token; expected"
+                        ' "{}", found "{}"'.format(organization, payload["org_id"])
+                    )
+            else:
+                if "org_name" not in payload or not isinstance(
+                    payload["org_name"], str
+                ):
+                    raise TokenValidationError(
+                        "Organization (org_name) claim must be a string present in the ID"
+                        " token"
+                    )
+                if payload["org_name"] != organization.lower():
+                    raise TokenValidationError(
+                        "Organization (org_name) claim mismatch in the ID token; expected"
+                        ' "{}", found "{}"'.format(organization, payload["org_name"])
+                    )
 
         # Authorized party
         if isinstance(payload["aud"], list) and len(payload["aud"]) > 1:


Test patch:
diff --git a/auth0/test/authentication/test_token_verifier.py b/auth0/test/authentication/test_token_verifier.py
index df2af2e..33dab69 100644
--- a/auth0/test/authentication/test_token_verifier.py
+++ b/auth0/test/authentication/test_token_verifier.py
@@ -506,7 +506,48 @@ class TestTokenVerifier(unittest.TestCase):
         tv._clock = MOCKED_CLOCK
         tv.verify(token, organization="org_123")
 
-    def test_fails_when_org_specified_but_not_present(self):
+    def test_fails_when_org_name_specified_but_not_present(self):
+        token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhdXRoMHxzZGs0NThma3MiLCJhdWQiOiJ0b2tlbnMtdGVzdC0xMjMiLCJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJleHAiOjE1ODc3NjUzNjEsImlhdCI6MTU4NzU5MjU2MX0.wotJnUdD5IfdZMewF_-BnHc0pI56uwzwr5qaSXvSu9w"
+        self.assert_fails_with_error(
+            token,
+            "Organization (org_name) claim must be a string present in the ID token",
+            signature_verifier=SymmetricSignatureVerifier(HMAC_SHARED_SECRET),
+            organization="org-123",
+        )
+
+    def test_fails_when_org_name_specified_but_not_string(self):
+        token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhdXRoMHxzZGs0NThma3MiLCJhdWQiOiJ0b2tlbnMtdGVzdC0xMjMiLCJvcmdfbmFtZSI6NDIsImlzcyI6Imh0dHBzOi8vdG9rZW5zLXRlc3QuYXV0aDAuY29tLyIsImV4cCI6MTU4Nzc2NTM2MSwiaWF0IjoxNTg3NTkyNTYxfQ.RXu-dz1u2pftk_iInk1To8z9g1B6TVA-5FAwoCx85T0"
+        self.assert_fails_with_error(
+            token,
+            "Organization (org_name) claim must be a string present in the ID token",
+            signature_verifier=SymmetricSignatureVerifier(HMAC_SHARED_SECRET),
+            organization="org-123",
+        )
+
+    def test_fails_when_org_name_specified_but_does_not_match(self):
+        token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhdXRoMHxzZGs0NThma3MiLCJhdWQiOiJ0b2tlbnMtdGVzdC0xMjMiLCJvcmdfbmFtZSI6Im9yZy1hYmMiLCJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJleHAiOjE1ODc3NjUzNjEsImlhdCI6MTU4NzU5MjU2MX0.P_ldJGEaFg58cARwGMtog_KTsqv7cGJZXoS9xdTEkvQ"
+        self.assert_fails_with_error(
+            token,
+            'Organization (org_name) claim mismatch in the ID token; expected "org-123",'
+            ' found "org-abc"',
+            signature_verifier=SymmetricSignatureVerifier(HMAC_SHARED_SECRET),
+            organization="org-123",
+        )
+
+    def test_succeeds_when_org_name_specified_matches(self):
+        token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhdXRoMHxzZGs0NThma3MiLCJhdWQiOiJ0b2tlbnMtdGVzdC0xMjMiLCJvcmdfbmFtZSI6Im9yZy0xMjMiLCJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJleHAiOjE1ODc3NjUzNjEsImlhdCI6MTU4NzU5MjU2MX0.P8Kba8Fgamyiw1qw_lBfp2OAzWn6NOLL6fBCDQhGvyc"
+        sv = SymmetricSignatureVerifier(HMAC_SHARED_SECRET)
+        tv = TokenVerifier(
+            signature_verifier=sv,
+            issuer=expectations["issuer"],
+            audience=expectations["audience"],
+        )
+        tv._clock = MOCKED_CLOCK
+        response = tv.verify(token)
+        self.assertIn("org_name", response)
+        self.assertEqual("org-123", response["org_name"])
+
+    def test_fails_when_org_id_specified_but_not_present(self):
         token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhdXRoMHxzZGs0NThma3MiLCJhdWQiOiJ0b2tlbnMtdGVzdC0xMjMiLCJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJleHAiOjE1ODc3NjUzNjEsImlhdCI6MTU4NzU5MjU2MX0.wotJnUdD5IfdZMewF_-BnHc0pI56uwzwr5qaSXvSu9w"
         self.assert_fails_with_error(
             token,
@@ -515,7 +556,7 @@ class TestTokenVerifier(unittest.TestCase):
             organization="org_123",
         )
 
-    def test_fails_when_org_specified_but_not_(self):
+    def test_fails_when_org_id_specified_but_not_string(self):
         token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhdXRoMHxzZGs0NThma3MiLCJhdWQiOiJ0b2tlbnMtdGVzdC0xMjMiLCJvcmdfaWQiOjQyLCJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJleHAiOjE1ODc3NjUzNjEsImlhdCI6MTU4NzU5MjU2MX0.fGL1_akaHikdovS7NRYla3flne1xdtCjP0ei_CRxO6k"
         self.assert_fails_with_error(
             token,
@@ -524,7 +565,7 @@ class TestTokenVerifier(unittest.TestCase):
             organization="org_123",
         )
 
-    def test_fails_when_org_specified_but_does_not_match(self):
+    def test_fails_when_org_id_specified_but_does_not_match(self):
         token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhdXRoMHxzZGs0NThma3MiLCJhdWQiOiJ0b2tlbnMtdGVzdC0xMjMiLCJvcmdfaWQiOiJvcmdfMTIzIiwiaXNzIjoiaHR0cHM6Ly90b2tlbnMtdGVzdC5hdXRoMC5jb20vIiwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjF9.hjSPgJpg0Dn2z0giCdGqVLD5Kmqy_yMYlSkgwKD7ahQ"
         self.assert_fails_with_error(
             token,