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,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,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'])
|