Azure AD B2C Custom Policies

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.

Table of Contents

Erste Schritte

Registrierung der Anwendung im B2C Tenant

Damit die Anwendung mit dem Tenant kommunizieren kann, müssen wir die zuerst registrieren.

  1. Im Azure AD B2C auf App Registrations klicken, dann auf „New Registration“ klicken.
  2. Name der App eingeben.
  3. Für Supported Account types „Accounts in any identity provider or organizational directory (for authenticating users with user flows)“ wählen.
  4. Redirect URL eingeben. Diese URL wird bei erfolgreicher Authentifizierung den ID Token des Benutzers von Azure AD B2C empfangen.
  5. „Grant admin consent to openid and offline_access permissions“ abhaken.
Registrierung der Anwendung im B2C Tenant

Die Anwendung verwendet den Implicit Flow, um sich gegen das Backend zu authentifizieren dafür müssen wir noch eine Sache einstellen:

  1. In „App Registrations“ auf die registrierte Anwendung klicken. Unter Manage links im Menü auf „Authentication“ klicken.
  2. Unter „Implicit Grant and Hybrid Flows“ sowohl „ID tokens als auch Access tokens abhaken“

Anlage der Signing und Encryption Keys

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.

  • Signing Key: 
      – Options: Generate
      – Name: TokenSigningKeyContainer
      – Key Type: RSA
      – Key usage: Signature

  • Encryption:
      – Options: Generate
      – Name: TokenEncryptionKeyContainer
      – Key Type: RSA
      – Key usage: Encryption

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.

Registrierung der Identity Experience Framework Anwendungen

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: Eine Web API
  • ProxyIdentityExperienceFramework: Eine native Anwendung mit delegierten Berechtigungen zu IdentityExperienceFramework

IdentityExperienceFramework:

  1. In „App Registrations“ auf „New Registration“ klicken.
  2. IdentityExperienceFramework als Name eingeben.
  3. Unter „Supported account types“ „Accounts in this organizational directory only“ wählen.
  4. Für die Redirect URI „Web“ wählen und https://.b2clogin.com/.onmicrosoft.com eingeben.
  5. Unter „Permissions“  „Grant admin consent to openid and offline_access permissions“ abhaken.

Danach müssen wir die API aussetzen:

  1. In „App Registrations“ auf die registrierte Anwendung klicken. Unter Manage links im Menü auf „Expose an API“ klicken.
  2. Um die Ausführung der Custom Policies zu erlauben müssen wir einen Scope hinzufügen, indem wir auf „Add a scope“ klicken.
  3. „user_impersonation“ für den Namen eingeben
  4. Für den Display name und die Beschreibung können wir jeweils „Access IdentityExperienceFramework“ und „Allow the application to access IdentityExperienceFramework on behalf of the signed-in user.“ eingeben (wie es in der offiziellen Dokumentation vorgeschlagen ist).
  5. mit „Add Scope“ bestätigen.

ProxyIdentityExperienceFramework:

  1. In „App Registrations“ auf „New Registration“ klicken.
  2. ProxyIdentityExperienceFramework als Name eingeben.
  3. Unter „Supported account types“ „Accounts in this organizational directory only“ wählen.
  4. Für die Redirect URI „Public client/native“ wählen und „myapp://auth“ eingeben.
  5. Unter „Permissions“  „Grant admin consent to openid and offline_access permissions“ abhaken.
  6. In „App Registrations“ auf die registrierte Anwendung klicken. Unter Manage links im Menü auf „Authentication“ klicken und dann in „Advanced Settings“ die Public Flows erlauben.
  7. Im Menü unter „Manage“ auf „API permissions“ klicken und eine neue Berechtigung hinzufügen.
  8. Den von IdentityExperienceFramework ausgesetzten Scope wählen und Admin Consent erteilen.

Custom Policies

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.

Sign up / Sign in

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:

  • TrustFrameworkBase.xml
  • TrustFrameworkExtensions.xml
  • SignUpOrSignin.xml

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. 

ClaimType

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).

Technical Profiles

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:

  • Self-Asserted: Jegliche direkte Interaktion mit dem Benutzer während einer Sign Up/In Operation.
  • RESTful provider: Aufruf einer REST API.
Self-Asserted

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>
RESTful Provider

Wir definieren nun zwei API Endpunkte, um:

  1. zu prüfen, ob der Username von einem anderen Benutzer belegt ist.
  2. die dem Benutzer zugewiesenen Rollen aus der Active Directory auszulesen.

Dafür verwenden wir Azure Functions.

Um mit den Endpunkten kommunizieren zu können, definieren wir zwei Technical Profiles in „TrustFrameworkExtensions.xml“ wie folgend:

Get Authorization Groups
<!-- 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:

  • URL: https://techstack-functions.azurewebsites.net/api/get-user-groups
  • AuthentificationType: None (nur weil es ein POC ist. In production ist eine Authentifizierung erforderlich, sonst kann jeder auf den API Endpunkt zugreifen)
  • Die Art, wie der Input angegeben wird: Body, das entspricht eine HTTP POST Methode (mögliche werte sind Form: POST, Header: GET, Url: GET, QueryString: GET)

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.

Check If Username Exists
<!-- 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

  1. 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

Schreibe einen Kommentar