Skip to content

Commit d1d8133

Browse files
authored
Auth0 improvements (#83)
* feat: add safeStringify utility for improved JSON serialization and update response handling * fix: improve error handling in auth0CallbackHandler by throwing errors instead of returning responses * fix: update function call in index.js to include request parameter in post-process handling * fix: update auth0 callback handling to improve JWT validation and response processing * fix: update wrangler dependency to version 3.87.0 and remove unused packages from package-lock.json * feat: add generateJsonResponse utility for standardized JSON responses and update response handling in index.js * refactor: enhance generateJsonResponse function for improved input handling and response generation * fix: add error handling for service response and service binding function calls in index.js * fix: improve error handling in service response and binding function calls in index.js * feat: add refresh token functionality and update integration type enum in index.js and auth0.js
1 parent 77f8d4b commit d1d8133

File tree

6 files changed

+245
-40
lines changed

6 files changed

+245
-40
lines changed

package-lock.json

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/common.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
function safeStringify(obj) {
2+
return JSON.stringify(obj, (key, value) => {
3+
// Check if the value is a function, undefined, symbol, or a Promise
4+
if (typeof value === 'function' ||
5+
typeof value === 'undefined' ||
6+
typeof value === 'symbol' ||
7+
(typeof value === 'object' && value !== null && typeof value.then === 'function')) {
8+
return undefined; // Exclude these values
9+
}
10+
return value; // Include all other values
11+
});
12+
}
13+
14+
function generateJsonResponse(input) {
15+
if (input instanceof Response) {
16+
return input;
17+
}
18+
19+
if (input === null) {
20+
return new Response(null, {
21+
headers: {
22+
'Content-Type': 'application/json',
23+
},
24+
status: 200,
25+
});
26+
}
27+
28+
if (typeof input === 'string') {
29+
return new Response(input, {
30+
headers: {
31+
'Content-Type': 'application/json',
32+
},
33+
status: 200,
34+
});
35+
}
36+
37+
if (typeof input === 'object') {
38+
const { statusCode, error, message, ...data } = input;
39+
const responseBody = {
40+
status: error ? 'error' : 'success',
41+
message: message || (error ? 'An error occurred.' : 'Operation completed successfully.'),
42+
data: error ? undefined : data,
43+
error: error || undefined,
44+
};
45+
46+
return new Response(JSON.stringify(responseBody), {
47+
headers: {
48+
'Content-Type': 'application/json',
49+
},
50+
status: statusCode !== undefined ? statusCode : (error ? 500 : 200),
51+
});
52+
}
53+
54+
// Default response for unsupported input types
55+
return new Response(null, {
56+
headers: {
57+
'Content-Type': 'application/json',
58+
},
59+
status: 400,
60+
});
61+
}
62+
63+
export { safeStringify, generateJsonResponse };

src/enums/integration-type.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ export const IntegrationTypeEnum = {
66
AUTH0CALLBACK : 'auth0_callback',
77
AUTH0USERINFO : 'auth0_userinfo',
88
AUTH0CALLBACKREDIRECT : 'auth0_callback_redirect',
9+
AUTH0REFRESH: 'auth0_refresh',
910
}

src/index.js

Lines changed: 132 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1+
import { safeStringify, generateJsonResponse } from "./common";
12
const { jwtAuth } = await import('./auth');
2-
const responses = await import('./responses');
33
const { ValueMapper } = await import('./mapping');
44
const { setCorsHeaders } = await import('./cors');
55
const { PathOperator } = await import('./path-ops');
66
const { AuthError } = await import('./types/error_types');
77
const { setPoweredByHeader } = await import('./powered-by');
88
const { createProxiedRequest } = await import('./requests');
99
const { IntegrationTypeEnum } = await import('./enums/integration-type');
10-
const { auth0CallbackHandler, validateIdToken, getProfile, redirectToLogin } = await import('./integrations/auth0');
1110
const { ServerlessAPIGatewayContext } = await import('./types/serverless_api_gateway_context');
12-
11+
const { auth0CallbackHandler, validateIdToken, getProfile, redirectToLogin, refreshToken } = await import('./integrations/auth0');
1312

1413
export default {
1514
async fetch(request, env, ctx) {
@@ -69,7 +68,7 @@ export default {
6968
return setPoweredByHeader(
7069
setCorsHeaders(
7170
request,
72-
new Response(JSON.stringify({ error: error.message, code: error.code }), {
71+
new Response(safeStringify({ error: error.message, code: error.code }), {
7372
status: error.statusCode,
7473
headers: { 'Content-Type': 'application/json' },
7574
}),
@@ -83,13 +82,13 @@ export default {
8382
}
8483
else if (sagContext.apiConfig.authorizer && matchedPath.config.auth && sagContext.apiConfig.authorizer.type == 'auth0') {
8584
try {
86-
sagContext.jwtPayload = await validateIdToken(request, sagContext.apiConfig.authorizer);
85+
sagContext.jwtPayload = await validateIdToken(request, null, sagContext.apiConfig.authorizer);
8786
} catch (error) {
8887
if (error instanceof AuthError) {
8988
return setPoweredByHeader(
9089
setCorsHeaders(
9190
request,
92-
new Response(JSON.stringify({ error: error.message, code: error.code }), {
91+
new Response(safeStringify({ error: error.message, code: error.code }), {
9392
status: error.statusCode,
9493
headers: { 'Content-Type': 'application/json' },
9594
}),
@@ -128,23 +127,88 @@ export default {
128127
const module = await import(`${service.entrypoint}.js`);
129128
const Service = module.default;
130129
const serviceInstance = new Service();
131-
const response = serviceInstance.fetch(request, env, ctx);
132-
return setPoweredByHeader(setCorsHeaders(request, response, sagContext.apiConfig.cors));
130+
const response = await serviceInstance.fetch(request, env, ctx);
131+
try {
132+
return setPoweredByHeader(setCorsHeaders(request, generateJsonResponse(response), sagContext.apiConfig.cors));
133+
} catch (error) {
134+
return setPoweredByHeader(
135+
setCorsHeaders(
136+
request,
137+
new Response(safeStringify({ error: error.message, code: error.code }), {
138+
status: error.statusCode || 500,
139+
headers: { 'Content-Type': 'application/json' },
140+
}),
141+
sagContext.apiConfig.cors
142+
)
143+
);
144+
}
133145
}
134146
} else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.SERVICE_BINDING) {
135147
const service =
136148
sagContext.apiConfig.serviceBindings &&
137149
sagContext.apiConfig.serviceBindings.find((serviceBinding) => serviceBinding.alias === matchedPath.config.integration.binding);
138150

139151
if (service) {
140-
const response = await env[service.binding][matchedPath.config.integration.function](request, JSON.stringify(env), JSON.stringify(sagContext));
141-
return setPoweredByHeader(setCorsHeaders(request, response, sagContext.apiConfig.cors));
152+
try {
153+
const response = await env[service.binding][matchedPath.config.integration.function](request, safeStringify(env), safeStringify(sagContext));
154+
return setPoweredByHeader(setCorsHeaders(request, generateJsonResponse(response), sagContext.apiConfig.cors));
155+
} catch (error) {
156+
return setPoweredByHeader(
157+
setCorsHeaders(
158+
request,
159+
new Response(safeStringify({ error: error.message, code: error.code }), {
160+
status: error.statusCode || 500,
161+
headers: { 'Content-Type': 'application/json' },
162+
}),
163+
sagContext.apiConfig.cors
164+
)
165+
);
166+
}
142167
}
143168
} else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.AUTH0CALLBACK) {
144-
const urlParams = new URLSearchParams(sagContext.requestUrl.search);
145-
const code = urlParams.get('code');
169+
try {
170+
const urlParams = new URLSearchParams(sagContext.requestUrl.search);
171+
const code = urlParams.get('code');
172+
173+
const jwt = await auth0CallbackHandler(code, sagContext.apiConfig.authorizer);
174+
sagContext.jwtPayload = await validateIdToken(null, jwt.id_token, sagContext.apiConfig.authorizer);
175+
176+
// Post-process logic
177+
if (matchedPath.config.integration.post_process) {
178+
const postProcessConfig = matchedPath.config.integration.post_process;
179+
if (postProcessConfig.type === 'service_binding') {
180+
const postProcessService = sagContext.apiConfig.serviceBindings.find(
181+
(serviceBinding) => serviceBinding.alias === postProcessConfig.binding
182+
);
183+
if (postProcessService) {
184+
await env[postProcessService.binding][postProcessConfig.function](request, safeStringify(env), safeStringify(sagContext));
185+
}
186+
}
187+
}
146188

147-
return auth0CallbackHandler(code, sagContext.apiConfig.authorizer);
189+
return setPoweredByHeader(setCorsHeaders(
190+
request,
191+
new Response(safeStringify(jwt), {
192+
status: 200,
193+
headers: { 'Content-Type': 'application/json' },
194+
}),
195+
sagContext.apiConfig.cors
196+
));
197+
} catch (error) {
198+
console.error('Error processing Auth0 callback', error);
199+
if (error instanceof AuthError) {
200+
return setPoweredByHeader(setCorsHeaders(
201+
request,
202+
new Response(safeStringify({ error: error.message, code: error.code }), {
203+
status: error.statusCode,
204+
headers: { 'Content-Type': 'application/json' },
205+
}),
206+
sagContext.apiConfig.cors
207+
));
208+
} else {
209+
return setPoweredByHeader(setCorsHeaders(request, responses.internalServerErrorResponse(), sagContext.apiConfig.cors));
210+
}
211+
}
148212
} else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.AUTH0USERINFO) {
149213
const urlParams = new URLSearchParams(sagContext.requestUrl.search);
150214
const accessToken = urlParams.get('access_token');
@@ -153,11 +217,13 @@ export default {
153217
} else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.AUTH0CALLBACKREDIRECT) {
154218
const urlParams = new URLSearchParams(sagContext.requestUrl.search);
155219
return redirectToLogin({ state: urlParams.get('state') }, sagContext.apiConfig.authorizer);
220+
} else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.AUTH0REFRESH) {
221+
return this.refreshTokenLogic(request, env, sagContext);
156222
} else {
157223
return setPoweredByHeader(
158224
setCorsHeaders(
159225
request,
160-
new Response(JSON.stringify(matchedPath.config.response), { headers: { 'Content-Type': 'application/json' } }),
226+
new Response(safeStringify(matchedPath.config.response), { headers: { 'Content-Type': 'application/json' } }),
161227
sagContext.apiConfig.cors
162228
),
163229
);
@@ -185,4 +251,56 @@ export default {
185251

186252
return apiConfig;
187253
},
254+
255+
async refreshTokenLogic(request, env, sagContext) {
256+
const urlParams = new URLSearchParams(sagContext.requestUrl.search);
257+
const refreshTokenParam = urlParams.get('refresh_token');
258+
259+
if (!refreshTokenParam) {
260+
return setPoweredByHeader(setCorsHeaders(request,
261+
new Response(
262+
safeStringify({ error: 'Missing refresh token', code: 'missing_refresh_token' }),
263+
{ status: 400, headers: { 'Content-Type': 'application/json' } }
264+
),
265+
sagContext.apiConfig.cors));
266+
}
267+
268+
try {
269+
sagContext.jwtPayload = await validateIdToken(request, null, sagContext.apiConfig.authorizer);
270+
return setPoweredByHeader(setCorsHeaders(request,
271+
new Response(
272+
safeStringify({ message: 'Token is still valid', code: 'token_still_valid' }),
273+
{ status: 200, headers: { 'Content-Type': 'application/json' } }
274+
),
275+
sagContext.apiConfig.cors));
276+
277+
} catch (error) {
278+
if (error instanceof AuthError && error.code === 'ERR_JWT_EXPIRED') {
279+
try {
280+
const newTokens = await refreshToken(refreshTokenParam, sagContext.apiConfig.authorizer);
281+
return setPoweredByHeader(setCorsHeaders(
282+
request,
283+
new Response(safeStringify(newTokens), { status: 200, headers: { 'Content-Type': 'application/json' }, }),
284+
sagContext.apiConfig.cors
285+
));
286+
} catch (refreshError) {
287+
return setPoweredByHeader(setCorsHeaders(
288+
request,
289+
new Response(safeStringify({ error: refreshError.message, code: refreshError.code }), {
290+
status: refreshError.statusCode || 500, headers: { 'Content-Type': 'application/json' },
291+
}),
292+
sagContext.apiConfig.cors
293+
));
294+
}
295+
} else {
296+
return setPoweredByHeader(
297+
setCorsHeaders(
298+
request, new Response(safeStringify({ error: error.message, code: error.code }), {
299+
status: error.statusCode || 500, headers: { 'Content-Type': 'application/json' },
300+
}),
301+
sagContext.apiConfig.cors
302+
));
303+
}
304+
}
305+
}
188306
};

src/integrations/auth0.js

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -25,38 +25,25 @@ async function auth0CallbackHandler(code, authorizer) {
2525

2626
if (!response.ok) {
2727
const errorData = await response.json();
28-
return new Response(JSON.stringify({
29-
error: 'Failed to fetch token',
30-
details: errorData
31-
}), {
32-
status: response.status,
33-
headers: { 'Content-Type': 'application/json' }
34-
});
28+
throw new Error(`Failed to fetch token: ${JSON.stringify(errorData)}`);
3529
}
3630

37-
const data = await response.json();
38-
return new Response(JSON.stringify(data), {
39-
status: 200,
40-
headers: { 'Content-Type': 'application/json' }
41-
});
31+
const jwt = await response.json();
32+
return jwt;
4233
} catch (error) {
43-
return new Response(JSON.stringify({
44-
error: 'Internal Server Error',
45-
message: error.message
46-
}), {
47-
status: 500,
48-
headers: { 'Content-Type': 'application/json' }
49-
});
34+
throw new Error(`Internal Server Error: ${error.message}`);
5035
}
5136
}
5237

53-
async function validateIdToken(request, authorizer) {
38+
async function validateIdToken(request, jwt, authorizer) {
5439
const { domain, jwks, jwks_uri } = authorizer;
55-
const authHeader = request.headers.get('Authorization');
56-
if (!authHeader || !authHeader.startsWith('Bearer ')) {
57-
throw new AuthError('No token provided or token format is invalid.', 'AUTH_ERROR', 401);
40+
if (!jwt) {
41+
const authHeader = request.headers.get('Authorization');
42+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
43+
throw new AuthError('No token provided or token format is invalid.', 'AUTH_ERROR', 401);
44+
}
45+
jwt = authHeader.split(' ')[1];
5846
}
59-
const jwt = authHeader.split(' ')[1];
6047

6148
try {
6249
// Create a JWK Set from the JWKS endpoint or the JWKS data
@@ -153,4 +140,37 @@ async function redirectToLogin(params, authorizer) {
153140
return Response.redirect(loginUrl, 302);
154141
}
155142

156-
export { auth0CallbackHandler, validateIdToken, getProfile, redirectToLogin };
143+
async function refreshToken(refreshToken, authorizer) {
144+
const { domain, client_id, client_secret } = authorizer;
145+
146+
const tokenUrl = `https://${domain}/oauth/token`;
147+
148+
const body = new URLSearchParams({
149+
grant_type: 'refresh_token',
150+
client_id,
151+
client_secret,
152+
refresh_token: refreshToken
153+
});
154+
155+
try {
156+
const response = await fetch(tokenUrl, {
157+
method: 'POST',
158+
headers: {
159+
'Content-Type': 'application/x-www-form-urlencoded'
160+
},
161+
body: body.toString()
162+
});
163+
164+
if (!response.ok) {
165+
const errorData = await response.json();
166+
throw new Error(`Failed to fetch token: ${JSON.stringify(errorData)}`);
167+
}
168+
169+
const jwt = await response.json();
170+
return jwt;
171+
} catch (error) {
172+
throw new Error(`Internal Server Error: ${error.message}`);
173+
}
174+
}
175+
176+
export { auth0CallbackHandler, validateIdToken, getProfile, redirectToLogin, refreshToken };

src/types/error_types.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ class AuthError extends Error {
77
}
88
}
99

10-
export { AuthError }; // Ensure AuthError is exported
10+
export { AuthError };

0 commit comments

Comments
 (0)