Note: This repository is outdated and uses an obsolete version of amplify. Please see the docs at https://aws-amplify.github.io/docs/js/authentication for the updated CLI.
Get going with yarn install && yarn start
This is meant to demonstrate how to create an application AWS-Amplify with a focus on Authentication.
Some pages will require authentication, other will not require it.
Please note that I will be assuming familiarity with react and react-router. I will be assuming that you may be fairly new to AWS and the Amplify library, so we will touch on those elements in more detail.
- First, let's create an app. We'll be using create-react-app for this walkthrough. If you don't have it already you can install it by running
yarn global add create-react-app
in a terminal window. Oncecreate-react-app
is installed, run the following command to create a new application.
create-react-app amplify-auth-examples
cd amplify-auth-examples
- Next, we want to initialize our project using the
awsmobile
CLI. If you don't already have it, you can install by runningnpm install -g awsmobile
in a terminal window.
Before we go forward, we need to setup a user to act as the admin for our project so the CLI can access the things it needs (e.g. permissions to provision AWS services for us on our behalf). Head over to AWS IAM Management and login with your AWS credentials.
-
Next, click on
Users
on the left-hand menu. Click theAdd User
button at the top. Enter a username, likeamplify-auth-examples-admin
and select the checkbox next toProgrammatic access
for theAccess Type
. ClickNext
. -
On the next screen, click the
Create group
button beneath theAdd user to group
heading. For thegroup name
, enterAuthAdministrator
or something similar and select the checkbox next to theAdministratorAccess
item from the list. ClickCreate group
at the bottom of the screen to confirm and close the dialog. Click theNext:Review
button at the bottom. -
On the next screen, click the
Create user
button. -
You should see a success message at the top of the page confirming that our user was created successfully. At this point, you can download the CSV using the button provided, or copy the
Access key ID
andSecret access key
elsewhere. We will need them in just a minute. -
Back in your terminal, run the following command in the terminal to initialize
awsmobile
:
awsmobile init
This will prompt you multiple times, and you can press enter to accept the defaults for each of the first four prompts. After these prompts, it will say missing aws account settings
. It will ask if you want to configure aws account settings. Type Y
to accept.
This is where the Access key ID
and Secret access key
come into play. Paste the Access key ID
and Secret access key
when it promps for it. (If you don't have them or you mess up, you can just press Ctrl + C
(Cmd
on OSX) to exit and try again.) Use the arrow keys to select us-east-1
for the region and press enter. It will ask what name you would like to use for the project. Press enter to accept the default or choose your own. It will take a second to initialize your project and leave you back at the command-line.
- Next, we need to initailize
user-signin
to allow us to interact with AWS Cognito and provide authentication functionality for our users. Run the following commands in the terminal.
awsmobile user-signin enable
Press enter to accept the default.
awsmobile push
This command will sync your local configuration changes to your project in Mobile Hub.
Just to make sure everything is working, let's test the app by running yarn start
at the command line. The application should start and you should see the default create-react-app
splash page.
Next, we need to install react-router
, react-router-dom
, and the aws-amplify-react
library.
yarn add react-router react-router-dom aws-amplify-react
These get our libraries out of the way. We'll be focusing on App.js
from now on. Open that file and delete everything inside the div
tag with the className
of App
. In its place, just put an <h1>
tag with the words Home Page
. You can delete the logo
import as well. Your App.js
should look like this:
import React, { Component } from 'react';
import './App.css';
class App extends Component {
render() {
return (
<div className="App">
<h1>Amplify Routes Example</h1>
</div>
);
}
}
export default App;
We're not going to worry about creating separate files. I'll leave that as an exercise for the reader. :P
Next, we'll add a simple routes structure to demonstrate having different pages. Since I'm assuming some familiarity with react-router
, I'm not going to go as in depth on these. If you need a primer, you can check out this article or the official docs.
Add the Link
and Router
imports from react-router-dom
to the imports section on the page.
import { Link, BrowserRouter as Router } from 'react-router-dom';
Beneath the imports, add the following functional component.
const HeaderLinks = props => (
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/auth">Create Account/Login</Link>
</li>
<li>
<Link to="/secret">Secret Page</Link>
</li>
<li>
<Link to="/about">
About Page (we don't care if you're logged in or not)
</Link>
</li>
</ul>
);
Add the declaration for this component beneat the h1
in our App
declaration. We also need to add our Router
component so react-router
can compose the Link
component properly.
class App extends Component {
render() {
return (
<div className="App">
<h1>Amplify Routes Example</h1>
<HeaderLinks />
</div>
);
}
}
const AppWithRouter = () => (
<Router>
<App />
</Router>
);
export default AppWithRouter;
Now let's add some router outlet to our application. This will be the place where our different route components get injected in the page when we click on our header links.
Create a Routes
functional component with the following code. Place it right above the App
declaration.
class AuthComponent extends Component {
render() {
return <div>AuthComponent Section</div>;
}
}
const Routes = () => (
<Switch>
<Route exact path="/" render={() => <div>Home</div>} />
<Route exact path="/auth" render={AuthComponent} />
<Route
exact
path="/secret"
render={() => <div>Keep it secret! Keep it safe!</div>}
/>
<Route exact path="/about" render={() => <div>About Content</div>} />
</Switch>
);
We will come back to the AuthComponent
component in a bit to implement our authentication with aws-amplify
.
Place the declaration for this beneath the HeaderLinks
declaration in the App
.
class App extends Component {
render() {
return (
<div className="App">
<h1>Amplify Routes Example</h1>
<HeaderLinks />
<br />
<Routes />
</div>
);
}
}
If you run your app now, you should see that clicking on the links load the different pieces of content we have defined for each of our routes. However, we want the secret page to only be accessible to logged in users, and if someone tries to access it, they should be redirected to the auth
page to login or create an account. Let's implement this functionality.
We're going to implement a ProtectedRoute
component that prevents the user from accessing content unless they're logged in. This will follow the same structure as a Route
from react-router
; we will just be passing some additional information to it.
- Right above the
AuthComponent
component declaration, create aProtectedRoute
component with the sameRoute
as our secret route.
const ProtectedRoute = props => (
<Route
exact
path="/secret"
render={() => <div>Keep it secret! Keep it safe!</div>}
/>
);
- Because we will be using this in place of our
Route
, the elements that our specific to our component will be passed in. This means that we will replace them with the props of the component.
const ProtectedRoute = props => (
<Route exact={props.exact} path={props.path} render={props.render} />
);
- Replace the
Route
declaration for the secret page in ourRoutes
list and you should see that everything is working the same way as before.
const Routes = () => (
<Switch>
<Route exact path="/" render={() => <div>Home</div>} />
<Route exact path="/auth" render={AuthComponent} />
<ProtectedRoute
exact
path="/secret"
render={() => <div>Keep it secret! Keep it safe!</div>}
/>
<Route exact path="/about" render={() => <div>About Content</div>} />
</Switch>
);
- We have successfully replaced our
Route
with ourProtectedRoute
! Now we can work to implement the logic to redirect if the user is not logged in. This will take one more change to ourApp
component and theRoutes
component. Above therender
method of theApp
component, declare the following state object so we can track whether the user is logged in.
state = {
authState: {
isLoggedIn: false
}
};
- Let's pass this to our
Routes
component in ourApp
as anauthState
prop.
...
<div className="App">
<h1>Amplify Routes Example</h1>
<HeaderLinks />
<br />
<Routes authState={this.state.authState} />
</div>
...
- Next, update our
Routes
declaration to accept these new props and pass it down to ourProtectedRoute
.
const Routes = ({ authState }) => (
<Switch>
<Route exact path="/" render={() => <div>Home</div>} />
<Route exact path="/auth" render={AuthComponent} />
<ProtectedRoute
exact
path="/secret"
render={() => <div>Keep it secret! Keep it safe!</div>}
props={authState}
/>
<Route exact path="/about" render={() => <div>About Content</div>} />
</Switch>
);
- Now we need to implement our redirect. If the user is logged in, we want to render the component we passed in. If the user is not logged in, we want to redirect them to the auth page and store the redirect URL so we can let them to continue to the intended page after they have logged in.
Add the Redirect
import to our react-router-dom
import at the top of App.js
.
import { Link, BrowserRouter as Router, Switch, Route, Redirect } from 'react-router-dom';
- Update the ProtectedRoute declaration to handle our new state that's passed in.
const ProtectedRoute = props => (
<Route
exact={props.exact}
path={props.path}
render={rProps =>
props.props.isLoggedIn ? (
<props.render exact={props.exact} />
) : (
<Redirect
to={`/auth?redirect=${props.location.pathname}${
props.location.search
}`}
/>
)
}
/>
);
If you try your app now, you should see that clicking the Secret Page link should change the URL and content to render the auth
route, while keeping the redirect set to the secret
route. Change the isLoggedIn
state to true to test what happens if the user isLoggedIn
. This time, you should pass straight to the secret
route without any redirect.
- Finally, we need to clean up from our component evolution. It works as is, but we can use destructuring to clarify our meaning and make it look prettier. Let's start by descturing out the props that we care about.
const ProtectedRoute = ({ render: C, props: childProps, ...rest }) => (
...
Here, we have taken the render
prop that contains our div
, and rename it to C
. This is the Component that we eventually want to render. We rename props
to childProps
because we're going to pass it to the child component that we will eventually render (in this case, C
). Finally, we take all the other props and assign them to the rest
variable using the spread operator.
The following snippet is the entire component. It uses the C
declaration to create a component and spread the rProps
from the render
prop and the childProps
from our parent component so that our final rendered component has everything that we passed down to begin with. The redirect is the same as before.
const ProtectedRoute = ({ render: C, props: childProps, ...rest }) => (
<Route
{...rest}
render={rProps =>
childProps.isLoggedIn ? (
<C {...rProps} {...childProps} />
) : (
<Redirect
to={`/auth?redirect=${rProps.location.pathname}${
rProps.location.search
}`}
/>
)
}
/>
);
This gives us a good foundation to implement our AuthComponent
component using aws-amplify
. Let's continue.
Let's change our focus to the AuthComponent component we stubbed our earlier. It should look like this.
class AuthComponent extends Component {
render() {
return <div>Auth Section</div>;
}
}
That's fairly simple. Let's add the basic aws-amplify
Authenticator
component. We have to add it to the imports at the top of the page.
import { Authenticator } from 'aws-amplify-react';
Replace our Auth Section
text in the AuthComponent
with the Authenticator
declaration.
class AuthComponent extends Component {
render() {
return (
<div>
<Authenticator />
</div>
);
}
}
You should see the component on the page when you go to the auth route.
The nice part about Amplify is that this is all there is to it. We now have Authentication available to us in a simple way. However, we are diverging from the more common use case of wrapping our entire app with this component. This will enable us to have certain sections of the app where the user need not be logged in to interact with our application. We want to be able to communicate our authenticated state to the rest of the app. This is where the amplify library can also help us. It provides a hook for handling the change of state that occurs with this component. It's called onStateChange
. Let's implement that now.
Inside your AuthComponent
component declaration, create a method called handleStateChange
. This is what will take in the state of the component and allow us to check against it. The code for this method is below.
class AuthComponent extends Component {
handleStateChange = state => {
if (state === 'signedIn') {
// handle state change
}
};
render() {
return (
<div>
<Authenticator />
</div>
);
}
}
Update the Authenticator
to use this method.
class AuthComponent extends Component {
handleStateChange = state => {
if (state === 'signedIn') {
// handle state change
}
};
render() {
return (
<div>
<Authenticator onStateChange={this.handleStateChange} />
</div>
);
}
}
This should be all we need to enable the creation of a user. However, if you try, you will get an error that says No userPool
. This is because we have not yet told the amplify library to use the configuration we setup earlier. Let's do that now.
Import the Amplify
and Auth
libraries from aws-amplify
.
import Amplify, { Auth } from 'aws-amplify';
We need to also import the aws_exports configuration that was created for us when we ran awsmobile init
.
import aws_exports from './aws-exports';
Now, we can use that configuration file to configure the Amplify
library. Add the following line just below the imports at the top of App.js
.
Amplify.configure(aws_exports);
With this out of the way, head over to the auth page and use the Authenticator to create a user. The default settings have multi-factor authentication (MFA) turned on, so you'll go through a couple text messages to sign up and login, and if successful, you should see a greeting like this.
Success!
Let's add a console log statement to our handleStateChange
function to see what this component is doing.
...
handleStateChange = state => {
console.log(state);
if (state === 'signedIn') {
// handle state change
}
};
Try logging in, and watch the console. You should see confirmSignIn
and signedIn
events that are passed to our handleStateChange
function.
Now that we can get access to our state, let's implement the hook that allows us to tell the rest of the app when we are logged in.
Recall from earlier that we have our isLoggedIn
state available at the application level. This is what we can pass to our components to let us know if the user is logged in. We need to be able to update this piece of state, so we need to pass a function to our AuthComponent
to let us do this.
- In the App component, create a function called
handleUserSignIn
.
handleUserSignIn = () => {
this.setState({ authState: { isLoggedIn: true } });
};
- Replace the render method with the following snippet. This will collect our multiple props into a childProps object that we can pass into our routes.
render() {
const childProps = {
isLoggedIn: this.state.authState.isLoggedIn,
onUserSignIn: this.handleUserSignIn
};
return (
<div className="App">
<h1>Amplify Routes Example</h1>
<HeaderLinks />
<br />
<Routes childProps={childProps} />
</div>
);
}
- Before we can pass our function to our auth route, we need to create a route that accepts props. This will be very similar to our authenticated route, but we won't care whether the user is logged in or not. Create a new functional component called
ProppedRoute
right beneat theProtectedRoute
.
const ProppedRoute = ({ render: C, props: childProps, ...rest }) => (
<Route {...rest} render={rProps => <C {...rProps} {...childProps} />} />
);
- Update the
Routes
component to accept the newchildProps
prop and pass it to the auth route, which we have changed to be our newProppedRoute
component.
const Routes = ({ childProps }) => (
<Switch>
<Route exact path="/" render={() => <div>Home</div>} />
<ProppedRoute
exact
path="/auth"
render={AuthComponent}
props={childProps}
/>
<ProtectedRoute
exact
path="/secret"
render={() => <div>Keep it secret! Keep it safe!</div>}
props={childProps}
/>
<Route exact path="/about" render={() => <div>About Content</div>} />
</Switch>
);
- Now that we have a function we can use to alert the app that we're signed in, let's wire it up. In the
AuthComponent
, replace the//handle state change
comment with a call to theonUserSignIn
function prop.
handleStateChange = state => {
console.log(state);
if (state === 'signedIn') {
this.props.onUserSignIn();
}
};
- To check that it worked, add this snippet beneath the
HeaderLinks
component to let us know the status of the user. Sign out and refresh the app. Head to the auth page and sign in. You should see the text change from 'Not Logged In' to 'User is Logged In', confirming that our state update took place!
<div>
{this.state.authState.isLoggedIn ? 'User is Logged In' : 'Not Logged In'}
</div>
I hope this was a clear and concise introduction to using aws-amplify
for authentication between different types of routes. If you noticed, there's actually very little hanging off amplify
itself. The key is using the library to abstract out the tedious bits, and hang your application state off of it to utilize wherever you need it. This example should serve as a great baseline to expand upon when you need different elements of authentication state among different components and routes.
The code for this walkthrough in its entirety can be found here.
The text in markdown format is available here
I can be reached on Twitter @mwarger - hit me up for any questions or if you see any problems with this example.