Skip to content

Commit b42613a

Browse files
authored
Merge pull request #39 from VectorInstitute/add_auth_for_onboarding_status_page
Add auth for onboarding status page
2 parents 38ffba3 + b078737 commit b42613a

File tree

16 files changed

+1033
-471
lines changed

16 files changed

+1033
-471
lines changed

.github/workflows/deploy-onboarding-status-web.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ jobs:
8787
-e GCP_PROJECT_ID=test \
8888
-e FIRESTORE_DATABASE_ID=test \
8989
-e GITHUB_TOKEN=test \
90+
-e NEXT_PUBLIC_GOOGLE_CLIENT_ID=test-client-id \
91+
-e GOOGLE_CLIENT_SECRET=test-secret \
92+
-e SESSION_SECRET=test-session-secret-32chars-min \
93+
-e NEXT_PUBLIC_APP_URL=http://localhost:8080 \
94+
-e ALLOWED_DOMAINS=vectorinstitute.ai \
9095
onboarding-status-web:${{ github.sha }}
9196
9297
# Wait for container to start and verify it stays running
@@ -232,7 +237,7 @@ jobs:
232237
--min-instances=0 \
233238
--concurrency=80 \
234239
--port=8080 \
235-
--set-env-vars="GCP_PROJECT_ID=${{ env.PROJECT_ID }},FIRESTORE_DATABASE_ID=${{ env.FIRESTORE_DATABASE_ID }},GITHUB_TOKEN=${{ secrets.GH_ORG_TOKEN }},DEPLOYMENT_SHA=${{ github.sha }}" \
240+
--set-env-vars="GCP_PROJECT_ID=${{ env.PROJECT_ID }},FIRESTORE_DATABASE_ID=${{ env.FIRESTORE_DATABASE_ID }},GITHUB_TOKEN=${{ secrets.GH_ORG_TOKEN }},DEPLOYMENT_SHA=${{ github.sha }},NEXT_PUBLIC_GOOGLE_CLIENT_ID=${{ secrets.GOOGLE_CLIENT_ID }},GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }},SESSION_SECRET=${{ secrets.SESSION_SECRET }},NEXT_PUBLIC_APP_URL=${{ secrets.APP_URL }},REDIRECT_URI=${{ secrets.REDIRECT_URI }},ALLOWED_DOMAINS=vectorinstitute.ai" \
236241
--update-labels="deployed-by=github-actions,commit=${{ github.sha }}" \
237242
--update-annotations="git-commit=${{ github.sha }},git-ref=${{ github.ref }},deployed-at=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
238243
--quiet

docs/onboarding-status-web-auth.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Onboarding Status Web Authentication
2+
3+
## Overview
4+
5+
The Onboarding Status Web dashboard uses Google OAuth 2.0 with server-side sessions for authentication. Only @vectorinstitute.ai email addresses can access the dashboard.
6+
7+
## Architecture
8+
9+
- **Library**: `@vector-institute/aieng-auth-core`
10+
- **Session Management**: `iron-session` with encrypted HTTP-only cookies
11+
- **Security**: PKCE flow, domain restriction, encrypted sessions
12+
- **Path**: All routes under `/onboarding` base path
13+
14+
## Authentication Flow
15+
16+
1. User visits `/onboarding` → redirected to `/onboarding/login` if not authenticated
17+
2. Click "Sign in with Google" → `/onboarding/api/auth/login`
18+
3. Google OAuth flow with PKCE
19+
4. Callback to `/onboarding/api/auth/callback`
20+
5. Session created, user redirected to dashboard
21+
22+
## Files
23+
24+
### Configuration
25+
- `lib/auth-config.ts` - OAuth config
26+
- `lib/session.ts` - Session management
27+
28+
### API Routes
29+
- `app/api/auth/login/route.ts` - Initiate OAuth
30+
- `app/api/auth/callback/route.ts` - Handle callback
31+
- `app/api/auth/logout/route.ts` - Destroy session
32+
- `app/api/auth/session/route.ts` - Get session info
33+
34+
### Pages
35+
- `app/page.tsx` - Protected dashboard
36+
- `app/login/page.tsx` - Login page
37+
- `app/dashboard-content.tsx` - Dashboard UI
38+
39+
## Environment Variables
40+
41+
```bash
42+
# OAuth
43+
NEXT_PUBLIC_GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
44+
GOOGLE_CLIENT_SECRET=GOCSPX-your-secret
45+
SESSION_SECRET=generate-with-openssl-rand-base64-32
46+
47+
# URLs
48+
NEXT_PUBLIC_APP_URL=http://localhost:3000
49+
REDIRECT_URI=http://localhost:3000/onboarding/api/auth/callback
50+
51+
# Domain restriction
52+
ALLOWED_DOMAINS=vectorinstitute.ai
53+
```
54+
55+
## Local Development
56+
57+
1. Copy OAuth credentials from `aieng-template-auth/apps/demo-nextjs/.env`
58+
2. Update redirect URI in `.env`: `http://localhost:3000/onboarding/api/auth/callback`
59+
3. Run `npm run dev`
60+
4. Visit `http://localhost:3000/onboarding`
61+
5. Sign in with @vectorinstitute.ai account
62+
63+
## Production Deployment
64+
65+
### Required GitHub Secrets
66+
- `GOOGLE_CLIENT_ID` - Shared Vector OAuth client ID
67+
- `GOOGLE_CLIENT_SECRET` - OAuth client secret
68+
- `SESSION_SECRET` - Generated with `openssl rand -base64 32`
69+
- `APP_URL` - Production URL (e.g., `https://your-service.run.app`)
70+
- `REDIRECT_URI` - Production callback URL (e.g., `https://your-service.run.app/onboarding/api/auth/callback`)
71+
72+
### Setup Steps
73+
1. Get shared OAuth client ID from admin
74+
2. Ask admin to add production redirect URI to Google OAuth client
75+
3. Set GitHub secrets in repository settings
76+
4. Deploy via GitHub Actions workflow
77+
78+
## Troubleshooting
79+
80+
**"Invalid redirect_uri"**
81+
- Verify redirect URI registered in Google Cloud Console
82+
- Check `REDIRECT_URI` matches registered value
83+
84+
**"Unauthorized domain"**
85+
- User must have @vectorinstitute.ai email
86+
- Check `ALLOWED_DOMAINS` environment variable
87+
88+
**Session issues**
89+
- Verify `SESSION_SECRET` is at least 32 characters
90+
- Clear browser cookies
91+
92+
## Dependencies
93+
94+
```json
95+
{
96+
"@vector-institute/aieng-auth-core": "^0.1.x",
97+
"iron-session": "^8.0.1"
98+
}
99+
```

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ nav:
3131
- Overview: developer_guide.md
3232
- Infrastructure:
3333
- Onboarding Status Web - Load Balancer Setup: onboarding-status-web-load-balancer-setup.md
34+
- Onboarding Status Web - Authentication: onboarding-status-web-auth.md
3435
plugins:
3536
- search
3637
- mkdocstrings:
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Google Cloud Configuration
2+
GCP_PROJECT_ID=coderd
3+
FIRESTORE_DATABASE_ID=onboarding
4+
5+
# GitHub Configuration
6+
GITHUB_TOKEN=your-github-token-here
7+
8+
# Google OAuth Configuration
9+
NEXT_PUBLIC_GOOGLE_CLIENT_ID=your-client-id-here.apps.googleusercontent.com
10+
GOOGLE_CLIENT_SECRET=GOCSPX-your-client-secret-here
11+
12+
# Session Configuration (generate with: openssl rand -base64 32)
13+
SESSION_SECRET=generate-a-random-32-character-string-here
14+
15+
# App URLs
16+
NEXT_PUBLIC_APP_URL=http://localhost:3000
17+
REDIRECT_URI=http://localhost:3000/onboarding/api/auth/callback
18+
19+
# Optional: Restrict access to specific email domains (comma-separated)
20+
ALLOWED_DOMAINS=vectorinstitute.ai

services/onboarding-status-web/README.md

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ A modern, real-time web dashboard built with Next.js to display participant onbo
44

55
## Features
66

