mirror of
https://github.com/wahyd4/heygo.git
synced 2026-08-08 21:05:48 +10:00
docs: record auth provider decision for workers
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
# Auth provider spike: Cloudflare Workers OAuth
|
||||
|
||||
## Decision summary
|
||||
|
||||
**Choose Better Auth as the preferred auth route implementation for Heygo**, using its Cloudflare Workers/D1-compatible runtime path and configuring only Google and GitHub social providers for MVP.
|
||||
|
||||
For this task, the code change is intentionally small: `worker/auth.ts` records provider/session constants and cookie policy helpers for the next session-guard task. We are **not** installing Better Auth or implementing the OAuth callback flow yet, because Task 7 is the session guard and there are no production OAuth secrets or remote Cloudflare resources in this workspace.
|
||||
|
||||
Apple is **disabled by default**. Add Sign in with Apple only after confirming an active paid Apple Developer Program membership and the required web credentials exist.
|
||||
|
||||
## Chosen auth approach for this project
|
||||
|
||||
1. **Primary path:** Better Auth on Cloudflare Workers with D1.
|
||||
- Better Auth currently publishes as `better-auth@1.6.19`.
|
||||
- Its docs describe D1/SQLite support through the built-in Kysely/D1 path, programmatic migrations for Cloudflare/serverless environments, social providers, and custom table/column names through `modelName`/`fields`.
|
||||
- It supports Google and GitHub social provider configuration in code.
|
||||
2. **Project integration style:** mount Better Auth under an API prefix such as `/api/auth/*` in the Worker, backed by the existing `DB` D1 binding.
|
||||
3. **MVP session guard:** Task 7 should read and validate the existing `sessions` table directly using an HttpOnly cookie token, so private `my.heygo.cc` routes can be protected before the full OAuth login/callback route is wired.
|
||||
4. **Fallback if Better Auth schema/runtime integration blocks implementation:** manual OAuth authorization-code flow using Workers-native `fetch`, Web Crypto, and D1. Auth.js with `@auth/d1-adapter` remains a secondary fallback but is less attractive because its D1 adapter creates fixed core table names (`accounts`, `sessions`, `users`, `verification_tokens`) and does not fit the existing `oauth_accounts` table as cleanly.
|
||||
|
||||
## Why this fits Cloudflare Workers
|
||||
|
||||
- Uses standard Web APIs available in Workers (`Request`, `Response`, `fetch`, `URL`, `Headers`, Web Crypto) rather than Node-only `fs`, raw `net`, or Node `crypto` APIs.
|
||||
- Can use the existing Cloudflare D1 binding (`env.DB`) as the system of record for users, OAuth accounts, and sessions.
|
||||
- Better Auth docs explicitly cover database-backed auth, D1/SQLite migrations, and programmatic migration support for Cloudflare/serverless contexts.
|
||||
- No new package was installed for this spike, avoiding lockfile churn before OAuth routes are implemented.
|
||||
|
||||
## Mapping to existing D1 tables
|
||||
|
||||
Existing migration tables are already close to the MVP auth model:
|
||||
|
||||
### `users`
|
||||
|
||||
Use as the canonical application user table.
|
||||
|
||||
| Existing column | Auth meaning |
|
||||
| --- | --- |
|
||||
| `id` | app user id |
|
||||
| `email` | provider email, nullable for providers that do not return/verify email |
|
||||
| `name` | display name |
|
||||
| `image_url` | avatar/profile image URL |
|
||||
| `role` | app authorization role (`user`/`admin`) |
|
||||
| `created_at`, `updated_at` | audit timestamps |
|
||||
|
||||
### `oauth_accounts`
|
||||
|
||||
Use as the provider account-link table.
|
||||
|
||||
| Existing column | Auth meaning |
|
||||
| --- | --- |
|
||||
| `id` | local account-link id |
|
||||
| `user_id` | foreign key to `users.id` |
|
||||
| `provider` | `google`, `github`, or future `apple` |
|
||||
| `provider_account_id` | stable subject/id from the OAuth provider |
|
||||
| `created_at` | account-link creation timestamp |
|
||||
|
||||
If Better Auth requires additional account token columns for refresh/access/id tokens, add nullable columns in a future migration rather than storing tokens in code or secrets files.
|
||||
|
||||
### `sessions`
|
||||
|
||||
Use as the session table for the Worker session guard.
|
||||
|
||||
| Existing column | Auth meaning |
|
||||
| --- | --- |
|
||||
| `id` | session row id |
|
||||
| `user_id` | foreign key to `users.id` |
|
||||
| `session_token_hash` | hash of the opaque cookie token; never store raw cookie token |
|
||||
| `expires_at` | absolute session expiry |
|
||||
| `created_at` | session creation timestamp |
|
||||
|
||||
Task 7 should query this table by a hash of the cookie token, reject expired sessions, and join/load the associated `users` row.
|
||||
|
||||
## Cookie/session policy
|
||||
|
||||
Session cookie name: `heygo_session`.
|
||||
|
||||
Production cookie attributes:
|
||||
|
||||
```text
|
||||
HttpOnly; Secure; SameSite=Lax; Path=/; Domain=.heygo.cc
|
||||
```
|
||||
|
||||
Rationale:
|
||||
|
||||
- `HttpOnly` prevents client-side JavaScript from reading the session token.
|
||||
- `Secure` restricts transmission to HTTPS.
|
||||
- `SameSite=Lax` supports ordinary top-level OAuth redirects while reducing CSRF exposure.
|
||||
- `Path=/` makes the cookie available to the whole app.
|
||||
- `Domain=.heygo.cc` allows the session set on `heygo.cc` to be visible to `my.heygo.cc`.
|
||||
|
||||
Local development fallback: keep the same attributes but **omit `Domain`** for `localhost`/non-heygo hosts so browsers accept the host-only cookie.
|
||||
|
||||
## Provider support
|
||||
|
||||
Enabled for MVP:
|
||||
|
||||
- Google OAuth
|
||||
- GitHub OAuth
|
||||
|
||||
Disabled by default:
|
||||
|
||||
- Apple / Sign in with Apple
|
||||
|
||||
`worker/auth.ts` now encodes this as:
|
||||
|
||||
```ts
|
||||
AUTH_ENABLED_PROVIDERS = ['google', 'github']
|
||||
AUTH_DISABLED_PROVIDERS = ['apple']
|
||||
```
|
||||
|
||||
## Apple checkpoint
|
||||
|
||||
Do **not** block MVP on Apple.
|
||||
|
||||
Sign in with Apple for web requires Apple developer credentials such as:
|
||||
|
||||
- Service ID
|
||||
- Team ID
|
||||
- Key ID
|
||||
- private key (`.p8`)
|
||||
|
||||
Current research indicates an active paid Apple Developer Program membership is likely required for the resources needed to configure Sign in with Apple for web. That program is commonly priced at **$99/year**. Apple should remain disabled until the account and credentials are confirmed.
|
||||
|
||||
## Secrets needed later
|
||||
|
||||
Set these as Wrangler/Cloudflare secrets or local development variables when OAuth routes are implemented. Do not commit them.
|
||||
|
||||
Required for Google/GitHub MVP:
|
||||
|
||||
- `GOOGLE_CLIENT_ID`
|
||||
- `GOOGLE_CLIENT_SECRET`
|
||||
- `GITHUB_CLIENT_ID`
|
||||
- `GITHUB_CLIENT_SECRET`
|
||||
- `SESSION_SECRET`
|
||||
|
||||
Optional only if Apple is enabled later:
|
||||
|
||||
- `APPLE_CLIENT_ID` / Service ID
|
||||
- `APPLE_TEAM_ID`
|
||||
- `APPLE_KEY_ID`
|
||||
- `APPLE_PRIVATE_KEY`
|
||||
|
||||
## Next implementation steps for Task 7 session guard
|
||||
|
||||
1. Add `SESSION_SECRET` to the Worker `Env` type only when code needs it.
|
||||
2. Parse `Cookie` headers and extract `heygo_session`.
|
||||
3. Hash the opaque token with Workers-compatible Web Crypto; do not use Node `crypto`.
|
||||
4. Query D1 `sessions` by `session_token_hash` and require `expires_at` to be in the future.
|
||||
5. Load the matching `users` row and expose a typed authenticated user/session context to protected `my.heygo.cc` routes.
|
||||
6. Return 401/redirect for missing, invalid, or expired sessions.
|
||||
7. Keep `/api/auth/*` reserved for the later OAuth route implementation.
|
||||
|
||||
## Verification notes
|
||||
|
||||
Research checked during this spike:
|
||||
|
||||
- `npm view better-auth` reports `1.6.19` and a dependency stack centered on Web-compatible TypeScript packages such as Kysely/Jose/Noble.
|
||||
- Better Auth docs/search results describe social providers, D1 support, programmatic migrations for Cloudflare/serverless, and custom table/column names.
|
||||
- Auth.js D1 adapter docs confirm it is available for D1 but creates fixed core tables including `accounts`, `sessions`, `users`, and `verification_tokens`.
|
||||
- Apple/third-party provider docs confirm the web credential shape for Sign in with Apple and the likely paid Apple Developer Program requirement.
|
||||
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
AUTH_DISABLED_PROVIDERS,
|
||||
AUTH_ENABLED_PROVIDERS,
|
||||
AUTH_SESSION_COOKIE_NAME,
|
||||
getSessionCookieAttributes,
|
||||
isAuthProviderEnabled,
|
||||
} from '../worker/auth';
|
||||
|
||||
describe('auth configuration constants', () => {
|
||||
it('enables Google and GitHub while keeping Apple disabled', () => {
|
||||
expect(AUTH_ENABLED_PROVIDERS).toEqual(['google', 'github']);
|
||||
expect(AUTH_DISABLED_PROVIDERS).toEqual(['apple']);
|
||||
expect(isAuthProviderEnabled('google')).toBe(true);
|
||||
expect(isAuthProviderEnabled('github')).toBe(true);
|
||||
expect(isAuthProviderEnabled('apple')).toBe(false);
|
||||
});
|
||||
|
||||
it('uses the canonical session cookie name', () => {
|
||||
expect(AUTH_SESSION_COOKIE_NAME).toBe('heygo_session');
|
||||
});
|
||||
});
|
||||
|
||||
describe('session cookie policy', () => {
|
||||
it('uses a shared heygo.cc cookie domain in production', () => {
|
||||
expect(getSessionCookieAttributes('https://my.heygo.cc/app')).toEqual([
|
||||
'HttpOnly',
|
||||
'Secure',
|
||||
'SameSite=Lax',
|
||||
'Path=/',
|
||||
'Domain=.heygo.cc',
|
||||
]);
|
||||
});
|
||||
|
||||
it('omits Domain for localhost development', () => {
|
||||
expect(getSessionCookieAttributes('http://localhost:5173/app')).toEqual([
|
||||
'HttpOnly',
|
||||
'Secure',
|
||||
'SameSite=Lax',
|
||||
'Path=/',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
export const AUTH_SESSION_COOKIE_NAME = 'heygo_session' as const;
|
||||
|
||||
export const AUTH_ENABLED_PROVIDERS = ['google', 'github'] as const;
|
||||
export const AUTH_DISABLED_PROVIDERS = ['apple'] as const;
|
||||
|
||||
export type EnabledAuthProvider = (typeof AUTH_ENABLED_PROVIDERS)[number];
|
||||
export type DisabledAuthProvider = (typeof AUTH_DISABLED_PROVIDERS)[number];
|
||||
export type AuthProvider = EnabledAuthProvider | DisabledAuthProvider;
|
||||
|
||||
export interface AuthUser {
|
||||
id: string;
|
||||
email: string | null;
|
||||
name: string | null;
|
||||
imageUrl: string | null;
|
||||
role: 'user' | 'admin';
|
||||
}
|
||||
|
||||
export interface AuthSession {
|
||||
id: string;
|
||||
userId: string;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
const ENABLED_PROVIDER_SET = new Set<string>(AUTH_ENABLED_PROVIDERS);
|
||||
|
||||
export function isAuthProviderEnabled(provider: string): provider is EnabledAuthProvider {
|
||||
return ENABLED_PROVIDER_SET.has(provider);
|
||||
}
|
||||
|
||||
export function getSessionCookieAttributes(requestUrl: string | URL): string[] {
|
||||
const url = typeof requestUrl === 'string' ? new URL(requestUrl) : requestUrl;
|
||||
const attributes = ['HttpOnly', 'Secure', 'SameSite=Lax', 'Path=/'];
|
||||
|
||||
if (isHeygoProductionHost(url.hostname)) {
|
||||
attributes.push('Domain=.heygo.cc');
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
function isHeygoProductionHost(hostname: string): boolean {
|
||||
return hostname === 'heygo.cc' || hostname.endsWith('.heygo.cc');
|
||||
}
|
||||
Reference in New Issue
Block a user