Xamarin.Forms

Authentification Azure Directory B2C dans une application Xamarin.Forms

par
publié le

On m’a récemment demandé d’étudier la faisabilité d’une authentification depuis Azure Directory B2C dans une application Xamarin.Forms.

La documentation officielle a un peu tendance à tourner en rond avec des liens en références circulaires, le portail Azure est vaste et pas super user-friendly (on peut passer beaucoup de temps à chercher une option qu’on est pourtant certain d’avoir aperçue cinq minutes plus tôt…).

J’ai donc rédigé un guide rapide à l’attention des développeurs de l’équipe, je vous le livre tel quel, ce n’est donc pas nécessairement très bien rédigé, mais vous aurez toutes les informations pour débuter, depuis la création du Directory B2C sur Azure jusqu’à l’authentification et la récupération des données de l’utilisateur dans l’application Xamarin.Forms.

Résumé

  1. Créer un tenant Active Directory B2C dans Azure.
  2. Enregistrer l’application mobile dans le tenant Active Directory B2C.
  3. Créer des User Flows avec des polices de Signin, réinitialisation de mot de passe…
  4. Utiliser la Microsoft Authentication Library (MSAL) pour gérer le flux d’authentification dans l’application.

Sources

Côté serveur

Création du compte Azure

https://aka.ms/azfree-docs-mobileapps

Pré-requis :

  • Un compte microsoft
  • Une carte bancaire

Je ne détaille pas, il suffit de suivre les étapes. Sachez simplement que vous disposez d’un crédit de 170€ pour débuter, de quoi faire des tests et de l’autoformation.

Configuration du portail en ANGLAIS

Très important, peut-être un bug temporaire, mais si vous conservez le portail en français, la création de AD B2C échouera avec un message abscons vous indiquant que la valeur de l’emplacement du groupe de ressource est invalide alors qu’on la sélectionne dans une liste (et peu importe la valeur sélectionnée) !

C’est simplement que la liste est traduite et que visiblement quelque part dans le code, Azure attend une valeur en dur tirée de la liste en anglais. Franchement, j’ai honte pour Microsoft !

Donc évitons-nous bien des soucis et passons immédiatement le portail en anglais.

Configuration du portail en anglais

Enregistrement du namespace Mirosoft.AzureActiveDirectory

Aucun tuto ne vous le dira, et c’est peut-être un bug temporaire, mais si vous ne le faites pas maintenant, vous obtiendrez cette erreur à la toute fin du processus et vous devrez tout recommencer. :-)

Message d'erreur concernant Microsoft.AzureActiveDirectory

Cliquez sur Subscriptions

Navigation vers les subscriptions

Dans la liste, sélectionnez votre subscription courante.

Si vous venez de créer un compte gratuit, il n’y en aura qu’une :

Sélection de la subscription

Dans le panneau de gauche, cliquez sur Resource providers

Navigation vers les resource providers

Cherchez Azure, sélectionnez Microsoft.AzureActiveDirectory et cliquez sur Register

Recherche de Microsoft.AzureActiveDirectory

Patientez… patientez… patientez… patientez… Cliquez de temps en temps sur Refresh au cas où. Si ça ne fonctionne pas, recommencez (sélection, register), au bout d’un moment ça finit par passer. :-)

Microsoft.AzureActiveDirectory est enregistré

C’est bon, on y est, on peut commencer !

Création de l’Active Directory B2C

Cliquez sur Create a resource

Créer une ressources

Cherchez et sélectionnez Active Directory B2C

Sélectionner Active Directory B2C

Cliquez sur Create

Cliquer sur Create

Create a New Azure AD B2C tenant

Create a New Azure AD B2C tenant

Renseignez les informations du tenant

Créer un Groupe de ressources avec le lien Create New s’il n’en existe pas déjà un.

Renseigner les informations du tenant

Cliquez sur Review + Create pour valider les informations

Valider les informations

Cliquez sur Create pour en finir !

Patientez. Tant que ça gigote là-dedans c’est que ça bosse :

Indicateur d'activité dans la toolbar

Cliquez sur le lien, bienvenue à Black Mesa !

Lien menant vers votre tenant Active Directory B2C

Configuration d’une application côté serveur

Selection du tenant B2C

Dans la toolbar en haut à droite, trouvez le bouton qui ressemble à un carnet avec un entonnoir :

Buton de sélection des directories

Mais en principe vous devriez déjà être dessus si vous avez cliqué sur le lien à la fin de l’étape précédente.

Si vous vous retrouvez sur une page d’accueil comme ça et que vous ne retrouvez pas votre tenant AD B2C

Page d'accueil du Directory

Saisissez simplement B2C dans la barre de recherche en haut et dans la rubrique Services, sélectionnez Azure AD B2C.

Recherche du tenant B2C

Enregistrement de l’application

