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 features.

The Google Identity Platform lets you access information about your users and helps to protect their sign-in credentials. OAuth 2.0 provides users of the app with a sign-in flow and provides the app basic profile information about authenticated users.

This page is part of a multipage tutorial. To start from the beginning and read the setup instructions, go to Node.js Bookshelf app.

Creating a web app client ID

A web app client ID lets your app authorize users and access Google APIs.

  1. In the Google Cloud Platform Console, go to the Credentials page.

    Go to the Credentials Page

  2. Click OAuth consent screen.

  3. For the product name, enter Node.js Bookshelf App.

  4. For Authorized Domains, add your App Engine app name as [YOUR_PROJECT_ID].appspot.com.

    Replace [YOUR_PROJECT_ID] with your GCP project ID.

  5. Fill in any other relevant, optional fields, and then click Save.

  6. Click Create credentials > OAuth client ID.

  7. In the Application type drop-down list, click Web Application.

  8. In the Name field, enter Node.js Bookshelf Client.

  9. In the Authorized redirect URIs field, enter the following URLs, one at a time.

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

  10. Click Create.

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

Configuring settings

  1. Copy your config.json file from the Cloud Storage part of this tutorial to the nodejs-getting-started/4-auth directory.
  2. 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",
    

    Replace [YOUR_OAUTH2_CLIENT_ID] and [YOUR_OAUTH2_CLIENT_SECRET] with the app's client ID and secret that you created previously.

Installing dependencies

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

npm install

Running the app on your local machine

  1. Start a local web server using npm:

    npm 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 by using the My Books link in the top navigation bar.

Deploying the app to the App Engine standard environment

  1. To enable online authentication, change the OAUTH2_CALLBACK line of config.json in the nodejs-getting-started/4-auth directory to the following:

    "OAUTH2_CALLBACK": "https://[YOUR_PROJECT_ID].appspot.com/auth/google/callback"
    

    Replace [YOUR_PROJECT_ID] with your GCP project ID.

  2. Deploy the sample app from the nodejs-getting-started/4-auth directory:

    gcloud app deploy
    
  3. In your web browser, enter this address:

    https://[YOUR_PROJECT_ID].appspot.com
    

    Replace [YOUR_PROJECT_ID] with your GCP project ID.

If you update your app, you deploy the updated version by entering the same command that you used to deploy the app. 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 shouldn't 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.

App structure

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

Auth sample structure

Understanding the code

This section walks you through the app's code and explains how it works.

About sessions

Before your app can authenticate users, you need a way to store information about the current user in a session. Express comes with a memory-based implementation to perform this function. However, this implementation is unsuitable for an app that can be served from multiple instances, as the session that is recorded in one instance might differ from other instances. For this reason the Bookshelf sample app uses Cloud Datastore to store session data.

// Configure the session and session storage.
const sessionConfig = {
  resave: false,
  saveUninitialized: false,
  secret: config.get('SECRET'),
  signed: true,
  store: new DatastoreStore({
    dataset: new Datastore({kind: 'express-sessions'}),
  }),
};

app.use(session(sessionConfig));

Authenticating users

The Bookshelf app 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',
      userProfileURL: 'https://www.googleapis.com/oauth2/v3/userinfo',
    },
    (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 what user information your app 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 app requires only the basic scopes email and profile, which grant the app 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 app.

Redirecting to Google

Your app sends the user to Google's authorization service. The app generates a URL by using your client ID and the scopes that the app 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 app then redirects the user to the generated URL. The user is prompted to let your app access the specified scopes. After accepting, the user is redirected back to your app.

Process the authorization response

Google sends the user back to your app along with an authorization code. This code, along with your client secret, is exchanged for the user’s credentials and other information about the user. This step of the flow happens 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 app is the one requesting credentials.

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 create middleware functions to assist the authentication process:

// 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 signed in or signed 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 signed-in user's information available through middleware, you can track 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.
    model.create(data, (err, savedData) => {
      if (err) {
        next(err);
        return;
      }
      res.redirect(`${req.baseUrl}/${savedData.id}`);
    });
  }
);

You can also use the signed-in user's information to show them which books they 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) => {
  model.listBy(
    req.user.id,
    10,
    req.query.pageToken,
    (err, entities, cursor) => {
      if (err) {
        next(err);
        return;
      }
      res.render('books/list.pug', {
        books: entities,
        nextPageToken: cursor,
      });
    }
  );
});

The preceding code uses a new model method called listBy.

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);
  });
}
Var denne siden nyttig? Si fra hva du synes:

Send tilbakemelding om ...