Skip to content

Commit 89295dd

Browse files
committed
updated auth and ci/cd pipeline
1 parent c5a90cf commit 89295dd

10 files changed

Lines changed: 641 additions & 19 deletions

File tree

.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,11 @@ STACKONE_MCP_URL=https://api.stackone.com/mcp
99
STACKONE_API_KEY=your_stackone_api_key_here
1010
STACKONE_ACCOUNT_ID=your_pylon_account_id_here
1111

12+
# Google OAuth Configuration
13+
GOOGLE_CLIENT_ID=your_google_client_id_here
14+
GOOGLE_CLIENT_SECRET=your_google_client_secret_here
15+
AUTH_REDIRECT_URI=http://localhost:3000/auth/callback
16+
COOKIE_SECRET=your_random_secret_at_least_32_chars_here
17+
1218
# Server Configuration
1319
PORT=3000

bun.lock

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# Google OAuth Gating for Dashboard
2+
3+
## Summary
4+
5+
Replace basic auth with Google OAuth, restricting access to @stackone.com email domain only. Uses Arctic OAuth library for edge-compatible implementation on Cloudflare Workers.
6+
7+
## Requirements
8+
9+
- Google OAuth as sole authentication method
10+
- Only @stackone.com emails allowed
11+
- 24-hour session duration
12+
- Remove existing basic auth
13+
14+
## Architecture
15+
16+
```
17+
┌─────────────┐ ┌─────────────────┐ ┌─────────────┐
18+
│ Browser │────▶│ Cloudflare Worker│────▶│ Google OAuth│
19+
│ │◀────│ (Hono + Arctic)│◀────│ Server │
20+
└─────────────┘ └─────────────────┘ └─────────────┘
21+
22+
┌───────┴───────┐
23+
│ Signed Cookie │
24+
│ (Session) │
25+
└───────────────┘
26+
```
27+
28+
**Components:**
29+
- **Arctic** - handles OAuth 2.0 flow with Google
30+
- **Hono middleware** - protects routes, redirects unauthenticated users
31+
- **Signed cookie** - stores session (email + expiry), signed with secret key
32+
- **No database sessions** - stateless auth via cookie signature verification
33+
34+
**New files:**
35+
- `src/auth/google.ts` - Arctic Google provider setup
36+
- `src/auth/session.ts` - Cookie session management
37+
- `src/auth/middleware.ts` - Auth middleware for route protection
38+
- `src/auth/routes.ts` - `/auth/login`, `/auth/callback`, `/auth/logout`
39+
40+
## Authentication Flow
41+
42+
**Login (`/auth/login`):**
43+
1. Generate random `state` parameter (CSRF protection)
44+
2. Store `state` in short-lived cookie (5 min)
45+
3. Redirect to Google OAuth consent screen
46+
47+
**Callback (`/auth/callback`):**
48+
1. Validate `state` matches cookie (prevents CSRF)
49+
2. Exchange authorization code for tokens via Arctic
50+
3. Fetch user info from Google (email, name)
51+
4. Reject if email domain ≠ @stackone.com
52+
5. Create signed session cookie (24h expiry)
53+
6. Redirect to dashboard (`/`)
54+
55+
**Logout (`/auth/logout`):**
56+
1. Clear session cookie
57+
2. Redirect to `/auth/login`
58+
59+
**Unauthenticated access:**
60+
- Any protected route → redirect to `/auth/login`
61+
- After login → redirect back to originally requested URL
62+
63+
## Session Management
64+
65+
**Session cookie structure:**
66+
```typescript
67+
{
68+
email: "user@stackone.com",
69+
name: "User Name",
70+
exp: 1711324800 // Unix timestamp (24h from login)
71+
}
72+
```
73+
74+
**Cookie configuration:**
75+
- **Name:** `session`
76+
- **Signed:** Yes, using `COOKIE_SECRET` env var (HMAC-SHA256)
77+
- **HttpOnly:** Yes (not accessible via JavaScript)
78+
- **Secure:** Yes (HTTPS only in production)
79+
- **SameSite:** Lax (allows redirects from Google)
80+
- **Max-Age:** 86400 (24 hours)
81+
82+
## Route Protection
83+
84+
**Protected routes (require auth):**
85+
- `/` - Dashboard
86+
- `/api/*` - All API endpoints (except webhook)
87+
88+
**Public routes (no auth):**
89+
- `/auth/login` - Login page/redirect
90+
- `/auth/callback` - OAuth callback
91+
- `/auth/logout` - Logout
92+
- `/health` - Health check
93+
- `/api/pylon/webhook` - Pylon webhook (has its own HMAC verification)
94+
95+
## Error Handling
96+
97+
**OAuth errors (callback failures):**
98+
- Invalid state → redirect to `/auth/login?error=invalid_state`
99+
- Google denies access → redirect to `/auth/login?error=access_denied`
100+
- Token exchange fails → redirect to `/auth/login?error=token_error`
101+
102+
**Domain rejection:**
103+
- Non-@stackone.com email → redirect to `/auth/login?error=unauthorized_domain`
104+
105+
**Error messages:**
106+
| Error | Message |
107+
|-------|---------|
108+
| `unauthorized_domain` | "Access restricted to @stackone.com accounts" |
109+
| `access_denied` | "Google sign-in was cancelled" |
110+
| `invalid_state` | "Session expired. Please try again." |
111+
| `token_error` | "Authentication failed. Please try again." |
112+
113+
## Environment Configuration
114+
115+
**New environment variables:**
116+
117+
| Variable | Description |
118+
|----------|-------------|
119+
| `GOOGLE_CLIENT_ID` | OAuth client ID from Google Cloud Console |
120+
| `GOOGLE_CLIENT_SECRET` | OAuth client secret |
121+
| `COOKIE_SECRET` | Random string for signing cookies (32+ chars) |
122+
| `AUTH_REDIRECT_URI` | OAuth callback URL |
123+
124+
**Removed:**
125+
- `DASHBOARD_PASSWORD` - no longer needed
126+
127+
**Google Cloud Console setup required:**
128+
1. Create OAuth 2.0 credentials (Web application)
129+
2. Add authorized redirect URI: `{domain}/auth/callback`
130+
3. Enable Google People API (for user info)

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
},
1212
"dependencies": {
1313
"@anthropic-ai/sdk": "^0.78.0",
14+
"arctic": "^3.7.0",
1415
"hono": "^4.12.7",
1516
"react": "^19.0.0",
1617
"react-dom": "^19.0.0"

src/auth/google.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Google } from 'arctic';
2+
3+
export type AuthEnv = {
4+
GOOGLE_CLIENT_ID: string;
5+
GOOGLE_CLIENT_SECRET: string;
6+
AUTH_REDIRECT_URI: string;
7+
COOKIE_SECRET: string;
8+
};
9+
10+
let googleClient: Google | null = null;
11+
let authEnv: AuthEnv | null = null;
12+
13+
export function setAuthEnv(env: AuthEnv): void {
14+
authEnv = env;
15+
googleClient = new Google(
16+
env.GOOGLE_CLIENT_ID,
17+
env.GOOGLE_CLIENT_SECRET,
18+
env.AUTH_REDIRECT_URI
19+
);
20+
}
21+
22+
export function getGoogleClient(): Google {
23+
if (!googleClient) {
24+
throw new Error('Google OAuth client not initialized. Call setAuthEnv first.');
25+
}
26+
return googleClient;
27+
}
28+
29+
export function getAuthEnv(): AuthEnv {
30+
if (!authEnv) {
31+
throw new Error('Auth env not initialized. Call setAuthEnv first.');
32+
}
33+
return authEnv;
34+
}
35+
36+
export function isAuthConfigured(): boolean {
37+
return authEnv !== null;
38+
}
39+
40+
export const ALLOWED_DOMAIN = 'stackone.com';