Sélectionnez App registrations

Navigation vers l'enregistrement d'applications

Puis New registration

Nouvel enregistrement

Renseignez les informations

  • Nom et type de compte supporté :

    Nom et type de compte

  • Redirect URI : Cela dépend du type d’application (application mobile, web app…). Dans notre cas, il s’agira d’une application mobile Xamarin.Forms. Attention, l’uri répond à un schéma bien particulier : msal[redirect_uri]://auth.

    Redirect URi

Cet URi sera à renseigner dans les manifestes Android et iOS.

  • Permissions :
Permisssions

Validez le tout en cliquant sur le bouton Register. Bravo ! Votre application est enregistrée :

Application enregistrée

Mais ce n’est pas fini : la case à cocher secrète

Si vous vous en tenez là et suivez le tuto Microsoft et l’application Sample fournie, cela ne fonctionnera pas : les URI utilisés comme point d’accès dans l’application ne sont pas autorisés par défaut !

Rendez-vous dans la rubrique Authentication et cochez les cases :

Cocher les Uri d'authentification

Pensez bien à sauvegarder. Le bouton en haut, je vous laisse le trouver tout seul.

Création des User Flows

Créer un user flow revient à autoriser les utilisateurs à interagir avec leur compte AD : se connecter, réinitialiser son mot de passe, modifier son profile…

NB : les flows sont partageables entre les applications.

Sign In

Au minima, les utilisateurs auront besoin de se connecter. Dans la page de votre tenant AD B2C, allez sur User flows dans la rubrique Policies et cliquez sur New user flow.

New User Flow

Puis sélectionnez Sign In et créez le flow dans sa version recommandée.

Signin User Flow

Renseignez les informations en fonction du mode de connexion et de l’authentification multiple souhaités.

Notez bien le nom, vous en aurez besoin côté client :

Nom de la police

La dernière rubrique permet de sélectionner les données qui seront récupérées côté client lors de la connexion utilisateur. Cliquez sur Show more… pour obtenir la liste complète.

Claims

Finalisez en cliquant sur le bouton Create.

Autres flows

La procédure est similaire pour les autres types de flows. Créez ceux dont vous avez besoin.

Création des utilisateurs

Sur la page de votre tenant Azure AD B2C, cliquez sur Users dans la rubrique Manage.

Création des utilisateurs

Ensuite, cela dépend du mode de connexion envisagé. Il sera peut-être nécessaire de d’abord configurer un ou plusieurs fournisseurs d’identité (compte Microsoft, compte local, Github, Facebook…), dans la même rubrique que précédemment, lien Identity providers.

A priori, tout est en place côté serveur, reste à implémenter la couche MSAL côté client.

Côté client

Package nuget Microsoft Authentication Library (MSAL)

Direction le nuget package manager. Installez Microsoft.Identity.Client sur le projet commun et les projets des plateformes :

Package nuget Microsoft.Identity.Client

Préparation du projet commun

Définition de quelques constantes

Pour commencer, il est nécessaire de définir quelques constantes.

  • tenantName : le nom de domaine que vous avez défini pour votre tenant Directory B2C

  • tenantId : le même suffixé par .onmicrosoft.com

  • clientId : Application (client) Id, vous trouverez cette information dans l’overview de votre application sur le portail Azure :

    ClientId dans l'overview de l'application

  • redirect_uri : l’uri enregistré côté azure

  • policySignin et policyPassword : le nom des polices telles que vous les avez définies précédemment

  • iosKeychainSecurityGroup : identifiant du Bundle iOS que vous trouvez dans Info.plist.

public static class Constants
{
    const string tenantName = "[your_tenant_name]";
    const string tenantId = "[your_tenant_name].onmicrosoft.com";
    const string clientId = "[your_application_client_id]";
    const string redirectUri = "[redirect_uri]";
    const string policySignin = "B2C_1_signin";
    const string policyPassword = "B2C_1_resetpassword";
    const string iosKeychainSecurityGroup = "[application iOS Bundle identifier]";
    
    static readonly string[] scopes = { "" };
    static readonly string authorityBase = $"https://{tenantName}.b2clogin.com/tfp/{tenantId}/";

    public static string RedirectMsalScheme => $"msal{redirectUri}://auth"; 
    public static string AuthorityBase => authorityBase;
    public static string[] Scopes => scopes;
    public static string PolicySignin => policySignin;
    public static string PolicyPassword => policyPassword;
    public static string IosKeychainSecurityGroups => iosKeychainSecurityGroup;
    public static string ClientId => clientId;
    public static string AuthoritySignin => $"{authorityBase}{policySignin}";
    public static string AuthorityPasswordReset => $"{authorityBase}{policyPassword}";
}

Le service d’authentification : AuthenticationClient

Dans le fichier App.xaml.cs, définir les propriétés AppUi (nécessaire pour l’implémentation dans Android) et AuthenticationClient :

using Microsoft.Identity.Client;
[...]

public partial class App : Application
{
    public static IPublicClientApplication AuthenticationClient { get; private set; }
    public static object UIParent { get; set; } = null;
    
    [...]
}

Le service d’authentification implémente IPublicClientApplication, pour l’instancier, on utilise le builder PublicClientApplicationBuilder :

public App()
{
    InitializeComponent();

    AuthenticationClient = PublicClientApplicationBuilder.Create(Constants.ClientId)
            .WithIosKeychainSecurityGroup(Constants.IosKeychainSecurityGroups)
            .WithB2CAuthority(Constants.AuthoritySignin)
            .WithRedirectUri(Constants.RedirectMsalScheme)
            .Build();
    [...]
}

Configuration de l’application iOS

Info.plist

Souvenez-vous côté serveur, vous avez configuré un Redirect Uri sous la forme msal[redirect_uri]://auth

Il s’agit ici de renseigner cette valeur dans le fichier Info.plist du projet iOS. Cela se passe dans l’onglet Advanced, rubrique URL Types :

Redirect Uri dans Info.plist

Keychain dans Entitlements.plist

Il est ensuite nécessaire d’enregistrer un Keychain dans Entitlements.plist :

Keychain dans Entitlements.plist

Si cela ne s’est pas fait automatiquement, vérifiez bien que le fichier Entitlements.plist est sélectionné comme Custom Entitlements dans l’onglet iOS Bundle Signing du projet iOS.

Custom Entitlements

Faîtes-le bien pour chacune de vos configurations de Build, sinon vous aurez une exception à l’exécution concernant le Keychain !

Surcharge de la méthode OpenUrl dans AppDelegate

using Microsoft.Identity.Client;
[...]

[Register("AppDelegate")]
public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
{
    [...]
    
    public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
    {
        AuthenticationContinuationHelper.SetAuthenticationContinuationEventArgs(url);
        return base.OpenUrl(app, url, options);
    }
}

Configuration l’application Android

Manifeste

Comme pour iOS, le Redirect Uri doit être déclaré dans le manifeste Android.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.companyname.adb2capp">
    <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="30" />
    <application android:label="ADB2CApp.Android" android:theme="@style/MainTheme">
        
        <!-- MSAL REGISTRATION START -->
        <activity android:name="microsoft.identity.client.BrowserTabActivity">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="msal[redirect_uri]" android:host="auth" />
            </intent-filter>
        </activity>
        <!-- MSAL REGISTRATION END -->
    </application>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>

Modifications dans MainActivity

L’authentification MSAL nécessite :

  • De passer l’activity à App.UIParent
  • De surcharger OnActivityResult
using Microsoft.Identity.Client;
[...]

public class MainActivity : FormsAppCompatActivity
{
    protected override void OnCreate(Bundle bundle)
    {
        TabLayoutResource = Resource.Layout.Tabbar;
        ToolbarResource = Resource.Layout.Toolbar;

        base.OnCreate(bundle);

        Xamarin.Essentials.Platform.Init(this, savedInstanceState);
        global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
        LoadApplication(new App());
        // ADDED
        App.UIParent = this;
    }

    // OVERRIDED
    protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
    {
        base.OnActivityResult(requestCode, resultCode, data);
        AuthenticationContinuationHelper.SetAuthenticationContinuationEventArgs(requestCode, resultCode, data);
    }
    
    [...]
}

Implémentation dans le projet Xamarin.Forms

Création d’une page de Login

On va faire super simple, créez une page LoginPage.xaml, ajoutez-y un bouton Login et abonnez-le à l’événement Clicked.

<ContentPage.Content>
    <Button x:Name="LoginButton"
        Text="LOGIN"
        Clicked="LoginButton_Clicked"
        VerticalOptions="Center"
        HorizontalOptions="Center"
        />
</ContentPage.Content>

Visuellement, difficile de faire plus basique :

Login Page

A l’apparition de la page, appelez AcquireTokenSilentAsync pour rafraîchir le token d’authentification, au cas où un utilisateur soit déjà authentifié.

public partial class LoginPage : ContentPage
{
    [...]

    protected override async void OnAppearing()
    {
        try
        {
            // Look for existing account
            IEnumerable<IAccount> accounts = await App.AuthenticationClient.GetAccountsAsync();
            
            AuthenticationResult result = await App.AuthenticationClient
                .AcquireTokenSilent(Constants.Scopes, accounts.FirstOrDefault())
                .ExecuteAsync();
                
            await Navigation.PushAsync(new LogoutPage(result));
        }
        catch
        {
            // Do nothing - the user isn't logged in
        }
        base.OnAppearing();
    }

    [...]
}

