Authenticating Users with Node.js

This part of the Node.js Bookshelf tutorial shows how to create a sign-in flow for users and how to use profile information to provide users with personalized functionality.

By using Google Identity Platform, you can easily access information about your users while ensuring their sign-in credentials are safely managed by Google. OAuth 2.0 makes it easy to provide a sign-in flow for all users of your app and provides your application with access to basic profile information about authenticated users.

This page is part of a multi-page tutorial. To start from the beginning and see instructions for setting up, go to Node.js Bookshelf App.

Creating a web application client ID

A web application client ID allows your application to authorize users and access Google APIs on behalf of your users.

  1. Go to the credentials section in the Google Cloud Platform Console.

  2. Click OAuth consent screen. For the product name, enter Node.js Bookshelf App. Fill in any relevant optional fields. Click Save.

  3. Click Create credentials > OAuth client ID.

  4. Under Application type, select Web Application.

  5. Under Name, enter Node.js Bookshelf Client.

  6. Under Authorized redirect URIs enter the following URLs, one at a time. Replace [YOUR_PROJECT_ID] with your project ID:

    http://localhost:8080/auth/google/callback
    http://[YOUR_PROJECT_ID].appspot.com/auth/google/callback
    https://[YOUR_PROJECT_ID].appspot.com/auth/google/callback

  7. Click Create.

  8. Copy the client ID and client secret and save them for later use.

Creating a Memcached instance

To properly store sessions across multiple App Engine instances, session data must be stored in locations that are accessible to all App Engine instances. In order to do this, we'll be using a Memcached instance to store our cross-server session data.

If you do not have a Memcached instance already, get a free one from Redis Labs. If you're using Redis Labs, the Endpoint property lists the URL that points to your Memcached instance.

Configuring settings

Copy your config.json file from the Cloud Storage part of this tutorial to the nodejs-getting-started/4-auth directory. Add these lines to the copied file:

"OAUTH2_CLIENT_ID": "[YOUR_OAUTH2_CLIENT_ID]",
"OAUTH2_CLIENT_SECRET": "[YOUR_OAUTH2_CLIENT_SECRET]",
"OAUTH2_CALLBACK": "http://localhost:8080/auth/google/callback",
"MEMCACHE_URL": "[YOUR_MEMCACHE_URL]"
  1. Replace [YOUR_OAUTH2_CLIENT_ID] and [YOUR_OAUTH2_CLIENT_SECRET] with the application client ID and secret that you created previously.
  2. Replace [YOUR_MEMCACHE_URL] with a URL pointing to your Memcached instance. This URL should be of the form hostname:port.
  3. If your Memcached instance requires a username and a password, add "MEMCACHE_USER": "[YOUR_MEMCACHE_USERNAME]" and "MEMCACHE_PASSWORD": "[YOUR_MEMCACHE_PASSWORD]" to config.json as well. Replace [YOUR_MEMCACHE_USERNAME] and [YOUR_MEMCACHE_PASSWORD] with values of your own.

Installing dependencies

Install dependencies in the nodejs-getting-started/4-auth directory:

  • Using npm:

    npm install
    
  • Using yarn:

    yarn install
    

Running the app on your local machine

  1. Start a local web server using npm or yarn:

    • Using npm:

      npm start
      
    • Using yarn:

      yarn start
      
  2. In your web browser, enter the following address:

    http://localhost:8080

Now you can browse the app's web pages, sign in using your Google account, add books, and see the books you've added using the My Books link in the top navigation bar.

Deploying the app to the App Engine standard environment

  1. Change the OAUTH2_CALLBACK line of config.json to the following to enable online authentication. Replace [YOUR_PROJECT_ID] with your project ID:

    "OAUTH2_CALLBACK": "https://[YOUR_PROJECT_ID].appspot.com/auth/google/callback"
    
  2. Deploy the sample app:

    gcloud app deploy
    

  3. In your web browser, enter this address. Replace [YOUR_PROJECT_ID] with your project ID:

    https://[YOUR_PROJECT_ID].appspot.com
    

If you update your app, you can deploy the updated version by entering the same command you used to deploy the app the first time. The new deployment creates a new version of your app and promotes it to the default version. The older versions of your app remain. By default the App Engine standard environment scales to 0 instances when there is no incoming traffic to a version, thus unused versions should not cost anything. However, all of these app versions are still billable resources.

