React Authentication

Part 3, Lesson 8



Let's add some methods to handle a user signing up, logging in, and logging out...


With the Form component set up, we can now configure the methods to:

  1. Handle form submit event
  2. Obtain user input
  3. Send AJAX request
  4. Update the page

These steps should look familiar since we already went through this process in the React Forms lesson. Put yourself to test and implement the code yourself before going through this lesson.

Handle form submit event

Turn to Form.jsx. Which method gets fired on the form submit?

<form onSubmit={(event) => props.handleUserFormSubmit(event)}>

Add the method to the App component:

handleUserFormSubmit(event) {
  event.preventDefault();
  console.log('sanity check!')
}

And then pass it down via the props:

<Route exact path='/register' render={() => (
  <Form
    formType={'Register'}
    formData={this.state.formData}
    handleUserFormSubmit={this.handleUserFormSubmit.bind(this)}
  />
)} />
<Route exact path='/login' render={() => (
  <Form
    formType={'Login'}
    formData={this.state.formData}
    handleUserFormSubmit={this.handleUserFormSubmit.bind(this)}
  />
)} />

Test it out in the browser. You should see sanity check! in the JavaScript console on form submit for both forms. Remove the console.log('sanity check!') when done.

Obtain user input

Next, to get the user inputs, add the following method to App:

handleFormChange(event) {
  const obj = this.state.formData;
  obj[event.target.name] = event.target.value;
  this.setState(obj);
}

Pass it down on the props:

handleFormChange={this.handleFormChange.bind(this)}

Add a console.log() to the method - console.log(this.state.formData); - to ensure it works when you test it in the browser. Remove it once done.

What's next? AJAX!

Send AJAX request

Update the handleUserFormSubmit method to send the data to the user service on a successful form submit:

handleUserFormSubmit(event) {
  event.preventDefault();
  const formType = window.location.href.split('/').reverse()[0];
  let data;
  if (formType === 'login') {
    data = {
      email: this.state.formData.email,
      password: this.state.formData.password
    }
  }
  if (formType === 'register') {
    data = {
      username: this.state.formData.email,
      email: this.state.formData.email,
      password: this.state.formData.password
    }
  }
  const url = `${process.env.REACT_APP_USERS_SERVICE_URL}/auth/${formType}`
  axios.post(url, data)
  .then((res) => {
    console.log(res.data);
  })
  .catch((err) => { console.log(err); })
}

Test the user registration out. If you have everything set up correctly, you should see an object in the JavaScript console with an auth token:

{
  auth_token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0OTc3NTM2ODMsImlhdCI6MTQ5Nzc1MzY3OCwic3ViIjo0fQ.vcRFb5v3znHkz8An12QUxrgXsLqoKv93kIsMf-pdfVw",
  message: "Successfully registered.",
  status: "success"
}

Test logging in as well. Again, you should see the very same object in the console.

Update the page

After a user register or logs in, we need to:

  1. Clear the formData object
  2. Save the auth token in the browser's LocalStorage, a client-side data store
  3. Update the state to indicate that the user is authenticated
  4. Redirect the user to /

First, to clear the form, update the .then within handleUserFormSubmit():

.then((res) => {
  this.setState({
    formData: {username: '', email: '', password: '' },
    username: '',
    email: ''
  });
})

Try this out. After you register or log in, the field inputs should be cleared since we set the properties in the formData object to empty strings.

What happens if you enter data for the registration form but don't submit it and then navigate to the login form? The fields should remain. Is this okay? Should we clear the state on page load? Your call. You could simply update the state within the componentWillMount lifecycle method.

Next, let's save the auth token in LocalStorage so that we can use it for subsequent API calls that require a user to be authenticated. To do this, add the following code to the .then, just below the setState:

window.localStorage.setItem('authToken', res.data.auth_token);

