EN FR

Denis VOITURON

for a better .NET world

Teams et Bazor WebAssembly - Authentication Token

Posted on 2024-06-03

Introduction

App Registration

  1. Enregistrez une nouvelle application dans le portail Microsoft Entra: Identity / Applications / App Registrations
  2. Sélectionnez New Registration et définissez les valeurs suivantes :
    • Définissez le Nom comme étant le nom de votre application.
    • Choisissez Accounts in any organizational directory (Any Microsoft Entra ID tenant - Multitenant).
    • Laissez l’URI de redirection vide.
    • Enregistrer.
  3. Naviguer vers Expose an API.
    • Sélectionnez le lien Add pour générer l’URI de l’Application ID, sous la forme api://<fully-qualified-domain-name>/<AppID>.

      L’identifiant AppId est la valeur affichée dans Overview / Application (client) ID.

      Ex: api://thbkwlr9-my-tunnel.euw.devtunnels.ms/74f3d663-9438-4ea0-9c32-8cf1d000fe9d.

    • Sélectionnez le bouton Add a scope.
      • Saisissez Scope name comme access_as_user .
      • Définissez l’option Who can consent? sur Admins and Users.
      • Remplissez les champs de configuration des invites de consentement de l’administrateur et de l’utilisateur avec des valeurs appropriées :
        • Titre du consentement de l’administrateur : Teams peut accéder au profil de l'utilisateur.
        • Description du consentement de l’administrateur : Permet à Teams d'appeler les API web de l'application en tant qu'utilisateur actuel.
        • Titre du consentement de l’utilisateur : Teams peut accéder au profil de l'utilisateur et faire des demandes en son nom.
        • Description du consentement de l’utilisateur : Permet à Teams d'appeler les API de cette application avec les mêmes droits que l'utilisateur.
      • Assurez-vous que le State est défini sur Enabled
    • La grille Scopes doit automatiquement correspondre à l’URI de l’ID de l’application défini à l’étape précédente:

      api://thbkwlr9-my-tunnel.euw.devtunnels.ms/74f3d663-9438-4ea0-9c32-8cf1d000fe9d/access_as_user.

      ⚠️ Contrairement aux autres URLs où il est possible d’ajouter plusieurs domaines différents, cette adresse devra être adaptée avec l’adresse de Production.

    • Dans la section Authorized client applications, identifiez les applications clients que vous voulez autoriser. Chacun des identifiants suivants doit être saisi :
      • 1fec8e78-bce4-4aaf-ab1b-5451cc387264 (Teams mobile/desktop application)
      • 5e3ce6c0-2b1f-4285-8d4b-75ee78787346 (Teams web application)

      Ces GUIDs sont décrits dans la documentation de Microsoft et correspondent aux identifiants des applications Teams. Ils doivent être utilisés tels quels.

      Expose and API

  4. Naviguez jusqu’à API Permissions, et assurez-vous d’ajouter les permissions suivantes :
    • Sélectionnez Microsoft Graph et Delegated permissions.
      • User.Read (activé par défaut)
      • email
      • offline_access
      • openId
      • profil

      API Persmissions

  5. Naviguer vers Authentication. Les utilisateurs devront donner leur accord la première fois qu’ils utiliseront l’application.
    • Définir une URI de redirection :
      • Sélectionnez Add a plateform.
      • Sélectionnez Single-page application.
      • Saisissez l’URI de redirection au format suivant : https://<fully-qualified-domain-name>/authentication/login-callback. Il s’agit de la page où un flux d’octroi implicite réussi redirigera l’utilisateur.
    • Activez les deux cases à cocher de Implicit grant and hybrid flows:
      • Access tokens (used for implicit flows)
      • ID tokens (used for implicit and hybrid flows)

      API Persmissions

Teams Manifest

Une application Teams est définir grâce à son Manifest qui centralise toutes les configurations et fonctionnalités disponibles.

Vous pouvez le créer visuellement via le portail https://dev.teams.microsoft.com

  1. Créez un nouvelle application en appuyant sur New app et donnez lui un nom.

  2. Dans la page Basic information :
    • Remplissez tous les champs obligatoires (nom, description, )
    • Encoder le Application (client) ID, identique à l’identifiant AppId qui est la valeur affichée dans Overview / Application (client) ID.

      Basic Informations

  3. Naviguer vers Configure / App features et cliquez sur Group and channel app:
    • Précisez la Configuration URL qui renseigne la page de configuration de votre projet (config.html)

      Ex. https://thbkwlr9-my-tunnel.euw.devtunnels.ms/config.html

    • Cochez la case Users can reconfigure the app.
    • Selectionner le scope Group chat.
    • Laissez les autres champs intacts.

    App features

  4. Naviguez vers Single sign-on
    • Complétez l’Application ID URI par l’adresse de l’API exposée dans Azure (ci-dessus). Vérifiez que la valeur access_as_user n’est PAS présente en fin d’URL.

      Ex. api://thbkwlr9-my-tunnel.euw.devtunnels.ms/74f3d663-9438-4ea0-9c32-8cf1d000fe9d

    App features

  5. Naviguez vers Domains et vérifiez que votre URL est bien présente.

