Power Automateだけで作るなんちゃってワンタイムパスワードフォーム

# はじめに
大尊敬する[Matthew Devaney](https://www.linkedin.com/in/matthew-devaney/)さんがLinkedInで、Power AutomateでホストしたHTMLフォームを再現されていました。
ただただ圧巻です。Power AutomateクラウドフローでHTMLを返すと、フォームを起動させられるようです。

先日検証結果を動画にしました。
<blockquote class="twitter-tweet" data-media-max-width="560"><p lang="ja" dir="ltr">最近の実験結果🐟<br>Power Automateを使ったHTMLフォーム &amp; 疑似的なワンタイムパスワード認証。<br>詳細はQiitaに書く予定。<a href="https://twitter.com/hashtag/PowerAutomate?src=hash&amp;ref_src=twsrc%5Etfw">#PowerAutomate</a> <a href="https://twitter.com/hashtag/Qiita?src=hash&amp;ref_src=twsrc%5Etfw">#Qiita</a> <a href="https://t.co/rVyEaGbLhc">pic.twitter.com/rVyEaGbLhc</a></p>&mdash; 出戻りガツオ🐟 Microsoft MVP (@DemodoriGatsuo) <a href="https://twitter.com/DemodoriGatsuo/status/1870481604310892868?ref_src=twsrc%5Etfw">December 21, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

今回はその内容を踏まえて、`HTMLフォーム` > `パスワード認証` > `ワンタイムパスワード認証` > `投稿`までの一連の流れを`Power Automate`で再現したいと思います。

# 一連の流れ
今回必要になるのは、合計で5つのフローです。
いずれも`HTTP 要求の受信時`にトリガーするフローとなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/d81f0e0d-0ca3-3374-3475-fc702afd539c.png)

こちらのトリガーのレファレンスは、`Power Automate`で検索するとヒットしにくいですが、`Azure Logic Apps`で検索すると、[下記の記事](https://learn.microsoft.com/ja-jp/azure/connectors/connectors-native-reqres?tabs=consumption)がヒットします。

https://learn.microsoft.com/ja-jp/azure/connectors/connectors-native-reqres?tabs=consumption

:::note warn
このフローは`プレミアムコネクタ`が使えるライセンスを必要とします。
:::


必要となるフローは
1. `HTML フォーム`を表示
1. パスワード認証を実施
1. ワンタイムパスワードを送信
1. ワンタイムパスワードを検証
1. 問い合わせの投稿

合計で5つになります。

# 1. `HTML フォーム`を表示
まずはフローの全体像です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/ddd7f9ff-18d4-6883-871d-5d49c2ebda8a.png)

```mermaid
flowchart TD
    Trigger[HTTP 要求の受信時 GET Method]
    
    subgraph Endpoints[エンドポイント設定 作成 アクション]
        MAIN[ENDPOINT_MAIN メインOTPエンドポイント]
        VERIFY[ENDPOINT_VERIFY OTP検証エンドポイント]
        PASSWORD[ENDPOINT_PASSWORD パスワード認証エンドポイント]
        RESPONSE[ENDPOINT_RESPONSE 問い合わせ送信エンドポイント]
    end
    
    Trigger --> Endpoints
    
    Endpoints --> HTML[BASIC_HTML_FORM Composeアクション HTMLフォーム生成]
    
    HTML --> Response[HTTP Response Content-Type: text/html ステータス: 200]
    style Trigger fill:#f9f,stroke:#333
    style HTML fill:#bbf,stroke:#333
    style Response fill:#bfb,stroke:#333
    style Endpoints fill:#fdb,stroke:#333
```

