diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index a052d97824a5c..8e2720be896f3 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -503,6 +503,7 @@ The following third-party identity providers are currently supported in the CDK - [Facebook Login](https://developers.facebook.com/docs/facebook-login/) - [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) 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 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 321ee0ecad5d9..fd7ad04af70fe 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 @@ -2,4 +2,5 @@ export * from './base'; export * from './apple'; export * from './amazon'; export * from './facebook'; -export * from './google'; \ No newline at end of file +export * from './google'; +export * from './oidc'; diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/oidc.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/oidc.ts new file mode 100644 index 0000000000000..f23e80adef4de --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/oidc.ts @@ -0,0 +1,157 @@ +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 UserPoolIdentityProviderOidc + */ +export interface UserPoolIdentityProviderOidcProps extends UserPoolIdentityProviderProps { + /** + * The client id + */ + readonly clientId: string; + + /** + * The client secret + */ + readonly clientSecret: string; + + /** + * Issuer URL + */ + readonly issuerUrl: string; + + /** + * The name of the provider + * + * @default - the unique ID of the construct + */ + readonly name?: string; + + /** + * The OAuth 2.0 scopes that you will request from OpenID Connect. Scopes are + * groups of OpenID Connect user attributes to exchange with your app. + * + * @default ['openid'] + */ + readonly scopes?: string[]; + + /** + * Identifiers + * + * Identifiers can be used to redirect users to the correct IdP in multitenant apps. + * + * @default - no identifiers used + */ + readonly identifiers?: string[] + + /** + * The method to use to request attributes + * + * @default OidcAttributeRequestMethod.GET + */ + readonly attributeRequestMethod?: OidcAttributeRequestMethod + + /** + * OpenID connect endpoints + * + * @default - auto discovered with issuer URL + */ + readonly endpoints?: OidcEndpoints; +} + +/** + * OpenID Connect endpoints + */ +export interface OidcEndpoints { + /** + * Authorization endpoint + */ + readonly authorization: string; + + /** + * Token endpoint + */ + readonly token: string; + + /** + * UserInfo endpoint + */ + readonly userInfo: string; + + /** + * Jwks_uri endpoint + */ + readonly jwksUri: string; +} + +/** + * The method to use to request attributes + */ +export enum OidcAttributeRequestMethod { + /** GET */ + GET = 'GET', + /** POST */ + POST = 'POST' +} + +/** + * Represents a identity provider that integrates with OpenID Connect + * @resource AWS::Cognito::UserPoolIdentityProvider + */ +export class UserPoolIdentityProviderOidc extends UserPoolIdentityProviderBase { + public readonly providerName: string; + + constructor(scope: Construct, id: string, props: UserPoolIdentityProviderOidcProps) { + 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)`); + } + + const scopes = props.scopes ?? ['openid']; + + const resource = new CfnUserPoolIdentityProvider(this, 'Resource', { + userPoolId: props.userPool.userPoolId, + providerName: this.getProviderName(props.name), + providerType: 'OIDC', + providerDetails: { + client_id: props.clientId, + client_secret: props.clientSecret, + authorize_scopes: scopes.join(' '), + attributes_request_method: props.attributeRequestMethod ?? OidcAttributeRequestMethod.GET, + oidc_issuer: props.issuerUrl, + authorize_url: props.endpoints?.authorization, + token_url: props.endpoints?.token, + attributes_url: props.endpoints?.userInfo, + jwks_uri: props.endpoints?.jwksUri, + }, + 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}oidc`; + } + + if (uniqueId.length > 32) { + return uniqueId.substring(0, 16) + uniqueId.substring(uniqueId.length - 16); + } + return uniqueId; + } +} diff --git a/packages/@aws-cdk/aws-cognito/package.json b/packages/@aws-cdk/aws-cognito/package.json index 44d19ecf0ce92..506d0cd1935bc 100644 --- a/packages/@aws-cdk/aws-cognito/package.json +++ b/packages/@aws-cdk/aws-cognito/package.json @@ -122,7 +122,8 @@ "props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderFacebookProps", "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.UserPoolIdentityProviderAppleProps", + "props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderOidcProps" ] }, "stability": "stable", diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.oidc.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.oidc.ts new file mode 100644 index 0000000000000..159c7e305d2ff --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.oidc.ts @@ -0,0 +1,45 @@ +import { App, CfnOutput, RemovalPolicy, Stack } from '@aws-cdk/core'; +import { ProviderAttribute, UserPool, UserPoolIdentityProviderOidc } from '../lib'; + +/* + * Stack verification steps + * * Visit the URL provided by stack output 'SignInLink' in a browser, and verify the 'cdk' sign in link shows up. + */ +const app = new App(); +const stack = new Stack(app, 'integ-user-pool-idp-google'); + +const userpool = new UserPool(stack, 'pool', { + removalPolicy: RemovalPolicy.DESTROY, +}); + +new UserPoolIdentityProviderOidc(stack, 'cdk', { + userPool: userpool, + name: 'cdk', + clientId: 'client-id', + clientSecret: 'client-secret', + issuerUrl: 'https://www.issuer-url.com', + endpoints: { + authorization: 'https://www.issuer-url.com/authorize', + token: 'https://www.issuer-url.com/token', + userInfo: 'https://www.issuer-url.com/userinfo', + jwksUri: 'https://www.issuer-url.com/jwks', + }, + scopes: ['openid', 'phone'], + attributeMapping: { + phoneNumber: ProviderAttribute.other('phone_number'), + }, +}); + +const client = userpool.addClient('client'); + +const domain = userpool.addDomain('domain', { + cognitoDomain: { + domainPrefix: 'cdk-test-pool', + }, +}); + +new CfnOutput(stack, 'SignInLink', { + value: domain.signInUrl(client, { + redirectUri: 'https://example.com', + }), +}); diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idp.oidc.integ.snapshot/cdk.out b/packages/@aws-cdk/aws-cognito/test/user-pool-idp.oidc.integ.snapshot/cdk.out new file mode 100644 index 0000000000000..2efc89439fab8 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idp.oidc.integ.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"18.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idp.oidc.integ.snapshot/integ-user-pool-idp-google.template.json b/packages/@aws-cdk/aws-cognito/test/user-pool-idp.oidc.integ.snapshot/integ-user-pool-idp-google.template.json new file mode 100644 index 0000000000000..f5a02bb5abf1f --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idp.oidc.integ.snapshot/integ-user-pool-idp-google.template.json @@ -0,0 +1,121 @@ +{ + "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": "OIDC", + "UserPoolId": { + "Ref": "pool056F3F7E" + }, + "AttributeMapping": { + "phone_number": "phone_number" + }, + "ProviderDetails": { + "client_id": "client-id", + "client_secret": "client-secret", + "authorize_scopes": "openid phone", + "attributes_request_method": "GET", + "oidc_issuer": "https://www.issuer-url.com", + "authorize_url": "https://www.issuer-url.com/authorize", + "token_url": "https://www.issuer-url.com/token", + "attributes_url": "https://www.issuer-url.com/userinfo", + "jwks_uri": "https://www.issuer-url.com/jwks" + } + } + } + }, + "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" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idp.oidc.integ.snapshot/integ.json b/packages/@aws-cdk/aws-cognito/test/user-pool-idp.oidc.integ.snapshot/integ.json new file mode 100644 index 0000000000000..6c7dfa2d2f275 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idp.oidc.integ.snapshot/integ.json @@ -0,0 +1,14 @@ +{ + "version": "18.0.0", + "testCases": { + "integ.user-pool-idp.oidc": { + "stacks": [ + "integ-user-pool-idp-google" + ], + "diffAssets": false, + "stackUpdateWorkflow": true + } + }, + "synthContext": {}, + "enableLookups": false +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idp.oidc.integ.snapshot/manifest.json b/packages/@aws-cdk/aws-cognito/test/user-pool-idp.oidc.integ.snapshot/manifest.json new file mode 100644 index 0000000000000..ff3038ce4b3e3 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idp.oidc.integ.snapshot/manifest.json @@ -0,0 +1,52 @@ +{ + "version": "18.0.0", + "artifacts": { + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + }, + "integ-user-pool-idp-google": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "integ-user-pool-idp-google.template.json", + "validateOnSynth": false + }, + "metadata": { + "/integ-user-pool-idp-google/pool/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "pool056F3F7E" + } + ], + "/integ-user-pool-idp-google/pool/client/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "poolclient2623294C" + } + ], + "/integ-user-pool-idp-google/pool/domain/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "pooldomain430FA744" + } + ], + "/integ-user-pool-idp-google/cdk/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "cdk52888317" + } + ], + "/integ-user-pool-idp-google/SignInLink": [ + { + "type": "aws:cdk:logicalId", + "data": "SignInLink" + } + ] + }, + "displayName": "integ-user-pool-idp-google" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idp.oidc.integ.snapshot/tree.json b/packages/@aws-cdk/aws-cognito/test/user-pool-idp.oidc.integ.snapshot/tree.json new file mode 100644 index 0000000000000..ded3b9a167598 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idp.oidc.integ.snapshot/tree.json @@ -0,0 +1,202 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "@aws-cdk/core.Construct", + "version": "0.0.0" + } + }, + "integ-user-pool-idp-google": { + "id": "integ-user-pool-idp-google", + "path": "integ-user-pool-idp-google", + "children": { + "pool": { + "id": "pool", + "path": "integ-user-pool-idp-google/pool", + "children": { + "Resource": { + "id": "Resource", + "path": "integ-user-pool-idp-google/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-idp-google/pool/client", + "children": { + "Resource": { + "id": "Resource", + "path": "integ-user-pool-idp-google/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-idp-google/pool/domain", + "children": { + "Resource": { + "id": "Resource", + "path": "integ-user-pool-idp-google/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-idp-google/cdk", + "children": { + "Resource": { + "id": "Resource", + "path": "integ-user-pool-idp-google/cdk/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Cognito::UserPoolIdentityProvider", + "aws:cdk:cloudformation:props": { + "providerName": "cdk", + "providerType": "OIDC", + "userPoolId": { + "Ref": "pool056F3F7E" + }, + "attributeMapping": { + "phone_number": "phone_number" + }, + "providerDetails": { + "client_id": "client-id", + "client_secret": "client-secret", + "authorize_scopes": "openid phone", + "attributes_request_method": "GET", + "oidc_issuer": "https://www.issuer-url.com", + "authorize_url": "https://www.issuer-url.com/authorize", + "token_url": "https://www.issuer-url.com/token", + "attributes_url": "https://www.issuer-url.com/userinfo", + "jwks_uri": "https://www.issuer-url.com/jwks" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cognito.CfnUserPoolIdentityProvider", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cognito.UserPoolIdentityProviderOidc", + "version": "0.0.0" + } + }, + "SignInLink": { + "id": "SignInLink", + "path": "integ-user-pool-idp-google/SignInLink", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idps/oidc.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/oidc.test.ts new file mode 100644 index 0000000000000..c28feb073ae13 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/oidc.test.ts @@ -0,0 +1,214 @@ +import { Template } from '@aws-cdk/assertions'; +import { Stack } from '@aws-cdk/core'; +import { ProviderAttribute, UserPool, UserPoolIdentityProviderOidc } from '../../lib'; + +describe('UserPoolIdentityProvider', () => { + describe('oidc', () => { + test('defaults', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderOidc(stack, 'userpoolidp', { + userPool: pool, + clientId: 'client-id', + clientSecret: 'client-secret', + issuerUrl: 'https://my-issuer-url.com', + }); + + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'userpoolidp', + ProviderType: 'OIDC', + ProviderDetails: { + client_id: 'client-id', + client_secret: 'client-secret', + authorize_scopes: 'openid', + attributes_request_method: 'GET', + oidc_issuer: 'https://my-issuer-url.com', + }, + }); + }); + + test('endpoints', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderOidc(stack, 'userpoolidp', { + userPool: pool, + clientId: 'client-id', + clientSecret: 'client-secret', + issuerUrl: 'https://my-issuer-url.com', + endpoints: { + authorization: 'https://my-issuer-url.com/authorize', + token: 'https://my-issuer-url.com/token', + userInfo: 'https://my-issuer-url.com/userinfo', + jwksUri: 'https://my-issuer-url.com/jwks', + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPoolIdentityProvider', { + ProviderDetails: { + client_id: 'client-id', + client_secret: 'client-secret', + authorize_scopes: 'openid', + attributes_request_method: 'GET', + oidc_issuer: 'https://my-issuer-url.com', + authorize_url: 'https://my-issuer-url.com/authorize', + token_url: 'https://my-issuer-url.com/token', + attributes_url: 'https://my-issuer-url.com/userinfo', + jwks_uri: 'https://my-issuer-url.com/jwks', + }, + }); + }); + + test('scopes', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderOidc(stack, 'userpoolidp', { + userPool: pool, + clientId: 'client-id', + clientSecret: 'client-secret', + issuerUrl: 'https://my-issuer-url.com', + scopes: ['scope1', 'scope2'], + }); + + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPoolIdentityProvider', { + ProviderDetails: { + client_id: 'client-id', + client_secret: 'client-secret', + authorize_scopes: 'scope1 scope2', + attributes_request_method: 'GET', + oidc_issuer: 'https://my-issuer-url.com', + }, + }); + }); + + test('registered with user pool', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + const provider = new UserPoolIdentityProviderOidc(stack, 'userpoolidp', { + userPool: pool, + clientId: 'client-id', + clientSecret: 'client-secret', + issuerUrl: 'https://my-issuer-url.com', + }); + + // THEN + expect(pool.identityProviders).toContain(provider); + }); + + test('attribute mapping', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderOidc(stack, 'userpoolidp', { + userPool: pool, + clientId: 'client-id', + clientSecret: 'client-secret', + issuerUrl: 'https://my-issuer-url.com', + 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 UserPoolIdentityProviderOidc(stack, 'userpoolidp', { + userPool: pool, + name: 'my-provider', + clientId: 'client-id', + clientSecret: 'client-secret', + issuerUrl: 'https://my-issuer-url.com', + }); + + // 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 UserPoolIdentityProviderOidc(stack, 'userpoolidp', { + userPool: pool, + name: 'xy', + clientId: 'client-id', + clientSecret: 'client-secret', + issuerUrl: 'https://my-issuer-url.com', + })).toThrow(/Expected provider name to be between 3 and 32 characters/); + }); + + test('generates a valid name when unique id is too short', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderOidc(stack, 'xy', { + userPool: pool, + clientId: 'client-id', + clientSecret: 'client-secret', + issuerUrl: 'https://my-issuer-url.com', + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'xyoidc', + }); + }); + + test('generates a valid name when unique id is too long', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderOidc(stack, `${'oidc'.repeat(10)}xyz`, { + userPool: pool, + clientId: 'client-id', + clientSecret: 'client-secret', + issuerUrl: 'https://my-issuer-url.com', + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'oidcoidcoidcoidccoidcoidcoidcxyz', + }); + }); + }); +});