7+
- **Authentication**: Secure Google OAuth SSO with domain restriction (@vectorinstitute.ai)
78
- **Real-time Status Tracking**: Displays live participant onboarding status fetched from Firestore
89
- **Clean, Polished UI**: Modern, responsive design with dark mode support
910
- **Summary Statistics**: Shows total participants, onboarded count, completion percentage
@@ -53,18 +54,24 @@ services/onboarding-status-web/
5354
npm install
5455
```
5556

56-
2. Set environment variables:
57+
2. Set up environment variables:
5758
```bash
58-
export GCP_PROJECT_ID=coderd
59-
export FIRESTORE_DATABASE_ID=onboarding
59+
cp .env.example .env
6060
```
6161

62+
Then edit `.env` and fill in the required values:
63+
- Get Google OAuth credentials from [Google Cloud Console](https://console.cloud.google.com/apis/credentials)
64+
- Generate a session secret: `openssl rand -base64 32`
65+
- Set your GitHub token with appropriate permissions
66+
6267
3. Run the development server:
6368
```bash
6469
npm run dev
6570
```
6671

67-
4. Open [http://localhost:3000](http://localhost:3000) in your browser
72+
4. Open [http://localhost:3000/onboarding](http://localhost:3000/onboarding) in your browser
73+
74+
5. Sign in with a Vector Institute Google account (@vectorinstitute.ai)
6875

6976
## Deployment
7077

@@ -120,15 +127,34 @@ cd /path/to/aieng-platform
120127

121128
## Environment Variables
122129

130+
### Required
123131
- `GCP_PROJECT_ID`: Google Cloud Project ID (default: `coderd`)
124132
- `FIRESTORE_DATABASE_ID`: Firestore database ID (default: `onboarding`)
133+
- `GITHUB_TOKEN`: GitHub personal access token for API access
134+
- `NEXT_PUBLIC_GOOGLE_CLIENT_ID`: Google OAuth client ID
135+
- `GOOGLE_CLIENT_SECRET`: Google OAuth client secret
136+
- `SESSION_SECRET`: Secret for encrypting session cookies (generate with: `openssl rand -base64 32`)
137+
138+
### Optional
139+
- `NEXT_PUBLIC_APP_URL`: Full application URL (default: `http://localhost:3000`)
140+
- `REDIRECT_URI`: OAuth callback URL (default: `${NEXT_PUBLIC_APP_URL}/onboarding/api/auth/callback`)
141+
- `ALLOWED_DOMAINS`: Comma-separated list of allowed email domains (default: `vectorinstitute.ai`)
125142
- `PORT`: Port to run the server on (default: `8080`)
126143

127144
## API Endpoints
128145

146+
### Authentication Endpoints
147+
148+
- `GET /api/auth/login` - Initiates Google OAuth flow
149+
- `GET /api/auth/callback` - Handles OAuth callback and creates session
150+
- `POST /api/auth/logout` - Destroys user session
151+
- `GET /api/auth/session` - Returns current session information
152+
153+
### Data Endpoints
154+
129155
### GET /api/participants
130156

131-
Returns participant onboarding status and summary statistics.
157+
Returns participant onboarding status and summary statistics. Requires authentication.
132158