Try logging in again. After a successful login, open the Application tab within Chrome DevTools. Click the arrow before LocalStorage and select http://localhost:3000. You should see a key of authToken with a value of the actual token in the pane.

Instead of always checking LocalStorage for the auth token, let's add a boolean to the state so we can quickly tell if there is a user authenticated.

Add an isAuthenticated property to the state:

this.state = {
  users: [],
  username: '',
  email: '',
  title: 'TestDriven.io',
  formData: {
    username: '',
    email: '',
    password: ''
  },
  isAuthenticated: false
}

Now, we can update the state in the .then within handleUserFormSubmit():

this.setState({
  formData: {username: '', email: '', password: '' },
  username: '',
  email: '',
  isAuthenticated: true
});

Finally, to redirect the user after a successful log in or registration, pass isAuthenticated through to the Form component:

<Route exact path='/register' render={() => (
  <Form
    formType={'Register'}
    formData={this.state.formData}
    handleFormChange={this.handleFormChange.bind(this)}
    handleUserFormSubmit={this.handleUserFormSubmit.bind(this)}
    isAuthenticated={this.state.isAuthenticated}
  />
)} />
<Route exact path='/login' render={() => (
  <Form
    formType={'Login'}
    formData={this.state.formData}
    handleFormChange={this.handleFormChange.bind(this)}
    handleUserFormSubmit={this.handleUserFormSubmit.bind(this)}
    isAuthenticated={this.state.isAuthenticated}
  />
)} />

Then, within Form.jsx add the following conditional right before the return:

if (props.isAuthenticated) {
  return <Redirect to='/' />;
}

Add the import:

import { Redirect } from 'react-router-dom';

To test, log in and then make sure that you are redirected to /. Also, you should be redirected if you try to go to the /register or /login links. Before moving on, try registering a new user. Did you notice that even though the redirect works, the users list is not updating?

To update that, fire this.getUsers() in the .then within handleUserFormSubmit():

.then((res) => {
  this.setState({
    formData: {username: '', email: '', password: ''},
    username: '',
    email: '',
    isAuthenticated: true
  });
  window.localStorage.setItem('authToken', res.data.auth_token);
  this.getUsers();
})

Test it out again.

Logout

How about logging out? Add a new component to the "components" folder called Logout.jsx:

import React, { Component } from 'react';
import { Link } from 'react-router-dom';

class Logout extends Component {
  componentDidMount() {
    this.props.logoutUser();
  }
  render() {
    return (
      <div>
        <p>You are now logged out. Click <Link to="/login">here</Link> to log back in.</p>
      </div>
    )
  }
}

export default Logout

Then, add a logoutUser method to the App component to remove the token from LocalStorage and update the state.

logoutUser() {
  window.localStorage.clear();
  this.setState({ isAuthenticated: false });
}

Import the component into App.jsx, and then add the new route:

<Route exact path='/logout' render={() => (
  <Logout
    logoutUser={this.logoutUser.bind(this)}
    isAuthenticated={this.state.isAuthenticated}
  />
)} />

To test:

  1. Log in
  2. Verify that the token was added to LocalStorage
  3. Log out
  4. Verify that the token was removed from LocalStorage

User Status

For the /status link, we need to add a new component that displays the response from a call to /auth/status on the users service. Remember: You need to be authenticated to hit this end-point successfully. So, we will need to add the token to the header prior to sending the AJAX request.

First, add a new component called UserStatus.jsx:

import React, { Component } from 'react';
import axios from 'axios';

