演習 - ユーザー認証を追加する

完了

あなたのショッピング リスト Web アプリにはユーザー認証が必要です。 この演習では、アプリにログインとログアウトを実装し、現在のユーザーのログイン状態を表示します。

この演習では、次の手順を行います。

  1. ローカル開発用の Static Web Apps CLI をインストールします。
  2. ローカル認証エミュレーションを使用して、ローカルでアプリと API を実行します。
  3. 複数の認証プロバイダー用のログイン ボタンを追加します。
  4. ユーザーがログインしている場合のログアウト ボタンを追加します。
  5. ユーザーのログイン状態を表示します。
  6. ローカルで認証ワークフローをテストします。
  7. 更新されたアプリをデプロイします。

ローカル開発の準備をする

Static Web Apps CLI (SWA CLI とも呼ばれます) はローカル開発ツールです。このツールを使用すると、Web アプリと API をローカルで実行し、認証と承認サーバーをエミュレートすることができます。

  1. コンピューターでターミナルを開きます。

  2. 次のコマンドを実行して SWA CLI をインストールします。

    npm install -g @azure/static-web-apps-cli
    

アプリをローカルで実行する

ここでは、開発サーバーを使用して、ローカルでアプリと API を実行します。 この方法では、コード内で変更を行うため、自分の変更を確認してテストすることができます。

  1. Visual Studio Code でプロジェクトを開きます。

  2. Visual Studio Code で、F1 キーを押してコマンド パレットを開きます。

  3. Terminal: Create New Integrated Terminal」と入力して選択します。

  4. 以下のように、任意のフロントエンド フレームワークのフォルダーに移動します。

    cd angular-app
    
    cd react-app
    
    cd svelte-app
    
    cd vue-app
    
  5. 開発サーバーを使用して、フロントエンド クライアント アプリケーションを実行します。

    npm start
    
    npm start
    
    npm run dev
    
    npm run serve
    

    このサーバーはバックグラウンドで実行したままにします。 ここでは、SWA CLI を使用して、API と認証サーバー エミュレーターを実行します。

  6. Visual Studio Code で、F1 キーを押してコマンド パレットを開きます。

  7. Terminal: Create New Integrated Terminal」と入力して選択します。

  8. 次のコマンドを実行して 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
    
  9. http://localhost:4280 を参照します。

SWA CLI によって使用される最終的なポートは、前に示したものとは異なります。これはリバース プロキシを使用して、3 つの異なるコンポーネントに要求を転送するためです。

  • ご自身のフレームワーク開発サーバー
  • 認証および承認エミュレーター
  • Functions ランタイムによってホストされている API

Screenshot of the Static Web Apps CLI architecture.

コードを変更する間は、アプリケーションを実行したままにしておきます。

ユーザーのログイン状態を取得する

まず、クライアント内で /.auth/me に対してクエリを実行して、ユーザーのログイン状態にアクセスする必要があります。

  1. angular-app/src/app/core/models/user-info.ts ファイルを作成し、ユーザー情報のインターフェイスを表す次のコードを追加します。

    export interface UserInfo {
      identityProvider: string;
      userId: string;
      userDetails: string;
      userRoles: string[];
    }
    
  2. 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;
      }
    }
    
  3. 新しいクラス プロパティ userInfo を作成し、コンポーネントの初期化時に非同期関数 getUserInfo() の結果を格納します。 OnInit インターフェイスを実装し、OnInitUserInfo をインポートする 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();
      }
      // ...
    }
    
  1. 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 (
      // ...
    
  1. 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;
      }
    }
    
  1. vue-app/src/components/nav-bar.vue ファイルを編集し、userInfo をデータ オブジェクトに追加します。

     data() {
       return {
         userInfo: {
           type: Object,
           default() {},
         },
       };
     },
    
  2. 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;
        }
      },
    },
    
  3. created ライフサイクル フックをコンポーネントに追加します。

    async created() {
      this.userInfo = await this.getUserInfo();
    },
    

    コンポーネントが作成されると、ユーザー情報が自動的に取り込まれます。