並列となっているアクションは全て[作成アクション](https://learn.microsoft.com/ja-jp/power-automate/data-operations#use-the-compose-action)です。

https://learn.microsoft.com/ja-jp/power-automate/data-operations#use-the-compose-action

:::note info
更新を要しない値は[作成アクション](https://learn.microsoft.com/ja-jp/power-automate/data-operations#use-the-compose-action)で全ていいのではと思います。
[変数の初期化より処理速度も速いようです。](https://fredrikengseth.com/speeding-up-power-automate-replacing-append-to-variable-with-compose-for-efficient-workflows/)

https://fredrikengseth.com/speeding-up-power-automate-replacing-append-to-variable-with-compose-for-efficient-workflows/
:::

並列処理の後に実施する[作成アクション](https://learn.microsoft.com/ja-jp/power-automate/data-operations#use-the-compose-action)で`HTML`、`CSS`、`JavaScript`を全て定義し、応答として返すフローです。

:::note alert
安全性という観点で、このフローは実装に不向きです。
:::

■ [HTTP 要求の受信時 トリガー](https://learn.microsoft.com/ja-jp/azure/connectors/connectors-native-reqres?tabs=consumption)

ほかのフローも同様ですが「フローをトリガーできるユーザー」は全て*だれでも*に設定します。
メソッドは`GET`に設定してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/58192e44-87e7-7936-c5bb-82ccfde62794.png)

:::note info
HTTP URLはフロー保存後に作成されます。
:::

■ 並列処理の[作成アクション](https://learn.microsoft.com/ja-jp/power-automate/data-operations#use-the-compose-action)
※こちらは全てほかのフローをキックするURLです。
このような機密性の高い情報は`セキュア入力`を`オン`にすることを推奨します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/f033dfcc-8124-7859-afc0-12ca57769864.png)

■ フォームやスクリプトを定義する[作成アクション](https://learn.microsoft.com/ja-jp/power-automate/data-operations#use-the-compose-action)

下記を作成アクションに設定します。
<details><summary>長いので折りたたみ式にします</summary>

```html:作成アクションに設定する値
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Login and Onetime password!!</title>
    <!-- Font Awesome -->
    <link
      rel="stylesheet"
      href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
    />
    <style>
      body {
        font-family: sans-serif;
        max-width: 600px;
        margin: 0 auto;
        padding: 20px;
        background-color: #f5f5f5;
      }
      .card {
        background: white;
        padding: 30px;
        border-radius: 12px;
        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
      }
      .form-title {
        text-align: center;
        color: #2c3e50;
        margin-bottom: 30px;
        font-size: 24px;
      }
      .form-group {
        margin-bottom: 20px;
        position: relative;
      }
      label {
        display: block;
        margin-bottom: 8px;
        font-weight: bold;
        color: #2c3e50;
        font-size: 14px;
      }
      .input-group {
        position: relative;
        display: flex;
        align-items: center;
      }
      .input-icon {
        position: absolute;
        left: 12px;
        color: #7f8c8d;
        font-size: 16px;
      }
      input[type="text"],
      input[type="email"],
      input[type="password"],
      input[type="number"],
      textarea {
        width: 100%;
        padding: 12px 12px 12px 40px;
        border: 2px solid #e0e0e0;
        border-radius: 8px;
        box-sizing: border-box;
        font-size: 16px;
        transition: all 0.3s ease;
      }
      input:focus,
      textarea:focus {
        outline: none;
        border-color: #3498db;
        box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
      }
      textarea {
        min-height: 120px;
        resize: vertical;
      }
      button {
        background-color: #3498db;
        color: white;
        padding: 14px 20px;
        border: none;
        border-radius: 8px;
        cursor: pointer;
        font-size: 16px;
        width: 100%;
        transition: background-color 0.3s ease;
      }
      button:hover {
        background-color: #2980b9;
      }
      button:disabled {
        background-color: #bdc3c7;
        cursor: not-allowed;
      }
      .error {
        color: #e74c3c;
        font-size: 14px;
        margin-top: 5px;
        display: none;
      }
      .success {
        color: #27ae60;
        font-size: 14px;
        margin-top: 5px;
        display: none;
        padding: 10px;
        background-color: #d4edda;
        border-radius: 4px;
      }
      .loading {
        text-align: center;
        margin: 10px 0;
        display: none;
        color: #7f8c8d;
      }
      #otpForm {
        display: none;
      }
      .otp-input {
        font-size: 24px;
        letter-spacing: 0.5em;
        text-align: center;
        padding-left: 0.25em;
      }
      .timer {
        text-align: center;
        margin: 15px 0;
        color: #7f8c8d;
        font-size: 16px;
      }
      .info-message {
        background-color: #ebf5fb;
        border: 2px solid #3498db;
        color: #2980b9;
        padding: 15px;
        margin: 15px 0;
        border-radius: 8px;
        font-size: 14px;
        line-height: 1.5;
      }
      .loading .spinner {
        animation: spin 1s infinite linear;
        display: inline-block;
      }
      @keyframes spin {
        from {
          transform: rotate(0deg);
        }
        to {
          transform: rotate(360deg);
        }
      }
    </style>
  </head>
  <body>
    <div class="card">
      <h1 class="form-title">Form</h1>

      <!-- メインフォーム -->
      <form id="loginContactForm">
        <div class="form-group">
          <label for="email">Mail</label>
          <div class="input-group">
            <i class="input-icon fas fa-envelope"></i>
            <input type="email" id="email" name="email" required />
          </div>
          <div class="error" id="emailError"></div>
        </div>

        <div class="form-group">
          <label for="password">Password</label>
          <div class="input-group">
            <i class="input-icon fas fa-lock"></i>
            <input type="password" id="password" name="password" required />
          </div>
          <div class="error" id="passwordError"></div>
        </div>

        <div class="form-group">
          <label for="subject">Subject</label>
          <div class="input-group">
            <i class="input-icon fas fa-heading"></i>
            <input type="text" id="subject" name="subject" required />
          </div>
          <div class="error" id="subjectError"></div>
        </div>

        <div class="form-group">
          <label for="message">Message</label>
          <div class="input-group">
            <i class="input-icon fas fa-comment" style="top: 12px"></i>
            <textarea id="message" name="message" required></textarea>
          </div>
          <div class="error" id="messageError"></div>
        </div>

        <button type="submit" id="submitButton">
          <i class="fas fa-paper-plane"></i> Submit
        </button>
      </form>

      <!-- OTP確認フォーム -->
      <form id="otpForm">
        <div class="info-message">
          <i class="fas fa-info-circle"></i>
          確認コードをメールアドレスに送信しました。<br />
          3分以内に入力してください。
        </div>
        <div class="timer" id="timer">
          <i class="fas fa-clock"></i> <span></span>
        </div>
        <div class="form-group">
          <label for="otp">ワンタイムパスワード</label>
          <div class="input-group">
            <i class="input-icon fas fa-key"></i>
            <input
              type="number"
              id="otp"
              name="otp"
              class="otp-input"
              minlength="6"
              maxlength="6"
              required
            />
          </div>
          <div class="error" id="otpError"></div>
        </div>
        <button type="submit"><i class="fas fa-check"></i> 確認</button>
      </form>

      <div class="loading" id="loading">
        <i class="fas fa-circle-notch spinner"></i> 送信中...
      </div>
      <div class="success" id="successMessage">
        <i class="fas fa-check-circle"></i> 送信が完了しました。
      </div>
      <div class="error" id="generalError"></div>
    </div>

    <script>
      // フォームの参照を保持
      const loginForm = document.getElementById("loginContactForm");
      const otpForm = document.getElementById("otpForm");
      const submitButton = document.getElementById("submitButton");
      const loading = document.getElementById("loading");
      const generalError = document.getElementById("generalError");
      const successMessage = document.getElementById("successMessage");

      // セッション情報を保持
      let sessionData = {
        email: "",
        subject: "",
        message: "",
        sessionId: "",
      };

      // メインフォームの送信処理
      loginForm.addEventListener("submit", async function (e) {
        e.preventDefault();
        resetErrors();

        const formData = {
          email: document.getElementById("email").value,
          password: document.getElementById("password").value,
          subject: document.getElementById("subject").value,
          message: document.getElementById("message").value,
        };

        if (!validateForm(formData)) {
          return;
        }

        submitButton.disabled = true;
        loading.style.display = "block";

        try {
          // 1. パスワード認証
          const authResponse = await fetch(
            "@{outputs('ENDPOINT_PASSWORD')}",
            {
              method: "POST",
              headers: {
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                email: formData.email,
                password: formData.password,
              }),
            }
          );

          if (!authResponse.ok) {
            throw new Error("認証に失敗しました");
          }

          const authResult = await authResponse.json();

          if (!authResult.authenticated) {
            throw new Error(
              "メールアドレスまたはパスワードが正しくありません。"
            );
          }

          // セッションIDの生成
          const currentSessionId = generateSessionId();

          // 2. OTP生成・送信リクエスト
          const otpResponse = await fetch(
            "@{outputs('ENDPOINT_MAIN')}",
            {
              method: "POST",
              headers: {
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                email: formData.email,
                sessionId: currentSessionId,
              }),
            }
          );

          if (!otpResponse.ok) {
            throw new Error("確認コードの送信に失敗しました");
          }

          // セッションデータの保存
          sessionData = {
            email: formData.email,
            subject: formData.subject,
            message: formData.message,
            sessionId: currentSessionId,
          };

          // メインフォームを非表示にし、OTP入力フォームを表示
          loginForm.style.display = "none";
          otpForm.style.display = "block";
          startOtpTimer(180); // 3分のタイマーを開始
        } catch (error) {
          generalError.textContent = error.message;
          generalError.style.display = "block";
        } finally {
          submitButton.disabled = false;
          loading.style.display = "none";
        }
      });

      // OTP検証フォームの送信処理
      otpForm.addEventListener("submit", async function (e) {
        e.preventDefault();
        resetErrors();

        const otpInput = document.getElementById("otp").value;
        loading.style.display = "block";

        try {
          // OTP検証リクエスト
          const verifyResponse = await fetch(
            "@{outputs('ENDPOINT_VERIFY')}",
            {
              method: "POST",
              headers: {
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                email: sessionData.email,
                otp: otpInput,
                sessionId: sessionData.sessionId,
              }),
            }
          );

          if (!verifyResponse.ok) {
            throw new Error("確認コードの検証に失敗しました");
          }

          const verifyResult = await verifyResponse.json();

          if (!verifyResult.verified) {
            throw new Error("確認コードが正しくありません");
          }

          // 問い合わせデータの送信
          const inquiryResponse = await fetch(
            "@{outputs('ENDPOINT_RESPONSE')}",
            {
              method: "POST",
              headers: {
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                email: sessionData.email,
                subject: sessionData.subject,
                message: sessionData.message,
                timestamp: new Date().toISOString(),
              }),
            }
          );

          if (!inquiryResponse.ok) {
            throw new Error("問い合わせの送信に失敗しました");
          }

          // 成功メッセージの表示
          successMessage.style.display = "block";
          this.style.display = "none";
          clearInterval(timerInterval);
        } catch (error) {
          generalError.textContent = error.message;
          generalError.style.display = "block";
        } finally {
          loading.style.display = "none";
        }
      });

      // OTPタイマー管理
      let timerInterval = null;

      function startOtpTimer(duration) {
        const timerDisplay = document.getElementById("timer");
        let timer = duration;

        clearInterval(timerInterval);
        timerInterval = setInterval(() => {
          const minutes = Math.floor(timer / 60);
          const seconds = timer % 60;

          timerDisplay.textContent = `残り時間: ${minutes}:${seconds
            .toString()
            .padStart(2, "0")}`;

          if (--timer < 0) {
            clearInterval(timerInterval);
            timerDisplay.textContent = "有効期限が切れました";
            otpForm.style.display = "none";
            loginForm.style.display = "block";
            generalError.textContent =
              "確認コードの有効期限が切れました。もう一度お試しください。";
            generalError.style.display = "block";
          }
        }, 1000);
      }

      // セッションID生成
      function generateSessionId() {
        return (
          "session-" +
          Date.now() +
          "-" +
          Math.random().toString(36).substr(2, 9)
        );
      }

      // バリデーション
      function validateForm(formData) {
        let isValid = true;

        if (!formData.email || !isValidEmail(formData.email)) {
          document.getElementById("emailError").textContent =
            "有効なメールアドレスを入力してください。";
          document.getElementById("emailError").style.display = "block";
          isValid = false;
        }

        if (!formData.password || formData.password.length < 8) {
          document.getElementById("passwordError").textContent =
            "パスワードは8文字以上で入力してください。";
          document.getElementById("passwordError").style.display = "block";
          isValid = false;
        }

        if (!formData.subject || formData.subject.trim().length === 0) {
          document.getElementById("subjectError").textContent =
            "タイトルを入力してください。";
          document.getElementById("subjectError").style.display = "block";
          isValid = false;
        }

        if (!formData.message || formData.message.trim().length === 0) {
          document.getElementById("messageError").textContent =
            "問い合わせ内容を入力してください。";
          document.getElementById("messageError").style.display = "block";
          isValid = false;
        }

        return isValid;
      }

      // メールアドレスのバリデーション
      function isValidEmail(email) {
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        return emailRegex.test(email);
      }

      // エラーメッセージのリセット
      function resetErrors() {
        const errorElements = document.getElementsByClassName("error");
        Array.from(errorElements).forEach((element) => {
          element.style.display = "none";
          element.textContent = "";
        });
        successMessage.style.display = "none";
        generalError.style.display = "none";
      }
    </script>
  </body>
</html>
```

</details>

:::note info
Claudeに作ってもらいました。
:::

まるっとスクリプトを含めて確認しています。

- メインフォームの送信処理
- ワンタイムパスワードの送信
- ワンタイムパスワードの検証
- 検証後の処理

上記のURLは前述の作成アクションから値を渡して設定します。

### フォームで実行している処理のイメージ

```mermaid
sequenceDiagram
    actor User
    participant Form as HTML Form
    participant Auth as Password Auth
    participant OTP as OTP Service
    participant Verify as OTP Verify
    participant Response as Response Service

    User->>Form: アクセス
    Form->>User: フォーム表示

    %% メインフォーム送信フロー
    User->>Form: メールアドレス<br/>パスワード<br/>件名<br/>メッセージ入力
    Form->>Auth: パスワード認証リクエスト
    Auth-->>Form: 認証結果

    alt 認証成功
        Form->>OTP: OTPリクエスト
        OTP-->>User: OTPをメール送信
        Form->>User: OTP入力フォーム表示

        %% OTP検証フロー
        User->>Form: OTP入力
        Form->>Verify: OTP検証リクエスト
        
        alt OTP検証成功
            Verify-->>Form: 検証OK
            Form->>Response: 問い合わせデータ送信
            Response-->>Form: 送信完了
            Form->>User: 完了メッセージ表示
        else OTP検証失敗
            Verify-->>Form: 検証エラー
            Form->>User: エラーメッセージ表示
        end

    else 認証失敗
        Form->>User: エラーメッセージ表示
    end

    note over Form: タイムアウト(3分)で<br/>メインフォームに戻る
```

それぞれ並んでいるハコ`HTML Form`、`Password Auth`、`OTP Service`、`OTP Verify`、`Response Service`の機能をPower Automateで担っています。

フローの最後の`応答`は下記の値で返します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/3868edcd-29c4-73e3-8e95-6c21cabdeee5.png)

`Status Code` : 200

```json: Headers
{
  "Content-Type": "text/html"
}
```

`Body`: `@{outputs('BASIC_HTML_FORM')}`

まったくエラー処理が加味されていないですね。
応答で返す`Content-Type`だけポイントというところです。

# 2. パスワードの認証
実装方法は複数あると思います。
`SQL Server`や`Dataverse`といった領域に保存し、認証を実施する方式です。
今回は複数人に公開する意図もないため、割愛します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/98992fa5-db79-ec27-10bc-92aee08fac2f.png)

トリガーの`スキーマ検証`をオンにして、フロー間の対話の形式だけ決めておきましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/f7957651-c99e-0f79-fb80-04d3e7e1baa6.png)

`Method`: `POST`

```json:Schema
{
    "type": "object",
    "properties": {
        "email": {
            "type": "string"
        },
        "PASSWORD": {
            "type": "string"
        }
    }
}
```

HTMLフォームの処理を継続させるためには、`応答`で下記を返すことが必要です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/25886c3b-ba7e-476a-f2bb-0b6354bf082c.png)

`Status Code`: 200

```json:Headers
{
  "Content-Type": "application/json"
}
```

```json: Body
{
  "authenticated": true
}
```

`authenticated`が`true`である場合に後続の処理を走らせます。

:::note warn
繰り返しになりますが実装は一切想定していないです。
:::

# 3. ワンタイムパスワードの作成と送信
ワンタイムパスワードの作成と送信を`Power Automate`で実行します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/a0510275-e891-5252-cd3d-f1f57b56c516.png)