En appuyant sur le bouton Publish en haut, à droite de l’écran, vous générez un ZIP contenant ce Manifest:

{
  "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.17/MicrosoftTeams.schema.json",
  "version": "1.0.0",
  "manifestVersion": "1.17",
  "id": "699234a3-9171-43ff-a1f1-3a822de11635",
  "name": {
    "short": "My Application",
    "full": "Managing ..."
  },
  "developer": {
    "name": "Denis Voituron",
    "mpnId": "",
    "websiteUrl": "https://dvoituron.com",
    "privacyUrl": "https://dvoituron.com",
    "termsOfUseUrl": "https://dvoituron.com"
  },
  "description": {
    "short": "My Application",
    "full": "Managing ..."
  },
  "icons": {
    "outline": "outline.png",
    "color": "color.png"
  },
  "accentColor": "#FFFFFF",
  "configurableTabs": [
    {
      "configurationUrl": "https://thbkwlr9-my-tunnel.euw.devtunnels.ms/config.html",
      "canUpdateConfiguration": true,
      "scopes": [
        "groupChat"
      ]
    }
  ],
  "validDomains": [
    "thbkwlr9-my-tunnel.euw.devtunnels.ms"
  ],
  "webApplicationInfo": {
    "id": "74f3d663-9438-4ea0-9c32-8cf1d000fe9d",
    "resource": "api://thbkwlr9-my-tunnel.euw.devtunnels.ms/74f3d663-9438-4ea0-9c32-8cf1d000fe9d"
  }
}

⚠️ Si vous avez besoin d’un environnement de Test et un autre de Production, vous devrez créer deux Manifest (ZIP) avec les URLs correctes.

