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 1 commit
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"
akshbhu marked this conversation as resolved.
Show resolved Hide resolved
*/
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;
14 changes: 13 additions & 1 deletion packages/amplify-e2e-tests/src/__tests__/auth_2a.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
/* eslint-disable spellcheck/spell-checker */
import {
addAuthWithDefaultSocial,
addFunction,
amplifyPull,
amplifyPushAuth,
createNewProjectDir,
deleteProject,
deleteProjectDir,
generateRandomShortId,
getAppId,
getProjectMeta,
getUserPool,
getUserPoolClients,
Expand All @@ -29,10 +33,11 @@ describe('amplify add auth...', () => {
});

it('...should init a project and add auth with defaultSocial', async () => {
await initJSProjectWithProfile(projRoot, defaultsSettings);
await initJSProjectWithProfile(projRoot, { ...defaultsSettings, disableAmplifyAppCreation: false });
await addAuthWithDefaultSocial(projRoot);
expect(isDeploymentSecretForEnvExists(projRoot, 'integtest')).toBeTruthy();
await amplifyPushAuth(projRoot);
const appId = getAppId(projRoot);
const meta = getProjectMeta(projRoot);
expect(isDeploymentSecretForEnvExists(projRoot, 'integtest')).toBeFalsy();
const authMeta = Object.keys(meta.auth).map((key) => meta.auth[key])[0];
Expand All @@ -47,5 +52,12 @@ describe('amplify add auth...', () => {
expect(clients[0].UserPoolClient.CallbackURLs[0]).toEqual('https://www.google.com/');
expect(clients[0].UserPoolClient.LogoutURLs[0]).toEqual('https://www.nytimes.com/');
expect(clients[0].UserPoolClient.SupportedIdentityProviders).toHaveLength(5);

// amplify pull should work
const functionName = `testcorsfunction${generateRandomShortId()}`;
const projRoot2 = await createNewProjectDir('auth2');
await addFunction(projRoot, { functionTemplate: 'Hello World', name: functionName }, 'nodejs');
await amplifyPull(projRoot2, { emptyDir: true, appId, envName: 'integtest', yesFlag: true });
deleteProjectDir(projRoot2);
akshbhu marked this conversation as resolved.
Show resolved Hide resolved
});
});