DEV Community

Cover image for Microservices Authentication Strategies: Theory to Practice
Tom Weiss for Aspecto

Posted on • Edited on • Originally published at aspecto.io

Microservices Authentication Strategies: Theory to Practice

In this article, we will walk through common ways of implementing authentication microservices.

We will have 2 parts:

1.The theoretical part talking about OpenID Connect, OAuth 2.0, JWT, etc.

Here I try to save you time wandering through the web and giving you all the basics you need to understand in order to start coding.

2.The practical part, where we will implement two Node.js microservices, one responsible for user authentication via google login, another responsible for greeting the user that has a token created by the previous service. Plus, we add a react JS app to interact with those services.

The Theory

What is authentication?

Authentication is the answer users give us when we ask them “Who are you?”. For us to believe users, they need to go through a process providing some proof.

For example – by providing a username & password or by using a social login provider.

What is authorization?

Authorization is usually relevant when we already know who the user is, thus the user is authenticated (unless we allow anonymous access, but we won’t get into that use case here).

Our users want to perform certain actions in our system, and the process of checking if they are allowed to do it or not is called authorization.

(Note: The reason we’re talking about this authorization in an authentication article is that these terms are often confused, and we need to understand it to understand concepts like OAuth 2.0 & OpenID Connect)

A good real-world analogy for both of the above would be while checking in a hotel room. Authentication is your passport, and authorization is if I’m allowed to enter a certain room (because I booked it).

OAuth 2.0

OAuth 2.0 is an authorization protocol. To understand it best, let’s remember the days when it did not exist. In the image below you can see a Facebook screen asking for our Gmail password to search for our contacts on Facebook and add them as friends.

Facebook from the days before OAuth

Facebook from the days before OAuth, source: https://oauth.net/videos/

Think of what this means: Facebook developers would have access to your Gmail password, and essentially all your Gmail data including emails.

OAuth enables an app like Gmail to grant access solely to specific resources from that app, in this case, your contacts. It does so by creating an access token that can talk to an API and retrieve this data.

OAuth provides us with 2 tokens: a refresh toke and an access token.

The access token is short-lived and enables you to access the restricted API.

The refresh token’s role is to enable us to obtain new access tokens, without requiring the user to log in each time our access token expires, which results in a better user experience.

OpenID Connect (OIDC)

OpenID is an authentication protocol built on top of OAuth 2.0 and its main addition is the ID token. The ID token is intended for use with client-side applications, whereas the access token provided by OAuth 2.0 is meant to be used with the resource server (the API).

OAuth token audiences

OAuth token audiences, source: https://oauth.net/videos/

JWT

JWT – Json Web Token is a standard method for representing claims securely between two parties. The information in the token is digitally signed to avoid tampering.

While OAuth doesn’t enforce token type, a lot of implementations use JWT tokens to store the refresh & access tokens. OpenID Connect on the other hand – defines that the token must be in the
JWT format.

NodeJS Microservices Authentication Strategies

Now that we have a basic understanding of the relevant terms (and before we dive into practice) we can start exploring possibilities to implement authentication in our microservices:

1.The obvious way: use a database to store user data, write your logic for creating users, registration, store passwords, etc. You can then create a form in your client-side where the user logs in, and once logged in you can store user information wherever you see fit (cookies, app state, etc).

2.OpenID Connect: you can use services like Google & Facebook that would enable your users to log in using their corresponding accounts. Then, you create a corresponding user in your database. Here you don’t need to implement any user creation UI or store passwords.

When your user logs in you can store the JWT token in a cookie, and your microservice could know who the user is according to that token. It could also allow or forbid certain actions based on that, but that’s out of the scope of this post.

You could of course combine this with option one, having some of the users created via identity providers like Google/Facebook and others in your own system.

3.Use an identity as a service tool like Auth0 / Okta, which essentially helps with both use cases above and can save you time implementing everything on your own. I won’t dive into this one, but you can check their websites for more info.

For the practical part of this guide I have selected option 2, because I feel it gives the most benefit in understanding authentication in Node.js and you will most likely use it anyway, whether directly or under the hood.

The Practice

