【失敗】100日チャレンジ day19 (OIDC/OAuthのIDaaS基盤)
昨日
100日チャレンジに感化されたので、アレンジして自分でもやってみます。
やりたいこと
- 世の中のさまざまなドメインの簡易実装をつくり、バックエンドの実装に慣れる(dbスキーマ設計や、関数の分割、使いやすいインターフェイスの切り方に慣れる
- 設計力(これはシステムのオーバービューを先に自分で作ってaiに依頼できるようにする
- 生成aiをつかったバイブコーティングになれる
- 実際にやったことはzennのスクラップにまとめ、成果はzennのブログにまとめる(アプリ自体の公開は必須ではないかコードはgithubにおく)
できたもの
TBD
承知しました。Day 19 で作成する OIDC プロバイダー (IDaaS) の基本仕様を以下にまとめます。まずはコア機能に絞り、シンプルさを保ちます。
Day 19: OIDC プロバイダー (IDaaS) - 基本仕様
-
目的:
- 他の独立した Web アプリケーション (クライアントアプリ/RP) から認証・認可のために利用できる、独立した OIDC プロバイダー (IDaaS) を構築する。
- 異なるクライアントアプリ間でシングルサインオン (SSO) を実現する。
-
コアプロトコル:
- OpenID Connect (OIDC) 1.0 に準拠 (OAuth 2.0 を基盤とする)。
- 主要フローとして Authorization Code Flow をサポートする。
-
主要機能:
-
OIDC/OAuth 2.0 エンドポイント:
-
/authorize: 認証リクエストを受け付け、ユーザーにログインと同意を求める。 -
/token: 認可コード (code) を受け取り、IDトークンとアクセストークンを発行する。 -
/userinfo: 有効なアクセストークンに基づき、保護されたユーザー情報 (今回は主にメールアドレス) を返す。 -
/jwks: IDトークンやアクセストークンの署名を検証するための公開鍵セット (JSON Web Key Set) を提供する。
-
-
ユーザー認証:
- メールアドレスとパスワードを使用した認証方式を提供。
- パスワードは安全な方法 (bcryptなど) でハッシュ化してデータベースに保存。
-
クライアントアプリケーション管理:
- 信頼されたクライアントアプリケーション(例: アプリA, アプリB)の情報をデータベースで管理 (Client ID, Client Secret, Redirect URI など)。
- クライアント認証は
/tokenエンドポイントで行う (例: Client Secret Basic)。
-
同意画面:
- ユーザーがクライアントアプリケーションに対し、どの情報 (スコープ、例:
openid,email,profile) へのアクセスを許可するかを選択するインターフェースを提供。
- ユーザーがクライアントアプリケーションに対し、どの情報 (スコープ、例:
-
OIDC/OAuth 2.0 エンドポイント:
-
技術スタック:
-
フレームワーク: Next.js (App Router) -
localhost:3001で動作 - 言語: TypeScript
- データベース: SQLite
- ORM: Prisma
-
OIDCライブラリ:
node-oidc-provider(OIDC/OAuth 2.0 の複雑なロジックを管理) - スタイリング: Tailwind CSS
-
パスワードハッシュ化:
bcrypt
-
フレームワーク: Next.js (App Router) -
-
データモデル (Prisma
schema.prisma):-
User: ID、メールアドレス、ハッシュ化パスワードなど。 -
Client: Client ID、ハッシュ化された Client Secret、リダイレクトURIリストなど。 - その他
node-oidc-providerがアダプター経由で要求する可能性のあるモデル (例:Session,Interaction,Grant) を定義。
-
-
提供する UI (IDaaS 側):
- ログインページ: ユーザーがメールアドレスとパスワードを入力するフォーム。
- 同意ページ: ユーザーがクライアントアプリへのスコープ許可を承認/拒否するページ。
- (任意) ユーザー登録ページ: 今回は必須とせず、開発用に手動でDBにユーザーを作成する形でも可。
-
テストシナリオ:
- 別途、簡単なテスト用クライアントアプリ A (
localhost:3002) と B (localhost:3003) を作成する。 - IDaaS (
localhost:3001) を起動し、クライアント A, B の情報をDBに登録する。 - テストユーザーをDBに登録する。
- ブラウザでアプリ A にアクセス → IDaaS にリダイレクト → ログイン → 同意 → アプリ A に戻りログイン成功。
- 同じブラウザでアプリ B にアクセス → IDaaS にリダイレクト (ログインはスキップされるはず) → 同意 → アプリ B に戻り同じユーザーでログイン成功 (SSO確認)。
- 別途、簡単なテスト用クライアントアプリ A (
-
今回のスコープ外 (実装しない機能):
- リフレッシュトークン
- パスワードリセット機能
- メールアドレス確認フロー
- 管理者向け管理画面
- 多要素認証 (MFA)
- シングルログアウト (SLO)
- 高度なユーザープロファイル管理 (今回は Email のみ)
承知しました。OIDC プロバイダー (IDaaS) の実装に向けた作業手順を以下に示します。
Day 19: OIDC プロバイダー (IDaaS) - 作業手順
-
プロジェクト初期化 & 基本設定:
-
templateディレクトリをday19_oidc_providerとしてコピー (再試行)。 -
day19_oidc_providerディレクトリに移動 (cd day19_oidc_provider)。 -
package.jsonのnameフィールドを"day19_oidc_provider"に変更。 -
README.mdを更新し、今回の「OIDC プロバイダー (IDaaS)」の概要、目的、基本仕様を記述 (既存の内容は置き換え)。
-
-
依存ライブラリの導入:
- OIDC プロバイダー本体とパスワードハッシュ化ライブラリをインストール:
npm install oidc-provider bcrypt -
bcryptの型定義をインストール:npm install --save-dev @types/bcrypt
- OIDC プロバイダー本体とパスワードハッシュ化ライブラリをインストール:
-
データベースモデリング (
prisma/schema.prisma):-
Userモデル (id, email, passwordHash) を定義。 -
Clientモデル (id, clientId, clientSecretHash, redirectUris, grantTypes) を定義。 -
node-oidc-providerがアダプター経由で必要とするモデル群を定義 (例:Session,AccessToken,AuthorizationCode,RefreshToken,DeviceCode,Interaction,Grantなど。詳細はnode-oidc-providerドキュメント参照)。
-
-
データベースマイグレーション:
- スキーマ変更をデータベースに適用:
npx prisma migrate deploy
- スキーマ変更をデータベースに適用:
-
環境変数設定 (
.env):- 必須の環境変数を設定:
DATABASE_URL="file:./dev.db" ISSUER_URL="http://localhost:3001" # IDaaS自身の公開URL PORT=3001 # Cookie署名などに使う秘密鍵 (例: openssl rand -hex 32 で生成) SECURE_KEY="your-super-secret-key-change-this" # bcrypt のソルトラウンド数 BCRYPT_SALT_ROUNDS=10 NEXT_PUBLIC_ISSUER_URL="http://localhost:3001" # クライアントサイドでも使う場合 -
.gitignoreに.envが含まれていることを確認。
- 必須の環境変数を設定:
-
OIDC Provider 設定 (
lib/oidc.ts):-
oidc-providerのインスタンスを生成し、基本設定を行う。-
issuer:ISSUER_URLを設定。 -
clients: DBからクライアント情報をロードする設定 (初期は空配列でも可、後で Prisma Adapter 経由で設定)。 -
cookies.keys:SECURE_KEYを設定。 -
findAccount: 指定されたctxとidに基づいてユーザーアカウント情報を返す関数を実装 (Prisma を使用)。 -
claims: サポートするクレーム (openid,email,profileなど) を定義。 -
features: 有効にする機能 (例:userinfo,revocation) を設定。resourceIndicatorsを有効にする場合は設定。 -
pkce.required:trueに設定 (Authorization Code Flow with PKCE を強制)。 -
Prisma Adapter の設定:
node-oidc-providerが Prisma と連携するためのアダプターを設定/実装する (重要なステップ)。 - JWKS の設定 (デフォルトでは自動生成される)。
-
-
-
API エンドポイント実装 (
app/api/oidc/[...oidc]/route.ts):-
lib/oidc.tsで作成したproviderインスタンスのcallback()メソッドを使用して、OIDC のリクエストを処理する Route Handler を作成。
-
-
インタラクション UI 実装 (
app/(interaction)/[uid]/...):-
oidc-providerの設定で、ログインと同意のインタラクションが発生した際にリダイレクトするカスタムURL (/interaction/:uid/login,/interaction/:uid/confirm) を設定する。 -
ログインページ (
app/(interaction)/[uid]/login/page.tsx):-
provider.interactionDetails(req, res)を使用してインタラクションの詳細を取得。 - メールアドレス/パスワード入力フォームを作成。
- フォーム送信時に Server Action または API Route でユーザー認証 (DB照合、
bcrypt.compare) を行い、provider.interactionFinishedを呼び出す。
-
-
同意ページ (
app/(interaction)/[uid]/confirm/page.tsx):-
provider.interactionDetails(req, res)を使用してクライアント情報や要求スコープを取得・表示。 - 「許可」「拒否」ボタンを設置。
- ボタン操作に応じて
provider.interactionFinishedまたはprovider.interactionResultを呼び出す。
-
-
-
初期データ登録 (開発用):
- テスト用のユーザー (
User) とクライアントアプリケーション (Client, アプリ A, B の情報) を Prisma を使ってデータベースに登録する。- ユーザーパスワードは
bcrypt.hashでハッシュ化する。 - クライアントシークレットもハッシュ化して保存するのが望ましい。
- ユーザーパスワードは
- テスト用のユーザー (
-
テストクライアント準備 (別プロジェクト):
- 簡単な Next.js アプリ (または Node.js スクリプト) を 2 つ (
client-a,client-b) 作成し、それぞれ異なるポート (3002,3003) で動作するようにする。 -
openid-clientなどのライブラリを使用して、IDaaS への認証フローを開始するボタンと、コールバック (/callback) でトークンを取得し、ユーザー情報を表示する機能を実装する。
- 簡単な Next.js アプリ (または Node.js スクリプト) を 2 つ (
-
結合テスト & デバッグ:
- IDaaS、クライアントA、クライアントB を起動する。
- 定義したテストシナリオ(アプリAでログイン→アプリBでSSO確認)を実行し、意図通り動作するか確認。
-
console.logやブラウザの開発者ツール、oidc-providerのデバッグログなどを活用して問題を特定・修正する。
-
ドキュメント更新:
-
README.mdにセットアップ手順、起動方法、テスト方法を追記。 -
.cursor/rules/knowledge.mdcに Day 19 の成果を記録。
-
うーん、のっけからなかなか動かなくてこまってるね
NextAuth.js に使うことを提案された。
oidc-provider v8.x と Node.js v20 (または Next.js/Turbopack 環境) との間に互換性の問題がある可能性が極めて高いです。URL.parse は Node.js の古い API であり、最近のバージョンでは new URL() コンストラクタの使用が推奨されています。ライブラリが古い API に依存していると、このような問題が発生しやすくなります。
Node.js のバージョンは v20.10.0 ですね。これは oidc-provider が警告している推奨バージョン (v22.x LTS) よりも古いです。
やはり、これが crypto.hash エラーの根本原因である可能性が非常に高いです。Node.js v20 と v22 では crypto モジュール周りの API や内部実装に変更があった可能性があります。
解決策:
Node.js のバージョンを v22.x LTS (現時点での最新 LTS) にアップデートしてください。
Node.js のバージョン管理ツール ( nvm, volta など) をお使いの場合は、それらを使って v22 に切り替えるのが簡単です。
ごめんよ
全然終わらん・・・
アプローチの変更 (推奨):
oidc-provider を別の軽量 Node.js サーバー (例: 標準の http モジュールや fastify など) で実行します。
Next.js の API ルート (/api/oidc/[...oidc]/route.ts) は、その別サーバーへのリクエストプロキシとして機能するように実装します。これは比較的実装が容易で、oidc-provider を本来の環境に近い形で動作させられます。
Next.js の rewrites は、/auth などをこのプロキシ API ルートに転送するように設定します。
nextjs で oidc サーバをやらせるのは荷が重いことがわかった
これ一回やり直した方がいいまであるな
✅ シンプルさと実装しやすさを最優先にした技術選定
| 役割 | 採用スタック | 採用理由 |
|---|---|---|
| UI (ログイン/同意画面) | Next.js 15 (App Router) + React Server Components + Tailwind CSS | React/Tailwind で UI を素早く構築。app/login/page.tsx などファイル=URL で直感的。すべて Node ランタイムなので OIDC と同一プロセスにまとめやすい。 |
| OIDC/OAuth 2.0 コア | node-oidc-provider v8 | OpenID Certified™ かつ最新の v8 系列がメンテされており、PoC でも十分な機能が一式揃う。 |
| サーバーランタイム | Express 5(カスタム Next.js サーバー方式) | Next.js をカスタムサーバーモードで起動し、provider.callback() をマウントするだけで一体化できる。 |
| 永続化 | SQLite + Prisma ORM | 依存ファイルは dev.db だけ。Prisma の型安全クライアントで開発が高速化。 |
| OIDC⇔DB アダプター | node-oidc-provider-prisma-adapter | 既存 OSS があるので自作不要。 |
| パスワードハッシュ | bcrypt (bcryptjs でも可) | コスト 10 程度なら開発環境でも十分高速。 |
推奨アーキテクチャ(単一リポジトリ・単一プロセス)
┌────────────┐ http://localhost:3001
│ Next+Express│
│ (custom) │
│ ┌────────┐ │ /authorize /token /userinfo /jwks
│ │oidc-prov│─┼────────────────────────────────────────┐
│ └────────┘ │ │
│ ↑ interaction (302) │
│ ┌────────────┐ │
│ │ /login │ React + Tailwind (Next.js pages) │
│ │ /consent │ │
│ └────────────┘ │
└────────────────────────────────────────────────────────┘
ポイント
- 画面レンダリングは 全部 Next.js。node-oidc-provider の view 機能は完全にオフ。
- OIDC ルートだけ
/oidcプレフィックスで Express にマウントし、残りは Next.js ルーティングに任せる。
最小構成サンプル
server.ts — Next.js + Express + OIDC Provider
import express from 'express';
import next from 'next';
import { Provider } from 'oidc-provider';
import { PrismaAdapter } from './prisma/adapter';
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
(async () => {
await app.prepare();
const oidc = new Provider('http://localhost:3001', {
adapter: PrismaAdapter,
clients: [
{
client_id: 'appA',
client_secret: 'secretA',
redirect_uris: ['http://localhost:3002/callback'],
response_types: ['code'],
grant_types: ['authorization_code'],
},
],
features: {
devInteractions: { enabled: false },
},
interactions: {
url: ctx => `/interaction/${ctx.oidc.uid}`,
},
});
const server = express();
server.use('/oidc', oidc.callback()); // /oidc/authorize など
server.all('*', (req, res) => handle(req, res));
server.listen(3001, () =>
console.log('🚀 IDaaS running at http://localhost:3001'),
);
})();
Prisma schema.prisma(抜粋)
model User {
id String @id @default(uuid())
email String @unique
password String
createdAt DateTime @default(now())
}
model Client {
id String @id
clientSecret String
redirectUris String[]
grants String[]
}
/// 以下は adapter が期待するテーブル群
model Session { @@map("oidc_Session") /* ... */ }
model Interaction { @@map("oidc_Interaction") /* ... */ }
model Grant { @@map("oidc_Grant") /* ... */ }
UI フロー(Next.js 側)
-
/interaction/[uid]/page.tsx
provider.interactionDetails()で prompt を取得 → 未ログインなら/login?uid=…へリダイレクト。 -
/login/page.tsx
フォーム送信後provider.interactionFinished(res, uid, { login: { accountId } })。 -
/consent/page.tsx
スコープ一覧を表示し、承認でinteractionFinished。
単一プロセス構成を勧める理由
| 項目 | 単一プロセス | Next / OIDC 分離 |
|---|---|---|
| セットアップ | pnpm i && pnpm dev |
2 つの起動スクリプト |
| ポート管理 | 3001 だけ | 3001 / 4000 など |
| Cookie ドメイン/CORS | 同一オリジンで設定不要 | SameSite=None・CORS 設計が必要 |
| デプロイ | Dockerfile 1 枚 | コンテナ 2 個または Nginx 経由 |
PoC なら 最速で動く単一構成が圧倒的に楽です。
将来スケール時に OIDC 部分だけを独立させることも容易です。
次のアクション
- 上記サンプルをクローンして起動
-
.env→DATABASE_URL="file:./dev.db" -
pnpm dev→http://localhost:3001/oidc/.well-known/openid-configurationが JSON を返せば OK
-
- テスト用クライアント A/B を用意して Authorization Code Flow → SSO 動作を確認
- ハマりどころが出たらいつでも質問してください!🙌
仕切り直し
Day 19: OIDC プロバイダー (IDaaS) - 基本仕様
-
目的:
- 他の独立した Web アプリケーション (クライアントアプリ/RP) から認証・認可のために利用できる、独立した OIDC プロバイダー (IDaaS) を構築する。
- 異なるクライアントアプリ間でシングルサインオン (SSO) を実現する。
-
コアプロトコル:
- OpenID Connect (OIDC) 1.0 に準拠 (OAuth 2.0 を基盤とする)。
- 主要フローとして Authorization Code Flow をサポートする。
-
主要機能:
-
OIDC/OAuth 2.0 エンドポイント:
-
/authorize: 認証リクエストを受け付け、ユーザーにログインと同意を求める。 -
/token: 認可コード (code) を受け取り、IDトークンとアクセストークンを発行する。 -
/userinfo: 有効なアクセストークンに基づき、保護されたユーザー情報 (今回は主にメールアドレス) を返す。 -
/jwks: IDトークンやアクセストークンの署名を検証するための公開鍵セット (JSON Web Key Set) を提供する。
-
-
ユーザー認証:
- メールアドレスとパスワードを使用した認証方式を提供。
- パスワードは安全な方法 (bcryptなど) でハッシュ化してデータベースに保存。
-
クライアントアプリケーション管理:
- 信頼されたクライアントアプリケーション(例: アプリA, アプリB)の情報をデータベースで管理 (Client ID, Client Secret, Redirect URI など)。
- クライアント認証は
/tokenエンドポイントで行う (例: Client Secret Basic)。
-
同意画面:
- ユーザーがクライアントアプリケーションに対し、どの情報 (スコープ、例:
openid,email,profile) へのアクセスを許可するかを選択するインターフェースを提供。
- ユーザーがクライアントアプリケーションに対し、どの情報 (スコープ、例:
-
OIDC/OAuth 2.0 エンドポイント:
-
データモデル (Prisma
schema.prisma):-
User: ID、メールアドレス、ハッシュ化パスワードなど。 -
Client: Client ID、ハッシュ化された Client Secret、リダイレクトURIリストなど。 - その他
node-oidc-providerがアダプター経由で要求する可能性のあるモデル (例:Session,Interaction,Grant) を定義。
-
-
提供する UI (IDaaS 側):
- ログインページ: ユーザーがメールアドレスとパスワードを入力するフォーム。
- 同意ページ: ユーザーがクライアントアプリへのスコープ許可を承認/拒否するページ。
- (任意) ユーザー登録ページ: 今回は必須とせず、開発用に手動でDBにユーザーを作成する形でも可。
-
テストシナリオ:
- 別途、簡単なテスト用クライアントアプリ A (
localhost:3002) と B (localhost:3003) を作成する。 - IDaaS (
localhost:3001) を起動し、クライアント A, B の情報をDBに登録する。 - テストユーザーをDBに登録する。
- ブラウザでアプリ A にアクセス → IDaaS にリダイレクト → ログイン → 同意 → アプリ A に戻りログイン成功。
- 同じブラウザでアプリ B にアクセス → IDaaS にリダイレクト (ログインはスキップされるはず) → 同意 → アプリ B に戻り同じユーザーでログイン成功 (SSO確認)。
- 別途、簡単なテスト用クライアントアプリ A (
-
今回のスコープ外 (実装しない機能):
- リフレッシュトークン
- パスワードリセット機能
- メールアドレス確認フロー
- 管理者向け管理画面
- 多要素認証 (MFA)
- シングルログアウト (SLO)
- 高度なユーザープロファイル管理 (今回は Email のみ)
✅ シンプルさと実装しやすさを最優先にした技術選定
| 役割 | 採用スタック | 採用理由 |
|---|---|---|
| UI (ログイン/同意画面) | Next.js 15 (App Router) + React Server Components + Tailwind CSS | React/Tailwind で UI を素早く構築。app/login/page.tsx などファイル=URL で直感的。すべて Node ランタイムなので OIDC と同一プロセスにまとめやすい。 |
| OIDC/OAuth 2.0 コア | node-oidc-provider v8 | OpenID Certified™ かつ最新の v8 系列がメンテされており、PoC でも十分な機能が一式揃う。 |
| サーバーランタイム | Express 5(カスタム Next.js サーバー方式) | Next.js をカスタムサーバーモードで起動し、provider.callback() をマウントするだけで一体化できる。 |
| 永続化 | SQLite + Prisma ORM | 依存ファイルは dev.db だけ。Prisma の型安全クライアントで開発が高速化。 |
| OIDC⇔DB アダプター | node-oidc-provider-prisma-adapter | 既存 OSS があるので自作不要。 |
| パスワードハッシュ | bcrypt (bcryptjs でも可) | コスト 10 程度なら開発環境でも十分高速。 |
推奨アーキテクチャ(単一リポジトリ・単一プロセス)
┌────────────┐ http://localhost:3001
│ Next+Express│
│ (custom) │
│ ┌────────┐ │ /authorize /token /userinfo /jwks
│ │oidc-prov│─┼────────────────────────────────────────┐
│ └────────┘ │ │
│ ↑ interaction (302) │
│ ┌────────────┐ │
│ │ /login │ React + Tailwind (Next.js pages) │
│ │ /consent │ │
│ └────────────┘ │
└────────────────────────────────────────────────────────┘
ポイント
- 画面レンダリングは 全部 Next.js。node-oidc-provider の view 機能は完全にオフ。
- OIDC ルートだけ
/oidcプレフィックスで Express にマウントし、残りは Next.js ルーティングに任せる。
最小構成サンプル
server.ts — Next.js + Express + OIDC Provider
import express from 'express';
import next from 'next';
import { Provider } from 'oidc-provider';
import { PrismaAdapter } from './prisma/adapter';
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
(async () => {
await app.prepare();
const oidc = new Provider('http://localhost:3001', {
adapter: PrismaAdapter,
clients: [
{
client_id: 'appA',
client_secret: 'secretA',
redirect_uris: ['http://localhost:3002/callback'],
response_types: ['code'],
grant_types: ['authorization_code'],
},
],
features: {
devInteractions: { enabled: false },
},
interactions: {
url: ctx => `/interaction/${ctx.oidc.uid}`,
},
});
const server = express();
server.use('/oidc', oidc.callback()); // /oidc/authorize など
server.all('*', (req, res) => handle(req, res));
server.listen(3001, () =>
console.log('🚀 IDaaS running at http://localhost:3001'),
);
})();
Prisma schema.prisma(抜粋)
model User {
id String @id @default(uuid())
email String @unique
password String
createdAt DateTime @default(now())
}
model Client {
id String @id
clientSecret String
redirectUris String[]
grants String[]
}
/// 以下は adapter が期待するテーブル群
model Session { @@map("oidc_Session") /* ... */ }
model Interaction { @@map("oidc_Interaction") /* ... */ }
model Grant { @@map("oidc_Grant") /* ... */ }
UI フロー(Next.js 側)
-
/interaction/[uid]/page.tsx
provider.interactionDetails()で prompt を取得 → 未ログインなら/login?uid=…へリダイレクト。 -
/login/page.tsx
フォーム送信後provider.interactionFinished(res, uid, { login: { accountId } })。 -
/consent/page.tsx
スコープ一覧を表示し、承認でinteractionFinished。
単一プロセス構成を勧める理由
| 項目 | 単一プロセス | Next / OIDC 分離 |
|---|---|---|
| セットアップ | pnpm i && pnpm dev |
2 つの起動スクリプト |
| ポート管理 | 3001 だけ | 3001 / 4000 など |
| Cookie ドメイン/CORS | 同一オリジンで設定不要 | SameSite=None・CORS 設計が必要 |
| デプロイ | Dockerfile 1 枚 | コンテナ 2 個または Nginx 経由 |
PoC なら 最速で動く単一構成が圧倒的に楽です。
将来スケール時に OIDC 部分だけを独立させることも容易です。
という感じで day19 は進めたいです。
疑問点や質問はありますか?
承知いたしました。フィードバックありがとうございます。パスワードは平文で保存し、テストクライアントアプリも Day 19 のスコープに含めます。
それでは、修正した作業計画を以下に示します。
作業計画 (修正版)
-
IDaaS プロジェクト初期化:
-
templateをコピーしてday19_oidc_providerディレクトリを作成。 -
package.jsonのnameをday19_oidc_providerに更新。 -
README.mdを更新し、今回の仕様概要を記載。 - 必要な依存パッケージ (
oidc-provider,express,@types/express,node-oidc-provider-prisma-adapter) を追加。 - カスタムサーバー (
server.ts) の基本設定とpackage.jsonスクリプト (dev,build,start) をカスタムサーバーを使用するように更新。
-
-
IDaaS データモデリングとDB設定:
-
prisma/schema.prismaにUser(パスワードは平文String),Client及びアダプター要求モデル (Session,Interaction,Grant,DeviceCode,PushedAuthorizationRequest,ReplayDetection,InitialAccessToken,RegistrationAccessToken,AccessToken,AuthorizationCode,RefreshToken) を定義。 -
prisma migrate deployでDBスキーマ適用。 -
テスト用データ投入:
- Prisma のシード機能、または簡単なスクリプトを用意し、テスト用ユーザー(例:
test@example.com/password)とテストクライアント(アプリA:client_id=appA,client_secret=secretA,redirect_uris=['http://localhost:3002/callback']、アプリB:client_id=appB,client_secret=secretB,redirect_uris=['http://localhost:3003/callback'])をデータベースに登録。
- Prisma のシード機能、または簡単なスクリプトを用意し、テスト用ユーザー(例:
-
-
IDaaS OIDC プロバイダー設定:
-
server.tsでoidc-providerインスタンスを初期化。-
PrismaAdapterを使用。 -
clientsオプションで DB からクライアント情報を動的に読み込む設定 (または起動時に読み込む)。 -
interactions.urlを設定 (/interaction/:uid)。 -
featuresでdevInteractionsを無効化。 -
jwksの設定 (キー生成・管理)。 - その他必要な設定 (スコープ
openid email profileなど)。
-
- Express ミドルウェアとして
/oidcパスにoidc.callback()をマウント。
-
-
IDaaS UI 実装:
-
app/layout.tsxで基本レイアウトと Tailwind CSS の設定。 -
app/interaction/[uid]/page.tsx:-
provider.interactionDetails()を呼び出し、プロンプトの種類 (login,consent) を判別。 - ログインが必要なら
/login?uid=...へリダイレクト。 - 同意が必要なら
/consent?uid=...へリダイレクト。
-
-
app/login/page.tsx:- メールアドレスとパスワード入力フォーム。
- 送信時にサーバーアクションまたは API ルートでユーザーをDBから検索・検証 (平文比較)。
- 成功したら
provider.interactionFinished(req, res, { login: { accountId: user.id } }, { mergeWithLastSubmission: false })を呼び出し、元のインタラクションへリダイレクト。
-
app/consent/page.tsx:-
provider.interactionDetails()で要求されているスコープ (prompt.details.scopes.new) を表示。 - 「許可」「拒否」ボタン。
- 許可時に
provider.interactionFinished(req, res, { consent: { grantId: interaction.grantId } }, { mergeWithLastSubmission: true })を呼び出し、クライアントへリダイレクト。
-
- アプリケーションタイトルを "Day 19 - OIDC Provider" に設定。
-
-
テストクライアントアプリ (A, B) の準備:
-
templateをコピーしてday19_oidc_client_aとday19_oidc_client_bディレクトリを作成。 - それぞれの
package.jsonのnameを更新。 - それぞれの
README.mdを更新 (テストクライアントである旨を記載)。 - 注意: これらのクライアントアプリは非常にシンプルにします。状態管理ライブラリ等は不要で、サーバーサイドで完結する処理を中心に実装します。
-
-
テストクライアントアプリ A の実装 (
localhost:3002):-
package.jsonのdevスクリプトでポートを3002に変更。 - ルートページ (
app/page.tsx):- 「アプリAでログイン (OIDC)」ボタン。
- ボタンクリックで IDaaS の
/authorizeエンドポイントへリダイレクト (Client IDappA, Redirect URIhttp://localhost:3002/callback, Scopeopenid email profile, Response Typecode)。
- コールバックページ (
app/callback/page.tsx):- URLから認可コード (
code) を取得。 - 取得したコードを使って、サーバーサイドから IDaaS の
/tokenエンドポイントにリクエスト (Client ID/Secret を Basic 認証ヘッダーに含める)。 - IDトークンとアクセストークンを取得。
- (任意) アクセストークンを使って IDaaS の
/userinfoエンドポイントにリクエストし、ユーザー情報を取得。 - 取得した情報 (例: IDトークンのクレーム、
userinfoの結果) を表示。
- URLから認可コード (
-
-
テストクライアントアプリ B の実装 (
localhost:3003):- アプリ A とほぼ同様の手順で実装。
- ポートは
3003、Client ID はappB、Redirect URI はhttp://localhost:3003/callbackを使用。
-
デバッグとテスト:
- 3つのターミナルを開く。
- ターミナル1:
cd day19_oidc_provider && npm run dev(IDaaS を:3001で起動)。 - ターミナル2:
cd day19_oidc_client_a && npm run dev(アプリ A を:3002で起動)。 - ターミナル3:
cd day19_oidc_client_b && npm run dev(アプリ B を:3003で起動)。 - ブラウザで
http://localhost:3002にアクセス。 - ログインボタンをクリック → IDaaS にリダイレクトされることを確認。
- テストユーザー情報 (
test@example.com/password) でログイン → 同意画面が表示されることを確認。 - 同意 → アプリ A (
localhost:3002/callback) にリダイレクトされ、ユーザー情報が表示されることを確認。 - 同じブラウザで別タブを開き
http://localhost:3003にアクセス。 - ログインボタンをクリック → IDaaS にリダイレクトされることを確認 (ログイン画面はスキップされるはず)。
- 同意画面が表示されることを確認。
- 同意 → アプリ B (
localhost:3003/callback) にリダイレクトされ、同じユーザー情報が表示されることを確認 (SSO成功)。 -
curl http://localhost:3001/oidc/.well-known/openid-configurationで設定確認。 -
curl http://localhost:3001/oidc/jwksで公開鍵確認。 - (アプリA/Bで取得したアクセストークンを使い)
curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" http://localhost:3001/oidc/me(/userinfo相当) でユーザー情報取得確認。 - 不要なファイルやコードを削除。
-
ドキュメント作成:
-
day19_oidc_provider/README.mdにセットアップ方法、テスト手順を記載。 -
day19_oidc_client_a/README.md,day19_oidc_client_b/README.mdに簡単な説明を記載。 -
.cursor/rules/knowledge.mdcに Day 19 の実装内容を追記。
-
この計画で進めてよろしいでしょうか? よろしければ、まず IDaaS のプロジェクト初期化から開始します。
さらに仕切り直し
承知いたしました。Next.js + Node.js での問題発生を踏まえ、バックエンドを Go で実装し、IDaaS 側のフロントエンド(ログイン・同意画面)を Next.js で実装する方針で進めます。
以下、Go バックエンド + Next.js フロントエンドでの作業計画です。
作業計画:
-
プロジェクト初期化:
-
templateディレクトリをday19_oidc_providerにコピーします。 -
package.jsonのnameフィールドをday19-oidc-providerに変更します。 -
README.mdに今回の仕様概要 (OIDC プロバイダー、Go バックエンド + Next.js フロントエンド) を記述します。
-
-
Go バックエンド セットアップ:
-
day19_oidc_provider内にbackend_goディレクトリを作成します。 -
backend_goでgo mod init github.com/user/day19_oidc_provider/backend_go(ユーザー名は適宜変更) を実行します。 -
go getで必要なライブラリを追加します:-
github.com/go-chi/chi/v5(ルーター) -
github.com/go-chi/cors(CORS ミドルウェア) -
golang.org/x/oauth2(OAuth2 クライアント、一部ヘルパー利用) -
github.com/coreos/go-oidc/v3/oidc(OIDC クライアント、検証用) -
github.com/golang-jwt/jwt/v5(JWT 生成・検証) -
golang.org/x/crypto/bcrypt(パスワードハッシュ化) -
github.com/mattn/go-sqlite3(SQLite ドライバー) -
github.com/jmoiron/sqlx(DB 操作ヘルパー) -
github.com/google/uuid(UUID 生成) - (マイグレーションツール
golang-migrate/migrateの CLI をインストール)
-
-
-
データモデリングと DB 設定 (Go):
-
backend_go/db/migrationsディレクトリを作成し、golang-migrate/migrate用のマイグレーションファイル (SQL) を作成します (User, Client, AuthorizationCode, Session テーブルなど)。 -
backend_go/internal/storeパッケージなどで DB 操作ロジックを実装します (sqlxを使用)。 - マイグレーションを実行して
prisma/dev.db(今回はこれを共有) にテーブルを作成します。 - (任意) テストユーザーとクライアント情報を登録するスクリプトまたはロジックを作成します。
-
-
Go バックエンド OIDC コア実装:
- 設定: Issuer URL, JWKS URI, 秘密鍵などを設定ファイルまたは環境変数で管理します。
-
JWKS: RSA キーペアを生成・保持し、
/jwksエンドポイントを実装します。 -
ハンドラー:
chiルーターを使用して各エンドポイント (/authorize,/token,/userinfo,/jwks,/login,/consent) のハンドラー関数を作成します。 -
認証/同意フロー:
-
/authorizeで認証/同意が必要な場合、一時的な ID (例:interaction_id) を生成し、対応する Next.js ページ (例:http://localhost:3001/login?interaction_id=...) にリダイレクトします。 - Next.js 側 (
/login,/consent) のフォーム送信先として/login(POST),/consent(POST) を実装し、interaction_idを元に状態を復元して処理を進めます。
-
- トークン発行: ID トークン (JWT)、アクセストークン (JWT or Opaque) を生成します。
- セッション管理: ログイン状態を管理するセッション機構 (例: Cookie ストア) を実装します。
-
DB連携:
internal/storeの関数を利用してデータを永続化・取得します。
-
Next.js フロントエンド実装 (UI):
-
app/(interaction)/login/page.tsx: ログインフォーム。Go バックエンドの/login(POST) にinteraction_idと共に送信します。 -
app/(interaction)/consent/page.tsx: 同意フォーム。Go バックエンドの/consent(POST) にinteraction_idと共に送信します。 - Go バックエンドからリダイレクトされた際に
interaction_idをクエリパラメータから取得し、フォームに含める処理を追加します。 - Tailwind CSS でスタイリングします。
-
-
連携とテスト:
- Go バックエンド (例: ポート 8080) と Next.js フロントエンド (ポート 3001) を起動します。
- テスト用クライアント A (ポート 3002), B (ポート 3003) を別途作成し、Go バックエンド (例:
http://localhost:8080) を IDaaS として設定します。 - ブラウザでテストシナリオを実行し、ログイン、同意、SSO を確認します。
-
curlで Go バックエンドの OIDC エンドポイントをテストします。
-
ドキュメント作成:
-
README.mdと.cursor/rules/knowledge.mdcを更新します。
-
それでは、作業を開始します。
1. プロジェクト初期化
まず、template ディレクトリをコピーして day19_oidc_provider ディレクトリを作成し、package.json と README.md を更新します。
やっと go サーバが起動した
これ今更だけどサーバとフロントと oidc provider で別に要件つくって、IF定義作ってもらってそれぞれ別個に実装してもらった方が絶対良かったな。
1コンテキストで全部やらせようとしたせいでめちゃくちゃ大変なことになってる
ちょっと一旦 OIDC はやり直そう。。いつか再挑戦する。
はい、率直に言って、ご指摘の通りだと思います。このプロジェクトは以下の理由から分割して実装する方が良かったでしょう:
-
責任範囲の明確化:
- OIDCプロバイダー
- フロントエンド
- クライアントアプリケーション
これらが明確に分離されていれば、各コンポーネントの役割と責任が明確になります。
-
インターフェース設計の重要性:
- 先にAPI仕様書(OpenAPIなど)を作成し
- 明確なインターフェース契約を決めてから
- 各コンポーネントを独立して実装する方が混乱が少なくなります
-
デバッグの容易さ:
- 今回のように「どこで問題が発生しているか」を特定するのが難しい状況を避けられます
- 各部分が独立していれば、問題の切り分けが容易になります
-
複雑性の管理:
- OIDCプロバイダーのような複雑なシステムは、それだけで十分に複雑です
- フロントエンドとの統合を同時に行うと、複雑性が掛け算的に増加します
-
学習効果の向上:
- まずはOIDCの仕様と実装に集中
- 次にフロントエンド連携
という段階的アプローチの方が学習効果も高いでしょう
リベンジする際には、まずはOIDCプロバイダーのバックエンドを単体で実装し、APIテストツールで動作確認してから、フロントエンドとの連携に進むことをお勧めします。また、クライアントアプリケーションも別途実装することで、全体としてのOIDCフローを理解しやすくなります。
時間がかかりすぎたので、次