mcpField
H
heartwood-auth

heartwood-auth

github.com/autumnsgroveセキュリティ666 回インストール4 日前に更新
リポジトリ

heartwood-auth について

heartwood-authは、GroveアプリにHeartwood認証を組み込む開発者向けのスキルです。Better Auth、サブドメイン間のCookie SSO、旧トークンフロー、ルート保護の手順を扱います。

主な機能

  1. 01Better Authクライアントの統合
  2. 02サブドメイン間Cookie SSOの設定
  3. 03旧OAuthトークンフローへの対応
  4. 04セッション検証とルート保護
  5. 05クライアント認証情報とエラーの管理

活用シーン

  1. 01Groveアプリへのログイン追加
  2. 02管理画面とAPIルートの保護
  3. 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/socialOAuth サインイン
GET/api/auth/session現在のセッションを取得
POST/api/auth/sign-outサインアウト
従来のエンドポイント
メソッドエンドポイント用途
GET/loginログインページ
POST/tokenコードをトークンと交換
GET/verifyアクセストークンを検証
GET/userinfoユーザー情報を取得

ベストプラクティス

推奨事項
  • 新しい連携には Better Auth クライアントを使用します
  • 保護されたリクエストごとにセッションを検証します
  • トークンの保存には httpOnly Cookie を使用します
  • 認証エラーに対する適切なエラーハンドリングを実装します
  • セッションの有効期限が切れたら、ユーザーを適切にログアウトさせます
禁止事項
  • トークンを 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-001ACCESS_DENIEDユーザーに権限がありません
HW-AUTH-002PROVIDER_ERROROAuth プロバイダーでエラーが発生しました
HW-AUTH-004REDIRECT_URI_MISMATCHコールバック URL が登録済みクライアントと一致しません
HW-AUTH-020NO_SESSIONセッション Cookie が見つかりません
HW-AUTH-021SESSION_EXPIREDセッションがタイムアウトしました
HW-AUTH-022INVALID_TOKENトークンの検証に失敗しました
HW-AUTH-023TOKEN_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

コメント