Freigeben über


Tutorial: Vorbereiten einer Angular-SPA für die Authentifizierung in einem externen Mandanten

Dieses Tutorial ist Teil 2 einer Reihe, in der das Erstellen einer Single-Page-Webanwendung (Single-Page Application, SPA) in Angular (Schützen des privilegierten Zugriffs) und ihre Vorbereitung für die Authentifizierung mithilfe des Microsoft Entra Admin Centers veranschaulicht wird. In Teil 1 dieser Reihe haben Sie eine Anwendung registriert und Benutzerflows in Ihrem externen Mandanten konfiguriert. In diesem Tutorial wird veranschaulicht, wie Sie mithilfe von npm eine Angular-SPA erstellen. Zudem wird das Erstellen von Dateien veranschaulicht, die für die Authentifizierung und Autorisierung erforderlich sind.

In diesem Tutorial:

  • Erstellen eines Angular-Projekts in Visual Studio Code
  • Konfigurieren der Benutzeroberfläche für die Anwendung
  • Konfigurieren der Startseiten- und geschützten Komponenten

Voraussetzungen

Erstellen eines Angular-Projekts

In diesem Abschnitt erstellen wir ein neues Angular-Projekt mit der Angular CLI in Visual Studio Code.

  1. Öffnen Sie Visual Studio Code und wählen Sie Datei>Ordner öffnen …. Navigieren Sie zu dem Speicherort, an dem Sie Ihr Projekt erstellen möchten, und wählen Sie ihn aus.

  2. Öffnen Sie ein neues Terminal, indem Sie Terminal>Neues Terminal auswählen.

  3. Führen Sie die folgenden Befehle aus, um ein neues Angular-Projekt mit dem Namen angularspalocal zu erstellen, Angular Material-Komponentenbibliotheken, MSAL Browser und MSAL Angular zu installieren und Startseiten- und geschützte Komponenten zu generieren.

    npm install -g @angular/cli@14.2.0
    ng new angularspalocal --routing=true --style=css --strict=false
    cd angularspalocal
    npm install @angular/material@13.0.0 @angular/cdk@13.0.0
    npm install @azure/msal-browser@2.37.0 @azure/msal-angular@2.5.7
    ng generate component home
    ng generate component guarded
    

Konfigurieren von UI-Elementen

Die folgenden Schritte konfigurieren die Benutzeroberflächenelemente der Anwendung. CSS-Formatierung wird der Anwendung hinzugefügt, um die Farben und Schriftarten zu definieren. Header und Footer der Anwendung werden in der HTML-Datei definiert, und die CSS-Formatierung wird der Startseite der Anwendung hinzugefügt.

  1. Öffnen Sie die Datei src/styles.css, und ersetzen Sie den vorhandenen Codeblock durch den folgenden Codeschnipsel.

    @import '~@angular/material/prebuilt-themes/deeppurple-amber.css';
    html, body { height: 100%; }
    body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
    
  2. Öffnen Sie src/app/app.component.html, und ersetzen Sie den vorhandenen Code durch den folgenden Codeschnipsel.

      <mat-toolbar color="primary">
          <a class="title" href="/">{{ title }}</a>
          <div class="toolbar-spacer"></div>
          <a mat-button [routerLink]="['guarded']">Guarded Component</a>
          <button mat-raised-button *ngIf="!loginDisplay" (click)="login()">Login</button>
          <button mat-raised-button color="accent" *ngIf="loginDisplay" (click)="logout()">Logout</button>
        </mat-toolbar>
        <div class="container">
          <!--This is to avoid reload during acquireTokenSilent() because of hidden iframe -->
          <router-outlet *ngIf="!isIframe"></router-outlet>
        </div>
        <footer *ngIf="loginDisplay">
          <mat-toolbar>
            <div class="footer-text"> How did we do? <a
                href="https://forms.office.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR_ivMYEeUKlEq8CxnMPgdNZUNDlUTTk2NVNYQkZSSjdaTk5KT1o4V1VVNS4u"
                target="_blank"> Share your experience with us!</a>
            </div>
          </mat-toolbar>
        </footer>
    
  3. Öffnen Sie src/app/app.component.css, und ersetzen Sie den vorhandenen Code durch den folgenden Codeschnipsel.

    .toolbar-spacer {
      flex: 1 1 auto;
    }
    
    a.title {
      color: white;
    }
    
    footer {
      position: fixed;
      left: 0;
      bottom: 0;
      width: 100%;
      color: white;
      text-align: center;
    }
    
    .footer-text {
      font-size: small;
      text-align: center;
      flex: 1 1 auto;
    }
    

Konfigurieren von Anwendungskomponenten

In diesem Abschnitt konfigurieren Sie die Startseiten- und geschützten Komponenten der Anwendung. Die Startseitenkomponente ist die Landing Page der Anwendung, und die geschützte Komponente ist die Seite, auf die nur authentifizierte Benutzer*innen zugreifen können.

  1. Öffnen Sie src/app/home/home.component.ts, und ersetzen Sie den vorhandenen Code durch den folgenden Codeschnipsel.

    import { Component, Inject, OnInit } from '@angular/core';
    import { Subject } from 'rxjs';
    import { filter } from 'rxjs/operators';
    
    import { MsalBroadcastService, MsalGuardConfiguration, MsalService, MSAL_GUARD_CONFIG } from '@azure/msal-angular';
    import { AuthenticationResult, InteractionStatus, InteractionType } from '@azure/msal-browser';
    
    import { createClaimsTable } from '../claim-utils';
    
    @Component({
      selector: 'app-home',
      templateUrl: './home.component.html',
      styleUrls: ['./home.component.css'],
    })
    export class HomeComponent implements OnInit {
      loginDisplay = false;
      dataSource: any = [];
      displayedColumns: string[] = ['claim', 'value', 'description'];
    
      private readonly _destroying$ = new Subject<void>();
    
      constructor(
        @Inject(MSAL_GUARD_CONFIG)
        private msalGuardConfig: MsalGuardConfiguration,
        private authService: MsalService,
        private msalBroadcastService: MsalBroadcastService
      ) { }
    
      ngOnInit(): void {
    
        this.msalBroadcastService.inProgress$
          .pipe(
            filter((status: InteractionStatus) => status === InteractionStatus.None)
          )
          .subscribe(() => {
            this.setLoginDisplay();
            this.getClaims(
              this.authService.instance.getActiveAccount()?.idTokenClaims
            );
          });
      }
    
      setLoginDisplay() {
        this.loginDisplay = this.authService.instance.getAllAccounts().length > 0;
      }
    
      getClaims(claims: any) {
        if (claims) {
          const claimsTable = createClaimsTable(claims);
          this.dataSource = [...claimsTable];
        }
      }
    
      signUp() {
        if (this.msalGuardConfig.interactionType === InteractionType.Popup) {
          this.authService.loginPopup({
            scopes: [],
            prompt: 'create',
          })
            .subscribe((response: AuthenticationResult) => {
              this.authService.instance.setActiveAccount(response.account);
            });
        } else {
          this.authService.loginRedirect({
            scopes: [],
            prompt: 'create',
          });
        }
    
      }
    
      // unsubscribe to events when component is destroyed
      ngOnDestroy(): void {
        this._destroying$.next(undefined);
        this._destroying$.complete();
      }
    }
    
  2. Öffnen Sie src/app/home/home.component.html, und ersetzen Sie den vorhandenen Code durch den folgenden Codeschnipsel. Dieser Code definiert die HTML-Elemente der Startseite der Anwendung.

    <mat-card class="card-section" *ngIf="!loginDisplay">
      <mat-card-title>Angular single-page application built with MSAL Angular</mat-card-title>
      <mat-card-subtitle>Sign in with Microsoft Entra External ID</mat-card-subtitle>
      <mat-card-content>This sample demonstrates how to configure MSAL Angular to sign up, sign in and sign out with Microsoft Entra External ID</mat-card-content>
      <button mat-raised-button color="primary" (click)="signUp()">Sign up</button>
    </mat-card>
    <br>
    <p class="text-center" *ngIf="loginDisplay"> See below the claims in your <strong> ID token </strong>. For more
      information, visit: <span>
        <a href="https://docs.microsoft.com/en-us/azure/active-directory/develop/id-tokens#claims-in-an-id-token">
          docs.microsoft.com </a>
      </span>
    </p>
    <div id="table-container">
      <table mat-table [dataSource]="dataSource" class="mat-elevation-z8" *ngIf="loginDisplay">
        <!-- Claim Column -->
        <ng-container matColumnDef="claim">
          <th mat-header-cell *matHeaderCellDef> Claim </th>
          <td mat-cell *matCellDef="let element"> {{element.claim}} </td>
        </ng-container>
        <!-- Value Column -->
        <ng-container matColumnDef="value">
          <th mat-header-cell *matHeaderCellDef> Value </th>
          <td mat-cell *matCellDef="let element"> {{element.value}} </td>
        </ng-container>
        <!-- Value Column -->
        <ng-container matColumnDef="description">
          <th mat-header-cell *matHeaderCellDef> Description </th>
          <td mat-cell *matCellDef="let element"> {{element.description}} </td>
        </ng-container>
        <tr mat-header-row *matHeaderRowDef="displayedColumns sticky: true"></tr>
        <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
      </table>
    </div>
    
  3. Öffnen Sie src/app/home/home.component.css. Ersetzen Sie den vorhandenen Code durch den folgenden Codeschnipsel.

    #table-container {
      height: '100vh';
      overflow: auto;
    }
    
    table {
      margin: 3% auto 1% auto;
      width: 70%;
    }
    
    .mat-row {
      height: auto;
    }
    
    .mat-cell {
      padding: 8px 8px 8px 0;
    }
    
    p {
      text-align: center;
    }
    
    .card-section {
      margin: 10%;
      padding: 5%;
    }
    
  4. Öffnen Sie src/app/guarded/guarded.component.ts, und ersetzen Sie den vorhandenen Code durch den folgenden Codeschnipsel.

    import { Component, OnInit } from '@angular/core';
    
    @Component({
      selector: 'app-guarded',
      templateUrl: './guarded.component.html',
      styleUrls: ['./guarded.component.css']
    })
    export class GuardedComponent implements OnInit {
    
      constructor() { }
    
      ngOnInit(): void {
      }
    
    }
    
  5. Erstellen Sie eine Datei mit dem Namen claim-utils.ts im Ordner src/app/, und fügen Sie den folgenden Codeschnipsel ein.

    /**
     * Populate claims table with appropriate description
     * @param {Record} claims ID token claims
     * @returns claimsTable
     */
    export const createClaimsTable = (claims: Record<string, string>): any[] => {
      const claimsTable: any[] = [];
    
      Object.keys(claims).map((key) => {
        switch (key) {
          case 'aud':
            populateClaim(
              key,
              claims[key],
              "Identifies the intended recipient of the token. In ID tokens, the audience is your app's Application ID, assigned to your app in the Azure portal.",
              claimsTable
            );
            break;
          case 'iss':
            populateClaim(
              key,
              claims[key],
              'Identifies the issuer, or authorization server that constructs and returns the token. It also identifies the Azure AD tenant for which the user was authenticated. If the token was issued by the v2.0 endpoint, the URI will end in /v2.0.',
              claimsTable
            );
            break;
          case 'iat':
            populateClaim(
              key,
              changeDateFormat(+claims[key]),
              '"Issued At" indicates the timestamp (UNIX timestamp) when the authentication for this user occurred.',
              claimsTable
            );
            break;
          case 'nbf':
            populateClaim(
              key,
              changeDateFormat(+claims[key]),
              'The nbf (not before) claim dictates the time (as UNIX timestamp) before which the JWT must not be accepted for processing.',
              claimsTable
            );
            break;
          case 'exp':
            populateClaim(
              key,
              changeDateFormat(+claims[key]),
              "The exp (expiration time) claim dictates the expiration time (as UNIX timestamp) on or after which the JWT must not be accepted for processing. It's important to note that in certain circumstances, a resource may reject the token before this time. For example, if a change in authentication is required or a token revocation has been detected.",
              claimsTable
            );
            break;
          case 'name':
            populateClaim(
              key,
              claims[key],
              "The name claim provides a human-readable value that identifies the subject of the token. The value isn't guaranteed to be unique, it can be changed, and it's designed to be used only for display purposes. The 'profile' scope is required to receive this claim.",
              claimsTable
            );
            break;
          case 'preferred_username':
            populateClaim(
              key,
              claims[key],
              'The primary username that represents the user. It could be an email address, phone number, or a generic username without a specified format. Its value is mutable and might change over time. Since it is mutable, this value must not be used to make authorization decisions. It can be used for username hints, however, and in human-readable UI as a username. The profile scope is required in order to receive this claim.',
              claimsTable
            );
            break;
          case 'nonce':
            populateClaim(
              key,
              claims[key],
              'The nonce matches the parameter included in the original /authorize request to the IDP.',
              claimsTable
            );
            break;
          case 'oid':
            populateClaim(
              key,
              claims[key],
              'The oid (user object id) is the only claim that should be used to uniquely identify a user in an Azure AD tenant.',
              claimsTable
            );
            break;
          case 'tid':
            populateClaim(
              key,
              claims[key],
              'The id of the tenant where this application resides. You can use this claim to ensure that only users from the current Azure AD tenant can access this app.',
              claimsTable
            );
            break;
          case 'upn':
            populateClaim(
              key,
              claims[key],
              'upn (user principal name) might be unique amongst the active set of users in a tenant but tend to get reassigned to new employees as employees leave the organization and others take their place or might change to reflect a personal change like marriage.',
              claimsTable
            );
            break;
          case 'email':
            populateClaim(
              key,
              claims[key],
              'Email might be unique amongst the active set of users in a tenant but tend to get reassigned to new employees as employees leave the organization and others take their place.',
              claimsTable
            );
            break;
          case 'acct':
            populateClaim(
              key,
              claims[key],
              'Available as an optional claim, it lets you know what the type of user (homed, guest) is. For example, for an individual's access to their data you might not care for this claim, but you would use this along with tenant id (tid) to control access to say a company-wide dashboard to just employees (homed users) and not contractors (guest users).',
              claimsTable
            );
            break;
          case 'sid':
            populateClaim(
              key,
              claims[key],
              'Session ID, used for per-session user sign-out.',
              claimsTable
            );
            break;
          case 'sub':
            populateClaim(
              key,
              claims[key],
              'The sub claim is a pairwise identifier - it is unique to a particular application ID. If a single user signs into two different apps using two different client IDs, those apps will receive two different values for the subject claim.',
              claimsTable
            );
            break;
          case 'ver':
            populateClaim(
              key,
              claims[key],
              'Version of the token issued by the Microsoft identity platform',
              claimsTable
            );
            break;
          case 'login_hint':
            populateClaim(
              key,
              claims[key],
              'An opaque, reliable login hint claim. This claim is the best value to use for the login_hint OAuth parameter in all flows to get SSO.',
              claimsTable
            );
            break;
          case 'idtyp':
            populateClaim(
              key,
              claims[key],
              'Value is app when the token is an app-only token. This is the most accurate way for an API to determine if a token is an app token or an app+user token',
              claimsTable
            );
            break;
          case 'uti':
          case 'rh':
            break;
          default:
            populateClaim(key, claims[key], '', claimsTable);
        }
      });
    
      return claimsTable;
    };
    
    /**
     * Populates claim, description, and value into an claimsObject
     * @param {String} claim
     * @param {String} value
     * @param {String} description
     * @param {Array} claimsObject
     */
    const populateClaim = (
      claim: string,
      value: string,
      description: string,
      claimsTable: any[]
    ): void => {
      claimsTable.push({
        claim: claim,
        value: value,
        description: description,
      });
    };
    
    /**
     * Transforms Unix timestamp to date and returns a string value of that date
     * @param {number} date Unix timestamp
     * @returns
     */
    const changeDateFormat = (date: number) => {
      let dateObj = new Date(date * 1000);
      return `${date} - [${dateObj.toString()}]`;
    };
    
  6. Öffnen Sie src/index.html, und ersetzen Sie den Code durch den folgenden Codeschnipsel.

    <!doctype html>
    <html lang="en">
    <head>
      <meta charset="utf-8">
      <title>Microsoft identity platform</title>
      <base href="/">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <link rel="icon" type="image/x-icon" href="favicon.svg">
      <link rel="preconnect" href="https://fonts.gstatic.com">
      <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
      <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
    </head>
    <body class="mat-typography">
      <app-root></app-root>
      <app-redirect></app-redirect>
    </body>
    </html>
    

Nächster Schritt