Source code for this post can be found here
Not too long ago, writing a full authentication flow for an app, was a task that required a lot of effort and care, nowadays with tools like Aws amplify and modern UI libraries ( exp: ReactJS ), we are just hours away from building this essential functionality.
Let's start by creating an app with create react app
npx create-react-app my-login-app
Environment & prerequisites
Before we begin with all the other setups, make sure you have the following installed:
- Node.js v10.x or later installed
- A valid and confirmed AWS account
Installing and Initializing an AWS Amplify Project
NPM
$ npm install -g @aws-amplify/clicURL (Mac & Linux)
curl -sL https://aws-amplify.github.io/amplify-cli/install | >bash && $SHELLcURL (Windows)
curl -sL https://aws-amplify.github.io/amplify-cli/install->win -o install.cmd && install.cmd
Let's now configure the CLI with our credentials.
If you'd like to see a walkthrough of this configuration process, Nader Dabit has a video that show's how here.
$ amplify configure
- Specify the AWS Region: us-east-1 || us-west-2 || eu-central-1
- Specify the username of the new IAM user: your-user-name
> In the AWS Console, click Next: Permissions, Next: Tags, Next: Review, & Create User to create the new IAM user. Then return to the command line & press Enter.
- Enter the access key of the newly created user:
? accessKeyId: (<YOUR_ACCESS_KEY_ID>)
? secretAccessKey: (<YOUR_SECRET_ACCESS_KEY>)
- Profile Name: your-user-name
Let's initialize a new amplify setup by running: >$ amplify init
? Enter a name for the project myloginapp
? Enter a name for the environment dev
? Choose your default editor: Visual Studio Code
? Choose the type of app that you're building javascript
Please tell us about your project
? What javascript framework are you using react
? Source Directory Path: src
? Distribution Directory Path: build
? Build Command: npm run-script build
? Start Command: npm run-script start
Using default provider awscloudformation
For more information on AWS Profiles, see:
https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html
? Do you want to use an AWS profile? (Y/n) y
? Please choose the profile you want to use: your-user-name
Now, let's open the src/index.js
file and add the following lines:
import App from './App';
....
import Amplify from 'aws-amplify';
import awsconfig from './aws-exports';
Amplify.configure(awsconfig);
We now need to add our authentication service, Amplify uses Amazon Cognito as the main authentication provider, which gives all the tools to handle registration, authentication, account recovery & other operations.
Let's follow the next steps:
amplify add auth
β― Default configuration
Default configuration with Social Provider (Federation)
Manual configuration
I want to learn more.
How do you want users to be able to sign in? (Use arrow keys)
β― Username
Email
Phone Number
Email or Phone Number
I want to learn more.
Do you want to configure advanced settings? (Use arrow keys)
β― No, I am done.
Yes, I want to make some additional changes.
Finally, we can push our progress to our account so that AWS can know about it, by doing amplify push
, this will build all your local backend resources and provision them in the cloud.
Tailwind CSS
We want to have a nice looking design without spending too much time on the CSS side, let's install Tailwind CSS quickly by running:
npm install -D tailwindcss postcss autoprefixer
Then, let's do:
npx tailwindcss init -p
Inside the package.json
file let's add the following lines inside the scripts
object:
"scripts": {
"build:tailwind": "tailwindcss build src/tailwind.css -o src/tailwind.generated.css",
"prestart": "npm run build:tailwind",
"prebuild": "npm run build:tailwind",
.....
Let's create a new file inside our src folder src/tailwind.css
, where we are going to be importing tailwind default styles, add the following lines:
@tailwind base;
@tailwind components;
@tailwind utilities;
These two steps will generate a new tailwind.generated.css
file which we then need to import into the App.js
file in order to be able to use tailwind classes across the whole app.
import './App.css';
import './tailwind.generated.css';
Finally, let's clean our App.css
file by just leaving this amount of code:
.App {
text-align: center;
}
.App-header {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
}
React router
We are going to need the help of a router "system" to manage a few screens, lest's install react-router:
npm i react-router && react-router-dom
Let's also update the App.js
for the following:
import React from 'react';
import {
BrowserRouter as Router,
Switch,
Route,
} from "react-router-dom";
import './App.css'
function App() {
return (
<div className="App">
<header className="App-header">
<Router>
<Switch>
<div>
My app
</div>
</Switch>
</Router>
</header>
</div>
)
}
export default App
Time to create our first screen for the auth flow, on a component/Register.js
file, add the following code:
import React, { useState } from 'react';
import { Auth } from 'aws-amplify';
import Input from '../../common/Input';
import { Link, useHistory } from 'react-router-dom';
const Register = () => {
let history = useHistory();
const [user, setUser] = useState({ username: '', password: '', });
const handleInputChange = (event, keyName) => {
event.persist();
setUser((user) => {
return { ...user, [keyName]: event.target.value }
})
}
const signUp = async () => {
try {
await Auth.signUp({
username: user.username,
password: user.password,
attributes: {
email: user.username,
}
});
history.push("/confirm-register");
} catch (error) {
console.log('error', error);
}
}
return (
<div className="container w-4/12 w-medium">
<div className="bg-white shadow-xl rounded px-12 pt-6 pb-8 mb-4">
<h3 className="text-lg text-gray-700">Register</h3>
<Input
labelName='Email:'
value={user.username}
handleInputChange={(e) => handleInputChange(e, 'username')}
/>
<Input
labelName='Password:'
type="password"
value={user.password}
handleInputChange={(e) => handleInputChange(e, 'password')}
/>
<div className="flex items-center justify-between">
<button
className="mt-4 mb-4 w-full sm:w-auto border border-transparent px-6 py-3 text-base font-semibold leading-snug bg-gray-900 text-white rounded-md shadow-md hover:bg-gray-800 focus:outline-none focus:bg-gray-800 transition ease-in-out duration-150 hover:bg-gray-600"
type="button"
onClick={() => signUp()}
>
Send
</button>
</div>
<div className="w-full">
<hr />
<p className="text-gray-700 pb-2 pt-2 text-sm">You already habe an account?</p>
<Link
to={{
pathname: '/log-in'
}}
className="pt-2 text-sm text-blue-500 hover:text-blue-600"
>
Long in
</Link>
</div>
</div>
</div>
)
}
export default Register;
Create a common/Input.js
file for our small but practical input component.
import React from 'react';
const Input =({ labelName, value, type="text", handleInputChange }) => {
return (
<div className="pb-15">
<label className="block text-gray-700 text-sm font-bold mb-2">{labelName}</label>
<input
type={type}
className="account-input bg-white focus:outline-none focus:shadow-outline border border-gray-300 rounded-sm py-2 px-2 block w-full appearance-none leading-normal"
value={value}
onChange={handleInputChange}
/>
</div>
)
}
export default Input;
We are still not ready to test our app, after users add their register details (email and password), they will receive a confirmation email with a unique code to activate their account. Let's create a component/ConfirmRegister
screen for this step.
import { Auth } from 'aws-amplify';
import React, { useState } from 'react';
import Input from '../../common/Input';
import { Link, useHistory } from "react-router-dom";
const ConfirmRegister = () => {
let history = useHistory();
const [user, setUser] = useState({ username: '', authenticationCode: '', });
const handleInputChange = (event, keyName) => {
event.persist();
setUser((user) => {
return { ...user, [keyName]: event.target.value }
})
}
const confirmSignUp = async () => {
try {
await Auth.confirmSignUp(user.username, user.authenticationCode);
console.log('success confirm sign up');
history.push('./log-in')
} catch (error) {
console.log('error', error);
}
}
return (
<div className="container w-4/12 w-medium">
<div className="bg-white shadow-xl rounded px-12 pt-6 pb-8 mb-4">
<h3 className="text-lg text-gray-700">Confirm your account</h3>
<Input
labelName='Email:'
value={user.username}
handleInputChange={(e) => handleInputChange(e, 'username')}
/>
<Input
labelName='Code:'
value={user.authenticationCode}
handleInputChange={(e) => handleInputChange(e, 'authenticationCode')}
/>
<button
onClick={() => confirmSignUp()}
className="mt-4 mb-4 w-full sm:w-auto border border-transparent px-6 py-3 text-base font-semibold leading-snug bg-gray-900 text-white rounded-md shadow-md hover:bg-gray-800 focus:outline-none focus:bg-gray-800 transition ease-in-out duration-150 hover:bg-gray-600"
>
Confirm
</button>
<div>
<Link
to={{
pathname: '/register'
}}
className="pt-2 text-sm text-blue-500 hover:text-blue-600"
>
Back
</Link>
</div>
</div>
</div>
)
}
export default ConfirmRegister;
Our app is ready to start registering new accounts, you don't necessarily need to use your personal email, this brilliant 10 min email site can provide you a temporary one.
Now that we have registered user(s), let's create our components/Login.js
page by adding this code:
import { Auth } from 'aws-amplify';
import React, { useState } from 'react';
import { useHistory, Link } from "react-router-dom";
import Input from './common/Input';
const LogIn = () => {
let history = useHistory();
const [user, setUser] = useState({ username: '', password: '' });
const handleInputChange = (event, keyName) => {
event.persist();
setUser((user) => {
return { ...user, [keyName]: event.target.value }
})
}
const logIn = async () => {
try {
await Auth.signIn({
username: user.username,
password: user.password,
});
history.push('./home')
} catch (error) {
console.error('error', error);
}
}
return (
<div className="container w-4/12 w-medium">
<div className="bg-white shadow-xl rounded px-12 pt-6 pb-8 mb-4">
<h3 className="text-lg text-gray-800 mb-2">Log In</h3>
<Input
labelName='Email:'
value={user.username}
handleInputChange={(e) => handleInputChange(e, 'username')}
/>
<Input
labelName='Password:'
type="password"
value={user.password}
handleInputChange={(e) => handleInputChange(e, 'password')}
/>
<div className="flex items-center justify-between">
<button
onClick={() => logIn()}
className="mt-4 mb-4 w-full sm:w-auto border border-transparent px-6 py-3 text-base font-semibold leading-snug bg-gray-900 text-white rounded-md shadow-md hover:bg-gray-800 focus:outline-none focus:bg-gray-800 transition ease-in-out duration-150 hover:bg-gray-600"
>
Log in
</button>
</div>
<div className="w-full">
<hr />
<p className="text-gray-700 pb-2 pt-2 text-sm">Don't have an account?</p>
<Link
to={{
pathname: '/register'
}}
className="pt-2 text-sm text-blue-500 hover:text-blue-600"
>
Register
</Link>
</div>
</div>
</div>
)
}
export default LogIn;
After the user is login successful, we can finally grant them access to the home page.
Let's create a simple components/Home
page component:
import React from 'react'
import Auth from '@aws-amplify/auth';
import { Link } from "react-router-dom";
const Home = () => {
let signOut = async() => {
await Auth.signOut();
console.log("Sign out succesfully")
}
return (
<div>
<h2 className="px-3 mb-3 lg:mb-3 uppercase tracking-wide font-semibold text-sm lg:text-lg text-gray-900">
Home page
</h2>
<div className="ml-3 text-base">
<Link
to={{
pathname: '/log-in',
}}
onClick={signOut}
className="pt-2 text-sm text-gray-500 hover:text-gray-600"
>
Log out
</Link>
</div>
</div>
)
}
export default Home
We just need to put all these routes together to make the connection between pages with the help of react router, let's change our App.js
file to look like:
import React from 'react';
import {
BrowserRouter as Router,
Switch,
Route,
} from "react-router-dom";
import Login from './components/Login';
import Register from './components/Register';
import Home from './components/Home';
import ConfirmRegister from './components/ConfirmRegister';
import './App.css';
import './tailwind.generated.css';
function App() {
return (
<div className="App">
<header className="App-header">
<Router>
<Switch>
<Route component={Home} path="/home" />
<Route component={ConfirmRegister} path="/confirm-register" />
<Route component={Login} path="/log-in" />
<Route component={Register} path="/" />
</Switch>
</Router>
</header>
</div>
)
}
export default App
Finally, we can start testing our app, create accounts, logging in, etc! let's run npm start
or yarn start
, our registration page at http://localhost:3000
should be the first one to come up, looking like this:
But wait, this app is not fully done! someone can actually navigate to the home page (http://localhost:3000/home) without having an account or being authenticated, that's pretty bad!
Let's write a private route to solve this issue and secure our app, create a new components/PrivateRoute
file.
import React, { useState, useEffect } from 'react';
import { Redirect, Route } from "react-router-dom";
import { Auth } from 'aws-amplify';
import Homepage from './Home'
const PrivateRoute = ({ children, ...rest }) => {
const [signInUser, setSignInUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
let getUser = async() => {
try {
let user = await Auth.currentAuthenticatedUser();
await setSignInUser(user);
setIsLoading(false);
} catch (error) {
setIsLoading(false);
console.log(error)
}
}
getUser();
},[]);
if(isLoading) {
return <p>...Loading</p>
}
return (
<Route {...rest} render={({ location }) => {
return signInUser? <Homepage/>
: <Redirect to={{
pathname: '/log-in',
state: { from: location }
}} />
}} />
)
}
export default PrivateRoute;
In the App.js
file, let's "wrap" the home page with our private route component
.....
import PrivateRoute from './components/PrivateRoute';
import './App.css';
import './tailwind.generated.css';
function App() {
return (
<div className="App">
<header className="App-header">
<Router>
<Switch>
<PrivateRoute path="/home">
</PrivateRoute>
<Route component={ConfirmRegister} path="/confirm-register" />
<Route component={Login} path="/log-in" />
<Route component={Register} path="/" />
</Switch>
</Router>
</header>
</div>
)
}
export default App
Conclusion
We have our custom auth flow thanks mainly to AWS amplify and react, it even has a navigation security layer (the private route), all done in just a few steps.
Todo
You can do some homework by adding a forget password feature to the flow, let me know in the comments if you have questions.
If you think other people should read this post. Tweet, share and Follow me on twitter for the next articles
Top comments (4)
Thank you so much for this. I have been wrangling with cognito for weeks and this finally was a simple and effective, up to date guide.
Wow... I'm pretty new to react and I need to implement an auth flow soon so this article is not only timely but super helpful. Thank you.
But for me to use AWS amplify, should the account be for my company or for me personally?
Thanks for the comment Kehinde, the AWS account can be set for any of the two, hope that helps :)
Hey Rolando, how would you set up the PrivateRoutes if you wanted multiple "containers"/components to have them?