Authentification Azure Directory B2C dans une application Xamarin.Forms
par Sylvainpublié leOn 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é
- Créer un tenant Active Directory B2C dans Azure.
- Enregistrer l’application mobile dans le tenant Active Directory B2C.
- Créer des User Flows avec des polices de Signin, réinitialisation de mot de passe…
- Utiliser la Microsoft Authentication Library (MSAL) pour gérer le flux d’authentification dans l’application.
Sources
- Authenticate Users with Azure Active Directory B2C
- Tutorial: Create an Azure Active Directory B2C tenant
- Tutorial: Register a web application in Azure Active Directory B2C
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.
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. :-)
Cliquez sur 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 :
Dans le panneau de gauche, cliquez sur Resource providers
Cherchez Azure, sélectionnez Microsoft.AzureActiveDirectory et cliquez sur Register
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. :-)
C’est bon, on y est, on peut commencer !
Création de l’Active Directory B2C
Cliquez sur Create a resource
Cherchez et sélectionnez Active Directory B2C
Cliquez sur Create
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.
Cliquez sur Review + Create pour valider les informations
Cliquez sur Create pour en finir !
Patientez. Tant que ça gigote là-dedans c’est que ça bosse :
Cliquez sur le lien, bienvenue à Black Mesa !
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 :
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…
Saisissez simplement B2C dans la barre de recherche en haut et dans la rubrique Services, sélectionnez Azure AD B2C.
Enregistrement de l’application
Sélectionnez App registrations…
Puis New registration
Renseignez les informations
Nom et type de compte supporté :
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
.
Cet URi sera à renseigner dans les manifestes Android et iOS.
- Permissions :
Validez le tout en cliquant sur le bouton Register. Bravo ! Votre application est 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 :
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.
Puis sélectionnez Sign In et créez le flow dans sa version recommandée.
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 :
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.
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.
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 :
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 B2CtenantId
: 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 :redirect_uri
: l’uri enregistré côté azurepolicySignin
etpolicyPassword
: le nom des polices telles que vous les avez définies précédemmentiosKeychainSecurityGroup
: identifiant du Bundle iOS que vous trouvez dansInfo.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 :
Keychain dans Entitlements.plist
Il est ensuite nécessaire d’enregistrer un 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.
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 :
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 boutonLogout
<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
.
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
:
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 :
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();
}