Loading...
Loading...
Weave authentication webs with patient precision. Spin the threads, connect the strands, secure the knots, and bind the system. Use when integrating auth, setting up OAuth, or securing routes.
npx skill4agent add autumnsgrove/groveengine spider-weave/spider-weaveraccoon-auditbeaver-buildSPIN → CONNECT → SECURE → TEST → BIND
↓ ↓ ↓ ↓ ↓
Create Link Harden Verify Lock In
Threads Strands Knots Web Security| Pattern | Best For | Complexity |
|---|---|---|
| Session-based | Traditional web apps | Medium |
| JWT | Stateless APIs, SPAs | Medium |
| OAuth 2.0 | Third-party login | High |
| PKCE | Mobile/SPA OAuth | High |
| API Keys | Service-to-service | Low |
// PKCE flow setup
import { generatePKCE } from '$lib/auth/pkce';
const { codeVerifier, codeChallenge } = await generatePKCE();
// Store verifier (cookie or session)
cookies.set('pkce_verifier', codeVerifier, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 600 // 10 minutes
});
// Redirect to Heartwood
const authUrl = new URL('https://heartwood.grove.place/oauth/authorize');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
authUrl.searchParams.set('state', generateState());src/lib/auth/
├── index.ts # Main exports
├── types.ts # Auth-related types
├── session.ts # Session management
├── middleware.ts # Route protection
├── pkce.ts # PKCE utilities
└── client.ts # Heartwood/OAuth client// Users table (linked to Heartwood)
export const users = sqliteTable('users', {
id: integer('id').primaryKey(),
heartwoodId: text('heartwood_id').unique(),
email: text('email').unique(),
displayName: text('display_name'),
avatarUrl: text('avatar_url'),
role: text('role').default('user'), // admin, user, guest
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
});
// Sessions (if using session-based auth)
export const sessions = sqliteTable('sessions', {
id: text('id').primaryKey(),
userId: integer('user_id').references(() => users.id),
expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(),
});# OAuth/Heartwood
HEARTWOOD_CLIENT_ID=
HEARTWOOD_CLIENT_SECRET=
HEARTWOOD_AUTHORIZE_URL=https://heartwood.grove.place/oauth/authorize
HEARTWOOD_TOKEN_URL=https://heartwood.grove.place/oauth/token
HEARTWOOD_USERINFO_URL=https://heartwood.grove.place/oauth/userinfo
# App
AUTH_REDIRECT_URI=http://localhost:5173/auth/callback
SESSION_SECRET=generate_with_openssl_rand_hex_32// 1. Login route - redirect to provider
// src/routes/auth/login/+server.ts
export const GET: RequestHandler = async () => {
const { codeVerifier, codeChallenge } = generatePKCE();
const state = generateState();
// Store PKCE verifier
cookies.set('pkce_verifier', codeVerifier, { httpOnly: true, secure: true });
cookies.set('oauth_state', state, { httpOnly: true, secure: true });
const url = new URL(HEARTWOOD_AUTHORIZE_URL);
url.searchParams.set('client_id', HEARTWOOD_CLIENT_ID);
url.searchParams.set('code_challenge', codeChallenge);
url.searchParams.set('code_challenge_method', 'S256');
url.searchParams.set('redirect_uri', AUTH_REDIRECT_URI);
url.searchParams.set('state', state);
throw redirect(302, url.toString());
};
// 2. Callback route - handle OAuth response
// src/routes/auth/callback/+server.ts
export const GET: RequestHandler = async ({ url, cookies }) => {
const code = url.searchParams.get('code');
const state = url.searchParams.get('state');
const storedState = cookies.get('oauth_state');
// Verify state (CSRF protection)
if (state !== storedState) {
throw error(400, 'Invalid state parameter');
}
// Exchange code for tokens
const tokenResponse = await fetch(HEARTWOOD_TOKEN_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: HEARTWOOD_CLIENT_ID,
client_secret: HEARTWOOD_CLIENT_SECRET,
code: code!,
code_verifier: cookies.get('pkce_verifier')!,
redirect_uri: AUTH_REDIRECT_URI,
}),
});
const tokens = await tokenResponse.json();
// Get user info
const userResponse = await fetch(HEARTWOOD_USERINFO_URL, {
headers: { Authorization: `Bearer ${tokens.access_token}` },
});
const userInfo = await userResponse.json();
// Create/update user in database
const user = await upsertUser({
heartwoodId: userInfo.sub,
email: userInfo.email,
displayName: userInfo.name,
avatarUrl: userInfo.picture,
});
// Create session
const session = await createSession(user.id);
// Set session cookie
cookies.set('session', session.id, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7, // 7 days
});
// Clean up PKCE cookies
cookies.delete('pkce_verifier');
cookies.delete('oauth_state');
throw redirect(302, '/dashboard');
};// src/lib/auth/session.ts
export async function createSession(userId: number): Promise<Session> {
const sessionId = generateSecureId();
const expiresAt = new Date(Date.now() + SESSION_DURATION);
await db.insert(sessions).values({
id: sessionId,
userId,
expiresAt,
});
return { id: sessionId, userId, expiresAt };
}
export async function validateSession(sessionId: string): Promise<User | null> {
const session = await db.query.sessions.findFirst({
where: eq(sessions.id, sessionId),
with: { user: true },
});
if (!session || session.expiresAt < new Date()) {
return null;
}
return session.user;
}
export async function invalidateSession(sessionId: string): Promise<void> {
await db.delete(sessions).where(eq(sessions.id, sessionId));
}// src/lib/stores/auth.ts
import { writable } from 'svelte/store';
export interface AuthState {
user: User | null;
loading: boolean;
}
export const auth = writable<AuthState>({
user: null,
loading: true,
});
export async function loadUser() {
const response = await fetch('/api/auth/me');
if (response.ok) {
const user = await response.json();
auth.set({ user, loading: false });
} else {
auth.set({ user: null, loading: false });
}
}// src/lib/auth/middleware.ts
export function requireAuth(): Handle {
return async ({ event, resolve }) => {
const sessionId = event.cookies.get('session');
if (!sessionId) {
throw redirect(302, '/auth/login');
}
const user = await validateSession(sessionId);
if (!user) {
event.cookies.delete('session');
throw redirect(302, '/auth/login');
}
event.locals.user = user;
return resolve(event);
};
}
// Role-based protection
export function requireRole(allowedRoles: string[]): Handle {
return async ({ event, resolve }) => {
const user = event.locals.user;
if (!user || !allowedRoles.includes(user.role)) {
throw error(403, 'Forbidden');
}
return resolve(event);
};
}// src/hooks.server.ts
export const handle: Handle = async ({ event, resolve }) => {
const response = await resolve(event);
// Security headers
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
response.headers.set(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
);
return response;
};// For state-changing operations
export function validateCSRF(event: RequestEvent): void {
const origin = event.request.headers.get('origin');
const host = event.url.host;
if (origin && new URL(origin).host !== host) {
throw error(403, 'Invalid origin');
}
}// src/lib/auth/rate-limit.ts
const attempts = new Map<string, number[]>();
export function checkRateLimit(identifier: string, maxAttempts: number = 5): boolean {
const now = Date.now();
const windowStart = now - 15 * 60 * 1000; // 15 minutes
const userAttempts = attempts.get(identifier) || [];
const recentAttempts = userAttempts.filter(t => t > windowStart);
if (recentAttempts.length >= maxAttempts) {
return false;
}
recentAttempts.push(now);
attempts.set(identifier, recentAttempts);
return true;
}// Always use these for auth cookies
{
httpOnly: true, // Not accessible via JavaScript
secure: true, // HTTPS only in production
sameSite: 'lax', // CSRF protection
maxAge: 604800, // 7 days
path: '/', // Available site-wide
}// tests/auth/oauth.test.ts
describe('OAuth Flow', () => {
test('redirects to Heartwood with PKCE', async () => {
const response = await request(app).get('/auth/login');
expect(response.status).toBe(302);
expect(response.headers.location).toMatch(/heartwood\.grove\.place/);
expect(response.headers.location).toMatch(/code_challenge=/);
});
test('handles callback and creates session', async () => {
// Mock Heartwood responses
mockHeartwoodTokenEndpoint({ access_token: 'test-token' });
mockHeartwoodUserInfo({ sub: '123', email: 'test@example.com' });
const response = await request(app)
.get('/auth/callback?code=valid-code&state=valid-state')
.set('Cookie', ['oauth_state=valid-state; pkce_verifier=test-verifier']);
expect(response.status).toBe(302);
expect(response.headers.location).toBe('/dashboard');
// Verify session created
const cookies = response.headers['set-cookie'];
expect(cookies).toMatch(/session=/);
});
test('rejects invalid state (CSRF protection)', async () => {
const response = await request(app)
.get('/auth/callback?code=valid-code&state=wrong-state')
.set('Cookie', ['oauth_state=correct-state']);
expect(response.status).toBe(400);
});
});
describe('Route Protection', () => {
test('redirects unauthenticated users', async () => {
const response = await request(app).get('/dashboard');
expect(response.status).toBe(302);
expect(response.headers.location).toBe('/auth/login');
});
test('allows authenticated users', async () => {
const session = await createTestUserAndSession();
const response = await request(app)
.get('/dashboard')
.set('Cookie', [`session=${session.id}`]);
expect(response.status).toBe(200);
});
test('enforces role restrictions', async () => {
const user = await createTestUser({ role: 'user' });
const session = await createSession(user.id);
const response = await request(app)
.get('/admin')
.set('Cookie', [`session=${session.id}`]);
expect(response.status).toBe(403);
});
});// Test session fixation
test('session ID changes after login', async () => {
const oldSession = cookies.get('session');
await completeLoginFlow();
const newSession = cookies.get('session');
expect(newSession).not.toBe(oldSession);
});
// Test cookie security
test('auth cookies have secure attributes', async () => {
const response = await completeLoginFlow();
const cookies = response.headers['set-cookie'];
expect(cookies).toMatch(/HttpOnly/);
expect(cookies).toMatch(/SameSite=/);
});<!-- Login button states -->
<script>
let loading = $state(false);
let error = $state('');
</script>
{#if error}
<div role="alert" class="error">
{error}
</div>
{/if}
<button
on:click={handleLogin}
disabled={loading}
aria-busy={loading}
>
{#if loading}
<span class="spinner" aria-hidden="true" />
Connecting...
{:else}
Sign in with Heartwood
{/if}
</button>// Log auth events (without sensitive data)
logger.info('User authenticated', {
userId: user.id,
provider: 'heartwood',
ip: event.getClientAddress(),
});
// Alert on suspicious activity
if (failedAttempts > 10) {
logger.warn('Potential brute force attack', {
identifier,
attempts: failedAttempts,
});
}## Authentication System
### Architecture
- OAuth 2.0 with PKCE for secure token exchange
- Session-based auth for web app
- Heartwood (GroveAuth) as identity provider
### Flow
1. User clicks "Sign in" → Redirect to Heartwood
2. User authenticates with Heartwood
3. Heartwood redirects back with auth code
4. App exchanges code for tokens
5. App creates session, sets cookie
6. User is authenticated
### Protected Routes
Add to `src/routes/protected/+page.server.ts`:
```typescript
export const load = async ({ locals }) => {
if (!locals.user) {
throw redirect(302, '/auth/login');
}
return { user: locals.user };
};.env.example
**Completion Report:**
```markdown
## 🕷️ SPIDER WEAVE COMPLETE
### Auth System Integrated
- Provider: Heartwood (GroveAuth)
- Flow: OAuth 2.0 + PKCE
- Session: Cookie-based, 7-day expiry
### Files Created
- `src/lib/auth/` (6 files)
- `src/routes/auth/login/+server.ts`
- `src/routes/auth/callback/+server.ts`
- `src/routes/auth/logout/+server.ts`
- `src/lib/stores/auth.ts`
### Security Features
- ✅ PKCE for OAuth
- ✅ CSRF protection
- ✅ Rate limiting (5 attempts / 15 min)
- ✅ Secure cookie attributes
- ✅ Security headers
- ✅ Role-based access control
### Tests
- 15 unit tests
- 8 integration tests
- 100% pass rate
*The web is woven. The system is secure.* 🕷️| Situation | Approach |
|---|---|
| Simple app, internal users | Session-based auth |
| Public app, social login | OAuth 2.0 + PKCE |
| API for mobile/SPA | JWT with refresh tokens |
| Service-to-service | API keys with IP allowlist |
| Grove ecosystem | Heartwood integration |
eagle-architectswan-designelephant-buildraccoon-auditbeaver-builddeer-sense