Skip to content

Commit 3bc8edd

Browse files
feat: enforce server-side authentication for all protected features (#835)
* feat: enforce server-side authentication for all protected features Secured job board, resume translator, and LMS with server-side authentication to ensure only Vets Who Code organization members can access protected content in production. Changes: - Converted 8 pages from GetStaticProps to GetServerSideProps with auth checks - Added server-side session validation using getServerSession - Removed client-side auth checks and localStorage dev-session fallbacks - All protected routes now redirect to /login if unauthenticated - Dev login remains functional in development mode only Protected features: - Resume Translator (/resume-translator) - Course Catalog (/courses) - All Course Pages (software-engineering, data-engineering, ai-engineering, web-development, devops) - Lesson Player (/courses/web-development/[moduleId]/[lessonId]) - Job Board (already had protection) Security: - Authentication enforced at server-side before page render - Cannot be bypassed with browser DevTools or localStorage manipulation - GitHub org membership verified during login via GitHub API - Dev tools completely disabled in production (NODE_ENV check) Documentation: - Added SECURITY_VERIFICATION.md with complete security audit - Added Playwright tests for authentication protection - Tests verify redirects and bypass prevention * feat: add server-side authentication to profile page Secured profile page with server-side authentication to ensure only authenticated Vets Who Code organization members can access it. Changes: - Converted from GetStaticProps to GetServerSideProps with auth check - Added server-side session validation using getServerSession - Removed client-side dev-session localStorage fallback logic - Removed all devSession-related code and UI badges - Simplified component to use user prop from server-side auth - Profile now redirects to /login if unauthenticated Security: - Authentication enforced at server-side before page render - Cannot be bypassed with browser DevTools or localStorage manipulation - Consistent with other protected pages (courses, jobs, resume-translator) * fix: resolve Playwright test issues in auth protection suite - Removed localStorage clearing from beforeEach hook to prevent security errors - Updated callback URL assertions to accept both encoded and non-encoded formats - All 17 security tests now passing successfully Test results: - Protected routes properly redirect to login (9 tests) - Dev login blocked in production (2 tests) - Client-side bypass prevention working (2 tests) - Public routes accessible (4 tests) * fix: resolve NextAuth NO_SECRET error in Playwright tests - Load .env.local explicitly in Playwright config - Pass NEXTAUTH_SECRET to webServer environment - Prevents NO_SECRET error when running production build for tests * fix: remove conditional logic from Playwright tests for consistent CI behavior - Playwright webServer always runs in production mode (npm run build && npm run start) - Removed NODE_ENV checks that caused CI failures - Tests now always expect production behavior (redirect to homepage, 403 responses) - All 17 tests passing consistently in both local and CI environments
1 parent 7752e13 commit 3bc8edd

File tree

13 files changed

+687
-841
lines changed

13 files changed

+687
-841
lines changed

SECURITY_VERIFICATION.md

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
# Security Verification Report
2+
3+
## Protected Features - Production Security
4+
5+
This document verifies that all protected features are properly secured in production and cannot be accessed without proper authentication.
6+
7+
---
8+
9+
## ✅ Security Measures in Place
10+
11+
### 1. **Server-Side Authentication (GetServerSideProps)**
12+
13+
All protected pages use `GetServerSideProps` which runs on the server **before** the page is rendered. This means:
14+
- Authentication is checked on the server, not the client
15+
- Users cannot bypass checks by manipulating browser JavaScript
16+
- Unauthenticated users are redirected before any protected content loads
17+
18+
**Protected Pages:**
19+
- `/resume-translator` - Line 140
20+
- `/courses` - Line 262
21+
- `/courses/software-engineering` - Line 302
22+
- `/courses/data-engineering` - Line 302
23+
- `/courses/ai-engineering` - Line 302
24+
- `/courses/web-development` - Line 421
25+
- `/courses/devops` - Line 420
26+
- `/courses/web-development/[moduleId]/[lessonId]` - Line 419
27+
- `/jobs` - Line 347 (already had it)
28+
29+
### 2. **GitHub Organization Membership Verification**
30+
31+
**File:** `src/pages/api/auth/options.ts` (Lines 56-96)
32+
33+
In production, the login flow:
34+
1. User signs in with GitHub OAuth
35+
2. System checks if user is in "Vets-Who-Code" GitHub org via API call
36+
3. HTTP 204 response = member, allowed to proceed
37+
4. Any other response = denied access
38+
39+
**Exception:** Only `jeromehardaway` can login as admin regardless of org membership (Line 52)
40+
41+
**Development:** All GitHub users can login for testing (Line 47)
42+
43+
### 3. **Dev Login Protection**
44+
45+
**File:** `src/pages/dev-login.tsx` (Lines 91-100)
46+
47+
```typescript
48+
if (process.env.NODE_ENV !== "development") {
49+
return {
50+
redirect: {
51+
destination: "/",
52+
permanent: false,
53+
},
54+
};
55+
}
56+
```
57+
58+
- In production, `/dev-login` redirects to homepage
59+
- Dev login button is hidden in production (conditional rendering)
60+
- Dev session API endpoint returns 403 in production
61+
62+
**File:** `src/pages/api/auth/dev-session.ts` (Lines 10-12)
63+
64+
```typescript
65+
if (process.env.NODE_ENV !== 'development') {
66+
return res.status(403).json({ error: 'Not available in production' });
67+
}
68+
```
69+
70+
---
71+
72+
## 🔒 Security Verification Checklist
73+
74+
### Client-Side Bypasses - PREVENTED ✅
75+
76+
-**Cannot** bypass auth by manipulating localStorage
77+
- *Why:* All auth checks happen server-side via `getServerSession()`
78+
- *Old approach removed:* No longer checking `localStorage.getItem("dev-session")`
79+
80+
-**Cannot** bypass auth by manipulating browser DevTools
81+
- *Why:* Server-side rendering checks auth before sending HTML
82+
83+
-**Cannot** access `/dev-login` in production
84+
- *Why:* `GetServerSideProps` redirects to "/" in production
85+
86+
### Server-Side Bypasses - PREVENTED ✅
87+
88+
-**Cannot** access dev-session API in production
89+
- *Why:* Returns 403 if `NODE_ENV !== 'development'`
90+
91+
-**Cannot** forge NextAuth session
92+
- *Why:* Sessions are validated against database and signed with secret
93+
94+
-**Cannot** access protected pages without GitHub org membership
95+
- *Why:* Org membership verified during login via GitHub API
96+
97+
### Environment-Based Security ✅
98+
99+
**Development Mode (`NODE_ENV=development`):**
100+
- ✅ Dev login accessible for testing
101+
- ✅ All GitHub users can login
102+
- ✅ Dev-session API works
103+
- ✅ localStorage dev-session (legacy, not used on protected pages)
104+
105+
**Production Mode (`NODE_ENV=production`):**
106+
- ✅ Dev login redirects to homepage
107+
- ✅ Only Vets-Who-Code org members + jeromehardaway can login
108+
- ✅ Dev-session API returns 403
109+
- ✅ All protected pages require valid NextAuth session
110+
111+
---
112+
113+
## 🧪 How to Verify (Manual Testing)
114+
115+
### Test 1: Dev Login in Production
116+
1. Set `NODE_ENV=production`
117+
2. Build: `npm run build`
118+
3. Start: `npm start`
119+
4. Navigate to `/dev-login`
120+
5. **Expected:** Redirect to homepage
121+
122+
### Test 2: Protected Pages Without Auth
123+
1. Open browser in incognito mode
124+
2. Navigate to `/resume-translator`
125+
3. **Expected:** Redirect to `/login?callbackUrl=/resume-translator`
126+
4. Try `/courses`
127+
5. **Expected:** Redirect to `/login?callbackUrl=/courses`
128+
129+
### Test 3: Dev Session API in Production
130+
1. Set `NODE_ENV=production`
131+
2. Make POST request to `/api/auth/dev-session`
132+
3. **Expected:** `403 Forbidden` with error message
133+
134+
### Test 4: Non-Org Member Login Attempt
135+
1. Set `NODE_ENV=production`
136+
2. Login with GitHub user NOT in Vets-Who-Code org
137+
3. **Expected:** Login denied, redirected back
138+
139+
---
140+
141+
## 🔐 Environment Variables Required
142+
143+
```bash
144+
# Required for production security
145+
GITHUB_ORG=Vets-Who-Code
146+
GITHUB_CLIENT_ID=your-client-id
147+
GITHUB_CLIENT_SECRET=your-client-secret
148+
NODE_ENV=production
149+
NEXTAUTH_SECRET=your-secret-key
150+
NEXTAUTH_URL=https://your-domain.com
151+
```
152+
153+
---
154+
155+
## ✅ Security Validation: PASSED
156+
157+
**Build Status:** ✅ Successful (no TypeScript errors)
158+
159+
**Protected Routes:** 9 routes converted to server-side auth
160+
161+
**Dev Login:** ✅ Protected in production
162+
163+
**Org Membership:** ✅ Enforced via GitHub API
164+
165+
**Last Verified:** November 28, 2025
166+
167+
---
168+
169+
## 📋 Summary
170+
171+
All features are properly secured with:
172+
1. ✅ Server-side authentication checks
173+
2. ✅ GitHub organization membership verification
174+
3. ✅ No client-side bypass vulnerabilities
175+
4. ✅ Dev tools disabled in production
176+
5. ✅ Environment-based access control
177+
178+
**In production, only authenticated members of the Vets-Who-Code GitHub organization can access protected features.**

playwright.config.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { defineConfig, devices } from "@playwright/test";
22
import dotenv from "dotenv";
3+
import path from "path";
34

4-
dotenv.config();
5+
// Load .env.local for local development
6+
dotenv.config({ path: path.resolve(__dirname, ".env.local") });
57
/**
68
* Read environment variables from file.
79
* https://github.com/motdotla/dotenv
@@ -66,5 +68,8 @@ export default defineConfig({
6668
url: "http://localhost:3000",
6769
reuseExistingServer: !process.env.CI,
6870
timeout: 120000, // Adding a longer timeout for build process
71+
env: {
72+
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET || "test-secret-for-playwright",
73+
},
6974
},
7075
});

src/pages/courses/ai-engineering.tsx

Lines changed: 29 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
import React, { useState } from "react";
22
import Link from "next/link";
3-
import { useSession } from "next-auth/react";
43
import Layout01 from "@layout/layout-01";
5-
import type { GetStaticProps, NextPage } from "next";
4+
import type { GetServerSideProps, NextPage } from "next";
5+
import { getServerSession } from "next-auth/next";
6+
import { options } from "@/pages/api/auth/options";
67
import SEO from "@components/seo/page-seo";
78
import Breadcrumb from "@components/breadcrumb";
89

910
type PageProps = {
11+
user: {
12+
id: string;
13+
name: string | null;
14+
email: string;
15+
image: string | null;
16+
};
1017
layout?: {
1118
headerShadow: boolean;
1219
headerFluid: boolean;
@@ -93,67 +100,9 @@ const modules = [
93100
},
94101
];
95102

96-
const AIEngineeringCourse: PageWithLayout = () => {
97-
const { data: session, status } = useSession();
103+
const AIEngineeringCourse: PageWithLayout = ({ user: _user }) => {
98104
const [selectedModule, setSelectedModule] = useState<number | null>(null);
99105

100-
// Check for dev session as fallback
101-
const [devSession, setDevSession] = React.useState<{
102-
user: { id: string; name: string; email: string; image: string };
103-
} | null>(null);
104-
105-
React.useEffect(() => {
106-
if (typeof window !== "undefined") {
107-
const stored = localStorage.getItem("dev-session");
108-
if (stored) {
109-
try {
110-
const user = JSON.parse(stored);
111-
setDevSession({ user });
112-
} catch {
113-
localStorage.removeItem("dev-session");
114-
}
115-
}
116-
}
117-
}, []);
118-
119-
// Use either real session or dev session
120-
const currentSession = session || devSession;
121-
122-
if (status === "loading") {
123-
return (
124-
<div className="tw-container tw-py-16">
125-
<div className="tw-text-center">
126-
<div className="tw-mx-auto tw-h-32 tw-w-32 tw-animate-spin tw-rounded-full tw-border-b-2 tw-border-success" />
127-
<p className="tw-mt-4 tw-text-gray-600">Loading course...</p>
128-
</div>
129-
</div>
130-
);
131-
}
132-
133-
if (!currentSession) {
134-
return (
135-
<>
136-
<SEO title="AI Engineering - Sign In Required" />
137-
<div className="tw-container tw-py-16">
138-
<div className="tw-text-center">
139-
<h1 className="tw-mb-4 tw-text-4xl tw-font-bold tw-text-gray-900">
140-
Authentication Required
141-
</h1>
142-
<p className="tw-mb-8 tw-text-xl tw-text-gray-600">
143-
Please sign in to access the AI Engineering vertical.
144-
</p>
145-
<Link
146-
href="/login"
147-
className="tw-inline-flex tw-items-center tw-rounded-md tw-bg-success tw-px-6 tw-py-3 tw-font-semibold tw-text-white tw-transition-colors hover:tw-bg-success/90"
148-
>
149-
Sign In
150-
</Link>
151-
</div>
152-
</div>
153-
</>
154-
);
155-
}
156-
157106
return (
158107
<>
159108
<SEO title="AI Engineering Vertical" />
@@ -350,9 +299,27 @@ const AIEngineeringCourse: PageWithLayout = () => {
350299

351300
AIEngineeringCourse.Layout = Layout01;
352301

353-
export const getStaticProps: GetStaticProps<PageProps> = () => {
302+
export const getServerSideProps: GetServerSideProps<PageProps> = async (context) => {
303+
// Check authentication
304+
const session = await getServerSession(context.req, context.res, options);
305+
306+
if (!session?.user) {
307+
return {
308+
redirect: {
309+
destination: "/login?callbackUrl=/courses/ai-engineering",
310+
permanent: false,
311+
},
312+
};
313+
}
314+
354315
return {
355316
props: {
317+
user: {
318+
id: session.user.id,
319+
name: session.user.name || null,
320+
email: session.user.email || "",
321+
image: session.user.image || null,
322+
},
356323
layout: {
357324
headerShadow: true,
358325
headerFluid: false,

0 commit comments

Comments
 (0)