From c38554a9dda8e317e416e865bb83858e0d638fda Mon Sep 17 00:00:00 2001 From: tigerbot Date: Tue, 25 Jul 2017 17:18:14 -0600 Subject: [PATCH 01/21] added check for non-expired refresh token for session refresh --- oauth3.core.js | 72 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/oauth3.core.js b/oauth3.core.js index f14d429..91975e8 100644 --- a/oauth3.core.js +++ b/oauth3.core.js @@ -208,14 +208,16 @@ // 'abc.qrs.xyz' // [ 'abc', 'qrs', 'xyz' ] - // [ {}, {}, 'foo' ] - // { header: {}, payload: {}, signature: '' } + // {} var parts = str.split(/\./g); - var jsons = parts.slice(0, 2).map(function (urlsafe64) { - return JSON.parse(OAUTH3._base64.decodeUrlSafe(urlsafe64)); - }); + var err; + if (parts.length !== 3) { + err = new Error("Invalid JWT: required 3 '.' separated components not "+parts.length); + err.code = 'E_INVALID_JWT'; + throw err; + } - return { header: jsons[0], payload: jsons[1] }; + return JSON.parse(OAUTH3._base64.decodeUrlSafe(parts[1])); } , verify: function (jwk, token) { var parts = token.split(/\./g); @@ -224,20 +226,27 @@ return OAUTH3.crypto.core.verify(jwk, data, signature); } - , freshness: function (tokenMeta, staletime, _now) { - staletime = staletime || (15 * 60); - var now = _now || Date.now(); - var fresh = ((parseInt(tokenMeta.exp, 10) || 0) - Math.round(now / 1000)); - - if (fresh >= staletime) { + , freshness: function (tokenMeta, staletime, now) { + // If the token doesn't expire then it's always fresh. + if (!tokenMeta.exp) { return 'fresh'; } - if (fresh <= 0) { - return 'expired'; + staletime = staletime || (15 * 60); + now = now || Date.now(); + // This particular number used to check if time is in milliseconds or seconds will work + // for any date between the years 1973 and 5138. + if (now > 1e11) { + now = Math.round(now / 1000); + } + var exp = parseInt(tokenMeta.exp, 10) || 0; + if (exp < now) { + return 'expired'; + } else if (exp < now + staletime) { + return 'stale'; + } else { + return 'fresh'; } - - return 'stale'; } } , urls: { @@ -338,29 +347,36 @@ // , "username": "<>", "password": "password" } // opts = opts || {}; - var type = 'access_token'; - var grantType = 'refresh_token'; + var refresh_token = opts.refresh_token || (opts.session && opts.session.refresh_token); + var err; + if (!refresh_token) { + err = new Error('refreshing a token requires a refresh token'); + err.code = 'E_NO_TOKEN'; + throw err; + } + if (OAUTH3.jwt.freshness(OAUTH3.jwt.decode(refresh_token)) === 'expired') { + err = new Error('refresh token has also expired, login required again'); + err.code = 'E_EXPIRED_TOKEN'; + throw err; + } var scope = opts.scope || directive.authn_scope; - var clientSecret = opts.client_secret; - var args = directive[type]; + var args = directive.access_token; var params = { - "grant_type": grantType - , "refresh_token": opts.refresh_token || (opts.session && opts.session.refresh_token) + "grant_type": 'refresh_token' + , "refresh_token": refresh_token , "response_type": 'token' , "client_id": opts.client_id || opts.client_uri , "client_uri": opts.client_uri - //, "scope": undefined - //, "client_secret": undefined , debug: opts.debug || undefined }; var uri = args.url; var body; - if (clientSecret) { + if (opts.client_secret) { // TODO not allowed in the browser console.warn("if this is a browser, you must not use client_secret"); - params.client_secret = clientSecret; + params.client_secret = opts.client_secret; } if (scope) { @@ -482,7 +498,7 @@ oldSession.client_uri = clientUri; // azp // info about the newly-discovered token - oldSession.token = OAUTH3.jwt.decode(oldSession.access_token).payload; + oldSession.token = OAUTH3.jwt.decode(oldSession.access_token); oldSession.token.sub = oldSession.token.sub || (oldSession.token.acx||{}).id @@ -493,7 +509,7 @@ oldSession.token.provider_uri = providerUri; if (oldSession.refresh_token) { - oldSession.refresh = OAUTH3.jwt.decode(oldSession.refresh_token).payload; + oldSession.refresh = OAUTH3.jwt.decode(oldSession.refresh_token); oldSession.refresh.sub = oldSession.refresh.sub || (oldSession.refresh.acx||{}).id || ((oldSession.refresh.axs||[])[0]||{}).appScopedId From 1ca6f0a3245089b594e632131ba3041f3acd072d Mon Sep 17 00:00:00 2001 From: tigerbot Date: Wed, 26 Jul 2017 16:27:03 -0600 Subject: [PATCH 02/21] updated how grants are retrieved --- oauth3.core.js | 7 +- oauth3.issuer.js | 213 +++++++++++++++-------------------------------- 2 files changed, 73 insertions(+), 147 deletions(-) diff --git a/oauth3.core.js b/oauth3.core.js index 91975e8..321c8fe 100644 --- a/oauth3.core.js +++ b/oauth3.core.js @@ -168,9 +168,12 @@ } } , scope: { - stringify: function (scope) { + parse: function (scope) { + return (scope||'').split(/[+, ]+/g); + } + , stringify: function (scope) { if (Array.isArray(scope)) { - scope = scope.join(' '); + scope = scope.join(','); } return scope; } diff --git a/oauth3.issuer.js b/oauth3.issuer.js index 257a196..cacd206 100644 --- a/oauth3.issuer.js +++ b/oauth3.issuer.js @@ -3,39 +3,6 @@ var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3; -OAUTH3.query.parse = function (search) { - // parse a query or a hash - if (-1 !== ['#', '?'].indexOf(search[0])) { - search = search.substring(1); - } - // Solve for case of search within hash - // example: #/authorization_dialog/?state=...&redirect_uri=... - var queryIndex = search.indexOf('?'); - if (-1 !== queryIndex) { - search = search.substr(queryIndex + 1); - } - - var args = search.split('&'); - var argsParsed = {}; - var i, arg, kvp, key, value; - - for (i = 0; i < args.length; i += 1) { - arg = args[i]; - if (-1 === arg.indexOf('=')) { - argsParsed[decodeURIComponent(arg).trim()] = true; - } - else { - kvp = arg.split('='); - key = decodeURIComponent(kvp[0]).trim(); - value = decodeURIComponent(kvp[1]).trim(); - argsParsed[key] = value; - } - } - return argsParsed; -}; -OAUTH3.scope.parse = function (scope) { - return (scope||'').split(/[, ]/g); -}; OAUTH3.url.parse = function (url) { // TODO browser // Node should replace this @@ -58,8 +25,16 @@ OAUTH3.url._isRedirectHostSafe = function (referrerUrl, redirectUrl) { }; OAUTH3.url.checkRedirect = function (client, query) { console.warn("[security] URL path checking not yet implemented"); + if (!query) { + query = client; + client = query.client_uri; + } + client = client.url || client; - var clientUrl = OAUTH3.url.normalize(client.url); + // it doesn't matter who the referrer is as long as the destination + // is an authorized destination for the client in question + // (though it may not hurt to pass the referrer's info on to the client) + var clientUrl = OAUTH3.url.normalize(client); var redirectUrl = OAUTH3.url.normalize(query.redirect_uri); // General rule: @@ -72,6 +47,18 @@ OAUTH3.url.checkRedirect = function (client, query) { return true; } + var callbackUrl = 'https://oauth3.org/docs/errors#E_REDIRECT_ATTACK?'+OAUTH3.query.stringify({ + 'redirect_uri': redirectUrl + , 'allowed_urls': clientUrl + , 'client_id': client + , 'referrer_uri': OAUTH3.uri.normalize(window.document.referrer) + }); + if (query.debug) { + console.log('Redirect Attack'); + console.log(query); + window.alert("DEBUG MODE checkRedirect error encountered. Check the console."); + } + location.href = callbackUrl; return false; }; OAUTH3.url.redirect = function (clientParams, grants, tokenOrError) { @@ -195,14 +182,12 @@ OAUTH3.urls.grants = function (directive, opts) { console.warn("You should supply options.referrer"); } if (!opts.method) { - console.warn("You must supply options.method as either 'GET', or 'POST'"); + console.warn("You should supply options.method as either 'GET', or 'POST'"); + opts.method = 'GET'; } if ('POST' === opts.method) { if ('string' !== typeof opts.scope) { - console.warn("You should supply options.scope as a space-delimited string of scopes"); - } - if (-1 === ['token', 'code'].indexOf(opts.response_type)) { - throw new Error("You must supply options.response_type as 'token' or 'code'"); + throw new Error("You must supply options.scope as a comma-delimited string of scopes"); } } @@ -214,12 +199,10 @@ OAUTH3.urls.grants = function (directive, opts) { client_id: opts.client_id , client_uri: opts.client_uri , referrer: opts.referrer - , response_type: opts.response_type , scope: opts.scope - , tenant_id: opts.tenant_id }; - var body; + var body; if ('GET' === opts.method) { url += '?' + OAUTH3.query.stringify(data); } @@ -290,14 +273,13 @@ OAUTH3.authn.resourceOwnerPassword = function (directive, opts) { OAUTH3.authz = {}; OAUTH3.authz.scopes = function (providerUri, session, clientParams) { - // OAuth3.requests.grants(providerUri, {}); // return list of grants - // OAuth3.checkGrants(providerUri, {}); // var clientUri = OAUTH3.uri.normalize(clientParams.client_uri || OAUTH3._browser.window.document.referrer); - var scope = clientParams.scope || ''; - var clientObj = clientParams; - - if (!scope) { - scope = 'oauth3_authn'; + var scope = clientParams.scope || 'oauth3_authn'; + if ('oauth3_authn' === scope) { + // implicit ppid grant is automatic + console.warn('[security] fix scope checking on backend so that we can do automatic grants'); + // TODO check user preference if implicit ppid grant is allowed + //return generateToken(session, clientObj); } return OAUTH3.authz.grants(providerUri, { @@ -305,74 +287,30 @@ OAUTH3.authz.scopes = function (providerUri, session, clientParams) { , client_id: clientUri , client_uri: clientUri , session: session - }).then(function (grantResults) { - var grants; - var grantedScopes; - var grantedScopesMap; - var pendingScopes; - var acceptedScopes; - var scopes = scope.split(/[+, ]/g); - var callbackUrl; - - // it doesn't matter who the referrer is as long as the destination - // is an authorized destination for the client in question - // (though it may not hurt to pass the referrer's info on to the client) - if (!OAUTH3.url.checkRedirect(grantResults.client, clientObj)) { - callbackUrl = 'https://oauth3.org/docs/errors#E_REDIRECT_ATTACK' - + '?redirect_uri=' + clientObj.redirect_uri - + '&allowed_urls=' + grantResults.client.url - + '&client_id=' + clientUri - + '&referrer_uri=' + OAUTH3.uri.normalize(window.document.referrer) - ; - if (clientParams.debug) { - console.log('grantResults Redirect Attack'); - console.log(grantResults); - console.log(clientObj); - window.alert("DEBUG MODE checkRedirect error encountered. Check the console."); - } - location.href = callbackUrl; - return; + }).then(function (results) { + return results.grants; + }, function (err) { + if (!/no .*grants .*found/i.test(err.message)) { + console.error(err); } - - if ('oauth3_authn' === scope) { - // implicit ppid grant is automatic - console.warn('[security] fix scope checking on backend so that we can do automatic grants'); - // TODO check user preference if implicit ppid grant is allowed - //return generateToken(session, clientObj); - } - - grants = (grantResults).grants.filter(function (grant) { - if (clientUri === (grant.azp || grant.oauth_client_id || grant.oauthClientId)) { - return true; + return []; + }).then(function (granted) { + var requested = OAUTH3.scope.parse(scope); + var accepted = []; + var pending = []; + requested.forEach(function (scp) { + if (granted.indexOf(scp) < 0) { + pending.push(scp); + } else { + accepted.push(scp); } }); - grantedScopesMap = {}; - acceptedScopes = []; - pendingScopes = scopes.filter(function (requestedScope) { - return grants.every(function (grant) { - if (!grant.scope) { - grant.scope = 'oauth3_authn'; - } - var gscopes = grant.scope.split(/[+, ]/g); - gscopes.forEach(function (s) { grantedScopesMap[s] = true; }); - if (-1 !== gscopes.indexOf(requestedScope)) { - // already accepted in the past - acceptedScopes.push(requestedScope); - } - else { - // true, is pending - return true; - } - }); - }); - grantedScopes = Object.keys(grantedScopesMap); - return { - pending: pendingScopes // not yet accepted - , granted: grantedScopes // all granted, ever - , requested: scopes // all requested, now - , accepted: acceptedScopes // granted (ever) and requested (now) + requested: requested // all requested, now + , granted: granted // all granted, ever + , accepted: accepted // intersection of granted (ever) and requested (now) + , pending: pending // not yet accepted }; }); }; @@ -381,34 +319,27 @@ OAUTH3.authz.grants = function (providerUri, opts) { client_id: providerUri , debug: opts.debug }).then(function (directive) { + return OAUTH3.request(OAUTH3.urls.grants(directive, opts), opts); + }).then(function (grantsResult) { + var grants = grantsResult.originalData || grantsResult.data; + if (grants.error) { + return OAUTH3.PromiseA.reject(OAUTH3.error.parse(providerUri, grants)); + } + if ('POST' === opts.method) { + return grants; + } - return OAUTH3.request(OAUTH3.urls.grants(directive, opts), opts).then(function (grantsResult) { - if ('POST' === opts.method) { - // TODO this is clientToken - return grantsResult.originalData || grantsResult.data; - } - - var grants = grantsResult.originalData || grantsResult.data; - // TODO - if (grants.error) { - return OAUTH3.PromiseA.reject(OAUTH3.error.parse(providerUri, grants)); - } - - OAUTH3.hooks.grants.set(opts.client_id + '-client', grants.client); - grants.grants.forEach(function (grant) { - var clientId = grant.client_id || grant.oauth_client_id || grant.oauthClientId; - // TODO should save as an array - OAUTH3.hooks.grants.set(clientId, [ grant ]); - }); - - return { - client: OAUTH3.hooks.grants.get(opts.client_id + '-client') - , grants: OAUTH3.hooks.grants.get(opts.client_id) || [] - }; - }); + OAUTH3.hooks.grants.set(grants.sub+'/'+grants.azp, grants.scope); + return { + client: grants.azp + , grants: OAUTH3.scope.parse(grants.scope) + }; }); }; OAUTH3.authz.redirectWithToken = function (providerUri, session, clientParams, scopes) { + if (!OAUTH3.url.checkRedirect(clientParams.client_uri, clientParams)) { + return; + } scopes.new = scopes.new || []; @@ -427,15 +358,6 @@ OAUTH3.authz.redirectWithToken = function (providerUri, session, clientParams, s // TODO limit refresh token to an expirable token // TODO inform client not to persist token - /* - if (clientParams.dnsTxt) { - Object.keys(results).forEach(function (key) { - if (/refresh/.test(key)) { - results[key] = undefined; - } - }); - } - */ OAUTH3.url.redirect(clientParams, scopes, results); }); } @@ -447,6 +369,7 @@ OAUTH3.authz.redirectWithToken = function (providerUri, session, clientParams, s throw new Error("Authorization Code Redirect NOT IMPLEMENTED"); } }; + OAUTH3.requests = {}; OAUTH3.requests.accounts = {}; OAUTH3.requests.accounts.update = function (directive, session, opts) { From 28dbf9ab23c8eb0696d15c5e5891e5107d515a0c Mon Sep 17 00:00:00 2001 From: tigerbot Date: Wed, 26 Jul 2017 18:15:09 -0600 Subject: [PATCH 03/21] changed how grants are saved and how tokens are created for other clients --- oauth3.core.js | 4 +- oauth3.issuer.js | 145 +++++++++++++++++++++++++++++++++-------------- 2 files changed, 105 insertions(+), 44 deletions(-) diff --git a/oauth3.core.js b/oauth3.core.js index 321c8fe..37d7c63 100644 --- a/oauth3.core.js +++ b/oauth3.core.js @@ -78,7 +78,7 @@ , uri: { normalize: function (uri) { if ('string' !== typeof uri) { - console.error((new Error('stack')).stack); + throw new Error("attempted to normalize non-string URI: "+JSON.stringify(uri)); } // tested with // example.com @@ -94,7 +94,7 @@ , url: { normalize: function (url) { if ('string' !== typeof url) { - console.error((new Error('stack')).stack); + throw new Error("attempted to normalize non-string URL: "+JSON.stringify(url)); } // tested with // example.com diff --git a/oauth3.issuer.js b/oauth3.issuer.js index cacd206..d45c1e1 100644 --- a/oauth3.issuer.js +++ b/oauth3.issuer.js @@ -102,8 +102,6 @@ OAUTH3.urls.resourceOwnerPassword = function (directive, opts) { // , "username": "<>", "password": "password" } // opts = opts || {}; - var type = 'access_token'; - var grantType = 'password'; if (!opts.password) { if (opts.otp) { @@ -112,16 +110,13 @@ OAUTH3.urls.resourceOwnerPassword = function (directive, opts) { } } - var scope = opts.scope || directive.authn_scope; - var clientAgreeTos = 'oauth3.org/tos/draft'; // opts.clientAgreeTos || opts.client_agree_tos; - var clientUri = opts.client_uri; - var args = directive[type]; + var args = directive.access_token; var otpCode = opts.otp || opts.otpCode || opts.otp_code || opts.otpToken || opts.otp_token || undefined; // TODO require user agent var params = { client_id: opts.client_id || opts.client_uri , client_uri: opts.client_uri - , grant_type: grantType + , grant_type: 'password' , username: opts.username , password: opts.password || otpCode || undefined , totp: opts.totp || opts.totpToken || opts.totp_token || undefined @@ -136,23 +131,21 @@ OAUTH3.urls.resourceOwnerPassword = function (directive, opts) { //, "jwt": opts.jwt // TODO sign a proof with a previously loaded public_key , debug: opts.debug || undefined }; - var uri = args.url; - var body; - if (opts.totp) { - params.totp = opts.totp; - } - if (clientUri) { - params.clientAgreeTos = clientAgreeTos; - if (!clientAgreeTos) { + if (opts.client_uri) { + params.clientAgreeTos = 'oauth3.org/tos/draft'; // opts.clientAgreeTos || opts.client_agree_tos; + if (!params.clientAgreeTos) { throw new Error('Developer Error: missing clientAgreeTos uri'); } } + var scope = opts.scope || directive.authn_scope; if (scope) { params.scope = OAUTH3.scope.stringify(scope); } + var uri = args.url; + var body; if ('GET' === args.method.toUpperCase()) { uri += '?' + OAUTH3.query.stringify(params); } else { @@ -168,6 +161,10 @@ OAUTH3.urls.resourceOwnerPassword = function (directive, opts) { OAUTH3.urls.grants = function (directive, opts) { // directive = { issuer, authorization_decision } // opts = { response_type, scopes{ granted, requested, pending, accepted } } + var grantsDir = directive.grants; + if (!grantsDir) { + throw new Error("provider doesn't support grants"); + } if (!opts) { throw new Error("You must supply a directive and an options object."); @@ -183,15 +180,18 @@ OAUTH3.urls.grants = function (directive, opts) { } if (!opts.method) { console.warn("You should supply options.method as either 'GET', or 'POST'"); - opts.method = 'GET'; + opts.method = grantsDir.method || 'GET'; } if ('POST' === opts.method) { if ('string' !== typeof opts.scope) { throw new Error("You must supply options.scope as a comma-delimited string of scopes"); } + if ('string' !== typeof opts.sub) { + console.log("provide 'sub' to urls.grants to specify the PPID for the client"); + } } - var url = OAUTH3.url.resolve(directive.api, directive.grants.url) + var url = OAUTH3.url.resolve(directive.api, grantsDir.url) .replace(/(:azp|:client_id)/g, OAUTH3.uri.normalize(opts.client_id || opts.client_uri)) .replace(/(:sub|:account_id)/g, opts.session.token.sub || 'ISSUER:GRANT:TOKEN_SUB:UNDEFINED') ; @@ -200,13 +200,13 @@ OAUTH3.urls.grants = function (directive, opts) { , client_uri: opts.client_uri , referrer: opts.referrer , scope: opts.scope + , sub: opts.sub }; var body; if ('GET' === opts.method) { url += '?' + OAUTH3.query.stringify(data); - } - else { + } else { body = data; } @@ -217,6 +217,50 @@ OAUTH3.urls.grants = function (directive, opts) { , session: opts.session }; }; +OAUTH3.urls.clientToken = function (directive, opts) { + var tokenDir = directive.access_token; + if (!tokenDir) { + throw new Error("provider doesn't support getting access tokens"); + } + + if (!opts) { + throw new Error("You must supply a directive and an options object."); + } + if (!(opts.azp || opts.client_id)) { + throw new Error("You must supply options.client_id."); + } + if (!opts.session) { + throw new Error("You must supply options.session."); + } + if (!opts.method) { + opts.method = tokenDir.method || 'POST'; + } + + var params = { + grant_type: 'issuer_token' + , client_id: opts.azp || opts.client_id + , azp: opts.azp || opts.client_id + , aud: opts.aud + , exp: opts.exp + , refresh_token: opts.refresh_token + , refresh_exp: opts.refresh_exp + }; + + var url = OAUTH3.url.resolve(directive.api, tokenDir.url); + var body; + if ('GET' === opts.method) { + url += '?' + OAUTH3.query.stringify(params); + } else { + body = params; + } + + return { + method: opts.method + , url: url + , data: body + , session: opts.session + }; +}; OAUTH3.authn = {}; OAUTH3.authn.loginMeta = function (directive, opts) { @@ -340,34 +384,51 @@ OAUTH3.authz.redirectWithToken = function (providerUri, session, clientParams, s if (!OAUTH3.url.checkRedirect(clientParams.client_uri, clientParams)) { return; } + if ('token' !== clientParams.response_type) { + var message; + if ('code' === clientParams.response_type) { + message = "Authorization Code Redirect NOT IMPLEMENTED"; + } else { + message = "Authorization response type '"+clientParams.response_type+"' not supported"; + } + window.alert(message); + throw new Error(message); + } - scopes.new = scopes.new || []; - - if ('token' === clientParams.response_type) { - // get token and redirect client-side - return OAUTH3.authz.grants(providerUri, { - method: 'POST' + var prom; + if (scopes.new) { + prom = OAUTH3.authz.grants(providerUri, { + session: session + , method: 'POST' , client_id: clientParams.client_uri - , client_uri: clientParams.client_uri - , scope: scopes.granted.concat(scopes.new).join(',') - , response_type: clientParams.response_type , referrer: clientParams.referrer - , session: session - , debug: clientParams.debug - }).then(function (results) { - - // TODO limit refresh token to an expirable token - // TODO inform client not to persist token - OAUTH3.url.redirect(clientParams, scopes, results); + , scope: scopes.accepted.concat(scopes.new).join(',') }); + } else { + prom = OAUTH3.PromiseA.resolve(); } - else if ('code' === clientParams.response_type) { - // get token and redirect server-side - // (requires insecure form post as per spec) - //OAUTH3.requests.authorizationDecision(); - window.alert("Authorization Code Redirect NOT IMPLEMENTED"); - throw new Error("Authorization Code Redirect NOT IMPLEMENTED"); - } + + return prom.then(function () { + return OAUTH3.discover(providerUri, { client_id: providerUri, debug: clientParams.debug }); + }).then(function (directive) { + return OAUTH3.request(OAUTH3.urls.clientToken(directive, { + method: 'POST' + , session: session + , referrer: clientParams.referrer + , response_type: clientParams.response_type + , client_id: clientParams.client_uri + , azp: clientParams.client_uri + , aud: clientParams.aud + , exp: clientParams.exp + , refresh_token: clientParams.refresh_token + , refresh_exp: clientParams.refresh_exp + , debug: clientParams.debug + })); + }).then(function (results) { + // TODO limit refresh token to an expirable token + // TODO inform client not to persist token + OAUTH3.url.redirect(clientParams, scopes, results.originalData || results.data); + }); }; OAUTH3.requests = {}; From 5a5488f5046e3571846120b1080cb96519d77a3d Mon Sep 17 00:00:00 2001 From: tigerbot Date: Fri, 28 Jul 2017 13:02:36 -0600 Subject: [PATCH 04/21] changed the API for most of the crypto functions thus far I don't think anyone uses those functions so this should be safe --- oauth3.core.js | 51 ++++++++++++++++++++++--- oauth3.crypto.js | 98 ++++++++++++++---------------------------------- 2 files changed, 75 insertions(+), 74 deletions(-) diff --git a/oauth3.core.js b/oauth3.core.js index 37d7c63..02085a1 100644 --- a/oauth3.core.js +++ b/oauth3.core.js @@ -207,12 +207,12 @@ } , jwt: { // decode only (no verification) - decode: function (str) { + decode: function (token, opts) { // 'abc.qrs.xyz' // [ 'abc', 'qrs', 'xyz' ] // {} - var parts = str.split(/\./g); + var parts = token.split(/\./g); var err; if (parts.length !== 3) { err = new Error("Invalid JWT: required 3 '.' separated components not "+parts.length); @@ -220,14 +220,55 @@ throw err; } - return JSON.parse(OAUTH3._base64.decodeUrlSafe(parts[1])); + if (!opts || !opts.complete) { + return JSON.parse(OAUTH3._base64.decodeUrlSafe(parts[1])); + } + return { + header: JSON.parse(OAUTH3._base64.decodeUrlSafe(parts[0])) + , payload: JSON.parse(OAUTH3._base64.decodeUrlSafe(parts[1])) + }; } - , verify: function (jwk, token) { + , verify: function (token, jwk) { + if (!OAUTH3.crypto) { + return OAUTH3.PromiseA.reject(new Error("OAuth3 crypto library unavailable")); + } + jwk = jwk.publicKey || jwk; + var parts = token.split(/\./g); var data = OAUTH3._binStr.binStrToBuffer(parts.slice(0, 2).join('.')); var signature = OAUTH3._base64.urlSafeToBuffer(parts[2]); - return OAUTH3.crypto.core.verify(jwk, data, signature); + return OAUTH3.crypto.core.verify(jwk, data, signature).then(function () { + return OAUTH3.jwt.decode(token); + }); + } + , sign: function (payload, jwk) { + if (!OAUTH3.crypto) { + return OAUTH3.PromiseA.reject(new Error("OAuth3 crypto library unavailable")); + } + jwk = jwk.privateKey || jwk; + + var prom; + if (jwk.kid) { + prom = OAUTH3.PromiseA.resolve(jwk.kid); + } else { + prom = OAUTH3.crypto.thumbprintJwk(jwk); + } + + return prom.then(function (kid) { + // Currently the crypto part of the OAuth3 library only supports ES256 + var header = {type: 'JWT', alg: 'ES256', kid: kid}; + var input = [ + OAUTH3._base64.encodeUrlSafe(JSON.stringify(header, null)) + , OAUTH3._base64.encodeUrlSafe(JSON.stringify(payload, null)) + ].join('.'); + + return OAUTH3.crypto.core.sign(jwk, OAUTH3._binStr.binStrToBuffer(input)) + .then(OAUTH3._base64.bufferToUrlSafe) + .then(function (signature) { + return input + '.' + signature; + }); + }); } , freshness: function (tokenMeta, staletime, now) { // If the token doesn't expire then it's always fresh. diff --git a/oauth3.crypto.js b/oauth3.crypto.js index 67c27ff..85479fc 100644 --- a/oauth3.crypto.js +++ b/oauth3.crypto.js @@ -1,5 +1,5 @@ ;(function (exports) { -'use strict'; + 'use strict'; var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3; @@ -82,6 +82,7 @@ }; function checkWebCrypto() { + /* global OAUTH3_crypto_fallback */ var loadFallback = function() { var prom; loadFallback = function () { return prom; }; @@ -102,11 +103,10 @@ return prom; }; function checkException(name, func) { - new OAUTH3.PromiseA(function (resolve) { resolve(func()); }) + OAUTH3.PromiseA.resolve().then(func) .then(function () { OAUTH3.crypto.core[name] = webCrypto[name]; - }) - .catch(function (err) { + }, function (err) { console.warn('error with WebCrypto', name, '- using fallback', err); loadFallback().then(function () { OAUTH3.crypto.core[name] = OAUTH3_crypto_fallback[name]; @@ -195,101 +195,61 @@ .then(OAUTH3._base64.bufferToUrlSafe); }; - OAUTH3.crypto._createKey = function (ppid) { - var saltProm = OAUTH3.crypto.core.randomBytes(16); - var kekProm = saltProm.then(function (salt) { - return OAUTH3.crypto.core.pbkdf2(ppid, salt); - }); - - var ecdsaProm = OAUTH3.crypto.core.genEcdsaKeyPair() - .then(function (keyPair) { + OAUTH3.crypto.createKeyPair = function () { + // TODO: maybe support other types of key pairs, not just ECDSA P-256 + return OAUTH3.crypto.core.genEcdsaKeyPair().then(function (keyPair) { return OAUTH3.crypto.thumbprintJwk(keyPair.publicKey).then(function (kid) { keyPair.privateKey.alg = keyPair.publicKey.alg = 'ES256'; keyPair.privateKey.kid = keyPair.publicKey.kid = kid; return keyPair; }); }); + }; + + OAUTH3.crypto.encryptKeyPair = function (keyPair, password) { + var saltProm = OAUTH3.crypto.core.randomBytes(16); + var kekProm = saltProm.then(function (salt) { + return OAUTH3.crypto.core.pbkdf2(password, salt); + }); return OAUTH3.PromiseA.all([ kekProm - , ecdsaProm , saltProm - , OAUTH3.crypto.core.randomBytes(16) , OAUTH3.crypto.core.randomBytes(12) - , OAUTH3.crypto.core.randomBytes(12) - ]).then(function (results) { + , ]).then(function (results) { var kek = results[0]; - var keyPair = results[1]; - var salt = results[2]; - var userSecret = results[3]; - var ecdsaIv = results[4]; - var secretIv = results[5]; + var salt = results[1]; + var ecdsaIv = results[2]; - return OAUTH3.PromiseA.all([ - OAUTH3.crypto.core.encrypt(kek, ecdsaIv, OAUTH3._binStr.binStrToBuffer(JSON.stringify(keyPair.privateKey))) - , OAUTH3.crypto.core.encrypt(kek, secretIv, userSecret) - ]) - .then(function (encrypted) { + var privKeyBuf = OAUTH3._binStr.binStrToBuffer(JSON.stringify(keyPair.privateKey)); + return OAUTH3.crypto.core.encrypt(kek, ecdsaIv, privKeyBuf).then(function (encrypted) { return { publicKey: keyPair.publicKey - , privateKey: OAUTH3._base64.bufferToUrlSafe(encrypted[0]) - , userSecret: OAUTH3._base64.bufferToUrlSafe(encrypted[1]) + , privateKey: OAUTH3._base64.bufferToUrlSafe(encrypted) , salt: OAUTH3._base64.bufferToUrlSafe(salt) , ecdsaIv: OAUTH3._base64.bufferToUrlSafe(ecdsaIv) - , secretIv: OAUTH3._base64.bufferToUrlSafe(secretIv) - }; + , }; }); }); }; - OAUTH3.crypto._decryptKey = function (ppid, storedObj) { + OAUTH3.crypto.decryptKeyPair = function (storedObj, password) { var salt = OAUTH3._base64.urlSafeToBuffer(storedObj.salt); var encJwk = OAUTH3._base64.urlSafeToBuffer(storedObj.privateKey); var iv = OAUTH3._base64.urlSafeToBuffer(storedObj.ecdsaIv); - return OAUTH3.crypto.core.pbkdf2(ppid, salt) + return OAUTH3.crypto.core.pbkdf2(password, salt) .then(function (key) { return OAUTH3.crypto.core.decrypt(key, iv, encJwk); }) .then(OAUTH3._binStr.bufferToBinStr) - .then(JSON.parse); - }; - - OAUTH3.crypto._getKey = function (ppid) { - return OAUTH3.crypto.core.sha256(OAUTH3._binStr.binStrToBuffer(ppid)) - .then(function (hash) { - var name = 'kek-' + OAUTH3._base64.bufferToUrlSafe(hash); - var promise; - - if (window.localStorage.getItem(name) === null) { - promise = OAUTH3.crypto._createKey(ppid).then(function (key) { - window.localStorage.setItem(name, JSON.stringify(key)); - return key; - }); - } else { - promise = OAUTH3.PromiseA.resolve(JSON.parse(window.localStorage.getItem(name))); - } - - return promise.then(function (storedObj) { - return OAUTH3.crypto._decryptKey(ppid, storedObj); + .then(JSON.parse) + .then(function (privateKey) { + return { + privateKey: privateKey + , publicKey: storedObj.publicKey + , }; }); - }); - }; - - OAUTH3.crypto._signPayload = function (payload) { - return OAUTH3.crypto._getKey('some PPID').then(function (key) { - var header = {type: 'JWT', alg: key.alg, kid: key.kid}; - var input = [ - OAUTH3._base64.encodeUrlSafe(JSON.stringify(header, null)) - , OAUTH3._base64.encodeUrlSafe(JSON.stringify(payload, null)) - ].join('.'); - - return OAUTH3.crypto.core.sign(key, OAUTH3._binStr.binStrToBuffer(input)) - .then(OAUTH3._base64.bufferToUrlSafe) - .then(function (signature) { - return input + '.' + signature; - }); - }); }; }('undefined' !== typeof exports ? exports : window)); From 39c18ab1842bdf4579d5a5a7cec6ef515a62d869 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Fri, 28 Jul 2017 16:27:52 -0600 Subject: [PATCH 05/21] added hooks to store key pairs in localStorage --- oauth3.issuer.js | 91 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/oauth3.issuer.js b/oauth3.issuer.js index d45c1e1..19deb22 100644 --- a/oauth3.issuer.js +++ b/oauth3.issuer.js @@ -495,6 +495,7 @@ OAUTH3.requests.accounts.create = function (directive, session, account) { , data: data }); }; + OAUTH3.hooks.grants = { // Provider Only set: function (clientUri, newGrants) { @@ -516,6 +517,96 @@ OAUTH3.hooks.grants = { return this._cache[clientUri]; } }; +OAUTH3.hooks.keyPairs = { + set: function (id, keyPair) { + if (!keyPair && id.privateKey && id.publicKey && id.sub) { + keyPair = id; + id = keyPair.sub; + } + if (!keyPair) { + return OAUTH3.PromiseA.reject(new Error("no key pair provided to save")); + } + + if (OAUTH3._hooks && OAUTH3._hooks.keyPairs && OAUTH3._hooks.keyPairs.set) { + return OAUTH3._hooks.keyPairs.set(id, keyPair); + } + + if (!OAUTH3.hooks.keyPairs._warned) { OAUTH3.hooks.keyPairs._warned = {}; } + if (!OAUTH3.hooks.keyPairs._warned.set) { + console.warn('[Warn] Please implement OAUTH3._hooks.keyPairs.set = function (id, keyPair) { return PromiseA; }'); + OAUTH3.hooks.keyPairs._warned.set = true; + } + + window.localStorage.setItem('key_pair-'+id, JSON.stringify(keyPair)); + return OAUTH3.PromiseA.resolve(keyPair); + } +, get: function (id) { + if (OAUTH3._hooks && OAUTH3._hooks.keyPairs && OAUTH3._hooks.keyPairs.get) { + return OAUTH3._hooks.keyPairs.get(id); + } + + if (!OAUTH3.hooks.keyPairs._warned) { OAUTH3.hooks.keyPairs._warned = {}; } + if (!OAUTH3.hooks.keyPairs._warned.get) { + console.warn('[Warn] Please implement OAUTH3._hooks.keyPairs.get = function (id) { return PromiseA; }'); + OAUTH3.hooks.keyPairs._warned.get = true; + } + + return OAUTH3.PromiseA.resolve().then(function () { + return window.localStorage.getItem('key_pair-'+id) || 'null'; + }).then(JSON.parse); + } +, _get_all_keys: function () { + var pattern = /^key_pair-/; + var matching = []; + var ind, key; + for (ind = 0; ind < window.localStorage.length; ind++) { + key = window.localStorage.key(ind); + if (pattern.test(key)) { + matching.push(key); + } + } + return matching; + } +, all: function () { + if (OAUTH3._hooks && OAUTH3._hooks.keyPairs && OAUTH3._hooks.keyPairs.all) { + return OAUTH3._hooks.keyPairs.all(); + } + + if (!OAUTH3.hooks.keyPairs._warned) { OAUTH3.hooks.keyPairs._warned = {}; } + if (!OAUTH3.hooks.keyPairs._warned.all) { + console.warn('[Warn] Please implement OAUTH3._hooks.keyPairs.all = function (id) { return PromiseA; }'); + OAUTH3.hooks.keyPairs._warned.all = true; + } + + var result = {}; + OAUTH3.hooks.keyPairs._get_all_keys().forEach(function (key) { + var id = key.replace('key_pair-', ''); + try { + result[id] = JSON.parse(window.localStorage.getItem(key)); + } catch (err) { + console.error('failed to parse key', key, err); + window.localStorage.removeItem(key); + } + }); + return OAUTH3.PromiseA.resolve(result); + } +, clear: function () { + if (OAUTH3._hooks && OAUTH3._hooks.keyPairs && OAUTH3._hooks.keyPairs.clear) { + return OAUTH3._hooks.keyPairs.clear(); + } + + if (!OAUTH3.hooks.keyPairs._warned) { OAUTH3.hooks.keyPairs._warned = {}; } + if (!OAUTH3.hooks.keyPairs._warned.clear) { + console.warn('[Warn] Please implement OAUTH3._hooks.keyPairs.clear = function (id) { return PromiseA<>; }'); + OAUTH3.hooks.keyPairs._warned.clear = true; + } + + OAUTH3.hooks.keyPairs._get_all_keys().forEach(function (key) { + window.localStorage.removeItem(key); + }); + return OAUTH3.PromiseA.resolve(); + } +}; OAUTH3._browser.isIframe = function isIframe () { try { From 84a574e31b300f9599612ea46c5b75ba835cd868 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Fri, 28 Jul 2017 17:55:19 -0600 Subject: [PATCH 06/21] creating, publishing, and storing a key pair for remember_device --- oauth3.issuer.js | 74 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 59 insertions(+), 15 deletions(-) diff --git a/oauth3.issuer.js b/oauth3.issuer.js index 19deb22..ba53dd2 100644 --- a/oauth3.issuer.js +++ b/oauth3.issuer.js @@ -261,6 +261,32 @@ OAUTH3.urls.clientToken = function (directive, opts) { , session: opts.session }; }; +OAUTH3.urls.publishKey = function (directive, opts) { + var jwkDir = directive.publish_jwk; + if (!jwkDir) { + throw new Error("provider doesn't support publishing public keys"); + } + if (!opts) { + throw new Error("You must supply a directive and an options object."); + } + if (!opts.session) { + throw new Error("You must supply 'options.session'."); + } + if (!(opts.public_key || opts.publicKey)) { + throw new Error("You must supply 'options.public_key'."); + } + + var url = OAUTH3.url.resolve(directive.api, jwkDir.url) + .replace(/(:sub|:account_id)/g, opts.session.token.sub) + ; + + return { + method: jwkDir.method || opts.method || 'POST' + , url: url + , data: opts.public_key + , session: opts.session + }; +}; OAUTH3.authn = {}; OAUTH3.authn.loginMeta = function (directive, opts) { @@ -294,23 +320,41 @@ OAUTH3.authn.otp = function (directive, opts) { OAUTH3.authn.resourceOwnerPassword = function (directive, opts) { var providerUri = directive.issuer; - //var scope = opts.scope; - //var appId = opts.appId; - return OAUTH3.discover(providerUri, opts).then(function (directive) { - var prequest = OAUTH3.urls.resourceOwnerPassword(directive, opts); + return OAUTH3.request(OAUTH3.urls.resourceOwnerPassword(directive, opts)).then(function (resp) { + var data = resp.data; + data.provider_uri = providerUri; + if (data.error) { + return OAUTH3.PromiseA.reject(OAUTH3.error.parse(providerUri, data)); + } - // TODO return not the raw request? - return OAUTH3.request(prequest).then(function (req) { - var data = req.data; - data.provider_uri = providerUri; - if (data.error) { - return OAUTH3.PromiseA.reject(OAUTH3.error.parse(providerUri, data)); + return OAUTH3.hooks.session.refresh( + opts.session || { provider_uri: providerUri, client_uri: opts.client_uri || opts.clientUri } + , data + ); + }).then(function (session) { + if (!opts.rememberDevice && !opts.remember_device) { + return session; + } + + return OAUTH3.PromiseA.resolve().then(function () { + if (!OAUTH3.crypto) { + throw new Error("OAuth3 crypto library unavailable"); } - return OAUTH3.hooks.session.refresh( - opts.session || { provider_uri: providerUri, client_uri: opts.client_uri || opts.clientUri } - , data - ); + return OAUTH3.crypto.createKeyPair().then(function (keyPair) { + return OAUTH3.request(OAUTH3.urls.publishKey(directive, { + session: session + , publicKey: keyPair.publicKey + })).then(function () { + return OAUTH3.hooks.keyPairs.set(session.token.sub, keyPair); + }); + }); + }).then(function () { + return session; + }, function (err) { + console.error('failed to save keys to remember device', err); + window.alert('Failed to remember device'); + return session; }); }); }; @@ -335,7 +379,7 @@ OAUTH3.authz.scopes = function (providerUri, session, clientParams) { return results.grants; }, function (err) { if (!/no .*grants .*found/i.test(err.message)) { - console.error(err); + throw err; } return []; }).then(function (granted) { From 197c0fdcb2c35e12cefb29cb33383c69255ce9b0 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Mon, 31 Jul 2017 16:42:22 -0600 Subject: [PATCH 07/21] changed how the default session/directive storage works --- oauth3.core.js | 206 ++++++++++++++++++++++++++--------------- oauth3.node.js | 1 + oauth3.node.storage.js | 12 +-- 3 files changed, 137 insertions(+), 82 deletions(-) diff --git a/oauth3.core.js b/oauth3.core.js index 02085a1..2a3c75f 100644 --- a/oauth3.core.js +++ b/oauth3.core.js @@ -487,42 +487,44 @@ } } , hooks: { - directives: { - get: function (providerUri) { - providerUri = OAUTH3.uri.normalize(providerUri); - return OAUTH3.PromiseA.resolve(OAUTH3.hooks.directives._getCached(providerUri) - || OAUTH3.hooks.directives._get(providerUri)) - .then(function (directives) { - // or do .then(this._set) to keep DRY? - OAUTH3.hooks.directives._cache[providerUri] = directives; - return directives; - }); + _checkStorage: function (grpName, funcName) { + if (!OAUTH3._hooks) { + OAUTH3._hooks = {}; } - , _getCached: function (providerUri) { - providerUri = OAUTH3.uri.normalize(providerUri); - if (!OAUTH3.hooks.directives._cache) { OAUTH3.hooks.directives._cache = {}; } - return OAUTH3.hooks.directives._cache[providerUri]; + if (!OAUTH3._hooks[grpName]) { + console.log('using default storage for '+grpName+', set OAUTH3._hooks.'+grpName+' for custom storage'); + OAUTH3._hooks[grpName] = OAUTH3._defaultStorage[grpName]; + } + if (!OAUTH3._hooks[grpName][funcName]) { + throw new Error("'"+funcName+"' is not defined for custom "+grpName+" storage"); + } + } + , directives: { + get: function (providerUri) { + OAUTH3.hooks._checkStorage('directives', 'get'); + + if (!providerUri) { + throw new Error("providerUri is not set"); + } + return OAUTH3.PromiseA.resolve(OAUTH3._hooks.directives.get(OAUTH3.uri.normalize(providerUri))); } , set: function (providerUri, directives) { - providerUri = OAUTH3.uri.normalize(providerUri); - if (!OAUTH3.hooks.directives._cache) { OAUTH3.hooks.directives._cache = {}; } - OAUTH3.hooks.directives._cache[providerUri] = directives; - return OAUTH3.PromiseA.resolve(OAUTH3.hooks.directives._set(providerUri, directives)); - } - , _get: function (providerUri) { - if (!OAUTH3._hooks || !OAUTH3._hooks.directives || !OAUTH3._hooks.directives.get) { - console.warn('[Warn] Please implement OAUTH3._hooks.directives.get = function (providerUri) { return PromiseA; }'); - return JSON.parse(window.localStorage.getItem('directives-' + providerUri) || '{}'); + OAUTH3.hooks._checkStorage('directives', 'set'); + + if (!providerUri) { + throw new Error("providerUri is not set"); } - return OAUTH3._hooks.directives.get(providerUri); + return OAUTH3.PromiseA.resolve(OAUTH3._hooks.directives.set(OAUTH3.uri.normalize(providerUri), directives)); } - , _set: function (providerUri, directives) { - if (!OAUTH3._hooks || !OAUTH3._hooks.directives || !OAUTH3._hooks.directives.set) { - console.warn('[Warn] Please implement OAUTH3._hooks.directives.set = function (providerUri, directives) { return PromiseA; }'); - window.localStorage.setItem('directives-' + providerUri, JSON.stringify(directives)); - return directives; - } - return OAUTH3._hooks.directives.set(providerUri, directives); + , all: function () { + OAUTH3.hooks._checkStorage('directives', 'all'); + + return OAUTH3.PromiseA.resolve(OAUTH3._hooks.directives.all()); + } + , clear: function () { + OAUTH3.hooks._checkStorage('directives', 'clear'); + + return OAUTH3.PromiseA.resolve(OAUTH3._hooks.directives.clear()); } } , session: { @@ -563,7 +565,7 @@ } // set for a set of audiences - return OAUTH3.PromiseA.resolve(OAUTH3.hooks.session.set(providerUri, oldSession)); + return OAUTH3.hooks.session.set(providerUri, oldSession); } , check: function (preq, opts) { opts = opts || {}; @@ -616,62 +618,40 @@ return newSession; // oauth3.hooks.refreshSession(expiredSession, newSession); }); } - , _getCached: function (providerUri, id) { - providerUri = OAUTH3.uri.normalize(providerUri); - if (!OAUTH3.hooks.session._cache) { OAUTH3.hooks.session._cache = {}; } - if (id) { - return OAUTH3.hooks.session._cache[providerUri + id]; - } - return OAUTH3.hooks.session._cache[providerUri]; - } , set: function (providerUri, newSession, id) { + OAUTH3.hooks._checkStorage('sessions', 'set'); + if (!providerUri) { console.error(new Error('no providerUri').stack); throw new Error("providerUri is not set"); } providerUri = OAUTH3.uri.normalize(providerUri); - if (!OAUTH3.hooks.session._cache) { OAUTH3.hooks.session._cache = {}; } - OAUTH3.hooks.session._cache[providerUri + (id || newSession.id || newSession.token.id || '')] = newSession; - if (!id) { - OAUTH3.hooks.session._cache[providerUri] = newSession; - } - return OAUTH3.PromiseA.resolve(OAUTH3.hooks.session._set(providerUri, newSession)); + return OAUTH3.PromiseA.resolve(OAUTH3._hooks.sessions.set(providerUri, newSession, id)); } , get: function (providerUri, id) { - providerUri = OAUTH3.uri.normalize(providerUri); + OAUTH3.hooks._checkStorage('sessions', 'get'); + if (!providerUri) { throw new Error("providerUri is not set"); } + providerUri = OAUTH3.uri.normalize(providerUri); + return OAUTH3.PromiseA.resolve(OAUTH3._hooks.sessions.get(providerUri, id)); + } + , all: function (providerUri) { + OAUTH3.hooks._checkStorage('sessions', 'all'); - return OAUTH3.PromiseA.resolve( - OAUTH3.hooks.session._getCached(providerUri, id) || OAUTH3.hooks.session._get(providerUri, id) - ).then(function (session) { - var s = session || { token: {} }; - OAUTH3.hooks.session._cache[providerUri + (id || s.id || s.token.id || '')] = session; - if (!id) { - OAUTH3.hooks.session._cache[providerUri] = session; - } - return session; - }); + if (providerUri) { + providerUri = OAUTH3.uri.normalize(providerUri); + } + return OAUTH3.PromiseA.resolve(OAUTH3._hooks.sessions.all(providerUri)); } - , _get: function (providerUri, id) { - if (!OAUTH3._hooks || !OAUTH3._hooks.sessions || !OAUTH3._hooks.sessions.all) { - console.warn('[Warn] Please implement OAUTH3._hooks.sessions.all = function ([providerUri]) { return PromiseA; }'); + , clear: function (providerUri) { + OAUTH3.hooks._checkStorage('sessions', 'clear'); + + if (providerUri) { + providerUri = OAUTH3.uri.normalize(providerUri); } - if (!OAUTH3._hooks || !OAUTH3._hooks.sessions || !OAUTH3._hooks.sessions.get) { - console.warn('[Warn] Please implement OAUTH3._hooks.sessions.get = function (providerUri[, id]) { return PromiseA; }'); - return JSON.parse(window.sessionStorage.getItem('session-' + providerUri + (id || '')) || 'null'); - } - return OAUTH3._hooks.sessions.get(providerUri, id); - } - , _set: function (providerUri, newSession, id) { - if (!OAUTH3._hooks || !OAUTH3._hooks.sessions || !OAUTH3._hooks.sessions.set) { - console.warn('[Warn] Please implement OAUTH3._hooks.sessions.set = function (providerUri, newSession[, id]) { return PromiseA; }'); - window.sessionStorage.setItem('session-' + providerUri, JSON.stringify(newSession)); - window.sessionStorage.setItem('session-' + providerUri + (id || newSession.id || newSession.token.id || ''), JSON.stringify(newSession)); - return newSession; - } - return OAUTH3._hooks.sessions.set(providerUri, newSession, id); + return OAUTH3.PromiseA.resolve(OAUTH3._hooks.sessions.clear(providerUri)); } } } @@ -844,7 +824,7 @@ return OAUTH3.PromiseA.reject(OAUTH3.error.parse(directives.issuer /*providerUri*/, params)); } - OAUTH3.hooks.session._cache = {}; + OAUTH3.hooks.session.clear(); return params; }); } @@ -1119,6 +1099,82 @@ }; OAUTH3.login = OAUTH3.implicitGrant; + OAUTH3._defaultStorage = { + _getStorageKeys: function (prefix, storage) { + storage = storage || window.localStorage; + var matching = []; + var ind, key; + for (ind = 0; ind < storage.length; ind++) { + key = storage.key(ind); + if (key.indexOf(prefix || '') === 0) { + matching.push(key); + } + } + return matching; + } + , directives: { + prefix: 'directives-' + , get: function (providerUri) { + var result = JSON.parse(window.localStorage.getItem(this.prefix + providerUri) || '{}'); + return OAUTH3.PromiseA.resolve(result); + } + , set: function (providerUri, directives) { + window.localStorage.setItem(this.prefix + providerUri, JSON.stringify(directives)); + return this.get(providerUri); + } + , all: function () { + var prefix = this.prefix; + var result = {}; + OAUTH3._defaultStorage._getStorageKeys(prefix).forEach(function (key) { + result[key.replace(prefix, '')] = JSON.parse(window.localStorage.getItem(key) || '{}'); + }); + return OAUTH3.PromiseA.resolve(result); + } + , clear: function () { + OAUTH3._defaultStorage._getStorageKeys(this.prefix).forEach(function (key) { + window.localStorage.removeItem(key); + }); + return OAUTH3.PromiseA.resolve(); + } + } + , sessions: { + prefix: 'session-' + , get: function (providerUri, id) { + var result; + if (id) { + result = JSON.parse(window.sessionStorage.getItem(this.prefix + providerUri+id) || 'null'); + } else { + result = JSON.parse(window.sessionStorage.getItem(this.prefix + providerUri) || 'null'); + } + return OAUTH3.PromiseA.resolve(result); + } + , set: function (providerUri, newSession, id) { + var str = JSON.stringify(newSession); + window.sessionStorage.setItem(this.prefix + providerUri, str); + id = id || newSession.id || newSession.token.token.id; + if (id) { + window.sessionStorage.setItem(this.prefix + providerUri + id, str); + } + return this.get(providerUri, id); + } + , all: function (providerUri) { + var prefix = this.prefix + (providerUri || ''); + var result = {}; + OAUTH3._defaultStorage._getStorageKeys(prefix, window.sessionStorage).forEach(function (key) { + result[key.replace(prefix, '')] = JSON.parse(window.localStorage.getItem(key) || 'null'); + }); + return OAUTH3.PromiseA.resolve(result); + } + , clear: function (providerUri) { + var prefix = this.prefix + (providerUri || ''); + OAUTH3._defaultStorage._getStorageKeys(prefix, window.sessionStorage).forEach(function (key) { + window.localStorage.removeItem(key); + }); + return OAUTH3.PromiseA.resolve(); + } + } + }; + // TODO get rid of these OAUTH3.utils = { clientUri: OAUTH3.clientUri diff --git a/oauth3.node.js b/oauth3.node.js index 95b0bd3..97fecd1 100644 --- a/oauth3.node.js +++ b/oauth3.node.js @@ -28,6 +28,7 @@ OAUTH3._base64.atob = function (base64) { OAUTH3._base64.btoa = function (text) { return new Buffer(text, 'utf8').toString('base64'); }; +OAUTH3._defaultStorage = require('./oauth3.node.storage'); OAUTH3._node = {}; OAUTH3._node.discover = function(providerUri/*, opts*/) { diff --git a/oauth3.node.storage.js b/oauth3.node.storage.js index 370c678..c18046f 100644 --- a/oauth3.node.storage.js +++ b/oauth3.node.storage.js @@ -67,10 +67,9 @@ module.exports = { , sessions: { all: function (providerUri) { - var dirname = path.join(oauth3dir, 'sessions'); - return fs.readdirAsync(dirname).then(function (nodes) { + return fs.readdirAsync(sessionsdir).then(function (nodes) { return nodes.map(function (node) { - var result = require(path.join(dirname, node)); + var result = require(path.join(sessionsdir, node)); if (result.link) { return null; } @@ -91,7 +90,7 @@ module.exports = { result = require(path.join(sessionsdir, providerUri + '.json')); // TODO make safer if (result.link && '/' !== result.link[0] && !/\.\./.test(result.link)) { - result = require(path.join(oauth3dir, 'sessions', result.link)); + result = require(path.join(sessionsdir, result.link)); } } } catch(e) { @@ -113,10 +112,9 @@ module.exports = { }); } , clear: function () { - var dirname = path.join(oauth3dir, 'sessions'); - return fs.readdirAsync(dirname).then(function (nodes) { + return fs.readdirAsync(sessionsdir).then(function (nodes) { return PromiseA.all(nodes.map(function (node) { - return fs.unlinkAsync(path.join(dirname, node)); + return fs.unlinkAsync(path.join(sessionsdir, node)); })); }); } From e42079d856f891547919e91a6806eb83e5b42343 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Mon, 31 Jul 2017 19:39:52 -0600 Subject: [PATCH 08/21] changed default storage for grants and key pairs as well --- oauth3.issuer.js | 171 ++++++++++++++++++++++++----------------------- 1 file changed, 88 insertions(+), 83 deletions(-) diff --git a/oauth3.issuer.js b/oauth3.issuer.js index ba53dd2..abd5395 100644 --- a/oauth3.issuer.js +++ b/oauth3.issuer.js @@ -541,28 +541,45 @@ OAUTH3.requests.accounts.create = function (directive, session, account) { }; OAUTH3.hooks.grants = { - // Provider Only - set: function (clientUri, newGrants) { - clientUri = OAUTH3.uri.normalize(clientUri); - console.warn('[oauth3.hooks.setGrants] PLEASE IMPLEMENT -- Your Fault'); - console.warn(newGrants); - if (!this._cache) { this._cache = {}; } - console.log('clientUri, newGrants'); - console.log(clientUri, newGrants); - this._cache[clientUri] = newGrants; - return newGrants; + get: function (clientUri) { + OAUTH3.hooks._checkStorage('grants', 'get'); + + if (!clientUri) { + throw new Error("clientUri is not set"); + } + return OAUTH3.PromiseA.resolve(OAUTH3._hooks.grants.get(OAUTH3.uri.normalize(clientUri))); } -, get: function (clientUri) { - clientUri = OAUTH3.uri.normalize(clientUri); - console.warn('[oauth3.hooks.getGrants] PLEASE IMPLEMENT -- Your Fault'); - if (!this._cache) { this._cache = {}; } - console.log('clientUri, existingGrants'); - console.log(clientUri, this._cache[clientUri]); - return this._cache[clientUri]; +, set: function (clientUri, grants) { + OAUTH3.hooks._checkStorage('grants', 'set'); + + if (!clientUri) { + throw new Error("clientUri is not set"); + } + return OAUTH3.PromiseA.resolve(OAUTH3._hooks.grants.set(OAUTH3.uri.normalize(clientUri), grants)); + } +, all: function () { + OAUTH3.hooks._checkStorage('grants', 'all'); + + return OAUTH3.PromiseA.resolve(OAUTH3._hooks.grants.all()); + } +, clear: function () { + OAUTH3.hooks._checkStorage('grants', 'clear'); + + return OAUTH3.PromiseA.resolve(OAUTH3._hooks.grants.clear()); } }; OAUTH3.hooks.keyPairs = { - set: function (id, keyPair) { + get: function (id) { + OAUTH3.hooks._checkStorage('keyPairs', 'get'); + + if (!id) { + throw new Error("id is not set"); + } + return OAUTH3.PromiseA.resolve(OAUTH3._hooks.keyPairs.get(id)); + } +, set: function (id, keyPair) { + OAUTH3.hooks._checkStorage('keyPairs', 'set'); + if (!keyPair && id.privateKey && id.publicKey && id.sub) { keyPair = id; id = keyPair.sub; @@ -570,82 +587,70 @@ OAUTH3.hooks.keyPairs = { if (!keyPair) { return OAUTH3.PromiseA.reject(new Error("no key pair provided to save")); } - - if (OAUTH3._hooks && OAUTH3._hooks.keyPairs && OAUTH3._hooks.keyPairs.set) { - return OAUTH3._hooks.keyPairs.set(id, keyPair); + if (!id) { + throw new Error("id is not set"); } + keyPair.sub = keyPair.sub || id; - if (!OAUTH3.hooks.keyPairs._warned) { OAUTH3.hooks.keyPairs._warned = {}; } - if (!OAUTH3.hooks.keyPairs._warned.set) { - console.warn('[Warn] Please implement OAUTH3._hooks.keyPairs.set = function (id, keyPair) { return PromiseA; }'); - OAUTH3.hooks.keyPairs._warned.set = true; - } - - window.localStorage.setItem('key_pair-'+id, JSON.stringify(keyPair)); - return OAUTH3.PromiseA.resolve(keyPair); - } -, get: function (id) { - if (OAUTH3._hooks && OAUTH3._hooks.keyPairs && OAUTH3._hooks.keyPairs.get) { - return OAUTH3._hooks.keyPairs.get(id); - } - - if (!OAUTH3.hooks.keyPairs._warned) { OAUTH3.hooks.keyPairs._warned = {}; } - if (!OAUTH3.hooks.keyPairs._warned.get) { - console.warn('[Warn] Please implement OAUTH3._hooks.keyPairs.get = function (id) { return PromiseA; }'); - OAUTH3.hooks.keyPairs._warned.get = true; - } - - return OAUTH3.PromiseA.resolve().then(function () { - return window.localStorage.getItem('key_pair-'+id) || 'null'; - }).then(JSON.parse); - } -, _get_all_keys: function () { - var pattern = /^key_pair-/; - var matching = []; - var ind, key; - for (ind = 0; ind < window.localStorage.length; ind++) { - key = window.localStorage.key(ind); - if (pattern.test(key)) { - matching.push(key); - } - } - return matching; + return OAUTH3.PromiseA.resolve(OAUTH3._hooks.keyPairs.set(id, keyPair)); } , all: function () { - if (OAUTH3._hooks && OAUTH3._hooks.keyPairs && OAUTH3._hooks.keyPairs.all) { - return OAUTH3._hooks.keyPairs.all(); - } + OAUTH3.hooks._checkStorage('keyPairs', 'all'); - if (!OAUTH3.hooks.keyPairs._warned) { OAUTH3.hooks.keyPairs._warned = {}; } - if (!OAUTH3.hooks.keyPairs._warned.all) { - console.warn('[Warn] Please implement OAUTH3._hooks.keyPairs.all = function (id) { return PromiseA; }'); - OAUTH3.hooks.keyPairs._warned.all = true; - } + return OAUTH3.PromiseA.resolve(OAUTH3._hooks.keyPairs.all()); + } +, clear: function () { + OAUTH3.hooks._checkStorage('keyPairs', 'clear'); + return OAUTH3.PromiseA.resolve(OAUTH3._hooks.keyPairs.clear()); + } +}; + +OAUTH3._defaultStorage.grants = { + prefix: 'grants-' +, get: function (clientUri) { + var result = JSON.parse(window.localStorage.getItem(this.prefix + clientUri) || 'null'); + return OAUTH3.PromiseA.resolve(result); + } +, set: function (clientUri, grants) { + window.localStorage.setItem(this.prefix + clientUri, JSON.stringify(grants)); + return this.get(clientUri); + } +, all: function () { + var prefix = this.prefix; var result = {}; - OAUTH3.hooks.keyPairs._get_all_keys().forEach(function (key) { - var id = key.replace('key_pair-', ''); - try { - result[id] = JSON.parse(window.localStorage.getItem(key)); - } catch (err) { - console.error('failed to parse key', key, err); - window.localStorage.removeItem(key); - } + OAUTH3._defaultStorage._getStorageKeys(prefix, window.localStorage).forEach(function (key) { + result[key.replace(prefix, '')] = JSON.parse(window.localStorage.getItem(key) || 'null'); }); return OAUTH3.PromiseA.resolve(result); } , clear: function () { - if (OAUTH3._hooks && OAUTH3._hooks.keyPairs && OAUTH3._hooks.keyPairs.clear) { - return OAUTH3._hooks.keyPairs.clear(); - } - - if (!OAUTH3.hooks.keyPairs._warned) { OAUTH3.hooks.keyPairs._warned = {}; } - if (!OAUTH3.hooks.keyPairs._warned.clear) { - console.warn('[Warn] Please implement OAUTH3._hooks.keyPairs.clear = function (id) { return PromiseA<>; }'); - OAUTH3.hooks.keyPairs._warned.clear = true; - } - - OAUTH3.hooks.keyPairs._get_all_keys().forEach(function (key) { + OAUTH3._defaultStorage._getStorageKeys(this.prefix, window.localStorage).forEach(function (key) { + window.localStorage.removeItem(key); + }); + return OAUTH3.PromiseA.resolve(); + } +}; +OAUTH3._defaultStorage.keyPairs = { + prefix: 'key_pairs-' +, get: function (id) { + var result = JSON.parse(window.localStorage.getItem(this.prefix + id) || 'null'); + return OAUTH3.PromiseA.resolve(result); + } +, set: function (id, keyPair) { + window.localStorage.setItem(this.prefix + id, JSON.stringify(keyPair)); + return this.get(id); + } +, all: function () { + var prefix = this.prefix; + var result = {}; + OAUTH3._defaultStorage._getStorageKeys(prefix, window.localStorage).forEach(function (key) { + result[key.replace(prefix, '')] = JSON.parse(window.localStorage.getItem(key) || 'null'); + }); + return OAUTH3.PromiseA.resolve(result); + } +, clear: function () { + OAUTH3._defaultStorage._getStorageKeys(this.prefix, window.localStorage).forEach(function (key) { window.localStorage.removeItem(key); }); return OAUTH3.PromiseA.resolve(); From 39b8e19bae3b594a929b5200aa5f94239ba2c85c Mon Sep 17 00:00:00 2001 From: tigerbot Date: Tue, 1 Aug 2017 11:15:37 -0600 Subject: [PATCH 09/21] added hook to create new session with stored key --- oauth3.issuer.js | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/oauth3.issuer.js b/oauth3.issuer.js index abd5395..e09aadf 100644 --- a/oauth3.issuer.js +++ b/oauth3.issuer.js @@ -283,7 +283,7 @@ OAUTH3.urls.publishKey = function (directive, opts) { return { method: jwkDir.method || opts.method || 'POST' , url: url - , data: opts.public_key + , data: opts.public_key || opts.publicKey , session: opts.session }; }; @@ -605,6 +605,48 @@ OAUTH3.hooks.keyPairs = { return OAUTH3.PromiseA.resolve(OAUTH3._hooks.keyPairs.clear()); } }; +OAUTH3.hooks.session.get = function (providerUri, id) { + OAUTH3.hooks._checkStorage('sessions', 'get'); + var sessProm = OAUTH3.PromiseA.resolve(OAUTH3._hooks.sessions.get(providerUri, id)); + if (providerUri !== OAUTH3.clientUri(window.location)) { + return sessProm; + } + + return sessProm.then(function (session) { + if (session && OAUTH3.jwt.freshness(session.token) === 'fresh') { + return session; + } + + return OAUTH3.hooks.keyPairs.all().then(function (keyPairs) { + var pair; + if (id) { + pair = keyPairs[id]; + } else if (Object.keys(keyPairs).length === 1) { + id = Object.keys(keyPairs)[0]; + pair = keyPairs[id]; + } else if (Object.keys(keyPairs).length > 1) { + console.error("too many users, don't know which key to use"); + } + if (!pair) { + return null; + } + + var payload = { + iss: providerUri + , aud: providerUri + , azp: providerUri + , sub: pair.sub || id + , scope: '' + }; + return OAUTH3.jwt.sign(payload, pair.privateKey).then(function (token) { + return OAUTH3.hooks.session.refresh( + { provider_uri: providerUri, client_uri: providerUri || providerUri } + , { access_token: token } + ); + }); + }); + }); +}; OAUTH3._defaultStorage.grants = { prefix: 'grants-' From 5d42f3e2cc3588008b9e6a2387e64eb66b8ace49 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Tue, 1 Aug 2017 14:13:36 -0600 Subject: [PATCH 10/21] using stored grants before fetching them from the server --- oauth3.issuer.js | 63 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/oauth3.issuer.js b/oauth3.issuer.js index e09aadf..482e1a2 100644 --- a/oauth3.issuer.js +++ b/oauth3.issuer.js @@ -370,18 +370,28 @@ OAUTH3.authz.scopes = function (providerUri, session, clientParams) { //return generateToken(session, clientObj); } - return OAUTH3.authz.grants(providerUri, { - method: 'GET' - , client_id: clientUri - , client_uri: clientUri - , session: session - }).then(function (results) { - return results.grants; - }, function (err) { - if (!/no .*grants .*found/i.test(err.message)) { - throw err; + return OAUTH3.hooks.grants.get(session.token.sub, clientUri).then(function (granted) { + if (granted) { + if (typeof granted.scope === 'string') { + return OAUTH3.scope.parse(granted.scope); + } else if (Array.isArray(granted.scope)) { + return granted.scope; + } } - return []; + + return OAUTH3.authz.grants(providerUri, { + method: 'GET' + , client_id: clientUri + , client_uri: clientUri + , session: session + }).then(function (results) { + return results.grants; + }, function (err) { + if (!/no .*grants .*found/i.test(err.message)) { + throw err; + } + return []; + }); }).then(function (granted) { var requested = OAUTH3.scope.parse(scope); var accepted = []; @@ -413,13 +423,16 @@ OAUTH3.authz.grants = function (providerUri, opts) { if (grants.error) { return OAUTH3.PromiseA.reject(OAUTH3.error.parse(providerUri, grants)); } - if ('POST' === opts.method) { + // the responses for GET and POST requests are now the same, so we should alway be able to + // use the response and save it the same way. + if ('GET' !== opts.method && 'POST' !== opts.method) { return grants; } - OAUTH3.hooks.grants.set(grants.sub+'/'+grants.azp, grants.scope); + OAUTH3.hooks.grants.set(grants.sub, grants.azp, grants); return { client: grants.azp + , clientSub: grants.azpSub , grants: OAUTH3.scope.parse(grants.scope) }; }); @@ -541,17 +554,23 @@ OAUTH3.requests.accounts.create = function (directive, session, account) { }; OAUTH3.hooks.grants = { - get: function (clientUri) { + get: function (id, clientUri) { OAUTH3.hooks._checkStorage('grants', 'get'); + if (!id) { + throw new Error("id is not set"); + } if (!clientUri) { throw new Error("clientUri is not set"); } return OAUTH3.PromiseA.resolve(OAUTH3._hooks.grants.get(OAUTH3.uri.normalize(clientUri))); } -, set: function (clientUri, grants) { +, set: function (id, clientUri, grants) { OAUTH3.hooks._checkStorage('grants', 'set'); + if (!id) { + throw new Error("id is not set"); + } if (!clientUri) { throw new Error("clientUri is not set"); } @@ -650,19 +669,23 @@ OAUTH3.hooks.session.get = function (providerUri, id) { OAUTH3._defaultStorage.grants = { prefix: 'grants-' -, get: function (clientUri) { - var result = JSON.parse(window.localStorage.getItem(this.prefix + clientUri) || 'null'); +, get: function (id, clientUri) { + var key = this.prefix + id+'/'+clientUri; + var result = JSON.parse(window.localStorage.getItem(key) || 'null'); return OAUTH3.PromiseA.resolve(result); } -, set: function (clientUri, grants) { - window.localStorage.setItem(this.prefix + clientUri, JSON.stringify(grants)); +, set: function (id, clientUri, grants) { + var key = this.prefix + id+'/'+clientUri; + window.localStorage.setItem(key, JSON.stringify(grants)); return this.get(clientUri); } , all: function () { var prefix = this.prefix; var result = {}; OAUTH3._defaultStorage._getStorageKeys(prefix, window.localStorage).forEach(function (key) { - result[key.replace(prefix, '')] = JSON.parse(window.localStorage.getItem(key) || 'null'); + var split = key.replace(prefix, '').split('/'); + if (!result[split[0]]) { result[split[0]] = {}; } + result[split[0]][split[1]] = JSON.parse(window.localStorage.getItem(key) || 'null'); }); return OAUTH3.PromiseA.resolve(result); } From 516eda4ea623a47ab314062e5faf36b9fe242c86 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Tue, 1 Aug 2017 14:21:11 -0600 Subject: [PATCH 11/21] signing the token for the client in the browser --- oauth3.issuer.js | 113 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 95 insertions(+), 18 deletions(-) diff --git a/oauth3.issuer.js b/oauth3.issuer.js index 482e1a2..3a27402 100644 --- a/oauth3.issuer.js +++ b/oauth3.issuer.js @@ -437,6 +437,39 @@ OAUTH3.authz.grants = function (providerUri, opts) { }; }); }; +function calcExpiration(exp, now) { + if (!exp) { + return; + } + + if (typeof exp === 'string') { + var match = /^(\d+\.?\d*) *(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec(exp); + if (!match) { + return now; + } + var num = parseFloat(match[1]); + var type = (match[2] || 's').toLowerCase()[0]; + switch (type) { + case 'y': num *= 365.25; /* falls through */ + case 'd': num *= 24; /* falls through */ + case 'h': num *= 60; /* falls through */ + case 'm': num *= 60; /* falls through */ + case 's': exp = num; + } + } + if (typeof exp !== 'number') { + throw new Error('invalid expiration provided: '+exp); + } + + now = now || Math.floor(Date.now() / 1000); + if (exp > now) { + return exp; + } else if (exp > 31557600) { + console.warn('tried to set expiration to more that a year'); + exp = 31557600; + } + return now + exp; +} OAUTH3.authz.redirectWithToken = function (providerUri, session, clientParams, scopes) { if (!OAUTH3.url.checkRedirect(clientParams.client_uri, clientParams)) { return; @@ -466,25 +499,66 @@ OAUTH3.authz.redirectWithToken = function (providerUri, session, clientParams, s } return prom.then(function () { - return OAUTH3.discover(providerUri, { client_id: providerUri, debug: clientParams.debug }); - }).then(function (directive) { - return OAUTH3.request(OAUTH3.urls.clientToken(directive, { - method: 'POST' - , session: session - , referrer: clientParams.referrer - , response_type: clientParams.response_type - , client_id: clientParams.client_uri - , azp: clientParams.client_uri - , aud: clientParams.aud - , exp: clientParams.exp - , refresh_token: clientParams.refresh_token - , refresh_exp: clientParams.refresh_exp - , debug: clientParams.debug - })); - }).then(function (results) { + return OAUTH3.hooks.keyPairs.get(session.token.sub); + }).then(function (keyPair) { + if (!keyPair) { + return OAUTH3.discover(providerUri, { + client_id: providerUri + , debug: clientParams.debug + }).then(function (directive) { + return OAUTH3.request(OAUTH3.urls.clientToken(directive, { + method: 'POST' + , session: session + , referrer: clientParams.referrer + , response_type: clientParams.response_type + , client_id: clientParams.client_uri + , azp: clientParams.client_uri + , aud: clientParams.aud + , exp: clientParams.exp + , refresh_token: clientParams.refresh_token + , refresh_exp: clientParams.refresh_exp + , debug: clientParams.debug + })).then(function (result) { + return result.originalData || result.data; + }); + }); + } + + return OAUTH3.hooks.grants.get(keyPair.sub, clientParams.client_uri).then(function (grant) { + var now = Math.floor(Date.now()/1000); + var payload = { + iat: now + , iss: providerUri + , aud: clientParams.aud || providerUri + , azp: clientParams.client_uri + , sub: grant.azpSub + , scope: OAUTH3.scope.stringify(grant.scope) + , }; + + var signProms = []; + signProms.push(OAUTH3.jwt.sign(Object.assign({ + exp: calcExpiration(clientParams.exp || '1h', now) + }, payload))); + // if (clientParams.refresh_token) { + signProms.push(OAUTH3.jwt.sign(Object.assign({ + exp: calcExpiration(clientParams.refresh_exp, now) + }, payload))); + // } + return OAUTH3.PromiseA.all(signProms).then(function (tokens) { + return { + access_token: tokens[0] + , refresh_token: tokens[1] + , scope: OAUTH3.scope.stringify(grant.scope) + , token_type: 'bearer' + }; + }); + }); + }).then(function (session) { // TODO limit refresh token to an expirable token // TODO inform client not to persist token - OAUTH3.url.redirect(clientParams, scopes, results.originalData || results.data); + OAUTH3.url.redirect(clientParams, scopes, session); + }, function (err) { + OAUTH3.url.redirect(clientParams, scopes, {error: err}); }); }; @@ -650,12 +724,15 @@ OAUTH3.hooks.session.get = function (providerUri, id) { return null; } + var now = Math.floor(Date.now()/1000); var payload = { - iss: providerUri + iat: now + , iss: providerUri , aud: providerUri , azp: providerUri , sub: pair.sub || id , scope: '' + , exp: now + 3600 }; return OAUTH3.jwt.sign(payload, pair.privateKey).then(function (token) { return OAUTH3.hooks.session.refresh( From 623d94e045266cabd96998eb29857f65515130da Mon Sep 17 00:00:00 2001 From: tigerbot Date: Tue, 1 Aug 2017 15:08:21 -0600 Subject: [PATCH 12/21] misc bug fixes --- .ignore | 2 ++ oauth3.core.js | 8 ++++---- oauth3.issuer.js | 14 +++++++++----- 3 files changed, 15 insertions(+), 9 deletions(-) create mode 100644 .ignore diff --git a/.ignore b/.ignore new file mode 100644 index 0000000..ddb3cdd --- /dev/null +++ b/.ignore @@ -0,0 +1,2 @@ +prefactor +.well-known diff --git a/oauth3.core.js b/oauth3.core.js index 2a3c75f..74879c0 100644 --- a/oauth3.core.js +++ b/oauth3.core.js @@ -246,7 +246,7 @@ if (!OAUTH3.crypto) { return OAUTH3.PromiseA.reject(new Error("OAuth3 crypto library unavailable")); } - jwk = jwk.privateKey || jwk; + jwk = jwk.private_key || jwk.privateKey || jwk; var prom; if (jwk.kid) { @@ -1151,7 +1151,7 @@ , set: function (providerUri, newSession, id) { var str = JSON.stringify(newSession); window.sessionStorage.setItem(this.prefix + providerUri, str); - id = id || newSession.id || newSession.token.token.id; + id = id || newSession.id || newSession.token.sub || newSession.token.id; if (id) { window.sessionStorage.setItem(this.prefix + providerUri + id, str); } @@ -1161,14 +1161,14 @@ var prefix = this.prefix + (providerUri || ''); var result = {}; OAUTH3._defaultStorage._getStorageKeys(prefix, window.sessionStorage).forEach(function (key) { - result[key.replace(prefix, '')] = JSON.parse(window.localStorage.getItem(key) || 'null'); + result[key.replace(prefix, '')] = JSON.parse(window.sessionStorage.getItem(key) || 'null'); }); return OAUTH3.PromiseA.resolve(result); } , clear: function (providerUri) { var prefix = this.prefix + (providerUri || ''); OAUTH3._defaultStorage._getStorageKeys(prefix, window.sessionStorage).forEach(function (key) { - window.localStorage.removeItem(key); + window.sessionStorage.removeItem(key); }); return OAUTH3.PromiseA.resolve(); } diff --git a/oauth3.issuer.js b/oauth3.issuer.js index 3a27402..5848b74 100644 --- a/oauth3.issuer.js +++ b/oauth3.issuer.js @@ -538,13 +538,14 @@ OAUTH3.authz.redirectWithToken = function (providerUri, session, clientParams, s var signProms = []; signProms.push(OAUTH3.jwt.sign(Object.assign({ exp: calcExpiration(clientParams.exp || '1h', now) - }, payload))); + }, payload), keyPair)); // if (clientParams.refresh_token) { signProms.push(OAUTH3.jwt.sign(Object.assign({ exp: calcExpiration(clientParams.refresh_exp, now) - }, payload))); + }, payload), keyPair)); // } return OAUTH3.PromiseA.all(signProms).then(function (tokens) { + console.log('created new tokens for client'); return { access_token: tokens[0] , refresh_token: tokens[1] @@ -558,6 +559,7 @@ OAUTH3.authz.redirectWithToken = function (providerUri, session, clientParams, s // TODO inform client not to persist token OAUTH3.url.redirect(clientParams, scopes, session); }, function (err) { + console.error('unexpected error creating client tokens', err); OAUTH3.url.redirect(clientParams, scopes, {error: err}); }); }; @@ -637,7 +639,7 @@ OAUTH3.hooks.grants = { if (!clientUri) { throw new Error("clientUri is not set"); } - return OAUTH3.PromiseA.resolve(OAUTH3._hooks.grants.get(OAUTH3.uri.normalize(clientUri))); + return OAUTH3.PromiseA.resolve(OAUTH3._hooks.grants.get(id, OAUTH3.uri.normalize(clientUri))); } , set: function (id, clientUri, grants) { OAUTH3.hooks._checkStorage('grants', 'set'); @@ -648,7 +650,7 @@ OAUTH3.hooks.grants = { if (!clientUri) { throw new Error("clientUri is not set"); } - return OAUTH3.PromiseA.resolve(OAUTH3._hooks.grants.set(OAUTH3.uri.normalize(clientUri), grants)); + return OAUTH3.PromiseA.resolve(OAUTH3._hooks.grants.set(id, OAUTH3.uri.normalize(clientUri), grants)); } , all: function () { OAUTH3.hooks._checkStorage('grants', 'all'); @@ -721,7 +723,8 @@ OAUTH3.hooks.session.get = function (providerUri, id) { console.error("too many users, don't know which key to use"); } if (!pair) { - return null; + // even if the access token isn't fresh, the session might have a refresh token + return session; } var now = Math.floor(Date.now()/1000); @@ -735,6 +738,7 @@ OAUTH3.hooks.session.get = function (providerUri, id) { , exp: now + 3600 }; return OAUTH3.jwt.sign(payload, pair.privateKey).then(function (token) { + console.log('created new token for provider'); return OAUTH3.hooks.session.refresh( { provider_uri: providerUri, client_uri: providerUri || providerUri } , { access_token: token } From b60e9b8fce68797de88131059e1ab454f33a34c0 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Tue, 15 Aug 2017 14:06:15 -0600 Subject: [PATCH 13/21] updated the directives for the new issuer API --- well-known/oauth3/directives.json | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/well-known/oauth3/directives.json b/well-known/oauth3/directives.json index b710741..d546348 100644 --- a/well-known/oauth3/directives.json +++ b/well-known/oauth3/directives.json @@ -1,13 +1,12 @@ { "terms": [ "oauth3.org/tos/draft" ] , "api": "api.:hostname" , "authorization_dialog": { "url": "#/authorization_dialog" } -, "access_token": { "method": "POST", "url": "api/issuer@oauth3.org/access_token" } -, "otp": { "method": "POST", "url": "api/issuer@oauth3.org/otp" } -, "credential_otp": { "method": "POST", "url": "api/issuer@oauth3.org/otp" } -, "credential_meta": { "url": "api/issuer@oauth3.org/logins/meta/:type/:id" } -, "credential_create": { "method": "POST", "url": "api/issuer@oauth3.org/logins" } -, "grants": { "method": "GET", "url": "api/issuer@oauth3.org/grants/:azp/:sub" } -, "authorization_decision": { "method": "POST", "url": "api/issuer@oauth3.org/authorization_decision" } -, "callback": { "method": "GET", "url": ".well-known/oauth3/callback.html#/" } -, "logout": { "method": "GET", "url": "#/logout/" } +, "access_token": { "method": "POST", "url": "api/issuer@oauth3.org/access_token" } +, "otp": { "method": "POST", "url": "api/issuer@oauth3.org/access_token/send_otp" } +, "credential_otp": { "method": "POST", "url": "api/issuer@oauth3.org/access_token/send_otp" } +, "grants": { "method": "GET", "url": "api/issuer@oauth3.org/grants/:sub/:azp" } +, "publish_jwk": { "method": "POST", "url": "api/issuer@oauth3.org/jwks/:sub" } +, "retrieve_jwk": { "method": "GET", "url": "api/issuer@oauth3.org/jwks/:sub/:kid.json" } +, "callback": { "method": "GET", "url": ".well-known/oauth3/callback.html#/" } +, "logout": { "method": "GET", "url": "#/logout/" } } From 4d80074046d71c0dffdcca4893b5a3c0bd96603a Mon Sep 17 00:00:00 2001 From: aj Date: Tue, 15 Aug 2017 20:21:54 +0000 Subject: [PATCH 14/21] org.oauth3 -> oauth3.org --- bin/cli.js | 4 ++-- oauth3.core.js | 6 +++--- oauth3.crypto.js | 2 +- oauth3.issuer.js | 6 +++--- oauth3.tunnel.js | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/bin/cli.js b/bin/cli.js index da3be23..5319555 100644 --- a/bin/cli.js +++ b/bin/cli.js @@ -131,8 +131,8 @@ parseArgs(process.argv, { // authn / authz , [ 'devices', 'manages devices for your account(s)' ] - , [ 'devices:new', 'create a new device (default name is hostname, default ip is the result of :provider/api/org.oauth3.tunnel/checkip)'.replace(/\b:provider\b/, defaults.provider) ] - , [ 'devices:set', 'set the ip address of the device (defaults ip is the result of :provider/api/org.oauth3.tunnel/checkip)'.replace(/\b:provider\b/, defaults.provider) ] + , [ 'devices:new', 'create a new device (default name is hostname, default ip is the result of :provider/api/tunnel@oauth3.org/checkip)'.replace(/\b:provider\b/, defaults.provider) ] + , [ 'devices:set', 'set the ip address of the device (defaults ip is the result of :provider/api/tunnel@oauth3.org/checkip)'.replace(/\b:provider\b/, defaults.provider) ] , [ 'devices:attach', "attach a device to a domain's DNS record" ] , [ 'devices:detach', "detach an account from a domain's DNS record" ] , [ 'devices:select', '(re)claim the specified device as this device (i.e. you re-installed your OS or deleted your ~/.oauth3)' ] diff --git a/oauth3.core.js b/oauth3.core.js index d5ec46e..7658c0f 100644 --- a/oauth3.core.js +++ b/oauth3.core.js @@ -328,7 +328,7 @@ // Example Implicit Grant Request // (for generating a browser-only session, not a session on your server) // - // GET https://example.com/api/org.oauth3.provider/authorization_dialog + // GET https://example.com/api/issuer@oauth3.org/authorization_dialog // ?response_type=token // &scope=`encodeURIComponent('profile.login profile.email')` // &state=`cryptoutil.random().toString('hex')` @@ -1210,7 +1210,7 @@ , _resourceProviderUri: null , _identityProviderDirectives: null , _resourceProviderDirectives: null - //, _resourceProviderMap: null // map between xyz.com and org.oauth3.domains + //, _resourceProviderMap: null // map between xyz.com and domains@oauth3.org , _init: function (location, opts) { var me = this; if (location) { @@ -1266,7 +1266,7 @@ var me = this; return me.init().then(function () { return me.setIdentityProvider(providerUri).then(function () { - // TODO how to say "Use xyz.com for org.oauth3.domains, but abc.com for org.oauth3.dns"? + // TODO how to say "Use xyz.com for domains@oauth3.org, but abc.com for dns@oauth3.org"? return me.setResourceProvider(providerUri); }); }); diff --git a/oauth3.crypto.js b/oauth3.crypto.js index 85479fc..d87a158 100644 --- a/oauth3.crypto.js +++ b/oauth3.crypto.js @@ -97,7 +97,7 @@ resolve(); } }; - script.src = '/assets/org.oauth3/oauth3.crypto.fallback.js'; + script.src = '/assets/oauth3.org/oauth3.crypto.fallback.js'; body.appendChild(script); }); return prom; diff --git a/oauth3.issuer.js b/oauth3.issuer.js index 5848b74..f70b0ae 100644 --- a/oauth3.issuer.js +++ b/oauth3.issuer.js @@ -97,7 +97,7 @@ OAUTH3.urls.resourceOwnerPassword = function (directive, opts) { // Example Resource Owner Password Request // (generally for 1st party and direct-partner mobile apps, and webapps) // - // POST https://example.com/api/org.oauth3.provider/access_token + // POST https://example.com/api/issuer@oauth3.org/access_token // { "grant_type": "password", "client_id": "<>", "scope": "<>" // , "username": "<>", "password": "password" } // @@ -569,7 +569,7 @@ OAUTH3.requests.accounts = {}; OAUTH3.requests.accounts.update = function (directive, session, opts) { var dir = directive.update_account || { method: 'POST' - , url: OAUTH3.url.normalize(directive.api) + '/api/org.oauth3.provider/accounts/:accountId' + , url: OAUTH3.url.normalize(directive.api) + '/api/issuer@oauth3.org/accounts/:accountId' , bearer: 'Bearer' }; var url = dir.url @@ -593,7 +593,7 @@ OAUTH3.requests.accounts.update = function (directive, session, opts) { OAUTH3.requests.accounts.create = function (directive, session, account) { var dir = directive.create_account || { method: 'POST' - , url: OAUTH3.url.normalize(directive.api) + '/api/org.oauth3.provider/accounts' + , url: OAUTH3.url.normalize(directive.api) + '/api/issuer@oauth3.org/accounts' , bearer: 'Bearer' }; var data = { diff --git a/oauth3.tunnel.js b/oauth3.tunnel.js index 3ef5cbc..38bf827 100644 --- a/oauth3.tunnel.js +++ b/oauth3.tunnel.js @@ -9,7 +9,7 @@ OAUTH3.api['tunnel.token'] = function (providerUri, opts) { return OAUTH3.request({ method: 'POST' , url: OAUTH3.url.normalize(providerUri) - + '/api/org.oauth3.tunnel/accounts/' + session.token.sub + '/token' + + '/api/tunnel@oauth3.org/accounts/' + session.token.sub + '/token' , session: session , data: { domains: opts.data.domains From 6b7fd877ecc02c49a4d6304382d9564c459dfd09 Mon Sep 17 00:00:00 2001 From: aj Date: Tue, 15 Aug 2017 20:23:07 +0000 Subject: [PATCH 15/21] org.oauth3 -> oauth3.org --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5ccbddb..e7151fb 100644 --- a/README.md +++ b/README.md @@ -19,13 +19,13 @@ If you have no idea what you're doing 1. Create a folder for your project named after your app, such as `example.com/` 2. Inside of the folder `example.com/` a folder called `assets/` -3. Inside of the folder `example.com/assets` a folder called `org.oauth3/` +3. Inside of the folder `example.com/assets` a folder called `oauth3.org/` 4. Download [oauth3.js-v1.zip](https://git.daplie.com/OAuth3/oauth3.js/repository/archive.zip?ref=v1) 5. Double-click to unzip the folder. -6. Copy the file `oauth3.core.js` into the folder `example.com/assets/org.oauth3/` +6. Copy the file `oauth3.core.js` into the folder `example.com/assets/oauth3.org/` 7. Copy the folder `well-known` into the folder `example.com/` 8. Rename the folder `well-known` to `.well-known` (when you do this, it become invisible, that's okay) -9. Add `` to your `index.html` +9. Add `` to your `index.html` 9. Add `` to your `index.html` 10. Create files in `example.com` called `app.js` and `index.html` and put this in it: @@ -44,7 +44,7 @@ If you have no idea what you're doing - + @@ -81,7 +81,7 @@ function onClickLogin() { console.info('Secure PPID (aka subject):', session.token.sub); return oauth3.request({ - url: 'https://oauth3.org/api/org.oauth3.provider/inspect' + url: 'https://oauth3.org/api/issuer@oauth3.org/inspect' , session: session }).then(function (resp) { From 704337e30b6781412c6e97c78d6aeaccdbef3371 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Thu, 28 Sep 2017 13:50:52 -0600 Subject: [PATCH 16/21] changed error handling to try providing most useful messages --- oauth3.core.js | 14 +++++++++++--- oauth3.node.js | 14 ++++++-------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/oauth3.core.js b/oauth3.core.js index a31940e..2a77c9f 100644 --- a/oauth3.core.js +++ b/oauth3.core.js @@ -892,15 +892,14 @@ xhr = new XMLHttpRequest(); } xhr.onreadystatechange = function () { - var data; if (xhr.readyState !== XMLHttpRequest.DONE) { // nothing to do here return; } + var data, err; if (xhr.status !== 200) { - reject(new Error('bad status code: ' + xhr.status)); - return; + err = new Error('bad status code: ' + xhr.status); } try { @@ -909,6 +908,15 @@ data = xhr.responseText; } + if (data.error) { + err = new Error(data.error.message || data.error_description || JSON.stringify(data.error)); + } + if (err) { + err.result = data; + reject(err); + return; + } + resolve({ _request: xhr , headers: null // TODO diff --git a/oauth3.node.js b/oauth3.node.js index 95b0bd3..cd8d241 100644 --- a/oauth3.node.js +++ b/oauth3.node.js @@ -59,10 +59,7 @@ OAUTH3._node._parseJson = function (resp) { // TODO toCamelCase if (!(resp.statusCode >= 200 && resp.statusCode < 400)) { - // console.log('[A3] DEBUG', resp.body); err = new Error("bad response code: " + resp.statusCode); - err.result = resp.body; - return PromiseA.reject(err); } //console.log('resp.body', typeof resp.body); @@ -70,15 +67,16 @@ OAUTH3._node._parseJson = function (resp) { try { json = JSON.parse(json); } catch(e) { - err = new Error('response not parsable:' + resp.body); - err.result = resp.body; - return PromiseA.reject(err); + err = err || (new Error('response not parsable: ' + resp.body)); } } // handle both Oauth2- and node-style errors - if (json.error) { - err = new Error(json.error && json.error.message || json.error_description || json.error); + if (json && json.error) { + err = new Error(json.error.message || json.error_description || JSON.stringify(json.error)); + } + + if (err) { err.result = json; return PromiseA.reject(err); } From 26e9a1c08bbe4ed5074f07c59cb187ae1242469c Mon Sep 17 00:00:00 2001 From: tigerbot Date: Thu, 12 Oct 2017 12:10:42 -0600 Subject: [PATCH 17/21] fixed bug clearing old session fields in session refresh --- oauth3.core.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/oauth3.core.js b/oauth3.core.js index 2a77c9f..825a882 100644 --- a/oauth3.core.js +++ b/oauth3.core.js @@ -78,7 +78,7 @@ , uri: { normalize: function (uri) { if ('string' !== typeof uri) { - console.error((new Error('stack')).stack); + throw new Error('must provide a string to OAUTH3.uri.normalize'); } // tested with // example.com @@ -94,7 +94,7 @@ , url: { normalize: function (url) { if ('string' !== typeof url) { - console.error((new Error('stack')).stack); + throw new Error('must provide a string to OAUTH3.url.normalize'); } // tested with // example.com @@ -473,7 +473,7 @@ var providerUri = oldSession.provider_uri; var clientUri = oldSession.client_uri; - Object.keys(['access_token', 'token', 'client_uri', 'refresh', 'refresh_token', 'expires_in', 'provider_uri', 'scope', 'token_type']).forEach(function (key) { + ['access_token', 'token', 'client_uri', 'refresh', 'refresh_token', 'expires_in', 'provider_uri', 'scope', 'token_type'].forEach(function (key) { oldSession[key] = undefined; }); Object.keys(newSession).forEach(function (key) { @@ -1198,7 +1198,7 @@ } } } - , _initClient: function (location/*, opts*/) { + , _initClient: function () { var me = this; return OAUTH3.discover(me._clientUri, { client_id: me._clientUri }).then(function (clientDirectives) { me._clientDirectives = clientDirectives; From d7d07b841a27c23f80c422201645fcd871f95a3f Mon Sep 17 00:00:00 2001 From: tigerbot Date: Thu, 19 Oct 2017 15:27:35 -0600 Subject: [PATCH 18/21] made the jwt `freshness` function work with non-expiring tokens --- oauth3.core.js | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/oauth3.core.js b/oauth3.core.js index 825a882..b4385fd 100644 --- a/oauth3.core.js +++ b/oauth3.core.js @@ -224,20 +224,27 @@ return OAUTH3.crypto.core.verify(jwk, data, signature); } - , freshness: function (tokenMeta, staletime, _now) { - staletime = staletime || (15 * 60); - var now = _now || Date.now(); - var fresh = ((parseInt(tokenMeta.exp, 10) || 0) - Math.round(now / 1000)); - - if (fresh >= staletime) { + , freshness: function (tokenMeta, staletime, now) { + // If the token doesn't expire then it's always fresh. + if (!tokenMeta.exp) { return 'fresh'; } - if (fresh <= 0) { - return 'expired'; + staletime = staletime || (15 * 60); + now = now || Date.now(); + // This particular number used to check if time is in milliseconds or seconds will work + // for any date between the years 1973 and 5138. + if (now > 1e11) { + now = Math.round(now / 1000); + } + var exp = parseInt(tokenMeta.exp, 10) || 0; + if (exp < now) { + return 'expired'; + } else if (exp < now + staletime) { + return 'stale'; + } else { + return 'fresh'; } - - return 'stale'; } } , urls: { From 081b2a23de0ecb192cb5152b424f5b93fa166b87 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Wed, 25 Oct 2017 14:12:41 -0600 Subject: [PATCH 19/21] added ability to define request timeout --- oauth3.core.js | 10 +++++++++- oauth3.node.js | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/oauth3.core.js b/oauth3.core.js index dacc5eb..2467190 100644 --- a/oauth3.core.js +++ b/oauth3.core.js @@ -821,7 +821,7 @@ var logoutReq = OAUTH3.urls.logout( directives , { client_id: (opts.client_id || opts.client_uri || OAUTH3.clientUri(OAUTH3._browser.window.location)) - , windowType: 'popup' // we'll figure out background later + , windowType: 'popup' // TODO: figure out background later , broker: opts.broker //, state: opts._state , debug: opts.debug @@ -964,6 +964,11 @@ , status: xhr.status }); }; + xhr.ontimeout = function () { + var err = new Error('ETIMEDOUT'); + err.code = 'ETIMEDOUT'; + reject(err); + }; if (preq.progress) { xhr.upload.onprogress = function (ev) { @@ -981,6 +986,9 @@ // For assets.example.com/assets xhr.withCredentials = true; + if (preq.timeout) { + xhr.timeout = preq.timeout; + } if (preq.data) { headers['Content-Type'] = 'application/json'; // TODO XXX TODO utf8 } diff --git a/oauth3.node.js b/oauth3.node.js index 37b4d98..efa25c6 100644 --- a/oauth3.node.js +++ b/oauth3.node.js @@ -44,6 +44,7 @@ OAUTH3._node.request = function(preq/*, _sys*/) { method: preq.method , url: preq.url || preq.uri , headers: preq.headers + , timeout: preq.timeout || undefined , json: preq.data || preq.body || preq.json || undefined // TODO which to use? , formData: preq.formData || undefined }; From 6acb027b3acbedbe43bddc6f72a500f4ac0ed83b Mon Sep 17 00:00:00 2001 From: tigerbot Date: Wed, 25 Oct 2017 14:13:01 -0600 Subject: [PATCH 20/21] added more information to request errors --- oauth3.core.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/oauth3.core.js b/oauth3.core.js index 2467190..8d178d7 100644 --- a/oauth3.core.js +++ b/oauth3.core.js @@ -950,9 +950,12 @@ if (data.error) { err = new Error(data.error.message || data.error_description || JSON.stringify(data.error)); + Object.assign(err, data.error); } if (err) { - err.result = data; + err._request = xhr; + err.status = xhr.status; + err.data = data; reject(err); return; } From 69d5cb382a95290d750a6adb70a80b38838d4e39 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Tue, 7 Nov 2017 13:17:39 -0700 Subject: [PATCH 21/21] v1.2.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 606658b..47cec23 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oauth3.js", - "version": "1.0.10", + "version": "1.2.2", "description": "The world's smallest, fastest, and most secure OAuth3 (and OAuth2) JavaScript implementation.", "main": "oauth3.node.js", "scripts": {