class UserStatus extends Component {
  constructor (props) {
    super(props)
    this.state = {
      created_at: '',
      email: '',
      id: '',
      username: ''
    }
  }
  componentDidMount() {
    this.getUserStatus();
  }
  getUserStatus(event) {
    const options = {
      url: `${process.env.REACT_APP_USERS_SERVICE_URL}/auth/status`,
      method: 'get',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${window.localStorage.authToken}`
      }
    };
    return axios(options)
    .then((res) => { console.log(res.data.data) })
    .catch((error) => { console.log(error); })
  }
  render() {
    return (
      <div>
        <p>test</p>
      </div>
    )
  }
}

export default UserStatus

Here, we used a stateful, class-based component since the component has its own internal state. Notice how we also included a header with the AJAX request.

Import the component into App.jsx, and then add a new route:

<Route exact path='/status' component={UserStatus}/>

Test this out first when you're not logged in. You should see a 401 error. Try again when you are logged in. You should see an object with the keys active, created_at, email, id, and username.

To add the values to the component, update the .then:

.then((res) => {
  this.setState({
    created_at: res.data.data.created_at,
    email: res.data.data.email,
    id: res.data.data.id,
    username: res.data.data.username
  })
})

Also, update the render():

render() {
  return (
    <div>
      <ul>
        <li><strong>User ID:</strong> {this.state.id}</li>
        <li><strong>Email:</strong> {this.state.email}</li>
        <li><strong>Username:</strong> {this.state.username}</li>
      </ul>
    </div>
  )
}

Test it out.

Finally, let's make the following changes to the Navbar:

  1. When the user is logged in, the register and log in links should be hidden
  2. When the user is logged out, the log out and user status links should be hidden

Update the NavBar component like so to show/hide based on the value of isAuthenticated:

const NavBar = (props) => (
  <Navbar inverse collapseOnSelect>
    <Navbar.Header>
      <Navbar.Brand>
        <span>{props.title}</span>
      </Navbar.Brand>
      <Navbar.Toggle />
    </Navbar.Header>
    <Navbar.Collapse>
      <Nav>
        <LinkContainer to="/">
          <NavItem eventKey={1}>Home</NavItem>
        </LinkContainer>
        <LinkContainer to="/about">
          <NavItem eventKey={2}>About</NavItem>
        </LinkContainer>
        {props.isAuthenticated &&
          <LinkContainer to="/status">
            <NavItem eventKey={3}>User Status</NavItem>
          </LinkContainer>
        }
      </Nav>
      <Nav pullRight>
        {!props.isAuthenticated &&
          <LinkContainer to="/register">
            <NavItem eventKey={1}>Register</NavItem>
          </LinkContainer>
        }
        {!props.isAuthenticated &&
          <LinkContainer to="/login">
            <NavItem eventKey={2}>Log In</NavItem>
          </LinkContainer>
        }
        {props.isAuthenticated &&
          <LinkContainer to="/logout">
            <NavItem eventKey={3}>Log Out</NavItem>
          </LinkContainer>
        }
      </Nav>
    </Navbar.Collapse>
  </Navbar>
)

Make sure to pass isAuthenticated down on the props:

<NavBar
  title={this.state.title}
  isAuthenticated={this.state.isAuthenticated}
/>

This merely hides the links. An unauthenticated user could still access the route via entering the URL into the URL bar. To restrict access, update the render() in UserStatus.jsx:

render() {
  if (!this.props.isAuthenticated) {
    return <p>You must be logged in to view this. Click <Link to="/login">here</Link> to log back in.</p>
  }
  return (
    <div>
      <ul>
        <li><strong>User ID:</strong> {this.state.id}</li>
        <li><strong>Email:</strong> {this.state.email}</li>
        <li><strong>Username:</strong> {this.state.username}</li>
      </ul>
    </div>
  )
}

Add the import:

import { Link } from 'react-router-dom';

Then update the route in the App component:

<Route exact path='/status' render={() => (
  <UserStatus
    isAuthenticated={this.state.isAuthenticated}
  />
)} />

Open the JavaScript console, and then try this out. Did you notice that the AJAX request still fires when you were unauthenticated? To fix, add a conditional to the componentDidMount() in the UserStatus component:

componentDidMount() {
  if (this.props.isAuthenticated) {
    this.getUserStatus();
  }
}

Commit your code.


React Authentication

Let's add some methods to handle a user signing up, logging in, and logging out...


With the Form component set up, we can now configure the methods to:

  1. Handle form submit event
  2. Obtain user input
  3. Send AJAX request
  4. Update the page

These steps should look familiar since we already went through this process in the React Forms lesson. Put yourself to test and implement the code yourself before going through this lesson.

Handle form submit event

Turn to Form.jsx. Which method gets fired on the form submit?

<form onSubmit={(event) => props.handleUserFormSubmit(event)}>

Add the method to the App component:

handleUserFormSubmit(event) {
  event.preventDefault();
  console.log('sanity check!')
}

And then pass it down via the props:

<Route exact path='/register' render={() => (
  <Form
    formType={'Register'}
    formData={this.state.formData}
    handleUserFormSubmit={this.handleUserFormSubmit.bind(this)}
  />
)} />
<Route exact path='/login' render={() => (
  <Form
    formType={'Login'}
    formData={this.state.formData}
    handleUserFormSubmit={this.handleUserFormSubmit.bind(this)}
  />
)} />

Test it out in the browser. You should see sanity check! in the JavaScript console on form submit for both forms. Remove the console.log('sanity check!') when done.

Obtain user input

Next, to get the user inputs, add the following method to App:

handleFormChange(event) {
  const obj = this.state.formData;
  obj[event.target.name] = event.target.value;
  this.setState(obj);
}

Pass it down on the props:

handleFormChange={this.handleFormChange.bind(this)}

Add a console.log() to the method - console.log(this.state.formData); - to ensure it works when you test it in the browser. Remove it once done.

What's next? AJAX!

Send AJAX request

Update the handleUserFormSubmit method to send the data to the user service on a successful form submit:

handleUserFormSubmit(event) {
  event.preventDefault();
  const formType = window.location.href.split('/').reverse()[0];
  let data;
  if (formType === 'login') {
    data = {
      email: this.state.formData.email,
      password: this.state.formData.password
    }
  }
  if (formType === 'register') {
    data = {
      username: this.state.formData.email,
      email: this.state.formData.email,
      password: this.state.formData.password
    }
  }
  const url = `${process.env.REACT_APP_USERS_SERVICE_URL}/auth/${formType}`
  axios.post(url, data)
  .then((res) => {
    console.log(res.data);
  })
  .catch((err) => { console.log(err); })
}

Test the user registration out. If you have everything set up correctly, you should see an object in the JavaScript console with an auth token:

{
  auth_token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0OTc3NTM2ODMsImlhdCI6MTQ5Nzc1MzY3OCwic3ViIjo0fQ.vcRFb5v3znHkz8An12QUxrgXsLqoKv93kIsMf-pdfVw",
  message: "Successfully registered.",
  status: "success"
}

Test logging in as well. Again, you should see the very same object in the console.

Update the page

After a user register or logs in, we need to:

  1. Clear the formData object
  2. Save the auth token in the browser's LocalStorage, a client-side data store
  3. Update the state to indicate that the user is authenticated
  4. Redirect the user to /

First, to clear the form, update the .then within handleUserFormSubmit():

.then((res) => {
  this.setState({
    formData: {username: '', email: '', password: '' },
    username: '',
    email: ''
  });
})

Try this out. After you register or log in, the field inputs should be cleared since we set the properties in the formData object to empty strings.

What happens if you enter data for the registration form but don't submit it and then navigate to the login form? The fields should remain. Is this okay? Should we clear the state on page load? Your call. You could simply update the state within the componentWillMount lifecycle method.

Next, let's save the auth token in LocalStorage so that we can use it for subsequent API calls that require a user to be authenticated. To do this, add the following code to the .then, just below the setState:

window.localStorage.setItem('authToken', res.data.auth_token);

Try logging in again. After a successful login, open the Application tab within Chrome DevTools. Click the arrow before LocalStorage and select http://localhost:3000. You should see a key of authToken with a value of the actual token in the pane.

Instead of always checking LocalStorage for the auth token, let's add a boolean to the state so we can quickly tell if there is a user authenticated.

Add an isAuthenticated property to the state:

this.state = {
  users: [],
  username: '',
  email: '',
  title: 'TestDriven.io',
  formData: {
    username: '',
    email: '',
    password: ''
  },
  isAuthenticated: false
}

Now, we can update the state in the .then within handleUserFormSubmit():

this.setState({
  formData: {username: '', email: '', password: '' },
  username: '',
  email: '',
  isAuthenticated: true
});

Finally, to redirect the user after a successful log in or registration, pass isAuthenticated through to the Form component:

<Route exact path='/register' render={() => (
  <Form
    formType={'Register'}
    formData={this.state.formData}
    handleFormChange={this.handleFormChange.bind(this)}
    handleUserFormSubmit={this.handleUserFormSubmit.bind(this)}
    isAuthenticated={this.state.isAuthenticated}
  />
)} />
<Route exact path='/login' render={() => (
  <Form
    formType={'Login'}
    formData={this.state.formData}
    handleFormChange={this.handleFormChange.bind(this)}
    handleUserFormSubmit={this.handleUserFormSubmit.bind(this)}
    isAuthenticated={this.state.isAuthenticated}
  />
)} />

Then, within Form.jsx add the following conditional right before the return:

if (props.isAuthenticated) {
  return <Redirect to='/' />;
}

Add the import:

import { Redirect } from 'react-router-dom';

To test, log in and then make sure that you are redirected to /. Also, you should be redirected if you try to go to the /register or /login links. Before moving on, try registering a new user. Did you notice that even though the redirect works, the users list is not updating?

To update that, fire this.getUsers() in the .then within handleUserFormSubmit():

.then((res) => {
  this.setState({
    formData: {username: '', email: '', password: ''},
    username: '',
    email: '',
    isAuthenticated: true
  });
  window.localStorage.setItem('authToken', res.data.auth_token);
  this.getUsers();
})

Test it out again.

Logout

How about logging out? Add a new component to the "components" folder called Logout.jsx:

import React, { Component } from 'react';
import { Link } from 'react-router-dom';

class Logout extends Component {
  componentDidMount() {
    this.props.logoutUser();
  }
  render() {
    return (
      <div>
        <p>You are now logged out. Click <Link to="/login">here</Link> to log back in.</p>
      </div>
    )
  }
}

export default Logout

Then, add a logoutUser method to the App component to remove the token from LocalStorage and update the state.

logoutUser() {
  window.localStorage.clear();
  this.setState({ isAuthenticated: false });
}

Import the component into App.jsx, and then add the new route:

<Route exact path='/logout' render={() => (
  <Logout
    logoutUser={this.logoutUser.bind(this)}
    isAuthenticated={this.state.isAuthenticated}
  />
)} />

To test:

  1. Log in
  2. Verify that the token was added to LocalStorage
  3. Log out
  4. Verify that the token was removed from LocalStorage

User Status

For the /status link, we need to add a new component that displays the response from a call to /auth/status on the users service. Remember: You need to be authenticated to hit this end-point successfully. So, we will need to add the token to the header prior to sending the AJAX request.

First, add a new component called UserStatus.jsx:

import React, { Component } from 'react';
import axios from 'axios';

class UserStatus extends Component {
  constructor (props) {
    super(props)
    this.state = {
      created_at: '',
      email: '',
      id: '',
      username: ''
    }
  }
  componentDidMount() {
    this.getUserStatus();
  }
  getUserStatus(event) {
    const options = {
      url: `${process.env.REACT_APP_USERS_SERVICE_URL}/auth/status`,
      method: 'get',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${window.localStorage.authToken}`
      }
    };
    return axios(options)
    .then((res) => { console.log(res.data.data) })
    .catch((error) => { console.log(error); })
  }
  render() {
    return (
      <div>
        <p>test</p>
      </div>
    )
  }
}

