ワンタイムパスワードのサンプルを作成してみた

# 概要
多要素認証を実装する時OTP(ワンタイムパスワード)を使用したい.  
とりあえずゴリゴリ実装してみたのでその記録

# とりあえずソースコード
自分の過去の資料を見てもらえればわかるかもしれないが,React,TypeScriptで実装している.  
自分もよくわかってないので,説明不足なところはご勘弁.  

<details><summary>たためるようにしておきました.</summary>

```javascript
import * as speakeasy from 'speakeasy';
import QRCode from 'qrcode';
import React, { useEffect, useState } from 'react';

function OTPTest() {
    const [QRCodeDisplay, setQRCodeDisplay] = useState('');
    const [PassWord, setPassWord] = useState('');
    const [verificationResult, setVerificationResult] = useState('認証実行前');

    const test = async () => {
        console.log('test実行中...');
        const secret = generateSecret();
        try {
            await generateQRCodeASCII(secret);
            console.log('QRコードが正常に生成されました。');
        } catch (error) {
            console.error('QRコード生成中にエラーが発生しました:', error);
        }
    };

    const generateSecret = () => {
        const secret = speakeasy.generateSecret({ length: 20 });
        // DBに登録する必要がある. 以下はテスト用のダミーデータ
        const test_mode = true;
        if(test_mode) {
            return {
                ascii: '!{[L.wsBzx2}f}%?UqZW', // 基本これしか使わない
                hex: '217b5b4c2e7773427a78327d667d253f55715a57',
                base32: 'EF5VWTBOO5ZUE6TYGJ6WM7JFH5KXCWSX',
                otpauth_url: 'otpauth://totp/SecretKey?secret=EF5VWTBOO5ZUE6TYGJ6WM7JFH5KXCWSX'
            };
        }
        return secret;
    };

    const getSecret = (userId:string) => {
        // DBからsecretを取得する
        // 以下はテスト用のダミーデータ
        return {
            ascii: '!{[L.wsBzx2}f}%?UqZW', // 基本これしか使わない
            hex: '217b5b4c2e7773427a78327d667d253f55715a57',
            base32: 'EF5VWTBOO5ZUE6TYGJ6WM7JFH5KXCWSX',
            otpauth_url: 'otpauth://totp/SecretKey?secret=EF5VWTBOO5ZUE6TYGJ6WM7JFH5KXCWSX'
        };
    }

    const generateQRCodeASCII = async (secret: speakeasy.Key) => {
        const otpauthUrl = speakeasy.otpauthURL({
            secret: secret.ascii,
            label: encodeURIComponent('Your App Name:User Email'),
            issuer: 'Your App Name',
            algorithm: 'sha1',
            digits: 6,
            period: 30,
        });

        try {
            const url = await QRCode.toDataURL(otpauthUrl);
            console.log(url);
            setQRCodeDisplay(url); // Update QRCodeDisplay state
            console.log("QRCodeDisplayの表示");
            console.log(QRCodeDisplay);
        } catch (err) {
            console.error('QR Code生成エラー:', err);
        }
    };

    // 二要素認証コードを検証する関数
    const verifyToken = () => {
        // 秘密鍵を取得する（本来はデータベースから取得します）
        const secret = getSecret("dummy"); // この関数は、実際のアプリケーションではユーザーIDに基づいてユーザーの秘密鍵をデータベースから取得する必要があります。

        // speakeasyを使用してトークンの検証を試みる
        const verified = speakeasy.totp.verify({
            secret: secret.ascii,
            encoding: 'ascii',
            token: PassWord,
            window: 1 // ここで指定した範囲内であれば前後のトークンも有効とみなすことができます。
        });

        if (verified) {
            setVerificationResult('認証成功');
        } else {
            setVerificationResult('認証失敗');
        }
    };

    return (
        <div>
            <button onClick={test}>Generate QR Code</button>
            <div>
                {QRCodeDisplay && <img src={QRCodeDisplay} alt="Generated QR Code" style={{ marginTop: '20px' }} />}
            </div>
            <div>
                <input
                    type="text"
                    value={PassWord}
                    onChange={(e) => setPassWord(e.target.value)}
                    placeholder='Enter your 6-digit code here...'
                />
                <button onClick={verifyToken}>Verify Token</button>
            </div>
            <div>{verificationResult}</div>
        </div>
    );
}

export default OTPTest;

```
</details>

# 表示結果
まずは`Generate QR Code`でワンタイムパスワードの登録を行います.  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1505183/bb4dfffb-ab07-4906-68bc-c52eb3048a32.png)

下記のQRコードを`Microsoft Authenticator`で読込み,個人端末にてワンタイムパスワードを使用できるように設定します.  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1505183/cb250948-fb79-5b6f-b7e9-3ccc886d4b20.png)

`Microsoft Authenticator`では下記のように表示されます.  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1505183/dd4a2b54-5f70-7dc1-f3d6-ab872ec2e24b.png)

現在書かれているワンタイムパスワードを入力してみます.  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1505183/2f38de92-6253-57fa-af10-61ae8df1aac0.png)

時間が過ぎてしまい,認証に失敗しました.  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1505183/ffc9d705-f654-87e5-6644-0c12ac873d87.png)

今度こそ時間内に入力してみます.  
うまくいきました！  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1505183/267c4afc-ff60-21da-e796-08abe82aaaab.png)