heartwood-auth について
heartwood-authは、GroveアプリにHeartwood認証を組み込む開発者向けのスキルです。Better Auth、サブドメイン間のCookie SSO、旧トークンフロー、ルート保護の手順を扱います。
主な機能
- 01Better Authクライアントの統合
- 02サブドメイン間Cookie SSOの設定
- 03旧OAuthトークンフローへの対応
- 04セッション検証とルート保護
- 05クライアント認証情報とエラーの管理
活用シーン
- 01Groveアプリへのログイン追加
- 02管理画面とAPIルートの保護
- 03保護対象リクエストでのセッション検証
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ autumnsgrove/ lattice --skill heartwood-authインストール後、Claude Code で/heartwood-authを入力して読み込まれたか確認します。
詳細
Heartwood Auth Integration スキル
有効化するタイミング
次の場合にこのスキルを有効にします。
- Grove アプリケーションに認証を追加する場合
- 管理者用ルートを保護する場合
- ユーザーセッションを検証する場合
- OAuth サインインを設定する場合
- Heartwood(GroveAuth)と連携する場合
概要
Heartwood は、Better Auth を基盤とする Grove の一元認証サービスです。
| ドメイン | 用途 |
|---|---|
heartwood.grove.place | フロントエンド(ログイン UI) |
auth-api.grove.place | バックエンド API |
主な機能
- OAuth プロバイダー:Google
- マジックリンク:Resend を介したクリックしてログインできるメール
- パスキー:WebAuthn によるパスワードレス認証
- KV キャッシュセッション:100 ms 未満で検証
- サブドメイン間の SSO:すべての .grove.place で共通のセッション
連携方法
方法 A:Better Auth クライアント(推奨)
新しい連携には、Better Auth のクライアントライブラリを使用します。
// src/lib/auth/client.ts
import { createAuthClient } from "better-auth/client";
export const auth = createAuthClient({
baseURL: "https://auth-api.grove.place",
});
// Sign in with Google
await auth.signIn.social({ provider: "google" });
// Get current session
const session = await auth.getSession();
// Sign out
await auth.signOut();
方法 B:Cookie ベースの SSO(*.grove.place アプリ)
.grove.place サブドメイン上のアプリでは、Cookie を介してセッションが自動的に機能します。
// src/hooks.server.ts
import type { Handle } from "@sveltejs/kit";
export const handle: Handle = async ({ event, resolve }) => {
// Check session via Heartwood API
const sessionCookie = event.cookies.get("better-auth.session_token");
if (sessionCookie) {
try {
const response = await fetch("https://auth-api.grove.place/api/auth/session", {
headers: {
Cookie: `better-auth.session_token=${sessionCookie}`,
},
});
if (response.ok) {
const data = await response.json();
event.locals.user = data.user;
event.locals.session = data.session;
}
} catch {
// Session invalid, expired, or network error — silently continue
}
}
return resolve(event);
};
方法 C:従来のトークンフロー(後方互換)
従来の OAuth フローを使用する既存の連携には、次を使用します。
// 1. Redirect to Heartwood login
const params = new URLSearchParams({
client_id: "your-client-id",
redirect_uri: "https://yourapp.grove.place/auth/callback",
state: crypto.randomUUID(),
code_challenge: await generateCodeChallenge(verifier),
code_challenge_method: "S256",
});
redirect(302, `https://auth-api.grove.place/login?${params}`);
// 2. Exchange code for tokens (in callback route)
const tokens = await fetch("https://auth-api.grove.place/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code: code,
redirect_uri: "https://yourapp.grove.place/auth/callback",
client_id: "your-client-id",
client_secret: env.HEARTWOOD_CLIENT_SECRET,
code_verifier: verifier,
}),
}).then((r) => r.json());
// 3. Verify token on protected routes
const user = await fetch("https://auth-api.grove.place/verify", {
headers: { Authorization: `Bearer ${tokens.access_token}` },
}).then((r) => r.json());
保護されたルートのパターン
SvelteKit レイアウトの保護
// src/routes/admin/+layout.server.ts
import { redirect } from "@sveltejs/kit";
import type { LayoutServerLoad } from "./$types";
export const load: LayoutServerLoad = async ({ locals }) => {
if (!locals.user) {
throw redirect(302, "/auth/login");
}
return {
user: locals.user,
};
};
API ルートの保護
// src/routes/api/protected/+server.ts
import { json, error } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
export const GET: RequestHandler = async ({ locals }) => {
if (!locals.user) {
throw error(401, "Unauthorized");
}
return json({ message: "Protected data", user: locals.user });
};
セッションの検証
Better Auth セッションエンドポイント経由
async function validateSession(sessionToken: string) {
const response = await fetch("https://auth-api.grove.place/api/auth/session", {
headers: {
Cookie: `better-auth.session_token=${sessionToken}`,
},
});
if (!response.ok) return null;
const data = await response.json();
return data.session ? data : null;
}
従来の検証エンドポイント経由
async function validateToken(accessToken: string) {
const response = await fetch("https://auth-api.grove.place/verify", {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
const data = await response.json();
return data.active ? data : null;
}
クライアントの登録
新しいアプリを Heartwood と連携するには、クライアントとして登録する必要があります。
1. クライアント認証情報を生成する
# Generate a secure client secret
openssl rand -base64 32
# Example: YKzJChC3RPjZvd1f/OD5zUGAvcouOTXG7maQP1ernCg=
# Hash it for storage (base64url encoding)
echo -n "YOUR_SECRET" | openssl dgst -sha256 -binary | base64 | tr '+/' '-_' | tr -d '='
2. Heartwood データベースに登録する
INSERT INTO clients (id, name, client_id, client_secret_hash, redirect_uris, allowed_origins)
VALUES (
lower(hex(randomblob(16))),
'Your App Name',
'your-app-id',
'BASE64URL_HASHED_SECRET',
'["https://yourapp.grove.place/auth/callback"]',
'["https://yourapp.grove.place"]'
);
3. アプリにシークレットを設定する
# Set the client secret on your app
wrangler secret put HEARTWOOD_CLIENT_SECRET
# Paste: YKzJChC3RPjZvd1f/OD5zUGAvcouOTXG7maQP1ernCg=
環境変数
| 変数 | 説明 |
|---|---|
HEARTWOOD_CLIENT_ID | 登録済みのクライアント ID |
HEARTWOOD_CLIENT_SECRET | クライアントシークレット(絶対にコミットしないでください!) |
API エンドポイントのリファレンス
Better Auth エンドポイント(推奨)
| メソッド | エンドポイント | 用途 |
|---|---|---|
| POST | /api/auth/sign-in/social | OAuth サインイン |
| GET | /api/auth/session | 現在のセッションを取得 |
| POST | /api/auth/sign-out | サインアウト |
従来のエンドポイント
| メソッド | エンドポイント | 用途 |
|---|---|---|
| GET | /login | ログインページ |
| POST | /token | コードをトークンと交換 |
| GET | /verify | アクセストークンを検証 |
| GET | /userinfo | ユーザー情報を取得 |
ベストプラクティス
推奨事項
- 新しい連携には Better Auth クライアントを使用します
- 保護されたリクエストごとにセッションを検証します
- トークンの保存には
httpOnlyCookie を使用します - 認証エラーに対する適切なエラーハンドリングを実装します
- セッションの有効期限が切れたら、ユーザーを適切にログアウトさせます
禁止事項
- トークンを localStorage に保存しないでください(XSS に対して脆弱です)
- API ルートでのセッション検証を省略しないでください
- クライアントシークレットをハードコードしないでください
- トークンの有効期限を無視しないでください
型安全なエラーハンドリング
catch ブロックで手動によるエラー型の絞り込みを行わず、Rootwork の型ガードを使用します。@autumnsgrove/lattice/server からインポートします。
import { isRedirect, isHttpError } from "@autumnsgrove/lattice/server";
try {
// ... auth flow
} catch (err) {
if (isRedirect(err)) throw err; // Re-throw SvelteKit redirects
if (isHttpError(err)) {
// Handle HTTP errors with proper status code
console.error(`Auth failed: ${err.status} ${err.body}`);
}
// Fallback error handling
}
KV/キャッシュからのセッションデータの読み込み: 型安全なデシリアライズには safeJsonParse() を使用します。
import { safeJsonParse } from "@autumnsgrove/lattice/server";
import { z } from "zod";
const sessionSchema = z.object({
userId: z.string(),
email: z.string().email(),
});
const rawSession = await kv.get("session:123");
const session = safeJsonParse(rawSession, sessionSchema);
if (session) {
event.locals.user = { id: session.userId, email: session.email };
}
サブドメイン間の SSO
すべての .grove.place アプリで同じセッション Cookie が自動的に共有されます。
better-auth.session_token (domain=.grove.place)
Grove のいずれかのサイトでサインインすると、ユーザーはすべてのサイトでサインイン済みになります。
トラブルシューティング
「Session not found」エラー
- Cookie のドメインが
.grove.placeであることを確認します - SESSION_KV 名前空間にアクセスできることを確認します
- セッションの有効期限が切れていないことを確認します
OAuth コールバックエラー
- redirect_uri が登録済みクライアントと一致することを確認します
- client_id が正しいことを確認します
- client_secret_hash が base64url エンコーディングを使用していることを確認します
認証が遅い場合
- KV キャッシュが有効になっていることを確認してください(SESSION_KV バインディング)
- コールドスタートの問題を確認してください(Workers はスリープする場合があります)
エラーコード(HW-AUTH カタログ)
Heartwood には、16 個のコードを含む独自の Signpost エラーカタログがあります。
import {
AUTH_ERRORS,
getAuthError,
logAuthError,
buildErrorParams,
} from "@autumnsgrove/lattice/heartwood";
主なエラーコード:
| コード | キー | 発生条件 |
|---|---|---|
HW-AUTH-001 | ACCESS_DENIED | ユーザーに権限がありません |
HW-AUTH-002 | PROVIDER_ERROR | OAuth プロバイダーでエラーが発生しました |
HW-AUTH-004 | REDIRECT_URI_MISMATCH | コールバック URL が登録済みクライアントと一致しません |
HW-AUTH-020 | NO_SESSION | セッション Cookie が見つかりません |
HW-AUTH-021 | SESSION_EXPIRED | セッションがタイムアウトしました |
HW-AUTH-022 | INVALID_TOKEN | トークンの検証に失敗しました |
HW-AUTH-023 | TOKEN_EXCHANGE_FAILED | コードとトークンの交換に失敗しました |
OAuth エラーと Signpost コードの対応付け:
// In callback handler — map OAuth error param to structured error
const authError = getAuthError(errorParam); // e.g. "access_denied" → AUTH_ERRORS.ACCESS_DENIED
logAuthError(authError, { path: "/auth/callback", ip });
redirect(302, `/login?${buildErrorParams(authError)}`);
番号の範囲: 001~019 はインフラストラクチャ、020~039 はセッション/トークン、040 以上は予約済みです。
Signpost の全リファレンスについては、AgentUsage/error_handling.md を参照してください。
関連リソース
- Heartwood 仕様:
/Users/autumn/Documents/Projects/GroveAuth/GROVEAUTH_SPEC.md - Better Auth ドキュメント:https://better-auth.com
- クライアント設定ガイド:
/Users/autumn/Documents/Projects/GroveAuth/docs/OAUTH_CLIENT_SETUP.md