1

I'm currently implementind OpenID/OAuth authorization in my project and using openid-client package for that.

In this package we initialize an openid client with the following code:

const { Issuer } = require('openid-client');
Issuer.discover('https://accounts.google.com') // => Promise
  .then(function (googleIssuer) {
    console.log('Discovered issuer %s %O', googleIssuer.issuer, googleIssuer.metadata);
  });
  
const client = new googleIssuer.Client({
  client_id: 'zELcpfANLqY7Oqas',
  client_secret: 'TQV5U29k1gHibH5bx1layBo0OSAvAbRT3UYW3EWrSYBB5swxjVfWUa1BS8lqzxG/0v9wruMcrGadany3',
  redirect_uris: ['http://localhost:3000/cb'],
  response_types: ['code'],
  // id_token_signed_response_alg (default "RS256")
  // token_endpoint_auth_method (default "client_secret_basic")
}); // => Client

How we can implement a singleton client logic? To create a client only once and reuse it all over the application?

I've tried to create a separate class for that but not sure if it is correct:

import { Issuer } from 'openid-client';

export class OpenIdClient {
  

  async createOpenIdClient() {
    const issuer = await Issuer.discover(encodeURI(`http://localhost:3101/.well-knownendpoint`));

    const client = await new issuer.Client({
      client_id: 'clientId',
      client_secret: 'clientSecret'
    })

    return client;
  }
}
Karen
  • 1,249
  • 4
  • 23
  • 46
  • You may wish to redact your client_id and client_secret in the future. If this is just demo code then it's fine, but from a security standpoint, you'd consider your OAuth to be compromised at this point until you re-generate the secret. – UberMario Jan 17 '21 at 21:15
  • @UberMario, thank you for the advice, I'll not use hardcoded string values on production, it's just for stackoverflow example :) – Karen Jan 17 '21 at 21:17

1 Answers1

0

You're close here, and using a class can be a solution, though I find classes to be less useful if all you're after is a singleton. Classes are great when you want to instance more than one time (or at least have the ability to).

I usually just use a plain variable or object for a singleton. No need to use anything more complicated here.

Note there are other answers for how to create singletons, so this is just my own opinion on the best/simplest solution for your needs.

In a separate file (e.g. oauth-client.js):

// oauth-client.js
const { Issuer } = require('openid-client');

Issuer.discover('https://accounts.google.com')
  .then(function (googleIssuer) {
    console.log('Discovered issuer %s %O', googleIssuer.issuer, googleIssuer.metadata);
  });
  
const client = new googleIssuer.Client({
  client_id: 'redacted',
  client_secret: 'redacted',
  redirect_uris: ['http://localhost:3000/cb'],
  response_types: ['code'],
});

module.exports = client;

The first time it's loaded by a file, it'll instantiate. Any subsequent times it's loaded in the node.js app, it won't re-instantiate. It'll reuse the entity that's already in memory.

//other file, let's pretend in the same file path.
const oauthClient = require('./oauth-client');

...

If you're wanting to make sure for yourself, put a log line at the top of the singleton file. Then load the file from two different parts of your app. You'll see that it only logs it out once.


To answer the question in the comments below: Swap these lines for an object destructured approach, which makes it nice for if you want to export multiple things from the file:

...
module.exports = { oauthClient: client }
const { oauthClient } = require('./oauth-client');
...

Since we aren't exporting more than one thing (the client), the original answer is still fine.

UberMario
  • 193
  • 1
  • 13
  • thank you very much! is it possible to write it in ES6 CommonJS syntax? – Karen Jan 17 '21 at 21:51
  • It is in CommonJS syntax. Do you mean using the object destructured approach instead of single variable assignment on module.exports? Just trying to understand what you're asking – UberMario Jan 17 '21 at 23:27