From bd2ab7f1d9498ecd785c23682f86f837da25755e Mon Sep 17 00:00:00 2001 From: Kevin Fleming Date: Thu, 1 Sep 2022 14:23:55 +0100 Subject: [PATCH 01/11] Add SAML user pool identity provider construct --- .../aws-cognito/lib/user-pool-idps/index.ts | 1 + .../aws-cognito/lib/user-pool-idps/saml.ts | 103 ++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 packages/@aws-cdk/aws-cognito/lib/user-pool-idps/saml.ts diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/index.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/index.ts index fd7ad04af70fe..b99422478a85d 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/index.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/index.ts @@ -4,3 +4,4 @@ export * from './amazon'; export * from './facebook'; export * from './google'; export * from './oidc'; +export * from './saml'; diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/saml.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/saml.ts new file mode 100644 index 0000000000000..d338375e2d1c5 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/saml.ts @@ -0,0 +1,103 @@ +import { Names, Token } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnUserPoolIdentityProvider } from '../cognito.generated'; +import { UserPoolIdentityProviderProps } from './base'; +import { UserPoolIdentityProviderBase } from './private/user-pool-idp-base'; + +/** + * Properties to initialize UserPoolIdentityProviderSaml. + */ +export interface UserPoolIdentityProviderSamlProps extends UserPoolIdentityProviderProps { + /** + * The name of the provider. + * + * @default - the unique ID of the construct + */ + readonly name?: string; + + /** + * Identifiers + * + * Identifiers can be used to redirect users to the correct IdP in multitenant apps. + * + * @default - no identifiers used + */ + readonly identifiers?: string[] + + /** + * The SAML metadata file content. + * + * @default - no file content specified + */ + readonly metadataFile?: string; + + /** + * The SAML metadata URL. + * + * @default - no URL specified + */ + readonly metadataUrl?: string; + + /** + * Whether to enable the "Sign-out flow" feature. + * + * @default - false + */ + readonly idpSignout?: boolean; +} + +/** + * Represents a identity provider that integrates with SAML. + * @resource AWS::Cognito::UserPoolIdentityProvider + */ +export class UserPoolIdentityProviderSaml extends UserPoolIdentityProviderBase { + public readonly providerName: string; + + constructor(scope: Construct, id: string, props: UserPoolIdentityProviderSamlProps) { + super(scope, id, props); + + if (props.name && !Token.isUnresolved(props.name) && (props.name.length < 3 || props.name.length > 32)) { + throw new Error(`Expected provider name to be between 3 and 32 characters, received ${props.name} (${props.name.length} characters)`); + } + + if ((props.metadataFile === undefined && props.metadataUrl === undefined) || + (props.metadataFile !== undefined && props.metadataUrl !== undefined)) { + throw new Error('Specify exactly one of metadataUrl and metadataFile'); + } + + const resource = new CfnUserPoolIdentityProvider(this, 'Resource', { + userPoolId: props.userPool.userPoolId, + providerName: this.getProviderName(props.name), + providerType: 'SAML', + providerDetails: { + IDPSignout: props.idpSignout ?? false, + MetadataURL: props.metadataUrl, + MetadataFile: props.metadataFile, + }, + idpIdentifiers: props.identifiers, + attributeMapping: super.configureAttributeMapping(), + }); + + this.providerName = super.getResourceNameAttribute(resource.ref); + } + + private getProviderName(name?: string): string { + if (name) { + if (!Token.isUnresolved(name) && (name.length < 3 || name.length > 32)) { + throw new Error(`Expected provider name to be between 3 and 32 characters, received ${name} (${name.length} characters)`); + } + return name; + } + + const uniqueId = Names.uniqueId(this); + + if (uniqueId.length < 3) { + return `${uniqueId}saml`; + } + + if (uniqueId.length > 32) { + return uniqueId.substring(0, 16) + uniqueId.substring(uniqueId.length - 16); + } + return uniqueId; + } +} From ae1e2c5207fef32c154807524b42bb6873faea63 Mon Sep 17 00:00:00 2001 From: Kevin Fleming Date: Thu, 1 Sep 2022 14:42:53 +0100 Subject: [PATCH 02/11] Update Cognito README with SAML IdP support --- packages/@aws-cdk/aws-cognito/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 137cd1d214e10..e8766a08b56cd 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -528,6 +528,7 @@ The following third-party identity providers are currently supported in the CDK - [Google Login](https://developers.google.com/identity/sign-in/web/sign-in) - [Sign In With Apple](https://developer.apple.com/sign-in-with-apple/get-started/) - [OpenID Connect](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-oidc-idp.html) +- [SAML](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-saml-idp.html) The following code configures a user pool to federate with the third party provider, 'Login with Amazon'. The identity provider needs to be configured with a set of credentials that the Cognito backend can use to federate with the From 5ce936ab87e1b05627b80b25a71c7f4f790a7b80 Mon Sep 17 00:00:00 2001 From: Kevin Fleming Date: Thu, 1 Sep 2022 15:11:39 +0100 Subject: [PATCH 03/11] Ignore props-physical-name awslint rule for SAML props --- packages/@aws-cdk/aws-cognito/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-cognito/package.json b/packages/@aws-cdk/aws-cognito/package.json index 633a3b5195fa2..5946520f2ddc2 100644 --- a/packages/@aws-cdk/aws-cognito/package.json +++ b/packages/@aws-cdk/aws-cognito/package.json @@ -126,7 +126,8 @@ "props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderAmazonProps", "props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderGoogleProps", "props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderAppleProps", - "props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderOidcProps" + "props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderOidcProps", + "props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderSamlProps" ] }, "stability": "stable", From 512640094990aec62837bf7734da441afcded3b8 Mon Sep 17 00:00:00 2001 From: Kevin Fleming Date: Thu, 1 Sep 2022 15:35:53 +0100 Subject: [PATCH 04/11] Add unit tests for SAML user pool identity provider --- .../test/user-pool-idps/saml.test.ts | 208 ++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 packages/@aws-cdk/aws-cognito/test/user-pool-idps/saml.test.ts diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idps/saml.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/saml.test.ts new file mode 100644 index 0000000000000..9a8287930c61a --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/saml.test.ts @@ -0,0 +1,208 @@ +import { Template } from '@aws-cdk/assertions'; +import { Stack } from '@aws-cdk/core'; +import { ProviderAttribute, UserPool, UserPoolIdentityProviderSaml } from '../../lib'; + +describe('UserPoolIdentityProvider', () => { + describe('saml', () => { + test('metadata URL', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderSaml(stack, 'userpoolidp', { + userPool: pool, + metadataUrl: 'https://my-metadata-url.com', + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'userpoolidp', + ProviderType: 'SAML', + ProviderDetails: { + MetadataURL: 'https://my-metadata-url.com', + IDPSignout: false, + }, + }); + }); + + test('metadata file', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderSaml(stack, 'userpoolidp', { + userPool: pool, + metadataFile: 'my-file-contents', + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'userpoolidp', + ProviderType: 'SAML', + ProviderDetails: { + MetadataFile: 'my-file-contents', + IDPSignout: false, + }, + }); + }); + + test('idpSignout', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderSaml(stack, 'userpoolidp', { + userPool: pool, + metadataFile: 'my-file-contents', + idpSignout: true, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'userpoolidp', + ProviderType: 'SAML', + ProviderDetails: { + MetadataFile: 'my-file-contents', + IDPSignout: true, + }, + }); + }); + + test('registered with user pool', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + const provider = new UserPoolIdentityProviderSaml(stack, 'userpoolidp', { + userPool: pool, + metadataFile: 'my-file-contents', + }); + + // THEN + expect(pool.identityProviders).toContain(provider); + }); + + test('attribute mapping', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderSaml(stack, 'userpoolidp', { + userPool: pool, + metadataFile: 'my-file-contents', + attributeMapping: { + familyName: ProviderAttribute.other('family_name'), + givenName: ProviderAttribute.other('given_name'), + custom: { + customAttr1: ProviderAttribute.other('email'), + customAttr2: ProviderAttribute.other('sub'), + }, + }, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPoolIdentityProvider', { + AttributeMapping: { + family_name: 'family_name', + given_name: 'given_name', + customAttr1: 'email', + customAttr2: 'sub', + }, + }); + }); + + test('with provider name', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderSaml(stack, 'userpoolidp', { + userPool: pool, + name: 'my-provider', + metadataFile: 'my-file-contents', + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'my-provider', + }); + }); + + test('throws with invalid provider name', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // THEN + expect(() => new UserPoolIdentityProviderSaml(stack, 'userpoolidp', { + userPool: pool, + name: 'xy', + metadataFile: 'my-file-contents', + })).toThrow(/Expected provider name to be between 3 and 32 characters/); + }); + + test('throws when neither metadataUrl nor metadataFile is provided', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // THEN + expect(() => new UserPoolIdentityProviderSaml(stack, 'userpoolidp', { + userPool: pool, + })).toThrow(/Specify exactly one of metadataUrl and metadataFile/); + }); + + test('throws when both metadataUrl and metadataFile are provided', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // THEN + expect(() => new UserPoolIdentityProviderSaml(stack, 'userpoolidp', { + userPool: pool, + metadataUrl: 'https://my-metadata-url.com', + metadataFile: 'my-file-contents', + })).toThrow(/Specify exactly one of metadataUrl and metadataFile/); + }); + + test('generates a valid name when unique id is too short', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderSaml(stack, 'xy', { + userPool: pool, + metadataFile: 'my-file-contents', + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'xysaml', + }); + }); + + test('generates a valid name when unique id is too long', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderSaml(stack, `${'saml'.repeat(10)}xyz`, { + userPool: pool, + metadataFile: 'my-file-contents', + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'samlsamlsamlsamllsamlsamlsamlxyz', + }); + }); + }); +}); From 8394f9f58d8041d1d20b90f67bbd716f4012737e Mon Sep 17 00:00:00 2001 From: Kevin Fleming Date: Thu, 1 Sep 2022 17:41:19 +0100 Subject: [PATCH 05/11] Add integ test for SAML user pool identity provider --- packages/@aws-cdk/aws-cognito/package.json | 1 + .../test/integ.user-pool-idp.saml.ts | 40 +++ .../user-pool-idp.saml.integ.snapshot/cdk.out | 1 + ...l-identity-provider-saml-stack.assets.json | 19 ++ ...identity-provider-saml-stack.template.json | 145 +++++++++++ .../integ.json | 12 + ...efaultTestDeployAssert97F09C26.assets.json | 19 ++ ...aultTestDeployAssert97F09C26.template.json | 36 +++ .../manifest.json | 135 +++++++++++ .../tree.json | 228 ++++++++++++++++++ 10 files changed, 636 insertions(+) create mode 100644 packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.saml.ts create mode 100644 packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/cdk.out create mode 100644 packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/integ-user-pool-identity-provider-saml-stack.assets.json create mode 100644 packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/integ-user-pool-identity-provider-saml-stack.template.json create mode 100644 packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/integ.json create mode 100644 packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/integuserpoolidentityprovidersamltestDefaultTestDeployAssert97F09C26.assets.json create mode 100644 packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/integuserpoolidentityprovidersamltestDefaultTestDeployAssert97F09C26.template.json create mode 100644 packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/manifest.json create mode 100644 packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/tree.json diff --git a/packages/@aws-cdk/aws-cognito/package.json b/packages/@aws-cdk/aws-cognito/package.json index 5946520f2ddc2..d7e78bfc5cb27 100644 --- a/packages/@aws-cdk/aws-cognito/package.json +++ b/packages/@aws-cdk/aws-cognito/package.json @@ -83,6 +83,7 @@ "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/integ-runner": "0.0.0", + "@aws-cdk/integ-tests": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", "@types/jest": "^27.5.2", diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.saml.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.saml.ts new file mode 100644 index 0000000000000..0c2b7c2ca0eb0 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.saml.ts @@ -0,0 +1,40 @@ +import { App, CfnOutput, RemovalPolicy, Stack } from '@aws-cdk/core'; +import { IntegTest } from '@aws-cdk/integ-tests'; +import { Construct } from 'constructs'; +import { UserPool, UserPoolIdentityProviderSaml } from '../lib'; + +class TestStack extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + const userpool = new UserPool(this, 'pool', { + removalPolicy: RemovalPolicy.DESTROY, + }); + + new UserPoolIdentityProviderSaml(this, 'cdk', { + userPool: userpool, + name: 'cdk', + metadataUrl: 'https://fujifish.github.io/samling/public/metadata.xml', + }); + + const client = userpool.addClient('client'); + + const domain = userpool.addDomain('domain', { + cognitoDomain: { + domainPrefix: 'cdk-test-pool', + }, + }); + + new CfnOutput(this, 'SignInLink', { + value: domain.signInUrl(client, { + redirectUri: 'https://example.com', + }), + }); + } +} + +const app = new App(); +const testCase = new TestStack(app, 'integ-user-pool-identity-provider-saml-stack'); + +new IntegTest(app, 'integ-user-pool-identity-provider-saml-test', { + testCases: [testCase], +}); diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/cdk.out b/packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/cdk.out new file mode 100644 index 0000000000000..8ecc185e9dbee --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"21.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/integ-user-pool-identity-provider-saml-stack.assets.json b/packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/integ-user-pool-identity-provider-saml-stack.assets.json new file mode 100644 index 0000000000000..8e65975b437d9 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/integ-user-pool-identity-provider-saml-stack.assets.json @@ -0,0 +1,19 @@ +{ + "version": "21.0.0", + "files": { + "6f6f07786415216f13b738979cec5ad81dbab3283fae83b99324965935cc1d60": { + "source": { + "path": "integ-user-pool-identity-provider-saml-stack.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "6f6f07786415216f13b738979cec5ad81dbab3283fae83b99324965935cc1d60.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/integ-user-pool-identity-provider-saml-stack.template.json b/packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/integ-user-pool-identity-provider-saml-stack.template.json new file mode 100644 index 0000000000000..56cb33a5c739f --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/integ-user-pool-identity-provider-saml-stack.template.json @@ -0,0 +1,145 @@ +{ + "Resources": { + "pool056F3F7E": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "AccountRecoverySetting": { + "RecoveryMechanisms": [ + { + "Name": "verified_phone_number", + "Priority": 1 + }, + { + "Name": "verified_email", + "Priority": 2 + } + ] + }, + "AdminCreateUserConfig": { + "AllowAdminCreateUserOnly": true + }, + "EmailVerificationMessage": "The verification code to your new account is {####}", + "EmailVerificationSubject": "Verify your new account", + "SmsVerificationMessage": "The verification code to your new account is {####}", + "VerificationMessageTemplate": { + "DefaultEmailOption": "CONFIRM_WITH_CODE", + "EmailMessage": "The verification code to your new account is {####}", + "EmailSubject": "Verify your new account", + "SmsMessage": "The verification code to your new account is {####}" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "poolclient2623294C": { + "Type": "AWS::Cognito::UserPoolClient", + "Properties": { + "UserPoolId": { + "Ref": "pool056F3F7E" + }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthFlowsUserPoolClient": true, + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], + "SupportedIdentityProviders": [ + { + "Ref": "cdk52888317" + }, + "COGNITO" + ] + } + }, + "pooldomain430FA744": { + "Type": "AWS::Cognito::UserPoolDomain", + "Properties": { + "Domain": "cdk-test-pool", + "UserPoolId": { + "Ref": "pool056F3F7E" + } + } + }, + "cdk52888317": { + "Type": "AWS::Cognito::UserPoolIdentityProvider", + "Properties": { + "ProviderName": "cdk", + "ProviderType": "SAML", + "UserPoolId": { + "Ref": "pool056F3F7E" + }, + "ProviderDetails": { + "IDPSignout": false, + "MetadataURL": "https://fujifish.github.io/samling/public/metadata.xml" + } + } + } + }, + "Outputs": { + "SignInLink": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "pooldomain430FA744" + }, + ".auth.", + { + "Ref": "AWS::Region" + }, + ".amazoncognito.com/login?client_id=", + { + "Ref": "poolclient2623294C" + }, + "&response_type=code&redirect_uri=https://example.com" + ] + ] + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/integ.json b/packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/integ.json new file mode 100644 index 0000000000000..eb9199222282a --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "21.0.0", + "testCases": { + "integ-user-pool-identity-provider-saml-test/DefaultTest": { + "stacks": [ + "integ-user-pool-identity-provider-saml-stack" + ], + "assertionStack": "integ-user-pool-identity-provider-saml-test/DefaultTest/DeployAssert", + "assertionStackName": "integuserpoolidentityprovidersamltestDefaultTestDeployAssert97F09C26" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/integuserpoolidentityprovidersamltestDefaultTestDeployAssert97F09C26.assets.json b/packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/integuserpoolidentityprovidersamltestDefaultTestDeployAssert97F09C26.assets.json new file mode 100644 index 0000000000000..39aac6c49fa80 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/integuserpoolidentityprovidersamltestDefaultTestDeployAssert97F09C26.assets.json @@ -0,0 +1,19 @@ +{ + "version": "21.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "integuserpoolidentityprovidersamltestDefaultTestDeployAssert97F09C26.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/integuserpoolidentityprovidersamltestDefaultTestDeployAssert97F09C26.template.json b/packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/integuserpoolidentityprovidersamltestDefaultTestDeployAssert97F09C26.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/integuserpoolidentityprovidersamltestDefaultTestDeployAssert97F09C26.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/manifest.json b/packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/manifest.json new file mode 100644 index 0000000000000..dc88fad953da5 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/manifest.json @@ -0,0 +1,135 @@ +{ + "version": "21.0.0", + "artifacts": { + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + }, + "integ-user-pool-identity-provider-saml-stack.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "integ-user-pool-identity-provider-saml-stack.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "integ-user-pool-identity-provider-saml-stack": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "integ-user-pool-identity-provider-saml-stack.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/6f6f07786415216f13b738979cec5ad81dbab3283fae83b99324965935cc1d60.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "integ-user-pool-identity-provider-saml-stack.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "integ-user-pool-identity-provider-saml-stack.assets" + ], + "metadata": { + "/integ-user-pool-identity-provider-saml-stack/pool/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "pool056F3F7E" + } + ], + "/integ-user-pool-identity-provider-saml-stack/pool/client/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "poolclient2623294C" + } + ], + "/integ-user-pool-identity-provider-saml-stack/pool/domain/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "pooldomain430FA744" + } + ], + "/integ-user-pool-identity-provider-saml-stack/cdk/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "cdk52888317" + } + ], + "/integ-user-pool-identity-provider-saml-stack/SignInLink": [ + { + "type": "aws:cdk:logicalId", + "data": "SignInLink" + } + ], + "/integ-user-pool-identity-provider-saml-stack/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/integ-user-pool-identity-provider-saml-stack/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "integ-user-pool-identity-provider-saml-stack" + }, + "integuserpoolidentityprovidersamltestDefaultTestDeployAssert97F09C26.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "integuserpoolidentityprovidersamltestDefaultTestDeployAssert97F09C26.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "integuserpoolidentityprovidersamltestDefaultTestDeployAssert97F09C26": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "integuserpoolidentityprovidersamltestDefaultTestDeployAssert97F09C26.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "integuserpoolidentityprovidersamltestDefaultTestDeployAssert97F09C26.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "integuserpoolidentityprovidersamltestDefaultTestDeployAssert97F09C26.assets" + ], + "metadata": { + "/integ-user-pool-identity-provider-saml-test/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/integ-user-pool-identity-provider-saml-test/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "integ-user-pool-identity-provider-saml-test/DefaultTest/DeployAssert" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/tree.json b/packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/tree.json new file mode 100644 index 0000000000000..65caa4df4b09b --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idp.saml.integ.snapshot/tree.json @@ -0,0 +1,228 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.92" + } + }, + "integ-user-pool-identity-provider-saml-stack": { + "id": "integ-user-pool-identity-provider-saml-stack", + "path": "integ-user-pool-identity-provider-saml-stack", + "children": { + "pool": { + "id": "pool", + "path": "integ-user-pool-identity-provider-saml-stack/pool", + "children": { + "Resource": { + "id": "Resource", + "path": "integ-user-pool-identity-provider-saml-stack/pool/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Cognito::UserPool", + "aws:cdk:cloudformation:props": { + "accountRecoverySetting": { + "recoveryMechanisms": [ + { + "name": "verified_phone_number", + "priority": 1 + }, + { + "name": "verified_email", + "priority": 2 + } + ] + }, + "adminCreateUserConfig": { + "allowAdminCreateUserOnly": true + }, + "emailVerificationMessage": "The verification code to your new account is {####}", + "emailVerificationSubject": "Verify your new account", + "smsVerificationMessage": "The verification code to your new account is {####}", + "verificationMessageTemplate": { + "defaultEmailOption": "CONFIRM_WITH_CODE", + "emailMessage": "The verification code to your new account is {####}", + "emailSubject": "Verify your new account", + "smsMessage": "The verification code to your new account is {####}" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cognito.CfnUserPool", + "version": "0.0.0" + } + }, + "client": { + "id": "client", + "path": "integ-user-pool-identity-provider-saml-stack/pool/client", + "children": { + "Resource": { + "id": "Resource", + "path": "integ-user-pool-identity-provider-saml-stack/pool/client/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Cognito::UserPoolClient", + "aws:cdk:cloudformation:props": { + "userPoolId": { + "Ref": "pool056F3F7E" + }, + "allowedOAuthFlows": [ + "implicit", + "code" + ], + "allowedOAuthFlowsUserPoolClient": true, + "allowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "callbackUrLs": [ + "https://example.com" + ], + "supportedIdentityProviders": [ + { + "Ref": "cdk52888317" + }, + "COGNITO" + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cognito.CfnUserPoolClient", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cognito.UserPoolClient", + "version": "0.0.0" + } + }, + "domain": { + "id": "domain", + "path": "integ-user-pool-identity-provider-saml-stack/pool/domain", + "children": { + "Resource": { + "id": "Resource", + "path": "integ-user-pool-identity-provider-saml-stack/pool/domain/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Cognito::UserPoolDomain", + "aws:cdk:cloudformation:props": { + "domain": "cdk-test-pool", + "userPoolId": { + "Ref": "pool056F3F7E" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cognito.CfnUserPoolDomain", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cognito.UserPoolDomain", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cognito.UserPool", + "version": "0.0.0" + } + }, + "cdk": { + "id": "cdk", + "path": "integ-user-pool-identity-provider-saml-stack/cdk", + "children": { + "Resource": { + "id": "Resource", + "path": "integ-user-pool-identity-provider-saml-stack/cdk/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Cognito::UserPoolIdentityProvider", + "aws:cdk:cloudformation:props": { + "providerName": "cdk", + "providerType": "SAML", + "userPoolId": { + "Ref": "pool056F3F7E" + }, + "providerDetails": { + "IDPSignout": false, + "MetadataURL": "https://fujifish.github.io/samling/public/metadata.xml" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cognito.CfnUserPoolIdentityProvider", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cognito.UserPoolIdentityProviderSaml", + "version": "0.0.0" + } + }, + "SignInLink": { + "id": "SignInLink", + "path": "integ-user-pool-identity-provider-saml-stack/SignInLink", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + }, + "integ-user-pool-identity-provider-saml-test": { + "id": "integ-user-pool-identity-provider-saml-test", + "path": "integ-user-pool-identity-provider-saml-test", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "integ-user-pool-identity-provider-saml-test/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "integ-user-pool-identity-provider-saml-test/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.92" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "integ-user-pool-identity-provider-saml-test/DefaultTest/DeployAssert", + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTest", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.App", + "version": "0.0.0" + } + } +} \ No newline at end of file From 265c785f4c03b77d6cd598ec8d32bc3fcedf29e0 Mon Sep 17 00:00:00 2001 From: Kevin Fleming Date: Fri, 2 Sep 2022 13:42:58 +0100 Subject: [PATCH 06/11] Use Names.uniqueResourceName instead of Names.uniqueId --- .../@aws-cdk/aws-cognito/lib/user-pool-idps/saml.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/saml.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/saml.ts index d338375e2d1c5..515049335b596 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/saml.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/saml.ts @@ -89,15 +89,14 @@ export class UserPoolIdentityProviderSaml extends UserPoolIdentityProviderBase { return name; } - const uniqueId = Names.uniqueId(this); + const uniqueName = Names.uniqueResourceName(this, { + maxLength: 32, + }); - if (uniqueId.length < 3) { - return `${uniqueId}saml`; + if (uniqueName.length < 3) { + return `${uniqueName}saml`; } - if (uniqueId.length > 32) { - return uniqueId.substring(0, 16) + uniqueId.substring(uniqueId.length - 16); - } - return uniqueId; + return uniqueName; } } From 6946c0be9bdc74eba4e7a69c9384c0aa5392fd04 Mon Sep 17 00:00:00 2001 From: Kevin Fleming Date: Fri, 2 Sep 2022 13:45:32 +0100 Subject: [PATCH 07/11] Extract validateName function --- .../aws-cognito/lib/user-pool-idps/saml.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/saml.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/saml.ts index 515049335b596..91dfc96bbc42d 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/saml.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/saml.ts @@ -56,9 +56,7 @@ export class UserPoolIdentityProviderSaml extends UserPoolIdentityProviderBase { constructor(scope: Construct, id: string, props: UserPoolIdentityProviderSamlProps) { super(scope, id, props); - if (props.name && !Token.isUnresolved(props.name) && (props.name.length < 3 || props.name.length > 32)) { - throw new Error(`Expected provider name to be between 3 and 32 characters, received ${props.name} (${props.name.length} characters)`); - } + this.validateName(props.name); if ((props.metadataFile === undefined && props.metadataUrl === undefined) || (props.metadataFile !== undefined && props.metadataUrl !== undefined)) { @@ -83,9 +81,7 @@ export class UserPoolIdentityProviderSaml extends UserPoolIdentityProviderBase { private getProviderName(name?: string): string { if (name) { - if (!Token.isUnresolved(name) && (name.length < 3 || name.length > 32)) { - throw new Error(`Expected provider name to be between 3 and 32 characters, received ${name} (${name.length} characters)`); - } + this.validateName(name); return name; } @@ -99,4 +95,10 @@ export class UserPoolIdentityProviderSaml extends UserPoolIdentityProviderBase { return uniqueName; } + + private validateName(name?: string) { + if (name && !Token.isUnresolved(name) && (name.length < 3 || name.length > 32)) { + throw new Error(`Expected provider name to be between 3 and 32 characters, received ${name} (${name.length} characters)`); + } + } } From b3f44e1663bd826babfff283d3338b6b9427bc47 Mon Sep 17 00:00:00 2001 From: Kevin Fleming Date: Fri, 2 Sep 2022 14:18:11 +0100 Subject: [PATCH 08/11] Add provider name length restrictions to docstring --- packages/@aws-cdk/aws-cognito/lib/user-pool-idps/saml.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/saml.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/saml.ts index 91dfc96bbc42d..149c9b01f2ef1 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/saml.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/saml.ts @@ -9,7 +9,7 @@ import { UserPoolIdentityProviderBase } from './private/user-pool-idp-base'; */ export interface UserPoolIdentityProviderSamlProps extends UserPoolIdentityProviderProps { /** - * The name of the provider. + * The name of the provider. Must be between 3 and 32 characters. * * @default - the unique ID of the construct */ From afda2ffe8c28c05d036b644a7d2852c1a4b7410e Mon Sep 17 00:00:00 2001 From: Kevin Fleming Date: Fri, 2 Sep 2022 14:18:25 +0100 Subject: [PATCH 09/11] Rework how SAML metadata is specified --- .../aws-cognito/lib/user-pool-idps/saml.ts | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/saml.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/saml.ts index 149c9b01f2ef1..d2b1ceceab56c 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/saml.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/saml.ts @@ -25,18 +25,16 @@ export interface UserPoolIdentityProviderSamlProps extends UserPoolIdentityProvi readonly identifiers?: string[] /** - * The SAML metadata file content. - * - * @default - no file content specified + * The SAML metadata file type. */ - readonly metadataFile?: string; + readonly metadataType: UserPoolIdentityProviderSamlMetadataType; /** - * The SAML metadata URL. - * - * @default - no URL specified + * The SAML metadata content. + * If metadataType is set to URL, this should be the metadata URL. + * If metadataType is set to FILE, this should be the metadata file contents. */ - readonly metadataUrl?: string; + readonly metadataContent: string; /** * Whether to enable the "Sign-out flow" feature. @@ -46,6 +44,17 @@ export interface UserPoolIdentityProviderSamlProps extends UserPoolIdentityProvi readonly idpSignout?: boolean; } +/** + * Metadata types that can be used for a SAML user pool identity provider. + */ +export enum UserPoolIdentityProviderSamlMetadataType { + /** Metadata provided via a URL. */ + URL = 'url', + + /** Metadata provided via the contents of a file. */ + FILE = 'file', +} + /** * Represents a identity provider that integrates with SAML. * @resource AWS::Cognito::UserPoolIdentityProvider @@ -58,20 +67,21 @@ export class UserPoolIdentityProviderSaml extends UserPoolIdentityProviderBase { this.validateName(props.name); - if ((props.metadataFile === undefined && props.metadataUrl === undefined) || - (props.metadataFile !== undefined && props.metadataUrl !== undefined)) { - throw new Error('Specify exactly one of metadataUrl and metadataFile'); + const providerDetails: Record = { + IDPSignout: props.idpSignout ?? false, + }; + + if (props.metadataType === UserPoolIdentityProviderSamlMetadataType.URL) { + providerDetails.MetadataURL = props.metadataContent; + } else if (props.metadataType === UserPoolIdentityProviderSamlMetadataType.FILE) { + providerDetails.MetadataFile = props.metadataContent; } const resource = new CfnUserPoolIdentityProvider(this, 'Resource', { userPoolId: props.userPool.userPoolId, providerName: this.getProviderName(props.name), providerType: 'SAML', - providerDetails: { - IDPSignout: props.idpSignout ?? false, - MetadataURL: props.metadataUrl, - MetadataFile: props.metadataFile, - }, + providerDetails, idpIdentifiers: props.identifiers, attributeMapping: super.configureAttributeMapping(), }); From 62e0da1e0adfabd0d00ec2dedf15cee774888278 Mon Sep 17 00:00:00 2001 From: Kevin Fleming Date: Fri, 2 Sep 2022 14:45:41 +0100 Subject: [PATCH 10/11] Update SAML IdP tests --- .../test/integ.user-pool-idp.saml.ts | 5 +- .../test/user-pool-idps/saml.test.ts | 57 +++++++------------ 2 files changed, 24 insertions(+), 38 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.saml.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.saml.ts index 0c2b7c2ca0eb0..f841ab8a14d60 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.saml.ts +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.saml.ts @@ -1,7 +1,7 @@ import { App, CfnOutput, RemovalPolicy, Stack } from '@aws-cdk/core'; import { IntegTest } from '@aws-cdk/integ-tests'; import { Construct } from 'constructs'; -import { UserPool, UserPoolIdentityProviderSaml } from '../lib'; +import { UserPool, UserPoolIdentityProviderSaml, UserPoolIdentityProviderSamlMetadataType } from '../lib'; class TestStack extends Stack { constructor(scope: Construct, id: string) { @@ -13,7 +13,8 @@ class TestStack extends Stack { new UserPoolIdentityProviderSaml(this, 'cdk', { userPool: userpool, name: 'cdk', - metadataUrl: 'https://fujifish.github.io/samling/public/metadata.xml', + metadataType: UserPoolIdentityProviderSamlMetadataType.URL, + metadataContent: 'https://fujifish.github.io/samling/public/metadata.xml', }); const client = userpool.addClient('client'); diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idps/saml.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/saml.test.ts index 9a8287930c61a..877bc79bad1a4 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-idps/saml.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/saml.test.ts @@ -1,6 +1,6 @@ -import { Template } from '@aws-cdk/assertions'; +import { Template, Match } from '@aws-cdk/assertions'; import { Stack } from '@aws-cdk/core'; -import { ProviderAttribute, UserPool, UserPoolIdentityProviderSaml } from '../../lib'; +import { ProviderAttribute, UserPool, UserPoolIdentityProviderSaml, UserPoolIdentityProviderSamlMetadataType } from '../../lib'; describe('UserPoolIdentityProvider', () => { describe('saml', () => { @@ -12,7 +12,8 @@ describe('UserPoolIdentityProvider', () => { // WHEN new UserPoolIdentityProviderSaml(stack, 'userpoolidp', { userPool: pool, - metadataUrl: 'https://my-metadata-url.com', + metadataType: UserPoolIdentityProviderSamlMetadataType.URL, + metadataContent: 'https://my-metadata-url.com', }); // THEN @@ -34,7 +35,8 @@ describe('UserPoolIdentityProvider', () => { // WHEN new UserPoolIdentityProviderSaml(stack, 'userpoolidp', { userPool: pool, - metadataFile: 'my-file-contents', + metadataType: UserPoolIdentityProviderSamlMetadataType.FILE, + metadataContent: 'my-file-contents', }); // THEN @@ -56,7 +58,8 @@ describe('UserPoolIdentityProvider', () => { // WHEN new UserPoolIdentityProviderSaml(stack, 'userpoolidp', { userPool: pool, - metadataFile: 'my-file-contents', + metadataType: UserPoolIdentityProviderSamlMetadataType.FILE, + metadataContent: 'my-file-contents', idpSignout: true, }); @@ -79,7 +82,8 @@ describe('UserPoolIdentityProvider', () => { // WHEN const provider = new UserPoolIdentityProviderSaml(stack, 'userpoolidp', { userPool: pool, - metadataFile: 'my-file-contents', + metadataType: UserPoolIdentityProviderSamlMetadataType.FILE, + metadataContent: 'my-file-contents', }); // THEN @@ -94,7 +98,8 @@ describe('UserPoolIdentityProvider', () => { // WHEN new UserPoolIdentityProviderSaml(stack, 'userpoolidp', { userPool: pool, - metadataFile: 'my-file-contents', + metadataType: UserPoolIdentityProviderSamlMetadataType.FILE, + metadataContent: 'my-file-contents', attributeMapping: { familyName: ProviderAttribute.other('family_name'), givenName: ProviderAttribute.other('given_name'), @@ -125,7 +130,8 @@ describe('UserPoolIdentityProvider', () => { new UserPoolIdentityProviderSaml(stack, 'userpoolidp', { userPool: pool, name: 'my-provider', - metadataFile: 'my-file-contents', + metadataType: UserPoolIdentityProviderSamlMetadataType.FILE, + metadataContent: 'my-file-contents', }); // THEN @@ -143,34 +149,11 @@ describe('UserPoolIdentityProvider', () => { expect(() => new UserPoolIdentityProviderSaml(stack, 'userpoolidp', { userPool: pool, name: 'xy', - metadataFile: 'my-file-contents', + metadataType: UserPoolIdentityProviderSamlMetadataType.FILE, + metadataContent: 'my-file-contents', })).toThrow(/Expected provider name to be between 3 and 32 characters/); }); - test('throws when neither metadataUrl nor metadataFile is provided', () => { - // GIVEN - const stack = new Stack(); - const pool = new UserPool(stack, 'userpool'); - - // THEN - expect(() => new UserPoolIdentityProviderSaml(stack, 'userpoolidp', { - userPool: pool, - })).toThrow(/Specify exactly one of metadataUrl and metadataFile/); - }); - - test('throws when both metadataUrl and metadataFile are provided', () => { - // GIVEN - const stack = new Stack(); - const pool = new UserPool(stack, 'userpool'); - - // THEN - expect(() => new UserPoolIdentityProviderSaml(stack, 'userpoolidp', { - userPool: pool, - metadataUrl: 'https://my-metadata-url.com', - metadataFile: 'my-file-contents', - })).toThrow(/Specify exactly one of metadataUrl and metadataFile/); - }); - test('generates a valid name when unique id is too short', () => { // GIVEN const stack = new Stack(); @@ -179,7 +162,8 @@ describe('UserPoolIdentityProvider', () => { // WHEN new UserPoolIdentityProviderSaml(stack, 'xy', { userPool: pool, - metadataFile: 'my-file-contents', + metadataType: UserPoolIdentityProviderSamlMetadataType.FILE, + metadataContent: 'my-file-contents', }); // THEN @@ -196,12 +180,13 @@ describe('UserPoolIdentityProvider', () => { // WHEN new UserPoolIdentityProviderSaml(stack, `${'saml'.repeat(10)}xyz`, { userPool: pool, - metadataFile: 'my-file-contents', + metadataType: UserPoolIdentityProviderSamlMetadataType.FILE, + metadataContent: 'my-file-contents', }); // THEN Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPoolIdentityProvider', { - ProviderName: 'samlsamlsamlsamllsamlsamlsamlxyz', + ProviderName: Match.stringLikeRegexp('^\\w{3,32}$'), }); }); }); From 36965e73f1c9a62a1dcb48a37e0832cffc082692 Mon Sep 17 00:00:00 2001 From: Kevin Fleming Date: Sat, 3 Sep 2022 10:53:10 +0100 Subject: [PATCH 11/11] Rework how SAML metadata is provided --- .../aws-cognito/lib/user-pool-idps/saml.ts | 56 ++++++++++++------- .../test/integ.user-pool-idp.saml.ts | 5 +- .../test/user-pool-idps/saml.test.ts | 29 ++++------ 3 files changed, 49 insertions(+), 41 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/saml.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/saml.ts index d2b1ceceab56c..3c8846b904693 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/saml.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/saml.ts @@ -25,16 +25,9 @@ export interface UserPoolIdentityProviderSamlProps extends UserPoolIdentityProvi readonly identifiers?: string[] /** - * The SAML metadata file type. + * The SAML metadata. */ - readonly metadataType: UserPoolIdentityProviderSamlMetadataType; - - /** - * The SAML metadata content. - * If metadataType is set to URL, this should be the metadata URL. - * If metadataType is set to FILE, this should be the metadata file contents. - */ - readonly metadataContent: string; + readonly metadata: UserPoolIdentityProviderSamlMetadata; /** * Whether to enable the "Sign-out flow" feature. @@ -55,6 +48,35 @@ export enum UserPoolIdentityProviderSamlMetadataType { FILE = 'file', } +/** + * Metadata for a SAML user pool identity provider. + */ +export class UserPoolIdentityProviderSamlMetadata { + + /** + * Specify SAML metadata via a URL. + */ + public static url(url: string): UserPoolIdentityProviderSamlMetadata { + return new UserPoolIdentityProviderSamlMetadata(url, UserPoolIdentityProviderSamlMetadataType.URL); + } + + /** + * Specify SAML metadata via the contents of a file. + */ + public static file(fileContent: string): UserPoolIdentityProviderSamlMetadata { + return new UserPoolIdentityProviderSamlMetadata(fileContent, UserPoolIdentityProviderSamlMetadataType.FILE); + } + + /** + * Construct the metadata for a SAML identity provider. + * + * @param metadataContent A URL hosting SAML metadata, or the content of a file containing SAML metadata. + * @param metadataType The type of metadata, either a URL or file content. + */ + private constructor(public readonly metadataContent: string, public readonly metadataType: UserPoolIdentityProviderSamlMetadataType) { + } +} + /** * Represents a identity provider that integrates with SAML. * @resource AWS::Cognito::UserPoolIdentityProvider @@ -67,21 +89,17 @@ export class UserPoolIdentityProviderSaml extends UserPoolIdentityProviderBase { this.validateName(props.name); - const providerDetails: Record = { - IDPSignout: props.idpSignout ?? false, - }; - - if (props.metadataType === UserPoolIdentityProviderSamlMetadataType.URL) { - providerDetails.MetadataURL = props.metadataContent; - } else if (props.metadataType === UserPoolIdentityProviderSamlMetadataType.FILE) { - providerDetails.MetadataFile = props.metadataContent; - } + const { metadataType, metadataContent } = props.metadata; const resource = new CfnUserPoolIdentityProvider(this, 'Resource', { userPoolId: props.userPool.userPoolId, providerName: this.getProviderName(props.name), providerType: 'SAML', - providerDetails, + providerDetails: { + IDPSignout: props.idpSignout ?? false, + MetadataURL: metadataType === UserPoolIdentityProviderSamlMetadataType.URL ? metadataContent : undefined, + MetadataFile: metadataType === UserPoolIdentityProviderSamlMetadataType.FILE ? metadataContent : undefined, + }, idpIdentifiers: props.identifiers, attributeMapping: super.configureAttributeMapping(), }); diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.saml.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.saml.ts index f841ab8a14d60..fd1de9dd1fc77 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.saml.ts +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.saml.ts @@ -1,7 +1,7 @@ import { App, CfnOutput, RemovalPolicy, Stack } from '@aws-cdk/core'; import { IntegTest } from '@aws-cdk/integ-tests'; import { Construct } from 'constructs'; -import { UserPool, UserPoolIdentityProviderSaml, UserPoolIdentityProviderSamlMetadataType } from '../lib'; +import { UserPool, UserPoolIdentityProviderSaml, UserPoolIdentityProviderSamlMetadata } from '../lib'; class TestStack extends Stack { constructor(scope: Construct, id: string) { @@ -13,8 +13,7 @@ class TestStack extends Stack { new UserPoolIdentityProviderSaml(this, 'cdk', { userPool: userpool, name: 'cdk', - metadataType: UserPoolIdentityProviderSamlMetadataType.URL, - metadataContent: 'https://fujifish.github.io/samling/public/metadata.xml', + metadata: UserPoolIdentityProviderSamlMetadata.url('https://fujifish.github.io/samling/public/metadata.xml'), }); const client = userpool.addClient('client'); diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idps/saml.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/saml.test.ts index 877bc79bad1a4..464bd66396ca3 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-idps/saml.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/saml.test.ts @@ -1,6 +1,6 @@ import { Template, Match } from '@aws-cdk/assertions'; import { Stack } from '@aws-cdk/core'; -import { ProviderAttribute, UserPool, UserPoolIdentityProviderSaml, UserPoolIdentityProviderSamlMetadataType } from '../../lib'; +import { ProviderAttribute, UserPool, UserPoolIdentityProviderSaml, UserPoolIdentityProviderSamlMetadata } from '../../lib'; describe('UserPoolIdentityProvider', () => { describe('saml', () => { @@ -12,8 +12,7 @@ describe('UserPoolIdentityProvider', () => { // WHEN new UserPoolIdentityProviderSaml(stack, 'userpoolidp', { userPool: pool, - metadataType: UserPoolIdentityProviderSamlMetadataType.URL, - metadataContent: 'https://my-metadata-url.com', + metadata: UserPoolIdentityProviderSamlMetadata.url('https://my-metadata-url.com'), }); // THEN @@ -35,8 +34,7 @@ describe('UserPoolIdentityProvider', () => { // WHEN new UserPoolIdentityProviderSaml(stack, 'userpoolidp', { userPool: pool, - metadataType: UserPoolIdentityProviderSamlMetadataType.FILE, - metadataContent: 'my-file-contents', + metadata: UserPoolIdentityProviderSamlMetadata.file('my-file-contents'), }); // THEN @@ -58,8 +56,7 @@ describe('UserPoolIdentityProvider', () => { // WHEN new UserPoolIdentityProviderSaml(stack, 'userpoolidp', { userPool: pool, - metadataType: UserPoolIdentityProviderSamlMetadataType.FILE, - metadataContent: 'my-file-contents', + metadata: UserPoolIdentityProviderSamlMetadata.file('my-file-contents'), idpSignout: true, }); @@ -82,8 +79,7 @@ describe('UserPoolIdentityProvider', () => { // WHEN const provider = new UserPoolIdentityProviderSaml(stack, 'userpoolidp', { userPool: pool, - metadataType: UserPoolIdentityProviderSamlMetadataType.FILE, - metadataContent: 'my-file-contents', + metadata: UserPoolIdentityProviderSamlMetadata.file('my-file-contents'), }); // THEN @@ -98,8 +94,7 @@ describe('UserPoolIdentityProvider', () => { // WHEN new UserPoolIdentityProviderSaml(stack, 'userpoolidp', { userPool: pool, - metadataType: UserPoolIdentityProviderSamlMetadataType.FILE, - metadataContent: 'my-file-contents', + metadata: UserPoolIdentityProviderSamlMetadata.file('my-file-contents'), attributeMapping: { familyName: ProviderAttribute.other('family_name'), givenName: ProviderAttribute.other('given_name'), @@ -130,8 +125,7 @@ describe('UserPoolIdentityProvider', () => { new UserPoolIdentityProviderSaml(stack, 'userpoolidp', { userPool: pool, name: 'my-provider', - metadataType: UserPoolIdentityProviderSamlMetadataType.FILE, - metadataContent: 'my-file-contents', + metadata: UserPoolIdentityProviderSamlMetadata.file('my-file-contents'), }); // THEN @@ -149,8 +143,7 @@ describe('UserPoolIdentityProvider', () => { expect(() => new UserPoolIdentityProviderSaml(stack, 'userpoolidp', { userPool: pool, name: 'xy', - metadataType: UserPoolIdentityProviderSamlMetadataType.FILE, - metadataContent: 'my-file-contents', + metadata: UserPoolIdentityProviderSamlMetadata.file('my-file-contents'), })).toThrow(/Expected provider name to be between 3 and 32 characters/); }); @@ -162,8 +155,7 @@ describe('UserPoolIdentityProvider', () => { // WHEN new UserPoolIdentityProviderSaml(stack, 'xy', { userPool: pool, - metadataType: UserPoolIdentityProviderSamlMetadataType.FILE, - metadataContent: 'my-file-contents', + metadata: UserPoolIdentityProviderSamlMetadata.file('my-file-contents'), }); // THEN @@ -180,8 +172,7 @@ describe('UserPoolIdentityProvider', () => { // WHEN new UserPoolIdentityProviderSaml(stack, `${'saml'.repeat(10)}xyz`, { userPool: pool, - metadataType: UserPoolIdentityProviderSamlMetadataType.FILE, - metadataContent: 'my-file-contents', + metadata: UserPoolIdentityProviderSamlMetadata.file('my-file-contents'), }); // THEN