はじめに
こんにちは!サイバーセキュリティやゼロトラストアーキテクチャを学んでいる情報系の大学生です。
現在、7日で「セキュア社内掲示板」をゼロから開発するプロジェクトに挑戦しています。
今日(Day 6)は、これまで作ってきたバックエンド(FastAPI)とフロントエンド(Next.js)を結合し、ついに「パスキー(WebAuthn)認証」を含めた一気通貫のWebアプリケーションを完成させました!
この記事では、結合フェーズで立ちはだかった「あるあるな罠」とその解決策、実装のポイントを初学者向けにわかりやすく解説します。
今週の達成事項
- Next.jsからのAPI非同期通信(CRUD処理)の実装
- HttpOnly Cookieを使ったセキュアなセッション管理の確立
- WebAuthn(FIDO2)ブラウザAPIの呼び出しとバックエンド連携
- 新規ユーザー登録・ログアウト機能の実装
- 多層防御を意識したフロントエンド・バリデーションの実装
学びとつまずきポイント解説
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)

Tailwind CSSで実装したサイバーテイストなUI。通常のパスワード認証に加え、下部には生体認証(FIDO2)への導線を配置しています。
② パスキー(生体認証)ポップアップ

WebAuthn APIが呼び出され、デバイスの認証ダイアログが起動した瞬間。裏側ではFastAPIとの間で複雑なチャレンジ&レスポンスの暗号署名検証が行われています。
③ ダッシュボード画面

ログインに成功したユーザーのみが入れるメイン画面。画面右上に現在のユーザーIDを表示し、各投稿に対して「自分に権限があるか(BOLA対策)」をバックエンドで厳密に検証しています。
④ 新規投稿画面(多層防御のフロントエンド)

直感的に操作できる投稿フォーム。文字数などのバリデーションをフロントエンド側でも行い、無駄な通信を防ぐ「多層防御」の第一層として機能しています。
まとめと次回の予告
Week 6にて、以下の「ハッピーパス(正常系の一気通貫フロー)」が完成しました!
- 新規ユーザー登録
- パスワードログイン & WebAuthn(パスキー)登録
- パスキーでの生体認証ログイン
- 掲示板へのセキュアなデータ投稿・取得
- 安全なログアウト(Cookie削除)
さて、これで「完璧に動くアプリケーション」が完成しましたが、セキュリティ専攻としてはここからが本番です。
次回、最終日(Day 7)では、完成したこのシステムに対して「XSS(クロスサイトスクリプティング)」や「認可制御の欠如(BOLA/IDOR)」といった意図的なサイバー攻撃を仕掛けます。
そして、実際に攻撃が成功してしまう脆弱性を確認した上で、それを防ぐための「堅牢化(セキュアコーディング)」を行ってプロジェクトを完結させます!