L’authentification est un vaste sujet avec un large champ d’applications possibles : l’accès à un compte, à un document, à un service, etc. L’accès à ces différents éléments peut se faire via de nombreux supports, notamment le mobile. En tant qu’appareil, le mobile est ce qu’on peut considérer de plus personnel, avant même un PC : il est donc évident que l’authentification sur cet outil représente un enjeu important.

Dans le cadre du Mois de l’Identité, nous vous proposons aujourd’hui d’illustrer par un exemple simple comment authentifier un utilisateur via une application Flutter et azure B2C.

Pour en savoir plus sur Azure B2C vous pouvez consulter la documentation Microsoft.

Je vous invite également à lire notre précédent article de blog sur Comment démarrer un projet Flutter en venant de Xamarin.

 

S’identifier sur un mobile

Lorsque vous souhaitez donner l’accès à votre application après une authentification, plusieurs choix s’offrent à vous. Les plus fréquents sont de passer par une page native à l’application ou d’utiliser une page web.

Dans notre exemple, nous authentifierons nos utilisateurs via une page web. L’avantage d’une page web est d’externaliser le processus d’authentification en dehors de l’application.

Pour commencer, nous allons créer une page d’accueil qui aura pour but soit de proposer à notre utilisateur de se connecter, soit de le connecter automatiquement s’il l’a déjà été précédemment.

page d'accueil avec icône et bouton

Notre page d’accueil est très simple. Elle présentera à l’utilisateur trois éléments :

  • une icône ;
  • un texte d’accroche très court ;
  • un bouton pour se connecter.

Cette étape peut même être transparente pour l’utilisateur s’il est déjà authentifié.

Commencez par créer un widget MyLandingPage qui fera office de page d’accueil.

Dans votre méthode build, rajoutez les éléments nécessaires de manière à avoir le rendu qui vous intéresse.

N’hésitez pas à créer des styles que vous pourrez réutiliser sur vos composants : ici, par exemple, Button_MainStyle définit l’aspect général des boutons.

 

Pour le bouton de notre exemple, j’ai choisi d’utiliser un ElevatedButton. Grâce au onPressed de ce widget, l’utilisateur sera redirigé vers une page où il pourra saisir ses identifiants.

A ce stade, notre méthode build ressemble à ceci :

@override
Widget build(BuildContext context) {
  return Scaffold(
      backgroundColor: Color_MainBlue,
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Container(
                margin: EdgeInsets.only(bottom: 30, top:50),
                child: Image.asset('assets/images/rum.png', height: 120, width: 120)
            ),
            Text(
              'Ma petite cave à rhum secrète', style: TextStyle(color: Colors.white),
            ),
            Container(
                margin: EdgeInsets.only(top: 150),
                child: ElevatedButton(
                    onPressed: () => Navigator.pushNamed(context, '/auth'),
                    child: Text('Connexion'),
                    style: Button_MainStyle
                )
            ),
          ],
        ),
      )
  );
}


Comme vous le constatez, le déclenchement du onpress du bouton entraîne une navigation vers une nouvelle page : WebConnectionPage.

 

⚠️Conseil : avant d’aller plus loin, si vous n’êtes pas familier avec le workflow d’authentification utilisé par Azure AD B2C, je vous recommande de lire cet article sur le « Flux de code d’autorisation OAuth 2.0 dans Azure Active Directory B2C ».

 

Pour rappel Azure AD B2C utilise le protocole OAuth et le workflow que nous devrons suivre est le suivant :

                                                 +-------------------+

                                                 |   Authz Server    |

       +--------+                                | +---------------+ |

       |        |--(A)- Authorization Request ---->|               | |

       |        |       + t(code_verifier), t_m  | | Authorization | |

       |        |                                | |    Endpoint   | |

       |        |<-(B)---- Authorization Code -----|               | |

       |        |                                | +---------------+ |

       | Client |                                |                   |

       |        |                                | +---------------+ |

       |        |--(C)-- Access Token Request ---->|               | |

       |        |          + code_verifier       | |    Token      | |

       |        |                                | |   Endpoint    | |

       |        |<-(D)------ Access Token ---------|               | |

       +--------+                                | +---------------+ |

                                                 +-------------------+

 