■　トリガー
トリガーされる段階で、`email`と`sessionId`をフォームから受け取ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/2dfdc415-4c1e-7115-f453-6d36690a506f.png)

`Method`: `POST`

```json:Schema
{
    "type": "object",
    "properties": {
        "email": {
            "type": "string"
        },
        "sessionId": {
            "type": "string"
        }
    }
}
```

`Power Automate`の式でランダムな6桁数値を作ります。
[formatNumber 関数](https://learn.microsoft.com/ja-jp/azure/logic-apps/workflow-definition-language-functions-reference#formatNumber)とramdom関数を使います。

```plaintext:ワンタイムパスワードの作成
formatNumber(rand(0, 999999), 'd6')
```

https://learn.microsoft.com/ja-jp/azure/logic-apps/workflow-definition-language-functions-reference#formatNumber

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/8996b60d-4460-24ca-0f5d-ace80e3e6a78.png)

■ ワンタイムパスワードの通知
SharePointの`REST API`でメールを送信します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/f278ff44-c4e6-df3d-0002-457b5675543d.png)

このような形で`SharePoint Online`サービスから`noReply`でメールが送信できるのですが、こちら残念ながら引退してしまいます。

https://devblogs.microsoft.com/microsoft365dev/retirement-of-the-sharepoint-sendemail-api/