Let’s begin implementing our service with OpenID connect enabled.

Here’s what we’re going to create:

  1. account-service – a rest API that handles user creation. We will be using passport with passport-google-oauth strategy, which is based on OpenID Connect.
  2. greeting-service – a simple rest api that greets the user.
  3. A React app that lets the user login with google by consuming account-service and greets the user consuming the greeting-service.

Step 1 – Setting up Google project for login

1.Go to https://console.cloud.google.com/ and register if you haven’t done so yet

2.Create a new project

Google OAuth project creation
Google OAuth project creation

3.Select the newly created project, and click on Create Credentials

4.Select OAuth Client ID

5.Select Configure Consent Screen

6.Select Internal & Hit Create

7.Fill in the name and emails (you can leave the rest blank for now, we will get to it later)

8.In the scopes screen, click add scopes, and then select userinfo.email

Updating selected Google scopes<br>
Updating selected Google scopes

9.Hit Continue. Now we have our consent screen. Let’s go back to credentials & hit create credentials and choose OAuth Client ID

10.Choose your name, and for authorized redirect URI – add “http://localhost:5000/auth/google/callback” and hit create

11.You should now receive a pop-up with client id & client secret. Keep them, we will be using them soon

Step 2 – Creating account-service

Note: throughout this tutorial, for the sake of simplicity we’re using the default code generated with express-generator. So no good-looking typescript-like things to see here.

1. Create the project with express-generator & perform the installs needed

npx express-generator --no-view account-service
cd account-service
npm install
npm install --save jsonwebtoken passport passport-google-oauth cors
Enter fullscreen mode Exit fullscreen mode

2. Express generator created some default routes.

We won’t be using the users’ one, but use the index.js. So I’m deleting references to it. The initial project looks like this.

Express generator

3. In bin/www let’s change the port from 3000 to 5000

var port = normalizePort(process.env.PORT || '5000');
Enter fullscreen mode Exit fullscreen mode

4. Add passport

Passport is authentication middleware for Nodejs. It’s very simple to use and supports all the options we need so I chose to use it. Passport uses strategies to handle certain types of login. passport-google-login is a strategy for logging in with Google and is based on OpenID Connect.

In our app.js file – let’s add the following code so that it looks like this:

const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const cors = require('cors');
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth').OAuth2Strategy;

const indexRouter = require('./routes/index');

const app = express();
// This is here for our client side to be able to talk to our server side. you may want to be less permissive in production and define specific domains.
app.use(cors());

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));


app.use(passport.initialize());
app.use(passport.session());

app.use('/', indexRouter);

passport.serializeUser(function(user, cb) {
 cb(null, user);
});

passport.deserializeUser(function(obj, cb) {
 cb(null, obj);
});

passport.use(new GoogleStrategy({
   clientID: 'your-google-client-id',
   clientSecret: 'your-google-client-secret',
   callbackURL: "http://localhost:5000/auth/google/callback"
 },
 function(accessToken, refreshToken, profile, done) {
   // here you can create a user in the database if you want to
   return done(null, profile);
 }
));

module.exports = app;
Enter fullscreen mode Exit fullscreen mode

Notice we have a few interesting things here.

One – we added cors for the client-side (localhost:3000) to be able to make requests from our server-side. Having security in mind – you probably want to only allow specific domains in production.

Serialize & deserialize user – these are functions responsible for serializing & deserializing the user to and from the session.

GoogleStrategy – this is how we tell Passport we would be using google authentication.

Remember the client id & secret you saved earlier? Now is a good time to insert them.

5. Adding authentication routes

Now let’s go to the routes/index.js file and add the relevant routes.

const express = require('express');
const router = express.Router();
const passport = require('passport');
const jwt = require('jsonwebtoken');

router.get('/', function(req, res, next) {
 res.render('index', { title: 'Express' });
});

const TOKEN_SECRET = 'SECRET';

router.get('/auth/google',
 passport.authenticate('google', { scope : ['profile', 'email'] }));

