enhancement
major
minor
defect
critical
major
minor
major
#30172
Konten über eine externe Anmeldung erstellen und ihnen über einen Claim Gruppen zuweisen
Problem
Bei einer Anmeldung über ein externes Authentifizierungssystem (pac4j/OIDC) wird nur ein bereits vorhandenes Konto gefunden: „DefaultExternalUserMapping“ ordnet den Anmeldenamen einer Person zu und lehnt einen unbekannten Namen ab. Eine Anwendung, die möchte, dass sich ihre Benutzer über den Identitätsanbieter registrieren, muss jedes Konto im Voraus anlegen und die Gruppenzugehörigkeiten manuell synchronisieren.
Die Daten sind bereits vorhanden: „ExternalUserMapping.findAccountForIdentity(ExternalIdentity) “ erhält das Subjekt, den Anmeldenamen und alle Claims der Anmeldung.
Entwurf
Ein konfigurierbares „ExternalUserMapping“ (verwendbar als Benutzerzuordnung für jeden pac4j-Client), das Konten just-in-time bereitstellt:
- Lookup: das mit dem Identitätssubjekt verknüpfte Konto (stabil pro Client), andernfalls das Konto des Anmeldenamens.
- Erstellung (zuschaltbar): Wenn kein Konto existiert, wird eines angelegt. Anmeldename, Vorname, Nachname und E-Mail-Adresse werden aus konfigurierbaren Ansprüchen entnommen (Standardwerte: preferred_username, given_name, family_name, email). Die Erstellung kann auf eine bestätigte E-Mail-Adresse (email_verified) und auf eine Reihe zulässiger E-Mail-Domänen beschränkt werden. Eine abgelehnte Erstellung ist eine „LoginDeniedException“ mit einer Meldung, die auf der Anmeldeseite angezeigt wird.
- Gruppen: Ein konfigurierbarer Claim (z. B. „groups“ von Keycloak, „groups/`roles`“ von Entra ID) enthält Gruppennamen. Eine Zuordnungstabelle übersetzt Claim-Werte in `Group`s der Anwendung. Nicht zugeordnete Werte werden ignoriert oder optional als Gruppe mit demselben Namen übernommen, sofern diese existiert. Einem Konto, das ohne Gruppen-Claim angelegt wurde, wird eine konfigurierbare Standardgruppe zugewiesen.
- Synchronisierung bei jeder Anmeldung: Der Identitätsanbieter ist maßgeblich für die in der Zuordnung genannten Gruppen (und die Standardgruppe); diese werden bei jeder Anmeldung hinzugefügt und entfernt. Manuell in der Anwendung zugewiesene Gruppen bleiben davon unberührt.
- Das erstellte Konto hat kein Passwort; die Anmeldung erfolgt ausschließlich über das externe System.
- Das Löschen von Konten, die dem Identitätsanbieter nicht mehr bekannt sind, ist nicht Teil dieses Vorgangs (erfordert einen Push-Kanal wie SCIM).
Akzeptanz
- Bei einer Anmeldung einer unbekannten Identität mit aktivierter Kontoerstellung wird das Konto mit den Namen und der E-Mail-Adresse aus den Ansprüchen sowie den zugeordneten Gruppen angelegt; bei einer zweiten Anmeldung wird dasselbe Konto anhand des Subjekts auch nach einer Änderung des Anmeldenamens wiedergefunden.
- Bei einer Anmeldung, deren Gruppen-Claim eine zugeordnete Gruppe verloren hat, wird diese Mitgliedschaft entfernt; eine manuell zugewiesene Gruppe bleibt bestehen.
- Ist die Kontoerstellung deaktiviert, ist die E-Mail-Adresse unbestätigt oder stammt sie aus einer fremden Domain: Die Anmeldung wird mit einer sichtbaren Begründung abgelehnt, es wird kein Konto erstellt.
- Eine Identität ohne Gruppenbehauptung erhält die Standardgruppe.
Erforderlich für die Steuerungsanwendung „tl-factory“, deren integrierter OpenID-Anbieter diese Ansprüche für die Testbenutzer einer in der Entwicklung befindlichen Anwendung ausstellt.
Lösung
Modul com.top_logic.security.auth.pac4j (hängt nun von tl-element ab); Artikel „Authentifizierung/Kontoeinrichtung“.
Identität eines Kontos
- Modelltyp `OidcIdentity ` (Modul `tl.security.pac4j`: `client`, `subject`, `grantedGroups`) mit einem eindeutigen Index auf `(client, subject)`, der an `tl.accounts:Person` als mehrfache, schreibgeschützte Zusammensetzung `oidcIdentities` angehängt ist. Ein Konto kann Identitäten mehrerer Clients enthalten.
Provisioning-Zuordnung `OidcProvisioningUserMapping`
Ein `ExternalUserMapping` für die Benutzerzuordnung eines pac4j-Clients. Suchreihenfolge:
- das Konto mit einer ` OidcIdentity ` von (Client, Subject);
- mit „link-by-email“ (Standard: „false“, nur für Anbieter, deren E-Mail-Verifizierung als vertrauenswürdig gilt): das einzelne Konto mit der E-Mail-Adresse der Identität, sofern der Claim „email_verified“ wahr ist;
- mit Verknüpfung nach Namen (Standard: „true“): das Konto mit dem Anmeldenamen;
- mit „create-accounts“ (Standard: false): ein neues Konto; andernfalls wird die Anmeldung als unbekanntes Konto abgelehnt.
Ein in Schritt 2 oder 3 gefundenes Konto erhält die Identität zugewiesen. Wenn es bereits eine andere Identität desselben Clients trägt, wird die Anmeldung abgelehnt.
- Erstellung: Anmeldename, Vorname, Nachname und E-Mail-Adresse aus konfigurierbaren Claims (Standardwerte: „preferred_username“, „given_name“, „family_name“, „email“); optional „require-verified-email“ und „allowed-email-domains“. Das Konto erhält das Authentifizierungsgerät „authentication-device“ (Standard: „none“ – die Anmeldung erfolgt ausschließlich extern).
- E-Mail: Die E-Mail-Adresse eines Kontos entspricht der seines Kontakts (Person.getUser()), daher benötigen „link-by-email“ und die E-Mail-Adresse eines neu angelegten Kontos „tl-contact“; ohne diese Angabe werden nur Vorname und Nachname geschrieben.
- Namenskonflikt (der Anmeldename eines neuen Kontos wird bereits von einem nicht verknüpften Konto verwendet): Die Einstellung „name-conflict: refuse“ (Standard) lehnt die Anmeldung mit einer sichtbaren Begründung ab; „rename“ erstellt das Konto unter <name>@<client> (dann <name>@<client>-2, …). Ein Konflikt wird niemals stillschweigend verknüpft.
- Gruppen: „group-claim“ enthält Gruppennamen; „group-mapping“ ordnet einen Claim-Wert einer oder mehreren Gruppen zu; „groups-by-name“ betrachtet einen nicht zugeordneten Wert als bestehende Gruppe mit demselben Namen; „default-groups“ gelten, solange der Claim fehlt. Die OidcIdentity protokolliert, welche Mitgliedschaften sie gewährt hat: Bei jeder Anmeldung werden Gruppen hinzugefügt, die der Claim derzeit liefert; Gruppen, die zuvor gewährt wurden und nun nicht mehr geliefert werden, werden entfernt, es sei denn, eine andere Identität des Kontos hat sie ebenfalls gewährt. Mitgliedschaften, die bereits bestanden, bevor die Identität die Gruppe zugewiesen hat, werden niemals berührt. Der Anbieter ist für die von ihm zugewiesenen Gruppen maßgeblich: Eine manuell entfernte gewährte Mitgliedschaft wird bei der nächsten Anmeldung wiederhergestellt.
- Listener: die Java-Schnittstelle `ExternalLoginListener ` (in `com.top_logic.base.accesscontrol`) mit `onCreate`(Person account, ExternalIdentity identity) (nur bei einem neu angelegten Konto, z. B. zum Anlegen von Objekten und Verknüpfen des Kontos mit neuen oder bestehenden Objekten, damit es durch Rollenregeln sofort Rollen erhält) und onLogin(Person account, ExternalIdentity identity) (bei jeder Anmeldung). Das Mapping ruft die konfigurierten Listener in der Transaktion der Anmeldung nach der Gruppensynchronisation auf. Ein fehlgeschlagener Listener macht die Transaktion rückgängig und lehnt die Anmeldung ab.
- Listener im TL-Script: ExternalLoginListenerByExpression (tl-model-search, Tag „login-script“) führt die optionalen Skripte „on-create“ und „on-login“ aus, jeweils eine Funktion „Konto“ → „Claims“ → „Client“ → … (das Konto, die Claims der Identität als Dictionary, der Client-Name). Die Skripte werden ohne Sicherheitsprüfung ausgeführt (es ist noch kein Benutzer angemeldet) und können das Konto, dessen Kontaktdaten und andere Objekte ändern; sie werden bei ihrer ersten Verwendung kompiliert. Ein Skriptfehler verweigert die Anmeldung; eine mit `throw(...)` ausgelöste Meldung wird als Grund angezeigt. Dies ermöglicht es einer Anwendung, fehlerhafte Daten eines Identitätsanbieters durch Konfiguration zu umgehen, z. B. wenn ein `given_name` als „Nachname, Vorname“ geliefert wird:
{{{#!xml <listeners>
<login-script>
<on-login><![CDATA[
account -> claims -> client ->
$account.get(tl.accounts:Person#contact)
.set(Contacts:PersonContact#firstname,
regex("[,]*, *").regexReplace($claims["given_name"], ""))
]]></on-login>
</login-script>
</listeners> }}}
- Transaktionen: Eine Anmeldung, bei der das Konto anhand des Subjekts ermittelt wird, keine Gruppe geändert wird und keine Listener vorhanden sind, eröffnet keine Transaktion; andernfalls werden die Auflösung, die Gruppensynchronisation und die Listener in einer Transaktion ausgeführt.
- Ablehnungen: `Login.LoginDeniedException ` enthält einen optionalen Grund (`ResKey`); `ExternalAuthenticationServlet` zeigt diesen auf der Seite für den erneuten Anmeldeversuch anstelle der allgemeinen Meldung an.
- Ein Konto ohne Authentifizierungsgerät wird von den Passwortdialogen unterstützt; „ForgotPasswordDialog“ (tl-security-selfservice) behandelt es wie ein Konto, dessen Passwort nicht geändert werden kann.
Konfiguration von Extractor und Mapping
Die Eigenschaften „user-name-extractor“ und „user-mapping“ eines pac4j-Clients (ClientConfigurator.Config) sowie „user-mapping“ von J2eeRemoteUserAuthenticationServlet.Config sind polymorphe Konfigurationen anstelle von konfigurierten Instanzen, sodass sie im Service-Editor bearbeitet werden können. „Pac4jConfigFactory“ erstellt Extraktor und Zuordnung einmal pro Client; ein Re-Authentifizierungs-Client verwendet die des Clients, von dem er abgeleitet ist.
Nicht im Umfang enthalten: Löschen von Konten, die der Anbieter nicht mehr kennt (erfordert SCIM); explizites Verknüpfen eines weiteren Logins mit dem eigenen Konto im Profil: #30178.
Migration
Die Konfigurationseigenschaften `user-name-extractor` und `user-mapping ` (Pac4j-Clients in ` Pac4jConfigFactory`, `J2eeRemoteUserAuthenticationServlet`) akzeptieren keinen reinen Klassennamen mehr als Attributwert. Ersetzen Sie {{{#!xml <keycloak name="..." user-mapping="com.example.MyMapping"> }}} durch die folgende Elementform {{{#!xml <keycloak name="...">
<user-mapping class="com.example.MyMapping"/>
</keycloak> }}} Die Elementform <user-mapping class="..." .../> mit weiteren Attributen bleibt unverändert. Java-Code, der getUserMapping() / getUserNameExtractor() aus der Client-Konfiguration ausliest, bezieht die Instanzen stattdessen über Pac4jConfigFactory.getUserMapping(String) / getUserNameExtractor(String).