Skip to content

Instantly share code, notes, and snippets.

@ajmas
Created May 4, 2022 21:56
Show Gist options
  • Save ajmas/ec9f1a1020982f028e486efd142d47ae to your computer and use it in GitHub Desktop.
Save ajmas/ec9f1a1020982f028e486efd142d47ae to your computer and use it in GitHub Desktop.
Code for dealing with OpenId tokens, in node.js
import axios from 'axios';
import { Request, Response, NextFunction } from 'express';
import jwt, { GetPublicKeyOrSecret, Secret } from 'jsonwebtoken';
import jwksClient, { JwksClient } from 'jwks-rsa';
import UnauthenticatedError from '../errors/UnauthenticatedError';
import { getLogger } from './logging';
const logger = getLogger('openid');
/**
* Home crafted OpenId solution, since we weren't finding anything
* useful or straightforward that already existed.
*/
class OpenIdProvider {
baseUrl: string;
openIdConfig: Record<string, any>;
jwksClient: JwksClient;
constructor (baseUrl: string) {
this.baseUrl = baseUrl;
}
async init () {
const url = `${this.baseUrl}/.well-known/openid-configuration`;
logger.debug(`OpenIdProvider URL: ${url}`);
const response = await axios.get(url);
if (response.data) {
this.openIdConfig = response.data;
}
logger.debug(`OpenIdProvider openid-configuration: ${JSON.stringify(this.openIdConfig, undefined, 2)}`);
logger.debug(`OpenIdProvider jwks_uri: ${this.openIdConfig.jwks_uri}`);
this.jwksClient = jwksClient({
jwksUri: this.openIdConfig.jwks_uri
});
}
private async jwtVerify (token: string, secretOrPublicKey: Secret | GetPublicKeyOrSecret): Promise<object | undefined> {
return new Promise<object | undefined>((resolve, reject) => {
jwt.verify(token, secretOrPublicKey, (err: any, decoded: object | undefined) => {
if (err) {
reject(err);
} else {
resolve(decoded);
}
});
});
}
private async getKey (header, callback) {
this.jwksClient.getSigningKey(header.kid, (error: any, key: Record<string, any>) => {
if (error) {
callback(error);
}
if (key) {
const signingKey = key.publicKey || key.rsaPublicKey;
callback(null, signingKey);
} else {
callback(new Error('getSigningKey resulted in null key'));
}
});
}
async getUser (token: string): Promise<Record<string, any>> {
const response = await axios.get(this.openIdConfig.userinfo_endpoint, {
headers: {
'authorization': `Bearer ${token}`
}
});
if (response.data) {
return response.data;
}
return undefined;
}
private async verifyToken (token: string): Promise<Record<string, any>> {
try {
return this.jwtVerify(token, this.getKey.bind(this));
} catch (error) {
throw new UnauthenticatedError('expired or invalid jwt');
}
}
middleware (postAuthHandler?: Function) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
logger.debug('authorization', req.headers.authorization);
// if there is no token then pass on through. we do this, since
// some endpoints may not need an authentication session for access
if (req.headers.authorization && req.headers.authorization.toLowerCase().indexOf('bearer ') === 0) {
const token = req.headers.authorization.split(' ')[1];
const decodedToken = await this.verifyToken(token);
res.locals.decodedOpenIdToken = decodedToken;
res.locals.user = await this.getUser(token);
if (res.locals.user.sub) {
res.locals.user.id = res.locals.user.sub;
}
// This is somewhat of a hack and needs a better approach
if (postAuthHandler) {
res.locals.user = postAuthHandler(res.locals.user);
}
req.user = res.locals.user;
}
next();
} catch (error) {
next(error);
}
};
}
}
async function createProvider (baseUrl: string) {
const provider = new OpenIdProvider(baseUrl);
await provider.init();
return provider;
}
export default OpenIdProvider;
export { createProvider };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment