« Previous Tutorial Next Tutorial »

Welcome back. In this tutorial, we’re going to start the process of building a password reset page. Yes, that means this is going to take multiple tutorials to cover. It’s actually a surprisingly complicated process, especially once you include setting up a way to email the link to a user. Before we get to all of that, though, the first steps we need to take are as follows:

  1. Update the user model to accept a password reset confirmation string.
  2. Create a function which sends the email to the API
  3. Generate, on the API end, an encrypted hash to use as the confirmation string.
  4. Save that confirmation string to the user’s account
  5. Send back success or failure JSON

We do not, however, want to save the user’s confirm hash to the state, or generate it anywhere that front-end code can see it. This is because that code is vulnerable to inspection via the browser tools. If we generated the hash on the front-end, and then sent it to the API, a malicious user could simply look into the network activity in their dev tools, get the hash, and then circumvent the entire “you have to own the email address associated with the account” security concept. That’s a huge security flaw, so we’re not going to do it.

Let’s start by opening /models/user.js. We’re only adding one line here, and it comes under line 11. Here it is:

  passwordReset: { type: String, select: false },

The select: false part will keep it from showing up when we retrieve the user object elsewhere in the code, unless we specifically ask for it. Handy, huh?

Save that file and move on to /src/actions/authentication.js. Underneath line 13, let’s add two new action creators with these lines:

export const passwordResetHashCreated = () => ({ type: 'AUTHENTICATION_PASSWORD_RESET_HASH_CREATED' });
export const passwordResetHashFailure = () => ({ type: 'AUTHENTICATION_PASSWORD_RESET_HASH_FAILURE' });

Next, add a padding line under line 47, and then add a new function for sending the email address to the API to be hashed. Here’s the code:

// Send email to API for hashing
export function createHash(email) {
  return async (dispatch) => {
    // contact the API
    await fetch(
      // where to contact
      '/api/authentication/saveresethash',
      // what to send
      {
        method: 'POST',
        body: JSON.stringify({ email }),
        headers: {
          'Content-Type': 'application/json',
        },
        credentials: 'same-origin',
      },
    )
    .then((response) => {
      if (response.status === 200) {
        return response.json();
      }
      return null;
    })
    .then((json) => {
      if (json.username) {
        return dispatch(passwordResetHashCreated(json));
      }
      return dispatch(passwordResetHashFailure(new Error('Something went wrong. Please try again.')));
    })
    .catch(error => dispatch(passwordResetHashFailure(error)));
  };
}

As you can see, we’ll need to pass an email address to our function, so that it can send it along to the API. We’ll do that shortly. First, let’s build our API routine. Save this file and open up /routes/api/authentication.js. First, at the very top above everything else, add this line:

const crypto = require('crypto');

That’s a module built into Node that we’ll be using below. Head for the bottom. Our register routine should end on line 77. Add a padding line under that, and then on line 79, add the following:

// POST to saveresethash
router.post('/saveresethash', async (req, res) => {
  let result;
  try {
    // check and make sure the email exists
    const query = User.findOne({ email: req.body.email });
    const foundUser = await query.exec();

    // If the user exists, save their password hash
    const timeInMs = Date.now();
    const hashString = `${req.body.email}${timeInMs}`;
    const secret = 'alongrandomstringshouldgohere';
    const hash = crypto.createHmac('sha256', secret)
                       .update(hashString)
                       .digest('hex');
    foundUser.passwordReset = hash;

    foundUser.save((err) => {
      if (err) { result = res.send(JSON.stringify({ error: 'Something went wrong while attempting to reset your password. Please Try again' })); }
      result = res.send(JSON.stringify({ success: true }));
    });
  } catch (err) {
    // if the user doesn't exist, error out
    result = res.send(JSON.stringify({ error: 'Something went wrong while attempting to reset your password. Please Try again' }));
  }
  return result;
});

We’re doing something a little different, here. This is a slightly better way to use aysnc / await than what we’re doing in the other routines (which we should probably refactor). By defining a variable, result, and then using it to await a function’s returned value, we can use try / catch blocks even with asynchronous functions. Cool. This cuts down on the if (result) and if (!result) stuff we’ve been doing elsewhere. It also keeps our API from sending JSON back to our action even while the rest of the API function is still doing stuff. Good times!

Let’s also take a look at that hash a little more carefully. Node has several hashing algorithms built in, so we didn’t have to install any modules to do this. We’re first creating a string using the email address and a JavaScript timestamp, which would result in something like noreply@closebrace.com1507130167693. Then we’re hashing it, using a random string for further entropy and asking for hex-based output. The result will look something like this:

33b2441b79e5ee7c0e2148e35fef167188e1721adaee0815ba832bcee5a5ef79

