feat(sso): Azure AD / Entra ID SAML 2.0 integration
Aegis CI / lint-and-test (push) Has been cancelled
Aegis CI / lint-and-test (push) Has been cancelled
- sso_service: fix process_callback for Azure AD claim URIs (email, role) - Default role_attr to full Azure role claim URI - Fallback email resolution via Azure email claim URI + NameID - Username defaults to full email (prevents collision with local accounts) - User lookup also tries email field for existing local accounts - Logs warning when unknown role received from IdP - frontend/api/sso.ts: new API module with getSsoStatus, getSsoConfig, updateSsoConfig - LoginPage: redesigned for SSO-first flow - Shows Azure SSO button as primary when SSO enabled+configured - Local login collapsed under "Emergency admin access" section - Falls back to normal local login form when SSO is disabled - SystemPage: new SsoConfigSection component (guided 5-step wizard) - Step 1: Copy SP Entity ID and ACS URL for IT team + metadata XML download - Step 2: Azure App Roles reference table (6 roles with exact values) - Step 3: Tenant ID field auto-fills idp_entity_id and idp_sso_url - Step 4: X.509 certificate paste field - Step 5: Attribute mapping pre-filled with Azure AD claim URIs - Enable/disable toggle + save Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -176,21 +176,43 @@ def process_callback(db: Session, request_data: dict) -> User:
|
||||
# Extract attributes
|
||||
attrs = auth.get_attributes()
|
||||
name_id = auth.get_nameid()
|
||||
email_attr = cfg.attr_email or "email"
|
||||
username_attr = cfg.attr_username or "username"
|
||||
role_attr = cfg.attr_role or "role"
|
||||
|
||||
email = _first_attr(attrs, email_attr) or name_id or ""
|
||||
username = _first_attr(attrs, username_attr) or email.split("@")[0] or name_id
|
||||
role = _first_attr(attrs, role_attr) or cfg.default_role or "viewer"
|
||||
# Attribute claim URIs — defaults support both plain names and Azure AD full URIs
|
||||
email_attr = cfg.attr_email or "email"
|
||||
username_attr = cfg.attr_username or "email" # Azure AD: use email as username
|
||||
role_attr = cfg.attr_role or "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"
|
||||
|
||||
# Resolve email: try configured attr → Azure email claim URI → NameID
|
||||
email = (
|
||||
_first_attr(attrs, email_attr)
|
||||
or _first_attr(attrs, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress")
|
||||
or name_id
|
||||
or ""
|
||||
)
|
||||
|
||||
# Resolve username: keep full email (e.g. user@company.com) to avoid collisions with local accounts
|
||||
raw_username = _first_attr(attrs, username_attr) or email or name_id or ""
|
||||
username = raw_username.strip() or email.split("@")[0] or name_id
|
||||
|
||||
# Resolve role: try configured attr → Azure role claim URI → default
|
||||
role = (
|
||||
_first_attr(attrs, role_attr)
|
||||
or _first_attr(attrs, "http://schemas.microsoft.com/ws/2008/06/identity/claims/role")
|
||||
or cfg.default_role
|
||||
or "viewer"
|
||||
)
|
||||
|
||||
# Validate role
|
||||
valid_roles = {"admin", "red_lead", "blue_lead", "red_tech", "blue_tech", "viewer"}
|
||||
if role not in valid_roles:
|
||||
log.warning("SSO: unknown role '%s' for user '%s', falling back to default", role, username)
|
||||
role = cfg.default_role or "viewer"
|
||||
|
||||
# Look up or provision user
|
||||
user = db.query(User).filter(User.username == username).first()
|
||||
# Look up or provision user — try username first, then email (for existing local accounts)
|
||||
user = (
|
||||
db.query(User).filter(User.username == username).first()
|
||||
or db.query(User).filter(User.email == email).first()
|
||||
)
|
||||
if user:
|
||||
# Refresh role from IdP on every login
|
||||
user.role = role
|
||||
|
||||
Reference in New Issue
Block a user