-
Notifications
You must be signed in to change notification settings - Fork 9
feat: move Cognito WebUI auth into CFN stack #82
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -360,6 +360,18 @@ Parameters: | |
| Default: 'main' | ||
| Description: "Git branch to clone on the EC2 instance. Use for testing feature branches." | ||
|
|
||
| EnableWebUIAuth: | ||
| Type: String | ||
| Default: 'false' | ||
| AllowedValues: ['true', 'false'] | ||
| Description: "Enable Cognito-based WebUI authentication for KiroCrew dashboard." | ||
|
|
||
| WebUIAdminEmail: | ||
| Type: String | ||
| Default: '' | ||
| Description: "Email for the initial WebUI admin user. Required when EnableWebUIAuth is true." | ||
| AllowedPattern: '^([^@]+@[^@]+\.[^@]+)?$' | ||
|
|
||
| # ============================================================================ | ||
| # RULES | ||
| # ============================================================================ | ||
|
|
@@ -401,6 +413,9 @@ Conditions: | |
| RunSecurityServices: !Not [!Condition IsPersonalAssistant] | ||
| RunBedrockForm: !Equals [!Ref EnableBedrockForm, 'true'] | ||
| IsKiroCrew: !Equals [!Ref PackName, 'kirocrew'] | ||
| EnableWebUI: !And | ||
| - !Condition IsKiroCrew | ||
| - !Equals [!Ref EnableWebUIAuth, 'true'] | ||
| # KiroCrewSubnet2 (in-template second AZ subnet) only needed on new-VPC path; | ||
| # existing-VPC path uses ExistingSubnetId2 passed by the caller. | ||
| KiroCrewNeedsSubnet2: !And | ||
|
|
@@ -1389,6 +1404,205 @@ Resources: | |
| DashboardUrl: !Sub 'https://${KiroCrewDistribution.DomainName}' | ||
| Region: !Ref 'AWS::Region' | ||
|
|
||
| # -------------------------------------------------------------------------- | ||
| # WebUI Authentication (Cognito) — created only for KiroCrew with auth enabled | ||
| # -------------------------------------------------------------------------- | ||
| WebUIUserPool: | ||
| Type: AWS::Cognito::UserPool | ||
| Condition: EnableWebUI | ||
| Properties: | ||
| UserPoolName: !Sub 'lowkey-${PackName}-${EnvironmentName}' | ||
| AdminCreateUserConfig: | ||
| AllowAdminCreateUserOnly: true | ||
| AutoVerifiedAttributes: | ||
| UsernameAttributes: | ||
| Policies: | ||
| PasswordPolicy: | ||
| MinimumLength: 12 | ||
| RequireUppercase: true | ||
| RequireLowercase: true | ||
| RequireNumbers: true | ||
| RequireSymbols: true | ||
| TemporaryPasswordValidityDays: 1 | ||
| Schema: | ||
| - Name: email | ||
| Required: true | ||
| Mutable: true | ||
| UserPoolTags: | ||
| loki:managed: 'true' | ||
| loki:pack: !Ref PackName | ||
| loki:env: !Ref EnvironmentName | ||
|
|
||
| WebUIUserPoolDomain: | ||
| Type: AWS::Cognito::UserPoolDomain | ||
| Condition: EnableWebUI | ||
| Properties: | ||
| UserPoolId: !Ref WebUIUserPool | ||
| Domain: !Sub 'lowkey-${PackName}-${EnvironmentName}' | ||
|
Comment on lines
+1442
to
+1443
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For StackSet deployments, every target account using the same region, pack, and environment requests exactly the same Cognito managed-login prefix, but these prefixes must be unique across accounts within a region. Only the first stack can create the domain and the remaining stacks will roll back; the previous installer explicitly appended a random suffix and retried on collision. Include account/region-derived uniqueness or retain a randomized collision-safe suffix. Useful? React with 👍 / 👎. |
||
|
|
||
| WebUIUserPoolClient: | ||
| Type: AWS::Cognito::UserPoolClient | ||
| Condition: EnableWebUI | ||
| DependsOn: KiroCrewDistribution | ||
| Properties: | ||
| UserPoolId: !Ref WebUIUserPool | ||
| ClientName: !Sub '${PackName}-webui' | ||
| GenerateSecret: false | ||
| ExplicitAuthFlows: | ||
| - ALLOW_USER_SRP_AUTH | ||
| - ALLOW_REFRESH_TOKEN_AUTH | ||
| SupportedIdentityProviders: | ||
| - COGNITO | ||
| AllowedOAuthFlows: | ||
| - code | ||
| AllowedOAuthFlowsUserPoolClient: true | ||
|
Comment on lines
+1458
to
+1460
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When WebUI auth is enabled, this only creates an OAuth client; it never attaches authentication to the request path or passes the pool/client/domain to KiroCrew. The existing Useful? React with 👍 / 👎. |
||
| AllowedOAuthScopes: | ||
| - openid | ||
| CallbackURLs: | ||
| - !Sub 'https://${KiroCrewDistribution.DomainName}/auth/callback' | ||
| - 'http://localhost:5476/auth/callback' | ||
| LogoutURLs: | ||
| - !Sub 'https://${KiroCrewDistribution.DomainName}/' | ||
| - 'http://localhost:5476/' | ||
| PreventUserExistenceErrors: ENABLED | ||
| AccessTokenValidity: 1 | ||
| IdTokenValidity: 1 | ||
| RefreshTokenValidity: 30 | ||
| TokenValidityUnits: | ||
| AccessToken: hours | ||
| IdToken: hours | ||
| RefreshToken: days | ||
|
|
||
| WebUIUserCreationRole: | ||
| Type: AWS::IAM::Role | ||
| Condition: EnableWebUI | ||
| Properties: | ||
| RoleName: !Sub '${EnvironmentName}-webui-user-creation-role' | ||
| AssumeRolePolicyDocument: | ||
| Version: '2012-10-17' | ||
| Statement: | ||
| - Effect: Allow | ||
| Principal: | ||
| Service: lambda.amazonaws.com | ||
| Action: sts:AssumeRole | ||
| ManagedPolicyArns: | ||
| - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole | ||
| Policies: | ||
| - PolicyName: CognitoUserCreation | ||
| PolicyDocument: | ||
| Version: '2012-10-17' | ||
| Statement: | ||
| - Effect: Allow | ||
| Action: | ||
| - cognito-idp:AdminCreateUser | ||
| - cognito-idp:AdminSetUserPassword | ||
| Resource: !GetAtt WebUIUserPool.Arn | ||
|
|
||
| WebUIUserCreationFunction: | ||
| Type: AWS::Lambda::Function | ||
| Condition: EnableWebUI | ||
| Properties: | ||
| FunctionName: !Sub '${EnvironmentName}-webui-user-creation' | ||
| Runtime: python3.12 | ||
| Handler: index.handler | ||
| Timeout: 60 | ||
| Role: !GetAtt WebUIUserCreationRole.Arn | ||
| Code: | ||
| ZipFile: | | ||
| import json, urllib.request, secrets, string, boto3 | ||
|
|
||
| def send_response(event, context, status, reason='', data={}): | ||
| reason_str = (reason or f'See CW: {context.log_stream_name}')[:256] | ||
| phys_id = (context.log_stream_name or 'custom-resource')[-64:] | ||
| safe_data = {k: str(v)[:128] for k, v in (data or {}).items()} | ||
| body = json.dumps({ | ||
| 'Status': status, 'Reason': reason_str, | ||
| 'PhysicalResourceId': phys_id, | ||
| 'StackId': event['StackId'], | ||
| 'RequestId': event['RequestId'], | ||
| 'LogicalResourceId': event['LogicalResourceId'], | ||
| 'Data': safe_data if len(json.dumps(safe_data)) < 1024 else {} | ||
| }).encode() | ||
| req = urllib.request.Request(event['ResponseURL'], data=body, | ||
| headers={'Content-Type': 'application/json', 'Content-Length': len(body)}, | ||
| method='PUT') | ||
| urllib.request.urlopen(req) | ||
|
|
||
| def generate_password(): | ||
| upper = secrets.choice(string.ascii_uppercase) | ||
| lower = secrets.choice(string.ascii_lowercase) | ||
| digit = secrets.choice(string.digits) | ||
| symbol = secrets.choice('!@#$%&*') | ||
| remainder = [secrets.choice(string.ascii_letters + string.digits + '!@#$%&*') for _ in range(12)] | ||
| pwd = [upper, lower, digit, symbol] + remainder | ||
| secrets.SystemRandom().shuffle(pwd) | ||
| return ''.join(pwd) | ||
|
|
||
| def handler(event, context): | ||
| print(f'[INFO] Event: {json.dumps(event)}') | ||
| if event['RequestType'] == 'Delete': | ||
| send_response(event, context, 'SUCCESS', 'Delete is a no-op') | ||
| return | ||
|
Comment on lines
+1546
to
+1548
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an existing stack changes Useful? React with 👍 / 👎. |
||
|
|
||
| props = event.get('ResourceProperties', {}) | ||
| pool_id = props.get('UserPoolId', '') | ||
| email = props.get('AdminEmail', '') | ||
| region = props.get('Region', 'us-east-1') | ||
|
|
||
| if not pool_id or not email: | ||
| send_response(event, context, 'FAILED', 'Missing UserPoolId or AdminEmail') | ||
| return | ||
|
|
||
| cognito = boto3.client('cognito-idp', region_name=region) | ||
| password = generate_password() | ||
|
|
||
| try: | ||
| cognito.admin_create_user( | ||
| UserPoolId=pool_id, | ||
| Username=email, | ||
| UserAttributes=[ | ||
| {'Name': 'email', 'Value': email}, | ||
| {'Name': 'email_verified', 'Value': 'true'} | ||
| ], | ||
| MessageAction='SUPPRESS' | ||
| ) | ||
| except cognito.exceptions.UsernameExistsException: | ||
| send_response(event, context, 'SUCCESS', 'User already exists', | ||
| {'Email': email, 'Password': '(existing user - password unchanged)'}) | ||
| return | ||
| except Exception as e: | ||
| send_response(event, context, 'FAILED', f'User creation failed: {str(e)[:200]}') | ||
| return | ||
|
|
||
| try: | ||
| cognito.admin_set_user_password( | ||
| UserPoolId=pool_id, | ||
| Username=email, | ||
| Password=password, | ||
| Permanent=True | ||
| ) | ||
| except Exception as e: | ||
| send_response(event, context, 'FAILED', f'Password set failed: {str(e)[:200]}') | ||
| return | ||
|
|
||
| send_response(event, context, 'SUCCESS', 'Initial user created', | ||
| {'Email': email, 'Password': password}) | ||
|
|
||
| WebUIUserCreationResource: | ||
| Type: Custom::WebUIUserCreation | ||
| Condition: EnableWebUI | ||
| DependsOn: | ||
| - WebUIUserPool | ||
| - WebUIUserPoolClient | ||
| Properties: | ||
| ServiceToken: !GetAtt WebUIUserCreationFunction.Arn | ||
| UserPoolId: !Ref WebUIUserPool | ||
| AdminEmail: !Ref WebUIAdminEmail | ||
| Region: !Ref 'AWS::Region' | ||
|
|
||
| # SSM Session Manager Preferences (auto-login as ec2-user with welcome) | ||
| # -------------------------------------------------------------------------- | ||
| # -------------------------------------------------------------------------- | ||
|
|
@@ -1582,3 +1796,28 @@ Outputs: | |
| Description: KiroCrew ALB DNS name (do not access directly - use CloudFront) | ||
| Value: !GetAtt KiroCrewALB.DNSName | ||
|
|
||
| WebUICognitoPoolId: | ||
| Condition: EnableWebUI | ||
| Description: Cognito User Pool ID for WebUI auth | ||
| Value: !Ref WebUIUserPool | ||
|
|
||
| WebUICognitoClientId: | ||
| Condition: EnableWebUI | ||
| Description: Cognito App Client ID for WebUI auth | ||
| Value: !Ref WebUIUserPoolClient | ||
|
|
||
| WebUICognitoDomain: | ||
| Condition: EnableWebUI | ||
| Description: Cognito hosted-UI domain | ||
| Value: !Sub '${WebUIUserPoolDomain}.auth.${AWS::Region}.amazoncognito.com' | ||
|
|
||
| WebUIAdminEmailOutput: | ||
| Condition: EnableWebUI | ||
| Description: Email of the initial WebUI admin user | ||
| Value: !Ref WebUIAdminEmail | ||
|
|
||
| WebUIAdminPassword: | ||
| Condition: EnableWebUI | ||
| Description: One-time initial password for the WebUI admin user — save immediately, not retrievable later | ||
| Value: !GetAtt WebUIUserCreationResource.Password | ||
|
Comment on lines
+1819
to
+1822
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
With auth enabled, the Lambda sets this generated password as Useful? React with 👍 / 👎. |
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A direct CloudFormation or console deployment can set
EnableWebUIAuth=truewhile leaving this parameter at its allowed empty default. CloudFormation then creates the preceding resources beforeWebUIUserCreationFunctionrejects the missing email attemplate.yaml:1555-1557, causing a late full-stack rollback. Add a template rule tying a nonempty email to the enabled-auth setting so this invalid combination is rejected before deployment.Useful? React with 👍 / 👎.