悲しい。こちら個人的に好きなのですが、現状の設定値を記載します。

- サイトのアドレス: `@{parameters('SP_SITE (gtai_SP_SITE)')}`
- Method: `POST`
- URI: `_api/SP.Utilities.Utility.SendEmail`

```json:Headers
{
  "Accept": "application/json;odata=verbose",
  "Content-Type": "application/json;odata=verbose"
}
```

```json:Body
{
  "properties": {
    "To": {
      "results": ["@{triggerBody()?['email']}"]
    },
    "Subject": "ワンタイムパスワード",
    "Body": "@{outputs('OTP')}"
  }
}
```

引退しないでほしい。と思っていますが諸行無常、致し方なしです。

■ ワンタイムパスワードの保存
[項目の作成](https://learn.microsoft.com/ja-jp/sharepoint/dev/business-apps/power-automate/sharepoint-connector-actions-triggers#create-item)

https://learn.microsoft.com/ja-jp/sharepoint/dev/business-apps/power-automate/sharepoint-connector-actions-triggers#create-item

ワンタイムパスワードは`SharePoint Lists`に一時的に保存します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/5df88d1e-bf3d-4430-c247-86ec277234cd.png)

`sessionId`と作成した`ワンタイムパスワード`を後ほど検証で使います。

そして`応答`です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/d2713f9d-8016-b8a8-6cb0-cee2afcf6a7c.png)

