IdentityCenterSamlFactory.java
package com.cloudforgeci.api.security;
import com.cloudforgeci.api.core.annotation.BaseFactory;
import com.cloudforge.core.annotation.DeploymentContext;
import com.cloudforge.core.annotation.SystemContext;
import com.cloudforge.core.enums.AuthMode;
import com.cloudforge.core.interfaces.ApplicationSpec;
import com.cloudforge.core.interfaces.OidcIntegration;
import com.cloudforgeci.api.util.CfnStringUtils;
import software.amazon.awscdk.CfnOutput;
import software.amazon.awscdk.Fn;
import software.amazon.awscdk.customresources.AwsCustomResource;
import software.amazon.awscdk.customresources.AwsCustomResourcePolicy;
import software.amazon.awscdk.customresources.AwsSdkCall;
import software.amazon.awscdk.customresources.PhysicalResourceId;
import software.constructs.Construct;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
/**
* IAM Identity Center SAML Factory for automated SAML 2.0 application provisioning.
*
* <p>This factory creates a SAML 2.0 application in AWS IAM Identity Center (formerly AWS SSO)
* and configures it for use with applications like Mattermost that support SAML authentication.</p>
*
* <p><b>Quick Start:</b></p>
* <pre>
* {
* "authMode": "application-oidc",
* "autoProvisionIdentityCenter": true,
* "ssoInstanceArn": "arn:aws:sso:::instance/ssoins-xxxxxxxxxxxx"
* }
* </pre>
*
* <p><b>What Gets Created:</b></p>
* <ul>
* <li>SAML 2.0 application in IAM Identity Center</li>
* <li>Attribute mappings (email, firstName, lastName, groups)</li>
* <li>IdP certificate stored in Secrets Manager</li>
* <li>SAML metadata URL for automatic configuration</li>
* </ul>
*
* <p><b>Prerequisites:</b></p>
* <ul>
* <li>AWS Organizations enabled in the account</li>
* <li>IAM Identity Center enabled and configured</li>
* <li>SSO Instance ARN available (Settings page in Identity Center console)</li>
* </ul>
*
* <p><b>Post-Deployment:</b></p>
* <ol>
* <li>Assign users/groups to the application in IAM Identity Center console</li>
* <li>Users can then sign in using "Sign in with AWS IAM Identity Center"</li>
* </ol>
*
* @see <a href="https://docs.aws.amazon.com/singlesignon/latest/userguide/samlapps.html">IAM Identity Center SAML Apps</a>
*/
public class IdentityCenterSamlFactory extends BaseFactory {
private static final Logger LOG = Logger.getLogger(IdentityCenterSamlFactory.class.getName());
@DeploymentContext("authMode")
private AuthMode authMode;
@DeploymentContext("autoProvisionIdentityCenter")
private Boolean autoProvisionIdentityCenter;
@DeploymentContext("ssoInstanceArn")
private String ssoInstanceArn;
@DeploymentContext("stackName")
private String stackName;
@DeploymentContext("region")
private String region;
@DeploymentContext("fqdn")
private String fqdn;
@DeploymentContext("domain")
private String domain;
@DeploymentContext("subdomain")
private String subdomain;
@DeploymentContext("enableSsl")
private Boolean enableSsl;
// Reuse cognitoInitialAdminEmail for Identity Center - they're mutually exclusive
@DeploymentContext("cognitoInitialAdminEmail")
private String initialAdminEmail;
@SystemContext("applicationSpec")
private ApplicationSpec applicationSpec;
@SystemContext("alb")
private software.amazon.awscdk.services.elasticloadbalancingv2.ApplicationLoadBalancer alb;
@SystemContext("cognitoUserPool")
private software.amazon.awscdk.services.cognito.UserPool cognitoUserPool;
@SystemContext("cognitoUserPoolId")
private String cognitoUserPoolId;
// Guard to prevent multiple invocations of configureCognitoAsExternalIdP
private boolean cognitoConfigured = false;
public IdentityCenterSamlFactory(Construct scope, String id) {
super(scope, id);
}
@Override
public void create() {
// Only provision if authMode is APPLICATION_OIDC (SAML for app-level auth)
if (authMode != AuthMode.APPLICATION_OIDC) {
LOG.info("Application-level OIDC not enabled - skipping Identity Center SAML setup");
return;
}
// Check if auto-provisioning is enabled
if (autoProvisionIdentityCenter == null || !autoProvisionIdentityCenter) {
LOG.info("Identity Center auto-provisioning not enabled - skipping SAML setup");
return;
}
// Validate SSO instance ARN
if (ssoInstanceArn == null || ssoInstanceArn.isEmpty()) {
LOG.severe("ssoInstanceArn is required for Identity Center auto-provisioning");
throw new IllegalArgumentException(
"ssoInstanceArn is required when autoProvisionIdentityCenter = true. " +
"Find it in IAM Identity Center console > Settings > ARN"
);
}
// Check if application supports OIDC/SAML integration
if (applicationSpec == null || !applicationSpec.supportsOidcIntegration()) {
LOG.info("Application does not support OIDC/SAML integration - skipping Identity Center setup");
return;
}
OidcIntegration oidcIntegration = applicationSpec.getOidcIntegration();
if (oidcIntegration == null) {
LOG.info("Application has no OIDC integration configured - skipping Identity Center setup");
return;
}
// Check if application supports Identity Center SAML specifically
if (!oidcIntegration.supportsIdentityCenterSaml()) {
LOG.warning("Application '" + applicationSpec.applicationId() + "' does not support IAM Identity Center SAML");
LOG.warning(" Auth type: " + oidcIntegration.getAuthenticationType());
LOG.warning(" Supports Cognito: " + oidcIntegration.supportsCognito());
LOG.warning(" Supports Identity Center SAML: false");
LOG.warning("Skipping Identity Center SAML setup - use Cognito OIDC instead");
return;
}
LOG.info("Creating IAM Identity Center SAML application for: " + applicationSpec.applicationId());
LOG.info("SSO Instance ARN: " + ssoInstanceArn);
// Create the SAML application
createSamlApplication();
}
/**
* Creates a SAML 2.0 application in IAM Identity Center using Custom Resources.
*
* <p>The SSO Admin API is used to create and configure the application:</p>
* <ul>
* <li>CreateApplication - Creates the SAML app</li>
* <li>PutApplicationAccessScope - Configures access scopes</li>
* <li>PutApplicationAssignmentConfiguration - Configures user assignment</li>
* </ul>
*/
private void createSamlApplication() {
String appName = stackName + "-" + applicationSpec.applicationId();
String siteUrl = constructSiteUrl();
String acsUrl = siteUrl + "/login/sso/saml";
LOG.info("Creating SAML application: " + appName);
LOG.info(" Site URL: " + siteUrl);
LOG.info(" ACS URL: " + acsUrl);
// Extract instance ID from ARN (format: arn:aws:sso:::instance/ssoins-xxxxxxxxxxxx)
String instanceId = ssoInstanceArn.substring(ssoInstanceArn.lastIndexOf("/") + 1);
// Create SAML application using Custom Resource
// Note: CDK doesn't have native SSO Admin constructs, so we use Custom Resources
// Use ApplicationArn from response as PhysicalResourceId so it can be used in onDelete
AwsSdkCall createAppCall = AwsSdkCall.builder()
.service("SSOAdmin")
.action("createApplication")
.parameters(Map.of(
"ApplicationProviderArn", "arn:aws:sso::aws:applicationProvider/custom-saml",
"InstanceArn", ssoInstanceArn,
"Name", appName,
"Description", "SAML application for " + applicationSpec.applicationId() + " created by CloudForge",
"PortalOptions", Map.of(
"SignInOptions", Map.of(
"Origin", "APPLICATION",
"ApplicationUrl", siteUrl
),
"Visibility", "ENABLED"
),
"Status", "ENABLED"
))
// Use ApplicationArn from response as physical ID - this allows onDelete to reference it
.physicalResourceId(PhysicalResourceId.fromResponse("ApplicationArn"))
.region(region)
.build();
// Delete the SAML application on stack deletion
// PhysicalResourceIdReference resolves to the ApplicationArn set above
AwsSdkCall deleteAppCall = AwsSdkCall.builder()
.service("SSOAdmin")
.action("deleteApplication")
.parameters(Map.of(
"ApplicationArn", new software.amazon.awscdk.customresources.PhysicalResourceIdReference()
))
.region(region)
.ignoreErrorCodesMatching("ResourceNotFoundException")
.build();
AwsCustomResource samlApp = AwsCustomResource.Builder.create(this, "SamlApplication")
.onCreate(createAppCall)
.onDelete(deleteAppCall)
.policy(AwsCustomResourcePolicy.fromStatements(List.of(
software.amazon.awscdk.services.iam.PolicyStatement.Builder.create()
.actions(List.of(
"sso:CreateApplication",
"sso:DeleteApplication",
"sso:DescribeApplication",
"sso:PutApplicationGrant",
"sso:PutApplicationAuthenticationMethod",
"sso:PutApplicationAccessScope",
"sso:PutApplicationAssignmentConfiguration",
"sso:GetApplicationGrant",
"sso:ListApplicationGrants"
))
.resources(List.of("*"))
.build()
)))
.build();
// Get the application ARN from the response
String applicationArn = samlApp.getResponseField("ApplicationArn");
// Configure SAML authentication method
configureSamlAuthentication(applicationArn, siteUrl, acsUrl);
// Store IdP metadata URL and certificate in Secrets Manager
storeIdpConfiguration(instanceId, applicationArn);
// Export the SAML configuration to SystemContext
exportSamlConfiguration(siteUrl, acsUrl, instanceId);
// Output useful information
createOutputs(appName, siteUrl, acsUrl, instanceId);
LOG.info("IAM Identity Center SAML application created successfully");
}
/**
* Configure SAML authentication method for the application.
*
* <p>Note: The SSO Admin API for custom SAML applications has limited support
* for programmatic configuration. The CreateApplication call creates the app,
* but detailed SAML settings (ACS URL, attribute mappings) must be configured
* via the IAM Identity Center console.</p>
*
* <p>The application is created with status ENABLED and visible in the portal.
* Post-deployment steps:</p>
* <ol>
* <li>Go to IAM Identity Center > Applications</li>
* <li>Select the created application</li>
* <li>Configure SAML settings (ACS URL, Entity ID, Attributes)</li>
* <li>Assign users/groups</li>
* </ol>
*/
// codeql[java/unused-parameter] -- applicationArn is unused: SAML config here is limited to
// logging manual-configuration values (see this method's own comment on why the grant/auth
// method API calls are skipped); it's part of the SAML integration's still-incomplete state
// (see configureApplicationGrant's TODO below).
private void configureSamlAuthentication(String applicationArn, String siteUrl, String acsUrl) {
// Note: For custom SAML applications, the grant types and authentication methods
// are pre-configured by AWS when using ApplicationProviderArn = "arn:aws:sso::aws:applicationProvider/custom"
//
// The SSO Admin API doesn't support SAML-specific grant types like "saml2-bearer"
// and the PutApplicationAuthenticationMethod only accepts "IAM" which is for
// IAM Identity Center managed applications, not custom SAML apps.
//
// Instead, SAML configuration is done through:
// 1. The portal settings in CreateApplication (SignInOptions, Visibility)
// 2. Manual configuration in the IAM Identity Center console
//
// We skip the grant and auth method calls to avoid API errors.
LOG.info("SAML application created. ACS URL for manual configuration: " + acsUrl);
LOG.info("Entity ID (Audience) for manual configuration: " + siteUrl);
LOG.info("Post-deployment: Configure SAML settings in IAM Identity Center console");
}
/**
* Store IdP certificate and metadata URL in Secrets Manager.
* The application needs these to validate SAML responses.
*
* <p>Uses PutSecretValue which creates the secret if it doesn't exist
* or updates it if it does. This ensures we always have current values.</p>
*/
private void storeIdpConfiguration(String instanceId, String applicationArn) {
String appId = applicationSpec != null ? applicationSpec.applicationId() : "app";
// Construct IdP metadata URL
// Format: https://portal.sso.{region}.amazonaws.com/saml/metadata/{instanceId}
String metadataUrl = "https://portal.sso." + region + ".amazonaws.com/saml/metadata/" + instanceId;
// Construct IdP SSO URL
String ssoUrl = "https://portal.sso." + region + ".amazonaws.com/saml/assertion/" + instanceId;
LOG.info("IdP Metadata URL: " + metadataUrl);
LOG.info("IdP SSO URL: " + ssoUrl);
String secretName = stackName + "/" + appId + "/saml/idp-config";
String secretValue = String.format(
"{\"metadataUrl\":\"%s\",\"ssoUrl\":\"%s\",\"instanceId\":\"%s\",\"region\":\"%s\"}",
metadataUrl, ssoUrl, instanceId, region
);
String description = "IAM Identity Center SAML IdP configuration for " + appId;
// Delete secret on stack deletion - ALWAYS delete, no RETAIN behavior for this config
AwsSdkCall deleteSecretCall = AwsSdkCall.builder()
.service("SecretsManager")
.action("deleteSecret")
.parameters(Map.of(
"SecretId", secretName,
"ForceDeleteWithoutRecovery", true
))
.physicalResourceId(PhysicalResourceId.of("IdentityCenterSamlConfig-" + secretName))
.region(region)
.ignoreErrorCodesMatching("ResourceNotFoundException")
.build();
// Step 1: Create secret (ignore if exists)
AwsSdkCall createSecretCall = AwsSdkCall.builder()
.service("SecretsManager")
.action("createSecret")
.parameters(Map.of(
"Name", secretName,
"Description", description,
"SecretString", secretValue
))
.physicalResourceId(PhysicalResourceId.of("IdentityCenterSamlConfig-" + secretName))
.region(region)
.ignoreErrorCodesMatching("ResourceExistsException")
.build();
// Step 2: Update secret value (always runs to ensure current values)
AwsSdkCall updateSecretCall = AwsSdkCall.builder()
.service("SecretsManager")
.action("putSecretValue")
.parameters(Map.of(
"SecretId", secretName,
"SecretString", secretValue
))
.physicalResourceId(PhysicalResourceId.of("IdentityCenterSamlConfig-" + secretName))
.region(region)
.build();
// Single custom resource that creates, updates, and deletes
// Use scoped ARN pattern for least-privilege (secretName is known at synth time)
// Pattern: arn:aws:secretsmanager:REGION:*:secret:STACKNAME/APP_ID/saml/*
String secretArnPattern = "arn:aws:secretsmanager:" + region + ":*:secret:" + stackName + "/" + appId + "/saml/*";
AwsCustomResource.Builder.create(this, "SamlIdpConfig")
.onCreate(createSecretCall)
.onUpdate(updateSecretCall)
.onDelete(deleteSecretCall)
.policy(AwsCustomResourcePolicy.fromStatements(List.of(
software.amazon.awscdk.services.iam.PolicyStatement.Builder.create()
.actions(List.of(
"secretsmanager:CreateSecret",
"secretsmanager:PutSecretValue",
"secretsmanager:DeleteSecret"
))
.resources(List.of(secretArnPattern))
.build()
)))
.build();
LOG.info("SAML IdP configuration stored in Secrets Manager");
LOG.info("Secret will be deleted on stack deletion (ForceDeleteWithoutRecovery=true)");
// Store the secret name in SystemContext for the application to use
// Note: Just store the secret name - the full ARN isn't known until the secret is created
ctx.samlConfigSecretArn.set(secretName);
// Also store the metadata URL directly for MattermostSamlIntegration
ctx.samlIdpMetadataUrl.set(metadataUrl);
ctx.samlIdpSsoUrl.set(ssoUrl);
}
/**
* Export SAML configuration to SystemContext for application use.
*/
private void exportSamlConfiguration(String siteUrl, String acsUrl, String instanceId) {
// Construct IdP endpoints
String metadataUrl = "https://portal.sso." + region + ".amazonaws.com/saml/metadata/" + instanceId;
String ssoUrl = "https://portal.sso." + region + ".amazonaws.com/saml/assertion/" + instanceId;
// Store in SystemContext for MattermostSamlIntegration to use
ctx.samlSiteUrl.set(siteUrl);
ctx.samlAcsUrl.set(acsUrl);
ctx.samlIdpMetadataUrl.set(metadataUrl);
ctx.samlIdpSsoUrl.set(ssoUrl);
ctx.samlIdpEntityId.set("urn:amazon:webservices");
LOG.info("SAML configuration exported to SystemContext");
}
/**
* Create CloudFormation outputs for easy reference.
*/
private void createOutputs(String appName, String siteUrl, String acsUrl, String instanceId) {
String metadataUrl = "https://portal.sso." + region + ".amazonaws.com/saml/metadata/" + instanceId;
CfnOutput.Builder.create(this, "SamlApplicationName")
.description("IAM Identity Center SAML Application Name")
.value(appName)
.build();
CfnOutput.Builder.create(this, "SamlAcsUrl")
.description("SAML Assertion Consumer Service URL")
.value(acsUrl)
.build();
CfnOutput.Builder.create(this, "SamlEntityId")
.description("SAML Service Provider Entity ID")
.value(siteUrl)
.build();
CfnOutput.Builder.create(this, "SamlIdpMetadataUrl")
.description("IAM Identity Center SAML Metadata URL")
.value(metadataUrl)
.build();
// Direct console link for post-deployment configuration
String consoleUrl = "https://" + region + ".console.aws.amazon.com/singlesignon/home?region=" + region + "#/applications";
CfnOutput.Builder.create(this, "SamlConsoleUrl")
.description("IAM Identity Center Applications Console - configure SAML settings here")
.value(consoleUrl)
.build();
// CRITICAL: AWS SSO Admin API does NOT support SAML attribute mapping programmatically
// Users MUST manually configure these in the IAM Identity Center console
// The "Assign users" popup does NOT configure attributes - must use "Edit attribute mappings"
CfnOutput.Builder.create(this, "SamlPostDeployment")
.description("REQUIRED manual steps - AWS API limitation")
.value("1) Open console URL, 2) Select '" + appName + "', 3) Actions > Edit attribute mappings (NOT Assign users), 4) Add mappings below, 5) Then assign users")
.build();
// Output required attribute mappings for the SAML application
// These MUST be configured in IAM Identity Center for Mattermost to work
// AWS API does not support programmatic SAML attribute mapping - console only
CfnOutput.Builder.create(this, "SamlAttrMappings")
.description("REQUIRED attribute mappings - add ALL of these in Edit attribute mappings")
.value("email=${user:email}, firstName=${user:givenName}, lastName=${user:familyName}, preferred_username=${user:preferredUsername}")
.build();
// Output initial admin email if provided (reused from cognitoInitialAdminEmail)
if (initialAdminEmail != null && !initialAdminEmail.isEmpty()) {
CfnOutput.Builder.create(this, "SamlInitialAdminEmail")
.description("Initial admin email - assign this user in Identity Center")
.value(initialAdminEmail)
.build();
LOG.info("Initial admin email for Identity Center assignment: " + initialAdminEmail);
}
}
/**
* Construct the site URL from domain configuration or ALB DNS name.
*/
private String constructSiteUrl() {
String protocol = (enableSsl != null && enableSsl) ? "https" : "http";
if (fqdn != null && !fqdn.isEmpty()) {
return protocol + "://" + fqdn;
} else if (domain != null && !domain.isEmpty()) {
if (subdomain != null && !subdomain.isEmpty()) {
return protocol + "://" + subdomain + "." + domain;
}
return protocol + "://" + domain;
}
// For OIDC modes without custom domain, use ALB DNS name with Private CA
// IMPORTANT: ALB DNS names contain mixed case but Cognito callback URLs are case-sensitive.
// See: https://github.com/aws/aws-cdk/issues/11171
if (alb != null) {
// Lowercase the ALB DNS name for callback URL compatibility
String lowercaseDns = CfnStringUtils.toLowerCase(alb.getLoadBalancerDnsName());
String albUrl = Fn.join("", java.util.List.of(protocol, "://", lowercaseDns));
LOG.info("No custom domain configured - using ALB DNS name (lowercased): " + albUrl);
return albUrl;
}
// Fallback - should not happen in production
LOG.warning("No domain or ALB configured - using placeholder URL");
return "https://" + applicationSpec.applicationId() + ".example.com";
}
/**
* Configure Cognito User Pool as the external IdP (trusted token issuer) for Identity Center.
*
* <p>This enables the hybrid architecture where:</p>
* <ul>
* <li>Users authenticate with Cognito (user/group management)</li>
* <li>Cognito issues OIDC JWT tokens</li>
* <li>Identity Center trusts Cognito tokens via trusted token issuer</li>
* <li>Identity Center issues SAML assertions to applications</li>
* <li>Applications receive SAML with user attributes from Cognito</li>
* </ul>
*
* <p><b>Benefits of this approach:</b></p>
* <ul>
* <li>Fully automated - Cognito API supports complete configuration</li>
* <li>User/group management stays in Cognito only</li>
* <li>Identity Center provides SAML to applications</li>
* <li>No manual console steps for user provisioning</li>
* </ul>
*
* @param applicationArn Identity Center application ARN (not used currently, for future application-specific config)
* @param userPoolId Cognito User Pool ID
*/
private void configureCognitoAsExternalIdP(String applicationArn, String userPoolId) {
// Guard against multiple invocations from callback
if (cognitoConfigured) {
LOG.info("Cognito already configured as trusted token issuer - skipping duplicate invocation");
return;
}
cognitoConfigured = true;
LOG.info("Configuring Cognito as trusted token issuer for Identity Center");
LOG.info(" Application ARN: " + applicationArn);
LOG.info(" Cognito User Pool ID: " + userPoolId);
// Construct Cognito OIDC issuer URL
// Format: https://cognito-idp.{region}.amazonaws.com/{userPoolId}
String issuerUrl = String.format("https://cognito-idp.%s.amazonaws.com/%s", region, userPoolId);
String trustedTokenIssuerName = stackName + "-cognito-idp";
LOG.info(" Issuer URL: " + issuerUrl);
// Not logged with its value (nor below, with its resolved ARN) — resource identifiers,
// not credentials, but keeping them out of logs avoids handing an attacker with log
// access a ready-made resource reference.
LOG.info(" Trusted Token Issuer Name configured");
// Create trusted token issuer in Identity Center
// This tells Identity Center to trust OIDC JWT tokens from Cognito
AwsSdkCall createTrustedTokenIssuerCall = AwsSdkCall.builder()
.service("SSOAdmin")
.action("createTrustedTokenIssuer")
.parameters(Map.of(
"Name", trustedTokenIssuerName,
"InstanceArn", ssoInstanceArn,
"TrustedTokenIssuerType", "OIDC_JWT",
"TrustedTokenIssuerConfiguration", Map.of(
"OidcJwtConfiguration", Map.of(
"IssuerUrl", issuerUrl,
// Map Cognito email claim to Identity Center userId
"ClaimAttributePath", "email",
"IdentityStoreAttributePath", "emails.value",
// Retrieve JWKS from Cognito's well-known endpoint
"JwksRetrievalOption", "OPEN_ID_DISCOVERY"
)
)
))
.physicalResourceId(PhysicalResourceId.fromResponse("TrustedTokenIssuerArn"))
.region(region)
.build();
// Delete trusted token issuer on stack deletion
AwsSdkCall deleteTrustedTokenIssuerCall = AwsSdkCall.builder()
.service("SSOAdmin")
.action("deleteTrustedTokenIssuer")
.parameters(Map.of(
"TrustedTokenIssuerArn", new software.amazon.awscdk.customresources.PhysicalResourceIdReference()
))
.region(region)
.ignoreErrorCodesMatching("ResourceNotFoundException")
.build();
// Create Custom Resource for trusted token issuer
AwsCustomResource trustedTokenIssuer = AwsCustomResource.Builder.create(this, "CognitoTrustedTokenIssuer")
.onCreate(createTrustedTokenIssuerCall)
.onDelete(deleteTrustedTokenIssuerCall)
.policy(AwsCustomResourcePolicy.fromStatements(List.of(
software.amazon.awscdk.services.iam.PolicyStatement.Builder.create()
.actions(List.of(
"sso:CreateTrustedTokenIssuer",
"sso:DeleteTrustedTokenIssuer",
"sso:DescribeTrustedTokenIssuer",
"sso:UpdateTrustedTokenIssuer"
))
.resources(List.of("*"))
.build()
)))
.build();
String trustedTokenIssuerArn = trustedTokenIssuer.getResponseField("TrustedTokenIssuerArn");
LOG.info("Cognito configured as trusted token issuer for Identity Center");
// Not logged with its value — see the comment above configureCognitoAsExternalIdP's
// Trusted Token Issuer Name log for why.
LOG.info(" Trusted Token Issuer ARN configured");
// Configure application grant with audience claim for Cognito token validation
// Wait for Cognito Client ID to be available, then configure the grant
ctx.cognitoClientId.onSet(clientId -> {
LOG.info("Configuring application grant with Aud claim: " + clientId);
configureApplicationGrant(applicationArn, trustedTokenIssuerArn, clientId);
});
// Create CloudFormation output for trusted token issuer ARN
CfnOutput.Builder.create(this, "TrustedTokenIssuerArn")
.description("Cognito Trusted Token Issuer ARN for Identity Center")
.value(trustedTokenIssuerArn)
.build();
CfnOutput.Builder.create(this, "CognitoIdpIntegration")
.description("Cognito integration status")
.value("FULLY AUTOMATED - Cognito acts as IdP for Identity Center, users managed in Cognito only")
.build();
// Document group claim mapping requirements
LOG.info("Hybrid architecture configured: Cognito (users/groups) -> Identity Center (SAML) -> Application");
LOG.info("IMPORTANT: For group synchronization to work:");
LOG.info(" 1. Set cognitoAutoProvision=true to create Cognito User Pool");
LOG.info(" 2. Set cognitoCreateGroups=true to create groups in Cognito");
LOG.info(" 3. Cognito includes groups in 'cognito:groups' claim");
LOG.info(" 4. Configure SAML attribute mapping in Identity Center console:");
LOG.info(" - Attribute: groups");
LOG.info(" - Maps to: ${path:cognito:groups}");
// Create CloudFormation output with group mapping instructions
CfnOutput.Builder.create(this, "CognitoGroupMapping")
.description("Group attribute mapping - configure in Identity Center console")
.value("Add attribute mapping: groups -> ${path:cognito:groups} (requires cognitoCreateGroups=true)")
.build();
CfnOutput.Builder.create(this, "CognitoGroupsClaim")
.description("Cognito groups claim name")
.value("cognito:groups - automatically included in Cognito JWT when user is in groups")
.build();
CfnOutput.Builder.create(this, "ApplicationAssignmentStatus")
.description("Application assignment configuration status")
.value("AUTOMATED - AssignmentRequired=false (all Identity Center users can access)")
.build();
CfnOutput.Builder.create(this, "ApplicationGrantConfig")
.description("Application grant configuration status")
.value("AUTOMATED - JWT Bearer grant configured with Cognito as trusted token issuer")
.build();
CfnOutput.Builder.create(this, "AudienceClaimConfig")
.description("Audience claim validation")
.value("AUTOMATED - Aud claim set to Cognito App Client ID for token validation")
.build();
}
/**
* Configures the Identity Center application grant with JWT Bearer type and audience claim.
*
* <p>TODO: SAML integration is not yet complete. This stub exists so the module compiles.
* Implement once Identity Center SAML flow is ready.</p>
*
* @param applicationArn Identity Center application ARN
* @param trustedTokenIssuerArn ARN of the Cognito trusted token issuer (not used currently —
* its value is deliberately kept out of logs, see the log line
* below; reserved for the actual grant configuration once the
* SAML integration above is implemented)
* @param cognitoClientId Cognito app client ID (used as audience claim)
*/
// codeql[java/unused-parameter] -- applicationArn/trustedTokenIssuerArn are unused: this whole
// method is a stub (see its own TODO javadoc) so the module compiles ahead of the SAML
// integration actually being implemented.
private void configureApplicationGrant(String applicationArn, String trustedTokenIssuerArn, String cognitoClientId) {
LOG.warning("configureApplicationGrant() not yet implemented — SAML integration pending.");
LOG.warning(" Application ARN: " + applicationArn);
// Not logged with its value — see the comment above configureCognitoAsExternalIdP's
// Trusted Token Issuer Name log for why.
LOG.warning(" Trusted Token Issuer ARN configured");
LOG.warning(" Cognito Client ID: " + cognitoClientId);
}
}