router.get('/auth/google/callback',
 passport.authenticate('google', { failureRedirect: '/error' }),
 function(req, res) {
   const token = jwt.sign({ id: req.user.sub, name: req.user.name }, TOKEN_SECRET, {
     expiresIn: 60 * 60,
   });
   res.cookie('auth', token, { httpOnly: true });
   res.redirect('http://localhost:3000/');
});

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

TOKEN_SECRET – we will be using this to sign our JWT token.

/auth/google – the actual google login route.

Users are redirected to Google. Once they are done, they are redirected back to our server at /auth/google/callback. There we can create our JWT token.

Once created, we add it on the request as an httpOnly cookie, so that it is not accessible to javascript code (which is good in terms of security). You’ll soon see how this works on the client end.

When ready, we redirect back to the client-side.

Side note: we store the name inside the JWT for demonstration purposes, but you probably don’t need it, and may not be a best practice in terms of security.

Now that we’re done with our account service, let’s go on to the client-side.

Step 3: Create a client-side react application

npx create-react-app auth-strategies-client
cd auth-strategies-client/
yarn add axios
Enter fullscreen mode Exit fullscreen mode

We now have a default react app. Let’s modify the app js file to contain a link to google authentication.

import logo from './logo.svg';
import './App.css';

function App() {
 return (
   <div className="App">
     <header className="App-header">
       <img src={logo} className="App-logo" alt="logo" />
       <a href="http://localhost:5000/auth/google">Sign in with Google</a>
     </header>
   </div>
 );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

And after running it with yarn start, it looks like this:
React sign in with Google

Click on “sign in with Google”. After doing so you should be redirected to Google for authentication.

(Make sure you run npm start on accounts-service for it to run in port 5000).

Let’s take a look at what happened after our call to the accounts-service/auth/google/callback:

1.accounts-service made a POST request to google, which returned an access token & id token.

Aspecto live flow viewer

Zoom in to the response
Zoom in to the response

Zoom in to the services
Zoom in to the services

[P.S. These images are generated using Aspecto’s live flow viewer. If you’d like to visualize your services the way I did, you should try Aspecto (for free). It takes 2 minutes to start sending traffic].

2.It used that token to make another GET request to google to get the user’s personal info.

make another GET request to google

make another GET request to google, Zoom in to the response
Zoom in to the response

3.Our microservice has redirected us back to the client-side, with set-cookie in the response for our auth cookie creation.

Our microservice has redirected us back to the client-side

Therefore – after that, you will be redirected back to the exact same screen, with one difference: If you open your browser’s DevTools, you should see an auth cookie that is http only:

Browser’s DevTools

Now we’re ready to make use of this token. That leads us to our greeting-service. Keep the client-side handy as we will modify it soon.

Step 4 – Setting up the greetings-service

1. Let’s create our service

npx express-generator --no-view greetings-service
cd greetings-service
npm install
npm install --save passport passport-jwt cors
Enter fullscreen mode Exit fullscreen mode

2. Remove user.js file and routes in app.js, this is our fresh start:

const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const logger = require('morgan');

const indexRouter = require('./routes/index');

const app = express();

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);

module.exports = app;
Enter fullscreen mode Exit fullscreen mode

3. Modify port to 5001 in bin/www

var port = normalizePort(process.env.PORT || '5001');
Enter fullscreen mode Exit fullscreen mode

4. Set up passport to read our JWT token by modifying app.js:

const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const cors = require('cors');

const indexRouter = require('./routes/index');

const app = express();
app.use(cors({ credentials: true, origin: 'http://localhost:3000' }));


app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

const passport = require('passport');
const JwtStrategy = require('passport-jwt').Strategy,
 ExtractJwt = require('passport-jwt').ExtractJwt;

app.use(passport.initialize());
app.use(passport.session());

app.use('/', indexRouter);

const cookieExtractor = function(req) {
 let token = null;
 if (req && req.cookies)
 {
   token = req.cookies['auth'];
 }
 return token;
};

const TOKEN_SECRET = 'SECRET';

const opts = {
 jwtFromRequest: ExtractJwt.fromExtractors([cookieExtractor]),
 secretOrKey: TOKEN_SECRET,
};

