コンテンツにスキップ

Mail OTP

Mail OTP(ワンタイムパスワード)は、ユーザーのメールアドレスに一時的なコードを送信するパスワードレス認証方式です。ユーザーがパスワードを覚える必要がなく、強固なセキュリティを維持できます。

概要

sequenceDiagram
    participant User as ユーザー
    participant Authrim
    participant Email as メールサービス (SMTP)

    User->>Authrim: メールアドレスを入力
    Authrim->>Authrim: OTPコードを生成
    Authrim->>Email: OTPメールを送信
    Email->>User: OTPコードを配信
    User->>Authrim: OTPコードを入力
    Authrim->>Authrim: コードを検証
    Authrim->>User: トークンを発行

特徴

  • パスワードレス: パスワードの記憶や管理が不要
  • フィッシング耐性: OTPコードは時間制限付きで使い捨て
  • ユニバーサル: メールにアクセスできるすべてのデバイスで動作
  • アプリ不要: TOTPと異なり、認証アプリが不要
  • 馴染みのあるUX: ユーザーはメールベースの認証に慣れている

設定

環境変数

Terminal window
# メールサービス設定
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_PASSWORD=your-smtp-password
SMTP_FROM_NAME=Authrim
# OTP設定
OTP_LENGTH=6 # 桁数(デフォルト: 6)
OTP_EXPIRY_SECONDS=300 # 有効期間(デフォルト: 5分)
OTP_MAX_ATTEMPTS=3 # 最大検証試行回数
OTP_RATE_LIMIT_WINDOW=3600 # レート制限ウィンドウ(秒)
OTP_RATE_LIMIT_MAX=5 # ウィンドウ内の最大OTP数/メールアドレス

クライアント設定

OAuthクライアントでMail OTPを有効化:

{
"client_id": "your-client-id",
"allowed_grant_types": ["authorization_code", "refresh_token"],
"allowed_auth_methods": ["mail_otp", "password", "passkey"],
"mail_otp_config": {
"enabled": true,
"template": "default",
"subject": "ログインコード"
}
}

API使用方法

ステップ1: OTPリクエスト

POST /api/auth/otp/request
Content-Type: application/json
{
"email": "[email protected]",
"client_id": "your-client-id"
}

レスポンス:

{
"success": true,
"message": "OTPをメールに送信しました",
"expires_in": 300,
"otp_id": "otp_abc123..."
}

ステップ2: OTP検証

POST /api/auth/otp/verify
Content-Type: application/json
{
"otp_id": "otp_abc123...",
"code": "123456",
"client_id": "your-client-id"
}

成功レスポンス:

{
"success": true,
"auth_code": "auth_xyz789...",
"redirect_uri": "https://app.example.com/callback"
}

エラーレスポンス:

{
"success": false,
"error": "invalid_otp",
"error_description": "OTPコードが無効または期限切れです",
"attempts_remaining": 2
}

OAuth 2.0統合

Mail OTPはOAuth 2.0認可コードフローとシームレスに統合:

// 1. mail_otpヒント付きで認可を開始
const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('client_id', 'your-client-id');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('redirect_uri', 'https://app.example.com/callback');
authUrl.searchParams.set('scope', 'openid profile email');
authUrl.searchParams.set('acr_values', 'urn:authrim:acr:mail_otp'); // Mail OTPをリクエスト
window.location.href = authUrl.toString();
// 2. ユーザーがメールを受信し、AuthrimのUIでコードを入力
// 3. Authrimが認可コードとともにリダイレクト
// 4. コードをトークンと交換(標準OAuthフロー)

セキュリティ考慮事項

OTP生成

Authrimは暗号学的に安全なOTPコードを生成:

// OTP生成(内部処理)
function generateOTP(length: number = 6): string {
const array = new Uint8Array(length);
crypto.getRandomValues(array);
return Array.from(array)
.map(b => b % 10)
.join('');
}

レート制限

悪用を防ぐため、Mail OTPは複数のレート制限を実装:

