Skip to content

Commit 9907664

Browse files
committed
fix(auth): add missing email attribute to SAML attribute extraction
1 parent 9412022 commit 9907664

File tree

1 file changed

+43
-18
lines changed

1 file changed

+43
-18
lines changed

tracecat/auth/saml.py

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,43 @@ async def should_allow_email_for_org(
280280
return normalized_domain in active_domains
281281

282282

283+
def _extract_candidate_emails(parser: SAMLParser) -> list[str]:
284+
"""Extract candidate emails from known SAML attributes in priority order."""
285+
candidates = [
286+
parser.get_attribute_value("email"),
287+
# Okta
288+
parser.get_attribute_value(
289+
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
290+
),
291+
# Microsoft Entra ID (prefer explicit email over UPN/name)
292+
parser.get_attribute_value(
293+
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
294+
),
295+
parser.get_attribute_value(
296+
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"
297+
),
298+
]
299+
seen: set[str] = set()
300+
deduped: list[str] = []
301+
for value in candidates:
302+
email = value.strip()
303+
if not email or email in seen:
304+
continue
305+
seen.add(email)
306+
deduped.append(email)
307+
return deduped
308+
309+
310+
async def _select_allowlisted_email(
311+
session: AsyncSession, organization_id: OrganizationID, candidates: list[str]
312+
) -> str | None:
313+
"""Return the first candidate that satisfies org-domain allowlist."""
314+
for candidate in candidates:
315+
if await should_allow_email_for_org(session, organization_id, candidate):
316+
return candidate
317+
return None
318+
319+
283320
async def create_saml_client(
284321
saml_idp_metadata_url: str,
285322
) -> Saml2Client:
@@ -607,23 +644,8 @@ async def sso_acs(
607644
logger.info("SAML response validated successfully")
608645

609646
parser = SAMLParser(str(authn_response))
610-
611-
email = (
612-
parser.get_attribute_value("email")
613-
# Okta
614-
or parser.get_attribute_value(
615-
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
616-
)
617-
# Microsoft Entra ID
618-
or parser.get_attribute_value(
619-
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"
620-
)
621-
or parser.get_attribute_value(
622-
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
623-
)
624-
)
625-
626-
if not email:
647+
candidate_emails = _extract_candidate_emails(parser)
648+
if not candidate_emails:
627649
attributes = parser.attributes or {}
628650
logger.error(
629651
f"Expected attribute 'email' in the SAML response, but got {len(attributes)} attributes"
@@ -633,7 +655,10 @@ async def sso_acs(
633655
detail="Authentication failed",
634656
)
635657

636-
if not await should_allow_email_for_org(db_session, organization_id, email):
658+
email = await _select_allowlisted_email(
659+
db_session, organization_id, candidate_emails
660+
)
661+
if email is None:
637662
logger.warning("SAML login denied by org domain allowlist")
638663
raise HTTPException(
639664
status_code=status.HTTP_403_FORBIDDEN,

0 commit comments

Comments
 (0)