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,215 @@
1
+ import json
2
+ import urllib.parse
3
+ import urllib.request
4
+ from jwt import PyJWK
5
+ import jwt
6
+ import boto3
7
+ import logging
8
+ import hmac
9
+ import hashlib
10
+ import base64
11
+ import time
12
+ import uuid
13
+ from datetime import datetime
14
+ from config_generated import get_config
15
+
16
+ logger = logging.getLogger()
17
+ logger.setLevel(logging.INFO)
18
+
19
+ dynamodb = None
20
+ CONFIG = None
21
+ HMAC_SECRET = None
22
+
23
+
24
+ def get_config_cached():
25
+ global CONFIG, dynamodb
26
+ if CONFIG is None:
27
+ CONFIG = get_config()
28
+ dynamodb_region = CONFIG.get('dynamodb_region', 'us-east-1')
29
+ dynamodb = boto3.client('dynamodb', region_name=dynamodb_region)
30
+ return CONFIG
31
+
32
+
33
+ def get_hmac_secret():
34
+ global HMAC_SECRET
35
+ if HMAC_SECRET is not None:
36
+ return HMAC_SECRET
37
+ config = get_config_cached()
38
+ HMAC_SECRET = config.get('hmac_key')
39
+ if not HMAC_SECRET:
40
+ raise ValueError('hmac_key not found in config')
41
+ return HMAC_SECRET
42
+
43
+
44
+ def exchange_code_for_token(code, cognito_domain, client_id, redirect_uri, code_verifier):
45
+ token_url = f'https://{cognito_domain}/oauth2/token'
46
+ data = {
47
+ 'grant_type': 'authorization_code',
48
+ 'client_id': client_id,
49
+ 'code': code,
50
+ 'redirect_uri': redirect_uri,
51
+ 'code_verifier': code_verifier,
52
+ }
53
+ req = urllib.request.Request(
54
+ token_url,
55
+ data=urllib.parse.urlencode(data).encode('utf-8'),
56
+ headers={'Content-Type': 'application/x-www-form-urlencoded'},
57
+ )
58
+ try:
59
+ with urllib.request.urlopen(req) as response:
60
+ return json.loads(response.read().decode('utf-8'))
61
+ except urllib.error.HTTPError as e:
62
+ logger.error(f'Token exchange HTTP error: {e.code} {e.read().decode()}')
63
+ raise
64
+
65
+
66
+ def validate_jwt_token(id_token, client_id, user_pool_id, cognito_region):
67
+ jwks_url = f'https://cognito-idp.{cognito_region}.amazonaws.com/{user_pool_id}/.well-known/jwks.json'
68
+ with urllib.request.urlopen(jwks_url) as response:
69
+ jwks = json.loads(response.read().decode('utf-8'))
70
+
71
+ unverified_header = jwt.get_unverified_header(id_token)
72
+ rsa_key = next((k for k in jwks['keys'] if k['kid'] == unverified_header['kid']), None)
73
+ if not rsa_key:
74
+ raise ValueError('Unable to find matching key')
75
+
76
+ jwk = PyJWK.from_dict(rsa_key)
77
+ issuer = f'https://cognito-idp.{cognito_region}.amazonaws.com/{user_pool_id}'
78
+ return jwt.decode(id_token, jwk.key, algorithms=['RS256'], audience=client_id, issuer=issuer)
79
+
80
+
81
+ def lambda_handler(event, context):
82
+ request = event['Records'][0]['cf']['request']
83
+
84
+ try:
85
+ config = get_config_cached()
86
+ cognito_domain = config['cognito_domain']
87
+ client_id = config['cognito_client_id']
88
+ user_pool_id = config['cognito_user_pool_id']
89
+ cognito_region = config['cognito_region']
90
+ redirect_uri = config['redirect_uri']
91
+ table_name = config.get('dynamodb_table_name')
92
+ auto_revoke_on_reuse = config.get('auto_revoke_on_reuse', 'false').lower() == 'true'
93
+ jwt_claims_whitelist_str = config.get('jwt_claims_whitelist', '')
94
+ except Exception as e:
95
+ logger.error(f'Config load failed: {e}')
96
+ return {'status': '500', 'statusDescription': 'Internal Server Error', 'body': 'Configuration error'}
97
+
98
+ params = urllib.parse.parse_qs(request.get('querystring', ''))
99
+ code = params.get('code', [None])[0]
100
+ state = params.get('state', [None])[0]
101
+
102
+ if not code:
103
+ return {'status': '400', 'statusDescription': 'Bad Request', 'body': 'Missing authorization code'}
104
+
105
+ cookies = {}
106
+ for cookie in (request.get('headers', {}).get('cookie') or [{}])[0].get('value', '').split('; '):
107
+ if '=' in cookie:
108
+ name, value = cookie.split('=', 1)
109
+ cookies[name] = value
110
+
111
+ code_verifier = cookies.get('code_verifier')
112
+ if not code_verifier:
113
+ return {'status': '400', 'statusDescription': 'Bad Request', 'body': 'Authentication failed'}
114
+
115
+ stored_state = cookies.get('oauth_state')
116
+ if not stored_state or stored_state != state:
117
+ return {'status': '400', 'statusDescription': 'Bad Request', 'body': 'Invalid state parameter'}
118
+
119
+ if table_name and state:
120
+ try:
121
+ resp = dynamodb.get_item(
122
+ TableName=table_name,
123
+ Key={'pk': {'S': f'STATE#{state}'}, 'sk': {'S': f'STATE#{state}'}},
124
+ )
125
+ if not resp.get('Item'):
126
+ dynamodb.put_item(TableName=table_name, Item={
127
+ 'pk': {'S': f'STATE#{state}'}, 'sk': {'S': f'STATE#{state}'},
128
+ 'used': {'BOOL': True},
129
+ 'createdAt': {'N': str(int(time.time()))},
130
+ 'expiresAt': {'N': str(int(time.time()) + 600)},
131
+ })
132
+ elif resp['Item'].get('used', {}).get('BOOL') and auto_revoke_on_reuse:
133
+ return {'status': '400', 'statusDescription': 'Bad Request', 'body': 'Invalid or reused state token'}
134
+ except Exception as e:
135
+ logger.error(f'DynamoDB state check error: {e}')
136
+
137
+ redirect_path = '/'
138
+ try:
139
+ padded = state + '=' * (4 - len(state) % 4)
140
+ state_obj = json.loads(base64.urlsafe_b64decode(padded.replace('-', '+').replace('_', '/')))
141
+ redirect_path = state_obj.get('p', '/')
142
+ except Exception:
143
+ pass
144
+
145
+ try:
146
+ token_response = exchange_code_for_token(code, cognito_domain, client_id, redirect_uri, code_verifier)
147
+ id_token = token_response['id_token']
148
+
149
+ payload = validate_jwt_token(id_token, client_id, user_pool_id, cognito_region)
150
+ logger.info(f'JWT validated for: {payload.get("email", payload.get("cognito:username", "unknown"))}')
151
+
152
+ jti = f'sess_{uuid.uuid4().hex}'
153
+ user_id = payload.get('sub') or payload.get('email') or payload.get('cognito:username')
154
+
155
+ if jwt_claims_whitelist_str:
156
+ whitelist = json.loads(jwt_claims_whitelist_str)
157
+ filtered_payload = {k: v for k, v in payload.items() if k in whitelist}
158
+ else:
159
+ filtered_payload = {k: v for k, v in payload.items() if not k.startswith('cognito:')}
160
+
161
+ session_payload = {
162
+ **filtered_payload,
163
+ 'jti': jti,
164
+ 'exp': payload.get('exp', int(time.time()) + 3600),
165
+ 'iat': int(time.time()),
166
+ 'iss': redirect_uri,
167
+ 'idp': payload.get('iss'),
168
+ }
169
+
170
+ if table_name and user_id:
171
+ try:
172
+ dynamodb.put_item(TableName=table_name, Item={
173
+ 'pk': {'S': f'SESSION#{user_id}'}, 'sk': {'S': f'SESSION#{jti}'},
174
+ 'gsi1pk': {'S': f'USER#{user_id}'}, 'gsi1sk': {'S': f'SESSION#{int(time.time())}'},
175
+ 'jti': {'S': jti}, 'userId': {'S': user_id},
176
+ 'email': {'S': payload.get('email', '')},
177
+ 'createdAt': {'N': str(int(time.time()))},
178
+ 'revoked': {'BOOL': False},
179
+ 'expiresAt': {'N': str(int(time.time()) + 3600)},
180
+ })
181
+ except Exception as e:
182
+ logger.error(f'Failed to store session: {e}')
183
+
184
+ header_b64 = base64.urlsafe_b64encode(
185
+ json.dumps({'alg': 'HS256', 'typ': 'JWT'}, separators=(',', ':')).encode()
186
+ ).decode().rstrip('=')
187
+ payload_b64 = base64.urlsafe_b64encode(
188
+ json.dumps(session_payload, separators=(',', ':')).encode()
189
+ ).decode().rstrip('=')
190
+ signing_input = f'{header_b64}.{payload_b64}'
191
+ hmac_secret = get_hmac_secret()
192
+ sig = hmac.new(hmac_secret.encode(), signing_input.encode(), hashlib.sha256).digest()
193
+ cookie_value = f'{signing_input}.{base64.urlsafe_b64encode(sig).decode().rstrip("=")}'
194
+
195
+ exp_ts = payload.get('exp', int(time.time()) + 3600)
196
+ expires_str = datetime.utcfromtimestamp(exp_ts).strftime('%a, %d %b %Y %H:%M:%S GMT')
197
+
198
+ return {
199
+ 'status': '302',
200
+ 'statusDescription': 'Found',
201
+ 'headers': {
202
+ 'location': [{'key': 'Location', 'value': redirect_path}],
203
+ 'set-cookie': [
204
+ {'key': 'Set-Cookie', 'value': f'__Host-auth_session={cookie_value}; HttpOnly; Secure; SameSite=Lax; Path=/; Expires={expires_str}'},
205
+ {'key': 'Set-Cookie', 'value': 'oauth_state=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0'},
206
+ {'key': 'Set-Cookie', 'value': 'code_verifier=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0'},
207
+ ],
208
+ 'cache-control': [{'key': 'Cache-Control', 'value': 'no-store'}],
209
+ },
210
+ }
211
+
212
+ except Exception as e:
213
+ import traceback
214
+ logger.error(f'OAuth callback error: {traceback.format_exc()}')
215
+ return {'status': '500', 'statusDescription': 'Internal Server Error', 'body': 'Authentication failed'}
@@ -0,0 +1,3 @@
1
+ boto3>=1.35.36
2
+ PyJWT==2.8.0
3
+ cryptography==41.0.7
@@ -0,0 +1,6 @@
1
+ # This file will be generated by CDK at synth time with actual configuration values
2
+ AZURE_TENANT_ID = 'TENANT_ID_PLACEHOLDER'
3
+ # amazonq-ignore-next-line
4
+ AZURE_CLIENT_ID = 'CLIENT_ID_PLACEHOLDER'
5
+ REDIRECT_URI = 'REDIRECT_URI_PLACEHOLDER'
6
+ HMAC_SECRET_ARN = 'SECRET_ARN_PLACEHOLDER'
@@ -0,0 +1,12 @@
1
+ # Generated configuration
2
+ import json
3
+ import boto3
4
+ import os
5
+
6
+ # Well-known secret name pattern
7
+ CONFIG_SECRET_NAME = 'cloudfront-auth-config-cdk-api-dev'
8
+
9
+ def get_config():
10
+ client = boto3.client('secretsmanager', region_name='us-east-1')
11
+ response = client.get_secret_value(SecretId=CONFIG_SECRET_NAME)
12
+ return json.loads(response['SecretString'])