LoginSignup
8
8

More than 1 year has passed since last update.

維持費無料 Azure Active Directory + ASP.NET Core Web API + Vue.js でつくるシングルページアプリケーション

Posted at

はじめに

Azure Active Directory はお使いでしょうか?
Azure Active Directory は Microsoft 365 に付属していますので、導入している企業は意外と多いのではないかと思います。

本記事では、Azure Active Directory でシングルサインオンできるシングルページアプリケーションの開発をスタートする手順をご紹介いたします。

まず、Azure Active Directory をシングルページアプリケーションの認証基盤とすることのメリットを以下にまとめてみました。

  • 企業が導入している Microsoft 365 とシングルサインオンできます。
  • シングルページアプリケーションに利用者のパスワードを通過させることなく認証できます。
  • 二要素認証や条件付きアクセスポリシーなどの高度なセキュリティ機能を備えています。(一部機能は有料)
  • OAuth や OpenID Connect といった認証基盤を自分で開発する必要がありません。
    • そもそも標準規格に適合した認証基盤を開発するのは非常に地味な仕事で、かつ想像を絶するほど大変です。
  • 基本的な機能を無料で利用できますので開発環境の維持にコストがかかりません。
    • 無料プランの場合は SLA は提供されませんが、Azure や Microsoft 365 が利用していますので Microsoft にとって「一丁目一番地」と言えるほど重要なサービスであり信頼できます。社内アプリであれば本番環境を無料プランにしても良いでしょう。

Azure Active Directory を認証基盤にすることで業務効率化につながる社内アプリをセキュアで簡単、維持費無料でつくれるようになるというわけです。

この記事の手順を参考にして、ぜひ社内アプリの開発にトライしてみてください。

システム構成

Azure Active Directory には「アプリ」を登録することによってサインインしているユーザー情報をアプリに提供したり、サインインしているユーザーに設定した「アプリロール」をつかって、ユーザーがアクセスできる操作を制限することができます。

本記事で紹介するシステム構成と主なデータの流れは以下の通りです。認証のデータの流れの実態はもっと複雑なものですが、この図では簡略化しています。

システム構成イメージ.png

※上図下部の「Vue.js」はあなたがこれから作成するシングルページアプリケーションです。任意のサーバーに配置できます。
※上図右上の「ASP.NET Core」はあなたがこれから作成する Web API アプリケーションです。任意のサーバーに配置できます。

以下はそれぞれの番号の対応する処理の概要です。

① MSAL.js による認証

Azure Active Directory と Vue.js との認証には MSAL.js(Microsoft Authentication Library for JavaScript)を利用します。
MSAL.js をつかうと、Azure Active Directory の認証画面を呼び出して ID トークンやアクセストークンを検証して読み取るといった処理を透過的に実行することができます。

② Azure Active Directory アクセストークンの取得

MSAL.js によって認証後に OAuth 2.0 Authorization code flow (with PKCE) によってアクセストークンおよびリフレッシュトークンを受け取ります。

③ Azure Active Directory アクセストークンの利用

受け取ったアクセストークンを Authorization Bearer ヘッダーに指定することで Microsoft Graph API を呼び出します。

④ Microsoft Graph API からユーザー情報取得

Microsoft Graph API によってサインインしているユーザーの情報取得や更新、Azure Active Directory 内の他のユーザー情報を取得することができます。

⑤ ASP.NET Core Web API にアクセストークンで呼び出し

作成した ASP.NET Core の Web API アプリケーションのアクセストークンも利用可能です。
このアクセストークンには独自に定義した「アプリロール」の情報が含まれます。

⑥ アクセストークンの検証

ASP.NET Core に Microsoft.Identity.Web という NuGet パッケージを組み込むことで、アクセストークンを検証して読み取るといった処理が透過的に実行されます。アクセストークンの内容が改ざんされていないことが確認できているので、アクセストークンに含まれる「アプリロール」の情報は、Azure Active Directory から提供された信頼できる情報とみなすことができます。

