|
|
@@ -1,129 +1,72 @@ |
|
|
/** |
|
|
The MIT License (MIT) |
|
|
|
|
|
Copyright (c) 2017 Fredrik Westmark |
|
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
|
|
of this software and associated documentation files (the "Software"), to deal |
|
|
in the Software without restriction, including without limitation the rights |
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
|
copies of the Software, and to permit persons to whom the Software is |
|
|
furnished to do so, subject to the following conditions: |
|
|
|
|
|
The above copyright notice and this permission notice shall be included in all |
|
|
copies or substantial portions of the Software. |
|
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|
|
SOFTWARE. |
|
|
**/ |
|
|
* The MIT License (MIT) |
|
|
* Copyright (c) 2022 PaperNick |
|
|
* |
|
|
* Resouces used: |
|
|
* https://github.com/auth0/node-jwks-rsa |
|
|
* https://github.com/auth0/node-jsonwebtoken |
|
|
* https://auth0.com/blog/navigating-rs256-and-jwks/ |
|
|
* https://gist.github.com/westmark/faee223e05bcbab433bfd4ed8e36fb5f |
|
|
* https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/ |
|
|
*/ |
|
|
|
|
|
const AUTH0_AUTHORITY = 'https://auth0.tenant.com'; |
|
|
const AUTH0_AUDIENCE = 'https://api.yourapp.com/'; |
|
|
|
|
|
const jwksRsa = require('jwks-rsa'); |
|
|
const jwt = require('jsonwebtoken'); |
|
|
|
|
|
const jwksClient = jwksRsa({ |
|
|
jwksUri: `${AUTH0_AUTHORITY.replace(/\/+$/, '')}/.well-known/jwks.json`, |
|
|
}); |
|
|
|
|
|
/** |
|
|
Based on https://auth0.com/blog/navigating-rs256-and-jwks/ |
|
|
**/ |
|
|
|
|
|
const request = require( 'request' ); |
|
|
const jwt = require( 'jsonwebtoken' ); |
|
|
|
|
|
function certToPEM( cert ) { |
|
|
let pem = cert.match( /.{1,64}/g ).join( '\n' ); |
|
|
pem = `-----BEGIN CERTIFICATE-----\n${ cert }\n-----END CERTIFICATE-----\n`; |
|
|
return pem; |
|
|
} |
|
|
|
|
|
let jwks = null; |
|
|
|
|
|
function fetchJWKS( tenant ) { |
|
|
if ( jwks ) { |
|
|
return Promise.resolve(); |
|
|
* Attempt to parse the given JWT token. Throws an Error if token is invalid. |
|
|
* |
|
|
* @param {string} token |
|
|
* @throws {Error} |
|
|
* @returns {JwtPayload} |
|
|
*/ |
|
|
async function parseJwtToken(token) { |
|
|
if (!token) { |
|
|
throw new Error('Missing authorization token'); |
|
|
} |
|
|
return new Promise( ( resolve, reject ) => { |
|
|
request( |
|
|
{ |
|
|
uri: `https://${ tenant }/.well-known/jwks.json`, |
|
|
strictSsl: true, |
|
|
json: true, |
|
|
}, |
|
|
( err, res ) => { |
|
|
if ( err ) { |
|
|
reject( err ); |
|
|
} else if ( res.statusCode < 200 || res.statusCode >= 300 ) { |
|
|
reject( new Error( res.body && ( res.body.message || res.body ) ) ); |
|
|
} else { |
|
|
jwks = res.body.keys; |
|
|
resolve(); |
|
|
} |
|
|
} |
|
|
); |
|
|
} ); |
|
|
} |
|
|
|
|
|
function getJWKS() { |
|
|
return jwks; |
|
|
} |
|
|
|
|
|
function getJWKSSigningKeys() { |
|
|
return jwks |
|
|
.filter( |
|
|
( key ) => |
|
|
key.use === 'sig' && // JWK property `use` determines the JWK is for signing |
|
|
key.kty === 'RSA' && // We are only supporting RSA (RS256) |
|
|
key.kid && // The `kid` must be present to be useful for later |
|
|
( ( key.x5c && key.x5c.length ) || ( key.n && key.e ) ) // Has useful public keys |
|
|
) |
|
|
.map( ( key ) => ( { kid: key.kid, nbf: key.nbf, publicKey: certToPEM( key.x5c[ 0 ] ) } ) ); |
|
|
} |
|
|
|
|
|
function getJWKSSigningKey( kid ) { |
|
|
return getJWKSSigningKeys().find( ( key ) => key.kid === kid ); |
|
|
} |
|
|
|
|
|
function extractAuthenicationToken( req ) { |
|
|
const authHeader = req.headers.authorization; |
|
|
const parts = authHeader.split( ' ' ); |
|
|
|
|
|
if ( parts.length !== 2 ) { |
|
|
throw new Error( 'credentials_required', { message: 'No authorization token was found' } ); |
|
|
// Decode without verifying if the signature is valid. |
|
|
// Warning: do not access decoded token payload before verifying with jwt.verify() |
|
|
const decodedToken = jwt.decode(token, { complete: true }); |
|
|
if (!decodedToken || decodedToken.header.alg !== 'RS256') { |
|
|
// Only RS256 tokens are supported at the moment |
|
|
throw new Error('Invalid token or algorithm'); |
|
|
} |
|
|
|
|
|
const scheme = parts[ 0 ]; |
|
|
if ( !/^Bearer$/i.test( scheme ) ) { |
|
|
throw new Error( 'credentials_bad_scheme', { |
|
|
message: 'Format is Authorization: Bearer [token]', |
|
|
} ); |
|
|
let signingKey; |
|
|
try { |
|
|
signingKey = await jwksClient.getSigningKey(decodedToken.header.kid); |
|
|
} catch (error) { |
|
|
throw new Error('Could not retrieve key to verify token'); |
|
|
} |
|
|
|
|
|
return parts[ 1 ]; |
|
|
try { |
|
|
const jwtPayload = jwt.verify(token, signingKey.getPublicKey(), { audience: AUTH0_AUDIENCE }); |
|
|
return jwtPayload; |
|
|
} catch (error) { |
|
|
throw new Error('The token is invalid or has expired'); |
|
|
} |
|
|
} |
|
|
|
|
|
async function verifyJWTToken( tenant, req ) { |
|
|
await fetchJWKS( tenant ); |
|
|
const token = extractAuthenicationToken( req ); |
|
|
const decodedToken = jwt.decode( token, { complete: true } ); |
|
|
const { header } = decodedToken; |
|
|
|
|
|
if ( !header || header.alg !== 'RS256' ) { |
|
|
throw new Error( 'Token is not RS256 encoded' ); |
|
|
/** |
|
|
* Attempt to parse the given JWT token. Returns undefined on error. |
|
|
* |
|
|
* @param {string} token |
|
|
* @returns {JwtPayload|undefined} |
|
|
*/ |
|
|
async function parseJwtTokenQuiet(token) { |
|
|
try { |
|
|
return await parseJwtToken(token); |
|
|
} catch (error) { |
|
|
return; |
|
|
} |
|
|
|
|
|
const key = getJWKSSigningKey( header.kid ); |
|
|
const actualKey = key.publicKey || key.rsaPublicKey; |
|
|
|
|
|
return new Promise( ( resolve, reject ) => { |
|
|
jwt.verify( token, actualKey, { algorithms: [ 'RS256' ] }, ( err, decoded ) => { |
|
|
if ( err ) { |
|
|
reject( new Error( 'invalid_token', err ) ); |
|
|
} else { |
|
|
resolve( decoded ); |
|
|
} |
|
|
} ); |
|
|
} ); |
|
|
} |
|
|
|
|
|
module.exports = { |
|
|
verifyJWTToken, |
|
|
}; |
|
|
module.exports = { parseJwtToken, parseJwtTokenQuiet }; |