See the Cleaning up section in the final step of this tutorial for more information on cleaning up billable resources, including non-default app versions.

Application structure

The following diagram shows the application's components and how they connect to one another.

Auth sample structure

Understanding the code

This section walks you through the application code and explains how it works.

About sessions

Before your application can authenticate users, you need a way to store information about the current user in a session. Express comes with a memory-based implementation, but that is unsuitable for an application that can be served from multiple instances, as the session that is recorded in one instance might differ from other instances. The Bookshelf sample uses a MemcachedStore object to store session data in a remote Memcached service specified in config.json.

// Configure the session and session storage.
const sessionConfig = {
  resave: false,
  saveUninitialized: false,
  secret: config.get('SECRET'),
  signed: true
};

// In production use the Memcache instance to store session data,
// otherwise fallback to the default MemoryStore in development.
if (config.get('NODE_ENV') === 'production' && config.get('MEMCACHE_URL')) {
  if (config.get('MEMCACHE_USERNAME') && (config.get('MEMCACHE_PASSWORD'))) {
    sessionConfig.store = new MemcachedStore({
      servers: [config.get('MEMCACHE_URL')],
      username: config.get('MEMCACHE_USERNAME'),
      password: config.get('MEMCACHE_PASSWORD')});
  } else {
    sessionConfig.store = new MemcachedStore({
      servers: [config.get('MEMCACHE_URL')]
    });
  }
}

app.use(session(sessionConfig));

Authenticating users

The Bookshelf sample uses Passport.js to manage user authentication. Passport.js requires some setup before it can authenticate users:

const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;

function extractProfile (profile) {
  let imageUrl = '';
  if (profile.photos && profile.photos.length) {
    imageUrl = profile.photos[0].value;
  }
  return {
    id: profile.id,
    displayName: profile.displayName,
    image: imageUrl
  };
}

// Configure the Google strategy for use by Passport.js.
//
// OAuth 2-based strategies require a `verify` function which receives the
// credential (`accessToken`) for accessing the Google API on the user's behalf,
// along with the user's profile. The function must invoke `cb` with a user
// object, which will be set at `req.user` in route handlers after
// authentication.
passport.use(new GoogleStrategy({
  clientID: config.get('OAUTH2_CLIENT_ID'),
  clientSecret: config.get('OAUTH2_CLIENT_SECRET'),
  callbackURL: config.get('OAUTH2_CALLBACK'),
  accessType: 'offline'
}, (accessToken, refreshToken, profile, cb) => {
  // Extract the minimal profile information we need from the profile object
  // provided by Google
  cb(null, extractProfile(profile));
}));

passport.serializeUser((user, cb) => {
  cb(null, user);
});
passport.deserializeUser((obj, cb) => {
  cb(null, obj);
});

Scopes indicate which items of the user’s information your application is allowed to access. Google provides multiple services along with a set of scopes for each service. For example, there is a scope that allows read-only access to Google Drive and another scope that allows read-write access. This sample requires only the basic scopes email and profile, which grant the application access to the user's email address and basic profile information.

Authenticating the user involves two basic steps, which together are called the web service flow:

  1. Redirect the user to Google’s authorization service.
  2. Process the response when Google redirects the user back to your application.

Redirect to Google

Your application sends the user to Google’s authorization service. The application generates a URL by using your client ID and the scopes that the application needs.

router.get(
  // Login url
  '/auth/login',

  // Save the url of the user's current page so the app can redirect back to
  // it after authorization
  (req, res, next) => {
    if (req.query.return) {
      req.session.oauth2return = req.query.return;
    }
    next();
  },

  // Start OAuth 2 flow using Passport.js
  passport.authenticate('google', { scope: ['email', 'profile'] })
);

Your application then redirects the user to the generated URL. The user is prompted to allow your application to access the specified scopes. After accepting, the user is redirected back to your application.

Process the authorization response

Google sends the user back to your application along with an authorization code. This code, along with your client secret, can be exchanged for the user’s credentials and other information about the user. This step of the flow happens entirely on the server without having to redirect the user. This extra step is required to verify that the authorization code is authentic and that your application is the one requesting credentials. Passport.js makes this step easy:

