Skip to content

A simple express starter project that handles user authentication.

Notifications You must be signed in to change notification settings

jonathansgardner/express_auth

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Authentication

JSON Web Tokens are used for authentication. A new token is created and sent to the client when a user first creates their account and on each subsequent login. The token contains the unique id that was generated by MongoDB when the user signed up for their account. This token can be stored by the client, in localStorage or state for example, and sent as a header when making HTTP requests to protected endpoints.

Including the token in the Authorization header

The token should be sent in an Authorization header and prefixed with the string “Bearer” followed by a single space.

// grab the token from localStorage
// or wherever you are storing it on the client side
const token = localStorage.getItem( 'token' );

// include the token in the Authorization header
// prefix the token with "Bearer" (with an uppercase b and a single space)
header: { Authorization: `Bearer ${ token }` };

Creating protected routes

In the Express app, a middleware called requireAuth, which can be found in src/middlewares/requireAuth.js in the project’s root directory, can be passed as the second argument to any route that should require the user to be logged in to access. When the requireAuth middleware is applied to a route, it will check that the Authorization header is present, validate the token, find the user in the database using the id included in the token, and attach the user’s information to the req before passing it on to the route’s callback function. As a result, the current user’s info will be included in the request of all protected routes.

As an example, if you wanted to create a route that let the user change their email, it might look something like this:

// passing requireAuth as the second argument after the path
app.patch( '/user/email', requireAuth, ( req, res ) => {
  try {
    // get the id attached to the request by the requireAuth middleware
    const { id } = req.user
    // get the email address sent from the client that the user would like to be saved to their account
    const newEmail = { req.email };
    // find the user in the db using the id
    const user = User.findById( id );
    // update the email on the user object
    user.email = newEmail;
    // save the updated user to the db
    user.save();
    res.status( 200 ).send();
  } catch ( err ) {
    // handle errors
  }
});

On the frontend, the client could then make a PATCH request to /user/email, passing in the desired email address, making sure of course, to include the token in the Authorization header

Using Axios, that might look something like this:

