Azure Functions Http et sécurité : utiliser un Azure AD
Dans cet article, nous allons voir comment sécuriser l'accès à une Azure Function Http en utilisant la fonctionnalité "Authentication / Authorization" présente sur chaque AppService.
TL; DR;
On peut en quelques clics et gratuitement mettre en place une authentification sur n'importe quel AppService. Avec un peu de configuration, on peut aussi s'y connecter depuis du javascript (ou n'importe quelle application distante).
Sur Azure...
Partons du principe que nous avons créé une Azure Function Http : on peut donc la déclencher via un appel Http classique et elle va nous retourner un résultat de son choix. Dans mon cas, je créé une fonction HttpTrigger1
sur un AppService CallMeMaybe-authentication
depuis le portail et elle va utiliser du code très simple mais que je peux déclencher depuis le portail Azure... pratique pour les tests!
Ce que nous allons voir est valable sur un AppService, vous pourriez donc en faire de même pour une Api classique !
Configuration de l'App Service
Nous allons configurer notre AppService pour permettre une authentification de l'utilisateur via Azure Active Directory(AAD). Pour cela, on se rend sur la section "Authentication / Authorization" de l'app service et on active l'App Service Authentication.
Dans la liste de choix qui apparaît, on choisit alors de demander la connexion de l'utilisateur avec AAD si ce n'est pas déjà le cas. Maintenant, on clique sur Azure Active Directory et on choisit le mode express qui va créer automatiquement pour nous une application dans l'AD de notre tenant Azure. Une fois ceci effectué, on n'oublie pas de sauvegarder :)
Une fois ceci fait, tout appel à votre Azure Function HttpTrigger devra être authentifié. Ainsi, si j'ouvre l'URL de mon AF Http dans un navigateur, j'ai une mire de connexion qui s'affiche et on me demande si j'autorise ou non l'application à accéder à mon profil.
Une fois que j'ai donné mon accord, le résultat de mon AF s'affiche. On vient donc en quelques secondes de mettre en place une sécurisation de notre Azure Function http !
Bon, c'est parfait tout cela, mais dans les faits, il est rare que l'on demande à l'utilisateur d'appeler directement les endpoints de nos Apis dans son navigateur. Il va donc falloir faire un peu de configuration de notre application AAD.
Configuration de l'Azure Function et de l'application AAD
La première erreur que l'on risque d'avoir en appelant l'Api est celle-ci :
Access to XMLHttpRequest at 'https://ddd-authentication.azurewebsites.net/api/HttpTrigger1' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Pour corriger ce problème, on va dans la section "CORS" et on rajoute l'url de notre front javascript :
Ensuite, il va falloir aller configurer l'enregistrement de l'application correspondant à votre AppService dans l'AAD pour mettre l'url de votre front comme Url de redirection post authentification :
Un peu plus bas, il faudra cocher les cases Access Token et Id Token qui nous serons utiles plus tard.
Si vous souhaitez que des identités n'appartenant pas à votre tenant puissent se connecter, il faut activer l'option multi-tenant un peu plus bas sur ce même écran. Cela peut être notamment le cas si vous souhaitez que n'importe quelle personne avec un compte Microsoft puisse se connecter. (Si vous rencontrez des erreurs à la sauvegarde, cette réponse StackOverflow pourrait vous être utile).
Une fois que cela est fait, il vous faut aller noter et mettre de côté deux informations présentes sur la section "Expose an Api" :
- L'application Id Uri en haut de la page.
- Le scope d'accès à votre Azure function. Par exemple : https://monTennant.onmicrosoft.com/callmemaybe/user_impersonation
Cette "Application Id Uri" sera utilisée comme audience du jeton d'accès à l'Api. Il faut donc aller dire à votre AppService de reconnaître cette audience : dans la section "Authentication / Authorization", on reclique sur Azure Active Directory, on ajoute l'audience dans cette liste puis on sauvegarde.
On en a maintenant terminé avec toutes ces étapes de configuration ultra intuitives (sic...) et on va pouvoir faire un peu de code !
Faire un appel en javascript
Pour cela, j'utilise la librairie Msal de Microsoft. Prenez bien la dernière version car Microsoft apporte régulièrement des patchs de sécurité (comme l'utilisation du code-flow + PKCE dont on a déjà parlé avant dans un article). La liste complète des versions est présente sur le Github du projet. Il suffit d'ajouter cette balise dans votre html :
<script type="text/javascript" src="https://alcdn.msftauth.net/lib/1.3.0/js/msal.js" crossorigin="anonymous"> </script>
Le code pour se connecter suivra alors cette logique :
- Je vérifie si je suis connecté en récupérant l'utilisateur courant.
- Si je ne suis pas connecté, je demande une connexion de l'utilisateur dans la même page.
- Une fois connecté, je demande un jeton d'accès afin de pouvoir accéder aux AFs.
- Je peut maintenant faire mes appels avec le jeton.
// Laissons MSAL interprété les redirection vers notre page myMSALObj.handleRedirectCallback((error, response) => {}); // récupération de l'utilisateu courant const user = myMSALObj.getAccount(); // Pas connecté, je demande une connexion if (!user) { const loginRequest = { scopes: ['openid'] }; myMSALObj.loginRedirect(loginRequest); return; } // si je suis ici, je suis connecté == je demande un jeton d'accès const tokenRequest = { scopes: ['https://dd.onmicrosoft.com/callmemaybe/user_impersonation'] }; myMSALObj.acquireTokenSilent(tokenRequest) .then((value: Msal.AuthResponse) => { // Appel à l'Azure function en utilisant le token $.ajax( { headers: {'Authorization': `Bearer ${value.accessToken}`}, type: 'GET', url: urlVersLAF, success: (data) => { console.log('success ' + data); }, error: (jqXHR1, textStatus, errorThrown) => { console.log('error ' + textStatus + ' - ' + errorThrown); } } ) }).catch(error => { // on a pas réussi à obtenir le token de manière silencieuse : // réessai en mode "redirection" return myMSALObj.acquireTokenRedirect(tokenRequest); });
Accéder aux informations sur l'utilisateur connecté
Ici aussi c'est finalement simple : il suffit d'ajouter un paramètre de type ClaimsPrincipal à notre AzureFunction. Ce dernier sera rempli automatiquement par le runtime. Voici un code tout bête permettant d'inspecter ce qu'on a dedans :
public static async Task<IActionResult> Run ( HttpRequest req, ILogger log, ClaimsPrincipal principal) { string responseMessage = string.Empty; foreach (Claim claim in principal.Claims) { responseMessage += claim.Type + " - " + claim.Value + System.Environment.NewLine; } return new OkObjectResult (responseMessage); }
Nous aurons alors ce type d'informations retournées par notre fonction :
aud - https://monTenant.onmicrosoft.com/callmemaybe iss - https://sts.windows.net/9c9fb00d-5eee7efb6bf5/ http://schemas.microsoft.com/claims/authnclassreference - 1 aio - AUQAu/8PAAAAUzxKkKcsOtp8nPwr+enDDchcsA3FmqC585S0W8yk5mDIsstF2NV4Zuu5HxtwqDHBr7oAYY69G4fT7bIaXaYmxQ== http://schemas.microsoft.com/claims/authnmethodsreferences - pwd appid - 9bd6a8e1-62ca-4f00-b223-0fcea60e2d07 appidacr - 0 http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress - jantoine@infinitesquare.com http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname - ANTOINE http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname - Jonathan PRO http://schemas.microsoft.com/identity/claims/identityprovider - https://sts.windows.net/7c7020c6-d3ac-ddd-b644-32bb956b9753/ name - Jonathan ANTOINE http://schemas.microsoft.com/identity/claims/objectidentifier - c5896f20-8e42-ddd-9f0f-9495326f8f9d rh - 0.AQwADbCfnM5TK0dddpvKYgBPsiMPzqYOLQcMAAc. http://schemas.microsoft.com/identity/claims/scope - user_impersonation http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier - 78VdmMtFq9zF4sUAddNMuXwvdL_OL8ekVfNew08 http://schemas.microsoft.com/identity/claims/tenantid - dddd-53ce-dddd-ddd-ddddd http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name - jantoine@infinitesquare.com uti - 4mN4ybdddddN-grR7TlEAA ver - 1.0
On peut y retrouver facilement les nom, prénom et email de l'utilisateur : de quoi se débrouiller ;)
Limiter l'accès à certains utilisateurs
Avec ce que l'on vient de faire, toutes les personnes de votre AD peuvent se connecter à votre Azure Function. Il existe un moyen simple d'en limiter l'accès qu'à certaines personnes via le portail Azure en activant ce switch dans l'AppRegistration de votre application AAD :
Si l'utilisateur n'a pas accès, vous obtiendrez ce message d'erreur :
The signed in user '{EmailHidden}' is not assigned to a role for the application '9bddd3-0fcea60e2d07'(CallMeMaybe-authentication).
Dans votre javascript, vous pouvez modifier handleRedirectCallback pour gérer ce cas. Voici un exemple efficace :
let errorInSignin = false; myMSALObj.handleRedirectCallback((error, response) => { if (error) { errorInSignin = true; window.alert(error); } }); if (errorInSignin) { return; }
Série
Cet article fait partie d'une série et je vous invite à lire le précédent sur l'utilisation des access keys.
Happy coding !
Pour aller plus loin.
Commentaires