Menu

FeathersJS and Google OAuth 2.0

Implementing authentication may seem daunting with the number of libraries and modules available today. By understanding the purpose of each module, an OAuth 2.0 flow can be created with minimal configuration. The following is an implementation of the OAuth 2.0 authentication flow using Google and FeathersJS. The example being constructed will:

  • Expose an endpoint to begin OAuth flow
  • Allow a user to confirm which Google account will be used
  • Create a user based on requested Google account data
  • Create a JWT for use in future requests

Although a boilerplate project can be created using the FeathersJS CLI, all files in this article are created manually to illustrate each step.

Project setup

The directory hierarchy is arbitrary, however, there is a common theme in Feathers applications to assist in code organization. The directory structure for this example is as follows:

Directory structure

- /project-root
  - /config
  - /src
    - /services
      - /authentication
      - /items
      - /users
      - index.js
    - index.js
  - package.json

This article focuses on the /authentication service. To follow along, clone the full source and checkout the initial hash.

Modules

Feathers authentication is an abstraction of PassportJS. As such, Passport plugins are able to be used in Feathers applications. For this example, the below packages are needed and can be installed with npm or yarn.

npm install @feathersjs/authentication @feathersjs/authentication-jwt \
@feathersjs/authentication-oauth2 passport-google-oauth2
yarn add @feathersjs/authentication @feathersjs/authentication-jwt \
@feathersjs/authentication-oauth2 passport-google-oauth2

Adding authentication

Register a Google OAuth application

Following the guide provided by Google for setting up OAuth applications, there are four specific fields to verify:

  • Client ID
  • Client Secret
  • Authorized JavaScript Origins
  • Authorized redirect URIs

Google Developer Console Google developer console

The client ID and secret are provided by Google after registering a new application. The origin and redirect are specified by the author of the new application. For this example, a custom port of 9001 is used along with the default authentication route of /auth/google/callback. The callback route is utilized by Google after a user has been authenticated.

A new section, authentication, is added to /config/default.json. The newly created client ID and secret are stored here for use later.

Note: "authentication", "google", "clientID", and "clientSecret" are arbitrary keys.

/config/default.json

{
	"port": 9001,
	"authentication": {
		"google": {
			"clientID": "...apps.googleusercontent.com",
			"clientSecret": "..."
		}
	}
}

Create a secure JWT secret

Any string can be used to sign a JWT. The larger the secret is, in relation to the JWT body, the more secure. For more information regarding JWTs, see https://jwt.io. This application will use a random, hex string, generated from the crypto library in NodeJS. From a Node console:

node> crypto.randomBytes(256).toString('hex')

The JWT secret is also stored in /config/default.json:

Note: It is recommended to hold these values in environmental variables as to not expose secure keys to source control. The key "secret" is also an arbitrary name.

/config/default.json

{
	"port": 9001,
	"authentication": {
		"secret": "...",
		"google": {
			"clientID": "...apps.googleusercontent.com",
			"clientSecret": "..."
		}
	}
}

With the above configuration, the authentication service can be created and configured. The default path provided by Feathers is /auth/<provider> for OAuth requests and /authentication to create JWTs.

/src/services/authentication/index.js

/*
The authentication module sets up the `/authentication` endpoint, while the JWT module handles
the creation and verification of JWTs. The OAuth2 and Passport modules handle where OAuth requests
should be sent and how they are received. Passport handles the OAuth flow and upon completion invokes
the respective Feathers service call(s).
 */
const authentication = require('@feathersjs/authentication');
const jwt = require('@feathersjs/authentication-jwt');
const oauth2 = require('@feathersjs/authentication-oauth2');
const { Strategy } = require('passport-google-oauth2');

const auth = app => {
  // The `secret` and `google` properties correspond to the values configured earlier.
	const { secret, google } = app.get('authentication');

	app.configure(authentication({ secret }));
  /*
  This app uses basic JWT configuration. Options, such as issuer, can be configured in addition:
  https://docs.feathersjs.com/api/authentication/jwt.html#options
   */
	app.configure(jwt());
	app.configure(oauth2({
		name: 'google',
		Strategy,
		clientID: google.clientID,
		clientSecret: google.clientSecret,
		scope: ['email']
	}));

  /*
  This before hook exists to prevent anonymous requests from generating valid JWTs.
  The `authenticate` hook skips validation if the call is internal, such as from the
  OAuth module.
   */
	app.service('authentication').hooks({
		before: {
			create: [
				authentication.hooks.authenticate(['jwt'])
			]
		}
	});
};

module.exports = auth;

Using authentication hooks

With a completed /authentication service and using the authenticate hook provided with @feathersjs/authentication, existing services can now be secured.

A pre-existing /items service utilizing the authenticate hook:

/src/services/items/hooks.js

const { authenticate } = require('@feathersjs/authentication').hooks;

/*
By placing the `authenticate` hook in `before.all`, any request received by this application
must first pass through this hook. The `authenticate` hook accepts an array of named strategies.
This application is built on the premise that a valid JWT will be created after successful OAuth
verification. As a result, the JWT strategy is the only required input.
 */
module.exports = {
	before: {
		all: [
			authenticate(['jwt'])
		]
	}
};

Seeing authentication in action

Using Postman and Chrome, the OAuth 2.0 flow can be demonstrated.

To verify the JWT hooks are working as expected, a request is made to /items. Since no JWT is present, this request fails:

Request without JWT Request without JWT

Visiting /auth/google starts the OAuth process to verify a user and create a corresponding JWT:

OAuth request OAuth request

Switching these same requests into Chrome, a user is able to be logged in through Google, which successfully calls the /auth/google/callback route. This call returns a newly created JWT:

Successful OAuth verification Successful OAuth verification

Using this JWT, a request can now be made to the /items and /users services:

Successful items request Successful /items request

Making the same request to /users displays the user created based on the information returned by the Google OAuth system. If using multiple OAuth providers, such as Google and Facebook, it may also be required to normalize user data. This can be done with a custom hook as described in the Feathers documentation.

This token can be stored at the client application's discretion, typically in LocalStorage or a cookie, and sent as a Bearer token in the Authorization header on future requests. If FeathersJS is used on the client, the flow requires minimal integration. This will be discussed in a future article within this series.

Conclusion

Feathers provides a minimal API for registering services. Creating robust authentication may feel like piecing together a puzzle, however, once familiarized with the purpose of the underlying modules can be a quick, community supported solution.

Follow the FeathersJS series with Aquil.io as the client implementation and other authentication flows will be described.

Example code

The full working example used for this article can be found here.

This example was built on Node v9.x with the following dependencies:

"dependencies": {
  "@feathersjs/authentication": "^2.1.5",
  "@feathersjs/authentication-jwt": "^2.0.0",
  "@feathersjs/authentication-oauth2": "^1.0.3",
  "@feathersjs/configuration": "^1.0.2",
  "@feathersjs/express": "^1.2.2",
  "@feathersjs/feathers": "^3.1.4",
  "body-parser": "^1.18.2",
  "feathers-memory": "^2.1.1",
  "passport-google-oauth2": "^0.1.6"
}

References

We’d love to partner with you.