ReactJs — AWS Cognito Authentication with Auth0

Having recently worked on a react.js project that required the use of auth0 as an identity provider hence forth referred to as an idp — along with the use of AWS Amplify on the client and AWS Appsync for the backend, I found that the documentation on Amplify was somewhat limited for my needs. If however all you need is to use auth0 and Amplify to authenticate with AWS via STS, as a federated user, then the tutorial found on their documentation page is a good place to start, yet still lacks some slight nuances that require some extra research.

If what you want is a full user flow, authenticating with AWS as you would using Cognito directly, returning user attributes in the user object, then this post will be a good starting point.

This post will cover the configuration needed on auth0 and Cognito, along with the configuration object necessary for amplify on the client, which is generally generated automatically if you use Amplify cli (mine is manually generated) and the implementation of a custom login form and button for auth0, all the while using Cognito’s Hosted UI’s logic in the background, but by passing the actual UI provided by Cognito.

I will show you how to extend the functionality of the Authenticator Higher Order Component’s SignIn method from Amplify, covered in this amazing post by Kyle Galbraith with very slight changes that I needed to make to fit my needs.

This post assumes some basic knowledge of AWS services and ReactJs with Material-Ui.

Cognito Domain Name

If you already have a domain name defined, then copy it and skip to the next section.

The name has to be unique, but that is the only restriction. Copy the full url of your domain prefix once you have created and saved it. It will be necessary in the next steps when configuring our auth0 application.

The url format is https://<yourDomainName>.aws_region.amazoncognito.com

Image for post
Image for post

Auth0 set-up and configuration

Image for post
Image for post

Head over to your Applications menu (in the list on the right) and select the application you wish to authenticate with. You will need at least one user in that application before you can use it.

The application type used for this tutorial is an SPA (Single Page Application)

Image for post
Image for post

Select your application.

Scroll down until you get to the boxes that say Allowed Callback URLs, Application Login URI, Allowed Web Origins, Allowed Logout URLs and Allowed Origins (CORS).

Here we will be constructing the urls necessary to allow auth0 to talk to Cognito and vice-versa. Note that multiple urls can be added to the boxes as a comma separated list. This is useful for development purposes.

Url formats:

  • Application Login URI: https://<cognito domain url>login?response_type=code&client_id=<user pool web client id>&redirect_uri=<your application url>
  • Allowed Web Origins: Your application url, i.e. http://localhost:3000
  • Allowed Logout URLs: https://<cognito domain url>logout?response_type=code&client_id=<user pool web client id>&redirect_uri=<your application url>
  • Allowed Origins (CORS): Your application url, i.e. http://localhost:3000
Image for post
Image for post
Image for post
Image for post

Save your changes.

Cognito Indenitity Provider Configuration

Image for post
Image for post

Then select the OpenId Connect tab on the right. You should have something like this is you haven’t configured one already.

Image for post
Image for post

Give it a Provider Name. This name will later be used by Cognito to automatically create a group in your user pool when your first user logs in via auth0.

