.Net

Réaliser une authentification Azure Active Directory avec Flutter et le pattern Bloc

Jan 24, 2023

Ferenc DEKONINCK

Vous utilisez sans doute Flutter pour réaliser une application cross-platform pour mobile, web, bureau ou encore système embarqué, avec une API custom en .Net Core, et vous cherchez à intégrer une authentification avec Azure.

Vous êtes tombés au bon endroit, nous allons voir ensemble comment intégrer une authentification avec un Azure Active Directory dans une application Flutter en utilisant le pattern Bloc, et comment la configurer correctement pour l’utiliser également à travers un token dans une API custom.

Ce qu’il faut savoir

Standard horizontal lockup

La technologie Flutter s’appuie sur le langage Dart, le tout étant développé par Google. Dart possède son propre gestionnaire de packages nommé Pub.

Le gestionnaire de package pour Dart et Flutter ne contient aucun package officiel Microsoft, il nous faudra alors traiter avec des packages crées par la communauté.

Le pattern Bloc (Business Logic Component) permet de faire une séparation entre la partie dite de présentation (l’affichage) et la partie de logique métier. Afin de réaliser une application de démonstration pour notre sujet, nous utiliserons les packages bloc et flutter_bloc.
Si vous souhaitez en apprendre plus, vous pouvez consulter la documentation officielle ici :

Entrons dans le vif du sujet

Pour réaliser cette authentification nous allons utiliser le package aad_oauth, développé par l’entreprise EarlyByte. Il se base sur l’endpoint OAuth2 v2.0 d’Azure Active Directory.
Si vous souhaitez en apprendre plus par vous-même, voici la documentation de ce package :

Pour installer les packages nous allons utiliser les commandes suivantes :

flutter pub add aad_oauth
flutter pub add bloc
flutter pub add flutter_bloc

Dans un premier temps nous allons mettre en place le pattern bloc. Une fois le package installé, Android Studio (l’IDE que votre rédacteur utilise) vous proposera des options supplémentaires dans le menu contextuel pour créer nos fichiers.

Pensez à bien sélectionner « Equatable ».
Nous sélectionnons Equatable pour sa capacité à réaliser des comparaisons entre des objets et des données brutes.
Vous obtiendrez ces trois fichiers

login_state

part of 'login_bloc.dart';

enum LoginStatus { unknown, authenticated, unauthenticated }

class LoginState extends Equatable {
  const LoginState._({this.status = LoginStatus.unknown});

  const LoginState.unknown() : this._();
  const LoginState.authenticated() : this._(status: LoginStatus.authenticated);
  const LoginState.unauthenticated() : this._(status: LoginStatus.unauthenticated);

  final LoginStatus status;

  static Future<LoginState> initState() async{
    var token = await oauth.getAccessToken();
    if(token != null){
      var decodedToken = JwtDecoder.decode(token);
      final DateTime expirationTime = DateTime.fromMillisecondsSinceEpoch(decodedToken['exp'] * 1000);
      if(expirationTime.isAfter(DateTime.now())){
        return const LoginState.authenticated();
      }else{
        await oauth.logout();
        return const LoginState.unauthenticated();
      }
    }else{
      return const LoginState.unauthenticated();
    }
  }

  @override
  List<Object?> get props => [status];
}

Dans ce code nous avons réalisé trois statuts :
unknown : C’est le statut par défaut avant de savoir si un utilisateur est connecté
authenticated : Ce statut est retourné lorsqu’un utilisateur est connecté
unauthenticated : Ce statut est retourné lorsqu’aucun utilisateur n’est connecté

La méthode initState() vient appeler la méthode getAccessToken() du package aad_oauth (que nous détaillerons un peu plus bas) pour tester dans un premier temps si l’Access Token est nul, et dans le cas où il ne serait pas nul, sa validité.
En fonction des résultats de ces conditions, le statut retourné sera soit « Authentifié », soit « Non-authentifié », et pourra potentiellement forcer la déconnexion d’un compte en cas d’invalidité du token.

login_event

part of 'login_bloc.dart';

abstract class LoginEvent extends Equatable {
  const LoginEvent();

  @override
  List<Object> get props => [];
}

class LoginSuccessful extends LoginEvent {
  const LoginSuccessful();

  @override
  List<Object> get props => [];
}

class LoginUnlogged extends LoginEvent {
  const LoginUnlogged();
}

class InitEvent extends LoginEvent {
  const InitEvent();

  @override
  List<Object> get props => [];
}

Ce code permet de définir des événements que nous pourrons appeler tout au long du code que nous rédigerons.
Vous reconnaitrez les événements qui sont liés aux statuts crées précédemment.

login_bloc

import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:jwt_decoder/jwt_decoder.dart';

import '../config/config_builder.dart';

part 'login_event.dart';
part 'login_state.dart';

