raindancers-cloudfront 0.0.0 → 0.0.2

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 (38) 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/deployment/viteFrontendDeployment.d.ts +1 -0
  19. package/lib/cloudfront/deployment/viteFrontendDeployment.js +2 -2
  20. package/lib/cloudfront/lambda/certificate/index.py +219 -0
  21. package/lib/cloudfront/lambda/cognito-auth/oauth-callback.py +215 -0
  22. package/lib/cloudfront/lambda/cognito-auth/requirements.txt +3 -0
  23. package/lib/cloudfront/lambda/edge-auth/config.py +6 -0
  24. package/lib/cloudfront/lambda/edge-auth/config_generated.py +12 -0
  25. package/lib/cloudfront/lambda/edge-auth/oauth-callback.py +538 -0
  26. package/lib/cloudfront/lambda/edge-auth/requirements.txt +3 -0
  27. package/lib/cloudfront/lambda/hmacSecret/index.py +129 -0
  28. package/lib/cloudfront/lambda/hmacSecret/requirements.txt +1 -0
  29. package/lib/cloudfront/lambda/jwt-decoder/index.py +88 -0
  30. package/lib/cloudfront/lambda/pre-token/index.py +11 -0
  31. package/lib/cloudfront/lambda/rotateSecret/index.py +86 -0
  32. package/lib/cloudfront/lambda/session-revocation/index.py +80 -0
  33. package/lib/cloudfront/lambda/ssm-writer/index.py +44 -0
  34. package/lib/cloudfront/lambda/stream-processor/index.py +53 -0
  35. package/lib/cloudfront/lambda/webacl/index.py +356 -0
  36. package/lib/cloudfront/logging/README.md +144 -0
  37. package/lib/cloudfront/patterns/SPLIT_STACK_USAGE.md +138 -0
  38. package/package.json +1 -1