制限デフォルト説明
メールアドレス毎5/時間メールアドレス毎の最大OTPリクエスト数
IP毎20/時間IPアドレス毎の最大OTPリクエスト数
グローバル1000/時間全ユーザーでの最大OTPリクエスト数
検証試行3回OTP毎の最大試行回数

ブルートフォース保護

// 試行追跡付き検証
async function verifyOTP(otpId: string, code: string): Promise<VerifyResult> {
const otp = await getOTP(otpId);
if (!otp) {
throw new Error('OTPが見つからないか期限切れです');
}
if (otp.attempts >= MAX_ATTEMPTS) {
await deleteOTP(otpId);
throw new Error('最大試行回数を超えました');
}
if (otp.code !== code) {
await incrementAttempts(otpId);
return {
success: false,
attempts_remaining: MAX_ATTEMPTS - otp.attempts - 1
};
}
// 成功 - OTPを削除してセッションを作成
await deleteOTP(otpId);
return { success: true };
}

ストレージ

OTPレコードはセキュリティを考慮して保存:

CREATE TABLE otp_codes (
id TEXT PRIMARY KEY,
email_blind_index TEXT NOT NULL, -- 検索用ハッシュ化メール
code_hash TEXT NOT NULL, -- ハッシュ化OTPコード
client_id TEXT NOT NULL,
attempts INTEGER DEFAULT 0,
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
used_at INTEGER,
INDEX idx_email_blind (email_blind_index),
INDEX idx_expires (expires_at)
);

注意: OTPコードは保存前にハッシュ化されるため、データベースにアクセスしても実際のコードは分かりません。

メールテンプレート

デフォルトテンプレート

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>ログインコード</title>
</head>
<body style="font-family: sans-serif; padding: 20px;">
<h1>ログインコード</h1>
<p>以下のコードでサインインしてください:</p>
<div style="font-size: 32px; font-weight: bold;
letter-spacing: 8px; padding: 20px;
background: #f5f5f5; text-align: center;">
{{OTP_CODE}}
</div>
<p>このコードは{{EXPIRY_MINUTES}}分で期限切れになります。</p>
<p>このコードをリクエストしていない場合は、このメールを無視してください。</p>
</body>
</html>

カスタムテンプレート

クライアント毎にカスタムテンプレートを設定:

const emailTemplate = {
subject: "{{APP_NAME}} - 認証コード",
html: `
<div style="max-width: 600px; margin: 0 auto;">
<img src="{{LOGO_URL}}" alt="{{APP_NAME}}" width="150">
<h1>{{APP_NAME}}にサインイン</h1>
<p>認証コード:</p>
<code style="font-size: 24px; padding: 10px 20px;
background: #e0f7fa; border-radius: 4px;">
{{OTP_CODE}}
</code>
<p>{{EXPIRY_MINUTES}}分間有効です。</p>
</div>
`,
text: "{{APP_NAME}}の認証コード: {{OTP_CODE}}"
};

ベストプラクティス

  1. 適切な有効期限: 5分はセキュリティと使いやすさのバランスが良い
  2. レート制限の実装: 列挙攻撃とブルートフォース攻撃を防止
  3. OTPコードのハッシュ化: 平文のOTPコードをデータベースに保存しない
  4. 明確なUI: 残り時間を表示し、期限切れ後に再送信を許可
  5. 監査ログ: すべてのOTPリクエストと検証試行を記録
  6. メール配信性: 信頼できるSMTPプロバイダーを使用し、SPF/DKIM/DMARCを設定

他の方式との比較

方式セキュリティ使いやすさ必要なもの
Mail OTPメールアクセス
パスワード記憶
TOTP認証アプリ
Passkey非常に高対応デバイス
SMS OTP電話番号

トラブルシューティング

よくある問題

OTPが届かない:

  • 迷惑メールフォルダを確認
  • SMTP設定を検証
  • レート制限を確認
  • メールアドレスが有効か確認

OTPが期限切れ:

  • 新しいコードをリクエスト
  • システム時刻の同期を確認
  • 有効期限の延長を検討

最大試行回数超過:

  • レート制限ウィンドウがリセットされるまで待機
  • 新しいOTPコードをリクエスト
  • 問題が続く場合はサポートに連絡