From 85e953f43cf89b44f8e6dfeb98c3b7eb15ce478e Mon Sep 17 00:00:00 2001 From: awsluja <110861985+awsluja@users.noreply.github.com> Date: Wed, 14 Feb 2024 02:13:46 -0800 Subject: [PATCH] feat: enable support for multiple oidc providers (#1020) * feat: allow multiple oidc providers * chore: add changeset and update api * fix: bug with IDs for multiple oidc providers * chore: update tests * chore: update tests * chore: update tests --- .changeset/sour-rice-listen.md | 6 + packages/auth-construct/API.md | 2 +- packages/auth-construct/src/construct.test.ts | 287 ++++++++++++------ packages/auth-construct/src/construct.ts | 95 +++--- packages/auth-construct/src/types.ts | 2 +- packages/backend-auth/API.md | 2 +- .../src/translate_auth_props.test.ts | 24 +- .../backend-auth/src/translate_auth_props.ts | 27 +- packages/backend-auth/src/types.ts | 2 +- 9 files changed, 289 insertions(+), 158 deletions(-) create mode 100644 .changeset/sour-rice-listen.md diff --git a/.changeset/sour-rice-listen.md b/.changeset/sour-rice-listen.md new file mode 100644 index 0000000000..f6e67d3b40 --- /dev/null +++ b/.changeset/sour-rice-listen.md @@ -0,0 +1,6 @@ +--- +'@aws-amplify/auth-construct-alpha': minor +'@aws-amplify/backend-auth': minor +--- + +OIDC now supports a list of providers which will be configured for your user pool. diff --git a/packages/auth-construct/API.md b/packages/auth-construct/API.md index 62b4e620e7..2b50d23b42 100644 --- a/packages/auth-construct/API.md +++ b/packages/auth-construct/API.md @@ -59,7 +59,7 @@ export type ExternalProviderOptions = { facebook?: FacebookProviderProps; loginWithAmazon?: AmazonProviderProps; signInWithApple?: AppleProviderProps; - oidc?: OidcProviderProps; + oidc?: OidcProviderProps[]; saml?: SamlProviderProps; scopes?: ('PHONE' | 'EMAIL' | 'OPENID' | 'PROFILE' | 'COGNITO_ADMIN')[]; callbackUrls: string[]; diff --git a/packages/auth-construct/src/construct.test.ts b/packages/auth-construct/src/construct.test.ts index 934f8de6dc..0b96098725 100644 --- a/packages/auth-construct/src/construct.test.ts +++ b/packages/auth-construct/src/construct.test.ts @@ -32,7 +32,11 @@ const facebookClientSecret = 'facebookClientSecret'; const oidcClientId = 'oidcClientId'; const oidcClientSecret = 'oidcClientSecret'; const oidcIssuerUrl = 'https://mysampleoidcissuer.com'; -const oidcProviderName = 'myOidcProvider'; +const oidcProviderName = 'MyOidcProvider'; +const oidcClientId2 = 'oidcClientId2'; +const oidcClientSecret2 = 'oidcClientSecret2'; +const oidcIssuerUrl2 = 'https://mysampleoidcissuer2.com'; +const oidcProviderName2 = 'MyOidcProvider2'; const ExpectedGoogleIDPProperties = { ProviderDetails: { authorize_scopes: 'profile', @@ -82,6 +86,17 @@ const ExpectedOidcIDPProperties = { ProviderName: oidcProviderName, ProviderType: 'OIDC', }; +const ExpectedOidcIDPProperties2 = { + ProviderDetails: { + attributes_request_method: 'GET', + authorize_scopes: 'openid', + client_id: oidcClientId2, + client_secret: oidcClientSecret2, + oidc_issuer: oidcIssuerUrl2, + }, + ProviderName: oidcProviderName2, + ProviderType: 'OIDC', +}; const samlProviderName = 'samlProviderName'; const samlMetadataContent = ''; const ExpectedSAMLIDPProperties = { @@ -1242,26 +1257,28 @@ void describe('Auth construct', () => { loginWith: { email: true, externalProviders: { - oidc: { - clientId: oidcClientId, - clientSecret: oidcClientSecret, - issuerUrl: oidcIssuerUrl, - name: oidcProviderName, - attributeMapping: { - email: { - attributeName: 'email', + oidc: [ + { + clientId: oidcClientId, + clientSecret: oidcClientSecret, + issuerUrl: oidcIssuerUrl, + name: oidcProviderName, + attributeMapping: { + email: { + attributeName: 'email', + }, }, + attributeRequestMethod: attributeRequestMethod, + endpoints: { + authorization: authorizationURL, + jwksUri: jwksURI, + token: tokensURL, + userInfo: userInfoURL, + }, + identifiers: mockIdentifiers, + scopes: mockScopes, }, - attributeRequestMethod: attributeRequestMethod, - endpoints: { - authorization: authorizationURL, - jwksUri: jwksURI, - token: tokensURL, - userInfo: userInfoURL, - }, - identifiers: mockIdentifiers, - scopes: mockScopes, - }, + ], callbackUrls: ['https://redirect.com'], logoutUrls: ['https://logout.com'], }, @@ -1304,7 +1321,7 @@ void describe('Auth construct', () => { ':oidc-provider/cognito-idp.', { Ref: 'AWS::Region' }, '.amazonaws.com/', - { Ref: 'testOidcIDP12B3582F' }, + { Ref: 'testMyOidcProviderOidcIDP837BDEAD' }, ], ], }), @@ -1324,25 +1341,27 @@ void describe('Auth construct', () => { loginWith: { email: true, externalProviders: { - oidc: { - clientId: oidcClientId, - clientSecret: oidcClientSecret, - issuerUrl: oidcIssuerUrl, - name: oidcProviderName, - attributeMapping: { - email: { - attributeName: 'email', + oidc: [ + { + clientId: oidcClientId, + clientSecret: oidcClientSecret, + issuerUrl: oidcIssuerUrl, + name: oidcProviderName, + attributeMapping: { + email: { + attributeName: 'email', + }, }, + endpoints: { + authorization: authorizationURL, + jwksUri: jwksURI, + token: tokensURL, + userInfo: userInfoURL, + }, + identifiers: mockIdentifiers, + scopes: mockScopes, }, - endpoints: { - authorization: authorizationURL, - jwksUri: jwksURI, - token: tokensURL, - userInfo: userInfoURL, - }, - identifiers: mockIdentifiers, - scopes: mockScopes, - }, + ], callbackUrls: ['https://redirect.com'], logoutUrls: ['https://logout.com'], }, @@ -1385,7 +1404,7 @@ void describe('Auth construct', () => { ':oidc-provider/cognito-idp.', { Ref: 'AWS::Region' }, '.amazonaws.com/', - { Ref: 'testOidcIDP12B3582F' }, + { Ref: 'testMyOidcProviderOidcIDP837BDEAD' }, ], ], }), @@ -1399,12 +1418,14 @@ void describe('Auth construct', () => { loginWith: { phone: true, externalProviders: { - oidc: { - clientId: oidcClientId, - clientSecret: oidcClientSecret, - issuerUrl: oidcIssuerUrl, - name: oidcProviderName, - }, + oidc: [ + { + clientId: oidcClientId, + clientSecret: oidcClientSecret, + issuerUrl: oidcIssuerUrl, + name: oidcProviderName, + }, + ], callbackUrls: ['https://redirect.com'], logoutUrls: ['https://logout.com'], }, @@ -1432,7 +1453,81 @@ void describe('Auth construct', () => { ':oidc-provider/cognito-idp.', { Ref: 'AWS::Region' }, '.amazonaws.com/', - { Ref: 'testOidcIDP12B3582F' }, + { Ref: 'testMyOidcProviderOidcIDP837BDEAD' }, + ], + ], + }), + ], + }); + }); + void it('supports multiple oidc providers', () => { + const app = new App(); + const stack = new Stack(app); + new AmplifyAuth(stack, 'test', { + loginWith: { + email: true, + externalProviders: { + oidc: [ + { + clientId: oidcClientId, + clientSecret: oidcClientSecret, + issuerUrl: oidcIssuerUrl, + name: oidcProviderName, + }, + { + clientId: oidcClientId2, + clientSecret: oidcClientSecret2, + issuerUrl: oidcIssuerUrl2, + name: oidcProviderName2, + }, + ], + callbackUrls: ['https://redirect.com'], + logoutUrls: ['https://logout.com'], + }, + }, + }); + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::Cognito::UserPool', { + UsernameAttributes: ['email'], + AutoVerifiedAttributes: ['email'], + }); + template.hasResourceProperties( + 'AWS::Cognito::UserPoolIdentityProvider', + ExpectedOidcIDPProperties + ); + template.hasResourceProperties( + 'AWS::Cognito::UserPoolIdentityProvider', + ExpectedOidcIDPProperties2 + ); + template.hasResourceProperties('AWS::Cognito::IdentityPool', { + OpenIdConnectProviderARNs: [ + Match.objectEquals({ + 'Fn::Join': [ + '', + [ + 'arn:aws:iam:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':oidc-provider/cognito-idp.', + { Ref: 'AWS::Region' }, + '.amazonaws.com/', + { Ref: 'testMyOidcProviderOidcIDP837BDEAD' }, + ], + ], + }), + Match.objectEquals({ + 'Fn::Join': [ + '', + [ + 'arn:aws:iam:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':oidc-provider/cognito-idp.', + { Ref: 'AWS::Region' }, + '.amazonaws.com/', + { Ref: 'testMyOidcProvider2OidcIDP43D7B07B' }, ], ], }), @@ -1718,12 +1813,14 @@ void describe('Auth construct', () => { clientId: amazonClientId, clientSecret: amazonClientSecret, }, - oidc: { - clientId: oidcClientId, - clientSecret: oidcClientSecret, - issuerUrl: oidcIssuerUrl, - name: oidcProviderName, - }, + oidc: [ + { + clientId: oidcClientId, + clientSecret: oidcClientSecret, + issuerUrl: oidcIssuerUrl, + name: oidcProviderName, + }, + ], saml: { name: samlProviderName, metadata: { @@ -1800,12 +1897,14 @@ void describe('Auth construct', () => { clientId: amazonClientId, clientSecret: amazonClientSecret, }, - oidc: { - clientId: oidcClientId, - clientSecret: oidcClientSecret, - issuerUrl: oidcIssuerUrl, - name: oidcProviderName, - }, + oidc: [ + { + clientId: oidcClientId, + clientSecret: oidcClientSecret, + issuerUrl: oidcIssuerUrl, + name: oidcProviderName, + }, + ], callbackUrls: ['https://redirect.com'], logoutUrls: ['https://logout.com'], }, @@ -1879,12 +1978,14 @@ void describe('Auth construct', () => { clientId: amazonClientId, clientSecret: amazonClientSecret, }, - oidc: { - clientId: oidcClientId, - clientSecret: oidcClientSecret, - issuerUrl: oidcIssuerUrl, - name: oidcProviderName, - }, + oidc: [ + { + clientId: oidcClientId, + clientSecret: oidcClientSecret, + issuerUrl: oidcIssuerUrl, + name: oidcProviderName, + }, + ], callbackUrls: ['https://redirect.com'], logoutUrls: ['https://logout.com'], }, @@ -1984,17 +2085,19 @@ void describe('Auth construct', () => { fullname: ProviderAttribute.AMAZON_NAME, }, }, - oidc: { - clientId: oidcClientId, - clientSecret: oidcClientSecret, - issuerUrl: oidcIssuerUrl, - name: oidcProviderName, - attributeMapping: { - fullname: { - attributeName: 'name', + oidc: [ + { + clientId: oidcClientId, + clientSecret: oidcClientSecret, + issuerUrl: oidcIssuerUrl, + name: oidcProviderName, + attributeMapping: { + fullname: { + attributeName: 'name', + }, }, }, - }, + ], callbackUrls: ['https://redirect.com'], logoutUrls: ['https://logout.com'], }, @@ -2115,20 +2218,22 @@ void describe('Auth construct', () => { fullname: ProviderAttribute.AMAZON_NAME, }, }, - oidc: { - clientId: oidcClientId, - clientSecret: oidcClientSecret, - issuerUrl: oidcIssuerUrl, - name: oidcProviderName, - attributeMapping: { - email: { - attributeName: customEmailMapping, - }, - fullname: { - attributeName: 'name', + oidc: [ + { + clientId: oidcClientId, + clientSecret: oidcClientSecret, + issuerUrl: oidcIssuerUrl, + name: oidcProviderName, + attributeMapping: { + email: { + attributeName: customEmailMapping, + }, + fullname: { + attributeName: 'name', + }, }, }, - }, + ], callbackUrls: ['https://redirect.com'], logoutUrls: ['https://logout.com'], }, @@ -2228,12 +2333,14 @@ void describe('Auth construct', () => { clientId: amazonClientId, clientSecret: amazonClientSecret, }, - oidc: { - clientId: oidcClientId, - clientSecret: oidcClientSecret, - issuerUrl: oidcIssuerUrl, - name: oidcProviderName, - }, + oidc: [ + { + clientId: oidcClientId, + clientSecret: oidcClientSecret, + issuerUrl: oidcIssuerUrl, + name: oidcProviderName, + }, + ], saml: { name: samlProviderName, metadata: { diff --git a/packages/auth-construct/src/construct.ts b/packages/auth-construct/src/construct.ts index fa90f2e35a..16ab08e6a0 100644 --- a/packages/auth-construct/src/construct.ts +++ b/packages/auth-construct/src/construct.ts @@ -51,7 +51,7 @@ type IdentityProviderSetupResult = { facebook?: UserPoolIdentityProviderFacebook; amazon?: UserPoolIdentityProviderAmazon; apple?: UserPoolIdentityProviderApple; - oidc?: UserPoolIdentityProviderOidc; + oidc?: UserPoolIdentityProviderOidc[]; saml?: UserPoolIdentityProviderSaml; }; const authProvidersList = { @@ -332,14 +332,18 @@ export class AmplifyAuth // add other providers identityPool.supportedLoginProviders = providerSetupResult.oauthMappings; if (providerSetupResult.oidc) { - identityPool.openIdConnectProviderArns = [ - arnBuilder({ - service: 'iam', - region, - accountId: Stack.of(this).account, - resource: `oidc-provider/cognito-idp.${region}.amazonaws.com/${providerSetupResult.oidc.providerName}`, - }), - ]; + const oidcArns = []; + for (const oidcProvider of providerSetupResult.oidc) { + oidcArns.push( + arnBuilder({ + service: 'iam', + region, + accountId: Stack.of(this).account, + resource: `oidc-provider/cognito-idp.${region}.amazonaws.com/${oidcProvider.providerName}`, + }) + ); + } + identityPool.openIdConnectProviderArns = oidcArns; } if (providerSetupResult.saml) { identityPool.samlProviderArns = [ @@ -695,40 +699,45 @@ export class AmplifyAuth external.signInWithApple.clientId; result.providersList.push('APPLE'); } - if (external.oidc) { - const oidc = external.oidc; - const requestMethod = - oidc.attributeRequestMethod === undefined - ? 'GET' // default if not defined - : oidc.attributeRequestMethod; - result.oidc = new cognito.UserPoolIdentityProviderOidc( - this, - `${this.name}OidcIDP`, - { - userPool, - attributeRequestMethod: - requestMethod === 'GET' - ? OidcAttributeRequestMethod.GET - : OidcAttributeRequestMethod.POST, - clientId: oidc.clientId, - clientSecret: oidc.clientSecret, - endpoints: oidc.endpoints, - identifiers: oidc.identifiers, - issuerUrl: oidc.issuerUrl, - name: oidc.name, - scopes: oidc.scopes, - attributeMapping: { - ...(shouldMapEmailAttributes - ? { - email: { - attributeName: 'email', - }, - } - : undefined), - ...oidc.attributeMapping, - }, - } - ); + if (external.oidc && external.oidc.length > 0) { + const oidcProviders: UserPoolIdentityProviderOidc[] = []; + external.oidc.forEach((provider, index) => { + const requestMethod = + provider.attributeRequestMethod === undefined + ? 'GET' // default if not defined + : provider.attributeRequestMethod; + oidcProviders.push( + new cognito.UserPoolIdentityProviderOidc( + this, + `${this.name}${provider.name ?? index}OidcIDP`, + { + userPool, + attributeRequestMethod: + requestMethod === 'GET' + ? OidcAttributeRequestMethod.GET + : OidcAttributeRequestMethod.POST, + clientId: provider.clientId, + clientSecret: provider.clientSecret, + endpoints: provider.endpoints, + identifiers: provider.identifiers, + issuerUrl: provider.issuerUrl, + name: provider.name, + scopes: provider.scopes, + attributeMapping: { + ...(shouldMapEmailAttributes + ? { + email: { + attributeName: 'email', + }, + } + : undefined), + ...provider.attributeMapping, + }, + } + ) + ); + }); + result.oidc = oidcProviders; result.providersList.push('OIDC'); } if (external.saml) { diff --git a/packages/auth-construct/src/types.ts b/packages/auth-construct/src/types.ts index 107821b67a..d195707b5b 100644 --- a/packages/auth-construct/src/types.ts +++ b/packages/auth-construct/src/types.ts @@ -200,7 +200,7 @@ export type ExternalProviderOptions = { /** * OIDC Settings */ - oidc?: OidcProviderProps; + oidc?: OidcProviderProps[]; /** * SAML Settings */ diff --git a/packages/backend-auth/API.md b/packages/backend-auth/API.md index c88f4c082d..b79920f080 100644 --- a/packages/backend-auth/API.md +++ b/packages/backend-auth/API.md @@ -59,7 +59,7 @@ export type ExternalProviderSpecificFactoryProps = ExternalProviderGeneralFactor signInWithApple?: AppleProviderFactoryProps; loginWithAmazon?: AmazonProviderFactoryProps; facebook?: FacebookProviderFactoryProps; - oidc?: OidcProviderFactoryProps; + oidc?: OidcProviderFactoryProps[]; google?: GoogleProviderFactoryProps; }; diff --git a/packages/backend-auth/src/translate_auth_props.test.ts b/packages/backend-auth/src/translate_auth_props.test.ts index afd97a2886..7a3bf1f633 100644 --- a/packages/backend-auth/src/translate_auth_props.test.ts +++ b/packages/backend-auth/src/translate_auth_props.test.ts @@ -87,11 +87,13 @@ void describe('translateToAuthConstructLoginWith', () => { clientId: new TestBackendSecret(amazonClientId), clientSecret: new TestBackendSecret(amazonClientSecret), }, - oidc: { - clientId: new TestBackendSecret(oidcClientId), - clientSecret: new TestBackendSecret(oidcClientSecret), - issuerUrl: oidcIssueURL, - }, + oidc: [ + { + clientId: new TestBackendSecret(oidcClientId), + clientSecret: new TestBackendSecret(oidcClientSecret), + issuerUrl: oidcIssueURL, + }, + ], signInWithApple: { clientId: new TestBackendSecret(appleClientId), teamId: new TestBackendSecret(appleTeamId), @@ -123,11 +125,13 @@ void describe('translateToAuthConstructLoginWith', () => { clientId: amazonClientId, clientSecret: amazonClientSecret, }, - oidc: { - clientId: oidcClientId, - clientSecret: oidcClientSecret, - issuerUrl: oidcIssueURL, - }, + oidc: [ + { + clientId: oidcClientId, + clientSecret: oidcClientSecret, + issuerUrl: oidcIssueURL, + }, + ], signInWithApple: { clientId: appleClientId, teamId: appleTeamId, diff --git a/packages/backend-auth/src/translate_auth_props.ts b/packages/backend-auth/src/translate_auth_props.ts index 60da0d466b..c6c5592180 100644 --- a/packages/backend-auth/src/translate_auth_props.ts +++ b/packages/backend-auth/src/translate_auth_props.ts @@ -137,20 +137,25 @@ const translateFacebookProps = ( const translateOidcProps = ( backendSecretResolver: BackendSecretResolver, - oidcProviderProps?: OidcProviderFactoryProps -): OidcProviderProps | undefined => { - if (!oidcProviderProps) { + oidcProviderProps?: OidcProviderFactoryProps[] +): OidcProviderProps[] | undefined => { + if (!oidcProviderProps || oidcProviderProps.length === 0) { return undefined; } - const { clientId, clientSecret, ...noSecretProps } = oidcProviderProps; - return { - ...noSecretProps, - clientId: backendSecretResolver.resolveSecret(clientId).unsafeUnwrap(), - clientSecret: backendSecretResolver - .resolveSecret(clientSecret) - .unsafeUnwrap(), - }; + const result = []; + for (const provider of oidcProviderProps) { + const { clientId, clientSecret, ...noSecretProps } = provider; + result.push({ + ...noSecretProps, + clientId: backendSecretResolver.resolveSecret(clientId).unsafeUnwrap(), + clientSecret: backendSecretResolver + .resolveSecret(clientSecret) + .unsafeUnwrap(), + }); + } + + return result; }; const translateGoogleProps = ( diff --git a/packages/backend-auth/src/types.ts b/packages/backend-auth/src/types.ts index d5bb052d07..a42d3657ab 100644 --- a/packages/backend-auth/src/types.ts +++ b/packages/backend-auth/src/types.ts @@ -156,7 +156,7 @@ export type ExternalProviderSpecificFactoryProps = /** * OIDC Settings */ - oidc?: OidcProviderFactoryProps; + oidc?: OidcProviderFactoryProps[]; /** * Google OAuth Settings */