Application Blazor WebAssembly

  1. Vous pouvez maintenant créer votre application Blazor WASM et y ajouter les packages NuGet suivants (en précisant la).

     <PackageReference Include="Microsoft.AspNetCore.Http" />
     <PackageReference Include="Microsoft.Authentication.WebAssembly.Msal" />
     <PackageReference Include="System.IdentityModel.Tokens.Jwt" />
    
  2. Adapter votre méthode Program.Main comm suit.

     builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
    
     // Authenticate using Azure MASL
     builder.Services.AddMsalAuthentication(options =>
     {
         builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
         options.ProviderOptions.DefaultAccessTokenScopes.Add("openid");
         options.ProviderOptions.DefaultAccessTokenScopes.Add("offline_access");
     });
    
     // Get Token from Teams
     await builder.Services.AddTeamsAuthenticationAsync();
    
  3. La méthode AddMsalAuthentication va chercher ses configuration dans le fichier wwwroot/appsettings.json

     {
       "AzureAd": {
         "Authority": "https://login.microsoftonline.com/common",
         "ClientId": "74f3d663-9438-4ea0-9c32-8cf1d000fe9d",         // Azure Application Client ID
         "ValidateAuthority": true
       }
     }
    
  4. API de redirection

    Lors de l’enregistrement de l’application dans Azure, nous avons définit l’adresse de redirection vers /authentication/login-callback. Nous devons ajouter ce composant Authentication.razor pour intercepter les appels Azure.

     /* Authentication.razor */
     @page "/authentication/{action}"
     @using Microsoft.AspNetCore.Components.WebAssembly.Authentication
     <RemoteAuthenticatorView Action="@Action" />
    
     @code{
         [Parameter] public string? Action { get; set; }
     }
    
  5. Page d’authentification

    Nous avons également besoins des pages de Login et de Redirection suivantes.

     /* LoginDisplay.razor */
     @using Microsoft.AspNetCore.Components.WebAssembly.Authentication
     @inject NavigationManager Navigation
    
     <AuthorizeView>
       <Authorized>
       </Authorized>
       <NotAuthorized>
           <a href="authentication/login">Click here to open the log in page.</a>
       </NotAuthorized>
     </AuthorizeView>
    
     @code {
       public void BeginLogOut()
       {
           Navigation.NavigateToLogout("authentication/logout");
       }
     }
    
     /* RedirectToLogin.razor */
     @using Microsoft.AspNetCore.Components.WebAssembly.Authentication
     @inject NavigationManager Navigation
    
     @code {
         protected override void OnInitialized()
         {
             Navigation.NavigateToLogin("authentication/login");
         }
     }
    
  6. La méthode AddTeamsAuthenticationAsync est disponible via cette classe d’extensions qui fait appel à la classe xxx qui suit.

    L’appel à la fonction JavaScript insideTeams demande l’ajout de cette fonction dans le fichier index.html. Les autres appels des fonctions microsoftTeams proviennt du script MicrosoftTeams.min.js référencé également dans index.html.

     <script src="https://res.cdn.office.net/teams-js/2.0.0/js/MicrosoftTeams.min.js"></script>
     <script>
         function insideTeams() { return window.parent !== window.self; }
     </script>
    
     using Microsoft.AspNetCore.Components.Authorization;
     using Microsoft.JSInterop;
     using System.IdentityModel.Tokens.Jwt;
    
     public static class AuthenticationStateAuthenticationFromTeamsExtensionsProviderFromTeams
     {
         public static async Task<IServiceCollection> AddTeamsAuthenticationAsync(this IServiceCollection services)
         {
             var jsRuntime = services.BuildServiceProvider().GetRequiredService<IJSRuntime>();
    
             // Is embedded in Teams App?
             var insideTeams = await jsRuntime.InvokeAsync<bool>("insideTeams");
    
             // Get Teams Token
             if (insideTeams)
             {
                 await jsRuntime.InvokeVoidAsync("microsoftTeams.app.initialize");
                 var token = await jsRuntime.InvokeAsync<string>("microsoftTeams.authentication.getAuthToken");
    
                 if (new JwtSecurityTokenHandler().CanReadToken(token))
                 {
                     services.AddScoped<AuthenticationStateProvider>(factory => new AuthenticationStateProviderFromTeams(token));
                 }
             }
    
             return services;
         }
    
     using System.IdentityModel.Tokens.Jwt;
     using System.Security.Claims;
     using System.Threading.Tasks;
     using Microsoft.AspNetCore.Components.Authorization;
    
     public class AuthenticationStateProviderFromTeams : AuthenticationStateProvider
     {
         private string _token = string.Empty;
         private bool _authenticated = false;
         private readonly ClaimsPrincipal Unauthenticated = new(new ClaimsIdentity());
    
         public AuthenticationStateProviderFromTeams(string token)
         {
             _token = token;
         }
    
         public override Task<AuthenticationState> GetAuthenticationStateAsync()
         {
             _authenticated = false;
             var user = Unauthenticated;
             var jwtToken = new JwtSecurityTokenHandler().ReadJwtToken(_token);
                    
             var id = new ClaimsIdentity(jwtToken.Claims, "MyAuthentication");
             user = new ClaimsPrincipal(id);
             _authenticated = true;
    
             return Task.FromResult(new AuthenticationState(user));
         }
     }
    
  7. Lors de la création du manifest Teams, nous avons définit la page wwwroot/config.html comme point d’entrée lors de l’enregistrement de l’application dans Teams.

     <!DOCTYPE html>
     <html>
     <head>
       <script src="https://res.cdn.office.net/teams-js/2.0.0/js/MicrosoftTeams.min.js"></script>
     </head>
     <body style="background: white; font-family: 'Segoe UI Variable','Segoe UI','sans-serif';">
       <h1>Configuration</h1>
    
       <div id="error" style="color: red;"></div>
       <div id="confirmation" style="display: none;">
         <p>Welcome to my new application.</p>
       </div>
    
       <script>
         microsoftTeams.app.initialize().then(async () => {
    
           try {
             // Authenticate the user
             const token = await microsoftTeams.authentication.getAuthToken();
    
             // Display the confirmation
             document.getElementById("confirmation").style.display = "block";
    
             // Activate the [Save] button
             microsoftTeams.pages.config.setValidityState(true);
    
             // Will be made when you click on [Save].
             microsoftTeams.pages.config.registerOnSaveHandler((saveEvent) => {
    
               microsoftTeams.pages.config.setConfig({
                 websiteUrl: window.location.origin,
                 contentUrl: window.location.origin + "/",
                 entityId: "MyNewApp",
                 suggestedDisplayName: "My new app"  // Tab label
               });
    
               saveEvent.notifySuccess();
             });
    
           }
    
           // Error?
           catch (error) {
             document.getElementById("error").textContent = error;
           }
    
         });
       </script>
     </body>
     </html>
    
  8. Nous pouvons maintenant sécuriser nos pages comme nous le souhaitons.

    Dans App.razor nous ajoutons CascadingAuthenticationState et AuthorizeRouteView comme suit.

     <CascadingAuthenticationState>
         <Router AppAssembly="@typeof(App).Assembly">
             <Found Context="routeData">
                 <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                     <NotAuthorized>
                         @if (context.User.Identity?.IsAuthenticated != true)
                         {
                             <RedirectToLogin />
                         }
                         else
                         {
                             <p role="alert">You are not authorized to access this resource.</p>
                         }
                     </NotAuthorized>
                 </AuthorizeRouteView>
                 <FocusOnNavigate RouteData="@routeData" Selector="h1" />
             </Found>
             <NotFound>
                 <PageTitle>Not found</PageTitle>
                 <LayoutView Layout="@typeof(MainLayout)">
                     <p role="alert">Sorry, there's nothing at this address.</p>
                 </LayoutView>
             </NotFound>
         </Router>
     </CascadingAuthenticationState>
    

    Dans Home.razor

     @page "/"
    
     <PageTitle>Holiday Harmony - Dashboard</PageTitle>
    
     <AuthorizeView>
         <Authorized>
           ...
        </Authorized>
     </AuthorizeView>