演習 - ユーザー認証を追加する
あなたのショッピング リスト Web アプリにはユーザー認証が必要です。 この演習では、アプリにログインとログアウトを実装し、現在のユーザーのログイン状態を表示します。
この演習では、次の手順を行います。
- ローカル開発用の Static Web Apps CLI をインストールします。
- ローカル認証エミュレーションを使用して、ローカルでアプリと API を実行します。
- 複数の認証プロバイダー用のログイン ボタンを追加します。
- ユーザーがログインしている場合のログアウト ボタンを追加します。
- ユーザーのログイン状態を表示します。
- ローカルで認証ワークフローをテストします。
- 更新されたアプリをデプロイします。
ローカル開発の準備をする
Static Web Apps CLI (SWA CLI とも呼ばれます) はローカル開発ツールです。このツールを使用すると、Web アプリと API をローカルで実行し、認証と承認サーバーをエミュレートすることができます。
コンピューターでターミナルを開きます。
次のコマンドを実行して SWA CLI をインストールします。
npm install -g @azure/static-web-apps-cli
アプリをローカルで実行する
ここでは、開発サーバーを使用して、ローカルでアプリと API を実行します。 この方法では、コード内で変更を行うため、自分の変更を確認してテストすることができます。
Visual Studio Code でプロジェクトを開きます。
Visual Studio Code で、F1 キーを押してコマンド パレットを開きます。
「Terminal: Create New Integrated Terminal」と入力して選択します。
以下のように、任意のフロントエンド フレームワークのフォルダーに移動します。
cd angular-app
cd react-app
cd svelte-app
cd vue-app
開発サーバーを使用して、フロントエンド クライアント アプリケーションを実行します。
npm start
npm start
npm run dev
npm run serve
このサーバーはバックグラウンドで実行したままにします。 ここでは、SWA CLI を使用して、API と認証サーバー エミュレーターを実行します。
Visual Studio Code で、F1 キーを押してコマンド パレットを開きます。
「Terminal: Create New Integrated Terminal」と入力して選択します。
次のコマンドを実行して SWA CLI を実行します。
swa start http://localhost:4200 --api-location ./api
swa start http://localhost:3000 --api-location ./api
swa start http://localhost:5000 --api-location ./api
swa start http://localhost:8080 --api-location ./api
http://localhost:4280
を参照します。
SWA CLI によって使用される最終的なポートは、前に示したものとは異なります。これはリバース プロキシを使用して、3 つの異なるコンポーネントに要求を転送するためです。
- ご自身のフレームワーク開発サーバー
- 認証および承認エミュレーター
- Functions ランタイムによってホストされている API
コードを変更する間は、アプリケーションを実行したままにしておきます。
ユーザーのログイン状態を取得する
まず、クライアント内で /.auth/me
に対してクエリを実行して、ユーザーのログイン状態にアクセスする必要があります。
angular-app/src/app/core/models/user-info.ts
ファイルを作成し、ユーザー情報のインターフェイスを表す次のコードを追加します。export interface UserInfo { identityProvider: string; userId: string; userDetails: string; userRoles: string[]; }
angular-app/src/app/core/components/nav.component.ts
ファイルを編集し、次のメソッドをNavComponent
クラス内に追加します。async getUserInfo() { try { const response = await fetch('/.auth/me'); const payload = await response.json(); const { clientPrincipal } = payload; return clientPrincipal; } catch (error) { console.error('No profile could be found'); return undefined; } }
新しいクラス プロパティ
userInfo
を作成し、コンポーネントの初期化時に非同期関数getUserInfo()
の結果を格納します。OnInit
インターフェイスを実装し、OnInit
とUserInfo
をインポートする import ステートメントを更新します。 このコードでは、コンポーネントの初期化時にユーザー情報を取り込みます。import { Component, OnInit } from '@angular/core'; import { UserInfo } from '../model/user-info'; export class NavComponent implements OnInit { userInfo: UserInfo; async ngOnInit() { this.userInfo = await this.getUserInfo(); } // ... }
react-app/src/components/NavBar.js
ファイルを編集し、関数の先頭に次のコードを追加します。 このコードでは、コンポーネントが読み込まれるときにユーザー情報を取り込んで、状態に格納します。import React, { useState, useEffect } from 'react'; import { NavLink } from 'react-router-dom'; const NavBar = (props) => { const [userInfo, setUserInfo] = useState(); useEffect(() => { (async () => { setUserInfo(await getUserInfo()); })(); }, []); async function getUserInfo() { try { const response = await fetch('/.auth/me'); const payload = await response.json(); const { clientPrincipal } = payload; return clientPrincipal; } catch (error) { console.error('No profile could be found'); return undefined; } } return ( // ...
svelte-app/src/components/NavBar.svelte
ファイルを編集し、スクリプト セクションに次のコードを追加します。 このコードでは、コンポーネントが読み込まれるときにユーザー情報を取り込みます。import { onMount } from 'svelte'; let userInfo = undefined; onMount(async () => (userInfo = await getUserInfo())); async function getUserInfo() { try { const response = await fetch('/.auth/me'); const payload = await response.json(); const { clientPrincipal } = payload; return clientPrincipal; } catch (error) { console.error('No profile could be found'); return undefined; } }
vue-app/src/components/nav-bar.vue
ファイルを編集し、userInfo
をデータ オブジェクトに追加します。data() { return { userInfo: { type: Object, default() {}, }, }; },
getUserInfo()
メソッドを methods セクションに追加します。methods: { async getUserInfo() { try { const response = await fetch('/.auth/me'); const payload = await response.json(); const { clientPrincipal } = payload; return clientPrincipal; } catch (error) { console.error('No profile could be found'); return undefined; } }, },
created
ライフサイクル フックをコンポーネントに追加します。async created() { this.userInfo = await this.getUserInfo(); },
コンポーネントが作成されると、ユーザー情報が自動的に取り込まれます。
ログイン ボタンとログアウト ボタンを追加する
ログインしていない場合、ユーザー情報は undefined
になるため、現時点では変更は表示されません。 次に、さまざまなプロバイダー用のログイン ボタンを追加します。
angular-app/src/app/core/components/nav.component.ts
ファイルを編集して、NavComponent
クラス内にプロバイダー リストを追加します。providers = ['x', 'github', 'aad'];
次の
redirect
プロパティを追加して、ログイン後のリダイレクトに対する現在の URL をキャプチャします。redirect = window.location.pathname;
次のコードを、テンプレートの最初の
</nav>
要素の後に追加して、ログインとログアウト ボタンを表示します。<nav class="menu auth"> <p class="menu-label">Auth</p> <div class="menu-list auth"> <ng-container *ngIf="!userInfo; else logout"> <ng-container *ngFor="let provider of providers"> <a href="/.auth/login/{{provider}}?post_login_redirect_uri={{redirect}}">{{provider}}</a> </ng-container> </ng-container> <ng-template #logout> <a href="/.auth/logout?post_logout_redirect_uri={{redirect}}">Logout</a> </ng-template> </div> </nav>
ユーザーがログインしていない場合は、各プロバイダーのログイン ボタンが表示されます。 各ボタンによって
/.auth/login/<AUTH_PROVIDER>
にリンクされ、リダイレクト URL は現在のページに設定されます。それ以外の場合、ユーザーが既にログインしていれば、
/.auth/logout
にリンクしているログアウト ボタンが表示されます。また、リダイレクト URL は現在のページに設定されます。
ブラウザーに次の Web ページが表示されます。
react-app/src/components/NavBar.js
ファイルを編集して、プロバイダー リストを関数上部に追加します。const providers = ['x', 'github', 'aad'];
最初の変数の下に次の
redirect
変数を追加して、ログイン後のリダイレクトに対する現在の URL をキャプチャします。const redirect = window.location.pathname;
次のコードを JSX テンプレートの最初の
</nav>
要素の後に追加して、ログインおよびログアウト ボタンを表示します。<nav className="menu auth"> <p className="menu-label">Auth</p> <div className="menu-list auth"> {!userInfo && providers.map((provider) => ( <a key={provider} href={`/.auth/login/${provider}?post_login_redirect_uri=${redirect}`}> {provider} </a> ))} {userInfo && <a href={`/.auth/logout?post_logout_redirect_uri=${redirect}`}>Logout</a>} </div> </nav>
ユーザーがログインしていない場合は、各プロバイダーのログイン ボタンが表示されます。 各ボタンによって
/.auth/login/<AUTH_PROVIDER>
にリンクされ、リダイレクト URL は現在のページに設定されます。それ以外の場合、ユーザーが既にログインしていれば、
/.auth/logout
にリンクしているログアウト ボタンが表示されます。また、リダイレクト URL は現在のページに設定されます。
ブラウザーに次の Web ページが表示されます。
svelte-app/src/components/NavBar.svelte
ファイルを編集して、プロバイダー リストをスクリプト上部に追加します。const providers = ['x', 'github', 'aad'];
最初の変数の下に次の
redirect
変数を追加して、ログイン後のリダイレクトに対する現在の URL をキャプチャします。const redirect = window.location.pathname;
次のコードを、テンプレートの最初の
</nav>
要素の後に追加して、ログインとログアウト ボタンを表示します。<nav class="menu auth"> <p class="menu-label">Auth</p> <div class="menu-list auth"> {#if !userInfo} {#each providers as provider (provider)} <a href={`/.auth/login/${provider}?post_login_redirect_uri=${redirect}`}> {provider} </a> {/each} {/if} {#if userInfo} <a href={`/.auth/logout?post_logout_redirect_uri=${redirect}`}> Logout </a> {/if} </div> </nav>
ユーザーがログインしていない場合は、各プロバイダーのログイン ボタンが表示されます。 各ボタンによって
/.auth/login/<AUTH_PROVIDER>
にリンクされ、リダイレクト URL は現在のページに設定されます。それ以外の場合、ユーザーが既にログインしていれば、
/.auth/logout
にリンクしているログアウト ボタンが表示されます。また、リダイレクト URL は現在のページに設定されます。
ブラウザーに次の Web ページが表示されます。
vue-app/src/components/nav-bar.vue
ファイルを編集して、プロバイダーのリストをデータ オブジェクトに追加します。providers: ['x', 'github', 'aad'],
次の
redirect
プロパティを追加して、ログイン後のリダイレクトに対する現在の URL をキャプチャします。redirect: window.location.pathname,
次のコードを、テンプレートの最初の
</nav>
要素の後に追加して、ログインとログアウト ボタンを表示します。<nav class="menu auth"> <p class="menu-label">Auth</p> <div class="menu-list auth"> <template v-if="!userInfo"> <template v-for="provider in providers"> <a :key="provider" :href="`/.auth/login/${provider}?post_login_redirect_uri=${redirect}`"> {{ provider }} </a> </template> </template> <a v-if="userInfo" :href="`/.auth/login/${provider}?post_login_redirect_uri=${redirect}`"> Logout </a> </div> </nav>
ユーザーがログインしていない場合は、各プロバイダーのログイン ボタンが表示されます。 各ボタンによって
/.auth/login/<AUTH_PROVIDER>
にリンクされ、リダイレクト URL は現在のページに設定されます。それ以外の場合、ユーザーが既にログインしていれば、
/.auth/logout
にリンクしているログアウト ボタンが表示されます。また、リダイレクト URL は現在のページに設定されます。
ブラウザーに次の Web ページが表示されます。
ユーザーのログイン状態を表示する
認証ワークフローをテストする前に、ログインしているユーザーに関するユーザーの詳細を表示しましょう。
angular-app/src/app/core/components/nav.component.ts
ファイルを編集し、このコードをテンプレートの下部の最後の終了 </nav>
タグの後に追加します。
<div class="user" *ngIf="userInfo">
<p>Welcome</p>
<p>{{ userInfo?.userDetails }}</p>
<p>{{ userInfo?.identityProvider }}</p>
</div>
注意
userDetails
プロパティには、ログインに使用する ID に応じて、ユーザー名またはメール アドレスを指定できます。
完成したファイルは次のようになります。
import { Component, OnInit } from '@angular/core';
import { UserInfo } from '../model/user-info';
@Component({
selector: 'app-nav',
template: `
<nav class="menu">
<p class="menu-label">Menu</p>
<ul class="menu-list">
<a routerLink="/products" routerLinkActive="router-link-active">
<span>Products</span>
</a>
<a routerLink="/about" routerLinkActive="router-link-active">
<span>About</span>
</a>
</ul>
</nav>
<nav class="menu auth">
<p class="menu-label">Auth</p>
<div class="menu-list auth">
<ng-container *ngIf="!userInfo; else logout">
<ng-container *ngFor="let provider of providers">
<a href="/.auth/login/{{ provider }}?post_login_redirect_uri={{ redirect }}">{{ provider }}</a>
</ng-container>
</ng-container>
<ng-template #logout>
<a href="/.auth/logout?post_logout_redirect_uri={{ redirect }}">Logout</a>
</ng-template>
</div>
</nav>
<div class="user" *ngIf="userInfo">
<p>Welcome</p>
<p>{{ userInfo?.userDetails }}</p>
<p>{{ userInfo?.identityProvider }}</p>
</div>
`,
})
export class NavComponent implements OnInit {
providers = ['x', 'github', 'aad'];
redirect = window.location.pathname;
userInfo: UserInfo;
async ngOnInit() {
this.userInfo = await this.getUserInfo();
}
async getUserInfo() {
try {
const response = await fetch('/.auth/me');
const payload = await response.json();
const { clientPrincipal } = payload;
return clientPrincipal;
} catch (error) {
console.error('No profile could be found');
return undefined;
}
}
}
react-app/src/components/NavBar.js
ファイルを編集し、このコードを JSX テンプレートの下部の最後の終了 </nav>
タグの後に追加して、ログイン状態を表示します。
{
userInfo && (
<div>
<div className="user">
<p>Welcome</p>
<p>{userInfo && userInfo.userDetails}</p>
<p>{userInfo && userInfo.identityProvider}</p>
</div>
</div>
)
}
注意
userDetails
プロパティには、ログインに使用する ID に応じて、ユーザー名またはメール アドレスを指定できます。
完成したファイルは次のようになります。
import React, { useState, useEffect } from 'react';
import { NavLink } from 'react-router-dom';
const NavBar = (props) => {
const providers = ['x', 'github', 'aad'];
const redirect = window.location.pathname;
const [userInfo, setUserInfo] = useState();
useEffect(() => {
(async () => {
setUserInfo(await getUserInfo());
})();
}, []);
async function getUserInfo() {
try {
const response = await fetch('/.auth/me');
const payload = await response.json();
const { clientPrincipal } = payload;
return clientPrincipal;
} catch (error) {
console.error('No profile could be found');
return undefined;
}
}
return (
<div className="column is-2">
<nav className="menu">
<p className="menu-label">Menu</p>
<ul className="menu-list">
<NavLink to="/products" activeClassName="active-link">
Products
</NavLink>
<NavLink to="/about" activeClassName="active-link">
About
</NavLink>
</ul>
{props.children}
</nav>
<nav className="menu auth">
<p className="menu-label">Auth</p>
<div className="menu-list auth">
{!userInfo &&
providers.map((provider) => (
<a key={provider} href={`/.auth/login/${provider}?post_login_redirect_uri=${redirect}`}>
{provider}
</a>
))}
{userInfo && <a href={`/.auth/logout?post_logout_redirect_uri=${redirect}`}>Logout</a>}
</div>
</nav>
{userInfo && (
<div>
<div className="user">
<p>Welcome</p>
<p>{userInfo && userInfo.userDetails}</p>
<p>{userInfo && userInfo.identityProvider}</p>
</div>
</div>
)}
</div>
);
};
export default NavBar;
svelte-app/src/components/NavBar.svelte
ファイルを編集し、このコードをテンプレートの下部の最後の終了 </nav>
タグの後に追加して、ログイン状態を表示します。
{#if userInfo}
<div class="user">
<p>Welcome</p>
<p>{userInfo && userInfo.userDetails}</p>
<p>{userInfo && userInfo.identityProvider}</p>
</div>
{/if}
注意
userDetails
プロパティには、ログインに使用する ID に応じて、ユーザー名またはメール アドレスを指定できます。
完成したファイルは次のようになります。
<script>
import { onMount } from 'svelte';
import { Link } from 'svelte-routing';
const providers = ['x', 'github', 'aad'];
const redirect = window.location.pathname;
let userInfo = undefined;
onMount(async () => (userInfo = await getUserInfo()));
async function getUserInfo() {
try {
const response = await fetch('/.auth/me');
const payload = await response.json();
const { clientPrincipal } = payload;
return clientPrincipal;
} catch (error) {
console.error('No profile could be found');
return undefined;
}
}
function getProps({ href, isPartiallyCurrent, isCurrent }) {
const isActive = href === '/' ? isCurrent : isPartiallyCurrent || isCurrent;
// The object returned here is spread on the anchor element's attributes
if (isActive) {
return { class: 'router-link-active' };
}
return {};
}
</script>
<div class="column is-2">
<nav class="menu">
<p class="menu-label">Menu</p>
<ul class="menu-list">
<Link to="/products" {getProps}>Products</Link>
<Link to="/about" {getProps}>About</Link>
</ul>
</nav>
<nav class="menu auth">
<p class="menu-label">Auth</p>
<div class="menu-list auth">
{#if !userInfo}
{#each providers as provider (provider)}
<a href={`/.auth/login/${provider}?post_login_redirect_uri=${redirect}`}>
{provider}
</a>
{/each}
{/if}
{#if userInfo}
<a href={`/.auth/logout?post_logout_redirect_uri=${redirect}`}>
Logout
</a>
{/if}
</div>
</nav>
{#if userInfo}
<div class="user">
<p>Welcome</p>
<p>{userInfo && userInfo.userDetails}</p>
<p>{userInfo && userInfo.identityProvider}</p>
</div>
{/if}
</div>
vue-app/src/components/nav-bar.vue
ファイルを編集し、このコードをテンプレートの下部の最後の終了 </nav>
タグの後に追加して、ログイン状態を表示します。
<div class="user" v-if="userInfo">
<p>Welcome</p>
<p>{{ userInfo.userDetails }}</p>
<p>{{ userInfo.identityProvider }}</p>
</div>
注意
userDetails
プロパティには、ログインに使用する ID に応じて、ユーザー名またはメール アドレスを指定できます。
完成したファイルは次のようになります。
<script>
export default {
name: 'NavBar',
data() {
return {
userInfo: {
type: Object,
default() {},
},
providers: ['x', 'github', 'aad'],
redirect: window.location.pathname,
};
},
methods: {
async getUserInfo() {
try {
const response = await fetch('/.auth/me');
const payload = await response.json();
const { clientPrincipal } = payload;
return clientPrincipal;
} catch (error) {
console.error('No profile could be found');
return undefined;
}
},
},
async created() {
this.userInfo = await this.getUserInfo();
},
};
</script>
<template>
<div column is-2>
<nav class="menu">
<p class="menu-label">Menu</p>
<ul class="menu-list">
<router-link to="/products">Products</router-link>
<router-link to="/about">About</router-link>
</ul>
</nav>
<nav class="menu auth">
<p class="menu-label">Auth</p>
<div class="menu-list auth">
<template v-if="!userInfo">
<template v-for="provider in providers">
<a :key="provider" :href="`/.auth/login/${provider}?post_login_redirect_uri=${redirect}`">{{ provider }}</a>
</template>
</template>
<a v-if="userInfo" :href="`/.auth/logout?post_logout_redirect_uri=${redirect}`">Logout</a>
</div>
</nav>
<div class="user" v-if="userInfo">
<p>Welcome</p>
<p>{{ userInfo.userDetails }}</p>
<p>{{ userInfo.identityProvider }}</p>
</div>
</div>
</template>
ローカルで認証をテストする
すべてが配置されたら、 最後の手順として、すべてが想定どおりに動作するかどうかをテストします。
Web アプリで、ログインする ID プロバイダーの 1 つを選択します。
このページにリダイレクトされます。
これは、SWA CLI によって提供されるフェイクの認証画面です。SWA CLI を使用すると、自分のユーザー詳細情報を入力して、ローカルで認証をテストできます。
ユーザー名として
mslearn
、ユーザー ID として1234
を入力します。[ログイン] を選択します。
ログイン後、前のページにリダイレクトされます。 ログイン ボタンがログアウト ボタンに置き換えられているのがわかります。 また、ログアウト ボタンの下で、ご自身のユーザー名と選択したプロバイダーを確認することもできます。
ローカルですべてが想定どおりに動作することを確認したら、ご自身の変更をデプロイします。
実行中のアプリと API を停止するには、両方の端末で Ctrl + C キーを押します。
変更をデプロイする
Visual Studio Code で、F1 キーを押してコマンド パレットを開きます。
「Git: Commit All」と入力して選択します。
コミット メッセージとして「
Add authentication
」と入力し、Enter キーを押します。F1 キーを押して、コマンド パレットを開きます。
「Git: Push」と入力して選択し、Enterキーを押します。
変更をプッシュしたら、ビルドおよびデプロイ プロセスが実行されるまで待ちます。 その後、デプロイされたアプリ上に変更が表示されます。
次の手順
これでアプリがユーザー認証をサポートするようになりました。次は、アプリの一部を、認証されていないユーザーに制限します。