Au clic sur le bouton, AcquireTokenInteractive est utilisé pour ouvrir le navigateur de l’appareil et afficher la page de Signin.

  • Si la connexion réussit le résultat de l’authentification est passé à la page LogoutPage.
  • Si l’utilisateur clique sur l’option “j’ai oublié mon mot de passe”, une exception particulière est interceptée pour lancer la procédure de récupération de mot de passe.
private async void LoginButton_Clicked(object sender, EventArgs e)
{
    AuthenticationResult result;
    try
    {
        result = await App.AuthenticationClient
            .AcquireTokenInteractive(Constants.Scopes)
            .WithPrompt(Prompt.SelectAccount)
            .WithParentActivityOrWindow(App.UIParent)
            .ExecuteAsync();

        await Navigation.PushAsync(new LogoutPage(result));
    }
    catch (MsalException ex)
    {
        if (ex.Message != null && ex.Message.Contains("AADB2C90118"))
        {
            result = await OnForgotPassword();
            await Navigation.PushAsync(new LogoutPage(result));
        }
        else if (ex.ErrorCode != "authentication_canceled")
        {
            await DisplayAlert("An error has occurred", "Exception message: " + ex.Message, "Dismiss");
        }
    }
}

Et voici le code pour la réinitialisation du mot de passe :

private async Task<AuthenticationResult> OnForgotPassword()
{
    try
    {
        return await App.AuthenticationClient
            .AcquireTokenInteractive(Constants.Scopes)
            .WithPrompt(Prompt.SelectAccount)
            .WithParentActivityOrWindow(App.UIParent)
            .WithB2CAuthority(Constants.AuthorityPasswordReset)
            .ExecuteAsync();
    }
    catch (MsalException)
    {
        // Do nothing - ErrorCode will be displayed in OnLoginButtonClicked
        return null;
    }
}

Une fois la page créée, remplacez MainPage dans App par une NavigationPage : MainPage = new NavigationPage(new LoginPage()); de façon à naviguer directement sur la LoginPage à l’ouverture de l’application.

Création d’une page de logout

La page LogoutPage est très similaire à la page de login à ceci près :

  • Son constructeur reçoit en paramètre le résultat de l’authentification
  • Un Label affiche les informations de base de l’utilisateur
  • Le bouton Login est remplacé par un bouton Logout
<ContentPage.Content>
    <StackLayout VerticalOptions="Center" HorizontalOptions="Center">
        <Label x:Name="UserInfoLabel"
                   FontSize="Large"
                   HorizontalTextAlignment="Center"
                   />
        <Button x:Name="LogoutButton"
                    Text="LOGOUT"
                    Clicked="LogoutButton_Clicked"
                    HorizontalOptions="Center"
                    VerticalOptions="Center"
                    />
    </StackLayout>
</ContentPage.Content>

Commençons par récupérer l’AuthenticationResult dans le constructeur de la page :

AuthenticationResult authenticationResult;
public LogoutPage(AuthenticationResult result)
{
    InitializeComponent();	
    authenticationResult = result;
}

Les informations utilisateur (Claims)

Les informations concernant l’utilisateur, celles que vous avez sélectionnées parmi les Claims lors de la création de votre User Flow Signin sont codées dans la propriété authenticationResult.IdToken sous la forme d’un token jwt. Pour le décoder, vous aurez besoin d’installer le package nuget System.IdentityModel.Tokens.Jwt.

Package nuget JWT

Et voici le snippet pour le décoder :

var stream = authenticationResult.IdToken;
var handler = new JwtSecurityTokenHandler();
JwtSecurityToken decodedToken = (JwtSecurityToken)handler.ReadToken(stream);

Vous retrouverez toutes les informations dans une collection d’objet Claim, notamment au travers les propriétés Type et Value :

Structure des données de Claim

Exemple, pour afficher le nom et la ville de l’utilisateur :

string name = decodedToken.Claims.FirstOrDefault(c => c.Type == "name")?.Value ?? "Noname";
string city = decodedToken.Claims.FirstOrDefault(c => c.Type == "city")?.Value ?? "Unknown city";

UserInfoLabel.Text = $"Welcome {name}, from {city}";

Le code sera appelé de préférence dans le OnAppearing de la page plutôt que dans son constructeur.

Et voilà le résultat :

Page de Logout

Déconnexion

La déconnexion consiste principalement à retirer un compte de AuthenticationClient :

private async void LogoutButton_Clicked(object sender, EventArgs e)
{
    IEnumerable<IAccount> accounts = await App.AuthenticationClient.GetAccountsAsync();
    while (accounts.Any())
    {
        await App.AuthenticationClient.RemoveAsync(accounts.First());
        accounts = await App.AuthenticationClient.GetAccountsAsync();
    }
    
    await Navigation.PopAsync();
}

La page de Signin

Page de Signin

Page de réinitialisation de mot de passe

Page de réinitialisation de mot de passe