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.
- 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/deployment/viteFrontendDeployment.d.ts +1 -0
- package/lib/cloudfront/deployment/viteFrontendDeployment.js +2 -2
- 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,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
|
+
}
|