Die von Azure AD B2C angebotenen User Flows sind für typische Szenarien der Authentifizierung ausreichen und sogar empfohlen. Manchmal sind allerdings Operationen erforderlich, die die User Flows nicht unterstützen (z.B. das Validieren von User-Input, Abfrage/Anlage von Benutzerattribute von/in einer Datenbank, custom User Journey…). Hierfür werden Custom Policies umgesetzt. Custom Policies sind Konfigurationsdateien, die das Verhalten des Azure AD B2C Tenants definieren. In diesem Artikel werden wir erfahren, wie wir unsere eigene User Journey einrichten und was wir dafür alles brauchen.
Damit die Anwendung mit dem Tenant kommunizieren kann, müssen wir die zuerst registrieren.
Die Anwendung verwendet den Implicit Flow, um sich gegen das Backend zu authentifizieren dafür müssen wir noch eine Sache einstellen:
Im Azure AD B2C auf Identity Experience Framework klicken und dann auf „Policy Keys“. Auf „Add“ Klicken. Formular ausfüllen und mit „Create“ bestätigen.
Die Namen der Schlüssel müssen nicht unbedingt TokenSigningKeyContainer und TokenEncryptionKeyContainer sein. Diese Namen jedoch werden in den Konfigurationsdateien referenziert. Das heißt, wenn wir für andere Namen entscheiden, müssen wir die entsprechenden Namen in den Dateien auch ändern.
Azure AD B2C erfordert zwei Anwendungen zum Sign-Up/On von Benutzern mit Local Accounts (i.e. nicht von anderen Identity Providers). Diese Anwendungen heißen:
IdentityExperienceFramework:
Danach müssen wir die API aussetzen:
ProxyIdentityExperienceFramework:
Zunächst laden wir das Custom policy starter pack herunter. Es enthält Policies für verschiedene Szenarien, wo wir nur ein paar Konfigurationsparametern umstellen müssen (tenant id, ids der registrierten Anwendungen…). Man könnte auch von Grund auf neu die Policies schreiben. Das Starter pack hilft uns allerdings, Zeit zu sparen in dem wir inkrementell unsere custom policy auf funktionierender Basis erweitern damit das Debugging viel leichter ist.
Das Pack kann man als zip herunterladen oder hier finden:
git clone https://github.com/Azure-Samples/active-directory-b2c-custom-policy-starterpack
Eine ausführliche Dokumentation der einzelnen Blöcke und Parameter, die wir in den Policies definieren kann man hier finden.
Wir werden eine Policy definieren, mit der die Benutzer sich mit Local Accounts authentifizieren können. In diesem Artikel werden wir nur mit dem Sign up und Sign in beschäftigen. Dafür brauchen wir die folgenden Dateien vom Pack, die im Ordner „LocalAccounts“ liegen:
Die in diesen Dateien konfigurierte Policy wollen wir erweitern, indem der Benutzer zusätzlich einen Username beim Sign Up eingeben soll. Dieser Username muss eindeutig sein!
Außerdem wollen wir in unserer Anwendung ein RBAC (Role Based Access Control) Mechanismus einbauen, was im Gegensatz zu Azure Active Directory, Azure B2C nicht anbietet. Dafür brauchen wir noch den Claim „rol“ hinzuzufügen. Dies ist mit den User Flows nicht möglich, was die Verwendung von Custom Policies rechtfertigt. Im folgenden Abschnitt sehen wir, wie wir dieses Ziel erreichen können.
Claim Type ist wie eine Variable In einer Programmiersprache zu betrachten. Claim Typen sind im ClaimsSchema Block enthalten. Claims werden anhand des ClaimType IDs referenziert. Da wir für unseren use-case zwei neue Claims brauchen, müssen wir die entsprechenden Claim Types definieren. Als Best Practice empfiehlt Azure AD B2C die TrustFrameworkExtensions.xml Datei oder die Relying Party (i.e. SignUpOrSignin.xml) zu bearbeiten. Wir fügen ein ClaimSchema Block in die Extensions Datei hinzu wie folgend:
<!-- TrustFrameworkExtension.xml --> <ClaimsSchema> <ClaimType Id="extension_username"> <DisplayName>Username</DisplayName> <DataType>string</DataType> <UserHelpText/> <UserInputType>TextBox</UserInputType> <!-- <Restriction> <Pattern RegularExpression="%pattern%" HelpText="The username you provided is not valid. It must begin with an alphabet or number and can contain alphabets, numbers and the following symbols: _ -" /> </Restriction> --> </ClaimType> <ClaimType Id="rol"> <DisplayName>User authorization groups</DisplayName> <DataType>stringCollection</DataType> <UserHelpText>Authorization groups to which user belongs.</UserHelpText> </ClaimType> </ClaimsSchema>
Obwohl wir per Convention jedem custom Claim „extension_“ voranstellen sollen, haben wir den zweiten Claim „rol“ genannt, weil manche OAuth2 Bibliotheken per Default im ausgestellten Token nach dem Claim „rol“ suchen, um die Rollen des Benutzers auszulesen.
Der auskommentierte Parameter „Restriction“ wird benutzt, wenn wir ein bestimmtes Format oder Charset erfordern wollen. Ein anderer Typ von Restriction ist Enumeration (wenn der Benutzer von einer Liste vordefinierter Werte auswählen soll).
Ein Technical Profile ist wie eine Funktion zu betrachten. Es führt je nach Typ eine Logik aus, die Input Claims von einem Endpunkt bekommt und anhand dessen Output Claims erzeugt, die wiederum zum nächsten Profil weitergeleitet werden. Ein Technical Profile kann auch zum Validieren von Claims verwendet werden, i.e. Validation Technical Profile. Die Typen der Technical Profiles sind hier aufgelistet.
Von Interesse sind nämlich:
Im Sign up wird der Technical Profile mit der ID „LocalAccountSignUpWithLogonEmail“ verwendet. Es ist in TrustFrameworkBase.xml definiert. Das wollen wir in „TrustFrameworkExtensions.xml“ erweitern, damit der Benutzer einen Username eingeben kann. Dies erfolgt durch:
<!-- TrustFrameworkExtension.xml --> <ClaimsProvider> <DisplayName>Local Account</DisplayName> <TechnicalProfiles> <!--Local account sign-up page--> <TechnicalProfile Id="LocalAccountSignUpWithLogonEmail"> <OutputClaims> <OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" Required="true" /> <OutputClaim ClaimTypeReferenceId="newPassword" Required="true" /> <OutputClaim ClaimTypeReferenceId="reenterPassword" Required="true" /> <OutputClaim ClaimTypeReferenceId="displayName" /> <OutputClaim ClaimTypeReferenceId="givenName" /> <OutputClaim ClaimTypeReferenceId="surName" /> <OutputClaim ClaimTypeReferenceId="extension_username" Required="true"/> </OutputClaims> <ValidationTechnicalProfiles> <ValidationTechnicalProfile ReferenceId="Check-If-Username-Exists" ContinueOnError="false" /> <ValidationTechnicalProfile ReferenceId="AAD-UserWriteUsingLogonEmail" /> </ValidationTechnicalProfiles> </TechnicalProfile> </TechnicalProfiles> </ClaimsProvider>
Nehmen Sie zur Kenntnis, dass wir ein Validation Technical Profile benutzen, um zu prüfen, ob der Username in der Active Directory schon existiert. Gegebenenfalls wird das Sign up scheitern und eine Fehlermeldung wird dem Benutzer angezeigt.
Die AAD Technical Profiles müssen auch erweitert werden, damit der neue Claim Username in Active Directory persistiert.
<!-- TrustFrameworkExtension.xml --> <ClaimsProvider> <DisplayName>Azure Active Directory</DisplayName> <TechnicalProfiles> <!-- Write data during a local account sign-up flow. --> <TechnicalProfile Id="AAD-UserWriteUsingLogonEmail"> <PersistedClaims> <PersistedClaim ClaimTypeReferenceId="extension_username" DefaultValue="UNKNOWN"/> </PersistedClaims> </TechnicalProfile> <!-- Write data during edit profile flow. --> <TechnicalProfile Id="AAD-UserWriteProfileUsingObjectId"> <PersistedClaims> <PersistedClaim ClaimTypeReferenceId="extension_username" DefaultValue="UNKNOWN"/> </PersistedClaims> </TechnicalProfile> <!-- Read data after user authenticates with a local account. --> <TechnicalProfile Id="AAD-UserReadUsingEmailAddress"> <OutputClaims> <OutputClaim ClaimTypeReferenceId="extension_username" DefaultValue="UNKNOWN"/> </OutputClaims> </TechnicalProfile> <TechnicalProfile Id="AAD-UserReadUsingObjectId"> <OutputClaims> <OutputClaim ClaimTypeReferenceId="extension_username" DefaultValue="UNKNOWN"/> </OutputClaims> </TechnicalProfile> </TechnicalProfiles> </ClaimsProvider>
Wir definieren nun zwei API Endpunkte, um:
Dafür verwenden wir Azure Functions.
Um mit den Endpunkten kommunizieren zu können, definieren wir zwei Technical Profiles in „TrustFrameworkExtensions.xml“ wie folgend:
<!-- TrustFrameworkExtension.xml --> <ClaimsProvider> <DisplayName>Get-User-Authorization-Groups-On-Login</DisplayName> <TechnicalProfiles> <TechnicalProfile Id="Get-User-Authorization-Groups-On-Login"> <DisplayName>Get authorization groups from external authorization store for the user on login</DisplayName> <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" /> <Metadata> <Item Key="ServiceUrl">https://techstack-functions.azurewebsites.net/api/get-user-groups</Item> <Item Key="AuthenticationType">None</Item> <Item Key="AllowInsecureAuthInProduction">true</Item> <Item Key="SendClaimsIn">Body</Item> </Metadata> <InputClaims> <InputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="userId"/> </InputClaims> <OutputClaims> <OutputClaim ClaimTypeReferenceId="rol" PartnerClaimType="authorizationGroups" DefaultValue="[ROLE_USER]" /> </OutputClaims> <UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" /> </TechnicalProfile> </TechnicalProfiles> </ClaimsProvider> <ClaimsProvider>
Hier konstruieren wir den API Aufruf, indem wir die relevanten Parametern angeben:
Der Input ist der ObjectID des Benutzers. Die Azure Funktion ruft mit diesem Parameter die Graph API und gibt uns die dem Benutzer zugewiesenen Rollen zurück, die als OutputClaim des Technical Profile definiert sind.
<!-- TrustFrameworkExtension.xml --> <ClaimsProvider> <DisplayName>Check if provided username exists</DisplayName> <TechnicalProfiles> <TechnicalProfile Id="Check-If-Username-Exists"> <DisplayName>Checks if the provided username exists in the directory</DisplayName> <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" /> <Metadata> <Item Key="ServiceUrl">https://techstack-functions.azurewebsites.net/api/check-username?code=f1GZ8iqPJL3668392YOehFCxCxZi9NslRF3Y39jyFvlXkLI5IvJsA%3D%3D</Item> <Item Key="AuthenticationType">None</Item> <Item Key="AllowInsecureAuthInProduction">true</Item> <Item Key="SendClaimsIn">QueryString</Item> </Metadata> <InputClaims> <InputClaim ClaimTypeReferenceId="extension_username" PartnerClaimType="username" /> </InputClaims> <UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" /> </TechnicalProfile> </TechnicalProfiles> </ClaimsProvider> <ClaimsProvider>
Hier konstruieren wir den API Aufruf, indem wir die relevanten Parametern angeben:
Der Input ist der vom Benutzer angegebenen Username. Die Azure Funktion ruft mit diesem Parameter die Graph API und gibt einen HTTP Status zurück. Wenn der Status 200 Ok ist, erfolgt das Validieren. Wenn der Username schon belegt ist, gibt der Endpunkt einen 409 Conflict zurück (es muss 409 sein) und das Sign Up wird scheitern.
Zuletzt erweitern wir die UserJourney in der Relying Party (SignUpOrSignIn.xml), indem wir der extension_username Claim und der rol Claim als OutputClaims angeben
<!-- SignUpOrSignIn.xml --> <RelyingParty> <DefaultUserJourney ReferenceId="SignUpOrSignIn" /> <UserJourneyBehaviors> <SessionExpiryType>Rolling</SessionExpiryType> <SessionExpiryInSeconds>86400</SessionExpiryInSeconds> <JourneyInsights TelemetryEngine="ApplicationInsights" InstrumentationKey="XXXXXX-XXXXX-XXXX-XXXX-XXXXXXXX" DeveloperMode="true" ClientEnabled="false" ServerEnabled="true" TelemetryVersion="1.0.0" /> <ScriptExecution>Allow</ScriptExecution> </UserJourneyBehaviors> <TechnicalProfile Id="PolicyProfile"> <DisplayName>PolicyProfile</DisplayName> <Protocol Name="OpenIdConnect" /> <OutputClaims> <!-- <OutputClaim ClaimTypeReferenceId="identityProvider" /> --> <OutputClaim ClaimTypeReferenceId="displayName" /> <OutputClaim ClaimTypeReferenceId="newUser" /> <OutputClaim ClaimTypeReferenceId="rol" Required="true" DefaultValue="[user]" /> <OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub" /> <OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}" /> <OutputClaim ClaimTypeReferenceId="extension_username" Required="true" /> <OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" PartnerClaimType="email"/> <!-- <OutputClaim ClaimTypeReferenceId="trustFrameworkPolicy" Required="true" DefaultValue="{policy}" /> --> </OutputClaims> <SubjectNamingInfo ClaimType="sub" /> </TechnicalProfile> </RelyingParty>
Eine Antwort
Super! Dieser Artikel ist sehr hilfreich. Ich versuche ebenfalls, die Gruppen eines Benutzer auszulesen, um darauf in der Anwendung entsprechend reagieren zu können. Ich verwende eine Blazor Server-Clientanwendung.
Ich werde den Weg über Azure Functions auch ausprobieren.
PS: Leider ist die Schriftart „Roboto“ in 16px bei mir sehr schlecht lesbar.
Danke
Sven