mcpField
H
heartwood-auth

heartwood-auth

github.com/autumnsgrove安全666 次安装4 天前更新
代码仓库

关于 heartwood-auth

heartwood-auth 是面向 Grove 应用开发者的认证集成技能,涵盖 Heartwood 登录、会话验证与路由保护。它提供 Better Auth、跨子域 Cookie SSO 和兼容旧版令牌流程的接入指引。

主要功能

  1. 01接入 Better Auth 客户端
  2. 02配置跨子域 Cookie SSO
  3. 03兼容旧版 OAuth 令牌流程
  4. 04验证会话并保护路由
  5. 05管理客户端凭据与认证错误

适用场景

  1. 01为 Grove 应用添加登录认证
  2. 02保护管理页面和 API 路由
  3. 03在受保护请求中验证用户会话

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/autumnsgrove/lattice --skill heartwood-auth

安装后在 Claude Code 中输入/heartwood-auth确认已加载。

详细介绍

Heartwood 身份验证集成技能

适用场景

在以下情况下启用此技能:

  • 为 Grove 应用添加身份验证
  • 保护管理路由
  • 验证用户会话
  • 设置 OAuth 登录
  • 与 Heartwood(GroveAuth)集成

概览

Heartwood 是 Grove 基于 Better Auth 构建的集中式身份验证服务。

域名用途
heartwood.grove.place前端(登录界面)
auth-api.grove.place后端 API
主要功能
  • OAuth 提供商:Google
  • 魔法链接:通过 Resend 发送点击即可登录的邮件
  • 通行密钥:基于 WebAuthn 的无密码身份验证
  • KV 缓存会话:验证耗时不到 100 毫秒
  • 跨子域 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 服务上登录,就会在所有服务上保持登录状态。

故障排除

“未找到会话”错误
  • 检查 Cookie 域是否为 .grove.place
  • 确认 SESSION_KV 命名空间可访问
  • 检查会话是否已过期
OAuth 回调错误
  • 确认 redirect_uri 与已注册的客户端匹配
  • 检查 client_id 是否正确
  • 确保 client_secret_hash 使用 base64url 编码
认证速度慢
  • 确保已启用 KV 缓存(SESSION_KV 绑定)
  • 检查是否存在冷启动问题(Workers 可能会休眠)

错误代码(HW-AUTH 目录)

Heartwood 有自己的 Signpost 错误目录,共包含 16 个代码:

import {
	AUTH_ERRORS,
	getAuthError,
	logAuthError,
	buildErrorParams,
} from "@autumnsgrove/lattice/heartwood";

关键错误代码:

CodeKeyWhen
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

评论

支持 Markdown