diff --git a/.eslint-dictionary.json b/.eslint-dictionary.json index cd39c64a897..bd41ffecdab 100644 --- a/.eslint-dictionary.json +++ b/.eslint-dictionary.json @@ -146,4 +146,4 @@ "xcode", "yaml", "yyyymmddhhmmss" -] +] \ No newline at end of file diff --git a/packages/amplify-category-auth/amplify-plugin.json b/packages/amplify-category-auth/amplify-plugin.json index 4c1f7e5a6ec..3e7a1706ed9 100644 --- a/packages/amplify-category-auth/amplify-plugin.json +++ b/packages/amplify-category-auth/amplify-plugin.json @@ -1,6 +1,18 @@ { "name": "auth", "type": "category", - "commands": ["add", "console", "enable", "import", "push", "remove", "update", "help", "override"], - "eventHandlers": [] -} + "commands": [ + "add", + "console", + "enable", + "import", + "push", + "remove", + "update", + "help", + "override" + ], + "eventHandlers": [ + "PrePush" + ] +} \ No newline at end of file diff --git a/packages/amplify-category-auth/resources/auth-custom-resource/hostedUIProviderLambda.js b/packages/amplify-category-auth/resources/auth-custom-resource/hostedUIProviderLambda.js index 8195211da74..362ab4b35d1 100644 --- a/packages/amplify-category-auth/resources/auth-custom-resource/hostedUIProviderLambda.js +++ b/packages/amplify-category-auth/resources/auth-custom-resource/hostedUIProviderLambda.js @@ -1,104 +1,101 @@ const response = require('cfn-response'); const aws = require('aws-sdk'); const identity = new aws.CognitoIdentityServiceProvider(); -exports.handler = (event, context, callback) => { +const ssm = new aws.SSM(); + +exports.handler = async(event, context) => { try { const userPoolId = event.ResourceProperties.userPoolId; + let responseData; + const parameter = await ssm.getParameter({ + Name: process.env['hostedUIProviderCreds'], + WithDecryption: true, + }).promise(); let hostedUIProviderMeta = JSON.parse(event.ResourceProperties.hostedUIProviderMeta); - let hostedUIProviderCreds = JSON.parse(event.ResourceProperties.hostedUIProviderCreds); - if (hostedUIProviderCreds.length === 0) { - response.send(event, context, response.SUCCESS, {}); - } + let hostedUIProviderCreds = JSON.parse(parameter.Parameter.Value); if (event.RequestType == 'Delete') { - response.send(event, context, response.SUCCESS, {}); + await response.send(event, context, response.SUCCESS, {}); } if (event.RequestType == 'Update' || event.RequestType == 'Create') { - let getRequestParams = providerName => { - let providerMetaIndex = hostedUIProviderMeta.findIndex(provider => provider.ProviderName === providerName); - let providerMeta = hostedUIProviderMeta[providerMetaIndex]; - let providerCredsIndex = hostedUIProviderCreds.findIndex(provider => provider.ProviderName === providerName); - let providerCreds = hostedUIProviderCreds[providerCredsIndex]; - let requestParams = { - ProviderName: providerMeta.ProviderName, - UserPoolId: userPoolId, - AttributeMapping: providerMeta.AttributeMapping, - }; - if (providerMeta.ProviderName === 'SignInWithApple') { - if (providerCreds.client_id && providerCreds.team_id && providerCreds.key_id && providerCreds.private_key) { - requestParams.ProviderDetails = { - client_id: providerCreds.client_id, - team_id: providerCreds.team_id, - key_id: providerCreds.key_id, - private_key: providerCreds.private_key, - authorize_scopes: providerMeta.authorize_scopes, - }; - } else { - requestParams = null; - } + const result = await identity.listIdentityProviders({ UserPoolId: userPoolId, MaxResults: 60 }).promise(); + let providerList = result.Providers.map(provider => provider.ProviderName); + let providerListInParameters = hostedUIProviderMeta.map(provider => provider.ProviderName); + for (const providerMetadata of hostedUIProviderMeta){ + if (providerList.includes(providerMetadata.ProviderName)) { + responseData = await updateIdentityProvider(providerMetadata.ProviderName,userPoolId, hostedUIProviderMeta, hostedUIProviderCreds); } else { - if (providerCreds.client_id && providerCreds.client_secret) { - requestParams.ProviderDetails = { - client_id: providerCreds.client_id, - client_secret: providerCreds.client_secret, - authorize_scopes: providerMeta.authorize_scopes, - }; - } else { - requestParams = null; - } - } - return requestParams; - }; - let createIdentityProvider = providerName => { - let requestParams = getRequestParams(providerName); - if (!requestParams) { - return Promise.resolve(); + responseData = await createIdentityProvider(providerMetadata.ProviderName,userPoolId, hostedUIProviderMeta, hostedUIProviderCreds); } - requestParams.ProviderType = requestParams.ProviderName; - return identity.createIdentityProvider(requestParams).promise(); }; - let updateIdentityProvider = providerName => { - let requestParams = getRequestParams(providerName); - if (!requestParams) { - return Promise.resolve(); + for (const provider of providerList){ + if (!providerListInParameters.includes(provider)) { + responseData = await deleteIdentityProvider(provider,userPoolId); } - return identity.updateIdentityProvider(requestParams).promise(); - }; - let deleteIdentityProvider = providerName => { - let params = { ProviderName: providerName, UserPoolId: userPoolId }; - return identity.deleteIdentityProvider(params).promise(); }; - let providerPromises = []; - identity - .listIdentityProviders({ UserPoolId: userPoolId, MaxResults: 60 }) - .promise() - .then(result => { - console.log(result); - let providerList = result.Providers.map(provider => provider.ProviderName); - let providerListInParameters = hostedUIProviderMeta.map(provider => provider.ProviderName); - hostedUIProviderMeta.forEach(providerMetadata => { - if (providerList.indexOf(providerMetadata.ProviderName) > -1) { - providerPromises.push(updateIdentityProvider(providerMetadata.ProviderName)); - } else { - providerPromises.push(createIdentityProvider(providerMetadata.ProviderName)); - } - }); - providerList.forEach(provider => { - if (providerListInParameters.indexOf(provider) < 0) { - providerPromises.push(deleteIdentityProvider(provider)); - } - }); - return Promise.all(providerPromises); - }) - .then(() => { - response.send(event, context, response.SUCCESS, {}); - }) - .catch(err => { - console.log(err.stack); - response.send(event, context, response.FAILED, { err }); - }); + console.log(responseData); + await response.send(event, context, response.SUCCESS, {}); } } catch (err) { console.log(err.stack); - response.send(event, context, response.FAILED, { err }); + await response.send(event, context, response.FAILED, { err }); + } +}; + +const getRequestParams = (providerName,userPoolId, hostedUIProviderMeta, hostedUIProviderCreds) => { + const providerMetaIndex = hostedUIProviderMeta.findIndex(provider => provider.ProviderName === providerName); + const providerMeta = hostedUIProviderMeta[providerMetaIndex]; + const providerCredsIndex = hostedUIProviderCreds.findIndex(provider => provider.ProviderName === providerName); + const providerCreds = hostedUIProviderCreds[providerCredsIndex]; + let requestParams = { + ProviderName: providerMeta.ProviderName, + UserPoolId: userPoolId, + AttributeMapping: providerMeta.AttributeMapping, + }; + if (providerMeta.ProviderName === 'SignInWithApple') { + if (providerCreds.client_id && providerCreds.team_id && providerCreds.key_id && providerCreds.private_key) { + requestParams.ProviderDetails = { + client_id: providerCreds.client_id, + team_id: providerCreds.team_id, + key_id: providerCreds.key_id, + private_key: providerCreds.private_key, + authorize_scopes: providerMeta.authorize_scopes, + }; + } else { + requestParams = null; + } + } else { + if (providerCreds.client_id && providerCreds.client_secret) { + requestParams.ProviderDetails = { + client_id: providerCreds.client_id, + client_secret: providerCreds.client_secret, + authorize_scopes: providerMeta.authorize_scopes, + }; + } else { + requestParams = null; + } + } + return requestParams; +}; + +const deleteIdentityProvider = async (providerName, userPoolId) => { + let params = { ProviderName: providerName, UserPoolId: userPoolId }; + return identity.deleteIdentityProvider(params).promise(); +}; + + +const createIdentityProvider = async (providerName, userPoolId, hostedUIProviderMeta, hostedUIProviderCreds) => { + let requestParams = getRequestParams(providerName, userPoolId, hostedUIProviderMeta, hostedUIProviderCreds); + if (!requestParams) { + return; + } + requestParams.ProviderType = requestParams.ProviderName; + return identity.createIdentityProvider(requestParams).promise(); +}; + +const updateIdentityProvider = async (providerName, userPoolId, hostedUIProviderMeta, hostedUIProviderCreds) => { + let requestParams = getRequestParams(providerName, userPoolId, hostedUIProviderMeta, hostedUIProviderCreds); + if (!requestParams) { + return; } + return identity.updateIdentityProvider(requestParams).promise(); }; diff --git a/packages/amplify-category-auth/src/__tests__/commands/import.headless.test.ts b/packages/amplify-category-auth/src/__tests__/commands/import.headless.test.ts index b034744aae7..d57f86e10cb 100644 --- a/packages/amplify-category-auth/src/__tests__/commands/import.headless.test.ts +++ b/packages/amplify-category-auth/src/__tests__/commands/import.headless.test.ts @@ -1,8 +1,8 @@ -import { executeAmplifyHeadlessCommand } from '../../../src'; import { ImportAuthRequest } from 'amplify-headless-interface'; -import { messages } from '../../provider-utils/awscloudformation/assets/string-maps'; import { printer } from 'amplify-prompts'; import { stateManager } from 'amplify-cli-core'; +import { messages } from '../../provider-utils/awscloudformation/assets/string-maps'; +import { executeAmplifyHeadlessCommand } from '../..'; jest.mock('amplify-prompts', () => ({ printer: { @@ -137,9 +137,9 @@ describe('import auth headless', () => { input: { command: 'import', }, - usageData : { - pushHeadlessFlow : jest.fn() - } + usageData: { + pushHeadlessFlow: jest.fn(), + }, }; }); @@ -189,7 +189,7 @@ describe('import auth headless', () => { await executeAmplifyHeadlessCommand(mockContext, headlessPayloadString); expect(printer.warn).toBeCalledWith( - 'Auth has already been imported to this project and cannot be modified from the CLI. To modify, run "amplify remove auth" to unlink the imported auth resource. Then run "amplify import auth".', + 'Auth has already been imported to this project and cannot be modified from the CLI. To modify, run "amplify remove auth" to un-link the imported auth resource. Then run "amplify import auth".', ); }); @@ -209,11 +209,11 @@ describe('import auth headless', () => { fail('should throw error'); } catch (e) { - expect(e.message).toBe(`The previously configured Cognito User Pool: '' (user-pool-123) cannot be found.`); + expect(e.message).toBe('The previously configured Cognito User Pool: \'\' (user-pool-123) cannot be found.'); } }); - it('should throw web clients not found exception ', async () => { + it('should throw web clients not found exception', async () => { stateManager_mock.getMeta = jest.fn().mockReturnValue({ providers: { awscloudformation: {}, @@ -239,7 +239,7 @@ describe('import auth headless', () => { awscloudformation: {}, }, }); - const INVALID_USER_POOL_ID = USER_POOL_ID + '-invalid'; + const INVALID_USER_POOL_ID = `${USER_POOL_ID}-invalid`; const invalidHeadlessPayload = { ...headlessPayload, userPoolId: INVALID_USER_POOL_ID, diff --git a/packages/amplify-category-auth/src/__tests__/commands/remove.test.ts b/packages/amplify-category-auth/src/__tests__/commands/remove.test.ts index 63ee0520cae..53277834299 100644 --- a/packages/amplify-category-auth/src/__tests__/commands/remove.test.ts +++ b/packages/amplify-category-auth/src/__tests__/commands/remove.test.ts @@ -1,7 +1,7 @@ -import * as remove from '../../commands/auth/remove'; -import { messages } from '../../provider-utils/awscloudformation/assets/string-maps'; import { $TSContext } from 'amplify-cli-core'; import { printer } from 'amplify-prompts'; +import * as remove from '../../commands/auth/remove'; +import { messages } from '../../provider-utils/awscloudformation/assets/string-maps'; jest.mock('amplify-prompts', () => ({ printer: { @@ -9,25 +9,23 @@ jest.mock('amplify-prompts', () => ({ }, })); +jest.mock('../../provider-utils/awscloudformation/auth-secret-manager/sync-oauth-secrets'); + const saveCLIInputPayload_mock = jest.fn(); jest.mock('fs-extra'); -jest.mock('../../provider-utils/awscloudformation/auth-inputs-manager/auth-input-state', () => { - return { - AuthInputState: jest.fn().mockImplementation(() => { - return { - isCLIInputsValid: jest.fn(), - getCLIInputPayload: jest.fn().mockImplementation(() => ({ - cognitoConfig: { - userPoolGroupList: ['admin'], - }, - })), - saveCLIInputPayload: saveCLIInputPayload_mock, - }; - }), - }; -}); +jest.mock('../../provider-utils/awscloudformation/auth-inputs-manager/auth-input-state', () => ({ + AuthInputState: jest.fn().mockImplementation(() => ({ + isCLIInputsValid: jest.fn(), + getCLIInputPayload: jest.fn().mockImplementation(() => ({ + cognitoConfig: { + userPoolGroupList: ['admin'], + }, + })), + saveCLIInputPayload: saveCLIInputPayload_mock, + })), +})); jest.mock('amplify-cli-core', () => ({ AmplifySupportedService: { @@ -111,20 +109,20 @@ const mockContext = { }; const context_stub_typed = mockContext as unknown as $TSContext; -test(`remove method should detect existing categories metadata and display warning with no userPoolGroup`, async () => { +test('remove method should detect existing categories metadata and display warning with no userPoolGroup', async () => { await remove.run(context_stub_typed); expect(printer.info).toBeCalledWith(warningString); expect(context_stub_typed.amplify.removeResource).toBeCalled(); }); -test(`remove method should not display warning when there is no dependency with no userPoolGroup`, async () => { +test('remove method should not display warning when there is no dependency with no userPoolGroup', async () => { jest.clearAllMocks(); await remove.run(context_stub_typed); expect(printer.info).not.toBeCalledWith(warningString); expect(context_stub_typed.amplify.removeResource).toBeCalled(); }); -test(`remove method should still be called even when warning displayed for existing category resource and remmoves userPool group`, async () => { +test('remove method should still be called even when warning displayed for existing category resource and remmoves userPool group', async () => { await remove.run(context_stub_typed); expect(saveCLIInputPayload_mock).toBeCalledWith({ cognitoConfig: { userPoolGroupList: [] } }); }); diff --git a/packages/amplify-category-auth/src/__tests__/provider-utils/awscloudformation/auth-secret-manager/sync-oauth-secrets.test.ts b/packages/amplify-category-auth/src/__tests__/provider-utils/awscloudformation/auth-secret-manager/sync-oauth-secrets.test.ts new file mode 100644 index 00000000000..834154e1b01 --- /dev/null +++ b/packages/amplify-category-auth/src/__tests__/provider-utils/awscloudformation/auth-secret-manager/sync-oauth-secrets.test.ts @@ -0,0 +1,274 @@ +/* eslint-disable max-lines-per-function */ +import { $TSContext, stateManager, pathManager } from 'amplify-cli-core'; +import { mocked } from 'ts-jest'; +import path from 'path'; +import { syncOAuthSecretsToCloud } from '../../../../provider-utils/awscloudformation/auth-secret-manager/sync-oauth-secrets'; +import { OAuthSecretsStateManager } from '../../../../provider-utils/awscloudformation/auth-secret-manager/auth-secret-manager'; +import { getAppId } from '../../../../provider-utils/awscloudformation/utils/get-app-id'; +import { getOAuthObjectFromCognito } from '../../../../provider-utils/awscloudformation/utils/get-oauth-secrets-from-cognito'; + +jest.mock('amplify-cli-core'); +jest.mock('../../../../provider-utils/awscloudformation/auth-secret-manager/auth-secret-manager'); +jest.mock('../../../../provider-utils/awscloudformation/utils/get-app-id'); +jest.mock('../../../../provider-utils/awscloudformation/utils/get-oauth-secrets-from-cognito'); + +const stateManagerMock = mocked(stateManager); +const pathManagerMock = mocked(pathManager); +const getOAuthObjectFromCognitoMock = mocked(getOAuthObjectFromCognito); +const getAppIdMock = mocked(getAppId); +const OAuthSecretsStateManagerMock = mocked(OAuthSecretsStateManager); + +getAppIdMock.mockReturnValue('amplifyAppId'); +getOAuthObjectFromCognitoMock.mockResolvedValue('secretValue'); + +stateManagerMock.getLocalEnvInfo.mockReturnValue({ + envName: 'test', +}); +stateManagerMock.getTeamProviderInfo.mockReturnValue({}); + +stateManagerMock.getResourceParametersJson.mockReturnValue({ + hostedUI: true, + authProvidersUserPool: ['mockProvider'], +}); + +pathManagerMock.getBackendDirPath.mockReturnValue(path.join('test', 'path')); + +const setOAuthSecretsMock = jest.fn(); +const getOAuthSecretsMock = jest.fn(); +const hasOAuthSecretsMock = jest.fn(); +OAuthSecretsStateManagerMock.getInstance.mockResolvedValue(({ + setOAuthSecrets: setOAuthSecretsMock, + getOAuthSecrets: getOAuthSecretsMock, + hasOAuthSecrets: hasOAuthSecretsMock, +} as unknown) as OAuthSecretsStateManager); + +const inputPayload1 = { + cognitoConfig: { + identityPoolName: 'mockIdentityPool', + allowUnauthenticatedIdentities: true, + resourceNameTruncated: 'auth', + userPoolName: 'mockUserPool', + autoVerifiedAttributes: ['email'], + mfaConfiguration: 'ON', + mfaTypes: ['SMS Text Message', 'TOTP'], + smsAuthenticationMessage: 'Your authentication code is {####}', + smsVerificationMessage: 'Your verification code is {####}', + emailVerificationSubject: 'Your verification code', + emailVerificationMessage: 'Your verification code is {####}', + defaultPasswordPolicy: true, + passwordPolicyMinLength: 8, + passwordPolicyCharacters: ['Requires Lowercase', 'Requires Uppercase', 'Requires Numbers', 'Requires Symbols'], + requiredAttributes: [ + 'address', + 'email', + 'family_name', + 'middle_name', + 'gender', + 'locale', + 'given_name', + 'name', + 'nickname', + 'phone_number', + 'preferred_username', + 'picture', + 'profile', + 'updated_at', + 'website', + ], + userpoolClientGenerateSecret: false, + userpoolClientRefreshTokenValidity: 30, + userpoolClientWriteAttributes: ['address', 'email'], + userpoolClientReadAttributes: ['address', 'email'], + userpoolClientLambdaRole: 'extaut87063394_userpoolclient_lambda_role', + userpoolClientSetAttributes: true, + sharedId: '87063394', + resourceName: 'extauth38706339487063394', + authSelections: 'identityPoolAndUserPool', + authRoleArn: { + 'Fn::GetAtt': ['AuthRole', 'Arn'], + }, + unauthRoleArn: { + 'Fn::GetAtt': ['UnauthRole', 'Arn'], + }, + useDefault: 'manual', + thirdPartyAuth: true, + authProviders: ['graph.facebook.com', 'accounts.google.com', 'www.amazon.com', 'appleid.apple.com'], + facebookAppId: 'dfvsdcsdc', + googleClientId: 'svsdvsv', + amazonAppId: 'sdsafggas', + appleAppId: 'gfdbvafergew', + userPoolGroups: true, + adminQueries: false, + triggers: { + CreateAuthChallenge: ['captcha-create-challenge'], + CustomMessage: ['verification-link'], + DefineAuthChallenge: ['captcha-define-challenge'], + PostAuthentication: ['custom'], + PostConfirmation: ['add-to-group'], + PreAuthentication: ['custom'], + PreSignup: ['email-filter-allowlist'], + VerifyAuthChallengeResponse: ['captcha-verify'], + PreTokenGeneration: ['alter-claims'], + }, + hostedUI: true, + hostedUIDomainName: 'extauth387063394-87063394', + newCallbackURLs: ['https://localhost:3000/'], + newLogoutURLs: ['https://localhost:3000/'], + AllowedOAuthFlows: 'code', + AllowedOAuthScopes: ['phone', 'email', 'openid', 'profile', 'aws.cognito.signin.user.admin'], + authProvidersUserPool: ['Facebook', 'Google', 'LoginWithAmazon', 'SignInWithApple'], + selectedParties: + '{"graph.facebook.com":"dfvsdcsdc","accounts.google.com":"svsdvsv","www.amazon.com":"sdsafggas","appleid.apple.com":"gfdbvafergew"}', + hostedUIProviderMeta: + '[{"ProviderName":"Facebook","authorize_scopes":"email,public_profile","AttributeMapping":{"email":"email","username":"id"}},{"ProviderName":"Google","authorize_scopes":"openid email profile","AttributeMapping":{"email":"email","username":"sub"}},{"ProviderName":"LoginWithAmazon","authorize_scopes":"profile profile:user_id","AttributeMapping":{"email":"email","username":"user_id"}},{"ProviderName":"SignInWithApple","authorize_scopes":"email","AttributeMapping":{"email":"email"}}]', + oAuthMetadata: + '{"AllowedOAuthFlows":["code"],"AllowedOAuthScopes":["phone","email","openid","profile","aws.cognito.signin.user.admin"],"CallbackURLs":["https://localhost:3000/"],"LogoutURLs":["https://localhost:3000/"]}', + serviceName: 'Cognito', + verificationBucketName: 'extauth38706339487063394verificationbucket', + usernameCaseSensitive: false, + }, +}; + +const getCLIInputPayloadMock = jest.fn().mockReturnValue(inputPayload1); + +const cliInputFileExistsMock = jest.fn().mockReturnValue('true'); + +jest.mock('../../../../provider-utils/awscloudformation/auth-secret-manager/secret-name', () => ({ + ...(jest.requireActual('../../../../provider-utils/awscloudformation/auth-secret-manager/secret-name') as {}), + getAppId: jest.fn().mockReturnValue('mockAmplifyAppId'), +})); + +jest.mock('../../../../provider-utils/awscloudformation/auth-inputs-manager/auth-input-state.ts', () => ({ + AuthInputState: jest.fn().mockImplementation(() => ({ + getCLIInputPayload: getCLIInputPayloadMock, + cliInputFileExists: cliInputFileExistsMock, + })), +})); + +const getImportedAuthPropertiesMock = jest.fn().mockReturnValue({ imported: false }); + +const contextStub = { + amplify: { + getImportedAuthProperties: getImportedAuthPropertiesMock, + }, + print: { + error: jest.fn(), + }, +}; + +const contextStubTyped = (contextStub as unknown) as $TSContext; +describe('sync oAuth Secrets', () => { + it('set oauth in cloud and tpi file', async () => { + const oauthObjSecret = { + hostedUIProviderCreds: + // eslint-disable-next-line spellcheck/spell-checker + '[{"ProviderName":"Facebook","client_id":"sdcsdc","client_secret":"bfdsvsr"},{"ProviderName":"Google","client_id":"avearver","client_secret":"vcvereger"},{"ProviderName":"LoginWithAmazon","client_id":"vercvdsavcer","client_secret":"revfdsavrtv"},{"ProviderName":"SignInWithApple","client_id":"vfdvergver","team_id":"ervervre","key_id":"vfdavervfer","private_key":"vaveb"}]', + }; + const resourceName = 'mockResource'; + await syncOAuthSecretsToCloud(contextStubTyped, resourceName, oauthObjSecret); + expect(stateManagerMock.setTeamProviderInfo.mock.calls[0][1]).toMatchInlineSnapshot(` + Object { + "test": Object { + "categories": Object { + "auth": Object { + "mockResource": Object { + "oAuthSecretsPathAmplifyAppId": "amplifyAppId", + }, + }, + }, + }, + } + `); + expect(setOAuthSecretsMock.mock.calls[0][0]).toMatchInlineSnapshot( + // eslint-disable-next-line spellcheck/spell-checker + '"[{\\"ProviderName\\":\\"Facebook\\",\\"client_id\\":\\"sdcsdc\\",\\"client_secret\\":\\"bfdsvsr\\"},{\\"ProviderName\\":\\"Google\\",\\"client_id\\":\\"avearver\\",\\"client_secret\\":\\"vcvereger\\"},{\\"ProviderName\\":\\"LoginWithAmazon\\",\\"client_id\\":\\"vercvdsavcer\\",\\"client_secret\\":\\"revfdsavrtv\\"},{\\"ProviderName\\":\\"SignInWithApple\\",\\"client_id\\":\\"vfdvergver\\",\\"team_id\\":\\"ervervre\\",\\"key_id\\":\\"vfdavervfer\\",\\"private_key\\":\\"vaveb\\"}]"', + ); + }); + + it('update secret from parameter store', async () => { + const resourceName = 'mockResource'; + await syncOAuthSecretsToCloud(contextStubTyped, resourceName); + expect(stateManagerMock.setTeamProviderInfo.mock.calls[0][1]).toMatchInlineSnapshot(` + Object { + "test": Object { + "categories": Object { + "auth": Object { + "mockResource": Object { + "oAuthSecretsPathAmplifyAppId": "amplifyAppId", + }, + }, + }, + }, + } + `); + }); + + it('update secret from cognito if not present in parameter store', async () => { + const resourceName = 'mockResource'; + getOAuthSecretsMock.mockReset(); + getOAuthSecretsMock.mockResolvedValue(undefined); + stateManagerMock.getTeamProviderInfo.mockReset(); + stateManagerMock.getTeamProviderInfo.mockReturnValue({ + categories: { + auth: { + mockAuthResource: { + hostedUIProviderCreds: '[]', + }, + }, + }, + }); + await syncOAuthSecretsToCloud(contextStubTyped, resourceName); + expect(stateManagerMock.setTeamProviderInfo.mock.calls[0][1]).toMatchInlineSnapshot(` + Object { + "test": Object { + "categories": Object { + "auth": Object { + "mockResource": Object { + "oAuthSecretsPathAmplifyAppId": "amplifyAppId", + }, + }, + }, + }, + } + `); + }); + + it('removes appId if no userPool providers present', async () => { + const resourceName = 'mockResource'; + getCLIInputPayloadMock.mockReset(); + getCLIInputPayloadMock.mockReturnValue({ + cognitoConfig: { + ...inputPayload1.cognitoConfig, + authProvidersUserPool: [], + hostedUI: false, + }, + }); + stateManagerMock.getTeamProviderInfo.mockReset(); + stateManagerMock.getTeamProviderInfo.mockReturnValue({}); + stateManagerMock.setTeamProviderInfo.mockReset(); + await syncOAuthSecretsToCloud(contextStubTyped, resourceName); + expect(stateManagerMock.setTeamProviderInfo.mock.calls[0][1]).toMatchInlineSnapshot('Object {}'); + }); + + it('returns undefined if the auth is imported', async () => { + const oauthObjSecret = {}; + const resourceName = 'mockResource'; + getImportedAuthPropertiesMock.mockReset(); + getImportedAuthPropertiesMock.mockReturnValue({ imported: true }); + stateManagerMock.setTeamProviderInfo.mockReset(); + await syncOAuthSecretsToCloud(contextStubTyped, resourceName, oauthObjSecret); + expect(stateManagerMock.setTeamProviderInfo).not.toBeCalled(); + }); + + it('returns undefined if auth is not migrated', async () => { + const resourceName = 'mockResource'; + getImportedAuthPropertiesMock.mockReset(); + getImportedAuthPropertiesMock.mockReturnValue({ imported: false }); + cliInputFileExistsMock.mockReset(); + cliInputFileExistsMock.mockReturnValue(false); + stateManagerMock.getTeamProviderInfo.mockReset(); + stateManagerMock.getTeamProviderInfo.mockReturnValue({}); + await syncOAuthSecretsToCloud(contextStubTyped, resourceName); + expect(stateManagerMock.setTeamProviderInfo).not.toBeCalled(); + }); +}); diff --git a/packages/amplify-category-auth/src/__tests__/provider-utils/awscloudformation/auth-stack-builder/__snapshots__/auth-stack-transform.test.ts.snap b/packages/amplify-category-auth/src/__tests__/provider-utils/awscloudformation/auth-stack-builder/__snapshots__/auth-stack-transform.test.ts.snap index f31785e3247..65da8ef6e72 100644 --- a/packages/amplify-category-auth/src/__tests__/provider-utils/awscloudformation/auth-stack-builder/__snapshots__/auth-stack-transform.test.ts.snap +++ b/packages/amplify-category-auth/src/__tests__/provider-utils/awscloudformation/auth-stack-builder/__snapshots__/auth-stack-transform.test.ts.snap @@ -806,6 +806,9 @@ Object { "oAuthMetadata": Object { "Type": "String", }, + "oAuthSecretsPathAmplifyAppId": Object { + "Type": "String", + }, "passwordPolicyCharacters": Object { "Type": "CommaDelimitedList", }, @@ -1199,109 +1202,127 @@ exports.handler = (event, context, callback) => { "ZipFile": "const response = require('cfn-response'); const aws = require('aws-sdk'); const identity = new aws.CognitoIdentityServiceProvider(); -exports.handler = (event, context, callback) => { +const ssm = new aws.SSM(); + +exports.handler = async(event, context) => { try { const userPoolId = event.ResourceProperties.userPoolId; + let responseData; + const parameter = await ssm.getParameter({ + Name: process.env['hostedUIProviderCreds'], + WithDecryption: true, + }).promise(); let hostedUIProviderMeta = JSON.parse(event.ResourceProperties.hostedUIProviderMeta); - let hostedUIProviderCreds = JSON.parse(event.ResourceProperties.hostedUIProviderCreds); - if (hostedUIProviderCreds.length === 0) { - response.send(event, context, response.SUCCESS, {}); - } + let hostedUIProviderCreds = JSON.parse(parameter.Parameter.Value); if (event.RequestType == 'Delete') { - response.send(event, context, response.SUCCESS, {}); + await response.send(event, context, response.SUCCESS, {}); } if (event.RequestType == 'Update' || event.RequestType == 'Create') { - let getRequestParams = providerName => { - let providerMetaIndex = hostedUIProviderMeta.findIndex(provider => provider.ProviderName === providerName); - let providerMeta = hostedUIProviderMeta[providerMetaIndex]; - let providerCredsIndex = hostedUIProviderCreds.findIndex(provider => provider.ProviderName === providerName); - let providerCreds = hostedUIProviderCreds[providerCredsIndex]; - let requestParams = { - ProviderName: providerMeta.ProviderName, - UserPoolId: userPoolId, - AttributeMapping: providerMeta.AttributeMapping, - }; - if (providerMeta.ProviderName === 'SignInWithApple') { - if (providerCreds.client_id && providerCreds.team_id && providerCreds.key_id && providerCreds.private_key) { - requestParams.ProviderDetails = { - client_id: providerCreds.client_id, - team_id: providerCreds.team_id, - key_id: providerCreds.key_id, - private_key: providerCreds.private_key, - authorize_scopes: providerMeta.authorize_scopes, - }; - } else { - requestParams = null; - } + const result = await identity.listIdentityProviders({ UserPoolId: userPoolId, MaxResults: 60 }).promise(); + let providerList = result.Providers.map(provider => provider.ProviderName); + let providerListInParameters = hostedUIProviderMeta.map(provider => provider.ProviderName); + for (const providerMetadata of hostedUIProviderMeta){ + if (providerList.includes(providerMetadata.ProviderName)) { + responseData = await updateIdentityProvider(providerMetadata.ProviderName,userPoolId, hostedUIProviderMeta, hostedUIProviderCreds); } else { - if (providerCreds.client_id && providerCreds.client_secret) { - requestParams.ProviderDetails = { - client_id: providerCreds.client_id, - client_secret: providerCreds.client_secret, - authorize_scopes: providerMeta.authorize_scopes, - }; - } else { - requestParams = null; - } - } - return requestParams; - }; - let createIdentityProvider = providerName => { - let requestParams = getRequestParams(providerName); - if (!requestParams) { - return Promise.resolve(); + responseData = await createIdentityProvider(providerMetadata.ProviderName,userPoolId, hostedUIProviderMeta, hostedUIProviderCreds); } - requestParams.ProviderType = requestParams.ProviderName; - return identity.createIdentityProvider(requestParams).promise(); }; - let updateIdentityProvider = providerName => { - let requestParams = getRequestParams(providerName); - if (!requestParams) { - return Promise.resolve(); + for (const provider of providerList){ + if (!providerListInParameters.includes(provider)) { + responseData = await deleteIdentityProvider(provider,userPoolId); } - return identity.updateIdentityProvider(requestParams).promise(); }; - let deleteIdentityProvider = providerName => { - let params = { ProviderName: providerName, UserPoolId: userPoolId }; - return identity.deleteIdentityProvider(params).promise(); - }; - let providerPromises = []; - identity - .listIdentityProviders({ UserPoolId: userPoolId, MaxResults: 60 }) - .promise() - .then(result => { - console.log(result); - let providerList = result.Providers.map(provider => provider.ProviderName); - let providerListInParameters = hostedUIProviderMeta.map(provider => provider.ProviderName); - hostedUIProviderMeta.forEach(providerMetadata => { - if (providerList.indexOf(providerMetadata.ProviderName) > -1) { - providerPromises.push(updateIdentityProvider(providerMetadata.ProviderName)); - } else { - providerPromises.push(createIdentityProvider(providerMetadata.ProviderName)); - } - }); - providerList.forEach(provider => { - if (providerListInParameters.indexOf(provider) < 0) { - providerPromises.push(deleteIdentityProvider(provider)); - } - }); - return Promise.all(providerPromises); - }) - .then(() => { - response.send(event, context, response.SUCCESS, {}); - }) - .catch(err => { - console.log(err.stack); - response.send(event, context, response.FAILED, { err }); - }); + console.log(responseData); + await response.send(event, context, response.SUCCESS, {}); } } catch (err) { console.log(err.stack); - response.send(event, context, response.FAILED, { err }); + await response.send(event, context, response.FAILED, { err }); + } +}; + +const getRequestParams = (providerName,userPoolId, hostedUIProviderMeta, hostedUIProviderCreds) => { + const providerMetaIndex = hostedUIProviderMeta.findIndex(provider => provider.ProviderName === providerName); + const providerMeta = hostedUIProviderMeta[providerMetaIndex]; + const providerCredsIndex = hostedUIProviderCreds.findIndex(provider => provider.ProviderName === providerName); + const providerCreds = hostedUIProviderCreds[providerCredsIndex]; + let requestParams = { + ProviderName: providerMeta.ProviderName, + UserPoolId: userPoolId, + AttributeMapping: providerMeta.AttributeMapping, + }; + if (providerMeta.ProviderName === 'SignInWithApple') { + if (providerCreds.client_id && providerCreds.team_id && providerCreds.key_id && providerCreds.private_key) { + requestParams.ProviderDetails = { + client_id: providerCreds.client_id, + team_id: providerCreds.team_id, + key_id: providerCreds.key_id, + private_key: providerCreds.private_key, + authorize_scopes: providerMeta.authorize_scopes, + }; + } else { + requestParams = null; + } + } else { + if (providerCreds.client_id && providerCreds.client_secret) { + requestParams.ProviderDetails = { + client_id: providerCreds.client_id, + client_secret: providerCreds.client_secret, + authorize_scopes: providerMeta.authorize_scopes, + }; + } else { + requestParams = null; + } + } + return requestParams; +}; + +const deleteIdentityProvider = async (providerName, userPoolId) => { + let params = { ProviderName: providerName, UserPoolId: userPoolId }; + return identity.deleteIdentityProvider(params).promise(); +}; + + +const createIdentityProvider = async (providerName, userPoolId, hostedUIProviderMeta, hostedUIProviderCreds) => { + let requestParams = getRequestParams(providerName, userPoolId, hostedUIProviderMeta, hostedUIProviderCreds); + if (!requestParams) { + return; + } + requestParams.ProviderType = requestParams.ProviderName; + return identity.createIdentityProvider(requestParams).promise(); +}; + +const updateIdentityProvider = async (providerName, userPoolId, hostedUIProviderMeta, hostedUIProviderCreds) => { + let requestParams = getRequestParams(providerName, userPoolId, hostedUIProviderMeta, hostedUIProviderCreds); + if (!requestParams) { + return; } + return identity.updateIdentityProvider(requestParams).promise(); }; ", }, + "Environment": Object { + "Variables": Object { + "hostedUIProviderCreds": Object { + "Fn::Sub": Array [ + "/amplify/\${appId}/\${env}/AMPLIFY_\${resourceName}_\${oauthObjSecretKey}", + Object { + "appId": Object { + "Ref": "oAuthSecretsPathAmplifyAppId", + }, + "env": Object { + "Ref": "env", + }, + "oauthObjSecretKey": "hostedUIProviderCreds", + "resourceName": Object { + "Ref": "resourceName", + }, + }, + ], + }, + }, + }, "Handler": "index.handler", "Role": Object { "Fn::GetAtt": Array [ @@ -1317,7 +1338,7 @@ exports.handler = (event, context, callback) => { "HostedUIProvidersCustomResourceInputs": Object { "DeletionPolicy": "Delete", "DependsOn": Array [ - "HostedUIProvidersCustomResourceLogPolicy", + "hostedUIProvidersCustomResourceSecretPolicy", ], "Properties": Object { "ServiceToken": Object { @@ -1326,9 +1347,6 @@ exports.handler = (event, context, callback) => { "Arn", ], }, - "hostedUIProviderCreds": Object { - "Ref": "hostedUIProviderCreds", - }, "hostedUIProviderMeta": Object { "Ref": "hostedUIProviderMeta", }, @@ -2205,6 +2223,58 @@ exports.handler = (event, context, callback) => { }, "Type": "AWS::Cognito::UserPoolClient", }, + "hostedUIProvidersCustomResourceSecretPolicy": Object { + "DependsOn": Array [ + "HostedUIProvidersCustomResourcePolicy", + ], + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "ssm:GetParameter", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Sub": Array [ + "arn:aws:ssm:\${AWS::Region}:\${AWS::AccountId}:parameter/amplify/\${appId}/\${env}/AMPLIFY_\${resourceName}_\${oauthObjSecretKey}", + Object { + "appId": Object { + "Ref": "oAuthSecretsPathAmplifyAppId", + }, + "env": Object { + "Ref": "env", + }, + "oauthObjSecretKey": "hostedUIProviderCreds", + "resourceName": Object { + "Ref": "resourceName", + }, + }, + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": Object { + "Fn::Join": Array [ + "-", + Array [ + Object { + "Ref": "UserPool", + }, + "hostedUIProvidersCustomResourceSecretPolicy", + ], + ], + }, + "Roles": Array [ + Object { + "Ref": "UserPoolClientRole", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, }, } `; diff --git a/packages/amplify-category-auth/src/__tests__/provider-utils/awscloudformation/auth-stack-builder/auth-cognito-stack-builder.test.ts b/packages/amplify-category-auth/src/__tests__/provider-utils/awscloudformation/auth-stack-builder/auth-cognito-stack-builder.test.ts index c89ce92a1b5..ef6c03d91f6 100644 --- a/packages/amplify-category-auth/src/__tests__/provider-utils/awscloudformation/auth-stack-builder/auth-cognito-stack-builder.test.ts +++ b/packages/amplify-category-auth/src/__tests__/provider-utils/awscloudformation/auth-stack-builder/auth-cognito-stack-builder.test.ts @@ -1,9 +1,23 @@ -import { AmplifyAuthCognitoStack } from '../../../../provider-utils/awscloudformation/auth-stack-builder/auth-cognito-stack-builder'; -import { AuthStackSynthesizer } from '../../../../provider-utils/awscloudformation/auth-stack-builder/stack-synthesizer'; import * as cdk from '@aws-cdk/core'; import * as iam from '@aws-cdk/aws-iam'; +import { AmplifyAuthCognitoStack } from '../../../../provider-utils/awscloudformation/auth-stack-builder/auth-cognito-stack-builder'; +import { AuthStackSynthesizer } from '../../../../provider-utils/awscloudformation/auth-stack-builder/stack-synthesizer'; import { CognitoStackOptions } from '../../../../provider-utils/awscloudformation/service-walkthrough-types/cognito-user-input-types'; +jest.mock('amplify-cli-core', () => ({ + ...(jest.requireActual('amplify-cli-core') as {}), + stateManager: { + getLocalEnvInfo: jest.fn().mockReturnValue({ envName: 'mockEnv' }), + getMeta: jest.fn().mockReturnValue({ + providers: { + awscloudformation: { + AmplifyAppId: 'mockAmplifyAppId', + }, + }, + }), + }, +})); + describe('generateCognitoStackResources', () => { it('adds correct custom oauth lambda dependencies', () => { const testApp = new cdk.App(); @@ -31,83 +45,83 @@ describe('generateCognitoStackResources', () => { const testApp = new cdk.App(); const cognitoStack = new AmplifyAuthCognitoStack(testApp, 'CognitoPreSignUpTriggerTest', { synthesizer: new AuthStackSynthesizer() }); const props : CognitoStackOptions = { - "identityPoolName": "issue96802f106de3_identitypool_2f106de3", - "allowUnauthenticatedIdentities": false, - "resourceNameTruncated": "issue92f106de3", - "userPoolName": "issue96802f106de3_userpool_2f106de3", - "autoVerifiedAttributes": [ - "email" + identityPoolName: 'issue96802f106de3_identitypool_2f106de3', + allowUnauthenticatedIdentities: false, + resourceNameTruncated: 'issue92f106de3', + userPoolName: 'issue96802f106de3_userpool_2f106de3', + autoVerifiedAttributes: [ + 'email', + ], + mfaConfiguration: 'OFF', + mfaTypes: [ + 'SMS Text Message', + ], + smsAuthenticationMessage: 'Your authentication code is {####}', + smsVerificationMessage: 'Your verification code is {####}', + emailVerificationSubject: 'Your verification code', + emailVerificationMessage: 'Your verification code is {####}', + passwordPolicyMinLength: 8, + passwordPolicyCharacters: [], + requiredAttributes: [ + 'email', + ], + aliasAttributes: [], + userpoolClientGenerateSecret: false, + userpoolClientRefreshTokenValidity: 30, + userpoolClientWriteAttributes: [ + 'email', + ], + userpoolClientReadAttributes: [ + 'email', + ], + userpoolClientLambdaRole: 'issue92f106de3_userpoolclient_lambda_role', + userpoolClientSetAttributes: false, + sharedId: '2f106de3', + resourceName: 'issue96802f106de32f106de3', + authSelections: 'identityPoolAndUserPool', + useDefault: 'manual', + thirdPartyAuth: false, + userPoolGroups: false, + adminQueries: false, + triggers: { + PreSignup: [ + 'custom', ], - "mfaConfiguration": "OFF", - "mfaTypes": [ - "SMS Text Message" + }, + hostedUI: false, + userPoolGroupList: [], + serviceName: 'Cognito', + usernameCaseSensitive: false, + useEnabledMfas: true, + authRoleArn: { + 'Fn::GetAtt': [ + 'AuthRole', + 'Arn', ], - "smsAuthenticationMessage": "Your authentication code is {####}", - "smsVerificationMessage": "Your verification code is {####}", - "emailVerificationSubject": "Your verification code", - "emailVerificationMessage": "Your verification code is {####}", - "passwordPolicyMinLength": 8, - "passwordPolicyCharacters": [], - "requiredAttributes": [ - "email" + }, + unauthRoleArn: { + 'Fn::GetAtt': [ + 'UnauthRole', + 'Arn', ], - "aliasAttributes": [], - "userpoolClientGenerateSecret": false, - "userpoolClientRefreshTokenValidity": 30, - "userpoolClientWriteAttributes": [ - "email" - ], - "userpoolClientReadAttributes": [ - "email" - ], - "userpoolClientLambdaRole": "issue92f106de3_userpoolclient_lambda_role", - "userpoolClientSetAttributes": false, - "sharedId": "2f106de3", - "resourceName": "issue96802f106de32f106de3", - "authSelections": "identityPoolAndUserPool", - "useDefault": "manual", - "thirdPartyAuth": false, - "userPoolGroups": false, - "adminQueries": false, - "triggers": { - "PreSignup": [ - "custom" - ] - }, - "hostedUI": false, - "userPoolGroupList": [], - "serviceName": "Cognito", - "usernameCaseSensitive": false, - "useEnabledMfas": true, - "authRoleArn": { - "Fn::GetAtt": [ - "AuthRole", - "Arn" - ] + }, + breakCircularDependency: false, + dependsOn: [ + { + category: 'function', + resourceName: 'issue96802f106de32f106de3PreSignup', + attributes: [ + 'Arn', + 'Name', + ], }, - "unauthRoleArn": { - "Fn::GetAtt": [ - "UnauthRole", - "Arn" - ] - }, - "breakCircularDependency": false, - "dependsOn": [ - { - "category": "function", - "resourceName": "issue96802f106de32f106de3PreSignup", - "attributes": [ - "Arn", - "Name" - ] - } - ], - "permissions": [], - "authTriggerConnections": [ - {triggerType: "PreSignUp",lambdaFunctionName: "issue96802f106de32f106de3PreSignup"} - ], - "authProviders": [], - } + ], + permissions: [], + authTriggerConnections: [ + { triggerType: 'PreSignUp', lambdaFunctionName: 'issue96802f106de32f106de3PreSignup' }, + ], + authProviders: [], + }; cognitoStack.generateCognitoStackResources(props); expect(cognitoStack.userPool!.lambdaConfig).toHaveProperty('preSignUp'); expect(cognitoStack.lambdaConfigPermissions).toHaveProperty('UserPoolPreSignupLambdaInvokePermission'); diff --git a/packages/amplify-category-auth/src/__tests__/provider-utils/awscloudformation/auth-stack-builder/auth-stack-transform.test.ts b/packages/amplify-category-auth/src/__tests__/provider-utils/awscloudformation/auth-stack-builder/auth-stack-transform.test.ts index 1fa38d772f8..73ddc9a3524 100644 --- a/packages/amplify-category-auth/src/__tests__/provider-utils/awscloudformation/auth-stack-builder/auth-stack-transform.test.ts +++ b/packages/amplify-category-auth/src/__tests__/provider-utils/awscloudformation/auth-stack-builder/auth-stack-transform.test.ts @@ -165,6 +165,10 @@ const getCLIInputPayload_mock = jest.fn().mockReturnValueOnce(inputPayload1).moc const isCLIInputsValid_mock = jest.fn().mockReturnValue('true'); +jest.mock('../../../../provider-utils/awscloudformation/utils/get-app-id', () => ({ + getAppId: jest.fn().mockReturnValue('mockAmplifyAppId'), +})); + jest.mock('../../../../provider-utils/awscloudformation/auth-inputs-manager/auth-input-state.ts', () => { return { AuthInputState: jest.fn().mockImplementation(() => { @@ -231,7 +235,7 @@ describe('Check Auth Template', () => { const resourceName = 'mockResource'; const authTransform = new AmplifyAuthTransform(resourceName); const mock_template = await authTransform.transform(context_stub_typed); - expect(mock_template.Resources?.UserPool.Properties.UsernameConfiguration).toEqual({ CaseSensitive: false }); + expect(mock_template?.Resources?.UserPool.Properties.UsernameConfiguration).toEqual({ CaseSensitive: false }); getCLIInputPayload_mock.mockReturnValue({ cognitoConfig: { @@ -242,7 +246,7 @@ describe('Check Auth Template', () => { const authTransform2 = new AmplifyAuthTransform(resourceName); const mock_template2 = await authTransform2.transform(context_stub_typed); - expect(mock_template2.Resources?.UserPool.Properties).not.toContain('UsernameConfiguration'); + expect(mock_template2?.Resources?.UserPool.Properties).not.toContain('UsernameConfiguration'); }); it('should validate cfn parameters if no original', () => { diff --git a/packages/amplify-category-auth/src/commands/auth/remove.ts b/packages/amplify-category-auth/src/commands/auth/remove.ts index e5839e2f51e..e8ba7431486 100644 --- a/packages/amplify-category-auth/src/commands/auth/remove.ts +++ b/packages/amplify-category-auth/src/commands/auth/remove.ts @@ -1,25 +1,32 @@ -export const name = 'remove'; -const category = 'auth'; import { $TSContext, AmplifySupportedService, stateManager } from 'amplify-cli-core'; import { printer } from 'amplify-prompts'; import { messages } from '../../provider-utils/awscloudformation/assets/string-maps'; import { AuthInputState } from '../../provider-utils/awscloudformation/auth-inputs-manager/auth-input-state'; +import { removeOAuthSecretFromCloud } from '../../provider-utils/awscloudformation/auth-secret-manager/sync-oauth-secrets'; + +export const name = 'remove'; +const category = 'auth'; -export const run = async (context: $TSContext) => { +/** + * entry point to remove auth resource + */ +export const run = async (context: $TSContext): Promise => { const { amplify, parameters } = context; const resourceName = parameters.first; const meta = stateManager.getMeta(); - const dependentResources = Object.keys(meta).some(e => { - return ['analytics', 'api', 'storage', 'function'].includes(e) && Object.keys(meta[e]).length > 0; - }); + const dependentResources = Object.keys(meta).some(e => ['analytics', 'api', 'storage', 'function'].includes(e) && Object.keys(meta[e]).length > 0); if (dependentResources) { printer.info(messages.dependenciesExists); } - const authResourceName = Object.keys(meta.auth).filter(resourceKey => { - return meta.auth[resourceKey].service === AmplifySupportedService.COGNITO; - }); + const authResourceName = Object.keys(meta.auth).filter(resourceKey => meta.auth[resourceKey].service === AmplifySupportedService.COGNITO); + const authResource = Object.keys(meta.auth); try { + // remove oAuth secret from Parameter if only cognito reosurce is present + // if there is a cognito userPoolGroup resource, then it will be deleted in first iteration + if (authResource.length === 1) { + await removeOAuthSecretFromCloud(context, authResourceName[0]); + } const resource = await amplify.removeResource(context, category, resourceName); if (resource?.service === AmplifySupportedService.COGNITOUSERPOOLGROUPS) { // update cli input here diff --git a/packages/amplify-category-auth/src/commands/auth/update.ts b/packages/amplify-category-auth/src/commands/auth/update.ts index 4571192d9fc..292cf56ac5c 100644 --- a/packages/amplify-category-auth/src/commands/auth/update.ts +++ b/packages/amplify-category-auth/src/commands/auth/update.ts @@ -19,6 +19,9 @@ import { getAuthResourceName } from '../../utils/getAuthResourceName'; export const name = 'update'; export const alias = ['update']; +/** + * entry point to update auth resource + */ export const run = async (context: $TSContext) => { const { amplify } = context; const servicesMetadata = getSupportedServices(); @@ -26,32 +29,29 @@ export const run = async (context: $TSContext) => { const existingAuth = meta.auth ?? {}; if (_.isEmpty(existingAuth)) { return printer.warn('Project does not contain auth resources. Add auth using `amplify add auth`.'); - } else { - const authResources = Object.keys(existingAuth); - for (const authResourceName of authResources) { - const serviceMeta = existingAuth[authResourceName]; - if (serviceMeta.service === AmplifySupportedService.COGNITO && serviceMeta.mobileHubMigrated === true) { - printer.error('Auth is migrated from Mobile Hub and cannot be updated with Amplify CLI.'); - return context; - } else if (serviceMeta.service === AmplifySupportedService.COGNITO && serviceMeta.serviceType === 'imported') { - printer.error('Updating imported Auth resource is not supported.'); - return context; - } else if (serviceMeta.service === AmplifySupportedService.COGNITO && !FeatureFlags.getBoolean('auth.forceAliasAttributes')) { - const authAttributes = stateManager.getResourceParametersJson(undefined, AmplifyCategories.AUTH, authResourceName); - if (authAttributes.aliasAttributes && authAttributes.aliasAttributes.length > 0) { - const authUpdateWarning = await BannerMessage.getMessage('AMPLIFY_UPDATE_AUTH_ALIAS_ATTRIBUTES_WARNING'); - if (authUpdateWarning) { - printer.warn(authUpdateWarning); - } + } + const authResources = Object.keys(existingAuth); + for (const authResourceName of authResources) { + const serviceMeta = existingAuth[authResourceName]; + if (serviceMeta.service === AmplifySupportedService.COGNITO && serviceMeta.mobileHubMigrated === true) { + printer.error('Auth is migrated from Mobile Hub and cannot be updated with Amplify CLI.'); + return context; + } if (serviceMeta.service === AmplifySupportedService.COGNITO && serviceMeta.serviceType === 'imported') { + printer.error('Updating imported Auth resource is not supported.'); + return context; + } if (serviceMeta.service === AmplifySupportedService.COGNITO && !FeatureFlags.getBoolean('auth.forceAliasAttributes')) { + const authAttributes = stateManager.getResourceParametersJson(undefined, AmplifyCategories.AUTH, authResourceName); + if (authAttributes.aliasAttributes && authAttributes.aliasAttributes.length > 0) { + const authUpdateWarning = await BannerMessage.getMessage('AMPLIFY_UPDATE_AUTH_ALIAS_ATTRIBUTES_WARNING'); + if (authUpdateWarning) { + printer.warn(authUpdateWarning); } } } } printer.info('Please note that certain attributes may not be overwritten if you choose to use defaults settings.'); - const dependentResources = Object.keys(meta).some(e => { - return ['analytics', 'api', 'storage', 'function'].includes(e) && Object.keys(meta[e]).length > 0; - }); + const dependentResources = Object.keys(meta).some(e => ['analytics', 'api', 'storage', 'function'].includes(e) && Object.keys(meta[e]).length > 0); if (dependentResources) { printer.info(messages.dependenciesExists); } diff --git a/packages/amplify-category-auth/src/events/prePushHandler.ts b/packages/amplify-category-auth/src/events/prePushHandler.ts new file mode 100644 index 00000000000..097bc3e5712 --- /dev/null +++ b/packages/amplify-category-auth/src/events/prePushHandler.ts @@ -0,0 +1,11 @@ +import { $TSContext } from 'amplify-cli-core'; +import { syncOAuthSecretsToCloud } from '../provider-utils/awscloudformation/auth-secret-manager/sync-oauth-secrets'; +import { getAuthResourceName } from '../utils/getAuthResourceName'; + +/** + * pre deploy handler for auth + */ +export const prePushHandler = async (context: $TSContext): Promise => { + const authResourceName = await getAuthResourceName(context); + await syncOAuthSecretsToCloud(context, authResourceName); +}; diff --git a/packages/amplify-category-auth/src/index.js b/packages/amplify-category-auth/src/index.js index 675ca960ace..1a16f071fa0 100644 --- a/packages/amplify-category-auth/src/index.js +++ b/packages/amplify-category-auth/src/index.js @@ -1,9 +1,26 @@ +/* eslint-disable no-case-declarations */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable max-len */ +/* eslint-disable func-style */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable no-param-reassign */ +/* eslint-disable max-lines-per-function */ +/* eslint-disable global-require */ +/* eslint-disable prefer-arrow/prefer-arrow-functions */ +/* eslint-disable consistent-return */ +/* eslint-disable import/no-dynamic-require */ +/* eslint-disable spellcheck/spell-checker */ +/* eslint-disable @typescript-eslint/no-var-requires */ const category = 'auth'; -const _ = require('lodash'); const path = require('path'); const sequential = require('promise-sequential'); +const { validateAddAuthRequest, validateUpdateAuthRequest, validateImportAuthRequest } = require('amplify-util-headless-input'); +const { + stateManager, AmplifySupportedService, JSONUtilities, +} = require('amplify-cli-core'); +const { printer } = require('amplify-prompts'); const defaults = require('./provider-utils/awscloudformation/assets/cognito-defaults'); const { getAuthResourceName } = require('./utils/getAuthResourceName'); const { updateConfigOnEnvInit, migrate } = require('./provider-utils/awscloudformation'); @@ -12,12 +29,10 @@ const { ENV_SPECIFIC_PARAMS } = require('./provider-utils/awscloudformation/cons const { transformUserPoolGroupSchema } = require('./provider-utils/awscloudformation/utils/transform-user-pool-group'); const { uploadFiles } = require('./provider-utils/awscloudformation/utils/trigger-file-uploader'); -const { validateAddAuthRequest, validateUpdateAuthRequest, validateImportAuthRequest } = require('amplify-util-headless-input'); const { getAddAuthRequestAdaptor, getUpdateAuthRequestAdaptor } = require('./provider-utils/awscloudformation/utils/auth-request-adaptors'); const { getAddAuthHandler, getUpdateAuthHandler } = require('./provider-utils/awscloudformation/handlers/resource-handlers'); const { projectHasAuth } = require('./provider-utils/awscloudformation/utils/project-has-auth'); const { attachPrevParamsToContext } = require('./provider-utils/awscloudformation/utils/attach-prev-params-to-context'); -const { stateManager, AmplifySupportedService, JSONUtilities } = require('amplify-cli-core'); const { headlessImport } = require('./provider-utils/awscloudformation/import'); const { getFrontendConfig } = require('./provider-utils/awscloudformation/utils/amplify-meta-updaters'); const { AuthParameters } = require('./provider-utils/awscloudformation/import/types'); @@ -30,11 +45,16 @@ const { loadImportedAuthParameters, } = require('./provider-utils/awscloudformation/utils/auth-sms-workflow-helper'); const { AuthInputState } = require('./provider-utils/awscloudformation/auth-inputs-manager/auth-input-state'); -const { printer } = require('amplify-prompts'); const { privateKeys } = require('./provider-utils/awscloudformation/constants'); const { checkAuthResourceMigration } = require('./provider-utils/awscloudformation/utils/check-for-auth-migration'); +const { prePushHandler } = require('./events/prePushHandler'); +const { syncOAuthSecretsToCloud } = require('./provider-utils/awscloudformation/auth-secret-manager/sync-oauth-secrets'); -// this function is being kept for temporary compatability. +/** + * entry point for add auth command + * this function is being kept for temporary compatibility. + + */ async function add(context, skipNextSteps = false) { const { amplify } = context; const servicesMetadata = getSupportedServices(); @@ -65,6 +85,9 @@ async function add(context, skipNextSteps = false) { }); } +/** + * entry point to build transform auth stack + */ async function transformCategoryStack(context, resource) { if (resource.service === AmplifySupportedService.COGNITO) { if (canResourceBeTransformed(resource.resourceName)) { @@ -78,10 +101,16 @@ function canResourceBeTransformed(resourceName) { return resourceInputState.cliInputFileExists(); } +/** + * check and migrates auth resource to support overrides + */ async function migrateAuthResource(context, resourceName) { return checkAuthResourceMigration(context, resourceName, true); } +/** + * enables auth for categories implicitly + */ async function externalAuthEnable(context, externalCategory, resourceName, requirements) { const { amplify } = context; const serviceMetadata = getSupportedServices(); @@ -149,14 +178,14 @@ async function externalAuthEnable(context, externalCategory, resourceName, requi }; try { - authProps = await removeDeprecatedProps(authProps); + authProps = removeDeprecatedProps(authProps); // replace secret keys from cli inputs to be stored in deployment secrets - let sharedParams = Object.assign({}, authProps); + let sharedParams = { ...authProps }; privateKeys.forEach(p => delete sharedParams[p]); sharedParams = removeDeprecatedProps(sharedParams); // extracting env-specific params from parameters object - let envSpecificParams = {}; + const envSpecificParams = {}; const cliInputs = { ...sharedParams }; ENV_SPECIFIC_PARAMS.forEach(paramName => { if (paramName in authProps) { @@ -192,7 +221,7 @@ async function externalAuthEnable(context, externalCategory, resourceName, requi // Update Identity Pool dependency attributes on userpool groups const allResources = context.amplify.getProjectMeta(); if (allResources.auth && allResources.auth.userPoolGroups) { - let attributes = ['UserPoolId', 'AppClientIDWeb', 'AppClientID']; + const attributes = ['UserPoolId', 'AppClientIDWeb', 'AppClientID']; if (authParameters.identityPoolName) { attributes.push('IdentityPoolId'); } @@ -218,13 +247,14 @@ async function externalAuthEnable(context, externalCategory, resourceName, requi } } -async function checkRequirements(requirements, context, category, targetResourceName) { - // We only require the checking of two properties: - // - authSelections - // - allowUnauthenticatedIdentities - +/** + * We only require the checking of two properties: + * - authSelections + * - allowUnauthenticatedIdentities + */ +async function checkRequirements(requirements, context) { if (!requirements || !requirements.authSelections) { - const error = `Your plugin has not properly defined it's Cognito requirements.`; + const error = 'Your plugin has not properly defined it\'s Cognito requirements.'; return { errors: [error], }; @@ -267,11 +297,11 @@ async function checkRequirements(requirements, context, category, targetResource // Checks handcoded until refactoring of the requirements system // since intersections were not handled correctly. if ( - (requirements.authSelections === 'userPoolOnly' && - (authParameters.authSelections === 'userPoolOnly' || authParameters.authSelections === 'identityPoolAndUserPool')) || - (requirements.authSelections === 'identityPoolOnly' && authParameters.authSelections === 'identityPoolOnly') || - (requirements.authSelections === 'identityPoolOnly' && authParameters.authSelections === 'identityPoolAndUserPool') || - (requirements.authSelections === 'identityPoolAndUserPool' && authParameters.authSelections === 'identityPoolAndUserPool') + (requirements.authSelections === 'userPoolOnly' + && (authParameters.authSelections === 'userPoolOnly' || authParameters.authSelections === 'identityPoolAndUserPool')) + || (requirements.authSelections === 'identityPoolOnly' && authParameters.authSelections === 'identityPoolOnly') + || (requirements.authSelections === 'identityPoolOnly' && authParameters.authSelections === 'identityPoolAndUserPool') + || (requirements.authSelections === 'identityPoolAndUserPool' && authParameters.authSelections === 'identityPoolAndUserPool') ) { result.authSelections = true; } else { @@ -280,24 +310,28 @@ async function checkRequirements(requirements, context, category, targetResource } if ( - (requirements.allowUnauthenticatedIdentities === true && authParameters.allowUnauthenticatedIdentities === true) || - !requirements.allowUnauthenticatedIdentities // In this case it does not matter if IDP allows unauth access or not, requirements are met. + (requirements.allowUnauthenticatedIdentities === true && authParameters.allowUnauthenticatedIdentities === true) + // eslint-disable-next-line max-len + || !requirements.allowUnauthenticatedIdentities // In this case it does not matter if IDP allows unauth access or not, requirements are met. ) { result.allowUnauthenticatedIdentities = true; } else { result.allowUnauthenticatedIdentities = false; - result.errors.push(`Auth configuration is required to allow unauthenticated users, but it is not configured properly.`); + result.errors.push('Auth configuration is required to allow unauthenticated users, but it is not configured properly.'); } result.requirementsMet = result.authSelections && result.allowUnauthenticatedIdentities; return result; } - +/** + * entry point for init env + */ async function initEnv(context) { const { amplify } = context; - const { resourcesToBeCreated, resourcesToBeUpdated, resourcesToBeSynced, resourcesToBeDeleted, allResources } = - await amplify.getResourceStatus('auth'); + const { + resourcesToBeCreated, resourcesToBeUpdated, resourcesToBeSynced, resourcesToBeDeleted, allResources, + } = await amplify.getResourceStatus('auth'); const isPulling = context.input.command === 'pull' || (context.input.command === 'env' && context.input.subCommands[0] === 'pull'); let toBeCreated = []; let toBeUpdated = []; @@ -346,12 +380,16 @@ async function initEnv(context) { return async () => { const config = await updateConfigOnEnvInit(context, 'auth', resourceName); context.amplify.saveEnvResourceParameters(context, 'auth', resourceName, config); + await syncOAuthSecretsToCloud(context, resourceName, config); }; }); await sequential(authTasks); } +/** + * entry point to console command + */ async function authConsole(context) { const { amplify } = context; const amplifyMeta = amplify.getProjectMeta(); @@ -377,6 +415,9 @@ async function authConsole(context) { }); } +/** + * function to get permission policies for auth category + */ async function getPermissionPolicies(context, resourceOpsMapping) { const amplifyMetaFilePath = context.amplify.pathManager.getAmplifyMetaFilePath(); const amplifyMeta = context.amplify.readJsonFile(amplifyMetaFilePath); @@ -407,6 +448,9 @@ async function getPermissionPolicies(context, resourceOpsMapping) { return { permissionPolicies, resourceAttributes }; } +/** + * execute amplify interactive flow for auth commands + */ async function executeAmplifyCommand(context) { let commandPath = path.normalize(path.join(__dirname, 'commands')); if (context.input.command === 'help') { @@ -433,7 +477,7 @@ const executeAmplifyHeadlessCommand = async (context, headlessPayload) => { } await validateAddAuthRequest(headlessPayload) .then(getAddAuthRequestAdaptor(context.amplify.getProjectConfig().frontend)) - .then(getAddAuthHandler(context)) + .then(getAddAuthHandler(context)); return; case 'update': // migration check for headless update @@ -453,7 +497,9 @@ const executeAmplifyHeadlessCommand = async (context, headlessPayload) => { const providerPlugin = context.amplify.getPluginInstance(context, provider); const cognito = await providerPlugin.createCognitoUserPoolService(context); const identity = await providerPlugin.createIdentityPoolService(context); - const { userPoolId, identityPoolId, nativeClientId, webClientId } = JSONUtilities.parse(headlessPayload); + const { + userPoolId, identityPoolId, nativeClientId, webClientId, + } = JSONUtilities.parse(headlessPayload); const projectConfig = context.amplify.getProjectConfig(); const resourceName = projectConfig.projectName.toLowerCase().replace(/[^A-Za-z0-9_]+/g, '_'); const resourceParams = { @@ -469,19 +515,31 @@ const executeAmplifyHeadlessCommand = async (context, headlessPayload) => { return; default: context.print.error(`Headless mode for ${context.input.command} auth is not implemented yet`); - return; } }; +/** + * entry point for amplify events + */ async function handleAmplifyEvent(context, args) { - context.print.info(`${category} handleAmplifyEvent to be implemented`); context.print.info(`Received event args ${args}`); + switch (args.event) { + case 'PrePush': + await prePushHandler(context); + break; + } } -async function prePushAuthHook(context) { - //await transformUserPoolGroupSchema(context); +/** + * auth hook legacy support for userPoolGroups + */ +async function prePushAuthHook() { + // await transformUserPoolGroupSchema(context); } +/** + * entry point for import auth commands + */ async function importAuth(context) { const { amplify } = context; const servicesMetadata = getSupportedServices(); @@ -497,6 +555,9 @@ async function importAuth(context) { return providerController.importResource(context, serviceSelection, undefined, undefined, false); } +/** + * checks if sms enabled + */ async function isSMSWorkflowEnabled(context, resourceName) { const { imported, userPoolId } = context.amplify.getImportedAuthProperties(context); let userNameAndMfaConfig; @@ -516,7 +577,7 @@ module.exports = { add, migrate, initEnv, - console : authConsole, + console: authConsole, getPermissionPolicies, executeAmplifyCommand, executeAmplifyHeadlessCommand, diff --git a/packages/amplify-category-auth/src/provider-utils/awscloudformation/auth-secret-manager/auth-secret-manager.ts b/packages/amplify-category-auth/src/provider-utils/awscloudformation/auth-secret-manager/auth-secret-manager.ts new file mode 100644 index 00000000000..c5a6d3ffd3a --- /dev/null +++ b/packages/amplify-category-auth/src/provider-utils/awscloudformation/auth-secret-manager/auth-secret-manager.ts @@ -0,0 +1,97 @@ +import { $TSContext, stateManager } from 'amplify-cli-core'; +import aws from 'aws-sdk'; +import { getFullyQualifiedSecretName, oAuthObjSecretKey } from './secret-name'; + +/** + * Manages the state of OAuth secrets in AWS ParameterStore + */ +export class OAuthSecretsStateManager { + private static instance: OAuthSecretsStateManager; + + static getInstance = async (context: $TSContext): Promise => { + if (!OAuthSecretsStateManager.instance) { + OAuthSecretsStateManager.instance = new OAuthSecretsStateManager(await getSSMClient(context)); + } + return OAuthSecretsStateManager.instance; + }; + + private constructor(private readonly ssmClient: aws.SSM) { + } + + /** + * Set OAuth secret in parameter + */ + setOAuthSecrets = async (hostedUISecretObj: string, resourceName: string): Promise => { + const { envName } = stateManager.getLocalEnvInfo(); + const secretName = getFullyQualifiedSecretName(oAuthObjSecretKey, resourceName, envName); + const secretValue = hostedUISecretObj; + await this.ssmClient + .putParameter({ + Name: secretName, + Value: secretValue, + Type: 'SecureString', + Overwrite: true, + }).promise(); + } + + /** + * checks if the specified OAuth secrets exists in parameter store + */ + hasOAuthSecrets = async (resourceName: string): Promise => { + const { envName } = stateManager.getLocalEnvInfo(); + const secretName = getFullyQualifiedSecretName(oAuthObjSecretKey, resourceName, envName); + try { + await this.ssmClient + .getParameter({ + Name: secretName, + WithDecryption: true, + }) + .promise(); + } catch (err) { + return false; + } + return true; + } + + /** + * get the specified OAuth secrets from parameter store + */ + getOAuthSecrets = async (resourceName: string): Promise => { + const { envName } = stateManager.getLocalEnvInfo(); + const secretName = getFullyQualifiedSecretName(oAuthObjSecretKey, resourceName, envName); + const parameter = await this.ssmClient + .getParameter({ + Name: secretName, + WithDecryption: true, + }) + .promise(); + return parameter.Parameter?.Value; + } + + /** + * remove the specified OAuth secrets from parameter store + */ + removeOAuthSecrets = async (resourceName: string): Promise => { + const { envName } = stateManager.getLocalEnvInfo(); + const secretName = getFullyQualifiedSecretName(oAuthObjSecretKey, resourceName, envName); + try { + await this.ssmClient + .deleteParameter({ + Name: secretName, + }) + .promise(); + } catch (err) { + // parameter doesn't exist status code + if (err.statusCode !== 400) { + throw err; + } + } + }; +} + +const getSSMClient = async (context: $TSContext): Promise => { + const { client } = await context.amplify.invokePluginMethod(context, 'awscloudformation', undefined, 'getConfiguredSSMClient', [ + context, + ]); + return client as aws.SSM; +}; diff --git a/packages/amplify-category-auth/src/provider-utils/awscloudformation/auth-secret-manager/secret-name.ts b/packages/amplify-category-auth/src/provider-utils/awscloudformation/auth-secret-manager/secret-name.ts new file mode 100644 index 00000000000..8a98b579992 --- /dev/null +++ b/packages/amplify-category-auth/src/provider-utils/awscloudformation/auth-secret-manager/secret-name.ts @@ -0,0 +1,32 @@ +import { stateManager } from 'amplify-cli-core'; +import * as path from 'path'; +import { getAppId } from '../utils/get-app-id'; + +export const oAuthSecretsPathAmplifyAppIdKey = 'oAuthSecretsPathAmplifyAppId'; +export const oAuthObjSecretKey = 'hostedUIProviderCreds'; + +/** + * Returns the full name of the SSM parameter for secretName in resourceName in envName. + * + * If envName is not specified, the current env is assumed + */ +export const getFullyQualifiedSecretName = (secretName: string, resourceName: string, envName?: string) => `${getOAuthSecretPrefix(resourceName, envName)}${secretName}`; + +/** + * Returns the SSM parameter name prefix for all secrets for the given function in the given env + * + * If envName is not specified, the current env is assumed + */ +export const getOAuthSecretPrefix = (resourceName: string, envName?: string) => path.posix.join(getEnvSecretPrefix(envName), `AMPLIFY_${resourceName}_`); + +/** + * Returns the SSM parameter name prefix for all secrets in the given env. + * + * If envName is not specified, the current env is assumed + */ +export const getEnvSecretPrefix = (envName: string = stateManager.getLocalEnvInfo()?.envName) => { + if (!envName) { + throw new Error('Could not determine the current Amplify environment name. Try running `amplify env checkout`.'); + } + return path.posix.join('/amplify', getAppId(), envName); +}; diff --git a/packages/amplify-category-auth/src/provider-utils/awscloudformation/auth-secret-manager/sync-oauth-secrets.ts b/packages/amplify-category-auth/src/provider-utils/awscloudformation/auth-secret-manager/sync-oauth-secrets.ts new file mode 100644 index 00000000000..ac2488600c8 --- /dev/null +++ b/packages/amplify-category-auth/src/provider-utils/awscloudformation/auth-secret-manager/sync-oauth-secrets.ts @@ -0,0 +1,74 @@ +import { + $TSContext, $TSObject, +} from 'amplify-cli-core'; +import _ from 'lodash'; +import { AuthInputState } from '../auth-inputs-manager/auth-input-state'; +import { getOAuthObjectFromCognito } from '../utils/get-oauth-secrets-from-cognito'; +import { OAuthSecretsStateManager } from './auth-secret-manager'; +import { + removeAppIdForAuthInTeamProvider, setAppIdForAuthInTeamProvider, +} from './tpi-utils'; + +/** + * if secrets is defined, function stores the OAuth secret into parameter store with + * Key /amplify/{app-id}/{env}/{authResourceName}_HostedUIProviderCreds + * else fetches it from cognito to store in parameter store + */ +export const syncOAuthSecretsToCloud = async (context: $TSContext, authResourceName: string, secrets?: $TSObject) +: Promise => { +// check if its imported auth and check if auth is migrated + const { imported } = context.amplify.getImportedAuthProperties(context); + const cliState = new AuthInputState(authResourceName); + let oAuthSecretsString; + if (!imported) { + if (cliState.cliInputFileExists()) { + const authCliInputs = cliState.getCLIInputPayload(); + const oAuthSecretsStateManager = await OAuthSecretsStateManager.getInstance(context); + const authProviders = authCliInputs.cognitoConfig.authProvidersUserPool; + const { hostedUI, userPoolName } = authCliInputs.cognitoConfig; + if (!_.isEmpty(authProviders) && hostedUI) { + if (!_.isEmpty(secrets)) { + await oAuthSecretsStateManager.setOAuthSecrets(secrets?.hostedUIProviderCreds, authResourceName); + } else { + // check if parameter is set in the parameter store, + // if not then fetch the secrets from cognito and insert in parameter store + const hasOauthSecrets = await oAuthSecretsStateManager.hasOAuthSecrets(authResourceName); + // eslint-disable-next-line max-depth + if (!hasOauthSecrets) { + // data is present in deployent secrets , which can be fetched from cognito + const oAuthSecrets = await getOAuthObjectFromCognito(context, userPoolName!); + await oAuthSecretsStateManager.setOAuthSecrets(oAuthSecrets, authResourceName); + } + } + setAppIdForAuthInTeamProvider(authResourceName); + oAuthSecretsString = await oAuthSecretsStateManager.getOAuthSecrets(authResourceName); + } else { + removeAppIdForAuthInTeamProvider(authResourceName); + } + } + } + return oAuthSecretsString; +}; + +/** + * removes OAuth secret from parameter store + */ +export const removeOAuthSecretFromCloud = async (context: $TSContext, resourceName: string): Promise => { + // check if its imported auth and check if auth is migrated + const { imported } = context.amplify.getImportedAuthProperties(context); + const cliState = new AuthInputState(resourceName); + if (!imported) { + if (cliState.cliInputFileExists()) { + const authCliInputs = cliState.getCLIInputPayload(); + const oAuthSecretsStateManager = await OAuthSecretsStateManager.getInstance(context); + const authProviders = authCliInputs.cognitoConfig.authProvidersUserPool; + const { hostedUI } = authCliInputs.cognitoConfig; + if (!_.isEmpty(authProviders) && hostedUI) { + const hasOauthSecrets = await oAuthSecretsStateManager.hasOAuthSecrets(resourceName); + if (hasOauthSecrets) { + await oAuthSecretsStateManager.removeOAuthSecrets(resourceName); + } + } + } + } +}; diff --git a/packages/amplify-category-auth/src/provider-utils/awscloudformation/auth-secret-manager/tpi-utils.ts b/packages/amplify-category-auth/src/provider-utils/awscloudformation/auth-secret-manager/tpi-utils.ts new file mode 100644 index 00000000000..968409377d8 --- /dev/null +++ b/packages/amplify-category-auth/src/provider-utils/awscloudformation/auth-secret-manager/tpi-utils.ts @@ -0,0 +1,53 @@ +import { AmplifyCategories, stateManager } from 'amplify-cli-core'; +import _ from 'lodash'; +import { getAppId } from '../utils/get-app-id'; +import { oAuthObjSecretKey, oAuthSecretsPathAmplifyAppIdKey } from './secret-name'; +/** + * sets app id in team provider info + */ +export const setAppIdForAuthInTeamProvider = (authResourceName: string): void => { + const tpi = stateManager.getTeamProviderInfo(undefined, { throwIfNotExist: false, default: {} }); + const env = stateManager.getLocalEnvInfo()?.envName as string; + let authResourceTpi = tpi?.[env]?.categories?.[AmplifyCategories.AUTH]?.[authResourceName]; + if (!authResourceTpi) { + _.set(tpi, [env, 'categories', AmplifyCategories.AUTH, authResourceName], {}); + authResourceTpi = tpi[env].categories[AmplifyCategories.AUTH][authResourceName]; + } + _.assign(authResourceTpi, { [oAuthSecretsPathAmplifyAppIdKey]: getAppId() }); + stateManager.setTeamProviderInfo(undefined, tpi); +}; + +/** + * sets empty creds in team provider info for projects before ext migration + */ +export const setEmptyCredsForAuthInTeamProvider = (authResourceName: string): void => { + const tpi = stateManager.getTeamProviderInfo(undefined, { throwIfNotExist: false, default: {} }); + const env = stateManager.getLocalEnvInfo()?.envName as string; + let authResourceTpi = tpi?.[env]?.categories?.[AmplifyCategories.AUTH]?.[authResourceName]; + if (!authResourceTpi) { + _.set(tpi, [env, 'categories', AmplifyCategories.AUTH, authResourceName], {}); + authResourceTpi = tpi[env].categories[AmplifyCategories.AUTH][authResourceName]; + } + _.assign(authResourceTpi, { [oAuthObjSecretKey]: '[]' }); + stateManager.setTeamProviderInfo(undefined, tpi); +}; + +/** + * remove app id in team provider info + */ +export const removeAppIdForAuthInTeamProvider = (authResourceName: string): void => { + const tpi = stateManager.getTeamProviderInfo(undefined, { throwIfNotExist: false, default: {} }); + const env = stateManager.getLocalEnvInfo()?.envName as string; + _.unset(tpi, [env, 'categories', AmplifyCategories.AUTH, authResourceName, oAuthSecretsPathAmplifyAppIdKey]); + stateManager.setTeamProviderInfo(undefined, tpi); +}; + +/** + * remove empty hostedUICreds in team provider info + */ +export const removeEmptyCredsForAuthInTeamProvider = (authResourceName: string): void => { + const tpi = stateManager.getTeamProviderInfo(undefined, { throwIfNotExist: false, default: {} }); + const env = stateManager.getLocalEnvInfo()?.envName as string; + _.unset(tpi, [env, 'categories', AmplifyCategories.AUTH, authResourceName, oAuthObjSecretKey]); + stateManager.setTeamProviderInfo(undefined, tpi); +}; diff --git a/packages/amplify-category-auth/src/provider-utils/awscloudformation/auth-stack-builder/auth-cognito-stack-builder.ts b/packages/amplify-category-auth/src/provider-utils/awscloudformation/auth-stack-builder/auth-cognito-stack-builder.ts index 542a1798039..a6a4196afa6 100644 --- a/packages/amplify-category-auth/src/provider-utils/awscloudformation/auth-stack-builder/auth-cognito-stack-builder.ts +++ b/packages/amplify-category-auth/src/provider-utils/awscloudformation/auth-stack-builder/auth-cognito-stack-builder.ts @@ -4,8 +4,11 @@ import * as iam from '@aws-cdk/aws-iam'; import * as cognito from '@aws-cdk/aws-cognito'; import * as lambda from '@aws-cdk/aws-lambda'; import { AmplifyAuthCognitoStackTemplate } from '@aws-amplify/cli-extensibility-helper'; -import { CognitoStackOptions } from '../service-walkthrough-types/cognito-user-input-types'; import _ from 'lodash'; +import * as fs from 'fs-extra'; +import { $TSAny, AmplifyStackTemplate } from 'amplify-cli-core'; +import * as path from 'path'; +import { AttributeType } from '../service-walkthrough-types/awsCognito-user-input-types'; import { hostedUILambdaFilePath, hostedUIProviderLambdaFilePath, @@ -14,9 +17,8 @@ import { openIdLambdaFilePath, userPoolClientLambdaFilePath, } from '../constants'; -import * as fs from 'fs-extra'; -import { AmplifyStackTemplate } from 'amplify-cli-core'; -import { AttributeType } from '../service-walkthrough-types/awsCognito-user-input-types'; +import { CognitoStackOptions } from '../service-walkthrough-types/cognito-user-input-types'; +import { oAuthObjSecretKey, oAuthSecretsPathAmplifyAppIdKey } from '../auth-secret-manager/secret-name'; const CFN_TEMPLATE_FORMAT_VERSION = '2010-09-09'; const ROOT_CFN_DESCRIPTION = 'Amplify Cognito Stack for AWS Amplify CLI'; @@ -37,13 +39,20 @@ const authProvidersList: Record = { 'graph.facebook.com': 'facebookAppId', 'accounts.google.com': 'googleClientId', 'www.amazon.com': 'amazonAppId', + // eslint-disable-next-line spellcheck/spell-checker 'appleid.apple.com': 'appleAppId', }; +/** + * Props for Auth Stack Transform class + */ export type AmplifyAuthCognitoStackProps = { synthesizer: cdk.IStackSynthesizer; }; +/** + * AmplifyAuthCognitoStack class + */ export class AmplifyAuthCognitoStack extends cdk.Stack implements AmplifyAuthCognitoStackTemplate, AmplifyStackTemplate { private _scope: cdk.Construct; private _cfnParameterMap: Map = new Map(); @@ -66,7 +75,7 @@ export class AmplifyAuthCognitoStack extends cdk.Stack implements AmplifyAuthCog userPoolClientLambdaPolicy?: iam.CfnPolicy; userPoolClientLogPolicy?: iam.CfnPolicy; userPoolClientInputs?: cdk.CustomResource; - // customresources HostedUI + // custom resources HostedUI hostedUICustomResource?: lambda.CfnFunction; hostedUICustomResourcePolicy?: iam.CfnPolicy; hostedUICustomResourceLogPolicy?: iam.CfnPolicy; @@ -75,20 +84,21 @@ export class AmplifyAuthCognitoStack extends cdk.Stack implements AmplifyAuthCog hostedUIProvidersCustomResource?: lambda.CfnFunction; hostedUIProvidersCustomResourcePolicy?: iam.CfnPolicy; hostedUIProvidersCustomResourceLogPolicy?: iam.CfnPolicy; + hostedUIProviderCustomResourceSecretsPolicy?: iam.CfnPolicy; hostedUIProvidersCustomResourceInputs?: cdk.CustomResource; // custom resource OAUTH Provider oAuthCustomResource?: lambda.CfnFunction; oAuthCustomResourcePolicy?: iam.CfnPolicy; oAuthCustomResourceLogPolicy?: iam.CfnPolicy; oAuthCustomResourceInputs?: cdk.CustomResource; - //custom resource MFA + // custom resource MFA mfaLambda?: lambda.CfnFunction; mfaLogPolicy?: iam.CfnPolicy; mfaLambdaPolicy?: iam.CfnPolicy; mfaLambdaInputs?: cdk.CustomResource; mfaLambdaRole?: iam.CfnRole; - //custom resource identity pool - OPenId Lambda Role + // custom resource identity pool - OPenId Lambda Role openIdLambda?: lambda.CfnFunction; openIdLogPolicy?: iam.CfnPolicy; openIdLambdaIAMPolicy?: iam.CfnPolicy; @@ -103,6 +113,10 @@ export class AmplifyAuthCognitoStack extends cdk.Stack implements AmplifyAuthCog this.lambdaConfigPermissions = {}; this.lambdaTriggerPermissions = {}; } + + /** + * adds a cfn resource to auth stack + */ addCfnResource(props: cdk.CfnResourceProps, logicalId: string): void { if (!this._cfnResourceMap.has(logicalId)) { this._cfnResourceMap.set(logicalId, new cdk.CfnResource(this, logicalId, props)); @@ -110,25 +124,29 @@ export class AmplifyAuthCognitoStack extends cdk.Stack implements AmplifyAuthCog throw new Error(`Cfn Resource with LogicalId ${logicalId} already exists`); } } + + /** + * get cfn output + */ getCfnOutput(logicalId: string): cdk.CfnOutput { if (this._cfnOutputMap.has(logicalId)) { return this._cfnOutputMap.get(logicalId)!; - } else { - throw new Error(`Cfn Output with LogicalId ${logicalId} doesnt exist`); } + throw new Error(`Cfn Output with LogicalId ${logicalId} doesn't exist`); } + + /** + * get cfn mapping + */ getCfnMapping(logicalId: string): cdk.CfnMapping { if (this._cfnMappingMap.has(logicalId)) { return this._cfnMappingMap.get(logicalId)!; - } else { - throw new Error(`Cfn Mapping with LogicalId ${logicalId} doesnt exist`); } + throw new Error(`Cfn Mapping with LogicalId ${logicalId} doesn't exist`); } /** - * - * @param props :cdk.CfnOutputProps - * @param logicalId: : lodicalId of the Resource + * add cfn output to stack */ addCfnOutput(props: cdk.CfnOutputProps, logicalId: string): void { if (!this._cfnOutputMap.has(logicalId)) { @@ -139,9 +157,7 @@ export class AmplifyAuthCognitoStack extends cdk.Stack implements AmplifyAuthCog } /** - * - * @param props - * @param logicalId + * adds cfn mapping to auth stack */ addCfnMapping(props: cdk.CfnMappingProps, logicalId: string): void { if (!this._cfnMappingMap.has(logicalId)) { @@ -152,9 +168,7 @@ export class AmplifyAuthCognitoStack extends cdk.Stack implements AmplifyAuthCog } /** - * - * @param props - * @param logicalId + * adds cfn condition to auth stack */ addCfnCondition(props: cdk.CfnConditionProps, logicalId: string): void { if (!this._cfnConditionMap.has(logicalId)) { @@ -165,9 +179,7 @@ export class AmplifyAuthCognitoStack extends cdk.Stack implements AmplifyAuthCog } /** - * - * @param props - * @param logicalId + * adds cfn parameter to auth stack */ addCfnParameter(props: cdk.CfnParameterProps, logicalId: string): void { if (!this._cfnParameterMap.has(logicalId)) { @@ -177,33 +189,36 @@ export class AmplifyAuthCognitoStack extends cdk.Stack implements AmplifyAuthCog } } + /** + * return cfn parameter in stack + */ getCfnParameter(logicalId: string): cdk.CfnParameter { if (this._cfnParameterMap.has(logicalId)) { return this._cfnParameterMap.get(logicalId)!; - } else { - throw new Error(`Cfn Parameter with LogicalId ${logicalId} doesnt exist`); } + throw new Error(`Cfn Parameter with LogicalId ${logicalId} doesn't exist`); } + /** + * return cfn condition in stack + */ getCfnCondition(logicalId: string): cdk.CfnCondition { if (this._cfnConditionMap.has(logicalId)) { return this._cfnConditionMap.get(logicalId)!; - } else { - throw new Error(`Cfn Condition with LogicalId ${logicalId} doesnt exist`); } + throw new Error(`Cfn Condition with LogicalId ${logicalId} doesn't exist`); } - generateCognitoStackResources = async (props: CognitoStackOptions) => { + generateCognitoStackResources = async (props: CognitoStackOptions): Promise => { const autoVerifiedAttributes = props.autoVerifiedAttributes ? props.autoVerifiedAttributes - .concat(props.aliasAttributes ? props.aliasAttributes : []) - .filter((attr, i, aliasAttributeArray) => ['email', 'phone_number'].includes(attr) && aliasAttributeArray.indexOf(attr) === i) + .concat(props.aliasAttributes ? props.aliasAttributes : []) + .filter((attr, i, aliasAttributeArray) => ['email', 'phone_number'].includes(attr) && aliasAttributeArray.indexOf(attr) === i) : []; - const configureSMS = - (props.autoVerifiedAttributes && props.autoVerifiedAttributes.includes('phone_number')) || - (props.mfaConfiguration != 'OFF' && props.mfaTypes && props.mfaTypes.includes('SMS Text Message')) || - (props.requiredAttributes && props.requiredAttributes.includes('phone_number')) || - (props.usernameAttributes && props.usernameAttributes.includes(AttributeType.PHONE_NUMBER)); + const configureSMS = (props.autoVerifiedAttributes && props.autoVerifiedAttributes.includes('phone_number')) + || (props.mfaConfiguration != 'OFF' && props.mfaTypes && props.mfaTypes.includes('SMS Text Message')) + || (props.requiredAttributes && props.requiredAttributes.includes('phone_number')) + || (props.usernameAttributes && props.usernameAttributes.includes(AttributeType.PHONE_NUMBER)); if (props.verificationBucketName) { this.customMessageConfirmationBucket = new s3.CfnBucket(this, 'CustomMessageConfirmationBucket', { @@ -301,7 +316,7 @@ export class AmplifyAuthCognitoStack extends cdk.Stack implements AmplifyAuthCog } if (props.requiredAttributes && props.requiredAttributes.length > 0) { - let schemaAttributes: cognito.CfnUserPool.SchemaAttributeProperty[] = []; + const schemaAttributes: cognito.CfnUserPool.SchemaAttributeProperty[] = []; props.requiredAttributes.forEach(attr => { schemaAttributes.push({ name: attr, @@ -379,7 +394,7 @@ export class AmplifyAuthCognitoStack extends cdk.Stack implements AmplifyAuthCog this.userPool.emailVerificationSubject = cdk.Fn.ref('emailVerificationSubject'); } - //TODO: change this + // TODO: change this if (props.usernameAttributes && (props.usernameAttributes[0] as string) !== 'username') { this.userPool.usernameAttributes = cdk.Fn.ref('usernameAttributes') as unknown as string[]; } @@ -411,6 +426,7 @@ export class AmplifyAuthCognitoStack extends cdk.Stack implements AmplifyAuthCog this.userPool.addDependsOn(this.snsRole!); } + // eslint-disable-next-line spellcheck/spell-checker // updating Lambda Config when FF is (breakcirculardependency : false) if (!props.breakCircularDependency && props.triggers && props.dependsOn) { @@ -427,7 +443,7 @@ export class AmplifyAuthCognitoStack extends cdk.Stack implements AmplifyAuthCog } }); }); - //Updating lambda role with permissions to Cognito + // Updating lambda role with permissions to Cognito if (!_.isEmpty(props.permissions)) { this.generateIAMPolicies(props); } @@ -507,19 +523,19 @@ export class AmplifyAuthCognitoStack extends cdk.Stack implements AmplifyAuthCog } if ( - props.authProviders && - !_.isEmpty(props.authProviders) && - !(Object.keys(props.authProviders).length === 1 && props.authProviders[0] === 'accounts.google.com' && props.audiences) + props.authProviders + && !_.isEmpty(props.authProviders) + && !(Object.keys(props.authProviders).length === 1 && props.authProviders[0] === 'accounts.google.com' && props.audiences) ) { this.identityPool.supportedLoginProviders = cdk.Lazy.anyValue({ produce: () => { - let supprtedProvider: any = {}; + const supportedProvider: $TSAny = {}; props.authProviders?.forEach(provider => { if (Object.keys(authProvidersList).includes(provider)) { - supprtedProvider[provider] = cdk.Fn.ref(authProvidersList[provider]); + supportedProvider[provider] = cdk.Fn.ref(authProvidersList[provider]); } }); - return supprtedProvider; + return supportedProvider; }, }); } @@ -548,15 +564,15 @@ export class AmplifyAuthCognitoStack extends cdk.Stack implements AmplifyAuthCog // add Function for Custom Resource in Root stack /** - * - * @param _ - * @returns + * render cfn template for given synthesizer */ - public renderCloudFormationTemplate = (_: cdk.ISynthesisSession): string => { - return JSON.stringify(this._toCloudFormation(), undefined, 2); - }; + // eslint-disable-next-line @typescript-eslint/no-shadow + public renderCloudFormationTemplate = (_: cdk.ISynthesisSession): string => JSON.stringify(this._toCloudFormation(), undefined, 2); - createUserPoolClientCustomResource(props: CognitoStackOptions) { + /** + * creates userPool client custom resource + */ + createUserPoolClientCustomResource(props: CognitoStackOptions): void { // iam role this.userPoolClientRole = new iam.CfnRole(this, 'UserPoolClientRole', { roleName: cdk.Fn.conditionIf( @@ -604,6 +620,7 @@ export class AmplifyAuthCognitoStack extends cdk.Stack implements AmplifyAuthCog # Marked as depending on UserPoolClientRole for easier to understand CFN sequencing */ this.userPoolClientLambdaPolicy = new iam.CfnPolicy(this, 'UserPoolClientLambdaPolicy', { + // eslint-disable-next-line spellcheck/spell-checker policyName: `${props.resourceNameTruncated}_userpoolclient_lambda_iam_policy`, policyDocument: { Version: '2012-10-17', @@ -622,6 +639,7 @@ export class AmplifyAuthCognitoStack extends cdk.Stack implements AmplifyAuthCog // userPool Client Log policy this.userPoolClientLogPolicy = new iam.CfnPolicy(this, 'UserPoolClientLogPolicy', { + // eslint-disable-next-line spellcheck/spell-checker policyName: `${props.resourceNameTruncated}_userpoolclient_lambda_log_policy`, policyDocument: { Version: '2012-10-17', @@ -653,7 +671,10 @@ export class AmplifyAuthCognitoStack extends cdk.Stack implements AmplifyAuthCog this.userPoolClientInputs.node.addDependency(this.userPoolClientLogPolicy); } - createHostedUICustomResource() { + /** + * creates hostedUI custom resource + */ + createHostedUICustomResource(): void { // lambda function this.hostedUICustomResource = new lambda.CfnFunction(this, 'HostedUICustomResource', { code: { @@ -731,7 +752,10 @@ export class AmplifyAuthCognitoStack extends cdk.Stack implements AmplifyAuthCog this.hostedUICustomResourceInputs.node.addDependency(this.hostedUICustomResourceLogPolicy); } - createHostedUIProviderCustomResource() { + /** + * creates hostedUIProviders custom resource + */ + createHostedUIProviderCustomResource(): void { // lambda function this.hostedUIProvidersCustomResource = new lambda.CfnFunction(this, 'HostedUIProvidersCustomResource', { code: { @@ -741,6 +765,16 @@ export class AmplifyAuthCognitoStack extends cdk.Stack implements AmplifyAuthCog role: cdk.Fn.getAtt('UserPoolClientRole', 'Arn').toString(), runtime: 'nodejs14.x', timeout: 300, + environment: { + variables: { + hostedUIProviderCreds: cdk.Fn.sub(path.posix.join('/amplify', '${appId}', '${env}', 'AMPLIFY_${resourceName}_${oauthObjSecretKey}'), { + appId: cdk.Fn.ref(`${oAuthSecretsPathAmplifyAppIdKey}`), + env: cdk.Fn.ref('env'), + resourceName: cdk.Fn.ref('resourceName'), + oauthObjSecretKey: `${oAuthObjSecretKey}`, + }), + }, + }, }); this.hostedUIProvidersCustomResource.addDependsOn(this.userPoolClientRole!); @@ -796,22 +830,49 @@ export class AmplifyAuthCognitoStack extends cdk.Stack implements AmplifyAuthCog }, roles: [cdk.Fn.ref('UserPoolClientRole')], }); + this.hostedUIProvidersCustomResourceLogPolicy.addDependsOn(this.hostedUIProvidersCustomResourcePolicy); + // iam policy for hostedUIProvider Lambda Function to get OAuth secrets + + this.hostedUIProviderCustomResourceSecretsPolicy = new iam.CfnPolicy(this, 'hostedUIProvidersCustomResourceSecretPolicy', { + policyName: cdk.Fn.join('-', [cdk.Fn.ref('UserPool'), 'hostedUIProvidersCustomResourceSecretPolicy']), + policyDocument: { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Action: ['ssm:GetParameter'], + // eslint-disable-next-line spellcheck/spell-checker + Resource: cdk.Fn.sub(`arn:aws:ssm:\${AWS::Region}:\${AWS::AccountId}:parameter/${path.posix.join('amplify', '${appId}', '${env}', 'AMPLIFY_${resourceName}_${oauthObjSecretKey}')}`, { + appId: cdk.Fn.ref(`${oAuthSecretsPathAmplifyAppIdKey}`), + env: cdk.Fn.ref('env'), // this is dependent on the Amplify env name being a parameter to the CFN template which should always be the case + resourceName: cdk.Fn.ref('resourceName'), + oauthObjSecretKey: `${oAuthObjSecretKey}`, + }), + }], + }, + roles: [cdk.Fn.ref('UserPoolClientRole')], + }); + + this.hostedUIProviderCustomResourceSecretsPolicy.addDependsOn(this.hostedUIProvidersCustomResourcePolicy); + // userPoolClient Custom Resource this.hostedUIProvidersCustomResourceInputs = new cdk.CustomResource(this, 'HostedUIProvidersCustomResourceInputs', { serviceToken: this.hostedUIProvidersCustomResource.attrArn, resourceType: 'Custom::LambdaCallout', properties: { hostedUIProviderMeta: cdk.Fn.ref('hostedUIProviderMeta'), - hostedUIProviderCreds: cdk.Fn.ref('hostedUIProviderCreds'), userPoolId: cdk.Fn.ref('UserPool'), }, }); - this.hostedUIProvidersCustomResourceInputs.node.addDependency(this.hostedUIProvidersCustomResourceLogPolicy); + this.hostedUIProvidersCustomResourceInputs.node.addDependency(this.hostedUIProviderCustomResourceSecretsPolicy); } - createOAuthCustomResource() { + /** + * creates OAuth custom resource + */ + createOAuthCustomResource(): void { // lambda function this.oAuthCustomResource = new lambda.CfnFunction(this, 'OAuthCustomResource', { code: { @@ -885,7 +946,10 @@ export class AmplifyAuthCognitoStack extends cdk.Stack implements AmplifyAuthCog this.oAuthCustomResourceInputs.node.addDependency(this.oAuthCustomResourceLogPolicy); } - createMFACustomResource(props: CognitoStackOptions) { + /** + * creates MFA custom resource + */ + createMFACustomResource(props: CognitoStackOptions): void { // iam role this.mfaLambdaRole = new iam.CfnRole(this, 'MFALambdaRole', { roleName: cdk.Fn.conditionIf( @@ -1033,7 +1097,10 @@ export class AmplifyAuthCognitoStack extends cdk.Stack implements AmplifyAuthCog this.mfaLambdaInputs.node.addDependency(this.mfaLogPolicy); } - createOpenIdLambdaCustomResource(props: CognitoStackOptions) { + /** + * creates OpenIDLambda custom resource + */ + createOpenIdLambdaCustomResource(props: CognitoStackOptions): void { // iam role /** # Created to execute Lambda which sets MFA config values @@ -1077,7 +1144,7 @@ export class AmplifyAuthCognitoStack extends cdk.Stack implements AmplifyAuthCog }, ], }); - //TODO + // TODO this.openIdLambdaRole!.node.addDependency(this.userPoolClientInputs!.node!.defaultChild!); // lambda function /** diff --git a/packages/amplify-category-auth/src/provider-utils/awscloudformation/auth-stack-builder/auth-stack-transform.ts b/packages/amplify-category-auth/src/provider-utils/awscloudformation/auth-stack-builder/auth-stack-transform.ts index fa3fd1df441..f815f829fbf 100644 --- a/packages/amplify-category-auth/src/provider-utils/awscloudformation/auth-stack-builder/auth-stack-transform.ts +++ b/packages/amplify-category-auth/src/provider-utils/awscloudformation/auth-stack-builder/auth-stack-transform.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines-per-function */ import { AmplifyCategories, AmplifySupportedService, @@ -14,21 +15,25 @@ import { stateManager, $TSAny, } from 'amplify-cli-core'; -import { AmplifyAuthCognitoStack } from './auth-cognito-stack-builder'; -import { AuthStackSynthesizer } from './stack-synthesizer'; import * as cdk from '@aws-cdk/core'; -import { AuthInputState } from '../auth-inputs-manager/auth-input-state'; -import { CognitoStackOptions, AuthTriggerConnection, AuthTriggerPermissions } from '../service-walkthrough-types/cognito-user-input-types'; import _ from 'lodash'; import * as path from 'path'; import { printer, formatter } from 'amplify-prompts'; -import { generateNestedAuthTriggerTemplate } from '../utils/generate-auth-trigger-template'; -import { createUserPoolGroups, updateUserPoolGroups } from '../utils/synthesize-resources'; -import { AttributeType, CognitoCLIInputs } from '../service-walkthrough-types/awsCognito-user-input-types'; import * as vm from 'vm2'; import * as fs from 'fs-extra'; import os from 'os'; +import { AmplifyAuthCognitoStack } from './auth-cognito-stack-builder'; +import { AuthStackSynthesizer } from './stack-synthesizer'; +import { AuthInputState } from '../auth-inputs-manager/auth-input-state'; +import { CognitoStackOptions, AuthTriggerConnection, AuthTriggerPermissions } from '../service-walkthrough-types/cognito-user-input-types'; +import { generateNestedAuthTriggerTemplate } from '../utils/generate-auth-trigger-template'; +import { createUserPoolGroups, updateUserPoolGroups } from '../utils/synthesize-resources'; +import { AttributeType, CognitoCLIInputs } from '../service-walkthrough-types/awsCognito-user-input-types'; +import { getAppId } from '../utils/get-app-id'; +/** + * Auth CFN transformation class + */ export class AmplifyAuthTransform extends AmplifyCategoryTransform { private _app: cdk.App; private _category: string; @@ -44,10 +49,14 @@ export class AmplifyAuthTransform extends AmplifyCategoryTransform { this._app = new cdk.App(); this._category = AmplifyCategories.AUTH; this._service = AmplifySupportedService.COGNITO; + // eslint-disable-next-line spellcheck/spell-checker this._authTemplateObj = new AmplifyAuthCognitoStack(this._app, 'AmplifyAuthCongitoStack', { synthesizer: this._synthesizer }); } - public async transform(context: $TSContext): Promise