⑦ Web API の操作結果の応答

「アプリロール」に許可されている操作である場合は Web API の操作を実行して結果を返します。なお、事前に管理者と一般ユーザーの「アプリロール」を作成しておき、Azure Active Directory 上で各ユーザーに「アプリロール」を割り当てておく必要があります。(本記事にはその手順が含まれています)

Azure Active Directory の設定

さて、システム構成とデータの流れのイメージがわかったところで、さっそく Azure Active Directory から作成していきましょう。

多くの企業はすでに Azure Active Directory を持っていると思いますので新しく作成する必要はありませんが、本番環境のデータをつかっていきなり開発するわけにもいきませんので、まっさらな Azure Active Directory から開発環境を用意することにします。

Azure Active Directory の無料プランはクレジットカードを用意する必要もありません。

Azure ポータルから Azure Active Directory を作成する

このセクションで開発環境向けの新しい Azure Active Directory を作成してください。
すでに Azure Active Directory を持っている場合はこのセクションはスキップできます。
https://portal.azure.com/ にアクセスして、「リソースの作成」をクリックして「Azure Active Directory」で検索してください。

1.png

「作成」をクリックします。

2.png

「Azure Active Directory」が選択されていることを確認して「確認および作成」をクリックします。

3.png

「組織名」と「初期ドメイン名」、「国/地域」をそれぞれ入力して「確認および作成」をクリックします。

4.png

入力内容を確認して「作成」をクリックします。「初期ドメイン名」はあとで使いますのでメモしておいてください。

5.png

リソースの作成が終わったら「右上の歯車マーク」をクリックしてメニューの「ディレクトリとサブスクリプション」をクリックし「切り替え」をクリックします。

6.png

Azure Active Directory にアプリを登録する

左メニューの「アプリの登録」をクリックして画面上部の「新規登録」をクリックします。

7.png

アプリケーションの「名前」を入力します。「サポートされているアカウントの種類」は「この組織ディレクトリのみに含まれるアカウント」が選択されていることを確認し「登録」をクリックします。

8.png

左メニューの「概要」をクリックして「アプリケーション(クライアント)ID」と「ディレクトリ(テナント)ID」をメモしておいてください。

9.png

左メニューの「認証」をクリックしてください。「サポートされているアカウントの種類」は「この組織ディレクトリのみに含まれるアカウント」が選択されていることを確認して「プラットフォームを追加」をクリックします。

10.png

「プラットフォームの構成」のダイアログで「シングルページアプリケーション」を選択します。

11.png

「シングルページアプリケーションの構成」のダイアログで「リダイレクト URI」は「http://localhost」を入力します。「暗黙的な許可およびハイブリッドフロー」の選択肢では「IDトークン」を選択して「構成」をクリックします。

12.png

「リダイレクト URI」には、シングルページアプリケーションの配置先となる URL をホワイトリストとして設定します。「リダイレクト URI」に登録してある URL のみが Azure Active Directory からトークンを受け取ることができるという意味です。 ここでは localhost を設定しますので、ローカル開発環境でトークンを受け取ることができます。なお、ポートは明示的に指定しなくても任意のポート(80、443、8080)で受け取り可能です。 なお、「リダイレクト URI」は後から複数設定することができます。

つづいて左メニューの「アプリロール」を選択して「アプリロールの作成」をクリックします。

13.png

最初に管理者のアプリロールを作成します。アプリロールの作成ダイアログで「表示名」に「管理者」と入力します。「許可されたメンバーの種類」は「両方」を選択します。「値」は「Administrators」、「説明」は任意の値を入力し、「有効」にチェックされていることを確認してください。

14.png

次に一般ユーザーのアプリロールを作成します。アプリロールの作成ダイアログで「表示名」に「ユーザー」と入力します。「許可されたメンバーの種類」は「両方」を選択します。「値」は「Users」、「説明」は任意の値を入力し、「有効」にチェックされていることを確認してください。