ここは後続のステップに、このフローでは関係しないため割愛します。

# 4. ワンタイムパスワードの検証
まずはトリガーです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/fbf0033f-4f61-9e47-1722-233a1cf91993.png)

```json:Schema
{
    "type": "object",
    "properties": {
        "sessionId": {
            "type": "string"
        },
        "otp": {
            "type": "string"
        },
        "formData": {
            "type": "object",
            "properties": {
                "name": {
                    "type": "string"
                },
                "email": {
                    "type": "string"
                },
                "subject": {
                    "type": "string"
                },
                "message": {
                    "type": "string"
                },
                "sessionId": {
                    "type": "string"
                }
            }
        }
    }
}
```

[複数の項目の取得](https://learn.microsoft.com/ja-jp/sharepoint/dev/business-apps/power-automate/guidance/working-with-get-items-and-get-files)を使って、`sessionId`の有無を調べます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/1aa73808-2f88-5e6e-709d-9dd7191510fa.png)

```plaintext:フィルタークエリ
sessionId eq '@{triggerBody()?['sessionId']}' and otp eq '@{triggerBody()?['otp']}'
```

`sessionId`と`otp`が両方マッチするアイテム数を返します。
そちらをカウントする関数が下記です。

```plaintext:作成
length(outputs('複数の項目の取得')?['body/value'])
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/4d50b1ce-d71d-ae82-74bc-015362b9f50c.png)

配列の要素数が`0`以上であれば、対象のレコードありのため、認証を進めましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/09de8b70-68d8-46b4-867d-71fc362908c3.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/ba06ae86-8581-d75e-9764-950fa2913c66.png)

応答の本文である`verified`が`true`である場合に、後続の処理が走ります。

# 5. 認証後の処理
HTMLフォームに入力された値を`アダプティブカード`として投稿します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/f564f63e-c81f-acd8-043c-b70d4019ffe7.png)


```plaintext:アダプティブカード
{
  "type": "AdaptiveCard",
  "version": "1.4",
  "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
  "body": [
    {
      "type": "Container",
      "style": "emphasis",
      "items": [
        {
          "type": "ColumnSet",
          "columns": [
            {
              "type": "Column",
              "width": "stretch",
              "items": [
                {
                  "type": "TextBlock",
                  "text": "新規問い合わせ",
                  "weight": "Bolder",
                  "size": "Large",
                  "color": "Accent"
                }
              ]
            }
          ]
        }
      ]
    },
    {
      "type": "Container",
      "items": [
        {
          "type": "FactSet",
          "facts": [
            {
              "title": "送信日時",
              "value": "@{convertTimeZone(triggerBody()?['timestamp'],'UTC','Tokyo Standard Time')}"
            }
          ]
        }
      ]
    },
    {
      "type": "Container",
      "style": "default",
      "items": [
        {
          "type": "TextBlock",
          "text": "件名",
          "weight": "Bolder",
          "size": "Medium",
          "color": "Accent"
        },
        {
          "type": "TextBlock",
          "text": "@{triggerBody()?['subject']}",
          "wrap": true,
          "spacing": "Small"
        },
        {
          "type": "TextBlock",
          "text": "問い合わせ内容",
          "weight": "Bolder",
          "size": "Medium",
          "color": "Accent",
          "spacing": "Medium"
        },
        {
          "type": "TextBlock",
          "text": "@{triggerBody()?['message']}",
          "wrap": true,
          "spacing": "Small"
        }
      ]
    }
  ],
  "actions": [
    {
      "type": "Action.OpenUrl",
      "title": "返信する",
      "url": "mailto:${email}?subject=Re: ${subject}",
      "style": "positive"
    },
    {
      "type": "Action.ShowCard",
      "title": "メモを残す",
      "card": {
        "type": "AdaptiveCard",
        "body": [
          {
            "type": "Input.Text",
            "placeholder": "メモを入力してください",
            "id": "comment",
            "isMultiline": true
          }
        ],
        "actions": [
          {
            "type": "Action.Submit",
            "title": "保存",
            "style": "positive"
          }
        ]
      }
    }
  ]
}
```

:::note info
マスクしていますが、フォームに入力された値は、メールアドレス含め全て使えます。
:::

最終的にアダプティブカードとして投稿します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/584073a9-d23f-3ff5-c2c6-a6fe4c1c5e82.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/a8221114-ca41-e932-3eee-da67e1018794.png)

::: note info
アダプティブカードの応答によって、後続の処理を実施することも可能です。
:::

# おわりに
動画を見て「うわーすごい！！」と感動して、がーっとやってしまいました！
使う場面はなさそうですが、こういうことがいつか生きると信じて・・・、**Power Lifeを楽しみましょう！**