From 69ce9bf95fb53e1ef258de02de916789ec9663af Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 30 Nov 2017 23:12:28 +0000 Subject: [PATCH] WIP jwk get and verify --- oauth3.core.js | 149 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 147 insertions(+), 2 deletions(-) diff --git a/oauth3.core.js b/oauth3.core.js index e92af65..8fdd7e8 100644 --- a/oauth3.core.js +++ b/oauth3.core.js @@ -17,6 +17,13 @@ err.code = params.error.code || params.error; return err; } + , create: function (opts) { + var err = new Error(opts.message); + err.code = opts.code; + err.uri = opts.uri || opts.url; + err.subErr = opts.subErr; + return err; + } } , _binStr: { bufferToBinStr: function (buf) { @@ -205,6 +212,75 @@ } return str; } + , jwk: { + get: function (decoded) { + return OAUTH3.discover(decoded.payload.iss).then(function (directives) { + var urlObj = OAUTH3.jwk.url(directives, decoded); + return OAUTH3.request(urlObj).catch(function (err) { + return PromiseA.reject({ + message: 'failed to retrieve public key from token issuer' + , code: 'E_NO_PUB_KEY' + , url: 'https://oauth3.org/docs/errors#E_NO_PUB_KEY' + , subErr: err.toString() + }); + }); + }, function (err) { + return PromiseA.reject({ + message: 'token issuer is not a valid OAuth3 provider' + , code: 'E_INVALID_ISS' + , url: 'https://oauth3.org/docs/errors#E_INVALID_ISS' + , subErr: err.toString() + }); + }).then(function (res) { + if (res.data.error) { + return PromiseA.reject(res.data.error); + } + return res.data; + }); + } + , verifyToken: function (token) { + var decoded; + + if (!token) { + return PromiseA.reject({ + message: 'no token provided' + , code: 'E_NO_TOKEN' + , url: 'https://oauth3.org/docs/errors#E_NO_TOKEN' + }); + } + + try { + decoded = OAUTH3.jwt.decode(token, {complete: true}); + } catch (e) {} + if (!decoded) { + return PromiseA.reject({ + message: 'provided token not a JSON Web Token' + , code: 'E_NOT_JWT' + , url: 'https://oauth3.org/docs/errors#E_NOT_JWT' + }); + } + + return OAUTH3.jwk.get(decoded).then(function (jwk) { + var opts = {}; + if (Array.isArray(jwk.alg)) { + opts.algorithms = jwk.alg; + } else if (typeof jwk.alg === 'string') { + opts.algorithms = [ jwk.alg ]; + } + + try { + return OAUTH3.jwt.verify(token, jwk, opts); + } catch (err) { + return PromiseA.reject({ + message: 'token verification failed' + , code: 'E_INVALID_TOKEN' + , url: 'https://oauth3.org/docs/errors#E_INVALID_TOKEN' + , subErr: err.toString() + }); + } + }); + } + } , jwt: { // decode only (no verification) decode: function (token, opts) { @@ -228,7 +304,7 @@ , payload: JSON.parse(OAUTH3._base64.decodeUrlSafe(parts[1])) }; } - , verify: function (token, jwk) { + , verify: function (token, jwk/*, opts*/) { if (!OAUTH3.crypto) { return OAUTH3.PromiseA.reject(new Error("OAuth3 crypto library unavailable")); } @@ -237,9 +313,15 @@ var parts = token.split(/\./g); var data = OAUTH3._binStr.binStrToBuffer(parts.slice(0, 2).join('.')); var signature = OAUTH3._base64.urlSafeToBuffer(parts[2]); + var decoded = OAUTH3.jwt.decode(token, { complete: true }); + // TODO disallow none and hmac algorithms + // https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/ + if (!decoded.header.alg || 'none' === decoded.header.alg.toString() || /^HS/i.test(decoded.header.alg.toString())) { + throw new Error("token algorithm '" + decoded.header.alg + "' is not accepted"); + } return OAUTH3.crypto.core.verify(jwk, data, signature).then(function () { - return OAUTH3.jwt.decode(token); + return decoded; }); } , sign: function (payload, jwk) { @@ -335,6 +417,69 @@ opts._pathname = ".well-known/oauth3/scopes.json"; return OAUTH3.urls.rpc(providerUri, opts); } + , jwk: function (directives, decoded) { + var sub = decoded.payload.sub || decoded.payload.ppid || decoded.payload.appScopedId; + if (!sub) { + throw OAUTH3.error.create({ + message: 'token missing sub' + , code: 'E_MISSING_SUB' + , url: 'https://oauth3.org/docs/errors#E_MISSING_SUB' + }); + } + var kid = decoded.header.kid || decoded.payload.kid; + if (!kid) { + throw OAUTH3.error.create({ + message: 'token missing kid' + , code: 'E_MISSING_KID' + , url: 'https://oauth3.org/docs/errors#E_MISSING_KID' + }); + } + if (!decoded.payload.iss) { + throw OAUTH3.error.create({ + message: 'token missing iss' + , code: 'E_MISSING_ISS' + , url: 'https://oauth3.org/docs/errors#E_MISSING_ISS' + }); + } + + var args = (directives || {}).retrieve_jwk; + if (typeof args === 'string') { + args = { url: args, method: 'GET' }; + } + if (typeof (args || {}).url !== 'string') { + throw OAUTH3.error.create({ + message: 'token issuer does not support retrieving JWKs' + , code: 'E_INVALID_ISS' + , url: 'https://oauth3.org/docs/errors#E_INVALID_ISS' + }); + } + + var params = { + sub: sub + , kid: kid + }; + var url = args.url; + var body; + Object.keys(params).forEach(function (key) { + if (url.indexOf(':'+key) !== -1) { + url = url.replace(':'+key, params[key]); + delete params[key]; + } + }); + if (Object.keys(params).length > 0) { + if ('GET' === (args.method || 'GET').toUpperCase()) { + url += '?' + OAUTH3.query.stringify(params); + } else { + body = params; + } + } + + return { + url: OAUTH3.url.resolve(directives.api, url) + , method: args.method + , data: body + }; + } , implicitGrant: function (directive, opts) { // // Example Implicit Grant Request