15.png

アプリロールは、Web API においてユーザーが実行可能な操作を区別するために使用します。値に指定した「Administrators」および「Users」は改ざんできないように署名されたアクセストークンに入れられてシングルページアプリケーションから Web API に送信されます。ASP.NET Core Web API では、[Authorize(Roles = "Administrators")] という属性を Controller や Action に設定することで、認証された管理者しか実行できない操作を公開することができます。(ASP.NET Core Web API はこのあとの手順で作成します)

つづいて左メニューの「API のアクセス許可」を選択して「アクセス許可の追加」をクリックします。

16.png

ダイアログで「Microsoft Graph」を選択します。

17.png

「委任されたアクセス許可」を選択して「User」配下にある「User.Read」、「User.ReadBasic.All」、「User.ReadWrite」をチェックして「アクセス許可の追加」をクリックします。

18.png

Microsoft Graph は、Microsoft のサービスである Office 365 や Azure AD など、様々なサービスのデータをグラフ形式で扱えるエンドポイントです。ここで設定しているアクセス許可はそれぞれ以下の操作をできるようにするためのものです。 User.Read ... サインインおよびサインインしている自分自身のプロフィール情報を読み取りできます。 User.ReadBasic.All ... Azure AD に所属しているすべてのユーザーの基本的なプロフィール情報を読み取りできます。 User.ReadWrite ... サインインしている自分自身のプロフィール情報を読み書きできます。 詳細については https://docs.microsoft.com/ja-jp/graph/permissions-reference#user-permissions を参照してください。

アプリの設定はこれで最後です。左メニューの「API の公開」を選択して「Scopeの追加」をクリックします。

19.png

スコープの追加のダイアログで「アプリケーションID の URI」は初期値のままで問題ありません。「保存してから続ける」をクリックします。

20.png

「スコープ名」には「access_as_user」と入力します。「同意できるのはだれですか?」には「管理者とユーザー」を選択します。その他の必須項目は、図の通りに入力してください。

21.png

ここで設定するスコープは、本来であれば認証時にダイアログでユーザーに対して許可を求めるものになるはずです。しかしながら、「API の公開」で設定したすべてのスコープについて、シングルページアプリケーションから認証すると、ユーザーに許可を求めることなく自由に Web API のアクセストークンに組み込むことができてしまいます。 ドキュメントを探しても解説を見つけることができなくて、この挙動が不具合なのか仕様なのか判断できませんでした。 スコープの追加のダイアログには「こちらにスコープを追加すると、委任されたアクセス許可のみが作成されます。アプリケーション専用スコープを作成しようとしている場合は、'アプリ ロール' を使用して、アプリケーションの種類に割り当て可能なアプリ ロールを定義してください。」と表示されていますので、アクセスの制御はアプリロールをつかうべきなのでしょうか。 この記事は「access_as_user」という、汎用的なスコープを定義するにとどめて、シングルページアプリケーションからのアクセストークンを取得する Web API では、スコープをつかったアクセス制御を使わず、アプリロールを使ったアクセス制御をしています。 このままでは「API の公開」の存在意義がいま一つ理解できないので、詳細を知っている方はぜひ情報提供をお願いします。

Azure Active Directory にユーザーを追加する

アプリの登録と設定が終わりましたので、今度は Azure Active Directory にユーザーを追加して、各ユーザーにアプリロールを割り当てていきます。この設定を行うことで、認証したユーザーが管理者と一般ユーザーなのかを区別することができて、シングルページアプリケーションが呼び出し可能な Web API をアクセス権によって分離することができます。

Azure Active Directory の左メニューの「ユーザー」を選択してください。

22.png

これから画面上部の「新しいユーザー」をクリックして、管理者ロールを割り当てるユーザー、ユーザーロールを割り当てるユーザー、外部の Azure AD に所属しているユーザーの招待を行っていきます。「新しいゲストユーザー」で登録した場合は、Microsoft Graph から取得可能な情報の範囲に制限がかかります。(詳細はこちらを参照してください)