Maintenant que vous avez le workflow en tête, créez une nouvelle classe AuthService. Cette classe aura pour vocation de traiter les différentes phases et informations liées à l’authentification.

WebConnectionPage authentification

Créez également un widget WebConnectionPage.

Sur ce nouvel écran, nous allons simplement afficher notre page d’authentification depuis un navigateur. Dans la méthode build, on rajoute un composant WebView.

Pour avoir accès à ce composant, installez le package webview_flutter en l’ajoutant aux dépendances de votre projet dans le fichier pubspec.yaml.

La base de l’url à appeler dans la webview est GET https://{tenant}.b2clogin.com/{tenant}.onmicrosoft.com/{policy}/oauth2/v2.0/authorize

 

Dans les paramètres de la requête, n’oubliez pas de passer le client ID, ainsi que le code_challenge qui devra être renvoyé au moment de demander le token.

Dans notre exemple, j’ai choisi d’utiliser la méthode S256 (sha256) comme méthode de hash du code_challenge.

Pensez également à rajouter offline_access au scope, de façon à pouvoir récupérer un refresh_token en même temps que le token d’authentification.

Pour simplifier le développement, j’ai rajouté une méthode getLoginUrl qui va me retourner une url construite avec les bons paramètres et que j’utiliserai dans intialUrl de la webView.

 

String getLoginUrl()
{
  var pkce = generatePKCE();
  return authEndpoint + '&code_challenge=$pkce&code_challenge_method=S256';
}

 

String generatePKCE() {
  var code = getRandomString(128);
  challengeCode = code;
  var hash = sha256.convert(ascii.encode(code));
  return base64Url.encode(hash.bytes).replaceAll("=", "").replaceAll("+", "-").replaceAll("/", "_");
}

 

Je vous invite par ailleurs à consulter la documentation Microsoft sur le code flow authorization. 

 

Votre méthode build doit désormais ressembler à cela :

@override
Widget build(BuildContext context) {
  return Scaffold(
    backgroundColor: Color_MainBlue,
    appBar: AppBar(
      elevation: 0,
      iconTheme: IconThemeData( color:Color_Accentuation),
      backgroundColor: Color_MainBlue,
      title: Text(widget.title, style: TextStyle(color:Color_Accentuation )),
    ),
    body: WebView(
            javascriptMode: JavascriptMode.unrestricted,
            initialUrl: authService.getLoginUrl()
        )
  );
}

 

Ajoutez une méthode loginWithAuthCode à la classe AuthService avec, pour argument, un string correspondant à l’url de retour et un booléen en retour. Cette méthode aura pour but de gérer le workflow d’authentification, de la récupération du code d’authentification présent dans l’url de retour à la récupération du token d’accès.

 

Nous appellerons cette méthode en lui passant en argument la redirect_uri que l’on obtient après que l’utilisateur a cliqué sur « se connecter » .

 

Parcourez la chaîne de façon à récupérer le code d’authentification qui vous est renvoyé si la première étape est un succès. Si le code est trouvé, on peut alors passer à la suite en demandant le jeton d’authentification via le endpoint :

POST https://{tenant}.b2clogin.com/{tenant}.onmicrosoft.com/{policy}/oauth2/v2.0/token

 

Pour effectuer cette requête post, j’utilise le package http. Vous pouvez aussi utiliser directement la classe HttpClient disponible nativement dans dart.io.

Dans le body du post, n’oubliez pas d’ajouter le code d’authentification reçu précédemment, votre client_id et surtout de renseigner le code_verifier qui correspond au code_challenge sans hash, qui vous a servi à afficher la page de connexion.

 

En cas de retour positif de l’API, le contenu de la réponse a la structure suivante :

{
    "not_before": "1442340812",
    "token_type": "Bearer",
    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ik5HVEZ2ZEstZnl0aEV1Q...",
    "scope": "90c0fe63-bcf2-44d5-8fb7-b8bbc0b29dc6 offline_access",
    "expires_in": "3600",
    "refresh_token": "AAQfQmvuDy8WtUv-sd0TBwWVQs1rC-Lfxa_NDkLqpg50Cxp5Dxj0VPF1mx2Z...",
}

 