class LoginBloc extends Bloc<LoginEvent, LoginState> {
  LoginBloc() : super(const LoginState._()) {
    on<InitEvent>(_onInitialEvent);
    on<LoginSuccessful>(_onLoginSuccessful);
    on<LoginUnlogged>(_onUnLogging);
  }

  void _onInitialEvent(InitEvent event, Emitter<LoginState> emit) async {
    final status = await LoginState.initState();
    emit(status);
  }

  void _onLoginSuccessful(LoginSuccessful event, Emitter<LoginState> emit) {
    const status = LoginState.authenticated();
    emit(status);
  }

  void _onUnLogging(LoginUnlogged event, Emitter<LoginState> emit) async {
    await oauth.logout();
    const status = LoginState.unauthenticated();
    emit(status);
  }
}

Dans ce code, les diverses méthodes réalisées seront exécutées en fonction des événements qui auront été déclenchés dans l’application.

La configuration de aad_oauth

La configuration du package est flexible et permet de s’adapter à votre Active Directory, vos applications et leurs besoins.
Si vous épluchez la documentation du package, ou allez consulter le code source sur GitHub, vous pourrez retrouver toutes les propriétés disponibles ainsi que des explications pour chacune d’entre-elles.

Pour notre configuration nous allons utiliser 5 des propriétés disponibles : tenantId, clientId, redirectUri, scope, navigatorKey.

Pour obtenir certaines de ces valeurs, nous allons devoir créer une App Registration pour chacune des applications dans le portail Azure.

Nous pouvons dès à présent récupérer notre tenantId ainsi que le clientId.
Pour générer notre redirectUri, nous devons obtenir le signature hash de notre application. Pour cela, voici un lien très utile vers la documentation officielle Android :

Une fois notre signature hash obtenu, il faudra se rendre dans la partie Authentication de notre App Registration, ajouter une plateforme Android puis spécifier le nom du package (le nom complet de votre projet défini à sa création) ainsi que le signature hash, une URL de redirection sera ainsi générée pour notre application.

Pour obtenir notre scope nous avons un peu plus de manipulations à réaliser.

Dans l’App Registration de notre API, nous allons ajouter un Scope dans la partie Expose an API, que vous nommerez comme vous le souhaitez.

Nous allons copier l’URI du scope que nous venons de créer pour l’utiliser en tant que scope dans notre configuration.

De retour dans l’App Registration de notre application Flutter, dans la partie API Permissions, nous allons ajouter une permission.

Nous allons sélectionner notre API dans l’onglet My API.

Puis nous allons sélectionner la permission que nous venons de créer.

Cet ajout de permission est important, en effet, par défaut dans un token généré par les services de Microsoft est présent une propriété nonce dans le header de ceux-ci. Cette propriété vient « verrouiller » le token pour n’être utilisable que par les API de Microsoft.
Ce petit guide sur les problèmes rencontrés avec l’Active Directory saura vous en dire plus sur ce sujet :

Pour compléter notre configuration, il ne nous reste plus qu’à définir la navigatorKey. Pour cela nous devons définir une clé globale de type NavigatorState.

final navigator = GlobalKey<NavigatorState>();
La navigatorKey utilisée doit être la même que celui du widget MaterialApp !

Pour respecter ceci, nous vous conseillons de créer un fichier de constantes à la manière d’un app.settings en .Net.

Exemple d’utilisation du navigatorKey dans le MaterialApp :

class _AppViewState extends State<AppView> {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<LoginBloc, LoginState>(builder: (context, state) {
      return MaterialApp(
        title: 'Flutter Demo',
        navigatorKey: navigator,
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),

...

Nous avons désormais toutes nos valeurs pour compléter notre configuration !

Dans un fichier que nous allons nommer config_builder.dart nous allons construire la configuration et initialiser l’objet AadOAuth du package.

import 'package:aad_oauth/aad_oauth.dart';
import 'package:aad_oauth/model/config.dart';

import 'constants.dart';

final Config config = Config(
    tenant: tenantId,
    clientId: clientId,
    redirectUri: redirectUri,
    scope: scope,
    navigatorKey: navigator);

final AadOAuth oauth = AadOAuth(config);

Nous pouvons à présent appeler notre variable oauth dans tous les fichiers du projet pour utiliser ses méthodes !

Utiliser aad_oauth dans le projet

Dans notre application nous allons faire une petite page avec deux boutons pour gérer la connexion / déconnexion d’un utilisateur.

class _AppViewState extends State<AppView> {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<LoginBloc, LoginState>(builder: (context, state) {
      return MaterialApp(
        title: 'Flutter Demo',
        navigatorKey: navigator,
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: Scaffold(
            appBar: AppBar(
              title: const Text("Authentication Demo App"),
            ),
            body: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Builder(builder: (context) {
                    return ElevatedButton(
                        onPressed: () async {
                          try {
                            await oauth.login();
                            if(mounted){
                              context
                                  .read<LoginBloc>()
                                  .add(const LoginSuccessful());
                            }
                          } catch (e) {
                            print("error $e");
                          }
                        },
                        child: const Text("Se connecter"));
                  }),
                  if (state.status == LoginStatus.authenticated) ...[
                    const Text("Vous êtes connecté"),
                    OutlinedButton(
                        onPressed: () async {
                          try {
                            LoginBloc().add(const LoginUnlogged());
                          } catch (e) {
                            print("error $e");
                          }
                        },
                        child: const Text("Se déconnecter"))
                  ],
                ],
              ),
            )),
      );
    });
  }