23.png

「新しいユーザー」をクリックして、管理者となるユーザーとして任意の値を入力して「作成」をクリックしてください。ログインのパスワードが必要ですのでメモしておいてください。

24.png

「新しいユーザー」をクリックして、一般ユーザーとなるユーザーとして任意の値を入力して「作成」をクリックしてください。ログインのパスワードが必要ですのでメモしておいてください。

25.png

企業の Azure Active Directory のメールアドレスを持っている場合は、「新しいユーザー」をクリックして、「ユーザーの招待」を選択して「電子メールアドレス」にあなたのメールアドレスを入力してください。

26.png

招待メールは以下のような内容で届きます。「招待の承諾」をクリックすると登録が完了します。

27.png

「ユーザータイプ」の「メンバー」と「ゲスト」はプロファイルからいつでも変更することが可能です。

28.png

ここからはエンタープライズアプリケーションからの設定が必要です。作成したアプリの左メニュー「アプリロール」を選択して、「アプリロールを割り当てる方法」をクリックします。ダイアログで「アプリロールの割り当て」をクリックするとエンタープライズアプリケーションという設定画面に遷移できます。

29.png

エンタープライズアプリケーションの左メニューの「ユーザーとグループ」を選択して、「ユーザーまたはグループの追加」をクリックします。

30.png

「割り当ての追加」ダイアログにて先ほど作成したユーザーに対して管理ロールとユーザーロールの割り当てを行ってください。

31.png

割り当てが完了しました。これであなたが作成したアプリにアクセスできるユーザーの設定と、それぞれのユーザーが実行可能な操作を制御できる状態になりました。

32.png

アプリとエンタープライズアプリケーションは何が違うのでしょうか? ちょっとわかりにくい概念なので、この記事での解説は割愛します。 以下の記事にとてもわかりやすく解説されていますのでご参考まで。 もう多分怖くないサービスプリンシパル https://tech-lab.sios.jp/archives/23371

Vue.jsの設定

ここからは、シングルページアプリケーションの作成と設定の手順となります。
Node.js がインストールされていない場合は以下の URL から LTS 版をインストールしてください。

Vue CLIがインストールされていない場合は以下のコマンドでインストールしてください。

> npm install -g @vue/cli

手元のバージョンは以下の通りです。

> vue --version
@vue/cli 4.5.15

vue のアプリケーションを作成します。
ここでは、バージョンは Vue 3 で TypeScript のプロジェクトを作成しています。

> vue create msalvue
Vue CLI v4.5.15
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, TS, Linter  
? Choose a version of Vue.js that you want to start the project with 3.x
? Use class-style component syntax? No
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
? Pick a linter / formatter config: Basic
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No
? Pick the package manager to use when installing dependencies: NPM

vue のアプリケーションができたら以下のパッケージをインストールします。

  • @azure/msal-browser
    • MSAL.js のライブラリです。
  • axios
    • Vue.js における Promise ベースの定番 HTTP Client ライブラリです。
  • mitt
    • Vue.js における定番のイベントバスライブラリです。
> cd msalvue
> npm install @azure/msal-browser
> npm install axios
> npm install mitt

サンプルコードに mitt の使用は含まれていませんがサインイン・サインアウトのイベントをコンポーネント間でシームレスに伝播するのに必要になると思います。

インストールが終わったら起動するか確認します。以下のコマンドを実行して http://localhost:8080/ にアクセスしてください。

> npm run serve

起動できていれば大丈夫です。
HelloWorld.vue を以下のように書き換えてください。
{ClientID}{TenantID}は前の手順でメモした「アプリケーション(クライアント)ID」と「ディレクトリ(テナント)ID」にそれぞれ置き換えてください。なお、{WebAPI}は後で記入しますのでいったんそのままで大丈夫です。

