Created
March 9, 2022 20:33
-
-
Save theBoEffect/c4d98f3d8503089afea90c405858965d to your computer and use it in GitHub Desktop.
An example of validating an United Effects Core OIDC token with node and passport
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import jwt from 'jsonwebtoken'; | |
import qs from 'querystring'; | |
import jkwsClient from 'jwks-rsa'; | |
import axios from 'axios'; | |
import Boom from '@hapi/boom'; | |
import passport from "passport"; | |
import {Strategy as BearerStrategy} from "passport-http-bearer"; | |
// CORE is the domain + authGroup of the Core EOS OIDC connection (aka issuer) | |
const CORE = process.env.CORE_EOS; | |
// Your auth group ID | |
const AUTH_GROUP = process.env.CORE_AUTH_GROUP; | |
// OIDC Client ID for authorizations | |
const CLIENT_ID = process.env.CORE_CLIENT_ID; | |
// OIDC Client Secret | |
const CLIENT_SECRET = process.env.CORE_CLIENT_SECRET; | |
// Your API DNS address | |
const AUDIENCE = process.env.SWAGGER | |
// Helper function | |
const jwtCheck = /^([A-Za-z0-9\-_~+\/]+[=]{0,2})\.([A-Za-z0-9\-_~+\/]+[=]{0,2})(?:\.([A-Za-z0-9\-_~+\/]+[=]{0,2}))?$/; | |
function isJWT(str) { | |
return jwtCheck.test(str); | |
} | |
async function getUser(token) { | |
const url = `${CORE}/me`; | |
const options = { | |
url, | |
method: 'get', | |
headers: { | |
authorization: `bearer ${token}` | |
} | |
} | |
const result = await axios(options); | |
return result.data; | |
} | |
async function introspect(token) { | |
const introspection = `${CORE}/token/introspection`; | |
const options = { | |
url: introspection, | |
method: 'post', | |
auth: { | |
username: CLIENT_ID, | |
password: CLIENT_SECRET | |
}, | |
data: qs.stringify({ | |
token, | |
'token-hint': 'access_token' | |
}) | |
} | |
const result = await axios(options); | |
return result.data; | |
} | |
async function runDecodedChecks(token, issuer, decoded, authGroup) { | |
if(decoded.nonce) { | |
console.info('This is an ID Token'); | |
throw Boom.unauthorized('ID Tokens can not be used for API Access'); | |
} | |
if(decoded.iss !== issuer) { | |
console.info('Issuer is wrong'); | |
throw Boom.unauthorized('Token issuer not recognized'); | |
} | |
if(!decoded.group) { | |
console.info('Auth Group issue'); | |
throw Boom.unauthorized('No Auth Group detected in token'); | |
} | |
if(decoded.group !== authGroup) { | |
console.info('AG mismatch'); | |
throw Boom.unauthorized('Auth Group does not match'); | |
} | |
if(typeof decoded.aud === 'string') { | |
if(decoded.aud !== AUDIENCE) { | |
console.info('Audience Issue'); | |
throw Boom.unauthorized('Token audience is not valid'); | |
} | |
} | |
if(Array.isArray(decoded.aud)) { | |
if(!decoded.aud.includes(AUDIENCE)) { | |
console.info('Audience Issue'); | |
throw Boom.unauthorized('Token audience is not valid'); | |
} | |
} | |
if (decoded.client_id) { | |
if(decoded.client_id !== CLIENT_ID) { | |
console.info('wrong client'); | |
throw Boom.unauthorized('Unexpected Client ID'); | |
} | |
} | |
//check sub if present | |
if(decoded.sub && decoded.client_id !== decoded.sub) { | |
let user; | |
if(decoded.email) { | |
user = { | |
sub: decoded.sub, | |
email: decoded.email | |
} | |
} else { | |
user = await getUser(token); | |
} | |
if(!user) throw Boom.unauthorized('Token should include email'); | |
return { ...user, decoded }; | |
} | |
// client_credential - note, permissions may still stop the request | |
if((decoded.client_id === decoded.sub) || (!decoded.sub && decoded.client_id)) { | |
const out = { | |
client_credential: true, | |
sub: decoded.client_id, | |
client_id: decoded.client_id | |
}; | |
return { ...out, decoded }; | |
} | |
return decoded; | |
} | |
async function oidcValidate(req, token, next) { | |
try { | |
const authGroup = AUTH_GROUP; | |
const issuer = CORE; | |
if(isJWT(token)){ | |
const client = jkwsClient({ | |
jwksUri: `${CORE}/jwks` | |
}) | |
function getKey(header, cb) { | |
client.getSigningKey(header.kid, (err, key) => { | |
if(err) cb(err); | |
const signingKey = key.getPublicKey || key.rsaPublicKey; | |
cb(null, signingKey); | |
}) | |
} | |
const decoded = await jwt.verify(token, getKey); | |
if(decoded) { | |
const result = await runDecodedChecks(token, issuer, decoded, authGroup); | |
return next(null, result, { token }); | |
} | |
} | |
//opaque token | |
const inspect = await introspect(token); | |
if(inspect) { | |
if (inspect.active === false) return next(null, false); | |
const result = await runDecodedChecks(token, issuer, inspect, authGroup); | |
return next(null, result, { token }); | |
} | |
return next(null, false); | |
} catch (error) { | |
console.error(error); | |
return next(null, false); | |
} | |
} | |
passport.serializeUser((user, done) => { | |
done(null, user._id); | |
}); | |
passport.deserializeUser((user, done) => { | |
done(null, user); | |
}); | |
passport.use('oidc', new BearerStrategy({ | |
passReqToCallback: true | |
}, oidcValidate)); | |
export default { | |
isAuthenticated: passport.authenticate(['oidc'], { session: false }), | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment