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.
- package/lib/bicep/azure-functions/custom-claims-provider/function_app.py +164 -0
- package/lib/bicep/azure-functions/custom-claims-provider/host.json +10 -0
- package/lib/bicep/azure-functions/custom-claims-provider/requirements.txt +8 -0
- package/lib/bicep/deploy/lambda/Dockerfile +10 -0
- package/lib/bicep/deploy/lambda/index.py +341 -0
- package/lib/bicep/patterns/scripts/LOCALDEBUGREADME.md +86 -0
- package/lib/bicep/patterns/scripts/wire-custom-claims-extension.sh +122 -0
- package/lib/bicep/patterns/scripts/wire-custom-claims-extension.sh.bak +118 -0
- package/lib/cloudfront/cloudfront-functions/auth-check.js +220 -0
- package/lib/cloudfront/cloudfront-functions/modules/auth-check.js +263 -0
- package/lib/cloudfront/cloudfront-functions/modules/cognito-auth-check.js +223 -0
- package/lib/cloudfront/cloudfront-functions/modules/shared-utils.js +47 -0
- package/lib/cloudfront/cloudfront-functions/modules/tls-check.js +39 -0
- package/lib/cloudfront/cloudfront-functions/modules/url-rewrite.js +6 -0
- package/lib/cloudfront/cloudfront-functions/role-enforcer.js +93 -0
- package/lib/cloudfront/cloudfront-functions/tls-enforcer.js +55 -0
- package/lib/cloudfront/cloudfront-functions/userinfo-endpoint.js +208 -0
- package/lib/cloudfront/lambda/certificate/index.py +219 -0
- package/lib/cloudfront/lambda/cognito-auth/oauth-callback.py +215 -0
- package/lib/cloudfront/lambda/cognito-auth/requirements.txt +3 -0
- package/lib/cloudfront/lambda/edge-auth/config.py +6 -0
- package/lib/cloudfront/lambda/edge-auth/config_generated.py +12 -0
- package/lib/cloudfront/lambda/edge-auth/oauth-callback.py +538 -0
- package/lib/cloudfront/lambda/edge-auth/requirements.txt +3 -0
- package/lib/cloudfront/lambda/hmacSecret/index.py +129 -0
- package/lib/cloudfront/lambda/hmacSecret/requirements.txt +1 -0
- package/lib/cloudfront/lambda/jwt-decoder/index.py +88 -0
- package/lib/cloudfront/lambda/pre-token/index.py +11 -0
- package/lib/cloudfront/lambda/rotateSecret/index.py +86 -0
- package/lib/cloudfront/lambda/session-revocation/index.py +80 -0
- package/lib/cloudfront/lambda/ssm-writer/index.py +44 -0
- package/lib/cloudfront/lambda/stream-processor/index.py +53 -0
- package/lib/cloudfront/lambda/webacl/index.py +356 -0
- package/lib/cloudfront/logging/README.md +144 -0
- package/lib/cloudfront/patterns/SPLIT_STACK_USAGE.md +138 -0
- 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,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
|
+
}
|