From 94314cf920cc4f11c5a1690fa571f1ca049c62a7 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Fri, 6 May 2022 15:49:37 +0200 Subject: [PATCH 1/5] feat(cognito): OpenID Connect identity provider Add the `UserPoolIdentityProviderOidc` class to create an OpenID Connect identity provider for user pools. --- packages/@aws-cdk/aws-cognito/README.md | 1 + .../aws-cognito/lib/user-pool-idps/index.ts | 3 +- .../aws-cognito/lib/user-pool-idps/oidc.ts | 130 +++++++++++ packages/@aws-cdk/aws-cognito/package.json | 3 +- .../test/integ.user-pool-idp.oidc.ts | 45 ++++ .../user-pool-idp.oidc.integ.snapshot/cdk.out | 1 + .../integ-user-pool-idp-google.template.json | 121 +++++++++++ .../integ.json | 14 ++ .../manifest.json | 52 +++++ .../tree.json | 202 ++++++++++++++++++ .../test/user-pool-idps/oidc.test.ts | 150 +++++++++++++ 11 files changed, 720 insertions(+), 2 deletions(-) create mode 100644 packages/@aws-cdk/aws-cognito/lib/user-pool-idps/oidc.ts create mode 100644 packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.oidc.ts create mode 100644 packages/@aws-cdk/aws-cognito/test/user-pool-idp.oidc.integ.snapshot/cdk.out create mode 100644 packages/@aws-cdk/aws-cognito/test/user-pool-idp.oidc.integ.snapshot/integ-user-pool-idp-google.template.json create mode 100644 packages/@aws-cdk/aws-cognito/test/user-pool-idp.oidc.integ.snapshot/integ.json create mode 100644 packages/@aws-cdk/aws-cognito/test/user-pool-idp.oidc.integ.snapshot/manifest.json create mode 100644 packages/@aws-cdk/aws-cognito/test/user-pool-idp.oidc.integ.snapshot/tree.json create mode 100644 packages/@aws-cdk/aws-cognito/test/user-pool-idps/oidc.test.ts diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 8e7a0a7988417..5d08a45400227 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -492,6 +492,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..ff7bc74288004 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/oidc.ts @@ -0,0 +1,130 @@ +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 name of the provider + */ + readonly name: string; + + /** + * The client id + */ + readonly clientId: string; + + /** + * The client secret + */ + readonly clientSecret: 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 + + /** + * Issuer URL + */ + readonly issuerUrl: string; + + /** + * 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); + + const scopes = props.scopes ?? ['openid']; + + const resource = new CfnUserPoolIdentityProvider(this, 'Resource', { + userPoolId: props.userPool.userPoolId, + providerName: 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); + } +} diff --git a/packages/@aws-cdk/aws-cognito/package.json b/packages/@aws-cdk/aws-cognito/package.json index 5eaba8463c93a..416056c5140b8 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..522e7b572a04b --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/oidc.test.ts @@ -0,0 +1,150 @@ +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, + name: 'my-provider', + clientId: 'client-id', + clientSecret: 'client-secret', + issuerUrl: 'https://my-issuer-url.com', + }); + + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'my-provider', + 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, + name: 'my-provider', + 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', { + ProviderName: 'my-provider', + 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', + 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, + name: 'my-provider', + clientId: 'client-id', + clientSecret: 'client-secret', + issuerUrl: 'https://my-issuer-url.com', + scopes: ['scope1', 'scope2'], + }); + + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'my-provider', + ProviderType: 'OIDC', + 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, + name: 'my-provider', + 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, + name: 'my-provider', + 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', + }, + }); + }); + }); +}); From 7e96e6f16a84514ea2f9c4530386d3836e63b0d8 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Fri, 27 May 2022 15:10:24 +0200 Subject: [PATCH 2/5] move up --- .../@aws-cdk/aws-cognito/lib/user-pool-idps/oidc.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 index ff7bc74288004..538ceb2ddd467 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/oidc.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/oidc.ts @@ -22,6 +22,11 @@ export interface UserPoolIdentityProviderOidcProps extends UserPoolIdentityProvi */ readonly clientSecret: string; + /** + * Issuer URL + */ + readonly issuerUrl: 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. @@ -46,11 +51,6 @@ export interface UserPoolIdentityProviderOidcProps extends UserPoolIdentityProvi */ readonly attributeRequestMethod?: OidcAttributeRequestMethod - /** - * Issuer URL - */ - readonly issuerUrl: string; - /** * OpenID connect endpoints * From 0b86bc17caad534bf9f23bf1dd0eead4687c53c1 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Fri, 27 May 2022 15:30:50 +0200 Subject: [PATCH 3/5] name not required --- .../aws-cognito/lib/user-pool-idps/oidc.ts | 14 ++++++++------ .../aws-cognito/test/user-pool-idps/oidc.test.ts | 3 +-- 2 files changed, 9 insertions(+), 8 deletions(-) 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 index 538ceb2ddd467..0ac6abdcc16df 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/oidc.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/oidc.ts @@ -7,11 +7,6 @@ import { UserPoolIdentityProviderBase } from './private/user-pool-idp-base'; * Properties to initialize UserPoolIdentityProviderOidc */ export interface UserPoolIdentityProviderOidcProps extends UserPoolIdentityProviderProps { - /** - * The name of the provider - */ - readonly name: string; - /** * The client id */ @@ -27,6 +22,13 @@ export interface UserPoolIdentityProviderOidcProps extends UserPoolIdentityProvi */ readonly issuerUrl: string; + /** + * The name of the provider + * + * @default - the 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. @@ -108,7 +110,7 @@ export class UserPoolIdentityProviderOidc extends UserPoolIdentityProviderBase { const resource = new CfnUserPoolIdentityProvider(this, 'Resource', { userPoolId: props.userPool.userPoolId, - providerName: props.name, + providerName: props.name ?? this.node.id, providerType: 'OIDC', providerDetails: { client_id: props.clientId, 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 index 522e7b572a04b..01e3f483a634e 100644 --- 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 @@ -12,14 +12,13 @@ describe('UserPoolIdentityProvider', () => { // WHEN new UserPoolIdentityProviderOidc(stack, 'userpoolidp', { userPool: pool, - name: 'my-provider', clientId: 'client-id', clientSecret: 'client-secret', issuerUrl: 'https://my-issuer-url.com', }); Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPoolIdentityProvider', { - ProviderName: 'my-provider', + ProviderName: 'userpoolidp', ProviderType: 'OIDC', ProviderDetails: { client_id: 'client-id', From 1a724f7f04159425c6eac19632616ab41bb30517 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Sat, 28 May 2022 09:38:45 +0200 Subject: [PATCH 4/5] validate name and default name --- .../aws-cognito/lib/user-pool-idps/oidc.ts | 18 +++++- .../test/user-pool-idps/oidc.test.ts | 57 ++++++++++++++++--- 2 files changed, 66 insertions(+), 9 deletions(-) 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 index 0ac6abdcc16df..79ec352307f82 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/oidc.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/oidc.ts @@ -1,3 +1,4 @@ +import { Token } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnUserPoolIdentityProvider } from '../cognito.generated'; import { UserPoolIdentityProviderProps } from './base'; @@ -110,7 +111,7 @@ export class UserPoolIdentityProviderOidc extends UserPoolIdentityProviderBase { const resource = new CfnUserPoolIdentityProvider(this, 'Resource', { userPoolId: props.userPool.userPoolId, - providerName: props.name ?? this.node.id, + providerName: getProviderName(this.node.id, props.name), providerType: 'OIDC', providerDetails: { client_id: props.clientId, @@ -130,3 +131,18 @@ export class UserPoolIdentityProviderOidc extends UserPoolIdentityProviderBase { this.providerName = super.getResourceNameAttribute(resource.ref); } } + +function getProviderName(id: string, 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; + } + + if (id.length < 3 || id.length > 32) { + throw new Error(`Provider name defaults to construct's id (${id}) which is not between 3 and 32 characters. Please specify a valid name with \`name\`.`); + } + + return id; +} 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 index 01e3f483a634e..a43d0d1da84af 100644 --- 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 @@ -38,7 +38,6 @@ describe('UserPoolIdentityProvider', () => { // WHEN new UserPoolIdentityProviderOidc(stack, 'userpoolidp', { userPool: pool, - name: 'my-provider', clientId: 'client-id', clientSecret: 'client-secret', issuerUrl: 'https://my-issuer-url.com', @@ -51,8 +50,6 @@ describe('UserPoolIdentityProvider', () => { }); Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPoolIdentityProvider', { - ProviderName: 'my-provider', - ProviderType: 'OIDC', ProviderDetails: { client_id: 'client-id', client_secret: 'client-secret', @@ -75,7 +72,6 @@ describe('UserPoolIdentityProvider', () => { // WHEN new UserPoolIdentityProviderOidc(stack, 'userpoolidp', { userPool: pool, - name: 'my-provider', clientId: 'client-id', clientSecret: 'client-secret', issuerUrl: 'https://my-issuer-url.com', @@ -83,8 +79,6 @@ describe('UserPoolIdentityProvider', () => { }); Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPoolIdentityProvider', { - ProviderName: 'my-provider', - ProviderType: 'OIDC', ProviderDetails: { client_id: 'client-id', client_secret: 'client-secret', @@ -103,7 +97,6 @@ describe('UserPoolIdentityProvider', () => { // WHEN const provider = new UserPoolIdentityProviderOidc(stack, 'userpoolidp', { userPool: pool, - name: 'my-provider', clientId: 'client-id', clientSecret: 'client-secret', issuerUrl: 'https://my-issuer-url.com', @@ -121,7 +114,6 @@ describe('UserPoolIdentityProvider', () => { // WHEN new UserPoolIdentityProviderOidc(stack, 'userpoolidp', { userPool: pool, - name: 'my-provider', clientId: 'client-id', clientSecret: 'client-secret', issuerUrl: 'https://my-issuer-url.com', @@ -145,5 +137,54 @@ describe('UserPoolIdentityProvider', () => { }, }); }); + + 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('throws when default name is invalid', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // THEN + expect(() => new UserPoolIdentityProviderOidc(stack, 'xy', { + userPool: pool, + clientId: 'client-id', + clientSecret: 'client-secret', + issuerUrl: 'https://my-issuer-url.com', + })).toThrow(/Provider name defaults to construct's id \(xy\) which is not between 3 and 32 characters/); + }); }); }); From 10771c156d15d0ab5202058de5ba2275859793c2 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Tue, 31 May 2022 14:36:16 +0200 Subject: [PATCH 5/5] unique id --- .../aws-cognito/lib/user-pool-idps/oidc.ts | 37 ++++++++++++------- .../test/user-pool-idps/oidc.test.ts | 30 +++++++++++++-- 2 files changed, 50 insertions(+), 17 deletions(-) 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 index 79ec352307f82..f23e80adef4de 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/oidc.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/oidc.ts @@ -1,4 +1,4 @@ -import { Token } from '@aws-cdk/core'; +import { Names, Token } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnUserPoolIdentityProvider } from '../cognito.generated'; import { UserPoolIdentityProviderProps } from './base'; @@ -26,7 +26,7 @@ export interface UserPoolIdentityProviderOidcProps extends UserPoolIdentityProvi /** * The name of the provider * - * @default - the ID of the construct + * @default - the unique ID of the construct */ readonly name?: string; @@ -107,11 +107,15 @@ export class UserPoolIdentityProviderOidc extends UserPoolIdentityProviderBase { 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: getProviderName(this.node.id, props.name), + providerName: this.getProviderName(props.name), providerType: 'OIDC', providerDetails: { client_id: props.clientId, @@ -130,19 +134,24 @@ export class UserPoolIdentityProviderOidc extends UserPoolIdentityProviderBase { this.providerName = super.getResourceNameAttribute(resource.ref); } -} -function getProviderName(id: string, 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)`); + 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; } - return name; - } - if (id.length < 3 || id.length > 32) { - throw new Error(`Provider name defaults to construct's id (${id}) which is not between 3 and 32 characters. Please specify a valid name with \`name\`.`); - } + const uniqueId = Names.uniqueId(this); - return id; + 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/test/user-pool-idps/oidc.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/oidc.test.ts index a43d0d1da84af..c28feb073ae13 100644 --- 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 @@ -173,18 +173,42 @@ describe('UserPoolIdentityProvider', () => { })).toThrow(/Expected provider name to be between 3 and 32 characters/); }); - test('throws when default name is invalid', () => { + 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 - expect(() => new UserPoolIdentityProviderOidc(stack, 'xy', { + 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', - })).toThrow(/Provider name defaults to construct's id \(xy\) which is not between 3 and 32 characters/); + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'oidcoidcoidcoidccoidcoidcoidcxyz', + }); }); }); });