Dans le code ci-dessus , nous appelons la mire de connexion en appelant la méthode await oauth.login();. Une WebView s’ouvre alors automatiquement, il ne nous reste plus qu’à entrer les informations de connexion. Une fois la connexion validée, la WebView se ferme. Il ne vous restera plus qu’à implémenter le comportement qui vous plaira.

Pour réaliser une déconnexion, il vous suffira d’appeler await oauth.logout(); et le tour est joué !

Les méthodes disponibles avec aad_oauth

Nous avons vu précédemment deux méthodes : login et logout.

Il existe également deux autres méthodes qui se ressemblent : getAccessToken et getIdToken qui vous fourniront des tokens en fonction de votre besoin.

Pour communiquer avec notre API nous utilisons des Access Token qui contiennent toutes les informations dont nous avons besoin pour identifier un utilisateur à travers des appels API.

Implémenter l’Authorize dans une API

Afin de pouvoir limiter les accès aux routes de l’API, nous allons utiliser le célèbre attribut Authorize. Nous allons utiliser le projet par défaut généré par Visual Studio : le WeatherForecast Project.

Pour cela, nous allons dans un premier temps créer un fichier ServiceCollection.cs pour ajouter la méthode suivante :

using Microsoft.Identity.Web;

namespace Article_de_blog
{
    public static class ServiceCollection
    {
        public static void InitialiseAuthenticationConfig(this IServiceCollection services, IConfiguration configuration)
        {
            services.AddMicrosoftIdentityWebApiAuthentication(configuration);
            services.AddHttpContextAccessor();
        }
    }
}

Dans le fichier Program.cs, on va ajouter trois lignes de code pour obtenir ceci :

using Article_de_blog;

var builder = WebApplication.CreateBuilder(args);
IConfiguration configuration = builder.Configuration;

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

//Ajout de cette ligne
builder.Services.InitialiseAuthenticationConfig(configuration);


var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

//Ajout de ces deux lignes, attention l'ordre est important !
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

Dans notre appsettings.json, nous allons ajouter cet objet qu’il faudra remplir avec nos diverses informations :

"AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "ClientId": "Votre client Id",
    "TenantId": "Votre tenant Id",
    "Audience": "Votre audience (api://votre client Id en général)",
    "Domain": "Domaine de votre Active Directory"
  }

Enfin, dans notre controller nous allons ajouter l’attribut [Authorize] pour protéger notre route :

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Article_de_blog.Controllers
{
    [ApiController]
    [Route("[controller]")]
    //Attribut à ajouter
    [Authorize]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }

        [HttpGet(Name = "GetWeatherForecast")]
        public IEnumerable<WeatherForecast> Get()
        {
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = Summaries[Random.Shared.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }
}

Notre API est enfin prête, il ne nous reste plus qu’à réaliser l’appel dans notre application Flutter, puis afficher les données retournées par celle-ci.

Appeler l’API depuis l’application Flutter

Afin d’appeler l’API, nous allons utiliser le package http de Dart, et réaliser une méthode comme suivant

var uri = Uri(
        scheme: 'https',
        host: "10.0.2.2",
        path: "WeatherForecast",
        port: 7091);

var token = await oauth.getAccessToken();

var response = await http.get(uri, headers: {"Authorization": "Bearer $token"});
Lors du développement, si vous cherchez à contacter une API ou toute autre application qui fonctionne en local sur votre machine (localhost), il faudra alors appeler l’adresse “10.0.2.2”. Si vous appelez localhost, vous allez alors appeler une application sur votre émulateur Android ou iOS, ou l’appareil physique sur lequel vous exécutez votre application.

Nous pourrons alors traiter la variable response pour récupérer les données de son body afin de les traiter comme bon nous semble et les afficher.

Voici le résultat de notre projet :

Merci de nous avoir lu, et à bientôt pour un autre sujet !

Découvrez en plus sur notre offre Modernisation d’Application en vous rendant sur notre page dédiée.

0 commentaires

Soumettre un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Découvrez nos autres articles

git et la face cachée du Rebase

git et la face cachée du Rebase

"Faire une rebase ? *sight* heu... ok..." Jean-Michel Fullstack - Développeur fébrile Jean-Michel est inquiet. En effet, lorsque nous collaborons à plusieurs sur un projet, quelque soit les technologies utilisées, il est important de garder à l'esprit que notre...

lire plus
Aller au contenu principal