Skip to content

Commit

Permalink
update aws amplify from v5 to v6 and fix its test (#1513)
Browse files Browse the repository at this point in the history
* docs: fix typo in console error message for cognito index page

* fix: global replace of case-sensitive VITE_APP with VITE;

this seems to have been a mistake during the upgrade from create-react-app to vite.  See historical commits 7b78b6e and e66f559 for the mystery of where VITE_APP came from.  REACT_APP should have been replaced with VITE but was replaced with VITE_APP instead.

* chore: upgrade amplify-js library from version 5 to version 6 and

fix its example cypress tests for programmatic login and cy.origin() login.

* docs: prettier fixes to README.md

* build: update yarn lock changes only for amplify on chore branch

by: removing amplify from package.json, yarn install, replacing amplify in package.json, yarn install.

* Update README.md

backtick-quote cy.origin()

Co-authored-by: Bill Glesias <[email protected]>

* Update README.md

backtick-quote .env

Co-authored-by: Bill Glesias <[email protected]>

---------

Co-authored-by: Bill Glesias <[email protected]>
  • Loading branch information
timheilman and AtofStryker authored Mar 4, 2024
1 parent 2094abd commit 2002af2
Show file tree
Hide file tree
Showing 15 changed files with 1,146 additions and 2,916 deletions.
4 changes: 2 additions & 2 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ VITE_BACKEND_PORT=3001
#VITE_OKTA_DOMAIN="dev-your-domain-id.okta.com"
#VITE_OKTA_CLIENTID="your-client-id"

# AWS Cognito #Okta Configuration to be added to .env when running "yarn dev:cognito"
# AWS Cognito Configuration to be added to .env when running "yarn dev:cognito"
# Additional config taken from aws-exports.js
#AWS_COGNITO_USERNAME="[email protected]"
#AWS_COGNITO_PASSWORD="s3cret1234$"
#AWS_COGNITO_DOMAIN="https://YOUR_COGNITO_INSTANCE.auth.us-east-1.amazoncognito.com"
#AWS_COGNITO_DOMAIN="https://YOUR_COGNITO_USER_POOL_HOSTED_UI_DOMAIN_PREFIX.auth.us-east-1.amazoncognito.com"

# Google Auth Configuration to be added to .env when running "yarn dev:google"
# client ID should look something like <identifier>.apps.googleusercontent.com
Expand Down
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,26 @@ A [guide has been written with detail around adapting the RWA](http://on.cypress
Prerequisites include an [Amazon Cognito][cognito] account. Environment variables from [Amazon Cognito][cognito] are provided by the [AWS Amplify CLI][awsamplify].
To start the application with Cognito, replace the current **src/index.tsx** file with the **src/index.cognito.tsx** file and start the application with `yarn dev:cognito` and run Cypress with `yarn cypress:open`.
- A user pool is required (identity pool is not used here)
- The user pool must have a hosted UI domain configured, which must:
- allow callback and sign-out URLs of `http://localhost:3000/`,
- allow implicit grant Oauth grant type,
- allow these OpenID Connect scopes:
- aws.cognito.signin.user.admin
- email
- openid
- The user pool must have an app client configured, with:
- enabled auth flow `ALLOW_USER_PASSWORD_AUTH`, only for programmatic login flavor of test.
- The `cy.origin()` flavor of test only requires auth flow `ALLOW_USER_SRP_AUTH`, and does not require `ALLOW_USER_PASSWORD_AUTH`.
- The user pool must have a user corresponding to the `AWS_COGNITO` env vars mentioned below, and the user's Confirmation Status must be `Confirmed`. If it is `Force Reset Password`, then use a browser to log in once at `http://localhost:3000` while `yarn dev:cognito` is running to reset their password.
The test knobs are in a few places:
- The `.env` file has `VITE_AUTH_TOKEN_NAME` and vars beginning `AWS_COGNITO`. Be careful not to commit any secrets.
- Both `scripts/mock-aws-exports.js` and `scripts/mock-aws-exports-es5.js` must have the same data; only their export statements differ. These files can be edited manually or exported from the amplify CLI.
- `cypress.config.ts` has `cognito_programmatic_login` to control flavor of the test.
To start the application with Cognito, replace the current **src/index.tsx** file with the **src/index.cognito.tsx** file and start the application with `yarn dev:cognito` and run Cypress with `yarn cypress:open`. `yarn dev` may need to have been run once first.
The **only passing spec on this branch** will be the [cognito spec](./cypress/tests/ui-auth-providers/cognito.spec.ts); all others will fail.
Expand Down
6 changes: 4 additions & 2 deletions backend/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,14 @@ export const verifyOktaToken = (req: Request, res: Response, next: NextFunction)

// Amazon Cognito Validate the JWT Signature
// https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html#amazon-cognito-user-pools-using-tokens-step-2
const userPoolId = awsConfig.Auth.Cognito.userPoolId;
const region = userPoolId.split("_")[0];
const awsCognitoJwtConfig = {
secret: jwksRsa.expressJwtSecret({
jwksUri: `https://cognito-idp.${awsConfig.aws_cognito_region}.amazonaws.com/${awsConfig.aws_user_pools_id}/.well-known/jwks.json`,
jwksUri: `https://cognito-idp.${region}.amazonaws.com/${userPoolId}/.well-known/jwks.json`,
}),

issuer: `https://cognito-idp.${awsConfig.aws_cognito_region}.amazonaws.com/${awsConfig.aws_user_pools_id}`,
issuer: `https://cognito-idp.${region}.amazonaws.com/${userPoolId}`,
algorithms: ["RS256"],
};

Expand Down
58 changes: 28 additions & 30 deletions cypress/support/auth-provider-commands/cognito.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,44 @@
import { Amplify, Auth } from "aws-amplify";
import { Amplify } from "aws-amplify";
import { fetchAuthSession, signIn } from "aws-amplify/auth";

Amplify.configure(Cypress.env("awsConfig"));

const fetchJwts = async (username: string, password: string) => {
const options = { authFlowType: "USER_PASSWORD_AUTH" as const };
await signIn({ username, password, options });
const authSession = await fetchAuthSession();
const tokens = authSession.tokens!;
const accessToken = tokens.accessToken;
const accessTokenPayload = accessToken.payload;
return {
idToken: tokens.idToken!.toString(),
accessToken: accessToken.toString(),
clientId: accessTokenPayload.client_id as string,
accessTokenSub: accessTokenPayload.sub!,
};
};
type JwtResponse = Awaited<ReturnType<typeof fetchJwts>>;

// Amazon Cognito
Cypress.Commands.add("loginByCognitoApi", (username, password) => {
Cypress.Commands.add("loginByCognitoApi", (username: string, password: string) => {
const log = Cypress.log({
displayName: "COGNITO LOGIN",
message: [`🔐 Authenticating | ${username}`],
// @ts-ignore
autoEnd: false,
});

log.snapshot("before");

const signIn = Auth.signIn({ username, password });
cy.wrap(fetchJwts(username, password), { log: false }).then((unknownJwts) => {
const { idToken, accessToken, clientId, accessTokenSub } = unknownJwts as JwtResponse;

cy.wrap(signIn, { log: false }).then((cognitoResponse: any) => {
const keyPrefixWithUsername = `${cognitoResponse.keyPrefix}.${cognitoResponse.username}`;
window.localStorage.setItem(
`${keyPrefixWithUsername}.idToken`,
cognitoResponse.signInUserSession.idToken.jwtToken
);
window.localStorage.setItem(
`${keyPrefixWithUsername}.accessToken`,
cognitoResponse.signInUserSession.accessToken.jwtToken
);
window.localStorage.setItem(
`${keyPrefixWithUsername}.refreshToken`,
cognitoResponse.signInUserSession.refreshToken.token
);
window.localStorage.setItem(
`${keyPrefixWithUsername}.clockDrift`,
cognitoResponse.signInUserSession.clockDrift
);
window.localStorage.setItem(
`${cognitoResponse.keyPrefix}.LastAuthUser`,
cognitoResponse.username
);
const keyPrefix = `CognitoIdentityServiceProvider.${clientId}`;
const keyPrefixWithUsername = `${keyPrefix}.${accessTokenSub}`;

window.localStorage.setItem("amplify-authenticator-authState", "signedIn");
const ls = window.localStorage;
ls.setItem(`${keyPrefixWithUsername}.idToken`, idToken);
ls.setItem(`${keyPrefixWithUsername}.accessToken`, accessToken);
ls.setItem(`${keyPrefix}.LastAuthUser`, accessTokenSub);

log.snapshot("after");
log.end();
Expand All @@ -60,9 +60,6 @@ Cypress.Commands.add("loginByCognito", (username, password) => {
});

cy.visit("/");
cy.contains("Sign in with AWS", {
includeShadowDom: true,
}).click();

cy.origin(
Cypress.env("cognito_domain"),
Expand All @@ -73,6 +70,7 @@ Cypress.Commands.add("loginByCognito", (username, password) => {
},
},
({ username, password }) => {
cy.contains("Sign in with your email and password");
// cognito log in page has some elements of the same id but are off screen. we only want the visible elements to log in
cy.get('input[name="username"]:visible').type(username);
cy.get('input[name="password"]:visible').type(password, {
Expand Down
7 changes: 4 additions & 3 deletions cypress/tests/ui-auth-providers/cognito.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import "../../support/auth-provider-commands/cognito";
import { isMobile } from "../../support/utils";
const apiGraphQL = `${Cypress.env("apiUrl")}/graphql`;

if (Cypress.env("cognito_username")) {
// Sign in with AWS
if (Cypress.env("cognito_programmatic_login")) {
describe("AWS Cognito", function () {
describe("AWS Cognito, programmatic login (cypress.config.ts#cognito_programmatic_login: true)", function () {
beforeEach(function () {
cy.task("db:seed");

cy.intercept("POST", "/bankAccounts").as("createBankAccount");
cy.intercept("POST", apiGraphQL).as("createBankAccount");

cy.loginByCognitoApi(Cypress.env("cognito_username"), Cypress.env("cognito_password"));
});
Expand Down Expand Up @@ -49,7 +50,7 @@ if (Cypress.env("cognito_username")) {
});
});
} else {
describe("AWS Cognito", function () {
describe("AWS Cognito, cy.origin() login (cypress.config.ts#cognito_programmatic_login: false)", function () {
beforeEach(function () {
cy.task("db:seed");
cy.loginByCognito(Cypress.env("cognito_username"), Cypress.env("cognito_password"));
Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
},
"dependencies": {
"@auth0/auth0-react": "2.2.4",
"@aws-amplify/ui-react": "^5.0.4",
"@babel/core": "7.23.9",
"@babel/plugin-syntax-flow": "^7.14.5",
"@babel/plugin-transform-react-jsx": "^7.14.9",
Expand All @@ -28,7 +27,7 @@
"@okta/okta-react": "^6.7.0",
"@types/detect-port": "^1.3.2",
"@xstate/react": "3.2.2",
"aws-amplify": "^5.3.3",
"aws-amplify": "^6.0.16",
"axios": "0.27.2",
"clsx": "1.2.1",
"date-fns": "2.30.0",
Expand Down
17 changes: 15 additions & 2 deletions scripts/mock-aws-exports-es5.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
// mock aws-exports-es5.js

const awsmobile = {
aws_cognito_region: "",
aws_user_pools_id: "",
Auth: {
Cognito: {
userPoolId: "us-east-1_abcdefghi",
userPoolClientId: "a1b2c3d4e5f6g7h8i9j0k1l2m",
loginWith: {
oauth: {
domain: "YOUR_COGNITO_USER_POOL_HOSTED_UI_DOMAIN_PREFIX.auth.us-east-1.amazoncognito.com",
scopes: ["email", "openid", "aws.cognito.signin.user.admin"],
redirectSignIn: ["http://localhost:3000/"],
redirectSignOut: ["http://localhost:3000/"],
responseType: "token",
},
},
},
},
};

exports.default = awsmobile;
17 changes: 15 additions & 2 deletions scripts/mock-aws-exports.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
// mock aws-exports.js

const awsmobile = {
aws_cognito_region: "",
aws_user_pools_id: "",
Auth: {
Cognito: {
userPoolId: "us-east-1_abcdefghi",
userPoolClientId: "a1b2c3d4e5f6g7h8i9j0k1l2m",
loginWith: {
oauth: {
domain: "YOUR_COGNITO_USER_POOL_HOSTED_UI_DOMAIN_PREFIX.auth.us-east-1.amazoncognito.com",
scopes: ["email", "openid", "aws.cognito.signin.user.admin"],
redirectSignIn: ["http://localhost:3000/"],
redirectSignOut: ["http://localhost:3000/"],
responseType: "token",
},
},
},
},
};

export default awsmobile;
67 changes: 36 additions & 31 deletions src/containers/AppCognito.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
import React, { useEffect } from "react";
import { useActor, useMachine } from "@xstate/react";
import { makeStyles } from "@material-ui/core/styles";
import { CssBaseline, Container } from "@material-ui/core";
import { CssBaseline } from "@material-ui/core";

import { snackbarMachine } from "../machines/snackbarMachine";
import { notificationsMachine } from "../machines/notificationsMachine";
import { authService } from "../machines/authMachine";
import AlertBar from "../components/AlertBar";
import { bankAccountsMachine } from "../machines/bankAccountsMachine";
import PrivateRoutesContainer from "./PrivateRoutesContainer";
import { Amplify } from "aws-amplify";
import { Authenticator, useAuthenticator } from "@aws-amplify/ui-react";
import { Amplify, ResourcesConfig } from "aws-amplify";
import { fetchAuthSession, signInWithRedirect, signOut } from "aws-amplify/auth";

// @ts-ignore
import awsConfig from "../aws-exports";

Amplify.configure(awsConfig);
Amplify.configure(awsConfig as ResourcesConfig);

// @ts-ignore
if (window.Cypress) {
Expand All @@ -39,35 +38,53 @@ const AppCognito: React.FC = /* istanbul ignore next */ () => {

const [, , bankAccountsService] = useMachine(bankAccountsMachine);

const { route, signOut, user } = useAuthenticator();
const isLoggedIn =
authState.matches("authorized") ||
authState.matches("refreshing") ||
authState.matches("updating");

useEffect(() => {
console.log("auth route: ", route);
if (route === "authenticated") {
authService.send("COGNITO", { user });
if (!isLoggedIn) {
fetchAuthSession().then((authSession) => {
if (authSession && authSession.tokens && authSession.tokens.accessToken) {
const { tokens, userSub } = authSession;
authService.send("COGNITO", {
accessTokenJwtString: tokens!.accessToken.toString(),
userSub: userSub!,
email: tokens!.idToken!.payload.email,
});
} else {
void signInWithRedirect();
}
});
}
}, [route, user]);
}, [isLoggedIn]);

useEffect(() => {
authService.onEvent(async (event) => {
if (event.type === "done.invoke.performLogout") {
console.log("AppCognito authService.onEvent done.invoke.performLogout");
if (
event.type === "done.invoke.performLogout" ||
// we want the client-side app to discard its JWTs even if server-side errors out:
event.type.startsWith("error.platform.authentication.logout")
) {
console.log(
"AppCognito authService.onEvent done.invoke.performLogout|error.platform.authentication.logout"
);
await signOut();
}
});
}, [signOut]);
}, []);

const isLoggedIn =
authState.matches("authorized") ||
authState.matches("refreshing") ||
authState.matches("updating");
if (!isLoggedIn) {
return null;
}

return isLoggedIn ? (
return (
<div className={classes.root}>
<CssBaseline />

<PrivateRoutesContainer
isLoggedIn={isLoggedIn}
isLoggedIn={true}
notificationsService={notificationsService}
authService={authService}
snackbarService={snackbarService}
Expand All @@ -76,18 +93,6 @@ const AppCognito: React.FC = /* istanbul ignore next */ () => {

<AlertBar snackbarService={snackbarService} />
</div>
) : (
<Container component="main" maxWidth="xs">
<CssBaseline />
<Authenticator>
{({ signOut, user }) => (
<main>
<h1>Hello {user?.username}</h1>
<button onClick={signOut}>Sign out</button>
</main>
)}
</Authenticator>
</Container>
);
};

Expand Down
10 changes: 5 additions & 5 deletions src/index.auth0.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ const onRedirectCallback = (appState: any) => {
const root = createRoot(document.getElementById("root")!);

/* istanbul ignore if */
if (process.env.VITE_APP_AUTH0) {
if (process.env.VITE_AUTH0) {
root.render(
<Auth0Provider
domain={process.env.VITE_APP_AUTH0_DOMAIN!}
clientId={process.env.VITE_APP_AUTH0_CLIENTID!}
domain={process.env.VITE_AUTH0_DOMAIN!}
clientId={process.env.VITE_AUTH0_CLIENTID!}
redirectUri={window.location.origin}
audience={process.env.VITE_APP_AUTH0_AUDIENCE}
scope={process.env.VITE_APP_AUTH0_SCOPE}
audience={process.env.VITE_AUTH0_AUDIENCE}
scope={process.env.VITE_AUTH0_SCOPE}
onRedirectCallback={onRedirectCallback}
cacheLocation="localstorage"
>
Expand Down
4 changes: 2 additions & 2 deletions src/index.cognito.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const theme = createTheme({

const root = createRoot(document.getElementById("root")!);

if (process.env.VITE_APP_AWS_COGNITO) {
if (process.env.VITE_AWS_COGNITO) {
/* istanbul ignore next */
root.render(
<Router history={history}>
Expand All @@ -26,5 +26,5 @@ if (process.env.VITE_APP_AWS_COGNITO) {
</Router>
);
} else {
console.error("Cogntio is not configured.");
console.error("Cognito is not configured.");
}
2 changes: 1 addition & 1 deletion src/index.google.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const theme = createTheme({

const root = createRoot(document.getElementById("root")!);

if (process.env.VITE_APP_GOOGLE) {
if (process.env.VITE_GOOGLE) {
/* istanbul ignore next */
root.render(
<Router history={history}>
Expand Down
Loading

0 comments on commit 2002af2

Please sign in to comment.