HelloWorld.vue
<template>
  <div>
    <button @click="onClickSignIn">SignIn</button>
  </div>
  <div v-if="signedIn">
    <div style="margin: 10px">Hello {{userName}}!</div>
    <button style="margin: 5px" @click="onClickCallApi">CallApi</button>
    <button style="margin: 5px" @click="onClickSignOut">SignOut</button>
  </div>
  <div v-if="apiResponse">
    {{apiResponse}}
  </div>
</template>

<script lang="ts">
import { defineComponent, reactive, toRefs } from 'vue';
import { AuthenticationResult, InteractionRequiredAuthError, PublicClientApplication } from '@azure/msal-browser';
import axios from 'axios';

export default defineComponent({
  name: 'HelloWorld',
  setup() {
    const clientId = '{ClientID}';
    const tenantId = '{TenantID}';
    const webApiUrl = '{WebAPI}/WeatherForecast';
    const msalConfig = {
      auth: {
        clientId: clientId,
        authority: 'https://login.microsoftonline.com/' + tenantId,
        redirectUri: 'http://localhost:8080/',
        postLogoutRedirectUri: 'http://localhost:8080/',
      },
      cache: {
        cacheLocation: 'sessionStorage',
        storeAuthStateInCookie: false,
      },
    };
    const msal = new PublicClientApplication(msalConfig);
    const state = reactive({
      signedIn: false,
      userName: '',
      apiResponse: ''
    });

    msal
      .handleRedirectPromise()
      .then((response) => {
        if (response && response.account) {
          state.signedIn = true;
          state.userName = response.account.username;
        }
      })
      .catch((error) => {
        console.error(error);
      });

    const acquireToken = (scopes: string[]): Promise<AuthenticationResult | null> => {
      const accounts = msal.getAllAccounts();

      if (accounts.length >= 0) {
        const account = accounts[0];
        const silentRequest = {
          scopes: scopes,
          account: account,
          forceRefresh: false,
        };
        return msal
          .acquireTokenSilent(silentRequest)
          .then((response) => {
            return response;
          })
          .catch((error) => {
            if (error instanceof InteractionRequiredAuthError) {
              msal.acquireTokenRedirect(silentRequest);
            }
            return Promise.resolve(null);
          });
      }
      return Promise.reject(null);
    };

    const onClickSignIn = () => {
      msal.loginRedirect({ scopes: ['User.Read', 'User.ReadBasic.All', 'User.ReadWrite'] });
    }

    const onClickCallApi = () => {
      acquireToken(['api://' + clientId + '/access_as_user'])
        .then((response) => {
          if (response) {
            const config = { headers: { Authorization: `Bearer ${response.accessToken}` } };
            axios.get(webApiUrl, config)
              .then((apiResponse) => {
                state.apiResponse = apiResponse.data;
              })
              .catch((error) => {
                state.apiResponse = error.message;
              });
          }
        });
    }

    const onClickSignOut = () => {
      msal.logoutRedirect();
      state.signedIn = false;
    }

    return {
      ...toRefs(state),
      onClickSignIn,
      onClickCallApi,
      onClickSignOut
    }

  }
});
</script>

再度 http://localhost:8080 にアクセスしてください。「SingIn」をクリックしてください。

33.png

先ほど登録したユーザーのユーザー名を入力します。

34.png

引き続きそのユーザーのパスワードを入力します。

35.png

アクセス許可を求められます。ここで求められるのは、先ほど設定した「User.Read」、「User.ReadBasic.All」、「User.ReadWrite」に対応する Microsoft Graph に関するアクセス許可です。

36.png

ダイアログでは「組織の代理として同意する」というチェックボックスが表示されることがあります。 これは、Azure Active Directory の「グローバル管理者ロール」を持っているユーザーの場合に表示されます。 管理者がここにチェックを入れて承諾すると、個々のユーザーが個別に承諾することを省略することが可能です。

