feat(auth): LDAP and SSO users keep their source; directory TLS is real (#5036)
#5036 "SSO and LDAP: users keep their auth source, no merging into local accounts, is_active checked, LDAP TLS real and filter escaped". - users gain auth_source (local/ldap/oidc) and external_id (migration 0027). A provider account is found by (source, subject) only: the LDAP entry DN or the OIDC sub. A first sign-in whose username or email another account holds is refused with a message naming why; a disabled user stays out. - Local passwords sign in local accounts only. - LDAP: port 636 is LDAPS, otherwise StartTLS before any bind; the server certificate is checked against the system CAs or a CA pasted in Settings -> Auth. The login name is escaped into the filter (RFC 4515), and an empty password is refused before the directory sees it. - OIDC: the ID token must carry this sign-in's nonce and the userinfo sub. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -95,6 +95,25 @@ def decode_id_token_payload(id_token: str) -> dict:
|
||||
return {}
|
||||
|
||||
|
||||
def id_token_refusal(id_token: str, nonce: str | None, userinfo_sub: str) -> str | None:
|
||||
"""Why this sign-in's ID token does not belong to it, or None when it does.
|
||||
|
||||
The token must carry the nonce this browser was sent off with, so a token
|
||||
from another sign-in cannot be replayed into this one, and name the same
|
||||
subject as the userinfo response (OIDC Core 5.3.2). The signature is not
|
||||
checked (see the module note)."""
|
||||
if not userinfo_sub:
|
||||
return "SSO returned no subject (sub) for this user."
|
||||
claims = decode_id_token_payload(id_token) if id_token else {}
|
||||
if not claims:
|
||||
return "SSO returned no ID token. Check that the scopes include openid."
|
||||
if not nonce or claims.get("nonce") != nonce:
|
||||
return "This SSO sign-in does not match the one Steward started. Try again."
|
||||
if claims.get("sub") != userinfo_sub:
|
||||
return "SSO returned an ID token for a different user. Try again."
|
||||
return None
|
||||
|
||||
|
||||
def map_role(groups: list[str], admin_group: str, operator_group: str) -> str:
|
||||
"""Map IdP groups to a local role string."""
|
||||
if admin_group and admin_group in groups:
|
||||
|
||||
Reference in New Issue
Block a user