|
| 1 | +--- |
| 2 | +name: heartwood-auth |
| 3 | +description: Integrate Heartwood (GroveAuth) authentication into Grove applications. Use when adding sign-in, protecting routes, or validating sessions in any Grove property. |
| 4 | +--- |
| 5 | + |
| 6 | +# Heartwood Auth Integration Skill |
| 7 | + |
| 8 | +## When to Activate |
| 9 | + |
| 10 | +Activate this skill when: |
| 11 | +- Adding authentication to a Grove application |
| 12 | +- Protecting admin routes |
| 13 | +- Validating user sessions |
| 14 | +- Setting up OAuth sign-in |
| 15 | +- Integrating with Heartwood (GroveAuth) |
| 16 | + |
| 17 | +## Overview |
| 18 | + |
| 19 | +**Heartwood** is Grove's centralized authentication service powered by Better Auth. |
| 20 | + |
| 21 | +| Domain | Purpose | |
| 22 | +|--------|---------| |
| 23 | +| `heartwood.grove.place` | Frontend (login UI) | |
| 24 | +| `auth-api.grove.place` | Backend API | |
| 25 | + |
| 26 | +### Key Features |
| 27 | + |
| 28 | +- **OAuth Providers**: Google |
| 29 | +- **Magic Links**: Click-to-login emails via Resend |
| 30 | +- **Passkeys**: WebAuthn passwordless authentication |
| 31 | +- **KV-Cached Sessions**: Sub-100ms validation |
| 32 | +- **Cross-Subdomain SSO**: Single session across all .grove.place |
| 33 | + |
| 34 | +## Integration Approaches |
| 35 | + |
| 36 | +### Option A: Better Auth Client (Recommended) |
| 37 | + |
| 38 | +For new integrations, use Better Auth's client library: |
| 39 | + |
| 40 | +```typescript |
| 41 | +// src/lib/auth/client.ts |
| 42 | +import { createAuthClient } from 'better-auth/client'; |
| 43 | + |
| 44 | +export const auth = createAuthClient({ |
| 45 | + baseURL: 'https://auth-api.grove.place' |
| 46 | +}); |
| 47 | + |
| 48 | +// Sign in with Google |
| 49 | +await auth.signIn.social({ provider: 'google' }); |
| 50 | + |
| 51 | +// Get current session |
| 52 | +const session = await auth.getSession(); |
| 53 | + |
| 54 | +// Sign out |
| 55 | +await auth.signOut(); |
| 56 | +``` |
| 57 | + |
| 58 | +### Option B: Cookie-Based SSO (*.grove.place apps) |
| 59 | + |
| 60 | +For apps on `.grove.place` subdomains, sessions work automatically via cookies: |
| 61 | + |
| 62 | +```typescript |
| 63 | +// src/hooks.server.ts |
| 64 | +import type { Handle } from '@sveltejs/kit'; |
| 65 | + |
| 66 | +export const handle: Handle = async ({ event, resolve }) => { |
| 67 | + // Check session via Heartwood API |
| 68 | + const sessionCookie = event.cookies.get('better-auth.session_token'); |
| 69 | + |
| 70 | + if (sessionCookie) { |
| 71 | + try { |
| 72 | + const response = await fetch('https://auth-api.grove.place/api/auth/session', { |
| 73 | + headers: { |
| 74 | + Cookie: `better-auth.session_token=${sessionCookie}` |
| 75 | + } |
| 76 | + }); |
| 77 | + |
| 78 | + if (response.ok) { |
| 79 | + const data = await response.json(); |
| 80 | + event.locals.user = data.user; |
| 81 | + event.locals.session = data.session; |
| 82 | + } |
| 83 | + } catch { |
| 84 | + // Session invalid or expired |
| 85 | + } |
| 86 | + } |
| 87 | + |
| 88 | + return resolve(event); |
| 89 | +}; |
| 90 | +``` |
| 91 | + |
| 92 | +### Option C: Legacy Token Flow (Backwards Compatible) |
| 93 | + |
| 94 | +For existing integrations using the legacy OAuth flow: |
| 95 | + |
| 96 | +```typescript |
| 97 | +// 1. Redirect to Heartwood login |
| 98 | +const params = new URLSearchParams({ |
| 99 | + client_id: 'your-client-id', |
| 100 | + redirect_uri: 'https://yourapp.grove.place/auth/callback', |
| 101 | + state: crypto.randomUUID(), |
| 102 | + code_challenge: await generateCodeChallenge(verifier), |
| 103 | + code_challenge_method: 'S256' |
| 104 | +}); |
| 105 | +redirect(302, `https://auth-api.grove.place/login?${params}`); |
| 106 | + |
| 107 | +// 2. Exchange code for tokens (in callback route) |
| 108 | +const tokens = await fetch('https://auth-api.grove.place/token', { |
| 109 | + method: 'POST', |
| 110 | + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, |
| 111 | + body: new URLSearchParams({ |
| 112 | + grant_type: 'authorization_code', |
| 113 | + code: code, |
| 114 | + redirect_uri: 'https://yourapp.grove.place/auth/callback', |
| 115 | + client_id: 'your-client-id', |
| 116 | + client_secret: env.HEARTWOOD_CLIENT_SECRET, |
| 117 | + code_verifier: verifier |
| 118 | + }) |
| 119 | +}).then(r => r.json()); |
| 120 | + |
| 121 | +// 3. Verify token on protected routes |
| 122 | +const user = await fetch('https://auth-api.grove.place/verify', { |
| 123 | + headers: { Authorization: `Bearer ${tokens.access_token}` } |
| 124 | +}).then(r => r.json()); |
| 125 | +``` |
| 126 | + |
| 127 | +## Protected Routes Pattern |
| 128 | + |
| 129 | +### SvelteKit Layout Protection |
| 130 | + |
| 131 | +```typescript |
| 132 | +// src/routes/admin/+layout.server.ts |
| 133 | +import { redirect } from '@sveltejs/kit'; |
| 134 | +import type { LayoutServerLoad } from './$types'; |
| 135 | + |
| 136 | +export const load: LayoutServerLoad = async ({ locals }) => { |
| 137 | + if (!locals.user) { |
| 138 | + throw redirect(302, '/auth/login'); |
| 139 | + } |
| 140 | + |
| 141 | + return { |
| 142 | + user: locals.user |
| 143 | + }; |
| 144 | +}; |
| 145 | +``` |
| 146 | + |
| 147 | +### API Route Protection |
| 148 | + |
| 149 | +```typescript |
| 150 | +// src/routes/api/protected/+server.ts |
| 151 | +import { json, error } from '@sveltejs/kit'; |
| 152 | +import type { RequestHandler } from './$types'; |
| 153 | + |
| 154 | +export const GET: RequestHandler = async ({ locals }) => { |
| 155 | + if (!locals.user) { |
| 156 | + throw error(401, 'Unauthorized'); |
| 157 | + } |
| 158 | + |
| 159 | + return json({ message: 'Protected data', user: locals.user }); |
| 160 | +}; |
| 161 | +``` |
| 162 | + |
| 163 | +## Session Validation |
| 164 | + |
| 165 | +### Via Better Auth Session Endpoint |
| 166 | + |
| 167 | +```typescript |
| 168 | +async function validateSession(sessionToken: string) { |
| 169 | + const response = await fetch('https://auth-api.grove.place/api/auth/session', { |
| 170 | + headers: { |
| 171 | + Cookie: `better-auth.session_token=${sessionToken}` |
| 172 | + } |
| 173 | + }); |
| 174 | + |
| 175 | + if (!response.ok) return null; |
| 176 | + |
| 177 | + const data = await response.json(); |
| 178 | + return data.session ? data : null; |
| 179 | +} |
| 180 | +``` |
| 181 | + |
| 182 | +### Via Legacy Verify Endpoint |
| 183 | + |
| 184 | +```typescript |
| 185 | +async function validateToken(accessToken: string) { |
| 186 | + const response = await fetch('https://auth-api.grove.place/verify', { |
| 187 | + headers: { |
| 188 | + Authorization: `Bearer ${accessToken}` |
| 189 | + } |
| 190 | + }); |
| 191 | + |
| 192 | + const data = await response.json(); |
| 193 | + return data.active ? data : null; |
| 194 | +} |
| 195 | +``` |
| 196 | + |
| 197 | +## Client Registration |
| 198 | + |
| 199 | +To integrate a new app with Heartwood, you need to register it as a client. |
| 200 | + |
| 201 | +### 1. Generate Client Credentials |
| 202 | + |
| 203 | +```bash |
| 204 | +# Generate a secure client secret |
| 205 | +openssl rand -base64 32 |
| 206 | +# Example: YKzJChC3RPjZvd1f/OD5zUGAvcouOTXG7maQP1ernCg= |
| 207 | + |
| 208 | +# Hash it for storage (base64url encoding) |
| 209 | +echo -n "YOUR_SECRET" | openssl dgst -sha256 -binary | base64 | tr '+/' '-_' | tr -d '=' |
| 210 | +``` |
| 211 | + |
| 212 | +### 2. Register in Heartwood Database |
| 213 | + |
| 214 | +```sql |
| 215 | +INSERT INTO clients (id, name, client_id, client_secret_hash, redirect_uris, allowed_origins) |
| 216 | +VALUES ( |
| 217 | + lower(hex(randomblob(16))), |
| 218 | + 'Your App Name', |
| 219 | + 'your-app-id', |
| 220 | + 'BASE64URL_HASHED_SECRET', |
| 221 | + '["https://yourapp.grove.place/auth/callback"]', |
| 222 | + '["https://yourapp.grove.place"]' |
| 223 | +); |
| 224 | +``` |
| 225 | + |
| 226 | +### 3. Set Secrets on Your App |
| 227 | + |
| 228 | +```bash |
| 229 | +# Set the client secret on your app |
| 230 | +wrangler secret put HEARTWOOD_CLIENT_SECRET |
| 231 | +# Paste: YKzJChC3RPjZvd1f/OD5zUGAvcouOTXG7maQP1ernCg= |
| 232 | +``` |
| 233 | + |
| 234 | +## Environment Variables |
| 235 | + |
| 236 | +| Variable | Description | |
| 237 | +|----------|-------------| |
| 238 | +| `HEARTWOOD_CLIENT_ID` | Your registered client ID | |
| 239 | +| `HEARTWOOD_CLIENT_SECRET` | Your client secret (never commit!) | |
| 240 | + |
| 241 | +## API Endpoints Reference |
| 242 | + |
| 243 | +### Better Auth Endpoints (Recommended) |
| 244 | + |
| 245 | +| Method | Endpoint | Purpose | |
| 246 | +|--------|----------|---------| |
| 247 | +| POST | `/api/auth/sign-in/social` | OAuth sign-in | |
| 248 | +| POST | `/api/auth/sign-in/magic-link` | Magic link sign-in | |
| 249 | +| POST | `/api/auth/sign-in/passkey` | Passkey sign-in | |
| 250 | +| GET | `/api/auth/session` | Get current session | |
| 251 | +| POST | `/api/auth/sign-out` | Sign out | |
| 252 | + |
| 253 | +### Legacy Endpoints |
| 254 | + |
| 255 | +| Method | Endpoint | Purpose | |
| 256 | +|--------|----------|---------| |
| 257 | +| GET | `/login` | Login page | |
| 258 | +| POST | `/token` | Exchange code for tokens | |
| 259 | +| GET | `/verify` | Validate access token | |
| 260 | +| GET | `/userinfo` | Get user info | |
| 261 | + |
| 262 | +## Best Practices |
| 263 | + |
| 264 | +### DO |
| 265 | +- Use Better Auth client for new integrations |
| 266 | +- Validate sessions on every protected request |
| 267 | +- Use `httpOnly` cookies for token storage |
| 268 | +- Implement proper error handling for auth failures |
| 269 | +- Log out users gracefully when sessions expire |
| 270 | + |
| 271 | +### DON'T |
| 272 | +- Store tokens in localStorage (XSS vulnerable) |
| 273 | +- Skip session validation on API routes |
| 274 | +- Hardcode client secrets |
| 275 | +- Ignore token expiration |
| 276 | + |
| 277 | +## Cross-Subdomain SSO |
| 278 | + |
| 279 | +All `.grove.place` apps share the same session cookie automatically: |
| 280 | + |
| 281 | +``` |
| 282 | +better-auth.session_token (domain=.grove.place) |
| 283 | +``` |
| 284 | + |
| 285 | +Once a user signs in on any Grove property, they're signed in everywhere. |
| 286 | + |
| 287 | +## Troubleshooting |
| 288 | + |
| 289 | +### "Session not found" errors |
| 290 | +- Check cookie domain is `.grove.place` |
| 291 | +- Verify SESSION_KV namespace is accessible |
| 292 | +- Check session hasn't expired |
| 293 | + |
| 294 | +### OAuth callback errors |
| 295 | +- Verify redirect_uri matches registered client |
| 296 | +- Check client_id is correct |
| 297 | +- Ensure client_secret_hash uses base64url encoding |
| 298 | + |
| 299 | +### Slow authentication |
| 300 | +- Ensure KV caching is enabled (SESSION_KV binding) |
| 301 | +- Check for cold start issues (Workers may sleep) |
| 302 | + |
| 303 | +## Related Resources |
| 304 | + |
| 305 | +- **Heartwood Spec**: `/Users/autumn/Documents/Projects/GroveAuth/GROVEAUTH_SPEC.md` |
| 306 | +- **Better Auth Docs**: https://better-auth.com |
| 307 | +- **Client Setup Guide**: `/Users/autumn/Documents/Projects/GroveAuth/docs/OAUTH_CLIENT_SETUP.md` |
0 commit comments