Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: generate OAuth keys from cognito for amplify pull workflow #12518

Merged
merged 8 commits into from
May 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { $TSContext } from '@aws-amplify/amplify-cli-core';
import { updateConfigOnEnvInit } from '../../../provider-utils/awscloudformation/index';
import { getOAuthObjectFromCognito } from '../../../provider-utils/awscloudformation/utils/get-oauth-secrets-from-cognito';

jest.mock('@aws-amplify/amplify-environment-parameters');
jest.mock('../../../provider-utils/awscloudformation/utils/get-oauth-secrets-from-cognito');
jest.mock('@aws-amplify/amplify-cli-core', () => ({
...(jest.requireActual('@aws-amplify/amplify-cli-core') as {}),
JSONUtilities: {
Expand All @@ -18,12 +20,14 @@ jest.mock('@aws-amplify/amplify-cli-core', () => ({
const pluginInstanceMock = jest.fn();
const loadResourceParametersMock = jest.fn().mockReturnValue({
hostedUIProviderMeta:
'[{"ProviderName":"Facebook","authorize_scopes":"email,public_profile","AttributeMapping":{"email":"email","username":"id"}},{"ProviderName":"LoginWithAmazon","authorize_scopes":"profile profile:user_id","AttributeMapping":{"email":"email","username":"user_id"}},{"ProviderName":"Google","authorize_scopes":"openid email profile","AttributeMapping":{"email":"email","username":"sub"}}]',
'[{"ProviderName":"Facebook","authorize_scopes":"email,public_profile","AttributeMapping":{"email":"email","username":"id"}},{"ProviderName":"LoginWithAmazon","authorize_scopes":"profile profile:user_id","AttributeMapping":{"email":"email","username":"user_id"}},{"ProviderName":"Google","authorize_scopes":"openid email profile","AttributeMapping":{"email":"email","username":"sub"}},{"ProviderName":"SignInWithApple","authorize_scopes":"openid email profile","AttributeMapping":{"email":"email","username":"sub"}}]',
});
const pluginInstance = {
loadResourceParameters: loadResourceParametersMock,
};

const getOAuthObjectFromCognitoMock = getOAuthObjectFromCognito as jest.MockedFunction<typeof getOAuthObjectFromCognito>;

// mock context
let mockContext = {
amplify: {
Expand Down Expand Up @@ -61,8 +65,106 @@ let mockContext = {
},
} as unknown as $TSContext;

test('throws amplify error when auth headless params are missing during pull', async () => {
expect(() => updateConfigOnEnvInit(mockContext, 'auth', 'Cognito')).rejects.toThrowErrorMatchingInlineSnapshot(
`"auth headless is missing the following inputParameters facebookAppIdUserPool, facebookAppSecretUserPool, loginwithamazonAppIdUserPool, loginwithamazonAppSecretUserPool, googleAppIdUserPool, googleAppSecretUserPool"`,
);
describe('import checks', () => {
test('throws amplify error when auth headless params are missing during pull', async () => {
expect(() => updateConfigOnEnvInit(mockContext, 'auth', 'Cognito')).rejects.toThrowErrorMatchingInlineSnapshot(
`"auth headless is missing the following inputParameters facebookAppIdUserPool, facebookAppSecretUserPool, loginwithamazonAppIdUserPool, loginwithamazonAppSecretUserPool, googleAppIdUserPool, googleAppSecretUserPool"`,
);
});
});

describe('update config when amplify pull headless command', () => {
test('throws amplify error when auth headless params are missing during pull', async () => {
mockContext.input.command = 'pull';
getOAuthObjectFromCognitoMock.mockResolvedValue(undefined);
expect(() => updateConfigOnEnvInit(mockContext, 'auth', 'Cognito')).rejects.toThrowErrorMatchingInlineSnapshot(
`"auth headless is missing the following inputParameters facebookAppIdUserPool, facebookAppSecretUserPool, loginwithamazonAppIdUserPool, loginwithamazonAppSecretUserPool, googleAppIdUserPool, googleAppSecretUserPool"`,
);
});

test('works when secrets are fetched from userpool', async () => {
mockContext.input.command = 'pull';
getOAuthObjectFromCognitoMock.mockResolvedValue([
{
client_id: 'mockClientFacebook',
client_secret: 'mockSecretFacebook',
ProviderName: 'Facebook',
},
{
client_id: 'mockClientGoogle',
client_secret: 'mockSecretGoogle',
ProviderName: 'Google',
},
{
client_id: 'mockClientLoginWithAmazon',
client_secret: 'mockSecretLoginWithAmazon',
ProviderName: 'LoginWithAmazon',
},
{
client_id: 'mockClientSignInWithApple',
team_id: 'mockTeamIdSignInWithApple',
key_id: 'mockKeyIdSignInWithApple',
private_key: 'mockPrivayKeySignInWithApple',
ProviderName: 'SignInWithApple',
},
]);
const params = await updateConfigOnEnvInit(mockContext, 'auth', 'Cognito');
expect(params).toMatchInlineSnapshot(`
Object {
"hostedUIProviderCreds": "[{\\"ProviderName\\":\\"Facebook\\"},{\\"ProviderName\\":\\"LoginWithAmazon\\"},{\\"ProviderName\\":\\"Google\\"},{\\"ProviderName\\":\\"SignInWithApple\\"}]",
}
`);
});

test('works when secrets are present in deployment params', async () => {
mockContext.input.command = 'pull';
getOAuthObjectFromCognitoMock.mockResolvedValue(undefined);
mockContext.amplify.loadEnvResourceParameters = jest.fn().mockReturnValue({
hostedUIProviderCreds:
'[{"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 params = await updateConfigOnEnvInit(mockContext, 'auth', 'Cognito');
expect(params).toMatchInlineSnapshot(`
Object {
"hostedUIProviderCreds": "[{\\"ProviderName\\":\\"Facebook\\"},{\\"ProviderName\\":\\"LoginWithAmazon\\"},{\\"ProviderName\\":\\"Google\\"},{\\"ProviderName\\":\\"SignInWithApple\\"}]",
}
`);
});

test('test works when secrets are present in context input params', async () => {
mockContext.input.command = 'pull';
getOAuthObjectFromCognitoMock.mockResolvedValue(undefined);
mockContext.amplify.loadEnvResourceParameters = jest.fn().mockReturnValue('[]');
mockContext.exeInfo = {
inputParams: {
yes: true,
categories: {
auth: {
facebookAppIdUserPool: 'mockfacebookAppIdUserPool',
facebookAppSecretUserPool: 'facebookAppSecretUserPool',
googleAppIdUserPool: 'googleAppIdUserPool',
googleAppSecretUserPool: 'googleAppSecretUserPool',
loginwithamazonAppIdUserPool: 'loginwithamazonAppIdUserPool',
loginwithamazonAppSecretUserPool: 'loginwithamazonAppSecretUserPool',
signinwithappleClientIdUserPool: 'signinwithappleClientIdUserPool',
signinwithappleTeamIdUserPool: 'signinwithappleTeamIdUserPool',
signinwithappleKeyIdUserPool: 'signinwithappleKeyIdUserPool',
signinwithapplePrivateKeyUserPool: 'signinwithapplePrivateKeyUserPool',
},
},
},
localEnvInfo: {
projectPath: 'mockProjectPath',
defaultEditor: 'vscode',
envName: 'dev',
noUpdateBackend: false,
},
};
const params = await updateConfigOnEnvInit(mockContext, 'auth', 'Cognito');
expect(params).toMatchInlineSnapshot(`
Object {
"hostedUIProviderCreds": "[{\\"ProviderName\\":\\"Facebook\\",\\"client_id\\":\\"mockfacebookAppIdUserPool\\",\\"client_secret\\":\\"facebookAppSecretUserPool\\"},{\\"ProviderName\\":\\"LoginWithAmazon\\",\\"client_id\\":\\"loginwithamazonAppIdUserPool\\",\\"client_secret\\":\\"loginwithamazonAppSecretUserPool\\"},{\\"ProviderName\\":\\"Google\\",\\"client_id\\":\\"googleAppIdUserPool\\",\\"client_secret\\":\\"googleAppSecretUserPool\\"},{\\"ProviderName\\":\\"SignInWithApple\\",\\"client_id\\":\\"signinwithappleClientIdUserPool\\",\\"team_id\\":\\"signinwithappleTeamIdUserPool\\",\\"key_id\\":\\"signinwithappleKeyIdUserPool\\",\\"private_key\\":\\"signinwithapplePrivateKeyUserPool\\"}]",
}
`);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -897,7 +897,7 @@ const createEnvSpecificResourceParameters = (
return envSpecificResourceParameters;
};

const createOAuthCredentials = (identityProviders: IdentityProviderType[]): string => {
export const createOAuthCredentials = (identityProviders: IdentityProviderType[]): string => {
const credentials = identityProviders.map((idp) => {
if (idp.ProviderName === 'SignInWithApple') {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { getAddAuthHandler, getUpdateAuthHandler } from './handlers/resource-han
import { getSupportedServices } from '../supported-services';
import { importResource, importedAuthEnvInit } from './import';
import { AuthContext } from '../../context';
import { getOAuthObjectFromCognito } from './utils/get-oauth-secrets-from-cognito';

export { importResource } from './import';

Expand Down Expand Up @@ -133,6 +134,14 @@ export const updateConfigOnEnvInit = async (context: $TSContext, category: any,

if (hostedUIProviderMeta) {
currentEnvSpecificValues = getOAuthProviderKeys(currentEnvSpecificValues, resourceParams);
const authParamsFromCognito = await getOAuthObjectFromCognito(context, resourceParams.userPoolName);
// fill in the OAuthProvider Keys from userpool if missing from currentEnvValues
if (authParamsFromCognito) {
currentEnvSpecificValues = {
...getOAuthProviderKeys({ hostedUIProviderCreds: JSON.stringify(authParamsFromCognito) }, resourceParams),
...currentEnvSpecificValues,
};
}
}

// legacy headless mode (only supports init)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { ICognitoUserPoolService } from '@aws-amplify/amplify-util-import';
import { $TSContext, stateManager, JSONUtilities } from '@aws-amplify/amplify-cli-core';
import { IdentityProviderType } from 'aws-sdk/clients/cognitoidentityserviceprovider';
import { createOAuthCredentials } from '../import';

/**
* get oAuth secrets from cognito only for amplify generated userPools
*/
export const getOAuthObjectFromCognito = async (
context: $TSContext,
userPoolName: string,
): Promise<Array<OAuthProviderDetails> | undefined> => {
const { envName } = stateManager.getLocalEnvInfo();
const envUserPoolName = `${userPoolName}-${envName}`;
const cognito = (await context.amplify.invokePluginMethod(context, 'awscloudformation', undefined, 'createCognitoUserPoolService', [
context,
])) as ICognitoUserPoolService;
const userPool = (await cognito.listUserPools()).filter((userPoolCognito) => userPoolCognito.Name === envUserPoolName)[0];
const userPoolId = userPool?.Id;
if (userPoolId) {
const identityProviders: IdentityProviderType[] = await cognito.listUserPoolIdentityProviders(userPoolId);
if (identityProviders.length > 0) {
const providerObj = JSONUtilities.parse<Array<OAuthProviderDetails>>(createOAuthCredentials(identityProviders));
return providerObj;
}
}
return undefined;
};

/**
* type for "Facebook" | "Google" | "LoginWithAmazon"
*/
export type GenericProviderDetails = {
ProviderName: string;
client_id: string;
client_secret: string;
};

/**
* type for SignInWithApple
*/
export type AppleProviderDetails = {
ProviderName: string;
client_id: string;
team_id: string;
key_id: string;
private_key: string;
};

export type OAuthProviderDetails = GenericProviderDetails | AppleProviderDetails;
43 changes: 43 additions & 0 deletions packages/amplify-e2e-tests/src/__tests__/auth_2g.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/* eslint-disable spellcheck/spell-checker */
import {
addAuthWithDefaultSocial,
addFunction,
amplifyPull,
amplifyPushAuth,
createNewProjectDir,
deleteProject,
deleteProjectDir,
generateRandomShortId,
getAppId,
initJSProjectWithProfile,
} from '@aws-amplify/amplify-e2e-core';

const defaultsSettings = {
name: 'authTest',
};

describe('amplify add auth...', () => {
let projRoot: string;
let projRoot2;
beforeEach(async () => {
projRoot = await createNewProjectDir('auth');
projRoot2 = await createNewProjectDir('auth2');
});

afterEach(async () => {
await deleteProject(projRoot);
deleteProjectDir(projRoot);
deleteProjectDir(projRoot2);
});

it('...should init a project and add auth with defaultSocial and pull should succeed', async () => {
await initJSProjectWithProfile(projRoot, { ...defaultsSettings, disableAmplifyAppCreation: false });
await addAuthWithDefaultSocial(projRoot);
await amplifyPushAuth(projRoot);
const appId = getAppId(projRoot);
// amplify pull should work
const functionName = `testcorsfunction${generateRandomShortId()}`;
await addFunction(projRoot, { functionTemplate: 'Hello World', name: functionName }, 'nodejs');
await amplifyPull(projRoot2, { emptyDir: true, appId, envName: 'integtest', yesFlag: true });
});
});
1 change: 1 addition & 0 deletions scripts/split-e2e-tests-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ const TEST_EXCLUSIONS: { l: string[]; w: string[] } = {
'src/__tests__/storage-simulator/S3server.test.ts',
'src/__tests__/amplify-app.test.ts',
// failing in parsing JSON strings on powershell
'src/__tests__/auth_2g.test.ts',
'src/__tests__/auth_12.test.ts',
'src/__tests__/datastore-modelgen.test.ts',
'src/__tests__/diagnose.test.ts',
Expand Down