ReactJs — AWS Cognito Authentication with Auth0

Mario Mendes
11 min readNov 21, 2019

--

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

In your Cognito user pool, we will need to create a domain name so that we can leverage Cognito’s hosted-ui functionality. Doing this makes our lives a lot easier when it comes to authenticating with AWS via another idp.

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

Auth0 set-up and configuration

Assuming you already have set-up an auth0 application you should have a dashboard you can access.

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)

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:

  • Allowed Callback URLs: https://<cognito domain url>/oauth2/idpresponse
  • 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

Save your changes.

Cognito Indenitity Provider Configuration

Head back to Cognito and select the Identity Providers menu

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

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)

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>

Custom login page

By the time you have finished this, you should be presented with the following login page.

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

Not having the right configuration file can make implementing the login flow so much harder. If you used amplify cli to generate the config file you might not have to change it, but it’s worth double checking it is similar to what I ended up with in order to get this working, as my config file was manually generated.

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

As per request in the comments this is a sample of the package.json with the necessary dependencies

{
"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

To reuse the styling throughout the app, I created and exported a main theme. This lives in the root directory of the client

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

This is the main part that will enable us to customise our log-in page. Here we will create a component that extends the SingIn component from aws-amplify-react module. We do this as we still want to use the Authenticator HOC default logic and functionality.

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/>

./components/auth0LoginButton/index.js

Now we need to create the button that will trigger the log-in flow for auth0. In order to do this, we need to bypass the custom UI provided by Cognito and it’s HostedUI.

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

./components/googleLoginButton/index.js — The key difference here is the path that the button redirects to when clicked. Notice that the identity_provider query keyword is hard coded with the Google value. It is identical in everything else to the auth0 button apart from the button label, so much so, that it should probably be refactored into a component that takes parameters for the button label and identity provider value… but that is for another time.

/*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

Now to wrap things up nicely and use the Authenticator HOC, we will override the SignIn and Greetings components and pass in our own new CustomSignIn form. Hiding the Greetings component simply means removing a header which displays a logout button and the username of the logged in user. It doesn’t really break the flow if you keep it. Keep it if you don’t want the effort of creating your own header and logout buttons.

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.

--

--