export default UserStatus

Here, we used a stateful, class-based component since the component has its own internal state. Notice how we also included a header with the AJAX request.

Import the component into App.jsx, and then add a new route:

<Route exact path='/status' component={UserStatus}/>

Test this out first when you're not logged in. You should see a 401 error. Try again when you are logged in. You should see an object with the keys active, created_at, email, id, and username.

To add the values to the component, update the .then:

.then((res) => {
  this.setState({
    created_at: res.data.data.created_at,
    email: res.data.data.email,
    id: res.data.data.id,
    username: res.data.data.username
  })
})

Also, update the render():

render() {
  return (
    <div>
      <ul>
        <li><strong>User ID:</strong> {this.state.id}</li>
        <li><strong>Email:</strong> {this.state.email}</li>
        <li><strong>Username:</strong> {this.state.username}</li>
      </ul>
    </div>
  )
}

Test it out.

Finally, let's make the following changes to the Navbar:

  1. When the user is logged in, the register and log in links should be hidden
  2. When the user is logged out, the log out and user status links should be hidden

Update the NavBar component like so to show/hide based on the value of isAuthenticated:

const NavBar = (props) => (
  <Navbar inverse collapseOnSelect>
    <Navbar.Header>
      <Navbar.Brand>
        <span>{props.title}</span>
      </Navbar.Brand>
      <Navbar.Toggle />
    </Navbar.Header>
    <Navbar.Collapse>
      <Nav>
        <LinkContainer to="/">
          <NavItem eventKey={1}>Home</NavItem>
        </LinkContainer>
        <LinkContainer to="/about">
          <NavItem eventKey={2}>About</NavItem>
        </LinkContainer>
        {props.isAuthenticated &&
          <LinkContainer to="/status">
            <NavItem eventKey={3}>User Status</NavItem>
          </LinkContainer>
        }
      </Nav>
      <Nav pullRight>
        {!props.isAuthenticated &&
          <LinkContainer to="/register">
            <NavItem eventKey={1}>Register</NavItem>
          </LinkContainer>
        }
        {!props.isAuthenticated &&
          <LinkContainer to="/login">
            <NavItem eventKey={2}>Log In</NavItem>
          </LinkContainer>
        }
        {props.isAuthenticated &&
          <LinkContainer to="/logout">
            <NavItem eventKey={3}>Log Out</NavItem>
          </LinkContainer>
        }
      </Nav>
    </Navbar.Collapse>
  </Navbar>
)

Make sure to pass isAuthenticated down on the props:

<NavBar
  title={this.state.title}
  isAuthenticated={this.state.isAuthenticated}
/>

This merely hides the links. An unauthenticated user could still access the route via entering the URL into the URL bar. To restrict access, update the render() in UserStatus.jsx:

render() {
  if (!this.props.isAuthenticated) {
    return <p>You must be logged in to view this. Click <Link to="/login">here</Link> to log back in.</p>
  }
  return (
    <div>
      <ul>
        <li><strong>User ID:</strong> {this.state.id}</li>
        <li><strong>Email:</strong> {this.state.email}</li>
        <li><strong>Username:</strong> {this.state.username}</li>
      </ul>
    </div>
  )
}

Add the import:

import { Link } from 'react-router-dom';

Then update the route in the App component:

<Route exact path='/status' render={() => (
  <UserStatus
    isAuthenticated={this.state.isAuthenticated}
  />
)} />

Open the JavaScript console, and then try this out. Did you notice that the AJAX request still fires when you were unauthenticated? To fix, add a conditional to the componentDidMount() in the UserStatus component:

componentDidMount() {
  if (this.props.isAuthenticated) {
    this.getUserStatus();
  }
}

Commit your code.