raindancers-cloudfront 0.0.0 → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/lib/bicep/azure-functions/custom-claims-provider/function_app.py +164 -0
  2. package/lib/bicep/azure-functions/custom-claims-provider/host.json +10 -0
  3. package/lib/bicep/azure-functions/custom-claims-provider/requirements.txt +8 -0
  4. package/lib/bicep/deploy/lambda/Dockerfile +10 -0
  5. package/lib/bicep/deploy/lambda/index.py +341 -0
  6. package/lib/bicep/patterns/scripts/LOCALDEBUGREADME.md +86 -0
  7. package/lib/bicep/patterns/scripts/wire-custom-claims-extension.sh +122 -0
  8. package/lib/bicep/patterns/scripts/wire-custom-claims-extension.sh.bak +118 -0
  9. package/lib/cloudfront/cloudfront-functions/auth-check.js +220 -0
  10. package/lib/cloudfront/cloudfront-functions/modules/auth-check.js +263 -0
  11. package/lib/cloudfront/cloudfront-functions/modules/cognito-auth-check.js +223 -0
  12. package/lib/cloudfront/cloudfront-functions/modules/shared-utils.js +47 -0
  13. package/lib/cloudfront/cloudfront-functions/modules/tls-check.js +39 -0
  14. package/lib/cloudfront/cloudfront-functions/modules/url-rewrite.js +6 -0
  15. package/lib/cloudfront/cloudfront-functions/role-enforcer.js +93 -0
  16. package/lib/cloudfront/cloudfront-functions/tls-enforcer.js +55 -0
  17. package/lib/cloudfront/cloudfront-functions/userinfo-endpoint.js +208 -0
  18. package/lib/cloudfront/lambda/certificate/index.py +219 -0
  19. package/lib/cloudfront/lambda/cognito-auth/oauth-callback.py +215 -0
  20. package/lib/cloudfront/lambda/cognito-auth/requirements.txt +3 -0
  21. package/lib/cloudfront/lambda/edge-auth/config.py +6 -0
  22. package/lib/cloudfront/lambda/edge-auth/config_generated.py +12 -0
  23. package/lib/cloudfront/lambda/edge-auth/oauth-callback.py +538 -0
  24. package/lib/cloudfront/lambda/edge-auth/requirements.txt +3 -0
  25. package/lib/cloudfront/lambda/hmacSecret/index.py +129 -0
  26. package/lib/cloudfront/lambda/hmacSecret/requirements.txt +1 -0
  27. package/lib/cloudfront/lambda/jwt-decoder/index.py +88 -0
  28. package/lib/cloudfront/lambda/pre-token/index.py +11 -0
  29. package/lib/cloudfront/lambda/rotateSecret/index.py +86 -0
  30. package/lib/cloudfront/lambda/session-revocation/index.py +80 -0
  31. package/lib/cloudfront/lambda/ssm-writer/index.py +44 -0
  32. package/lib/cloudfront/lambda/stream-processor/index.py +53 -0
  33. package/lib/cloudfront/lambda/webacl/index.py +356 -0
  34. package/lib/cloudfront/logging/README.md +144 -0
  35. package/lib/cloudfront/patterns/SPLIT_STACK_USAGE.md +138 -0
  36. package/package.json +1 -1