133159
**Response:**
134160
```json
@@ -174,10 +200,12 @@ Returns participant onboarding status and summary statistics.
174200

175201
## Security
176202

177-
- Uses Google Cloud service account authentication for Firestore access
178-
- Runs as non-root user in Docker container
179-
- Follows Cloud Run security best practices
180-
- CORS configured for API routes
203+
- **OAuth 2.0 Authentication**: Secure Google OAuth with PKCE flow
204+
- **Session Management**: HTTP-only cookies with encrypted sessions using iron-session
205+
- **Domain Restriction**: Only @vectorinstitute.ai email addresses can access
206+
- **Firestore Access**: Service account authentication for database access
207+
- **Container Security**: Runs as non-root user in Docker
208+
- **CORS Configuration**: Properly configured API routes
181209

182210
## Performance
183211

@@ -206,6 +234,15 @@ If the Docker build fails:
206234
2. Check that the `public` directory exists
207235
3. Verify Node.js version compatibility
208236

237+
### Authentication Issues
238+
239+
If you can't sign in:
240+
1. Verify Google OAuth credentials are correct
241+
2. Ensure redirect URI is registered in Google Cloud Console
242+
3. Check that your email domain (@vectorinstitute.ai) is in ALLOWED_DOMAINS
243+
4. Verify SESSION_SECRET is at least 32 characters
244+
5. Check browser console for errors
245+
209246
### Firestore Connection Issues
210247

211248
If the dashboard can't fetch data:
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { GoogleOAuthClient } from '@vector-institute/aieng-auth-core';
3+
import { authConfig } from '@/lib/auth-config';
4+
import { createSession } from '@/lib/session';
5+
import { cookies } from 'next/headers';
6+
7+
export const dynamic = 'force-dynamic';
8+
9+
export async function GET(request: NextRequest) {
10+
try {
11+
const searchParams = request.nextUrl.searchParams;
12+
const code = searchParams.get('code');
13+
const state = searchParams.get('state');
14+
const error = searchParams.get('error');
15+
16+
if (error) {
17+
return NextResponse.redirect(new URL(`/onboarding?error=${encodeURIComponent(error)}`, request.url));
18+
}
19+
20+
if (!code || !state) {
21+
return NextResponse.redirect(new URL('/onboarding?error=invalid_callback', request.url));
22+
}
23+
24+
// Verify state
25+
const cookieStore = await cookies();
26+
const storedState = cookieStore.get('oauth_state')?.value;
27+
if (state !== storedState) {
28+
return NextResponse.redirect(new URL('/onboarding?error=invalid_state', request.url));
29+
}
30+
31+
// Get PKCE verifier
32+
const verifier = cookieStore.get('pkce_verifier')?.value;
33+
if (!verifier) {
34+
return NextResponse.redirect(new URL('/onboarding?error=missing_verifier', request.url));
35+
}
36+
37+
// Exchange code for tokens
38+
const client = new GoogleOAuthClient(authConfig);
39+
40+
const body = new URLSearchParams({
41+
grant_type: 'authorization_code',
42+
code,
43+
redirect_uri: authConfig.redirectUri,
44+
client_id: authConfig.clientId,
45+
client_secret: authConfig.clientSecret,
46+
code_verifier: verifier,
47+
});
48+
49+
const response = await fetch('https://oauth2.googleapis.com/token', {
50+
method: 'POST',
51+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
52+
body: body.toString(),
53+
});
54+
55+
if (!response.ok) {
56+
const errorData = await response.json().catch(() => ({}));
57+
console.error('Token exchange failed:', errorData);
58+
return NextResponse.redirect(new URL('/onboarding?error=token_exchange_failed', request.url));
59+
}
60+
61+
const data = await response.json();
62+
const tokens = {
63+
accessToken: data.access_token,
64+
refreshToken: data.refresh_token,
65+
tokenType: data.token_type || 'Bearer',
66+
expiresIn: data.expires_in,
67+
scope: data.scope,
68+
};
69+
70+
// Get user info
71+
const user = await client.getUserInfo(tokens.accessToken);
72+
73+
// Validate domain if configured
74+
if (authConfig.allowedDomains && authConfig.allowedDomains.length > 0) {
75+
const domain = user.email?.split('@')[1];
76+
if (!domain || !authConfig.allowedDomains.includes(domain)) {
77+
return NextResponse.redirect(
78+
new URL(`/onboarding?error=unauthorized_domain&domain=${domain}`, request.url)
79+
);
80+
}
81+
}
82+
83+
// Create session
84+
await createSession(tokens, user);
85+
86+
// Clean up temporary cookies
87+
const redirectResponse = NextResponse.redirect(new URL('/onboarding', request.url));
88+
redirectResponse.cookies.delete('pkce_verifier');
89+
redirectResponse.cookies.delete('oauth_state');
90+
91+
return redirectResponse;
92+
} catch (error) {
93+
console.error('Callback error:', error);
94+
return NextResponse.redirect(new URL('/onboarding?error=authentication_failed', request.url));
95+
}
96+
}

0 commit comments

Comments
 (0)