src/auth/middleware.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { Context, Next } from 'hono';
2+
import { getSession, type SessionData } from './session';
3+
import { isAuthConfigured } from './google';
4+
5+
declare module 'hono' {
6+
interface ContextVariableMap {
7+
user: SessionData;
8+
}
9+
}
10+
11+
export async function authMiddleware(c: Context, next: Next): Promise<Response | void> {
12+
// Check if auth is configured
13+
if (!isAuthConfigured()) {
14+
return c.html(`
15+
<!DOCTYPE html>
16+
<html>
17+
<head><title>Auth Not Configured</title></head>
18+
<body style="font-family: sans-serif; padding: 40px; background: #0f172a; color: #e2e8f0;">
19+
<h1>Authentication Not Configured</h1>
20+
<p>Google OAuth is not configured. Please set the following secrets:</p>
21+
<ul>
22+
<li>GOOGLE_CLIENT_ID</li>
23+
<li>GOOGLE_CLIENT_SECRET</li>
24+
<li>AUTH_REDIRECT_URI</li>
25+
<li>COOKIE_SECRET</li>
26+
</ul>
27+
<p>Run: <code>npx wrangler secret put SECRET_NAME</code> for each.</p>
28+
</body>
29+
</html>
30+
`, 500);
31+
}
32+
33+
const session = await getSession(c);
34+
35+
if (!session) {
36+
const currentUrl = new URL(c.req.url);
37+
const redirectParam = encodeURIComponent(currentUrl.pathname + currentUrl.search);
38+
return c.redirect(`/auth/login?redirect=${redirectParam}`);
39+
}
40+
41+
c.set('user', session);
42+
await next();
43+
}

0 commit comments

Comments
 (0)