From your application dashboard on auth0, you should get the values for client ID, client secret and the domain of your application (this value plus the protocol, i.e. https:// will be needed to fill in the Issuer in the above menu)

Image for post
Image for post

Set the attributes request method to POST.

For Authorize Scope paste the following: email profile openid email_verified

Use the domain of you application retrieved from the first step and prefix it with https:// . Paste this value into the Issuer box.

Save it.

The Attribute Mapping is entirely optional and will not break the authorisation flow, but I recommend that you configure it!

Open the Attribute Mapping menu and configure as per the image below.

Not doing this will mean that Cognito will not get any user attributes from auth0. This means that on the client side, when you try to access the signed in user’s details, all you will get will be an autogenerated username that is constructed using the idp name and a userid. Mapping name to preferred_username in the Attribute Mapping menu, will allow us to at least access a humanly readable username from the user object in the client. The subusername attribute is mapped for us and cannot be changed. This is because Cognito requires unique usernames.

In the App Client Setting page, make sure you have selected the newly created idp and filled in the values for the callback and sing-out urls. Their values are the url of your app. If you’re running the app locally that would be http://localhost:<port>

Image for post
Image for post

Custom login page

Image for post
Image for post

The Log In With Google button is not covered by this post, but the implementation logic is very similar to that of auth0.

AWS_CONFIG

const AWS_CONFIG = {
"aws_appsync_authenticationType": "AMAZON_COGNITO_USER_POOLS",
...,
...,
...,
"aws_user_pools_web_client_id": "...",
"oauth": {
"domain": "<domainName>.auth.<region>.amazoncognito.com",
"scope": ["email", "profile", "openid", "aws.cognito.signin.user.admin"],
"redirectSignIn": 'http://localhost:3000',
"redirectSignOut": 'http://localhost:3000',
"responseType": "code",
"auth0_identity_provider": "auth0test" // this is not necessary for the oauth to work. It has simply been placed here for convenience of access and is needed for the redirect.
}
}

Package.json

{
"name": "example-app",
"version": "0.1.0",
"dependencies": {
"@date-io/moment": "^1.3.13",
"@fortawesome/fontawesome-svg-core": "^1.2.29",
"@fortawesome/free-solid-svg-icons": "^5.13.1",
"@material-ui/core": "^4.9.10",
"@material-ui/pickers": "^3.2.10",
"aws-amplify": "^1.3.3",
"aws-amplify-react": "^2.6.3",
"react": "^16.13.1",
"react-redux": "^7.2.0",
"redux": "^4.0.5",
"typeface-roboto": "^0.0.54",

},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}

App.js

/*global AWS_CONFIG */
/*eslint no-undef: "error"*/
import React, {Component} from 'react';
import { MuiThemeProvider, withStyles } from '@material-ui/core/styles';
import { MuiPickersUtilsProvider } from '@material-ui/pickers';
import MomentUtils from '@date-io/moment';
import theme from './themes'

import 'typeface-roboto';

import { connect } from 'react-redux';
import API from './api/appsync';

import { library } from '@fortawesome/fontawesome-svg-core';
import { faLink } from '@fortawesome/free-solid-svg-icons';

import Amplify from 'aws-amplify';

library.add(faLink)

Amplify.configure(AWS_CONFIG);

const styles = theme => ({
root: {
display: 'flex',
flex: 1,
justifyContent: 'center',
padding: 20,
maxWidth: 1000,
margin: '0 auto',
flexDirection: 'column'
},
headerRoot: {
position: 'sticky',
top: 0,
paddingBottom: 3,
zIndex: theme.zIndex.appBar,
backgroundColor: theme.palette.background.default
},
paper: {
marginTop: theme.spacing(0),
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}
});

class App extends Component {
async componentDidMount() {
// ... do something here
}

render() {
if (this.props.authState === "signedIn") {
return (
<MuiThemeProvider theme={theme}>
<MuiPickersUtilsProvider utils={MomentUtils}>
<div>
Hello!!
</div>
</MuiPickersUtilsProvider>
</MuiThemeProvider>
)
} else {
return null;
}
}
}

export default connect()(withStyles(styles)(App));

By not using any of amplify’s HOC’s directly for the sign in, we have to manually manage the user state on this page. We achieve this with the value from this.props.authState.

./themes/index.js

import {createMuiTheme} from '@material-ui/core/styles';

const theme = createMuiTheme({
typography: { useNextVariants: true },
palette: {
primary: {
light: '#FD8AFD',
main: '#a918fe',
dark: '#7208fe',
contrastText: '#ffffff',
}
},
});

export default theme;

./components/loginForm/index.js

All of the details behind this form can be found here.

import React from 'react';
import Avatar from '@material-ui/core/Avatar';
import Button from '@material-ui/core/Button';
import CssBaseline from '@material-ui/core/CssBaseline';
import LockOutlinedIcon from '@material-ui/icons/LockOutlined';
import Typography from '@material-ui/core/Typography';
import TextField from '@material-ui/core/TextField';
import Link from '@material-ui/core/Link';
import Grid from '@material-ui/core/Grid';
import Auth0LoginButton from './../auth0LoginButton'
import GoogleLoginButton from './../googleLoginButton'
import {MuiThemeProvider, withStyles} from '@material-ui/core/styles';
import Container from '@material-ui/core/Container';

import { SignIn } from 'aws-amplify-react';
import theme from '../../themes';
import {MuiPickersUtilsProvider} from '@material-ui/pickers';
import MomentUtils from '@date-io/moment';

const styles = theme => ({
'@global': {
body: {
backgroundColor: theme.palette.common.white,
},
},
paper: {
marginTop: theme.spacing(8),
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
},
avatar: {
margin: theme.spacing(1),
backgroundColor: theme.palette.secondary.main,
},
form: {
width: '100%', // Fix IE 11 issue.
marginTop: theme.spacing(1),
'&:focus': {
borderColor: 'primary.dark'
}
},
submit: {
margin: theme.spacing(3, 0, 2),
background: 'primary.main',
'&:hover': {
backgroundColor: 'primary.dark',
}
},
link: {
color: 'primary.main'
},
progress: {
display: "flex",
justifyContent: "center",
margin: '25% auto'
}
});

class CustomSignIn extends SignIn {
constructor(props) {
super(props);
this._validAuthStates = ["signIn", "signedOut", "signedUp"];
}

showComponent() {
const {classes} = this.props;

if (window.location.search.includes('code')) {
return (
<div className={classes.progress}>
Loading...
</div>
)
}

return (
<MuiThemeProvider theme={theme}>
<MuiPickersUtilsProvider utils={MomentUtils}>
<Container component="main" maxWidth="xs">
<CssBaseline />
<div className={classes.paper}>
<Avatar className={classes.avatar}>
<LockOutlinedIcon />
</Avatar>
<Typography component="h1" variant="h5">
Sign In
</Typography>
<div className={classes.form} >
<Auth0LoginButton/>
<GoogleLoginButton/>
<form className={classes.form} noValidate>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
id="username"
label="Username"
name="username"
autoComplete="username"
onChange={this.handleInputChange}
/>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="current-password"
onChange={this.handleInputChange}
/>
<Button
type="button"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
onClick={() => super.signIn()}
>
Sign In With COGNITO
</Button>
<Grid container>
<Grid item xs>
<Link className={classes.link} href="#" variant="body2" onClick={() => super.changeState("forgotPassword")}>
Forgot password?
</Link>
</Grid>
<Grid item>
<Link className={classes.link} href="#" variant="body2" onClick={() => super.changeState("signUp")}>
{"Don't have an account? Sign Up"}
</Link>
</Grid>
</Grid>
</form>
</div>
</div>
</Container>
</MuiPickersUtilsProvider>
</MuiThemeProvider>
);
}
}

export default withStyles(styles)(CustomSignIn)

Notice how we have imported the Auth0LoginButton import Auth0LoginButton from ‘./../auth0LoginButton’ and then later used it to insert the button into our page <Auth0LoginButton/>

Image for post
Image for post

./components/auth0LoginButton/index.js

The beauty about this is that we will not have to handle redirects and tokens that we would need to otherwise when manually authenticating. We are going to be cheeky and take the users directly to the url that the HostedUi would eventually take us to, where everything else will be handled for us!

/*global AWS_CONFIG */
/*eslint no-undef: "error"*/
import React, {Component} from 'react';
import Button from '@material-ui/core/Button';
import CssBaseline from '@material-ui/core/CssBaseline';
import { withStyles } from '@material-ui/core/styles';
import Container from '@material-ui/core/Container';

const styles = theme => ({
submit: {
margin: theme.spacing(1, 0, 1)
},
});

function loginToAuth0() {
const {
oauth: {
domain,
scope,
responseType,
auth0_identity_provider,
redirectSignIn
},
aws_user_pools_web_client_id
} = AWS_CONFIG;
const url = new URL(`https://${domain}/oauth2/authorize?identity_provider=${auth0_identity_provider}&redirect_uri=${redirectSignIn}&response_type=${responseType.toUpperCase()}&client_id=${aws_user_pools_web_client_id}&scope=${scope.join(' ')}`);
window.location.assign(url.toString());
}

class Auth0LoginButton extends Component {
render() {
const {classes} = this.props;

return (
<Container component="main" maxWidth="xs">
<CssBaseline />
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
onClick={() => loginToAuth0()}
className={classes.submit}
>
Sign In With Auth0
</Button>
</Container>
);
}
}

export default withStyles(styles)(Auth0LoginButton)

This component is pretty simple. All it does apart from creating the button, is it forms the url we need to access to initiate the whole auth0 authentication flow. This is managed by the loginToAuth0 method.

Bonus — Google login Button

/*global AWS_CONFIG */
/*eslint no-undef: "error"*/
import React, {Component} from 'react';
import Button from '@material-ui/core/Button';
import CssBaseline from '@material-ui/core/CssBaseline';
import { withStyles } from '@material-ui/core/styles';
import Container from '@material-ui/core/Container';

const styles = theme => ({
submit: {
margin: theme.spacing(1, 0, 1)
},
});

function loginToGoogle() {
const {
oauth: {
domain,
scope,
responseType,
redirectSignIn
},
aws_user_pools_web_client_id
} = AWS_CONFIG;
const url = new URL(`https://${domain}/oauth2/authorize?identity_provider=Google&redirect_uri=${redirectSignIn}&response_type=${responseType.toUpperCase()}&client_id=${aws_user_pools_web_client_id}&scope=${scope.join(' ')}`);
window.location.assign(url.toString());
}

class GoogleLoginButton extends Component {
render() {
const {classes} = this.props;

return (
<Container component="main" maxWidth="xs">
<CssBaseline />
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
onClick={() => loginToGoogle()}
className={classes.submit}
>
Sign In With Google
</Button>
</Container>
);
}
}

export default withStyles(styles)(GoogleLoginButton)

./index.js

import "@babel/polyfill";
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './index.css';
import * as serviceWorker from './serviceWorker';
import CustomSignIn from "./components/loginForm";

import { Provider } from 'react-redux';
import { createStore } from 'redux'
import { SignIn, Authenticator, Greetings } from 'aws-amplify-react';

const store = createStore((state={}, action) => {return state});

ReactDOM.render(
<Provider store={store}>
<Authenticator hide={[SignIn, Greetings]}>
<CustomSignIn />
<App />
</Authenticator>
</Provider>,
document.getElementById('root')
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

That’s about it!

Alternatively, if you don’t want a custom sign-in page, simply remove hide={[SignIn, Greetings] and <CustomSignIn /> from the ./index.js file and that should do the trick. The Authenticator HOC should then handle everything for you as long as you have configured everything else correctly.

Please leave any question you might have in the comments section and I hope this has helped.

NodeJs Enthusiast

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store