Amazon Cognito Authentication
What you'll learn
- Log in to Amazon Cognito through the UI with
cy.origin()
- Programmatically authenticate with Amazon Cognito via a custom Cypress command
- Adapting your Amazon Cognito application for programmatic authentication during testing
Authenticate by visiting a different domain with
cy.origin()
Typically, logging in a user within your app by authenticating via a third-party provider requires visiting a login page hosted on a different domain. Before Cypress v12.0.0, Cypress tests were limited to visiting domains of the same origin, making programmatic login the only option for authenticating users with a third-party API. As of Cypress v12.0.0, Cypress tests are no longer limited to visiting domains of a single origin, meaning you can easily authenticate to federated AWS Cognito via the UI!
What is Amazon Cognito?
Amazon Cognito is an authentication provider apart of Amazon Web Services (AWS).
It "lets you add user sign-up, sign-in, and access control to your web and mobile apps quickly and easily" and "scales to millions of users and supports sign-in with social identity providers, such as Facebook, Google, and Amazon, and enterprise identity providers via SAML 2.0."
Authentication with Amazon Cognito
The documentation for Amazon Cognito recommends using the AWS Amplify Framework Authentication Library from the AWS Amplify Framework to interact with a deployed Amazon Cognito instance.
Using the AWS Amplify Framework Authentication Library, we are able to programmatically drive the creation and authentication of users against a fully deployed back end.
This illustrates the limited code from the AWS Amplify Framework needed to programmatically log an existing a user into an application.
// Add 'aws-amplify' library into your application
// Configure Auth category with your Amazon Cognito credentials
Amplify.configure({
Auth: {
identityPoolId: 'XX-XXXX-X:XXXXXXXX-XXXX', // Amazon Cognito Identity Pool ID
region: 'XX-XXXX-X', // Amazon Cognito Region
},
})
// Call Auth.signIn with user credentials
Auth.signIn(username, password)
.then((user) => console.log(user))
.catch((err) => console.log(err))
Amazon Cognito Setup
If not already setup, you will need to create an account with Amazon Web Services (AWS).
An Amazon Cognito integration is available in the Cypress Real World App.
Clone the Cypress Real World App and install the AWS Amazon Amplify CLI as follows:
npm install -g @aws-amplify/cli
The Cypress Real World App is configured with an optional Amazon Cognito instance via the AWS Amplify Framework Authentication Library.
The AWS Amazon Amplify CLI is used to provision the Amazon Web Services (AWS) infrastructure needed to configure your environment and cloud resources.
First, run the amplify init command to initialize the Cypress Real World App. This will provision the project with your AWS credentials.
amplify init
Next, run the amplify push command to create the Amazon Cognito resources in the cloud:
amplify push
Note
Use the yarn dev:cognito
command when starting the
Cypress Real World App.
Setting Amazon Cognito app credentials in Cypress
First, we need to configure Cypress to use the
AWS Cognito environment variables set in the
.env
file. In addition, we are using the aws-exports.js
supplied during the
AWS Amplify CLI build process.
const { defineConfig } = require('cypress')
// Populate process.env with values from .env file
require('dotenv').config()
// AWS exports
const awsConfig = require('./aws-exports-es5.js')
module.exports = defineConfig({
env: {
cognito_username: process.env.AWS_COGNITO_USERNAME,
cognito_password: process.env.AWS_COGNITO_PASSWORD,
awsConfig: awsConfig.default
}
})
import { defineConfig } from 'cypress'
// Populate process.env with values from .env file
require('dotenv').config()
// AWS exports
const awsConfig = require('./aws-exports-es5.js')
export default defineConfig({
env: {
cognito_username: process.env.AWS_COGNITO_USERNAME,
cognito_password: process.env.AWS_COGNITO_PASSWORD,
awsConfig: awsConfig.default
}
})
The plugins file is no longer supported as of Cypress version 10.0.0.
We recommend that you update your configuration. Please see the plugins guide and the migration guide for more information.
// cypress/plugins/index.js
// Populate process.env with values from .env file
require('dotenv').config()
// AWS exports
const awsConfig = require('../../aws-exports-es5.js')
module.exports = (on, config) => {
config.env.cognito_username = process.env.AWS_COGNITO_USERNAME
config.env.cognito_password = process.env.AWS_COGNITO_PASSWORD
config.env.awsConfig = awsConfig.default
return config
}
Custom Command for Amazon Cognito Authentication
There are two ways you can authenticate to AWS Cognito:
cy.origin()
Login with Next, we'll write a custom command called loginByCognito
to perform a login to
Amazon Cognito. This command will use
cy.origin()
to
- navigate to the Cognito origin
- input user credentials
- sign in and redirect back to the Cypress Real World App
- cache the results with
cy.session()
// cypress/support/auth-provider-commands/cognito.ts
// Amazon Cognito
const loginToCognito = (username: string, password: string) => {
Cypress.log({
displayName: 'COGNITO LOGIN',
message: [`🔐 Authenticating | ${username}`],
autoEnd: false,
})
cy.visit('/')
cy.contains('Sign in with AWS', {
includeShadowDom: true,
}).click()
cy.origin(
Cypress.env('cognito_domain'),
{
args: {
username,
password,
},
},
({ username, 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, {
// use log: false to prevent your password from showing in the Command Log
log: false,
})
cy.get('input[name="signInSubmitButton"]:visible').click()
}
)
// give a few seconds for redirect to settle
cy.wait(2000)
// verify we have made it passed the login screen
cy.contains('Get Started').should('be.visible')
}
// right now our custom command is light. More on this later!
Cypress.Commands.add('loginByCognito', (username, password) => {
return loginToCognito(username, password)
})
Now, we can use our loginByCognito
command in the test. Below is our test to
login as a user via Amazon Cognito, complete
the onboarding process and logout.
The runnable version of this test is in the Cypress Real World App.
describe('Cognito', function () {
beforeEach(function () {
// Seed database with test data
cy.task('db:seed')
// login via Amazon Cognito via cy.origin()
cy.loginByCognito(
Cypress.env('cognito_username'),
Cypress.env('cognito_password')
)
})
it('shows onboarding', function () {
cy.contains('Get Started').should('be.visible')
})
})
Lastly, we can refactor our login command to take advantage of
cy.session()
to store our logged in user so we don't
have to reauthenticate with everything test.
// cypress/support/auth-provider-commands/cognito.ts
// Amazon Cognito
Cypress.Commands.add('loginByCognito', (username, password) => {
cy.session(
`cognito-${username}`,
() => {
return loginToCognito(username, password)
},
{
validate() {
cy.visit('/')
// revalidate our session to make sure we are logged in
cy.contains('Get Started').should('be.visible')
},
}
)
})
Programmatic Login
Next, we'll write a command to perform a programmatic login into
Amazon Cognito and set items in localStorage
with the authenticated users details, which we will use in our application code
to verify we are authenticated under test.
In this loginByCognitoApi
command, we call Auth.signIn
, then use that
response to set the items inside of localStorage for the UI to know that our
user is logged into the application.
// cypress/support/auth-provider-commands/cognito.ts
import Amplify, { Auth } from 'aws-amplify'
Amplify.configure(Cypress.env('awsConfig'))
// Amazon Cognito
Cypress.Commands.add('loginByCognitoApi', (username, password) => {
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(signIn, { log: false }).then((cognitoResponse) => {
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
)
window.localStorage.setItem('amplify-authenticator-authState', 'signedIn')
log.snapshot('after')
log.end()
})
cy.visit('/')
})
Finally, we can use our loginByCognitoApi
command in at test. Below is our
test to login as a user via Amazon Cognito,
complete the onboarding process and logout.
The runnable version of this test is in the Cypress Real World App.
describe('Cognito', function () {
beforeEach(function () {
// Seed database with test data
cy.task('db:seed')
// Programmatically login via Amazon Cognito API
cy.loginByCognitoApi(
Cypress.env('cognito_username'),
Cypress.env('cognito_password')
)
})
it('shows onboarding', function () {
cy.contains('Get Started').should('be.visible')
})
})
Adapting an Amazon Cognito App for Testing
Programmatic Login
Unlike programmatic login, authenticating with
cy.origin()
does not require adapting the application
to work. This step is only needed if implementing programmatic login.
The Cypress Real World App is used and provides configuration and runnable code for both the React SPA and the Express back end.
The front end uses the AWS Amplify Framework Authentication Library. The back end uses the express-jwt to validate JWTs from Amazon Cognito.
Adapting the back end
In order to validate API requests from the frontend, we install express-jwt and jwks-rsa and configure validation for JWT's from Amazon Cognito.
// backend/helpers.ts
// ... initial imports
import jwt from 'express-jwt'
import jwksRsa from 'jwks-rsa'
// ...
const awsCognitoJwtConfig = {
secret: jwksRsa.expressJwtSecret({
jwksUri: `https://cognito-idp.${awsConfig.aws_cognito_region}.amazonaws.com/${awsConfig.aws_user_pools_id}/.well-known/jwks.json`,
}),
issuer: `https://cognito-idp.${awsConfig.aws_cognito_region}.amazonaws.com/${awsConfig.aws_user_pools_id}`,
algorithms: ['RS256'],
}
export const checkCognitoJwt = jwt(awsCognitoJwtConfig).unless({
path: ['/testData/*'],
})
Once this helper is defined, we can use globally to apply to all routes:
// backend/app.ts
// initial imports ...
import { checkCognitoJwt } from './helpers'
// ...
if (process.env.REACT_APP_AWS_COGNITO) {
app.use(checkCognitoJwt)
}
// routes ...
Adapting the front end
We need to update our front end React app to allow for authentication with Amazon Cognito using the AWS Amplify Framework Authentication Library.
First, we create a AppCognito.tsx
container, based off of the App.tsx
component.
A useEffect
hook is added to get the access token for the authenticated user
and send an COGNITO
event with the user
and token
objects to work with the
existing authentication layer (authMachine.ts
). We use the
AmplifyAuthenticator
component to provide the login form from
Amazon Cognito.
// src/containers/AppOkta.tsx
// initial imports ...
import Amplify from "aws-amplify";
import { AmplifyAuthenticator, AmplifySignUp, AmplifySignIn } from "@aws-amplify/ui-react";
import { AuthState, onAuthUIStateChange } from "@aws-amplify/ui-components";
import awsConfig from "../aws-exports";
Amplify.configure(awsConfig);
// ...
const AppCognito: React.FC = () => {
// ...
useEffect(() => {
return onAuthUIStateChange((nextAuthState, authData) => {
if (nextAuthState === AuthState.SignedIn) {
authService.send("COGNITO", { user: authData });
}
});
}, []);
// ...
return isLoggedIn ? (
// ...
) : (
<Container component="main" maxWidth="xs">
<CssBaseline />
<AmplifyAuthenticator usernameAlias="email">
<AmplifySignUp slot="sign-up" usernameAlias="email" />
<AmplifySignIn slot="sign-in" usernameAlias="email" />
</AmplifyAuthenticator>
</Container>
);
};
export default AppCognito;
Try it out
The complete AppCognito.tsx component is in the Cypress Real World App.
Next, we update our entry point (index.tsx
) to use our AppCognito.tsx
component.
// src/index.tsx
// ... initial imports
import AppCognito from './containers/AppCognito'
// ...
if (process.env.REACT_APP_AWS_COGNITO) {
ReactDOM.render(
<Router history={history}>
<ThemeProvider theme={theme}>
<AppCognito />
</ThemeProvider>
</Router>,
document.getElementById('root')
)
}