From 69a92fc2fdd89096ee8eb554c764fb635220a40f Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 14 Feb 2017 14:37:04 -0700 Subject: [PATCH] seems to work... --- oauth3.implicit.js | 348 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 302 insertions(+), 46 deletions(-) diff --git a/oauth3.implicit.js b/oauth3.implicit.js index e3a696b..cd49dc5 100644 --- a/oauth3.implicit.js +++ b/oauth3.implicit.js @@ -7,6 +7,12 @@ clientUri: function (location) { return OAUTH3.utils.uri.normalize(location.host + location.pathname); } + , _formatError: function (providerUri, params) { + var err = new Error(params.error_description || params.error.message || "Unknown error with provider '" + providerUri + "'"); + err.uri = params.error_uri || params.error.uri; + err.code = params.error.code || params.error; + return err; + } , atob: function (base64) { return (exports.atob || require('atob'))(base64); } @@ -117,6 +123,59 @@ return str; } } + , jwt: { + // decode only (no verification) + decode: function (str) { + + // 'abc.qrs.xyz' + // [ 'abc', 'qrs', 'xyz' ] + // [ {}, {}, 'foo' ] + // { header: {}, payload: {}, signature: } + var parts = str.split(/\./g); + var jsons = parts.slice(0, 2).map(function (urlsafe64) { + var atob = exports.atob || require('atob'); + var b64 = OAUTH3.utils._urlSafeBase64ToBase64(urlsafe64); + return atob(b64); + }); + + return { + header: JSON.parse(jsons[0]) + , payload: JSON.parse(jsons[1]) + , signature: parts[2] // should remain url-safe base64 + }; + } + , getFreshness: function (tokenMeta, staletime, now) { + staletime = staletime || (15 * 60); + now = now || Date.now(); + var fresh = ((parseInt(tokenMeta.exp, 10) || 0) - Math.round(now / 1000)); + + if (fresh >= staletime) { + return 'fresh'; + } + + if (fresh <= 0) { + return 'expired'; + } + + return 'stale'; + } + /* + // encode-only (no signature) + , encode: function (parts) { + parts.header = parts.header || { alg: 'none', typ: 'jwt' }; + parts.signature = parts.signature || ''; + + var btoa = exports.btoa || require('btoa'); + var result = [ + core.utils.base64ToUrlSafeBase64(btoa(JSON.stringify(parts.header, null))) + , core.utils.base64ToUrlSafeBase64(btoa(JSON.stringify(parts.payload, null))) + , parts.signature // should already be url-safe base64 + ].join('.'); + + return result; + } + */ + } , urls: { discover: function (providerUri, opts) { if (!providerUri) { @@ -205,6 +264,58 @@ return result; } + , refreshToken: function (directive, opts) { + // grant_type=refresh_token + + // Example Refresh Token Request + // (generally for 1st or 3rd party server-side, mobile, and desktop apps) + // + // POST https://example.com/api/oauth3/access_token + // { "grant_type": "refresh_token", "client_id": "<>", "scope": "<>" + // , "username": "<>", "password": "password" } + // + opts = opts || {}; + var type = 'access_token'; + var grantType = 'refresh_token'; + + var scope = opts.scope || directive.authn_scope; + var clientSecret = opts.client_secret; + var args = directive[type]; + var params = { + "grant_type": grantType + , "refresh_token": opts.refresh_token || (opts.session && opts.session.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) { + // TODO not allowed in the browser + console.warn("if this is a browser, you must not use client_secret"); + params.client_secret = clientSecret; + } + + if (scope) { + params.scope = OAUTH3.utils.scope.stringify(scope); + } + + if ('GET' === args.method.toUpperCase()) { + uri += '?' + OAUTH3.utils.query.stringify(params); + } else { + body = params; + } + + return { + url: uri + , method: args.method + , data: body + }; + } } , hooks: { directives: { @@ -236,6 +347,118 @@ return directives; } } + , session: { + refresh: function (oldSession, newSession) { + var providerUri = oldSession.provider_uri; + var clientUri = oldSession.client_uri; + + console.info('[oauth3.hooks.refreshSession] oldSession', JSON.parse(JSON.stringify(oldSession))); + console.info('[oauth3.hooks.refreshSession] newSession', newSession); + Object.keys(oldSession).forEach(function (key) { + oldSession[key] = undefined; + }); + Object.keys(newSession).forEach(function (key) { + oldSession[key] = newSession[key]; + }); + + // info about the session of this API call + oldSession.provider_uri = providerUri; // aud + oldSession.client_uri = clientUri; // azp + + // info about the newly-discovered token + oldSession.token = OAUTH3.jwt.decode(oldSession.access_token).payload; + + oldSession.token.sub = oldSession.token.sub || oldSession.token.acx.id; + oldSession.token.client_uri = clientUri; + oldSession.token.provider_uri = providerUri; + + if (oldSession.refresh_token) { + oldSession.refresh = OAUTH3.jwt.decode(oldSession.refresh_token).payload; + oldSession.refresh.sub = oldSession.refresh.sub || oldSession.refresh.acx.id; + oldSession.refresh.provider_uri = providerUri; + } + + console.info('[oauth3.hooks.refreshSession] refreshedSession', oldSession); + + // set for a set of audiences + return OAUTH3.PromiseA.resolve(OAUTH3.hooks.session.set(providerUri, oldSession)); + } + , check: function (preq, opts) { + if (!preq.session) { + console.warn('[oauth3.hooks.checkSession] no session'); + return OAUTH3.PromiseA.resolve(null); + } + var freshness = OAUTH3.jwt.freshness(preq.session.token, opts.staletime); + console.info('[oauth3.hooks.checkSession] freshness', freshness, preq.session); + + switch (freshness) { + case 'stale': + return OAUTH3.hooks.session.stale(preq.session); + case 'expired': + return OAUTH3.hooks.session.expired(preq.session).then(function (newSession) { + preq.session = newSession; + return newSession; + }); + //case 'fresh': + default: + return OAUTH3.PromiseA.resolve(preq.session); + } + } + , stale: function (staleSession) { + console.info('[oauth3.hooks.sessionStale] called'); + if (OAUTH3.hooks.session._stalePromise) { + return OAUTH3.PromiseA.resolve(staleSession); + } + + OAUTH3.hooks.session._stalePromise = OAUTH3._refreshToken( + staleSession.provider_uri + , { client_uri: staleSession.client_uri + , session: staleSession + , debug: staleSession.debug + } + ).then(function (newSession) { + OAUTH3.hooks.session._stalePromise = null; + return newSession; // oauth3.hooks.refreshSession(staleSession, newSession); + }, function () { + OAUTH3.hooks.session._stalePromise = null; + }); + + return OAUTH3.PromiseA.resolve(staleSession); + } + , expired: function (expiredSession) { + console.info('[oauth3.hooks.sessionExpired] called'); + return OAUTH3._refreshToken( + expiredSession.provider_uri + , { client_uri: expiredSession.client_uri + , session: expiredSession + , debug: expiredSession.debug + } + ).then(function (newSession) { + return newSession; // oauth3.hooks.refreshSession(expiredSession, newSession); + }); + } + , set: function (providerUri, newSession) { + if (!providerUri) { + console.error(new Error('no providerUri').stack); + throw new Error("providerUri is not set"); + } + providerUri = OAUTH3.utils.uri.normalize(providerUri); + console.warn('[Warn] Please implement OAUTH3.hooks.session.set = function (providerUri, newSession) { return PromiseA; }'); + console.warn(newSession); + if (!OAUTH3.hooks.session._sessions) { OAUTH3.hooks.session._sessions = {}; } + OAUTH3.hooks.session._sessions[providerUri] = newSession; + return OAUTH3.PromiseA.resolve(newSession); + } + , get: function (providerUri) { + providerUri = OAUTH3.utils.uri.normalize(providerUri); + if (!providerUri) { + throw new Error("providerUri is not set"); + } + console.warn('[Warn] Please implement OAUTH3.hooks.session.get = function (providerUri) { return PromiseA; }'); + if (!OAUTH3.hooks.session._sessions) { OAUTH3.hooks.session._sessions = {}; } + return OAUTH3.PromiseA.resolve(OAUTH3.hooks.session._sessions[providerUri]); + } + } } , discover: function (providerUri, opts) { if (!providerUri) { @@ -253,47 +476,6 @@ }); }); } - , request: function (preq) { - return OAUTH3._browser.request(preq); - } - , implicitGrant: function(providerUri, opts) { - var promise; - - if (opts.broker) { - // Discovery can happen in-flow because we know that this is - // a valid oauth3 provider - console.info("broker implicit grant"); - promise = OAUTH3._discoverThenImplicitGrant(providerUri, opts); - } - else { - // Discovery must take place before calling implicitGrant - console.info("direct implicit grant"); - promise = OAUTH3._implicitGrant(OAUTH3.hooks.directives._getCached(providerUri), opts); - } - - return promise.then(function (tokens) { - return OAUTH3.hooks.refreshSession( - opts.session || { - provider_uri: providerUri - , client_id: opts.client_id - , client_uri: opts.client_uri || opts.clientUri - } - , tokens - ); - }); - } - , _discoverThenImplicitGrant: function(providerUri, opts) { - opts.windowType = opts.windowType || 'popup'; - return OAUTH3.discover(providerUri, opts).then(function (directives) { - console.info('Discover complete'); - return OAUTH3._implicitGrant(directives, opts).then(function (tokens) { - console.info('Implicit Grant complete', tokens); - OAUTH3._browser.closeFrame(tokens.state || opts._state); - //opts._state = undefined; - return tokens; - }); - }); - } , _discoverHelper: function(providerUri, opts) { return OAUTH3._discover(providerUri, opts); } @@ -360,6 +542,70 @@ return directives; }); } + , request: function (preq, opts) { + function fetch() { + if (preq.session) { + // TODO check session.token.aud against preq.url to make sure they match + console.warn("[security] session audience checking has not been implemented yet (it's up to you to check)"); + preq.headers = preq.headers || {}; + preq.headers.Authorization = 'Bearer ' + (preq.session.access_token || preq.session.accessToken); + } + + return OAUTH3._requestHelper(preq, opts); + } + + if (!preq.session) { + return fetch(); + } + + return OAUTH3.hooks.session.check(preq, opts).then(fetch); + } + , _requestHelper: function (preq, opts) { + return OAUTH3._browser.request(preq, opts); + } + , implicitGrant: function(providerUri, opts) { + var promise; + + if (opts.broker) { + // Discovery can happen in-flow because we know that this is + // a valid oauth3 provider + console.info("broker implicit grant"); + promise = OAUTH3._discoverThenImplicitGrant(providerUri, opts); + } + else { + // Discovery must take place before calling implicitGrant + console.info("direct implicit grant"); + promise = OAUTH3._implicitGrant(OAUTH3.hooks.directives._getCached(providerUri), opts); + } + + return promise.then(function (tokens) { + // TODO abstract browser bits away + try { + OAUTH3._browser.closeFrame(tokens.state || opts._state, opts); + } catch(e) { + console.warn("[implicitGrant] TODO abstract browser bits away"); + } + opts._state = undefined; + return OAUTH3.hooks.session.refresh( + opts.session || { + provider_uri: providerUri + , client_id: opts.client_id + , client_uri: opts.client_uri || opts.clientUri + } + , tokens + ); + }); + } + , _discoverThenImplicitGrant: function(providerUri, opts) { + opts.windowType = opts.windowType || 'popup'; + return OAUTH3.discover(providerUri, opts).then(function (directives) { + console.info('Discover complete'); + return OAUTH3._implicitGrant(directives, opts).then(function (tokens) { + console.info('Implicit Grant complete', tokens); + return tokens; + }); + }); + } , _implicitGrant: function(directives, opts) { // TODO this may need to be synchronous for browser security policy // Do some stuff @@ -392,11 +638,24 @@ return OAUTH3.PromiseA.reject(OAUTH3.utils._formatError(directives.issuer /*providerUri*/, tokens)); } - OAUTH3._browser.closeFrame(authReq.state, { debug: opts.debug || tokens.debug }); - return tokens; }); } + , _refreshToken: function (providerUri, opts) { + console.info('[oauth3.requests.refreshToken] called', providerUri, opts); + return OAUTH3.discover(providerUri, opts).then(function (directive) { + var prequest = OAUTH3.urls.refreshToken(directive, opts); + + return OAUTH3.request(prequest).then(function (req) { + var data = req.data; + data.provider_uri = providerUri; + if (data.error) { + return OAUTH3.PromiseA.reject(OAUTH3.utils._formatError(providerUri, data)); + } + return OAUTH3.hooks.session.refresh(opts, data); + }); + }); + } // @@ -477,10 +736,7 @@ } - console.log('[oauth3.implicit.js] callbackName', '--oauth3-callback-' + state); window['--oauth3-callback-' + state] = function (params) { - console.log("YO HO YO HO, A Pirate's life for me!", state); - console.error(new Error("Pirate's Life").stack); resolve(params); cleanup(); };