ASP.NET Core Blazor Web アプリで TOTP 認証アプリ用の QR コードを生成できるようにする

この記事では、ASP.NET Core Blazor Web アプリに TOTP 認証アプリ用の QR コード生成を構成する方法について説明します。

時間ベースのワンタイム パスワード アルゴリズム (TOTP) を使用した認証アプリでの 2 要素認証 (2FA) の概要については、「ASP.NET Core で TOTP 認証アプリ用の QR コードを生成できるようにする」を参照してください。

Enable Authenticator コンポーネントをアプリにスキャフォールディングする

ASP.NET Core プロジェクトでの Identity のスキャフォールディング」のガイダンスに従って、アプリに Pages\Manage\EnableAuthenticator をスキャフォールディングします。

Note

この例では、スキャフォールディングの対象として EnableAuthenticator コンポーネントのみが選択されていますが、スキャフォールディングでは現在、Identity のすべてのコンポーネントがアプリに追加されます。 さらに、アプリへのスキャフォールディング プロセス中に例外がスローされる場合があります。 データベースの移行時に例外が発生した場合は、例外が発生するたびにアプリを停止し、アプリを再起動します。 詳細については、「Blazor Web アプリのスキャフォールディングの例外 (dotnet/Scaffolding #2694)」を参照してください。

移行が実行されている間は、しばらくお待ちください。 システムの速度によっては、データベースの移行が完了するまでに最大 1 - 2 分かかることがあります。

詳細については、「ASP.NET Core プロジェクトの Identity のスキャフォールディング」を参照してください。 Visual Studio ではなく .NET CLI を使用する場合のガイダンスについては、dotnet aspnet-codegenerator コマンドのページを参照してください。

2FA 構成ページへの QR コードの追加

次の手順では、Shim Sangminqrcode.js: JavaScript 用のクロスブラウザー QRCode ジェネレーター (davidshimjs/qrcodejs GitHub リポジトリ) を使用します。

ソリューションのサーバー プロジェクトの wwwroot フォルダーに qrcode.min.js ライブラリをダウンロードします。 ライブラリに依存関係はありません。

App コンポーネント (Components/App.razor) で、Blazor の <script> タグの後にライブラリ スクリプト参照を配置します。

<script src="qrcode.min.js"></script>

アプリの QR コード システムの一部であり、QR コードをユーザーに表示する EnableAuthenticator コンポーネントでは、拡張ナビゲーションを使用した静的サーバー側レンダリング (静的 SSR) が採用されています。 そのため、コンポーネントが拡張ナビゲーションで読み込まれたり更新されたりすると、通常のスクリプトは実行できません。 ページの読み込み時に QR コードの UI への読み込みをトリガーするには、追加の手順が必要です。 QR コードの読み込みを実現するために、「ASP.NET Core Blazor JavaScript と静的サーバー側レンダリング (静的 SSR)」で説明されているアプローチが採用されています。

次の JavaScript 初期化子をサーバー プロジェクトの wwwroot フォルダーに追加します。 Blazor でファイルを自動的に検索して読み込むには、{NAME} プレースホルダーがアプリのアセンブリの名前である必要があります。 サーバー アプリのアセンブリ名が BlazorSample である場合、ファイルには BlazorSample.lib.module.js という名前が付けられます。

wwwroot/{NAME}.lib.module.js:

const pageScriptInfoBySrc = new Map();

function registerPageScriptElement(src) {
  if (!src) {
    throw new Error('Must provide a non-empty value for the "src" attribute.');
  }

  let pageScriptInfo = pageScriptInfoBySrc.get(src);

  if (pageScriptInfo) {
    pageScriptInfo.referenceCount++;
  } else {
    pageScriptInfo = { referenceCount: 1, module: null };
    pageScriptInfoBySrc.set(src, pageScriptInfo);
    initializePageScriptModule(src, pageScriptInfo);
  }
}

function unregisterPageScriptElement(src) {
  if (!src) {
    return;
  }

  const pageScriptInfo = pageScriptInfoBySrc.get(src);
  
  if (!pageScriptInfo) {
    return;
  }

  pageScriptInfo.referenceCount--;
}

async function initializePageScriptModule(src, pageScriptInfo) {
  if (src.startsWith("./")) {
    src = new URL(src.substr(2), document.baseURI).toString();
  }

  const module = await import(src);

  if (pageScriptInfo.referenceCount <= 0) {
    return;
  }

  pageScriptInfo.module = module;
  module.onLoad?.();
  module.onUpdate?.();
}

function onEnhancedLoad() {
  for (const [src, { module, referenceCount }] of pageScriptInfoBySrc) {
    if (referenceCount <= 0) {
      module?.onDispose?.();
      pageScriptInfoBySrc.delete(src);
    }
  }

  for (const { module } of pageScriptInfoBySrc.values()) {
    module?.onUpdate?.();
  }
}

export function afterWebStarted(blazor) {
  customElements.define('page-script', class extends HTMLElement {
    static observedAttributes = ['src'];

    attributeChangedCallback(name, oldValue, newValue) {
      if (name !== 'src') {
        return;
      }

      this.src = newValue;
      unregisterPageScriptElement(oldValue);
      registerPageScriptElement(newValue);
    }

    disconnectedCallback() {
      unregisterPageScriptElement(this.src);
    }
  });

  blazor.addEventListener('enhancedload', onEnhancedLoad);
}

次の共有 PageScript コンポーネントをサーバー アプリに追加します。

Components/PageScript.razor:

<page-script src="@Src"></page-script>

@code {
    [Parameter]
    [EditorRequired]
    public string Src { get; set; } = default!;
}

Components/Account/Pages/Manage/EnableAuthenticator.razor にある EnableAuthenticator コンポーネント用に、次の併置された JS ファイルを追加します。 onLoad 関数は、Sangmin の qrcode.js ライブラリで、コンポーネントの @code ブロック内の GenerateQrCodeUri メソッドによって生成された QR コード URI を使用して、QR コードを作成します。

Components/Account/Pages/Manage/EnableAuthenticator.razor.js:

export function onLoad() {
  const uri = document.getElementById('qrCodeData').getAttribute('data-url');
  new QRCode(document.getElementById('qrCode'), uri);
}

EnableAuthenticator コンポーネント内の <PageTitle> コンポーネントの下に、併置された JS ファイルへのパスを含む PageScript コンポーネントを追加します。

<PageScript Src="./Components/Account/Pages/Manage/EnableAuthenticator.razor.js" />

Note

PageScript コンポーネントでのアプローチの代わりに使用する方法として、afterWebStartedJS 初期化子に登録されているイベント リスナー (blazor.addEventListener("enhancedload", {CALLBACK})) を使用して、拡張ナビゲーションによって起きるページの更新をリッスンします。 コールバック ({CALLBACK} プレースホルダー) は、QR コード初期化ロジックを実行します。

enhancedload でのコールバック アプローチを使用すると、QR コード <div> がレンダリングされない場合でも、すべての拡張ナビゲーションに対してコードが実行されます。 したがって、QR コードを追加するコードを実行する前に、<div> の存在を確認する追加のコードを加える必要があります。

QR コード命令を含む <div> 要素を削除します。

- <div class="alert alert-info">
-     Learn how to <a href="https://go.microsoft.com/fwlink/?Linkid=852423">enable 
-     QR code generation</a>.
- </div>

ページ内で QR コードを表示する場所と QR コード データが保存される場所の 2 つの <div> 要素を見つけます。

次の変更を行います。

  • 空の <div> に対して、要素に qrCodeid を指定します。
  • data-url 属性を持つ <div> に対して、要素に qrCodeDataid を指定します。
- <div></div>
- <div data-url="@authenticatorUri"></div>
+ <div id="qrCode"></div>
+ <div id="qrCodeData" data-url="@authenticatorUri"></div>

EnableAuthenticator コンポーネントの GenerateQrCodeUri メソッド内のサイト名を変更します。 既定値は Microsoft.AspNetCore.Identity.UI です。 ユーザーが認証アプリで他のアプリの他の QR コードと並べて簡単に識別できるように、値をわかりやすいサイト名に変更します。 値の URL はエンコードされたままにします。 開発者は通常、会社の名前と一致するサイト名を設定します。 例: Yahoo、Amazon、Etsy、Microsoft、Zoho。

次の例では、{SITE NAME} プレースホルダーがサイト (会社) 名の箇所です。

private string GenerateQrCodeUri(string email, string unformattedKey)
{
    return string.Format(
        CultureInfo.InvariantCulture,
        AuthenticatorUriFormat,
-       UrlEncoder.Encode("Microsoft.AspNetCore.Identity.UI"),
+       UrlEncoder.Encode("{SITE NAME}"),
        UrlEncoder.Encode(email),
        unformattedKey);
}

アプリを実行し、QR コードがスキャン可能であること、コードが検証されることを確認します。

参照元の EnableAuthenticator コンポーネント

参照元で EnableAuthenticator コンポーネントを検査できます。

参照元の EnableAuthenticator コンポーネント

Note

通常、.NET 参照ソースへのドキュメント リンクを使用すると、リポジトリの既定のブランチが読み込まれます。このブランチは、.NET の次回リリースに向けて行われている現在の開発を表します。 特定のリリースのタグを選択するには、[Switch branches or tags](ブランチまたはタグの切り替え) ドロップダウン リストを使います。 詳細については、「ASP.NET Core ソース コードのバージョン タグを選択する方法」 (dotnet/AspNetCore.Docs #26205) を参照してください。

その他のリソース