@@ -0,0 +1,122 @@
1
+ #!/bin/bash
2
+
3
+ echo "START: wire-custom-claims-extension"
4
+ REQUIRED_ROLES="Policy.Read.All Policy.ReadWrite.ApplicationConfiguration CustomAuthenticationExtension.ReadWrite.All EventListener.ReadWrite.All"
5
+ echo "Acquiring token (attempt 1/6)"
6
+ for i in 1 2 3 4 5 6; do
7
+ if [ -n "$TEST_CLIENT_ID" ]; then
8
+ TOKEN=$(curl -s -X POST \
9
+ "https://login.microsoftonline.com/${TEST_TENANT_ID}/oauth2/v2.0/token" \
10
+ -d "grant_type=client_credentials&client_id=${TEST_CLIENT_ID}&client_secret=${TEST_CLIENT_SECRET}&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default" \
11
+ | python3 -c 'import sys,json; print(json.load(sys.stdin)["access_token"])')
12
+ else
13
+ TOKEN=$(curl -s "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://graph.microsoft.com&bypass_cache=true" \
14
+ -H "Metadata: true" \
15
+ | python3 -c 'import sys,json; print(json.load(sys.stdin)["access_token"])')
16
+ fi
17
+ ROLES=$(echo "$TOKEN" | python3 -c 'import sys,base64,json; t=sys.stdin.read().strip(); p=t.split(".")[1]; p+="=="*((4-len(p)%4)%4); print(" ".join(json.loads(base64.b64decode(p)).get("roles",[])))')
18
+ MISSING=""
19
+ for role in $REQUIRED_ROLES; do
20
+ echo "$ROLES" | grep -q "$role" || MISSING="$MISSING $role"
21
+ done
22
+ [ -z "$MISSING" ] && break
23
+ echo "Waiting for role propagation (attempt $i/6), missing:$MISSING"
24
+ sleep 20
25
+ done
26
+ if [ -n "$MISSING" ]; then
27
+ echo "Required roles not propagated after 120s:$MISSING"
28
+ exit 1
29
+ fi
30
+ echo "TOKEN_LEN=${#TOKEN}"
31
+ GRAPH="https://graph.microsoft.com/v1.0"
32
+ TARGET_URL="https://${FUNCTION_HOSTNAME}/api/CustomClaimsProvider"
33
+ RESOURCE_ID="api://${FUNCTION_HOSTNAME}/${FUNCTION_APP_ID}"
34
+
35
+ echo "STEP: PATCH identifierUris"
36
+ curl -s --fail-with-body -X PATCH \
37
+ -H "Authorization: Bearer $TOKEN" \
38
+ -H "Content-Type: application/json" \
39
+ -d "{\"identifierUris\":[\"$RESOURCE_ID\"]}" \
40
+ "$GRAPH/applications(appId='${FUNCTION_APP_ID}')"
41
+
42
+ echo "STEP: PATCH acceptMappedClaims"
43
+ curl -s --fail-with-body -X PATCH \
44
+ -H "Authorization: Bearer $TOKEN" \
45
+ -H "Content-Type: application/json" \
46
+ -d '{"api":{"acceptMappedClaims":true}}' \
47
+ "$GRAPH/applications(appId='${CLF_APP_ID}')"
48
+
49
+ echo "STEP: check/create service principal"
50
+ EXISTING_SP=$(curl -s -H "Authorization: Bearer $TOKEN" \
51
+ "$GRAPH/servicePrincipals?\$filter=appId+eq+'${FUNCTION_APP_ID}'" \
52
+ | python3 -c 'import sys,json; v=json.load(sys.stdin)["value"]; print(v[0]["id"] if v else "")')
53
+ if [ -z "$EXISTING_SP" ]; then
54
+ curl -s --fail-with-body -X POST \
55
+ -H "Authorization: Bearer $TOKEN" \
56
+ -H "Content-Type: application/json" \
57
+ -d "{\"appId\":\"${FUNCTION_APP_ID}\"}" \
58
+ "$GRAPH/servicePrincipals"
59
+ fi
60
+
61
+ echo "STEP: check/create custom auth extension"
62
+ EXISTING_EXT=$(curl -s --fail-with-body -H "Authorization: Bearer $TOKEN" \
63
+ "$GRAPH/identity/customAuthenticationExtensions" \
64
+ | python3 -c 'import sys,json,os; exts=[x["id"] for x in json.load(sys.stdin)["value"] if x["displayName"]==os.environ["EXT_DISPLAY_NAME"]]; print(exts[0] if exts else "")')
65
+ if [ -z "$EXISTING_EXT" ]; then
66
+ EXTENSION_ID=$(curl -s --fail-with-body -X POST \
67
+ -H "Authorization: Bearer $TOKEN" \
68
+ -H "Content-Type: application/json" \
69
+ -d "{\"@odata.type\":\"#microsoft.graph.onTokenIssuanceStartCustomExtension\",\"displayName\":\"${EXT_DISPLAY_NAME}\",\"description\":\"Custom claims provider for AWS session tags\",\"authenticationConfiguration\":{\"@odata.type\":\"#microsoft.graph.azureAdTokenAuthentication\",\"resourceId\":\"$RESOURCE_ID\"},\"endpointConfiguration\":{\"@odata.type\":\"#microsoft.graph.httpRequestEndpoint\",\"targetUrl\":\"$TARGET_URL\"},\"claimsForTokenConfiguration\":[{\"claimIdInApiResponse\":\"Roles\"}]}" \
70
+ "$GRAPH/identity/customAuthenticationExtensions" \
71
+ | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
72
+ else
73
+ EXTENSION_ID=$EXISTING_EXT
74
+ curl -s --fail-with-body -X PATCH \
75
+ -H "Authorization: Bearer $TOKEN" \
76
+ -H "Content-Type: application/json" \
77
+ -d '{"@odata.type":"#microsoft.graph.onTokenIssuanceStartCustomExtension","claimsForTokenConfiguration":[{"claimIdInApiResponse":"Roles"}]}' \
78
+ "$GRAPH/identity/customAuthenticationExtensions/$EXTENSION_ID"
79
+ fi
80
+
81
+ echo "STEP: check/create claims mapping policy"
82
+ POLICY_ID=$(curl -s --fail-with-body -H "Authorization: Bearer $TOKEN" \
83
+ "$GRAPH/policies/claimsMappingPolicies" \
84
+ | python3 -c 'import sys,json,os; policies=[x["id"] for x in json.load(sys.stdin)["value"] if x["displayName"]==os.environ["EXT_DISPLAY_NAME"]+"-claims-policy"]; print(policies[0] if policies else "")')
85
+ if [ -z "$POLICY_ID" ]; then
86
+ POLICY_BODY='{"ClaimsMappingPolicy":{"Version":1,"IncludeBasicClaimSet":"true","ClaimsSchema":[{"Source":"CustomClaimsProvider","ID":"Roles","JwtClaimType":"https://aws.amazon.com/tags/principal_tags/Roles"}]}}'
87
+ POLICY_DEF=$(echo "$POLICY_BODY" | python3 -c "import json,sys; print(json.dumps(json.dumps(json.loads(sys.stdin.read()))))")
88
+ POLICY_ID=$(curl -s --fail-with-body -X POST \
89
+ -H "Authorization: Bearer $TOKEN" \
90
+ -H "Content-Type: application/json" \
91
+ -d "{\"definition\":[$POLICY_DEF],\"displayName\":\"${EXT_DISPLAY_NAME}-claims-policy\",\"isOrganizationDefault\":false}" \
92
+ "$GRAPH/policies/claimsMappingPolicies" \
93
+ | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
94
+ fi
95
+
96
+ echo "STEP: check/assign claims mapping policy"
97
+ CLF_SP_ID=$(curl -s -H "Authorization: Bearer $TOKEN" \
98
+ "$GRAPH/servicePrincipals(appId='${CLF_APP_ID}')" \
99
+ | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
100
+ EXISTING_POLICY_ASSIGN=$(curl -s -H "Authorization: Bearer $TOKEN" \
101
+ "$GRAPH/servicePrincipals/${CLF_SP_ID}/claimsMappingPolicies" \
102
+ | python3 -c 'import sys,json; policies=[x["id"] for x in json.load(sys.stdin)["value"] if x["id"]==sys.argv[1]]; print(policies[0] if policies else "")' "$POLICY_ID")
103
+ if [ -z "$EXISTING_POLICY_ASSIGN" ]; then
104
+ curl -s --fail-with-body -X POST \
105
+ -H "Authorization: Bearer $TOKEN" \
106
+ -H "Content-Type: application/json" \
107
+ -d "{\"@odata.id\":\"https://graph.microsoft.com/v1.0/policies/claimsMappingPolicies/$POLICY_ID\"}" \
108
+ "$GRAPH/servicePrincipals/${CLF_SP_ID}/claimsMappingPolicies/\$ref"
109
+ fi
110
+
111
+ echo "STEP: check/create token issuance listener"
112
+ EXISTING_LISTENER=$(curl -s --fail-with-body -H "Authorization: Bearer $TOKEN" \
113
+ "$GRAPH/identity/authenticationEventListeners" \
114
+ | python3 -c 'import sys,json,os; listeners=[x["id"] for x in json.load(sys.stdin)["value"] if any(a.get("appId")==os.environ["CLF_APP_ID"] for a in x.get("conditions",{}).get("applications",{}).get("includeApplications",[]))]; print(listeners[0] if listeners else "")')
115
+ if [ -z "$EXISTING_LISTENER" ]; then
116
+ curl -s --fail-with-body -X POST \
117
+ -H "Authorization: Bearer $TOKEN" \
118
+ -H "Content-Type: application/json" \
119
+ -d "{\"@odata.type\":\"#microsoft.graph.onTokenIssuanceStartListener\",\"conditions\":{\"applications\":{\"includeApplications\":[{\"appId\":\"$CLF_APP_ID\"}]}},\"handler\":{\"@odata.type\":\"#microsoft.graph.onTokenIssuanceStartCustomExtensionHandler\",\"customExtension\":{\"id\":\"$EXTENSION_ID\"}}}" \
120
+ "$GRAPH/identity/authenticationEventListeners"
121
+ fi
122
+ echo "DONE"
@@ -0,0 +1,118 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ echo "START: wire-custom-claims-extension"
5
+ # Token acquisition with role propagation retry (up to 120s)
6
+ # Required roles that must all be present before proceeding
7
+ REQUIRED_ROLES="Policy.Read.All Policy.ReadWrite.ApplicationConfiguration CustomAuthenticationExtension.ReadWrite.All EventListener.ReadWrite.All Application.ReadWrite.All"
8
+ echo "Acquiring token (attempt 1/6)"
9
+ for i in 1 2 3 4 5 6; do
10
+ if [ -n "$TEST_CLIENT_ID" ]; then
11
+ TOKEN=$(curl -s -X POST \
12
+ "https://login.microsoftonline.com/${TEST_TENANT_ID}/oauth2/v2.0/token" \
13
+ -d "grant_type=client_credentials&client_id=${TEST_CLIENT_ID}&client_secret=${TEST_CLIENT_SECRET}&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default" \
14
+ | python3 -c 'import sys,json; print(json.load(sys.stdin)["access_token"])')
15
+ else
16
+ TOKEN=$(az account get-access-token --resource https://graph.microsoft.com --query accessToken -o tsv)
17
+ fi
18
+ ROLES=$(echo "$TOKEN" | python3 -c 'import sys,base64,json; t=sys.stdin.read().strip(); p=t.split(".")[1]; p+="=="*((4-len(p)%4)%4); print(" ".join(json.loads(base64.b64decode(p)).get("roles",[])))')
19
+ MISSING=""
20
+ for role in $REQUIRED_ROLES; do
21
+ echo "$ROLES" | grep -q "$role" || MISSING="$MISSING $role"
22
+ done
23
+ [ -z "$MISSING" ] && break
24
+ echo "Waiting for role propagation (attempt $i/6), missing:$MISSING"
25
+ sleep 20
26
+ done
27
+ if [ -n "$MISSING" ]; then
28
+ echo "Required roles not propagated after 120s:$MISSING"
29
+ exit 1
30
+ fi
31
+ echo "TOKEN_LEN=${#TOKEN}"
32
+ GRAPH="https://graph.microsoft.com/v1.0"
33
+ TARGET_URL="https://${FUNCTION_HOSTNAME}/api/CustomClaimsProvider"
34
+ RESOURCE_ID="api://${FUNCTION_HOSTNAME}/${FUNCTION_APP_ID}"
35
+
36
+ echo "STEP: PATCH identifierUris"
37
+ curl -s --fail-with-body -X PATCH \
38
+ -H "Authorization: Bearer $TOKEN" \
39
+ -H "Content-Type: application/json" \
40
+ -d "{\"identifierUris\":[\"$RESOURCE_ID\"]}" \
41
+ "$GRAPH/applications(appId='${FUNCTION_APP_ID}')"
42
+
43
+ echo "STEP: PATCH acceptMappedClaims"
44
+ curl -s --fail-with-body -X PATCH \
45
+ -H "Authorization: Bearer $TOKEN" \
46
+ -H "Content-Type: application/json" \
47
+ -d '{"api":{"acceptMappedClaims":true}}' \
48
+ "$GRAPH/applications(appId='${CLF_APP_ID}')"
49
+
50
+ echo "STEP: check/create service principal"
51
+ EXISTING_SP=$(curl -s -H "Authorization: Bearer $TOKEN" \
52
+ "$GRAPH/servicePrincipals?\$filter=appId+eq+'${FUNCTION_APP_ID}'" \
53
+ | python3 -c 'import sys,json; v=json.load(sys.stdin)["value"]; print(v[0]["id"] if v else "")')
54
+ if [ -z "$EXISTING_SP" ]; then
55
+ curl -s --fail-with-body -X POST \
56
+ -H "Authorization: Bearer $TOKEN" \
57
+ -H "Content-Type: application/json" \
58
+ -d "{\"appId\":\"${FUNCTION_APP_ID}\"}" \
59
+ "$GRAPH/servicePrincipals"
60
+ fi
61
+
62
+ echo "STEP: check/create custom auth extension"
63
+ EXISTING_EXT=$(curl -s --fail-with-body -H "Authorization: Bearer $TOKEN" \
64
+ "$GRAPH/identity/customAuthenticationExtensions" \
65
+ | python3 -c 'import sys,json,os; exts=[x["id"] for x in json.load(sys.stdin)["value"] if x["displayName"]==os.environ["EXT_DISPLAY_NAME"]]; print(exts[0] if exts else "")')
66
+ if [ -z "$EXISTING_EXT" ]; then
67
+ EXTENSION_ID=$(curl -s --fail-with-body -X POST \
68
+ -H "Authorization: Bearer $TOKEN" \
69
+ -H "Content-Type: application/json" \
70
+ -d "{\"@odata.type\":\"#microsoft.graph.onTokenIssuanceStartCustomExtension\",\"displayName\":\"${EXT_DISPLAY_NAME}\",\"description\":\"Custom claims provider for AWS session tags\",\"authenticationConfiguration\":{\"@odata.type\":\"#microsoft.graph.azureAdTokenAuthentication\",\"resourceId\":\"$RESOURCE_ID\"},\"endpointConfiguration\":{\"@odata.type\":\"#microsoft.graph.httpRequestEndpoint\",\"targetUrl\":\"$TARGET_URL\"},\"claimsForTokenConfiguration\":[{\"claimIdInApiResponse\":\"Roles\"}]}" \
71
+ "$GRAPH/identity/customAuthenticationExtensions" \
72
+ | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
73
+ else
74
+ EXTENSION_ID=$EXISTING_EXT
75
+ fi
76
+
77
+ echo "STEP: check/create claims mapping policy"
78
+ POLICY_ID=$(curl -s --fail-with-body -H "Authorization: Bearer $TOKEN" \
79
+ "$GRAPH/policies/claimsMappingPolicies" \
80
+ | python3 -c 'import sys,json,os; policies=[x["id"] for x in json.load(sys.stdin)["value"] if x["displayName"]==os.environ["EXT_DISPLAY_NAME"]+"-claims-policy"]; print(policies[0] if policies else "")')
81
+ if [ -z "$POLICY_ID" ]; then
82
+ POLICY_BODY='{"ClaimsMappingPolicy":{"Version":1,"IncludeBasicClaimSet":"true","ClaimsSchema":[{"Source":"CustomClaimsProvider","ID":"Roles","JwtClaimType":"https://aws.amazon.com/tags/principal_tags/Roles"}]}}'
83
+ POLICY_DEF=$(echo "$POLICY_BODY" | python3 -c "import json,sys; print(json.dumps(json.dumps(json.loads(sys.stdin.read()))))")
84
+ POLICY_ID=$(curl -s --fail-with-body -X POST \
85
+ -H "Authorization: Bearer $TOKEN" \
86
+ -H "Content-Type: application/json" \
87
+ -d "{\"definition\":[$POLICY_DEF],\"displayName\":\"${EXT_DISPLAY_NAME}-claims-policy\",\"isOrganizationDefault\":false}" \
88
+ "$GRAPH/policies/claimsMappingPolicies" \
89
+ | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
90
+ fi
91
+
92
+ echo "STEP: check/assign claims mapping policy"
93
+ CLF_SP_ID=$(curl -s -H "Authorization: Bearer $TOKEN" \
94
+ "$GRAPH/servicePrincipals(appId='${CLF_APP_ID}')" \
95
+ | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
96
+ EXISTING_POLICY_ASSIGN=$(curl -s -H "Authorization: Bearer $TOKEN" \
97
+ "$GRAPH/servicePrincipals/${CLF_SP_ID}/claimsMappingPolicies" \
98
+ | python3 -c 'import sys,json; policies=[x["id"] for x in json.load(sys.stdin)["value"] if x["id"]==sys.argv[1]]; print(policies[0] if policies else "")' "$POLICY_ID")
99
+ if [ -z "$EXISTING_POLICY_ASSIGN" ]; then
100
+ curl -s --fail-with-body -X POST \
101
+ -H "Authorization: Bearer $TOKEN" \
102
+ -H "Content-Type: application/json" \
103
+ -d "{\"@odata.id\":\"https://graph.microsoft.com/v1.0/policies/claimsMappingPolicies/$POLICY_ID\"}" \
104
+ "$GRAPH/servicePrincipals/${CLF_SP_ID}/claimsMappingPolicies/\$ref"
105
+ fi
106
+
107
+ echo "STEP: check/create token issuance listener"
108
+ EXISTING_LISTENER=$(curl -s --fail-with-body -H "Authorization: Bearer $TOKEN" \
109
+ "$GRAPH/identity/authenticationEventListeners" \
110
+ | python3 -c 'import sys,json,os; listeners=[x["id"] for x in json.load(sys.stdin)["value"] if any(a.get("appId")==os.environ["CLF_APP_ID"] for a in x.get("conditions",{}).get("applications",{}).get("includeApplications",[]))]; print(listeners[0] if listeners else "")')
111
+ if [ -z "$EXISTING_LISTENER" ]; then
112
+ curl -s --fail-with-body -X POST \
113
+ -H "Authorization: Bearer $TOKEN" \
114
+ -H "Content-Type: application/json" \
115
+ -d "{\"@odata.type\":\"#microsoft.graph.onTokenIssuanceStartListener\",\"conditions\":{\"applications\":{\"includeApplications\":[{\"appId\":\"$CLF_APP_ID\"}]}},\"handler\":{\"@odata.type\":\"#microsoft.graph.onTokenIssuanceStartCustomExtensionHandler\",\"customExtension\":{\"id\":\"$EXTENSION_ID\"}}}" \
116
+ "$GRAPH/identity/authenticationEventListeners"
117
+ fi
118
+ echo "DONE"
@@ -0,0 +1,220 @@
1
+ import cf from 'cloudfront';
2
+ var crypto = require('crypto');
3
+
4
+ const kvsHandle = cf.kvs();
5
+
6
+ const AZURE_TENANT_ID = 'TENANT_ID_PLACEHOLDER';
7
+ const AZURE_CLIENT_ID = 'CLIENT_ID_PLACEHOLDER';
8
+ const REDIRECT_URI = 'REDIRECT_URI_PLACEHOLDER';
9
+
10
+ function base64urlDecode(str) {
11
+ var base64 = str.replace(/-/g, '+').replace(/_/g, '/');
12
+ while (base64.length % 4) {
13
+ base64 += '=';
14
+ }
15
+ return atob(base64);
16
+ }
17
+
18
+ function constantTimeCompare(a, b) {
19
+ if (a.length !== b.length) {
20
+ return false;
21
+ }
22
+ var result = 0;
23
+ for (var i = 0; i < a.length; i++) {
24
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
25
+ }
26
+ return result === 0;
27
+ }
28
+
29
+ async function validateHmacSignature(token) {
30
+ var parts = token.split('.');
31
+ if (parts.length !== 3) {
32
+ return false;
33
+ }
34
+
35
+ var signingInput = parts[0] + '.' + parts[1];
36
+ var providedSignature = parts[2];
37
+
38
+ try {
39
+ // Get current secret
40
+ var secret = await kvsHandle.get('jwt.secret');
41
+ if (!secret) {
42
+ return false;
43
+ }
44
+
45
+ // Try current secret first
46
+ var hmac = crypto.createHmac('sha256', secret);
47
+ hmac.update(signingInput);
48
+ var computedSignature = hmac.digest('base64url');
49
+
50
+ if (constantTimeCompare(computedSignature, providedSignature)) {
51
+ return true; // Valid with current secret
52
+ }
53
+
54
+ // Try old secret (for tokens signed before rotation)
55
+ try {
56
+ var oldSecret = await kvsHandle.get('jwt.secret.old');
57
+ if (oldSecret) {
58
+ var oldHmac = crypto.createHmac('sha256', oldSecret);
59
+ oldHmac.update(signingInput);
60
+ var oldComputedSignature = oldHmac.digest('base64url');
61
+
62
+ if (constantTimeCompare(oldComputedSignature, providedSignature)) {
63
+ console.log('Token validated with old secret (pre-rotation)');
64
+ return true; // Valid with old secret
65
+ }
66
+ }
67
+ } catch (e) {
68
+ // Old secret doesn't exist (first deployment or old secret expired)
69
+ }
70
+
71
+ return false; // Invalid signature with both secrets
72
+ } catch (e) {
73
+ return false;
74
+ }
75
+ }
76
+
77
+ function generateCodeVerifier() {
78
+ var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
79
+ var result = '';
80
+ for (var i = 0; i < 43; i++) {
81
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
82
+ }
83
+ return result;
84
+ }
85
+
86
+ function generateCodeChallenge(verifier) {
87
+ var hash = crypto.createHash('sha256');
88
+ hash.update(verifier);
89
+ return hash.digest('base64url');
90
+ }
91
+
92
+ function generateState(originalPath) {
93
+ var randomPart = Math.random().toString(36).substring(2) + Date.now().toString(36);
94
+ var stateObj = {
95
+ r: randomPart,
96
+ p: originalPath
97
+ };
98
+ return btoa(JSON.stringify(stateObj)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
99
+ }
100
+
101
+ function buildAzureAuthUrl(state, codeChallenge) {
102
+ var params = [
103
+ 'client_id=' + encodeURIComponent(AZURE_CLIENT_ID),
104
+ 'redirect_uri=' + encodeURIComponent(REDIRECT_URI),
105
+ 'response_type=code',
106
+ 'scope=' + encodeURIComponent('openid profile email'),
107
+ 'state=' + encodeURIComponent(state),
108
+ 'code_challenge=' + encodeURIComponent(codeChallenge),
109
+ 'code_challenge_method=S256'
110
+ ];
111
+
112
+ return 'https://login.microsoftonline.com/' + AZURE_TENANT_ID +
113
+ '/oauth2/v2.0/authorize?' + params.join('&');
114
+ }
115
+
116
+ function getOriginalPath(request) {
117
+ var qs = request.querystring;
118
+ if (!qs) {
119
+ return request.uri;
120
+ }
121
+ if (typeof qs === 'object') {
122
+ var params = [];
123
+ for (var key in qs) {
124
+ if (qs.hasOwnProperty(key)) {
125
+ var val = qs[key];
126
+ if (val && val.value !== undefined) {
127
+ params.push(encodeURIComponent(key) + '=' + encodeURIComponent(val.value));
128
+ } else {
129
+ params.push(encodeURIComponent(key) + '=' + encodeURIComponent(val));
130
+ }
131
+ }
132
+ }
133
+ return request.uri + (params.length > 0 ? '?' + params.join('&') : '');
134
+ }
135
+ return request.uri + '?' + qs;
136
+ }
137
+
138
+ function redirectToAuth(originalPath) {
139
+ var state = generateState(originalPath);
140
+ var codeVerifier = generateCodeVerifier();
141
+ var codeChallenge = generateCodeChallenge(codeVerifier);
142
+ return {
143
+ statusCode: 302,
144
+ headers: {
145
+ location: { value: buildAzureAuthUrl(state, codeChallenge) },
146
+ 'cache-control': { value: 'no-store' }
147
+ },
148
+ cookies: {
149
+ oauth_state: {
150
+ value: state,
151
+ attributes: 'HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=600'
152
+ },
153
+ code_verifier: {
154
+ value: codeVerifier,
155
+ attributes: 'HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=600'
156
+ }
157
+ }
158
+ };
159
+ }
160
+
161
+ async function handler(event) {
162
+ var request = event.request;
163
+
164
+ // Skip auth check for OAuth callback path
165
+ if (request.uri === '/oauth2/callback') {
166
+ return request;
167
+ }
168
+
169
+ var cookies = request.cookies;
170
+
171
+ // Check for session cookie
172
+ if (!cookies['__Host-auth_session']) {
173
+ return redirectToAuth(getOriginalPath(request));
174
+ }
175
+
176
+ var token = cookies['__Host-auth_session'].value;
177
+
178
+ if (!token || token.length === 0) {
179
+ return redirectToAuth(getOriginalPath(request));
180
+ }
181
+
182
+ try {
183
+ var originalPath = getOriginalPath(request);
184
+ var parts = token.split('.');
185
+
186
+ if (parts.length !== 3) {
187
+ return redirectToAuth(originalPath);
188
+ }
189
+
190
+ var isValid = await validateHmacSignature(token);
191
+ if (!isValid) {
192
+ return redirectToAuth(originalPath);
193
+ }
194
+
195
+ var payload = JSON.parse(base64urlDecode(parts[1]));
196
+ var now = Math.floor(Date.now() / 1000);
197
+
198
+ if (payload.exp && payload.exp < now) {
199
+ return redirectToAuth(originalPath);
200
+ }
201
+
202
+ // Check if session is revoked (denylist approach)
203
+ var jti = payload.jti;
204
+ if (jti) {
205
+ try {
206
+ var isRevoked = await kvsHandle.get('revoked:' + jti);
207
+ if (isRevoked) {
208
+ console.log('Session revoked: ' + jti);
209
+ return redirectToAuth(originalPath);
210
+ }
211
+ } catch (e) {
212
+ console.log('KVS error checking revocation: ' + e);
213
+ }
214
+ }
215
+
216
+ return request;
217
+ } catch (e) {
218
+ return redirectToAuth(getOriginalPath(request));
219
+ }
220
+ }