0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【FastAPI × Next.js】セキュアな社内掲示板を作ってみた 〜フロントエンド結合とWebAuthn完全実装編〜(開発記録Day6)

0
Posted at

はじめに

こんにちは!サイバーセキュリティやゼロトラストアーキテクチャを学んでいる情報系の大学生です。
現在、7日で「セキュア社内掲示板」をゼロから開発するプロジェクトに挑戦しています。

今日(Day 6)は、これまで作ってきたバックエンド(FastAPI)とフロントエンド(Next.js)を結合し、ついに「パスキー(WebAuthn)認証」を含めた一気通貫のWebアプリケーションを完成させました!

この記事では、結合フェーズで立ちはだかった「あるあるな罠」とその解決策、実装のポイントを初学者向けにわかりやすく解説します。


今週の達成事項

  1. Next.jsからのAPI非同期通信(CRUD処理)の実装
  2. HttpOnly Cookieを使ったセキュアなセッション管理の確立
  3. WebAuthn(FIDO2)ブラウザAPIの呼び出しとバックエンド連携
  4. 新規ユーザー登録・ログアウト機能の実装
  5. 多層防御を意識したフロントエンド・バリデーションの実装

学びとつまずきポイント解説

1. 「Cookieが送られない!?」CORSとCredentialsの罠

ログイン時に発行したJWTを「HttpOnly Cookie」としてブラウザに保存するセキュアな設計にしましたが、いざNext.jsからバックエンド(localhost:8000)のデータを取得しようとすると、401 Unauthorized(認証エラー)で弾かれてしまいました。

原因は、「別ポート(オリジン)へのfetchリクエストでは、デフォルトでCookieが送信されない」というブラウザのセキュリティ仕様です。

解決策

フロントエンドとバックエンドの両方で、明示的な許可設定が必要でした。

【FastAPI側(バックエンド)】
CORS設定で allow_credentials=True を設定する。

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],
    allow_credentials=True, # 必須!
    allow_methods=["*"],
    allow_headers=["*"],
)

【Next.js側(フロントエンド)】
fetch APIのオプションに credentials: 'include' を必ず付与する。

const response = await fetch('http://localhost:8000/posts', {
  method: 'GET',
  credentials: 'include', // ログイン時もデータ取得時も必須!
  headers: { 'Content-Type': 'application/json' },
});

これで無事に、ブラウザに保存されたJWTがバックエンドへ送られるようになりました!


2. WebAuthn実装の最大の壁「ArrayBufferとBase64URLの変換」

今回、パスワードレス認証を実現するために WebAuthn (FIDO2) を実装しました。
ブラウザの指紋認証やWindows Helloを呼び出すためには navigator.credentials.create() などのAPIを使いますが、ここで「型」の壁にぶつかりました。

  • FastAPI(JSON通信): データを「Base64URLの文字列」としてやり取りする。
  • ブラウザ(WebAuthn API): データを「ArrayBuffer(生のバイナリデータ)」として要求する。

そのままデータを渡すとエラーでクラッシュしてしまいます。

解決策

フロントエンド側に、Base64URL ↔ ArrayBuffer を相互変換するユーティリティ関数を自作して挟み込みました。

// バックエンドの文字列を、ブラウザAPI用にバイナリへ変換
export function bufferDecode(value: string): ArrayBuffer {
  const base64 = value.replace(/-/g, '+').replace(/_/g, '/');
  const binary = atob(base64.padEnd(base64.length + (4 - base64.length % 4) % 4, '='));
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
  return bytes.buffer;
}

さらに、バックエンドから送られてきたJSON文字列(options)を JSON.parse() でJavaScriptのオブジェクトに変換してから上記の処理に通すことで、無事に生体認証のポップアップを起動させることができました!


3. 多層防御(Defense in Depth)としてのフロントエンド検証

セキュリティの基本は「サーバー側で厳格に検証する」ことですが、フロントエンドでもバリデーションを行うことで「多層防御」を実現しました。

今回は投稿フォームで「タイトル5文字以上」「本文500文字以内」といったチェックをReact側で実装し、エラー時にはサイバー風のToast(トースト通知)を表示するようにしました。

// フロントエンドでの入力値バリデーション (多層防御の第一層)
if (title.trim().length < 5) {
  setToastConfig({ message: 'タイトルは5文字以上で入力してください。', type: 'error' });
  return;
}

これにより、無駄なAPI通信(リソース消費)を防ぎつつ、ユーザー体験(UX)を向上させることができました。


4. 【失敗談】モックデータと本番DBの繋ぎ忘れ

UIと通信のテストを優先するため、FastAPI側に一時的な「モックデータ(ダミーのリスト)」を返すエンドポイントを作って開発を進めていました。

しかし、いざ投稿テストをしてみると「投稿成功!」と出るのに画面に反映されない…。
原因は、モック実装のままで、PostgreSQL(SQLAlchemy)への INSERT 処理に切り替えるのを忘れていたことでした!

すぐに main.py を修正し、db.add(db_post) で本物のDBへ永続化するように軌道修正。ブラウザをリロードしても自分の投稿が消えずに残っているのを見た瞬間は、フルスタック開発ならではの大きな達成感がありました!


5. UI完成形

① ログイン画面(グラスモーフィズムUI)
ログイン画面.png
Tailwind CSSで実装したサイバーテイストなUI。通常のパスワード認証に加え、下部には生体認証(FIDO2)への導線を配置しています。

② パスキー(生体認証)ポップアップ
生体認証.png
WebAuthn APIが呼び出され、デバイスの認証ダイアログが起動した瞬間。裏側ではFastAPIとの間で複雑なチャレンジ&レスポンスの暗号署名検証が行われています。

③ ダッシュボード画面
ダッシュボード.png
ログインに成功したユーザーのみが入れるメイン画面。画面右上に現在のユーザーIDを表示し、各投稿に対して「自分に権限があるか(BOLA対策)」をバックエンドで厳密に検証しています。

④ 新規投稿画面(多層防御のフロントエンド)
投稿画面.png
直感的に操作できる投稿フォーム。文字数などのバリデーションをフロントエンド側でも行い、無駄な通信を防ぐ「多層防御」の第一層として機能しています。


まとめと次回の予告

Week 6にて、以下の「ハッピーパス(正常系の一気通貫フロー)」が完成しました!

  1. 新規ユーザー登録
  2. パスワードログイン & WebAuthn(パスキー)登録
  3. パスキーでの生体認証ログイン
  4. 掲示板へのセキュアなデータ投稿・取得
  5. 安全なログアウト(Cookie削除)

さて、これで「完璧に動くアプリケーション」が完成しましたが、セキュリティ専攻としてはここからが本番です。

次回、最終日(Day 7)では、完成したこのシステムに対して「XSS(クロスサイトスクリプティング)」や「認可制御の欠如(BOLA/IDOR)」といった意図的なサイバー攻撃を仕掛けます。
そして、実際に攻撃が成功してしまう脆弱性を確認した上で、それを防ぐための「堅牢化(セキュアコーディング)」を行ってプロジェクトを完結させます!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?