@@ -0,0 +1,263 @@
1
+ import cf from 'cloudfront';
2
+ var crypto = require('crypto');
3
+
4
+ const kvsHandle = cf.kvs();
5
+ const AZURE_TENANT_ID = 'TENANT_ID_PLACEHOLDER';
6
+ const AZURE_CLIENT_ID = 'CLIENT_ID_PLACEHOLDER';
7
+ const REDIRECT_URI = 'REDIRECT_URI_PLACEHOLDER';
8
+
9
+ function base64urlDecode(str) {
10
+ var base64 = str.replace(/-/g, '+').replace(/_/g, '/');
11
+ while (base64.length % 4) {
12
+ base64 += '=';
13
+ }
14
+ return atob(base64);
15
+ }
16
+
17
+ function constantTimeCompare(a, b) {
18
+ if (a.length !== b.length) {
19
+ return false;
20
+ }
21
+ var result = 0;
22
+ for (var i = 0; i < a.length; i++) {
23
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
24
+ }
25
+ return result === 0;
26
+ }
27
+
28
+ async function validateHmacSignature(token) {
29
+ var parts = token.split('.');
30
+ if (parts.length !== 3) {
31
+ return false;
32
+ }
33
+
34
+ var signingInput = parts[0] + '.' + parts[1];
35
+ var providedSignature = parts[2];
36
+
37
+ try {
38
+ var secret = await kvsHandle.get('jwt.secret');
39
+ if (!secret) {
40
+ return false;
41
+ }
42
+
43
+ var hmac = crypto.createHmac('sha256', secret);
44
+ hmac.update(signingInput);
45
+ var computedSignature = hmac.digest('base64url');
46
+
47
+ if (constantTimeCompare(computedSignature, providedSignature)) {
48
+ return true;
49
+ }
50
+
51
+ try {
52
+ var oldSecret = await kvsHandle.get('jwt.secret.old');
53
+ if (oldSecret) {
54
+ var oldHmac = crypto.createHmac('sha256', oldSecret);
55
+ oldHmac.update(signingInput);
56
+ var oldComputedSignature = oldHmac.digest('base64url');
57
+
58
+ if (constantTimeCompare(oldComputedSignature, providedSignature)) {
59
+ return true;
60
+ }
61
+ }
62
+ } catch (e) {}
63
+ return false;
64
+ } catch (e) {
65
+ return false;
66
+ }
67
+ }
68
+
69
+ function generateCodeVerifier() {
70
+ var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
71
+ var result = '';
72
+ for (var i = 0; i < 43; i++) {
73
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
74
+ }
75
+ return result;
76
+ }
77
+
78
+ function generateCodeChallenge(verifier) {
79
+ var hash = crypto.createHash('sha256');
80
+ hash.update(verifier);
81
+ return hash.digest('base64url');
82
+ }
83
+
84
+ function generateState(originalPath) {
85
+ var randomPart = Math.random().toString(36).substring(2) + Date.now().toString(36);
86
+ var stateObj = {
87
+ r: randomPart,
88
+ p: originalPath
89
+ };
90
+ return btoa(JSON.stringify(stateObj)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
91
+ }
92
+
93
+ function buildAzureAuthUrl(state, codeChallenge) {
94
+ var params = [
95
+ 'client_id=' + encodeURIComponent(AZURE_CLIENT_ID),
96
+ 'redirect_uri=' + encodeURIComponent(REDIRECT_URI),
97
+ 'response_type=code',
98
+ 'scope=' + encodeURIComponent('openid profile email'),
99
+ 'state=' + encodeURIComponent(state),
100
+ 'code_challenge=' + encodeURIComponent(codeChallenge),
101
+ 'code_challenge_method=S256'
102
+ ];
103
+
104
+ return 'https://login.microsoftonline.com/' + AZURE_TENANT_ID +
105
+ '/oauth2/v2.0/authorize?' + params.join('&');
106
+ }
107
+
108
+ function getOriginalPath(request) {
109
+ var qs = request.querystring;
110
+ if (!qs) {
111
+ return request.uri;
112
+ }
113
+ if (typeof qs === 'object') {
114
+ var params = [];
115
+ for (var key in qs) {
116
+ if (qs.hasOwnProperty(key)) {
117
+ var val = qs[key];
118
+ if (val && val.value !== undefined) {
119
+ params.push(encodeURIComponent(key) + '=' + encodeURIComponent(val.value));
120
+ } else {
121
+ params.push(encodeURIComponent(key) + '=' + encodeURIComponent(val));
122
+ }
123
+ }
124
+ }
125
+ return request.uri + (params.length > 0 ? '?' + params.join('&') : '');
126
+ }
127
+ return request.uri + '?' + qs;
128
+ }
129
+
130
+ function redirectToAuth(originalPath) {
131
+ var state = generateState(originalPath);
132
+ var codeVerifier = generateCodeVerifier();
133
+ var codeChallenge = generateCodeChallenge(codeVerifier);
134
+ return {
135
+ statusCode: 302,
136
+ headers: {
137
+ location: { value: buildAzureAuthUrl(state, codeChallenge) },
138
+ 'cache-control': { value: 'no-store' }
139
+ },
140
+ cookies: {
141
+ oauth_state: {
142
+ value: state,
143
+ attributes: 'HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=600'
144
+ },
145
+ code_verifier: {
146
+ value: codeVerifier,
147
+ attributes: 'HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=600'
148
+ }
149
+ }
150
+ };
151
+ }
152
+
153
+ async function checkAuth(event, decodedPayload, requiredRoles, roleMatchMode) {
154
+ var request = event.request;
155
+ if (request.uri === '/oauth2/callback') {
156
+ return { pass: true, payload: null };
157
+ }
158
+ if (decodedPayload) {
159
+ return { pass: true, payload: decodedPayload };
160
+ }
161
+ var cookies = request.cookies;
162
+ var originalPath = getOriginalPath(request);
163
+ if (!cookies['__Host-auth_session']) {
164
+ return {
165
+ pass: false,
166
+ response: redirectToAuth(originalPath)
167
+ };
168
+ }
169
+ var token = cookies['__Host-auth_session'].value;
170
+ if (!token || token.length === 0) {
171
+ return {
172
+ pass: false,
173
+ response: redirectToAuth(originalPath)
174
+ };
175
+ }
176
+ try {
177
+ var parts = token.split('.');
178
+ if (parts.length !== 3) {
179
+ return {
180
+ pass: false,
181
+ response: redirectToAuth(originalPath)
182
+ };
183
+ }
184
+ var isValid = await validateHmacSignature(token);
185
+ if (!isValid) {
186
+ return {
187
+ pass: false,
188
+ response: redirectToAuth(originalPath)
189
+ };
190
+ }
191
+ var payload = JSON.parse(base64urlDecode(parts[1]));
192
+ var now = Math.floor(Date.now() / 1000);
193
+ if (payload.exp && payload.exp < now) {
194
+ return {
195
+ pass: false,
196
+ response: redirectToAuth(originalPath)
197
+ };
198
+ }
199
+ var jti = payload.jti;
200
+ if (jti) {
201
+ try {
202
+ var isRevoked = await kvsHandle.get('revoked:' + jti);
203
+ if (isRevoked) {
204
+ return {
205
+ pass: false,
206
+ response: redirectToAuth(originalPath)
207
+ };
208
+ }
209
+ } catch (e) {}
210
+ }
211
+
212
+ // Role check (if required roles provided)
213
+ if (requiredRoles && requiredRoles.length > 0) {
214
+ var userRoles = payload.roles || [];
215
+ var hasAccess = false;
216
+
217
+ if (roleMatchMode === 'AND') {
218
+ hasAccess = true;
219
+ for (var i = 0; i < requiredRoles.length; i++) {
220
+ if (userRoles.indexOf(requiredRoles[i]) === -1) {
221
+ hasAccess = false;
222
+ break;
223
+ }
224
+ }
225
+ } else {
226
+ for (var i = 0; i < requiredRoles.length; i++) {
227
+ if (userRoles.indexOf(requiredRoles[i]) !== -1) {
228
+ hasAccess = true;
229
+ break;
230
+ }
231
+ }
232
+ }
233
+
234
+ if (!hasAccess) {
235
+ return {
236
+ pass: false,
237
+ response: {
238
+ statusCode: 403,
239
+ statusDescription: 'Forbidden',
240
+ body: 'Access denied: insufficient roles'
241
+ }
242
+ };
243
+ }
244
+ }
245
+
246
+ return { pass: true, payload: payload };
247
+ } catch (e) {
248
+ return {
249
+ pass: false,
250
+ response: redirectToAuth(originalPath)
251
+ };
252
+ }
253
+ }
254
+
255
+ function injectAzureToken(request, cookies) {
256
+ var azureToken = cookies['__Host-azure_token'];
257
+ if (azureToken && azureToken.value) {
258
+ request.headers['x-azure-token'] = {
259
+ value: azureToken.value
260
+ };
261
+ }
262
+ return request;
263
+ }
@@ -0,0 +1,223 @@
1
+ import cf from 'cloudfront';
2
+ var crypto = require('crypto');
3
+
4
+ const kvsHandle = cf.kvs();
5
+ const COGNITO_DOMAIN = 'COGNITO_DOMAIN_PLACEHOLDER';
6
+ const COGNITO_CLIENT_ID = 'CLIENT_ID_PLACEHOLDER';
7
+ const REDIRECT_URI = 'REDIRECT_URI_PLACEHOLDER';
8
+
9
+ function base64urlDecode(str) {
10
+ var base64 = str.replace(/-/g, '+').replace(/_/g, '/');
11
+ while (base64.length % 4) {
12
+ base64 += '=';
13
+ }
14
+ return atob(base64);
15
+ }
16
+
17
+ function constantTimeCompare(a, b) {
18
+ if (a.length !== b.length) {
19
+ return false;
20
+ }
21
+ var result = 0;
22
+ for (var i = 0; i < a.length; i++) {
23
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
24
+ }
25
+ return result === 0;
26
+ }
27
+
28
+ async function validateHmacSignature(token) {
29
+ var parts = token.split('.');
30
+ if (parts.length !== 3) {
31
+ return false;
32
+ }
33
+
34
+ var signingInput = parts[0] + '.' + parts[1];
35
+ var providedSignature = parts[2];
36
+
37
+ try {
38
+ var secret = await kvsHandle.get('jwt.secret');
39
+ if (!secret) {
40
+ return false;
41
+ }
42
+
43
+ var hmac = crypto.createHmac('sha256', secret);
44
+ hmac.update(signingInput);
45
+ var computedSignature = hmac.digest('base64url');
46
+
47
+ if (constantTimeCompare(computedSignature, providedSignature)) {
48
+ return true;
49
+ }
50
+
51
+ try {
52
+ var oldSecret = await kvsHandle.get('jwt.secret.old');
53
+ if (oldSecret) {
54
+ var oldHmac = crypto.createHmac('sha256', oldSecret);
55
+ oldHmac.update(signingInput);
56
+ var oldComputedSignature = oldHmac.digest('base64url');
57
+ if (constantTimeCompare(oldComputedSignature, providedSignature)) {
58
+ return true;
59
+ }
60
+ }
61
+ } catch (e) {}
62
+ return false;
63
+ } catch (e) {
64
+ return false;
65
+ }
66
+ }
67
+
68
+ function generateCodeVerifier() {
69
+ var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
70
+ var result = '';
71
+ for (var i = 0; i < 43; i++) {
72
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
73
+ }
74
+ return result;
75
+ }
76
+
77
+ function generateCodeChallenge(verifier) {
78
+ var hash = crypto.createHash('sha256');
79
+ hash.update(verifier);
80
+ return hash.digest('base64url');
81
+ }
82
+
83
+ function generateState(originalPath) {
84
+ var randomPart = Math.random().toString(36).substring(2) + Date.now().toString(36);
85
+ var stateObj = { r: randomPart, p: originalPath };
86
+ return btoa(JSON.stringify(stateObj)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
87
+ }
88
+
89
+ function buildCognitoAuthUrl(state, codeChallenge) {
90
+ var params = [
91
+ 'client_id=' + encodeURIComponent(COGNITO_CLIENT_ID),
92
+ 'redirect_uri=' + encodeURIComponent(REDIRECT_URI),
93
+ 'response_type=code',
94
+ 'scope=' + encodeURIComponent('openid profile email'),
95
+ 'state=' + encodeURIComponent(state),
96
+ 'code_challenge=' + encodeURIComponent(codeChallenge),
97
+ 'code_challenge_method=S256'
98
+ ];
99
+ return 'https://' + COGNITO_DOMAIN + '/oauth2/authorize?' + params.join('&');
100
+ }
101
+
102
+ function getOriginalPath(request) {
103
+ var qs = request.querystring;
104
+ if (!qs) {
105
+ return request.uri;
106
+ }
107
+ if (typeof qs === 'object') {
108
+ var params = [];
109
+ for (var key in qs) {
110
+ if (qs.hasOwnProperty(key)) {
111
+ var val = qs[key];
112
+ if (val && val.value !== undefined) {
113
+ params.push(encodeURIComponent(key) + '=' + encodeURIComponent(val.value));
114
+ } else {
115
+ params.push(encodeURIComponent(key) + '=' + encodeURIComponent(val));
116
+ }
117
+ }
118
+ }
119
+ return request.uri + (params.length > 0 ? '?' + params.join('&') : '');
120
+ }
121
+ return request.uri + '?' + qs;
122
+ }
123
+
124
+ function redirectToAuth(originalPath) {
125
+ var state = generateState(originalPath);
126
+ var codeVerifier = generateCodeVerifier();
127
+ var codeChallenge = generateCodeChallenge(codeVerifier);
128
+ return {
129
+ statusCode: 302,
130
+ headers: {
131
+ location: { value: buildCognitoAuthUrl(state, codeChallenge) },
132
+ 'cache-control': { value: 'no-store' }
133
+ },
134
+ cookies: {
135
+ oauth_state: {
136
+ value: state,
137
+ attributes: 'HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=600'
138
+ },
139
+ code_verifier: {
140
+ value: codeVerifier,
141
+ attributes: 'HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=600'
142
+ }
143
+ }
144
+ };
145
+ }
146
+
147
+ async function checkAuth(event, decodedPayload, requiredRoles, roleMatchMode) {
148
+ var request = event.request;
149
+ if (request.uri === '/oauth2/callback') {
150
+ return { pass: true, payload: null };
151
+ }
152
+ if (decodedPayload) {
153
+ return { pass: true, payload: decodedPayload };
154
+ }
155
+ var cookies = request.cookies;
156
+ var originalPath = getOriginalPath(request);
157
+ if (!cookies['__Host-auth_session']) {
158
+ return { pass: false, response: redirectToAuth(originalPath) };
159
+ }
160
+ var token = cookies['__Host-auth_session'].value;
161
+ if (!token || token.length === 0) {
162
+ return { pass: false, response: redirectToAuth(originalPath) };
163
+ }
164
+ try {
165
+ var parts = token.split('.');
166
+ if (parts.length !== 3) {
167
+ return { pass: false, response: redirectToAuth(originalPath) };
168
+ }
169
+ var isValid = await validateHmacSignature(token);
170
+ if (!isValid) {
171
+ return { pass: false, response: redirectToAuth(originalPath) };
172
+ }
173
+ var payload = JSON.parse(base64urlDecode(parts[1]));
174
+ var now = Math.floor(Date.now() / 1000);
175
+ if (payload.exp && payload.exp < now) {
176
+ return { pass: false, response: redirectToAuth(originalPath) };
177
+ }
178
+ var jti = payload.jti;
179
+ if (jti) {
180
+ try {
181
+ var isRevoked = await kvsHandle.get('revoked:' + jti);
182
+ if (isRevoked) {
183
+ return { pass: false, response: redirectToAuth(originalPath) };
184
+ }
185
+ } catch (e) {}
186
+ }
187
+
188
+ if (requiredRoles && requiredRoles.length > 0) {
189
+ var userRoles = payload.roles || [];
190
+ var hasAccess = false;
191
+ if (roleMatchMode === 'AND') {
192
+ hasAccess = true;
193
+ for (var i = 0; i < requiredRoles.length; i++) {
194
+ if (userRoles.indexOf(requiredRoles[i]) === -1) {
195
+ hasAccess = false;
196
+ break;
197
+ }
198
+ }
199
+ } else {
200
+ for (var i = 0; i < requiredRoles.length; i++) {
201
+ if (userRoles.indexOf(requiredRoles[i]) !== -1) {
202
+ hasAccess = true;
203
+ break;
204
+ }
205
+ }
206
+ }
207
+ if (!hasAccess) {
208
+ return {
209
+ pass: false,
210
+ response: {
211
+ statusCode: 403,
212
+ statusDescription: 'Forbidden',
213
+ body: 'Access denied: insufficient roles'
214
+ }
215
+ };
216
+ }
217
+ }
218
+
219
+ return { pass: true, payload: payload };
220
+ } catch (e) {
221
+ return { pass: false, response: redirectToAuth(originalPath) };
222
+ }
223
+ }
@@ -0,0 +1,47 @@
1
+ function decodeJWT(token) {
2
+ try {
3
+ var parts = token.split('.');
4
+ if (parts.length !== 3) {
5
+ return null;
6
+ }
7
+ var payloadBase64 = parts[1];
8
+ while (payloadBase64.length % 4 !== 0) {
9
+ payloadBase64 += '=';
10
+ }
11
+ var payloadJson = atob(payloadBase64);
12
+ return JSON.parse(payloadJson);
13
+ } catch (e) {
14
+ return null;
15
+ }
16
+ }
17
+
18
+ function getJWTFromHeaders(headers) {
19
+ var authHeader = headers['authorization'] ? headers['authorization'].value : '';
20
+ if (!authHeader.startsWith('Bearer ')) {
21
+ return null;
22
+ }
23
+ return authHeader.substring(7);
24
+ }
25
+
26
+ function createRedirect(location, requestId) {
27
+ return {
28
+ statusCode: 302,
29
+ statusDescription: 'Found',
30
+ headers: {
31
+ 'location': { value: location + '?ref=' + requestId },
32
+ 'cache-control': { value: 'no-store' },
33
+ },
34
+ };
35
+ }
36
+
37
+ function createErrorResponse(statusCode, message) {
38
+ return {
39
+ statusCode: statusCode,
40
+ statusDescription: message,
41
+ headers: {
42
+ 'content-type': { value: 'text/html; charset=utf-8' },
43
+ 'cache-control': { value: 'no-store' },
44
+ },
45
+ body: '<h1>' + statusCode + ' ' + message + '</h1>',
46
+ };
47
+ }
@@ -0,0 +1,39 @@
1
+ // tls-check.js - TLS 1.3 enforcement check
2
+ // Returns null if check passes, or redirect response if fails
3
+
4
+ /**
5
+ * Check if request uses TLS 1.3 with approved ciphers
6
+ * @param {object} event - CloudFront event object
7
+ * @returns {object|null} Redirect response or null if check passes
8
+ */
9
+ function checkTLS(event) {
10
+ var headers = event.request.headers;
11
+ var requestId = event.context.requestId;
12
+
13
+ // Get TLS info from CloudFront-Viewer-TLS header
14
+ var tlsInfo = headers['cloudfront-viewer-tls']
15
+ ? headers['cloudfront-viewer-tls'].value
16
+ : '';
17
+
18
+ // Parse TLS version and cipher
19
+ var parts = tlsInfo.split(':');
20
+ var tlsVersion = parts[0] || 'unknown';
21
+ var cipher = parts[1] || 'unknown';
22
+
23
+ // Check if using TLS 1.3
24
+ if (!tlsVersion.startsWith('TLSv1.3')) {
25
+ return createRedirect('/upgrade.html', requestId);
26
+ }
27
+
28
+ // Verify strong cipher (TLS 1.3 GCM only)
29
+ var allowedCiphers = [
30
+ 'TLS_AES_128_GCM_SHA256',
31
+ 'TLS_AES_256_GCM_SHA384'
32
+ ];
33
+
34
+ if (allowedCiphers.indexOf(cipher) === -1) {
35
+ return createRedirect('/upgrade.html', requestId);
36
+ }
37
+
38
+ return null; // Check passed
39
+ }
@@ -0,0 +1,6 @@
1
+ function rewriteToIndex(event) {
2
+ var uri = event.request.uri;
3
+ if (!uri.match(/\.[a-zA-Z0-9]+$/)) {
4
+ event.request.uri = uri.replace(/\/?$/, '/index.html');
5
+ }
6
+ }
@@ -0,0 +1,93 @@
1
+ // role-enforcer.js - CloudFront Function (viewer-request)
2
+ // Enforces role-based access control
3
+ // Requires: JWT in Authorization header (validated by auth-check.js)
4
+ // Requires: x-required-roles header set by construct
5
+
6
+ function handler(event) {
7
+ var request = event.request;
8
+ var headers = request.headers;
9
+
10
+ // Get JWT from Authorization header
11
+ var authHeader = headers['authorization'] ? headers['authorization'].value : '';
12
+ if (!authHeader.startsWith('Bearer ')) {
13
+ return {
14
+ statusCode: 401,
15
+ statusDescription: 'Unauthorized',
16
+ headers: {
17
+ 'content-type': { value: 'text/html; charset=utf-8' },
18
+ 'cache-control': { value: 'no-store' },
19
+ },
20
+ body: '<h1>401 Unauthorized</h1><p>Authentication required.</p>',
21
+ };
22
+ }
23
+
24
+ var token = authHeader.substring(7);
25
+
26
+ // Parse JWT payload (base64 decode middle section)
27
+ var parts = token.split('.');
28
+ if (parts.length !== 3) {
29
+ return {
30
+ statusCode: 401,
31
+ statusDescription: 'Unauthorized',
32
+ headers: {
33
+ 'content-type': { value: 'text/html; charset=utf-8' },
34
+ 'cache-control': { value: 'no-store' },
35
+ },
36
+ body: '<h1>401 Unauthorized</h1><p>Invalid token format.</p>',
37
+ };
38
+ }
39
+
40
+ // Decode payload
41
+ var payload;
42
+ try {
43
+ var payloadBase64 = parts[1];
44
+ // Add padding if needed
45
+ while (payloadBase64.length % 4 !== 0) {
46
+ payloadBase64 += '=';
47
+ }
48
+ var payloadJson = atob(payloadBase64);
49
+ payload = JSON.parse(payloadJson);
50
+ } catch (e) {
51
+ return {
52
+ statusCode: 401,
53
+ statusDescription: 'Unauthorized',
54
+ headers: {
55
+ 'content-type': { value: 'text/html; charset=utf-8' },
56
+ 'cache-control': { value: 'no-store' },
57
+ },
58
+ body: '<h1>401 Unauthorized</h1><p>Invalid token.</p>',
59
+ };
60
+ }
61
+
62
+ // Get required roles from custom header
63
+ var requiredRolesHeader = headers['x-required-roles'] ? headers['x-required-roles'].value : '';
64
+ var requiredRoles = requiredRolesHeader ? requiredRolesHeader.split(',') : [];
65
+
66
+ // Get user roles from JWT
67
+ var userRoles = payload.roles || [];
68
+
69
+ // Check if user has any required role
70
+ var hasRole = false;
71
+ for (var i = 0; i < requiredRoles.length; i++) {
72
+ if (userRoles.indexOf(requiredRoles[i]) !== -1) {
73
+ hasRole = true;
74
+ break;
75
+ }
76
+ }
77
+
78
+ if (!hasRole) {
79
+ // Include CloudFront Request ID for incident tracking
80
+ var requestId = event.context.requestId;
81
+ return {
82
+ statusCode: 302,
83
+ statusDescription: 'Found',
84
+ headers: {
85
+ 'location': { value: '/error.html?ref=' + requestId },
86
+ 'cache-control': { value: 'no-store' },
87
+ },
88
+ };
89
+ }
90
+
91
+ // Allow request to proceed
92
+ return request;
93
+ }