On y retrouve notamment le token de connexion et le refresh token (si vous avez passé à nouveau offline_access dans le scope), ainsi que les dates d’expiration.

Nous allons à présent stocker ces informations dans le secure storage de façon à pouvoir les utiliser plus tard.

Ajoutez le package flutter_secure_storage à votre fichier pubspec.yaml. Il nous offre un accès la classe secureStorage et sa méthode write.

 

A ce stade, la méthode loginWithAuthCode devrait être proche de celle-ci :

Future<bool> loginWithAuthCode (String url) async
{
  // Check scheme
  if(!url.startsWith('app://landing/?'))
  {
    return false;
  }

  // EXCTRACT CODE FROM REDIRECT_URI
  RegExp regExp = RegExp(r'[?&]code=([^&]+).*$');
  var match = regExp.allMatches(url);
  var authCode =  match.first.group(1);



  // PREPARE TOKEN REQUEST BODY
  var parametersMap =  new Map();
  parametersMap['grant_type'] = 'authorization_code';
  parametersMap['code'] = authCode;
  parametersMap['client_id'] = CLIENT_ID;
  parametersMap['redirect_uri'] = "app://landing";
  parametersMap['scope'] = "openid offline_access";
  parametersMap['code_verifier'] = challengeCode;

  // POST : TOKEN REQUEST
 var response =  await http.post(Uri.parse(tokenEndpoint), body: parametersMap);

 // CHECK REQUEST RESPONSE
 if(response.statusCode == 200)
   {
     token = Token.fromJson(json.decode(response.body));

     if(token.idToken != null)
       {

         // STORE TOKEN
         secureStorage.write(key: TOKEN_KEY , value: response.body);


         return true;
       }
   }

  return false;
}

 

Revenons à présent à notre web_connexion_page et appelons au onPageFinised de notre webview la méthode loginWithAuthCode que nous venons de terminer. En fonction du résultat, redirigez l’utilisateur vers la page « home » ou affichez un message d’erreur.

 

Widget build(BuildContext context) {
  return Scaffold(
    backgroundColor: Color_MainBlue,
    appBar: AppBar(
      elevation: 0,
      iconTheme: IconThemeData( color:Color_Accentuation),
      backgroundColor: Color_MainBlue,
      title: Text(widget.title, style: TextStyle(color:Color_Accentuation )),
    ),
    body: WebView(
      javascriptMode: JavascriptMode.unrestricted,
      initialUrl: widget.loginUrl,
        onPageFinished: (String redirectUrl) async
      {
        if (await this.authService.loginWithAuthCode(redirectUrl))
        {
         Navigator.pushNamedAndRemoveUntil(
            context, "/home", ModalRoute.withName('/'));
        }
      }
      ) 
  );
}

 

ecran d'accueil widget home build

Créez un nouveau widget pour l’écran home et dans la méthode build, construisez votre page comme bon vous semble.

Ajoutez également un bouton permettant de déconnecter l’utilisateur. Nous utiliserons l’action onPressed pour appeler la méthode logout que vous aurez préalablement ajoutée à la classe AuthentService.

logout aura pour but de supprimer le token du SecureStorage et d’appeler la route de déconnexion.

Nous avons à présent géré de bout en bout la connexion et la déconnexion d’un utilisateur à sa demande.  Votre token ID et votre token de refresh sont dans votre secureStorage : vous allez pouvoir les utiliser comme bon vous semble, notamment pour garder l’utilisateur connecté ou récupérer des donnés de vos APIs.

Pensez à créer une méthode getToken dans la classe AuthentService. Elle vous permettra, à l’ouverture de l’app sur la page, de rediriger l’utilisateur vers la page Home si un token valide non-expiré existe et de le rafraîchir le cas échéant grâce au refresh_token.

 

Pour aller plus loin :

Vous avez à présent toutes les armes pour authentifier vos utilisateurs dans une app Flutter en utilisant Azure B2C.

Vous souhaitez mettre en pratique les process les méthodes présentées tout au long de cette série dédiée à l’Identité ? N’hésitez pas à visionner le replay de notre webinaire !

Azure AD par la pratique