Codeer.LowCode.Blazor.Extras.Server の Auth 名前空間。ID/パスワードのログイン (Starter の Cookie テンプレート) に 2 段階目を足す。方式は 2 つ。
| 方式 | 有効にする方法 | 強度・特徴 |
|---|---|---|
| 認証アプリ (TOTP) | ユーザーモジュールの LoginAccountContractField に TOTP の 3 列 (秘密鍵 / 確認済み / 最終タイムステップ) を設定 |
RFC 6238 (HMAC-SHA1 / 30 秒 / 6 桁)。初回ログインで QR を登録。オフラインで動く |
| メールのワンタイムコード | ユーザーモジュールの LoginAccountContractField の TwoFactorEmail に送信先のフィールドを設定 |
6 桁コードをメールで送る。ユーザー側の登録不要で導入が軽い。メールを受け取れる = 本人という前提。列は不要 (コードはサーバーのキャッシュ)。メール送信の設定 (docs/Mail.md) が前提 |
両方あるときは認証アプリが優先される。クライアント (login.html / MAUI の Login.razor) は POST api/account/login の応答の status で 2 段階目の種類を知る。
パスワードが通ると、伏せ字の送信先とコード入力欄に切り替わる (login.html / MAUI の Login.razor 共通)。
- 有効化:
LoginAccountContractField.TwoFactorEmailにメールアドレスのフィールドを設定する (それだけ) - サーバー設定 (appsettings
EmailOtpLogin、すべて任意):MailInfraName(送信インフラの呼び名。空ならMail.DefaultInfraName) /Subject(既定 "認証コード: {code}") /Body({code} と {minutes} が置き換わる) /CodeLifetimeMinutes(既定 10) /MaxAttempts(既定 5。超えるとコード破棄) - 流れ: パスワード成功 → コードを発行してメール送信 →
status: "email"と伏せ字の送信先 (maskedEmail) を返す →TwoFactorCode付きで再送 →ok - コードの期限は発行したときに決まり、打ち間違えても延びない。コードは発行したときの送信先に結び付き、送信先が変わったら通らない
- 再送 = パスワードからやり直す (前のコードは無効になる)。同じコードは 1 回だけ。送れなかったときは
send_failed(サインインしない) - テンプレートは
MailDispatcher経由で送るので、Mail.DebugRedirectAllTo(開発環境の宛先リダイレクト) が効く。履歴モジュールには記録しない (コードを残さない) - コードの置き場は
IDistributedCache(テンプレートはCookieAuthentication.csでAddDistributedMemoryCache())。複数インスタンスでは Redis 等の共有キャッシュに差し替える (docs/ExternalLogin.md の「複数インスタンスの注意」)
ログインの流れの中では PasswordLogin が、パスワードが通った後 (契約に TOTP 列が無いときだけ) EmailOtpLogin.VerifyAsync を呼び、
ok 以外の結果 (email / invalid_code / code_expired / send_failed) をそのままクライアントへ返してサインインしません。メールの送信はテンプレートが MailDispatcher 経由で渡します。
LoginAccountContractField の TOTP 列 + TotpLogin。オーセンティケータアプリ (Google Authenticator / Microsoft Authenticator 等) の 6 桁コード。RFC 6238 (HMAC-SHA1 / 30 秒 / 6 桁)。QR コードは QRCoder。
- 有効・無効と列の場所は デザインで決まる: ユーザーモジュール (
AppSettings.CurrentUserModuleDesignName) のLoginAccountContractFieldに ユーザー行の 3 列 (秘密鍵 / 確認済み / 最終タイムステップ) を設定する。空に戻せば従来のログインに戻る。appsettings に列名の設定は無い - 3 列は契約の列名だけの宣言 (通常のモジュール読み書きには乗らない)。クライアントに秘密鍵が渡ることはない。 ユーザーを画面で編集しても列は触られない
- サインイン (Cookie の発行) はこれまでどおりテンプレートの
AccountController。検証の流れ (パスワード → コード) はパッケージのPasswordLoginが持ち、TotpLoginは「パスワードが通った後にコードを検証する」部分だけで、 表・列名はデザインから引いて SQL で直接読み書きする (ログイン時はまだ認証済みユーザーがいないため、パスワード照合と同じ経路) - 登録は最初のログイン時。パスワードが通ったユーザーに QR を出し、そのコードが合ったときに登録が確定する
- リセットは
MyTotpResetButtonField(本人用。設定画面などどこにでも置ける) /TotpResetButtonField(管理者用。ユーザーモジュールの詳細画面で表示中の行のユーザーを解除。通常の保存と同じ権限で制御) か、3 列を空にする (TotpLogin.ResetAsync)
-
ユーザーテーブルに 3 列を足す
totp_secret TEXT NULL, totp_confirmed INTEGER NULL, totp_last_timestep INTEGER NULL
-
デザイナでユーザーモジュールの
LoginAccountContractFieldに 3 列を割り当てる (契約の説明) -
任意: appsettings でオーセンティケータに表示するアプリ名を変える (既定 "LowCodeApp")
{ "TotpLogin": { "Issuer": "社内ポータル" } }
POST api/account/login が 2 段階になる。1 段階目は { Id, Password }、2 段階目は同じものに TwoFactorCode を足して送る。
応答は { status, secret?, otpauthUri?, qrPngBase64?, maskedEmail? }。
| status | 意味 | クライアントの動き |
|---|---|---|
setup |
(認証アプリ) パスワードは正しいが未登録 | QR (qrPngBase64) と手入力用の鍵 (secret) を表示し、コードを入力させて TwoFactorCode 付きで再送 |
totp |
(認証アプリ) パスワードは正しく登録済み | コードを入力させて TwoFactorCode 付きで再送 |
email |
(メール) コードを送った | 送信先 (maskedEmail) を案内し、コードを入力させて TwoFactorCode 付きで再送 |
invalid_code |
コード不一致 | 入力し直し |
code_expired |
(メール) 有効なコードが無い (期限切れ・使用済み・試行超過で破棄)。総当たり対策の失敗には数えない | パスワードの入力からやり直す (新しいコードが送られる) |
send_failed |
(メール) 送れなかった | 管理者に連絡するよう案内 |
ok |
サインイン済み | アプリへ |
二要素認証が無効なときも応答は { status: "ok" } (以前の空応答から変わっている。login.html / MAUI の Login.razor は両方を受け付ける)。
ログインの流れの中では PasswordLogin が、パスワードが通った後 TotpLogin.VerifyAsync を呼び (契約に TOTP 列が無ければ飛ばす)、
ok 以外の結果 (setup / totp / invalid_code) をそのままクライアントへ返してサインインしません。テンプレートの AccountController.Login はその結果を HTTP に写すだけです。
端末の紛失・機種変更のときは登録を解除します。解除すると次回ログイン時に QR から再登録になります (弱くなる方向ではありません)。どちらのボタンも確認ダイアログ付きです。
| フィールド | 使う人 | 置き場所 | 対象 |
|---|---|---|---|
| TotpResetButtonField (認証アプリ解除ボタン) | 管理者 | ユーザーモジュールの詳細画面 | 表示中の行のユーザー |
| MyTotpResetButtonField (自分の認証アプリ解除ボタン) | 本人 | 設定画面・マイページなど、どのモジュールにも置ける | ログイン中の自分 |
解除は「TOTP の 3 列を空にする」データを持った通常の保存です。そのためモジュールの UserWriteCondition / 行の DataWriteCondition がそのまま効き (その行を編集できる人だけが解除できる)、サーバーに専用の API はありません。
- ボタンを押すとモジュール全体が保存されます。編集中の他のフィールドも一緒に保存されます
- 新規行 (ユーザーがまだ無い) では表示されません
- 3 列は書き込み専用なので、秘密鍵はクライアントに来ず、登録済みかどうかも表示しません (ボタンだけ)
| プロパティ | デザイナ表示名 | 型 | 説明 |
|---|---|---|---|
| Text | ボタンの文字 | string | 空なら「認証アプリを解除」 |
| ConfirmMessage | 確認メッセージ | string | 解除前の確認メッセージ。空なら既定の文言 |
| DbColumnTotpSecret | 認証アプリ: 秘密鍵列 | string | 秘密鍵の列 (書き込み専用。解除で NULL) |
| DbColumnTotpConfirmed | 認証アプリ: 確認済み列 | string | 確認済みの列 (書き込み専用。解除で 0) |
| DbColumnTotpLastTimestep | 認証アプリ: 最終タイムステップ列 | string | 最終タイムステップの列 (書き込み専用。解除で 0) |
3 列は同じモジュールの LoginAccountContractField の TOTP 列と同じ値にしてください。サーバーのログイン処理が見るのは契約の列なので、ボタンが別の列を空にしても解除になりません。デザインチェックが次を指摘します。
TotpResetButtonFieldDesign:1— ログインユーザーモジュール (AppSettings.CurrentUserModuleDesignName) 以外に置いているTotpResetButtonFieldDesign:2— 3 列のどれかが契約の列と違う (同じモジュールに契約が無い・契約に TOTP 列が無いときも指摘される)
登録済みなら「認証アプリ: 登録済み」と解除ボタン、未登録なら「認証アプリ: 未登録」だけを表示します。アプリで認証アプリの二要素認証を使っていない (契約に TOTP の列が無い) ときは何も表示しません。状態は表示時にサーバーへ問い合わせます。
| プロパティ | デザイナ表示名 | 型 | 説明 |
|---|---|---|---|
| Text | ボタンの文字 | string | 空なら「認証アプリを解除」 |
| ConfirmMessage | 確認メッセージ | string | 解除前の確認メッセージ。空なら既定の文言 |
| フィールド | メンバー | 説明 |
|---|---|---|
| TotpResetButtonField | Reset() |
確認の後、表示中の行のユーザーの登録を解除してモジュールを保存する。成功なら true |
| MyTotpResetButtonField | Reset() |
確認の後、自分の登録を解除する。成功なら true |
| MyTotpResetButtonField | IsRegistered |
自分が登録済みか (bool?)。未取得・機能無効なら null |
Starter の Cookie テンプレートの共通の API (Controllers/ExtrasController.cs = POST api/extras) に組み込み済みです。対応表 Extras/ExtrasApiTable.cs に new TotpApi(SystemConfig.Instance.TotpLogin) を並べます。対象は常にログイン中のユーザーです。
| 操作 | 内容 |
|---|---|
Totp.Status |
状態 { Enabled, Registered } を返す (Enabled = 契約に TOTP 列がある) |
Totp.Reset |
自分の 3 列を空にし (TotpLogin.ResetAsync)、解除後の状態を返す |
クライアント側の設定は不要です (共通の API ExtrasTransport.EndPoint = /api/extras に送ります)。送る先が無いときは状態が取れないので、ボタンは何も表示しません。
専用のエンドポイントで受ける場合は、従来どおり TotpResetClient.StatusEndPoint (GET) / ResetEndPoint (POST) の両方に URL を設定すればそちらに送ります。
- 照合窓は現在時刻 ±1 ステップ (30 秒ずれまで許容)
- 一度成功したタイムステップ以下のコードは受け付けない (リプレイ防止)
- 登録が確定するまでは、登録画面を出すたびに新しい鍵を作る (途中で閉じた鍵は使えない = 確定前の鍵を見られても登録に使えない)
- 未登録のユーザーがコードだけ送っても鍵は作らない
- 初回ログイン時の登録: パスワードだけを知る第三者が先に登録できる余地がある。許容できない運用では、初回ログインを管理者立ち会いで行う等で補う
- コードの総当たりは、パスワードと同じ LoginLockout が回数を抑える (コードの不一致もログインの失敗として数え、ロックが繰り返されるたびに長くなる。既定では 1 つのアカウントに最初の 24 時間で 90 回、以降は 1 日 10 回ほど。1 インスタンスのとき)
- リカバリーコードは持たない。必要なら AccountController で足す
- 時刻ずれはサーバーと端末の時計に依存する。サーバーは NTP で合わせておく
同じ API を使う。テンプレートの Pages/Login.razor は setup / totp を受けるとコード入力に切り替わり、setup では QR を表示する。