A big ol’ hex string. That’s the string we’re going to email to the user. When they click the link, our system will make sure the string in the link matches the string saved in the database, and if so, allow them to change their password. We’ll build that part out in an upcoming tutorial.

One final note on security: we shouldn’t actually be storing our hashing secret key here in the code, since it’ll end up on github for all the world to see. It’s fine for right now, but in a forthcoming tutorial we’re going to talk about security tweaks, which will allow us to move this sort of thing to a JSON file that’s not kept under version control.

All right, we’re good here. Save the file, and then create a new file in /src/components/account/ called ResetPasswordPageContainer.jsx. This is where the logic will go, first for requesting a password reset and, again, in a later tutorial, for actually performing it.

Here’s what we start with:

import React from 'react';
import { connect } from 'react-redux';
import { createHash } from '../../actions/authentication';

import ResetPasswordPage from './ResetPasswordPage';

export class ResetPasswordPageContainer extends React.Component {
  constructor(props) {
    super(props);

    // bound functions
    this.resetPasswordRequest = this.resetPasswordRequest.bind(this);
  }

  resetPasswordRequest(email) {
    const { dispatch } = this.props;
    dispatch(createHash(email));
  }

  render() {
    return (
      <ResetPasswordPage resetPasswordFunction={this.resetPasswordRequest} />
    );
  }
}

export default connect()(ResetPasswordPageContainer);

This should all look pretty familiar at this point. We pull in our createHash function from our action file, then we use it in a little method that takes an email address as an argument, and pass that method on to our page component. Now we need to make that page component, so save this file, and then create a new file in /src/components/account called ResetPasswordPage.jsx. Add the following code:

import React from 'react';
import { AvForm, AvGroup, AvInput, AvFeedback } from 'availity-reactstrap-validation';
import { Button, Label } from 'reactstrap';

export default class ResetPasswordPage extends React.Component {
  constructor(props) {
    super(props);

    // bound functions
    this.handleEmailChange = this.handleEmailChange.bind(this);
    this.handleKeyPress = this.handleKeyPress.bind(this);
    this.handleValidSubmit = this.handleValidSubmit.bind(this);

    // component state
    this.state = {
      email: '',
    };
  }

  // update state as email value changes
  handleEmailChange(e) {
    this.setState({ email: e.target.value });
  }

  // catch enter clicks
  handleKeyPress(target) {
    if (target.charCode === 13) {
      this.handleValidSubmit();
    }
  }

  // Handle submission once all form data is valid
  handleValidSubmit() {
    const { resetPasswordFunction } = this.props;
    const formData = this.state;
    resetPasswordFunction(formData.email);
  }

  render() {
    return (
      <div className="row justify-content-center">
        <div className="col-10 col-sm-7 col-md-5 col-lg-4">
          <p>
            If you‘d like to reset your password, please enter your email here
            and a link to do so will be sent to the address you enter.
          </p>
          <AvForm onValidSubmit={this.handleValidSubmit}>
            <AvGroup>
              <Label for="userEmail">Email</Label>
              <AvInput
                id="userEmail"
                name="email"
                onChange={this.handleEmailChange}
                onKeyPress={this.handleKeyPress}
                placeholder="noreply@musiclist.com"
                required
                type="email"
                value={this.state.email}
              />
              <AvFeedback>A valid email is required to reset your password.</AvFeedback>
            </AvGroup>
            <Button color="primary">Reset Password</Button>
          </AvForm>
        </div>
      </div>
    );
  }
}

This is highly similar to our log-in page, just without the password input. So similar in fact that it’d probably behoove us to talk about storing helper methods like handleKeyPress in a separate file so they can be shared between multiple components … but that’s another tutorial.

Save the file and open /src/components/Template.jsx. We need to add a route so we can actually get to the page we just built. Below line 10 add this code:

import ResetPasswordPage from './account/ResetPasswordPageContainer';

And below line 24, add the following:

          <Route exact path="/account/reset-password" component={ResetPasswordPage} />

That’s it. Save the file and head for a browser. Make sure your server’s running, and do a hard-refresh because we changed action files, which don’t hot reload. Then head for localhost:3000/account/reset-password and bask in the glory of your new form. Then open your Network console, because right now that’s the only way you’re going to see that anything’s happening!

Clear your network panel, just so it’s easier to see, and then first try an email address that does not exist in your DB. Check the response in your network console, and you’ll see the server returned an error message (unsure how to do this? Watch the last minute or so of the video! It’s more clearly shown, there). Now clear your network panel again, and try a request using an email address that does exist in your database. Check the response again, and you’ll see we’re getting a success message. Also, if you went into your MongoDB console and checked the user manually, you’d see that the hash is now stored.

So, that’s great if you really like looking up JSON responses in your network console, but if you’re looking for a little more than that, we’re going to need to start writing reducers. We’ll do that in the next tutorial. See you there!

« Previous Tutorial Next Tutorial »