ログイン ボタンとログアウト ボタンを追加する

ログインしていない場合、ユーザー情報は undefined になるため、現時点では変更は表示されません。 次に、さまざまなプロバイダー用のログイン ボタンを追加します。

  1. angular-app/src/app/core/components/nav.component.ts ファイルを編集して、NavComponent クラス内にプロバイダー リストを追加します。

    providers = ['twitter', 'github', 'aad'];
    
  2. 次の redirect プロパティを追加して、ログイン後のリダイレクトに対する現在の URL をキャプチャします。

    redirect = window.location.pathname;
    
  3. 次のコードを、テンプレートの最初の </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 ページが表示されます。

Screenshot of the Angular web app with login buttons.

  1. react-app/src/components/NavBar.js ファイルを編集して、プロバイダー リストを関数上部に追加します。

    const providers = ['twitter', 'github', 'aad'];
    
  2. 最初の変数の下に次の redirect 変数を追加して、ログイン後のリダイレクトに対する現在の URL をキャプチャします。

    const redirect = window.location.pathname;
    
  3. 次のコードを 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 ページが表示されます。

Screenshot of the React web app with login buttons.

  1. svelte-app/src/components/NavBar.svelte ファイルを編集して、プロバイダー リストをスクリプト上部に追加します。

    const providers = ['twitter', 'github', 'aad'];
    
  2. 最初の変数の下に次の redirect 変数を追加して、ログイン後のリダイレクトに対する現在の URL をキャプチャします。

    const redirect = window.location.pathname;
    
  3. 次のコードを、テンプレートの最初の </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 ページが表示されます。

Screenshot of the Svelte web app with login buttons.

  1. vue-app/src/components/nav-bar.vue ファイルを編集して、プロバイダーのリストをデータ オブジェクトに追加します。

     providers: ['twitter', 'github', 'aad'],
    
  2. 次の redirect プロパティを追加して、ログイン後のリダイレクトに対する現在の URL をキャプチャします。

     redirect: window.location.pathname,
    
  3. 次のコードを、テンプレートの最初の </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 ページが表示されます。

Screenshot of the Vue web app with login buttons.

ユーザーのログイン状態を表示する

認証ワークフローをテストする前に、ログインしているユーザーに関するユーザーの詳細を表示しましょう。

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 = ['twitter', '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 = ['twitter', '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 = ['twitter', '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: ['twitter', '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>

ローカルで認証をテストする

すべてが配置されたら、 最後の手順として、すべてが想定どおりに動作するかどうかをテストします。

  1. Web アプリで、ログインする ID プロバイダーの 1 つを選択します。

  2. このページにリダイレクトされます。

    Screenshot showing SWA CLI fake authentication screen.

    これは、SWA CLI によって提供されるフェイクの認証画面です。SWA CLI を使用すると、自分のユーザー詳細情報を入力して、ローカルで認証をテストできます。

  3. ユーザー名として mslearn、ユーザー ID として 1234 を入力します。

  4. [ログイン] を選択します。

    ログイン後、前のページにリダイレクトされます。 ログイン ボタンがログアウト ボタンに置き換えられているのがわかります。 また、ログアウト ボタンの下で、ご自身のユーザー名と選択したプロバイダーを確認することもできます。

    ローカルですべてが想定どおりに動作することを確認したら、ご自身の変更をデプロイします。

  5. 実行中のアプリと API を停止するには、両方の端末で Ctrl + C キーを押します。

変更をデプロイする

  1. Visual Studio Code で、F1 キーを押してコマンド パレットを開きます。

  2. Git: Commit All」と入力して選択します。

  3. コミット メッセージとして「Add authentication」と入力し、Enter キーを押します。

  4. F1 キーを押して、コマンド パレットを開きます。

  5. Git: Push」と入力して選択し、Enterキーを押します。

変更をプッシュしたら、ビルドおよびデプロイ プロセスが実行されるまで待ちます。 その後、デプロイされたアプリ上に変更が表示されます。

次の手順

これでアプリがユーザー認証をサポートするようになりました。次は、アプリの一部を、認証されていないユーザーに制限します。