Skip to main content

grove-auth-integration

Integrate Heartwood authentication into a new or existing Grove property. Covers client registration, PKCE OAuth flow, SvelteKit route setup, session validation, and wrangler configuration. Use when adding auth to any Grove site.

설치로 이동

소스 정보

저장소
AutumnsGrove/Lattice
최근 소스 활동
2026년 2월 25일 14:16
감지된 SKILL.md 언어
영어
스타
6
포크
0

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
grove-auth-integration
description
Integrate Heartwood authentication into a new or existing Grove property. Covers client registration, PKCE OAuth flow, SvelteKit route setup, session validation, and wrangler configuration. Use when adding auth to any Grove site.
# Grove Auth Integration Add Heartwood authentication to a Grove property — from client registration through production deployment. ## When to Activate - User says "add auth to this project" or "wire up Heartwood" - User is building a new Grove property that needs login - User needs to register a new OAuth client with Heartwood - User explicitly calls `/grove-auth-integration` - User mentions needing sign-in, protected routes, or session validation - User says "integrate GroveAuth" or "add login" --- ## Key URLs | Service | URL | Purpose | | --------------- | ------------------------------- | -------------------------------- | | **Login UI** | `https://heartwood.grove.place` | Where users authenticate | | **API** | `https://auth-api.grove.place` | Token exchange, verify, sessions | | **D1 Database** | `groveauth` (via wrangler) | Client registration | --- ## The Pipeline ``` Identify → Register Client → Configure Secrets → Write Code → Wire Wrangler → Test ``` **Error Handling in Auth Flows:** Auth errors MUST use the `AUTH_ERRORS` Signpost catalog — never bare redirect with ad-hoc error strings. ```typescript import { AUTH_ERRORS, getAuthError, logAuthError, buildErrorParams, } from "@autumnsgrove/lattice/heartwood"; // In callback — map OAuth error to structured code if (errorParam) { const authError = getAuthError(errorParam); logAuthError(authError, { path: "/auth/callback" }); redirect(302, `/login?${buildErrorParams(authError)}`); } ``` See `AgentUsage/error_handling.md` for the full Signpost reference. **Type-Safe Error Handling in Catch Blocks:** Always use Rootwork type guards in catch blocks instead of manual property checks. Import `isRedirect()` and `isHttpError()` from `@autumnsgrove/lattice/server`: ```typescript import { isRedirect, isHttpError } from "@autumnsgrove/lattice/server"; try { // ... auth flow code } catch (err) { if (isRedirect(err)) throw err; // Re-throw SvelteKit redirects if (isHttpError(err)) { // Handle HTTP errors with proper status and message } redirect(302, "/?error=auth_failed"); } ``` --- ### Step 1: Identify the Integration Ask the user (or determine from context): 1. **Project name** — The Cloudflare Pages/Workers project name 2. **Client ID** — A simple slug (e.g., `grove-plant`, `grove-domains`, `arbor-admin`) 3. **Site URL** — Production URL (e.g., `https://plant.grove.place`) 4. **Callback path** — Usually `/auth/callback` 5. **Project type** — SvelteKit Pages (most common), Workers, or other 6. **Session approach** — OAuth tokens (standard) or SessionDO (faster, same-account only) --- ### Step 2: Register the OAuth Client #### 2a. Generate client secret ```bash CLIENT_SECRET=$(openssl rand -base64 32) echo "Client Secret: $CLIENT_SECRET" ``` Save this value — you'll need it for both the client secrets AND the database hash. #### 2b. Generate base64url hash **CRITICAL**: Heartwood uses **base64url encoding** — dashes (`-`), underscores (`_`), NO padding (`=`). ```bash CLIENT_SECRET_HASH=$(echo -n "$CLIENT_SECRET" | openssl dgst -sha256 -binary | base64 | tr '+/' '-_' | tr -d '=') echo "Secret Hash: $CLIENT_SECRET_HASH" ``` | Format | Example | Correct? | | ------------- | ---------------------------------- | -------- | | **base64url** | `Sdgtaokie8-H7GKw-tn0S_6XNSh1rdv` | YES | | base64 | `Sdgtaokie8+H7GKw+tn0S/6XNSh1rdv=` | NO | | hex | `49d82d6a89227bcf87ec62b0...` | NO | #### 2c. Insert into Heartwood database ```bash wrangler d1 execute groveauth --remote --command=" INSERT INTO clients (id, name, client_id, client_secret_hash, redirect_uris, allowed_origins) VALUES ( '$(uuidgen | tr '[:upper:]' '[:lower:]')', 'DISPLAY_NAME', 'CLIENT_ID', 'BASE64URL_HASH', '[\"https://SITE_URL/auth/callback\", \"http://localhost:5173/auth/callback\"]', '[\"https://SITE_URL\", \"http://localhost:5173\"]' ) ON CONFLICT(client_id) DO UPDATE SET client_secret_hash = excluded.client_secret_hash, redirect_uris = excluded.redirect_uris, allowed_origins = excluded.allowed_origins; " ``` **Always include localhost** in redirect_uris and allowed_origins for development. --- ### Step 3: Configure Secrets on the Client #### For Pages projects (SvelteKit): ```bash echo "CLIENT_ID" | wrangler pages secret put GROVEAUTH_CLIENT_ID --project PROJECT_NAME echo "CLIENT_SECRET" | wrangler pages secret put GROVEAUTH_CLIENT_SECRET --project PROJECT_NAME echo "https://SITE_URL/auth/callback" | wrangler pages secret put GROVEAUTH_REDIRECT_URI --project PROJECT_NAME echo "https://auth-api.grove.place" | wrangler pages secret put GROVEAUTH_URL --project PROJECT_NAME ``` #### For Workers projects: ```bash cd worker-directory echo "CLIENT_ID" | wrangler secret put GROVEAUTH_CLIENT_ID echo "CLIENT_SECRET" | wrangler secret put GROVEAUTH_CLIENT_SECRET ``` --- ### Step 4: Write the Auth Code (SvelteKit) Create these files in the SvelteKit project: #### 4a. Login initiation route: `src/routes/auth/+server.ts` ```typescript /** * OAuth Initiation - Start Heartwood OAuth flow * Redirects to GroveAuth with PKCE parameters. */ import { redirect } from "@sveltejs/kit"; import type { RequestHandler } from "./$types"; function generateRandomString(length: number): string { const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"; const randomValues = crypto.getRandomValues(new Uint8Array(length)); return Array.from(randomValues, (v) => charset[v % charset.length]).join(""); } async function generatePKCE(): Promise<{ verifier: string; challenge: string; }> { const verifier = generateRandomString(64); const encoder = new TextEncoder(); const data = encoder.encode(verifier); const hash = await crypto.subtle.digest("SHA-256", data); const challenge = btoa(String.fromCharCode(...new Uint8Array(hash))) .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=/g, ""); return { verifier, challenge }; } export const GET: RequestHandler = async ({ url, cookies, platform }) => { const env = platform?.env as Record<string, string> | undefined; const authBaseUrl = env?.GROVEAUTH_URL || "https://auth-api.grove.place"; const clientId = env?.GROVEAUTH_CLIENT_ID || "YOUR_CLIENT_ID"; const appBaseUrl = env?.PUBLIC_APP_URL || "https://YOUR_SITE_URL"; const redirectUri = `${appBaseUrl}/auth/callback`; const { verifier, challenge } = await generatePKCE(); const state = generateRandomString(32); const isProduction = url.hostname !== "localhost" && url.hostname !== "127.0.0.1"; const cookieOptions = { path: "/", httpOnly: true, secure: isProduction, sameSite: "lax" as const, maxAge: 60 * 10, // 10 minutes }; cookies.set("auth_state", state, cookieOptions); cookies.set("auth_code_verifier", verifier, cookieOptions); const authUrl = new URL(`${authBaseUrl}/login`); authUrl.searchParams.set("client_id", clientId); authUrl.searchParams.set("redirect_uri", redirectUri); authUrl.searchParams.set("response_type", "code"); authUrl.searchParams.set("scope", "openid profile email"); authUrl.searchParams.set("state", state); authUrl.searchParams.set("code_challenge", challenge); authUrl.searchParams.set("code_challenge_method", "S256"); redirect(302, authUrl.toString()); }; ``` #### 4b. Callback handler: `src/routes/auth/callback/+server.ts` ```typescript /** * OAuth Callback - Handle Heartwood OAuth response * Exchanges authorization code for tokens and creates session. */ import { redirect } from "@sveltejs/kit"; import { isRedirect } from "@autumnsgrove/lattice/server"; import type { RequestHandler } from "./$types"; export const GET: RequestHandler = async ({ url, cookies, platform }) => { const code = url.searchParams.get("code"); const state = url.searchParams.get("state"); const errorParam = url.searchParams.get("error"); if (errorParam) { redirect(302, `/?error=${encodeURIComponent(errorParam)}`); } // Validate state (CSRF protection) const savedState = cookies.get("auth_state"); if (!state || state !== savedState) { redirect(302, "/?error=invalid_state"); } // Get PKCE verifier const codeVerifier = cookies.get("auth_code_verifier"); if (!codeVerifier || !code) { redirect(302, "/?error=missing_credentials"); } // Clear auth cookies immediately cookies.delete("auth_state", { path: "/" }); cookies.delete("auth_code_verifier", { path: "/" }); const env = platform?.env as Record<string, string> | undefined; const authBaseUrl = env?.GROVEAUTH_URL || "https://auth-api.grove.place"; const clientId = env?.GROVEAUTH_CLIENT_ID || "YOUR_CLIENT_ID"; const clientSecret = env?.GROVEAUTH_CLIENT_SECRET || ""; const appBaseUrl = env?.PUBLIC_APP_URL || "https://YOUR_SITE_URL"; const redirectUri = `${appBaseUrl}/auth/callback`; try { // Exchange code for tokens const tokenResponse = await fetch(`${authBaseUrl}/token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "authorization_code", code, redirect_uri: redirectUri, client_id: clientId, client_secret: clientSecret, code_verifier: codeVerifier, }), }); if (!tokenResponse.ok) { redirect(302, "/?error=token_exchange_failed"); } const tokens = (await tokenResponse.json()) as { access_token: string; refresh_token?: string; expires_in?: number; }; // Fetch user info const userinfoResponse = await fetch(`${authBaseUrl}/userinfo`, { headers: { Authorization: `Bearer ${tokens.access_token}` }, }); if (!userinfoResponse.ok) { redirect(302, "/?error=userinfo_failed"); } const userinfo = (await userinfoResponse.json()) as { sub?: string; id?: string; email: string; name?: string; email_verified?: boolean; }; const userId = userinfo.sub || userinfo.id; const email = userinfo.email; if (!userId || !email) { redirect(302, "/?error=incomplete_profile"); } // Set session cookies
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기