以下は「グローバル管理者ロール」を持っていないユーザーの場合に表示されるダイアログです。
すでに管理者による同意が行われている場合は、この画面が表示されることはありません。

37.png

サインインしたユーザーの情報が表示されていたらサインイン成功です。

38.png

ASP.NET Core Web API の設定

ここからは、Web API の作成と設定の手順となります。
.NET Core がインストールされていない場合は以下の URL から LTS 版の .NET SDK をインストールしてください。

手元のバージョンは以下の通りです。

> dotnet --version
6.0.101

Web API のアプリケーションを作成します。

> dotnet new webapi -n adwebapi
テンプレート "ASP.NET Core Web API" が正常に作成されました。

> cd adwebapi

つづいて Microsoft.Identity.Web のパッケージをインストールします。

> dotnet add package Microsoft.Identity.Web

Program.cs を以下のように書き換えてください。

Program.cs
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// ここでアクセストークンを検証するために必要な設定を読み込んでいます
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));

// ここで CORS の設定を行い localhost からのアクセスをすべて許可しています
builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(builder =>
    {
        builder.SetIsOriginAllowed(origin => new Uri(origin).Host == "localhost")
            .AllowCredentials()
            .AllowAnyMethod()
            .AllowAnyHeader();
    });
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();

appsettings.json を以下のように書き換えて、前の手順でメモしておいた情報を {Domain} および {ClientID}{TenantID} に設定してください。

appsettings.json
{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "{Domain}",
    "ClientId": "{ClientID}",
    "TenantId": "{TenantID}"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

Controllers/WeatherForecastController.cs の冒頭に using Microsoft.AspNetCore.Authorization; の定義を追加して Get メソッドに [Authorize(Roles = "Administrators")] 付与します。

Controllers/WeatherForecastController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;

namespace adwebapi.Controllers;

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    ... 省略 ...

    // この設定でアクセストークンに含まれているロールを確認して操作を実行可能かどうか判定します
    [Authorize(Roles = "Administrators")]
    [HttpGet(Name = "GetWeatherForecast")]
    public IEnumerable<WeatherForecast> Get()
    {
        ... 省略 ...
    }
}

以下のコマンドを実行してください。
応答メッセージに待ち受けポートの情報が含まれています。

> dotnet run
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: https://localhost:7239
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5021
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.

上の例では https://localhost:7239 になりますので、前の手順で作成した Vue.js の HelloWorld.vue の {WebAPI} に設定してください。

HelloWorld.vue
export default defineComponent({
  name: 'HelloWorld',
  setup() {
    const clientId = '...設定済み...';
    const tenantId = '...設定済み...';
    const webApiUrl = 'https://localhost:7239/WeatherForecast';
    const msalConfig = {

これですべての準備が整いました!

動作確認

まずは管理者ロールを割り当てたユーザーでサインインして「CallApi」をクリックしてください。
画面下に応答メッセージが表示されれば成功です。

39.png

つぎにユーザーロールを割り当てたユーザーでサインインして「CallApi」をクリックしてください。
画面下にエラーメッセージが表示されれば成功です。

40.png

最後に新しいブラウザウィンドウを開いて https://localhost:7239/WeatherForecast に直接アクセスしてみてください。
401エラーのメッセージが表示されれば成功です。

41.png

最後に

すべての手順をなるべく丁寧に記載していきましたが、すごいボリュームの記事になってしまいました...。

内容を理解できていれば、やっていることは大したことではないのですが、Azure Active Directory の設定にかなりのスペースを割く結果となりました。この記事を公開後に Azure Active Directory のユーザーインターフェースや各種文言が変わってしまうことは避けられないので、2021年12月19日現在の状態ということでご了承ください。

もし、この記事が役立ったという方が多いようでしたら、なるべく最新の状態を維持していきたいと思っています。
手順が古くなっていて最新に更新した方が良い部分がありましたら、遠慮なくご連絡ください。

8
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
8