Const updateEmail = email => {

  // grab the token from localStorage
  // or wherever you are storing it on the client side
  const token = localStorage.getItem( 'token' );

  axios({
    method: 'patch',
    url: '/user/email',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${ token }`
    },
    data: { email }
  });
};

Note we don’t need to include any information about the current user in the request from the client because the current user’s info is attached to the request by the requireAuth middleware.

Setup

Development with Docker

In order for the app to work correctly, you will first need to configure a few environment variables.

Open the docker-compose.yml file found in the project’s root directory, look under services and where the api service is defined you will see the following listed under environment:

API_PORT: 8000
JWT_SECRET:
MONGO_USER: mongo
MONGO_PASS: mongo_password
MONGO_HOST: mongodb
MONGO_PORT: 27017

By default the API_PORT is set to 8000.

see Setting the API port

The JWT_SECRET is used during the token encryption process and therefore needs to be unique and secure. A default is provided. This can and should be changed to anything you’d like.

When the docker-compose.yml file is executed, a container that hosts an instance of MongoDB is created. By default, the MONGO_USER, MONGO_PASS MONGO_HOST, MONGO_PORT environment variables are set up to point to and connect with this instance.

see Changing MongoDB configuration in Docker

Once you have provided the necessary environment variables, navigate to the project’s root directory in your console and run the command

docker-compose up —build

It may take a minute or two the first time you run it. You will now have a MongoDB instance up and running in one container and the api running in another. The api should now be accessible at http://localhost:5000 (or whatever port you configured it to run on). If you navigate to this route in your browser you should see the following:

[Screenshot]

Development without Docker

If you’d prefer not to use Docker for development that’s fine also though it requires a good bit more setup.

You’ll need to have access to a MongoDB instance running on your local machine or on an external host such as mLab.

You’ll also need to make sure all of the environment variables are defined. You can either set up all of these variables on your local machine via the console or you can define them directly in the src/config/keys.js. If you choose this route, I suggest creating two new files in this directory, one for development keys and the other for production keys and then editing the keys.js file to export the correct file based on the value of process.env.NODE_ENV which will defaults to “development” if not defined elsewhere. This way, you can use a separate database, SMTP server, API port, and JWT secret for development and production. The new setup might look something like this:

[file structure]

[dev.js]

[prod.js]

[keys.js]

Production

To run the app in a production environment you’ll need to make sure that an instance of MongoDB is accessible and that all of the proper environment variables are defined. Alternatively, if you’d prefer, you can hard code any of these variables by defining them directly in src/config/keys.js.

Routes

POST /auth/signup

body: firstName, lastName, email, password

Creates a new user and stores it in the database. a token is created and sent to the client in the response. This token can then be stored and sent in the authorization header (prefixed with the string “Bearer“ followed by a single space) allowing the logged in user to access routes protected by the authRequired middleware.

see Customizing the User model see Including the token in the Authorization header

POST /auth/login

body: email, password

Searches the database for a user using the provided email and confirms that the provided password matches the password for that user. If found, and the provided password is valid, a token is created and sent to the client in the response. This token can then be stored and sent in the authorization header (prefixed with the string “Bearer“ followed by a single space) allowing the logged in user to access routes protected by the authRequired middleware.

see Including the token in the Authorization header

POST /auth/sendResetLink

body: email

Searches the database for a user using the provided email. If found, a password reset code is created for the user and stored in the database. An email with a url containing the newly created reset code is sent to the user. This url should be set up to direct the user to a page allowing them to create a new password for their account. If no user is found, an email explaining that a request for a reset link was made but that no account associated with the provided email address was found is sent.

see Customizing the password reset emails

POST /auth/resetPassword

body: passwordResetCode, password

Searches the database for a user using the provided passwordResetCode. If found, the user’s password is set to the provided password.

POST /auth/validatePasswordResetCode

body: passwordResetCode

Searches the database for a user using the provided passwordResetCode. This can be used to verify that the URL the user is using to try and reset their password contains a valid passwordResetCode.

GET /auth/validateToken

Requires Authorization header containing token (prefixed with the string “Bearer” followed by a single space)

Validates the token included in the Authorization header. Can be used to assess whether or not a token is valid.

see Including the token in the Authorization header

Customization

Customizing the User Model

By default, when I new user is created, There are four required fields: firstName, lastName, email, and password. The email must be unique across all users. Additionally a unique id is automatically generated and stored for the user by the database.

If you would like to add new properties, or remove any existing ones, you can simply edit the userSchema, defined in src/models/User.js. The email, password, or passwordResetCode are necessary for the authentication process to work correctly and any alterations made to these properties should be done with caution. Please consult the Mongoose documentation to learn more about about defining a Mongoose schema.

Setting the API port

By default the API is configured to run on port 8000. If you would like to change this there are a number of ways to do so. If you are running the app in a development environment using Docker, you can simply update the port mapping and API_PORT environment variable in the docker-compose.yml file found in the project’s root directory. To do this, look under services, where the api service is being defined, here you will see the following:

ports:
  - 8000:8000
environment:
  API_PORT: 8000

This line is mapping port 8000 for the Docker container the API is running in to port 8000 on your local machine. By changing the value to the right of the “:” you can change the port on which the API will be made available on your local machine.

If you are not using Docker, or you are setting the API up to run in a production environment, you can define the API port in one of two ways. The first is by setting the API_PORT environment variable to the desired port number. Alternatively, you can simply set the value in src/config/keys.js. This can be done by removing “API_PORT” from the destructing statement found at the top of the file and defining it underneath. For example, if you’d like to setup the API to run on port 8001

const API_PORT = 8001;

Configuring the mail server

This app uses Nodemailer to send password reset emails to users. The configuration for this service is defined in src/services/nodemailer.js. By default the app is configured to use an ethereal.email test account as outlined in the example found here on the Nodemailer website. When you hit the /auth/sendResetLink endpoint, an email will be sent to the ethereal.email test account and a confirmation as well as a link to the email will be logged to the console. You can follow this link to view the newly created email.

To configure this to work with an external SMTP server, I would suggest setting up the necessary configuration values as environment variables such as TRANSPORT_HOST, TRANSPORT_PORT, TRANSPORT_USER, and TRANSPORT_PASS. You can do this by defining them in the docker-compose.yml file found in the project’s root directory and simply adding them to the list variables you configured during setup. Make sure you then add the newly created variables to the list of variables destructured from process.env at the top of the keys.js file found at src/config/keys.js. You will then need to make sure they are included in the module.exports statement found at the bottom of the file as well.

Next, go to src/services/nodemailer.js and comment out or remove the following line:

let testAccount = await nodemailer.createTestAccount();

Finally, replace the current transporter definition with the following

let transporter = nodemailer.createTransport({
    host: keys.TRANSPORT_HOST,
    port: keys.TRANSPORT_PORT,
    // true for 465, false for other ports
    secure: keys.TRANSPORT_PORT === 465 ? true : false,
     auth: {
       user: keys.TRANSPORT_USER,
       pass: keys.TRANSPORT_PASS
    }
  });

You can also remove the console.logs found at the bottom of the sendEmail function as well

Alternatively, you can define and export the necessary values as variables directly in [src/config/keys.js]*(src/config/keys.js) or more directly still in src/services/nodemailer.js.

For more detailed information about how to configure the package to work with your email server please consult the Nodemailer website.

Customizing the password reset emails

The default password reset emails are as follows:

For users:

from: Express with Authentication [email protected]
to: email sent in request to the endpoint
subject: Reset Password

Click on the link below to reset your password. http://localhost:3000/resetPassword/${ passwordResetCode }

For non-users:

from: Express with Authentication [email protected]
to: email sent in request to the endpoint
subject: Reset Password

You are receiving this email because a request was made to reset the password associated with your account. Unfortunately, no account associated with this email address was found. If you did not make this request, please ignore this email. Otherwise, please confirm that the email in question is in fact the email you used when signing up for an account with us. Sorry for any inconvenience.

You can customize these emails in the src/services/nodemailer.js file found in the project’s root directory. The body of the email is passed to this file from the /auth/sendResetLink endpoint. These can be edited in the src/routes/authRoutes.js file found in the project’s root directory.

*** Make sure to update the url being sent in the user email to match the url on the frontend that will allow the user to reset their password. By default the url is set to http://localhost:3000/resetPassword/${ passwordResetCode }. You can update this in src/routes/authRoutes.js under the /sendResetLink endpoint where const url is declared.

For more detailed information on customizing emails with Nodemailer, please refer to the Nodemailer documentation.

Changing MongoDB configuration in Docker

When running the app with Docker, an instance of MongoDB will be built from the official MongoDB image found on Docker Hub. The instance is configured to run with a username of "mongo" and a password of "mongo_password". These and other configurations can be changed or added where the mongodb service is defined in the docker-compose.yml file found in the project's root directory. For more detailed information on using and configuring this image, please consult the official documentation found here

Running the Test App

I’ve included a test app that you can run to see the authentication flow as I imagined it working in an application. It is a simple react app that will let you sign up for an account, login, logout, and reset a user’s password.

To run the test app locally with Docker, navigate to the test directory found in the project’s root directory in your console and run the command:

docker-compose up —-build

or, directly from the project’s root directory you can run:

docker-compose -f test/docker-compose.yml up —-build

It may take a few minutes to build the first time. Once it finishes building, the app should run on port 3333 on your local machine, with the api running on port 8000.

The test app uses Mailslurper for email testing. When the app is running you can navigate to http://localhost:8080 to see the inbox. Any emails that are sent from the app, regardless of the email supplied in the “to” field, will end up here, allowing you to test the password reset flow. Mailslurper is a really cool open source SMTP mail server for development. If you aren’t familiar with it already, you can find out more at the Mailslurper website.

About

A simple express starter project that handles user authentication.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages