Skip to content

Commit c8ed5bb

Browse files
authored
Merge pull request #658 from cornell-dti/matt/add-cert-to-env
Prepare Carriage for deployment
2 parents fd807ad + 9218843 commit c8ed5bb

6 files changed

Lines changed: 153 additions & 60 deletions

File tree

frontend/public/_redirects

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/* /index.html 200

frontend/src/components/AuthManager/AuthManager.tsx

Lines changed: 44 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,45 @@ const AuthManager = () => {
7575
}
7676
}, []);
7777

78+
// Common logic to complete login from a JWT issued by the backend
79+
const completeLoginFromToken = (serverJWT: string) => {
80+
setCookie('jwt', serverJWT);
81+
const decoded: any = jwtDecode(serverJWT);
82+
setId(decoded.id);
83+
localStorage.setItem('userId', decoded.id);
84+
localStorage.setItem('userType', decoded.userType);
85+
setAuthToken(serverJWT);
86+
87+
// Refresh user data
88+
const refreshFunc = createRefresh(decoded.id, decoded.userType, serverJWT);
89+
refreshFunc();
90+
setRefreshUser(() => refreshFunc);
91+
setSignedIn(true);
92+
93+
// Navigate to appropriate dashboard based on userType
94+
if (decoded.userType === 'Admin') {
95+
navigate('/admin/home', { replace: true });
96+
} else if (decoded.userType === 'Driver') {
97+
navigate('/driver/rides', { replace: true });
98+
} else if (decoded.userType === 'Rider') {
99+
navigate('/rider/schedule', { replace: true });
100+
} else {
101+
// Invalid userType - this should never happen if backend is working correctly
102+
setSsoError('Invalid user type received. Please contact support.');
103+
logout();
104+
}
105+
};
106+
78107
// SSO Callback handler - fetches profile and JWT after successful SSO login
79-
const handleSSOCallback = async () => {
108+
// This is now primarily a fallback for environments where server-side
109+
// sessions are same-site (e.g., local development). In production, we
110+
// prefer the stateless JWT passed via the URL query parameter.
111+
const handleSSOCallback = async (event?: React.FormEvent<HTMLFormElement>) => {
80112
try {
81113
const response = await fetch(
82114
`${process.env.REACT_APP_SERVER_URL}/api/sso/profile`,
83115
{
84-
credentials: 'include', // CRITICAL: Sends session cookie
116+
credentials: 'include', // Send session cookie
85117
}
86118
);
87119

@@ -93,40 +125,8 @@ const AuthManager = () => {
93125
const { user: ssoUser, token: serverJWT } = data;
94126

95127
if (serverJWT && ssoUser) {
96-
// Store JWT in encrypted cookie (matching Google OAuth pattern)
97-
setCookie('jwt', serverJWT);
98-
99-
// Decode JWT to get user info
100-
const decoded: any = jwtDecode(serverJWT);
101-
102-
// Set auth state
103-
setId(decoded.id);
104-
localStorage.setItem('userId', decoded.id);
105-
localStorage.setItem('userType', decoded.userType);
106-
setAuthToken(serverJWT);
107-
108-
// Refresh user data
109-
const refreshFunc = createRefresh(
110-
decoded.id,
111-
decoded.userType,
112-
serverJWT
113-
);
114-
refreshFunc();
115-
setRefreshUser(() => refreshFunc);
116-
setSignedIn(true);
117-
118-
// Navigate to appropriate dashboard based on userType
119-
if (decoded.userType === 'Admin') {
120-
navigate('/admin/home', { replace: true });
121-
} else if (decoded.userType === 'Driver') {
122-
navigate('/driver/rides', { replace: true });
123-
} else if (decoded.userType === 'Rider') {
124-
navigate('/rider/schedule', { replace: true });
125-
} else {
126-
// Invalid userType - this should never happen if backend is working correctly
127-
setSsoError('Invalid user type received. Please contact support.');
128-
logout();
129-
}
128+
// Reuse common JWT login logic
129+
completeLoginFromToken(serverJWT);
130130
} else {
131131
setSsoError('Failed to complete SSO login. Please try again.');
132132
logout();
@@ -142,6 +142,7 @@ const AuthManager = () => {
142142
useEffect(() => {
143143
const authParam = searchParams.get('auth');
144144
const errorParam = searchParams.get('error');
145+
const tokenParam = searchParams.get('token');
145146

146147
if (errorParam) {
147148
// Handle user_not_found specially - fetch unregistered user info
@@ -185,8 +186,12 @@ const AuthManager = () => {
185186
}
186187

187188
if (authParam === 'sso_success') {
188-
// Fetch profile and JWT token from backend
189-
handleSSOCallback();
189+
if (tokenParam) {
190+
completeLoginFromToken(tokenParam);
191+
} else {
192+
// Fallback to session-based profile fetch (useful for local dev)
193+
handleSSOCallback();
194+
}
190195
}
191196
// eslint-disable-next-line react-hooks/exhaustive-deps
192197
}, [searchParams]);
@@ -248,6 +253,7 @@ const AuthManager = () => {
248253
deleteCookie('jwt');
249254
setAuthToken('');
250255
setSignedIn(false);
256+
setRefreshUser(() => () => {});
251257
window.location.href = `${process.env.REACT_APP_SERVER_URL}/api/sso/logout`;
252258
}
253259

server/src/auth/passport-sso.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,18 @@ interface SamlProfile extends Profile {
1010
email?: string;
1111
}
1212

13-
// Load IdP certificate
13+
// Load IdP certificate, necessary to authenticate with Cornell's SSO/SAML system. Get from Cornell IT or ask a previous developer.
1414
const idpCertPath =
1515
process.env.SAML_IDP_CERT_PATH || './config/cornell-idp-test.crt';
16-
const idpCert = fs.readFileSync(path.resolve(idpCertPath), 'utf-8');
16+
let idpCert = '';
17+
try {
18+
idpCert = fs.readFileSync(path.resolve(idpCertPath), 'utf-8');
19+
} catch (error) {
20+
if (!process.env.SAML_IDP_CERT) {
21+
throw new Error('SAML_IDP_CERT is not available');
22+
}
23+
idpCert = process.env.SAML_IDP_CERT;
24+
}
1725

1826
// Configure SAML strategy
1927
const samlStrategy = new SamlStrategy(

server/src/auth/session.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const sessionMiddleware = session({
2525
cookie: {
2626
httpOnly: true,
2727
secure: process.env.NODE_ENV === 'production', // HTTPS only in production
28-
sameSite: 'lax',
28+
sameSite: process.env.NODE_ENV === 'production' ? 'none' : 'lax',
2929
maxAge: sessionTTL,
3030
},
3131
});

server/src/router/common.ts

Lines changed: 85 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,44 @@ import { Rider } from '../models/rider';
66
import { Location } from '../models/location';
77
import { Driver } from '../models/driver';
88

9+
const MAX_BATCH_GET_ITEMS = 100;
10+
11+
// Internal helper: perform Dynamoose batchGet in chunks of MAX_BATCH_GET_ITEMS
12+
async function batchGetAllItems<T extends Item>(
13+
model: ModelType<T>,
14+
keys: ObjectType[]
15+
): Promise<T[]> {
16+
if (!keys.length) return [];
17+
18+
const chunks: ObjectType[][] = [];
19+
for (let i = 0; i < keys.length; i += MAX_BATCH_GET_ITEMS) {
20+
chunks.push(keys.slice(i, i + MAX_BATCH_GET_ITEMS));
21+
}
22+
23+
const chunkResults = await Promise.all(
24+
chunks.map(
25+
(chunk) =>
26+
new Promise<T[]>((resolve, reject) => {
27+
model.batchGet(chunk, (err: any, data: any) => {
28+
if (err) {
29+
reject(err);
30+
} else if (!data) {
31+
resolve([]);
32+
} else if (Array.isArray(data)) {
33+
resolve(data as T[]);
34+
} else if (typeof (data as any)[Symbol.iterator] === 'function') {
35+
resolve(Array.from(data as Iterable<T>));
36+
} else {
37+
resolve([]);
38+
}
39+
});
40+
})
41+
)
42+
);
43+
44+
return chunkResults.flat();
45+
}
46+
947
// Helper: batch fetch locations, riders, drivers and return maps keyed by id
1048
async function buildEntityMapsFromSets(
1149
locationIds: Set<string>,
@@ -14,13 +52,22 @@ async function buildEntityMapsFromSets(
1452
) {
1553
const [locationsArr, ridersArr, driversArr] = await Promise.all([
1654
locationIds.size
17-
? Location.batchGet(Array.from(locationIds).map((id) => ({ id })))
55+
? batchGetAllItems(
56+
Location,
57+
Array.from(locationIds).map((id) => ({ id }))
58+
)
1859
: Promise.resolve([]),
1960
riderIds.size
20-
? Rider.batchGet(Array.from(riderIds).map((id) => ({ id })))
61+
? batchGetAllItems(
62+
Rider,
63+
Array.from(riderIds).map((id) => ({ id }))
64+
)
2165
: Promise.resolve([]),
2266
driverIds.size
23-
? Driver.batchGet(Array.from(driverIds).map((id) => ({ id })))
67+
? batchGetAllItems(
68+
Driver,
69+
Array.from(driverIds).map((id) => ({ id }))
70+
)
2471
: Promise.resolve([]),
2572
]);
2673

@@ -208,19 +255,43 @@ export function batchGet(
208255
) {
209256
if (!keys.length) {
210257
res.send({ data: [] });
211-
} else {
212-
model.batchGet(keys, async (err, data) => {
213-
if (err) {
214-
res.status(err.statusCode || 500).send({ err: err.message });
215-
} else if (!data) {
216-
res.status(400).send({ err: `items not found in ${table}` });
217-
} else if (callback) {
218-
callback((await data.populate()).toJSON());
258+
return;
259+
}
260+
261+
(async () => {
262+
try {
263+
// Fetch all items in chunks to respect DynamoDB's 100-item batchGet limit
264+
const items = await batchGetAllItems(model, keys);
265+
266+
// Populate each item individually (since we no longer rely on a single DocumentArray)
267+
const populatedJson: any[] = [];
268+
for (const item of items as any[]) {
269+
if (item && typeof item.populate === 'function') {
270+
const populated = await item.populate();
271+
populatedJson.push(
272+
typeof populated.toJSON === 'function'
273+
? populated.toJSON()
274+
: populated
275+
);
276+
} else if (item && typeof item.toJSON === 'function') {
277+
populatedJson.push(item.toJSON());
278+
} else {
279+
populatedJson.push(item);
280+
}
281+
}
282+
283+
if (callback) {
284+
callback(populatedJson);
219285
} else {
220-
res.status(200).send({ data: (await data.populate()).toJSON() });
286+
res.status(200).send({ data: populatedJson });
221287
}
222-
});
223-
}
288+
} catch (err: any) {
289+
console.error('Error in batchGet:', err);
290+
const status = err?.statusCode || 500;
291+
const message = err?.message || `Error fetching items from ${table}`;
292+
res.status(status).send({ err: message });
293+
}
294+
})();
224295
}
225296

226297
export function getAll(

server/src/router/sso.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ router.post(
109109
return res.redirect(`${frontendUrl}/?error=user_not_found`);
110110
}
111111

112-
const { userType } = result as any;
112+
const { user, userType } = result as any;
113113

114114
// Store user in session
115115
req.session.user = {
@@ -119,12 +119,19 @@ router.post(
119119
lastName: samlUser.lastName,
120120
};
121121
req.session.authMethod = 'sso';
122-
// CRITICAL: Store the validated userType in session for /profile endpoint
122+
// Store the validated userType in session for /profile endpoint
123123
req.session.userType = userType;
124-
console.log('[SSO Callback] Stored userType in session:', userType);
125124

126-
// Redirect to frontend with success flag
127-
res.redirect(`${redirectUri}?auth=sso_success`);
125+
const token = sign({ id: user.id, userType }, JWT_SECRET, {
126+
expiresIn: '7d',
127+
});
128+
129+
const separator = redirectUri.includes('?') ? '&' : '?';
130+
const redirectWithToken = `${redirectUri}${separator}auth=sso_success&token=${encodeURIComponent(
131+
token
132+
)}`;
133+
134+
res.redirect(redirectWithToken);
128135
} catch (err) {
129136
console.error('SSO callback error:', err);
130137
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';

0 commit comments

Comments
 (0)