passport.use(
 'jwt',
 new JwtStrategy(opts, (jwt_payload, done) => {
   try {
     console.log('jwt_payload', jwt_payload);
     done(null, jwt_payload);
   } catch (err) {
     done(err);
   }
 }),
);

module.exports = app;
Enter fullscreen mode Exit fullscreen mode

Here we need to pay attention to a few interesting things.

cookieExtractor is responsible for reading the token from the httpOnly cookie we created earlier and will be passed along with the request (more on that later).

Notice we must use the same TOKEN_SECRET we used to create the token in order to read it or we will get an invalid signature error when reading.

The extractor is then passed to the JwtStrategy, which is responsible for providing us with the jwt_payload. We could be fetching more info about the user from the database if we were to add a database, but for the sake of simplicity, I decided not to.

Now we’ll add our greeting route in index.js:

const express = require('express');
const router = express.Router();
const passport = require('passport');

router.get('/', function(req, res, next) {
 res.render('index', { title: 'Express' });
});

router.get('/greetme', (req, res, next) => {
 passport.authenticate('jwt', { session: false }, (err, user, info) => {
   if (err) {
     console.log('error is', err);
     res.status(500).send('An error has occurred, we cannot greet you at the moment.');
   }
   else {
     res.send({ success: true, fullName: `${user.name.givenName} ${user.name.familyName}` })
   }
 })(req, res, next);
});


module.exports = router;
Enter fullscreen mode Exit fullscreen mode

What happens here is that Passport extracts the info from the JWT for us, and all we do is return it to the client.

Start the greetings-service in port 5001:

npm start
Enter fullscreen mode Exit fullscreen mode

Step 5 – Modify the client side to send the httpOnly cookie

Since we want the client JWT token to not be accessible to any malicious javascript code, we have stored it in an httpOnly cookie.

(Side note: in real life, you may want to also make it secure so that it’s only accessible via HTTPS).

So, we want to perform our greeting request to the greetings-service. For that, we need to send the contents of the cookie to the server. Let’s do that then.

Back in our client-side react application, we modify App.js by adding a button:

import React, { useState } from 'react';
import axios from "axios";
import logo from './logo.svg';
import './App.css';

function App() {
 const [name, setName] = useState('');
 return (
   <div className="App">
     <header className="App-header">
       <img src={logo} className="App-logo" alt="logo" />
       <a href="http://localhost:5000/auth/google">Sign in with Google</a>
       <br />
       <button onClick={async () => {
         const result = await axios.get('http://localhost:5001/greetme', {
           withCredentials: true
         });
         setName(result.data.fullName);
       }}>Greet me please</button>
       {name && <span>{`Hi, ${name}`}</span>}
     </header>
   </div>
 );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Now once we get a response with the current user’s full name, we would see “Hi, full name”.

Notice we added a basic axios with withCredentials: true – that is what makes the cookies pass alongside our request, for the server to extract.

And this emphasizes what happened here behind the scenes:

Aspecto live flow viewer

A simple GET request that returns a JSON with the user’s full name as it came from google and stored in the JWT token.

Here is what we get after clicking the button:

React Tom Zach

That’s it!

We have successfully created the account service for registration & JWT creation, and the greetings service that knows how to read the JWT token and provide data about the user.

I hope this helped you get a better understanding of authentication in general, and specifically while implementing it in nodejs.

Check out some of my other articles, like this one: Lerna Hello World: How to Create a Monorepo for Multiple Node Packages.

Top comments (4)

Collapse
 
skona27 profile image
Jakub Skoneczny

Hi!
Nice article :)

Have you considered creating another service to serve as an API gateway? Then, your client could talk to one service which would proxy requests to microservices. Also, the authentication would be a little simpler since you could add guards inside the gateway, and other microservices would not have to implement auth logic :)

Collapse
 
tomweiss profile image
Tom Weiss • Edited

Hi, thanks! I preferred to keep the post as simple as possible with less "moving parts", but I agree with you - it's definitely a great addition that the readers can implement :)

Collapse
 
vudknguyen profile image
Vu Nguyen

Great article, I'll definitely checkout Aspecto, really cool tool

Collapse
 
tomweiss profile image
Tom Weiss

Thank you, glad to hear you enjoyed it :)