router.get(
  // OAuth 2 callback url. Use this url to configure your OAuth client in the
  // Google Developers console
  '/auth/google/callback',

  // Finish OAuth 2 flow using Passport.js
  passport.authenticate('google'),

  // Redirect back to the original page, if any
  (req, res) => {
    const redirect = req.session.oauth2return || '/';
    delete req.session.oauth2return;
    res.redirect(redirect);
  }
);

After you obtain credentials, you can store information about the user. Passport.js automatically serializes the user to the session. After the user’s information is in the session, you can make a couple of middleware functions to make it easier to work with authentication:

// Middleware that requires the user to be logged in. If the user is not logged
// in, it will redirect the user to authorize the application and then return
// them to the original URL they requested.
function authRequired (req, res, next) {
  if (!req.user) {
    req.session.oauth2return = req.originalUrl;
    return res.redirect('/auth/login');
  }
  next();
}

// Middleware that exposes the user's profile as well as login/logout URLs to
// any templates. These are available as `profile`, `login`, and `logout`.
function addTemplateVariables (req, res, next) {
  res.locals.profile = req.user;
  res.locals.login = `/auth/login?return=${encodeURIComponent(req.originalUrl)}`;
  res.locals.logout = `/auth/logout?return=${encodeURIComponent(req.originalUrl)}`;
  next();
}

You can use the info provided by the middleware in your templates to indicate to the user that they are logged in or logged out:

if profile
  if profile.image
    img.img-circle(src=profile.image, width=24)
  span #{profile.displayName}  
    a(href=logout) (logout)
else
  a(href=login) Login

Personalization

Now that you have the logged-in user's information available through middleware, you can keep track of which user added which book to the database.

router.post(
  '/add',
  images.multer.single('image'),
  images.sendUploadToGCS,
  (req, res, next) => {
    const data = req.body;

    // If the user is logged in, set them as the creator of the book.
    if (req.user) {
      data.createdBy = req.user.displayName;
      data.createdById = req.user.id;
    } else {
      data.createdBy = 'Anonymous';
    }

    // Was an image uploaded? If so, we'll use its public URL
    // in cloud storage.
    if (req.file && req.file.cloudStoragePublicUrl) {
      data.imageUrl = req.file.cloudStoragePublicUrl;
    }

    // Save the data to the database.
    getModel().create(data, (err, savedData) => {
      if (err) {
        next(err);
        return;
      }
      res.redirect(`${req.baseUrl}/${savedData.id}`);
    });
  }
);

Now that you have that information in the database, you can show the user which books they have personally added to the database.

// Use the oauth2.required middleware to ensure that only logged-in users
// can access this handler.
router.get('/mine', oauth2.required, (req, res, next) => {
  getModel().listBy(
    req.user.id,
    10,
    req.query.pageToken,
    (err, entities, cursor, apiResponse) => {
      if (err) {
        next(err);
        return;
      }
      res.render('books/list.pug', {
        books: entities,
        nextPageToken: cursor
      });
    }
  );
});

This code uses a new model method called listBy. The implementation depends on which database backend you chose.

Datastore

function listBy (userId, limit, token, cb) {
  const q = ds.createQuery([kind])
    .filter('createdById', '=', userId)
    .limit(limit)
    .start(token);

  ds.runQuery(q, (err, entities, nextQuery) => {
    if (err) {
      cb(err);
      return;
    }
    const hasMore = nextQuery.moreResults !== Datastore.NO_MORE_RESULTS ? nextQuery.endCursor : false;
    cb(null, entities.map(fromDatastore), hasMore);
  });
}

Cloud SQL

function listBy (userId, limit, token, cb) {
  token = token ? parseInt(token, 10) : 0;
  connection.query(
    'SELECT * FROM `books` WHERE `createdById` = ? LIMIT ? OFFSET ?',
    [userId, limit, token],
    (err, results) => {
      if (err) {
        cb(err);
        return;
      }
      const hasMore = results.length === limit ? token + results.length : false;
      cb(null, results, hasMore);
    });
}
Was this page helpful? Let us know how we did:

Send feedback about...