使用 Azure Active Directory B2C 在您自己的 Angular 應用程式中啟用驗證
本文顯示如何將 Azure Active Directory B2C (Azure AD B2C) 驗證新增至您自己的 Angular 單頁應用程式 (SPA)。 了解如何將 Angular 應用程式與適用於 Angular 的 MSAL 驗證程式庫整合。
請搭配使用本文與標題為在範例 Angular 單頁應用程式中設定驗證的相關文章。 將範例 Angular 應用程式替換成您自己的 Angular 應用程式。 當您完成本文中的步驟之後,您的應用程式將會透過 Azure AD B2C 接受登入。
必要條件
完成範例Angular單頁應用程式一文中設定驗證的步驟。
建立 Angular 應用程式專案
您可以使用現有的 Angular 應用程式專案,或建立新專案。 若要建立新專案,請執行下列命令。
命令:
- 使用 npm 套件管理員安裝 Angular CLI。
- 使用路由傳送模組建立 Angular 工作區。 應用程式名稱為
msal-angular-tutorial
。 您可以將它變更為任何有效的 Angular 應用程式名稱,例如contoso-car-service
。 - 變更為應用程式目錄資料夾。
npm install -g @angular/cli
ng new msal-angular-tutorial --routing=true --style=css --strict=false
cd msal-angular-tutorial
安裝相依性
若要在您的應用程式中安裝 MSAL 瀏覽器和 MSAL Angular 程式庫,請在您的命令殼層中執行下列命令:
npm install @azure/msal-browser @azure/msal-angular
安裝 Angular 材質元件庫 (選用,針對 UI):
npm install @angular/material @angular/cdk
新增驗證元件
範例程式碼由下列元件組成:
元件 | 類型 | Description |
---|---|---|
auth-config.ts | 常數 | 此設定檔包含 Azure AD B2C 身分識別提供者和 Web API 服務的相關資訊。 Angular 應用程式會使用此資訊來建立與 Azure AD B2C 的信任關係、將使用者登入和登出、取得權杖,以及驗證權杖。 |
app.module.ts | Angular 模組 | 此元件說明應用程式組件如何彼此配合。 這是用來啟動和開啟應用程式的根模組。 在此逐步解說中,您會將某些元件新增至 app.module.ts 模組,並使用 MSAL 設定物件啟動 MSAL 程式庫。 |
app-routing.module.ts | Angular 路由傳送模組 | 此元件會解譯瀏覽器 URL 並載入對應的元件來啟用導覽。 在此逐步解說中,您會將某些元件新增至路由傳送模組,並使用 MSAL Guard 來保護元件。 只有授權使用者才能存取受保護的元件。 |
app.component.* | Angular 元件 |
ng new 命令已建立具有根元件的 Angular 專案。 在此逐步解說中,您會將 app 元件變更為裝載頂端導覽列。 導覽列包含各種按鈕,包括登入和登出按鈕。
app.component.ts 類別會處理登入和登出事件。 |
home.component.* | Angular 元件 | 在此逐步解說中,您會新增 home 元件,以轉譯匿名存取的首頁。 此元件示範如何檢查使用者是否已登入。 |
profile.component.* | Angular 元件 | 在此逐步解說中,您會新增 profile 元件,以了解如何讀取識別碼權杖宣告。 |
webapi.component.* | Angular 元件 | 在此逐步解說中,您會新增 webapi 元件,以了解如何呼叫 Web API。 |
若要將下列元件新增至您的應用程式,請執行下列 Angular CLI 命令。
generate component
命令:
- 建立每個元件的資料夾。 此資料夾包含 TypeScript、HTML、CSS 和測試檔案。
- 使用新元件的參考來更新
app.module.ts
和app-routing.module.ts
檔案。
ng generate component home
ng generate component profile
ng generate component webapi
新增應用程式設定
Azure AD B2C 身分識別提供者和 Web API 的設定會儲存在 auth-config.ts 檔案中。 在 src/app 資料夾中,建立名為 auth-config.ts 的檔案,其中包含下列程式碼。 然後變更設定,如3.1 設定 Angular 範例中所述。
import { LogLevel, Configuration, BrowserCacheLocation } from '@azure/msal-browser';
const isIE = window.navigator.userAgent.indexOf("MSIE ") > -1 || window.navigator.userAgent.indexOf("Trident/") > -1;
export const b2cPolicies = {
names: {
signUpSignIn: "b2c_1_susi_reset_v2",
editProfile: "b2c_1_edit_profile_v2"
},
authorities: {
signUpSignIn: {
authority: "https://your-tenant-name.b2clogin.com/your-tenant-name.onmicrosoft.com/b2c_1_susi_reset_v2",
},
editProfile: {
authority: "https://your-tenant-name.b2clogin.com/your-tenant-name.onmicrosoft.com/b2c_1_edit_profile_v2"
}
},
authorityDomain: "your-tenant-name.b2clogin.com"
};
export const msalConfig: Configuration = {
auth: {
clientId: '<your-MyApp-application-ID>',
authority: b2cPolicies.authorities.signUpSignIn.authority,
knownAuthorities: [b2cPolicies.authorityDomain],
redirectUri: '/',
},
cache: {
cacheLocation: BrowserCacheLocation.LocalStorage,
storeAuthStateInCookie: isIE,
},
system: {
loggerOptions: {
loggerCallback: (logLevel, message, containsPii) => {
console.log(message);
},
logLevel: LogLevel.Verbose,
piiLoggingEnabled: false
}
}
}
export const protectedResources = {
todoListApi: {
endpoint: "http://localhost:5000/api/todolist",
scopes: ["https://your-tenant-name.onmicrosoft.com/api/tasks.read"],
},
}
export const loginRequest = {
scopes: []
};
啟動驗證程式庫
公用用戶端應用程式不受信任,無法安全地保留應用程式秘密,因此它們沒有用戶端密碼。 在 src/app 資料夾中,開啟 app.module.ts,並進行下列變更:
- 匯入 MSAL Angular 和 MSAL 瀏覽器程式庫。
- 匯入 Azure AD B2C 設定模組。
- 匯入
HttpClientModule
。 HTTP 用戶端用來呼叫 Web API。 - 匯入 Angular HTTP 攔截器。 MSAL 會使用攔截器將持有人權杖插入 HTTP 授權標頭。
- 新增必要的 Angular 材質。
- 使用多個帳戶公用用戶端應用程式物件來具現化 MSAL。 MSAL 初始化包括傳遞:
- auth-config.ts 的設定物件。
- 路由傳送防護的設定物件。
- MSAL 攔截器的設定物件。 攔截器類別會自動取得傳出要求的權杖,而此權杖使用已知受保護資源的 Angular HttpClient 類別。
- 設定
HTTP_INTERCEPTORS
和MsalGuard
Angular 提供者。 - 將
MsalRedirectComponent
新增至 Angular 啟動程序。
在 src/app 資料夾中,編輯 app.module.ts,並進行下列程式碼片段中所示的修改。 這些變更會標示為「從這裡開始變更」和「在這裡結束變更」。
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
/* Changes start here. */
// Import MSAL and MSAL browser libraries.
import { MsalGuard, MsalInterceptor, MsalModule, MsalRedirectComponent } from '@azure/msal-angular';
import { InteractionType, PublicClientApplication } from '@azure/msal-browser';
// Import the Azure AD B2C configuration
import { msalConfig, protectedResources } from './auth-config';
// Import the Angular HTTP interceptor.
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { ProfileComponent } from './profile/profile.component';
import { HomeComponent } from './home/home.component';
import { WebapiComponent } from './webapi/webapi.component';
// Add the essential Angular materials.
import { MatButtonModule } from '@angular/material/button';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatListModule } from '@angular/material/list';
import { MatTableModule } from '@angular/material/table';
/* Changes end here. */
@NgModule({
declarations: [
AppComponent,
ProfileComponent,
HomeComponent,
WebapiComponent
],
imports: [
BrowserModule,
AppRoutingModule,
/* Changes start here. */
// Import the following Angular materials.
MatButtonModule,
MatToolbarModule,
MatListModule,
MatTableModule,
// Import the HTTP client.
HttpClientModule,
// Initiate the MSAL library with the MSAL configuration object
MsalModule.forRoot(new PublicClientApplication(msalConfig),
{
// The routing guard configuration.
interactionType: InteractionType.Redirect,
authRequest: {
scopes: protectedResources.todoListApi.scopes
}
},
{
// MSAL interceptor configuration.
// The protected resource mapping maps your web API with the corresponding app scopes. If your code needs to call another web API, add the URI mapping here.
interactionType: InteractionType.Redirect,
protectedResourceMap: new Map([
[protectedResources.todoListApi.endpoint, protectedResources.todoListApi.scopes]
])
})
/* Changes end here. */
],
providers: [
/* Changes start here. */
{
provide: HTTP_INTERCEPTORS,
useClass: MsalInterceptor,
multi: true
},
MsalGuard
/* Changes end here. */
],
bootstrap: [
AppComponent,
/* Changes start here. */
MsalRedirectComponent
/* Changes end here. */
]
})
export class AppModule { }
設定路由
在本節中,設定 Angular 應用程式的路由。 使用者選取頁面上的連結以在單頁應用程式內移動時,或在網址列中輸入 URL 時,路由會將 URL 對應至 Angular 元件。 Angular 路由傳送 canActivate 介面會使用 MSAL Guard 來檢查使用者是否已登入。 如果使用者未登入,則 MSAL 會將使用者帶到 Azure AD B2C 以進行驗證。
在 src/app 資料夾中,編輯 app-routing.module.ts,並進行下列程式碼片段中所示的修改。 這些變更會標示為「從這裡開始變更」和「在這裡結束變更」。
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { MsalGuard } from '@azure/msal-angular';
import { HomeComponent } from './home/home.component';
import { ProfileComponent } from './profile/profile.component';
import { WebapiComponent } from './webapi/webapi.component';
const routes: Routes = [
/* Changes start here. */
{
path: 'profile',
component: ProfileComponent,
// The profile component is protected with MSAL Guard.
canActivate: [MsalGuard]
},
{
path: 'webapi',
component: WebapiComponent,
// The profile component is protected with MSAL Guard.
canActivate: [MsalGuard]
},
{
// The home component allows anonymous access
path: '',
component: HomeComponent
}
/* Changes end here. */
];
@NgModule({
/* Changes start here. */
// Replace the following line with the next one
//imports: [RouterModule.forRoot(routes)],
imports: [RouterModule.forRoot(routes, {
initialNavigation:'enabled'
})],
/* Changes end here. */
exports: [RouterModule]
})
export class AppRoutingModule { }
新增登入和登出按鈕
在本節中,您會將登入和登出按鈕新增至 app 元件。 在 src/app 資料夾中,開啟 app.component.ts 檔案,並進行下列變更:
匯入必要元件。
變更類別以實作 OnInit 方法。
OnInit
方法會訂閱 MSAL MsalBroadcastServiceinProgress$
可觀察事件。 使用此事件可知道使用者互動的狀態,特別是檢查是否已完成互動。在與 MSAL account 物件互動之前,請先檢查
InteractionStatus
屬性是否傳回InteractionStatus.None
。subscribe
事件會呼叫setLoginDisplay
方法來檢查使用者是否通過驗證。新增類別變數。
新增可啟動授權流程的
login
方法。新增可登出使用者的
logout
方法。新增可檢查使用者是否通過驗證的
setLoginDisplay
方法。新增 ngOnDestroy 方法以清除
inProgress$
訂閱事件。
變更之後,您的程式碼看起來應該如下列程式碼片段所示:
import { Component, OnInit, Inject } from '@angular/core';
import { MsalService, MsalBroadcastService, MSAL_GUARD_CONFIG, MsalGuardConfiguration } from '@azure/msal-angular';
import { InteractionStatus, RedirectRequest } from '@azure/msal-browser';
import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
/* Changes start here. */
export class AppComponent implements OnInit{
title = 'msal-angular-tutorial';
loginDisplay = false;
private readonly _destroying$ = new Subject<void>();
constructor(@Inject(MSAL_GUARD_CONFIG) private msalGuardConfig: MsalGuardConfiguration, private broadcastService: MsalBroadcastService, private authService: MsalService) { }
ngOnInit() {
this.broadcastService.inProgress$
.pipe(
filter((status: InteractionStatus) => status === InteractionStatus.None),
takeUntil(this._destroying$)
)
.subscribe(() => {
this.setLoginDisplay();
})
}
login() {
if (this.msalGuardConfig.authRequest){
this.authService.loginRedirect({...this.msalGuardConfig.authRequest} as RedirectRequest);
} else {
this.authService.loginRedirect();
}
}
logout() {
this.authService.logoutRedirect({
postLogoutRedirectUri: 'http://localhost:4200'
});
}
setLoginDisplay() {
this.loginDisplay = this.authService.instance.getAllAccounts().length > 0;
}
ngOnDestroy(): void {
this._destroying$.next(undefined);
this._destroying$.complete();
}
/* Changes end here. */
}
在 src/app 資料夾中,編輯 app.component.html,並進行下列變更:
- 新增設定檔和 Web API 元件的連結。
- 新增 click 事件屬性設定為
login()
方法的登入按鈕。 只有在loginDisplay
類別變數為false
時,才會顯示此按鈕。 - 新增 click 事件屬性設定為
logout()
方法的登出按鈕。 只有在loginDisplay
類別變數為true
時,才會顯示此按鈕。 - 新增 router-outlet 元素。
變更之後,您的程式碼看起來應該如下列程式碼片段所示:
<mat-toolbar color="primary">
<a class="title" href="/">{{ title }}</a>
<div class="toolbar-spacer"></div>
<a mat-button [routerLink]="['profile']">Profile</a>
<a mat-button [routerLink]="['webapi']">Web API</a>
<button mat-raised-button *ngIf="!loginDisplay" (click)="login()">Login</button>
<button mat-raised-button *ngIf="loginDisplay" (click)="logout()">Logout</button>
</mat-toolbar>
<div class="container">
<router-outlet></router-outlet>
</div>
選擇性地使用下列 CSS 程式碼片段來更新 app.component.css 檔案:
.toolbar-spacer {
flex: 1 1 auto;
}
a.title {
color: white;
}
處理應用程式重新導向
當您搭配使用重新導向與 MSAL 時,必須將 app-redirect 指示詞新增至 index.html。 在 src 資料夾中,編輯 index.html,如下列程式碼片段所示:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>MsalAngularTutorial</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>
<!-- Changes start here -->
<app-redirect></app-redirect>
<!-- Changes end here -->
</body>
</html>
設定應用程式 CSS (選用)
在 /src 資料夾中,使用下列 CSS 程式碼片段來更新 styles.css 檔案:
@import '~@angular/material/prebuilt-themes/deeppurple-amber.css';
html, body { height: 100%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
.container { margin: 1%; }
提示
此時,您可以執行應用程式,並測試登入體驗。 若要執行您的應用程式,請參閱執行 Angular 應用程式小節。
檢查使用者是否通過驗證
home.component 檔案會示範如何檢查使用者是否通過驗證。 在 src/app/home 資料夾中,使用下列程式碼片段來更新 home.component.ts。
程式碼:
- 訂閱 MSAL MsalBroadcastService
msalSubject$
和inProgress$
可觀察事件。 - 確保事件會
msalSubject$
將驗證結果寫入至瀏覽器主控台。 - 確保
inProgress$
事件檢查使用者是否通過驗證。getAllAccounts()
方法會傳回一或多個物件。
import { Component, OnInit } from '@angular/core';
import { MsalBroadcastService, MsalService } from '@azure/msal-angular';
import { EventMessage, EventType, InteractionStatus } from '@azure/msal-browser';
import { filter } from 'rxjs/operators';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
loginDisplay = false;
constructor(private authService: MsalService, private msalBroadcastService: MsalBroadcastService) { }
ngOnInit(): void {
this.msalBroadcastService.msalSubject$
.pipe(
filter((msg: EventMessage) => msg.eventType === EventType.LOGIN_SUCCESS),
)
.subscribe((result: EventMessage) => {
console.log(result);
});
this.msalBroadcastService.inProgress$
.pipe(
filter((status: InteractionStatus) => status === InteractionStatus.None)
)
.subscribe(() => {
this.setLoginDisplay();
})
}
setLoginDisplay() {
this.loginDisplay = this.authService.instance.getAllAccounts().length > 0;
}
}
在 src/app/home 資料夾中,使用下列 HTML 程式碼片段來更新 home.component.html。
*ngIf 指示詞會檢查 loginDisplay
類別變數,以顯示或隱藏歡迎使用訊息。
<div *ngIf="!loginDisplay">
<p>Please sign-in to see your profile information.</p>
</div>
<div *ngIf="loginDisplay">
<p>Login successful!</p>
<p>Request your profile information by clicking Profile above.</p>
</div>
讀取識別碼權杖宣告
profile.component 檔案會示範如何存取使用者的識別碼權杖宣告。 在 src/app/profile 資料夾中,使用下列程式碼片段來更新 profile.component.ts。
程式碼:
- 匯入必要元件。
- 訂閱 MSAL MsalBroadcastService
inProgress$
可觀察事件。 此事件會載入帳戶,並讀取識別碼權杖宣告。 - 確保
checkAndSetActiveAccount
方法會檢查並設定使用中帳戶。 應用程式與多個 Azure AD B2C 使用者流程或自訂原則互動時,此動作十分常見。 - 確保
getClaims
方法會從使用中 MSAL account 物件取得識別碼權杖宣告。 此方法接著會將宣告新增至dataSource
陣列。 使用元件的範本繫結向使用者轉譯陣列。
import { Component, OnInit } from '@angular/core';
import { MsalBroadcastService, MsalService } from '@azure/msal-angular';
import { EventMessage, EventType, InteractionStatus } from '@azure/msal-browser';
import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
@Component({
selector: 'app-profile',
templateUrl: './profile.component.html',
styleUrls: ['./profile.component.css']
})
export class ProfileComponent implements OnInit {
displayedColumns: string[] = ['claim', 'value'];
dataSource: Claim[] = [];
private readonly _destroying$ = new Subject<void>();
constructor(private authService: MsalService, private msalBroadcastService: MsalBroadcastService) { }
ngOnInit(): void {
this.msalBroadcastService.inProgress$
.pipe(
filter((status: InteractionStatus) => status === InteractionStatus.None || status === InteractionStatus.HandleRedirect),
takeUntil(this._destroying$)
)
.subscribe(() => {
this.checkAndSetActiveAccount();
this.getClaims(this.authService.instance.getActiveAccount()?.idTokenClaims)
})
}
checkAndSetActiveAccount() {
let activeAccount = this.authService.instance.getActiveAccount();
if (!activeAccount && this.authService.instance.getAllAccounts().length > 0) {
let accounts = this.authService.instance.getAllAccounts();
this.authService.instance.setActiveAccount(accounts[0]);
}
}
getClaims(claims: any) {
let list: Claim[] = new Array<Claim>();
Object.keys(claims).forEach(function(k, v){
let c = new Claim()
c.id = v;
c.claim = k;
c.value = claims ? claims[k]: null;
list.push(c);
});
this.dataSource = list;
}
ngOnDestroy(): void {
this._destroying$.next(undefined);
this._destroying$.complete();
}
}
export class Claim {
id: number;
claim: string;
value: string;
}
在 src/app/profile 資料夾中,使用下列 HTML 程式碼片段來更新 profile.component.html:
<h1>ID token claims:</h1>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<!-- 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>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
呼叫 Web API
若要呼叫權杖型授權 Web API,應用程式必須具備有效的存取權杖。 MsalInterceptor 提供者會自動取得傳出要求的權杖,而此權杖使用已知受保護資源的 Angular HttpClient 類別。
重要
MSAL 初始化方法 (在 app.module.ts
類別中) 會使用 protectedResourceMap
物件,以將受保護資源 (例如 Web API) 對應至必要應用程式範圍。 如果您的程式碼需要呼叫另一個 Web API,則請將 Web API URI 和 Web API HTTP 方法 (具有對應的範圍) 新增至 protectedResourceMap
物件。 如需詳細資訊,請參閱 受保護資源對應。
HttpClient 物件呼叫 Web API 時,MsalInterceptor 提供者會採取下列步驟:
使用 Web API 端點的必要權限 (範圍) 取得存取權杖。
使用下列格式,在 HTTP 要求的授權標頭中,以持有人權杖的形式傳遞存取權杖:
Authorization: Bearer <access-token>
webapi.component 檔案會示範如何呼叫 Web API。 在 src/app/webapi 資料夾中,使用下列程式碼片段來更新 webapi.component.ts。
程式碼:
- 使用 Angular HttpClient 類別來呼叫 Web API。
- 讀取
auth-config
類別的protectedResources.todoListApi.endpoint
元素。 此元素會指定 Web API URI。 MSAL 攔截器會根據 Web API URI 來取得具有對應範圍的存取權杖。 - 從 Web API 取得設定檔,並設定
profile
類別變數。
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { protectedResources } from '../auth-config';
type ProfileType = {
name?: string
};
@Component({
selector: 'app-webapi',
templateUrl: './webapi.component.html',
styleUrls: ['./webapi.component.css']
})
export class WebapiComponent implements OnInit {
todoListEndpoint: string = protectedResources.todoListApi.endpoint;
profile!: ProfileType;
constructor(
private http: HttpClient
) { }
ngOnInit() {
this.getProfile();
}
getProfile() {
this.http.get(this.todoListEndpoint)
.subscribe(profile => {
this.profile = profile;
});
}
}
在 src/app/webapi 資料夾中,使用下列 HTML 程式碼片段來更新 webapi.component.html。 元件的範本會轉譯 Web API 所傳回的名稱。 在頁面底部,範本會轉譯 Web API 位址。
<h1>The web API returns:</h1>
<div>
<p><strong>Name: </strong> {{profile?.name}}</p>
</div>
<div class="footer-text">
Web API: {{todoListEndpoint}}
</div>
選擇性地使用下列 CSS 程式碼片段來更新 webapi.component.css 檔案:
.footer-text {
position: absolute;
bottom: 50px;
color: gray;
}
執行 Angular 應用程式
執行以下命令:
npm start
主控台視窗會顯示裝載應用程式的連接埠號碼。
Listening on port 4200...
提示
或者,若要執行 npm start
命令,請使用 Visual Studio Code 偵錯工具。 偵錯工具有助於加速您的編輯、編譯和偵錯迴圈。
在瀏覽器中移至 http://localhost:4200
可檢視應用程式。