diff --git a/oauth3.browser.js b/oauth3.browser.js index b0ceb70..856b08a 100644 --- a/oauth3.browser.js +++ b/oauth3.browser.js @@ -14,11 +14,14 @@ var browser = exports.OAUTH3_BROWSER = { discover: function (providerUri, opts) { + if (!providerUri) { + throw new Error('oauth3.discover(providerUri, opts) received providerUri as ' + providerUri); + } opts = opts || {}; opts.debug = true; console.log('discover providerUri', providerUri); providerUri = OAUTH3_CORE.normalizeUrl(providerUri); - var discObj = OAUTH3_CORE.discover(providerUri, { appUrl: (opts.appUrl || getDefaultAppUrl()), debug: opts.debug }); + var discObj = OAUTH3_CORE.urls.discover(providerUri, { appUrl: (opts.appUrl || getDefaultAppUrl()), debug: opts.debug }); return browser.insertIframe(discObj.url, discObj.state, opts).then(function (params) { if (params.error) { @@ -147,12 +150,12 @@ // // Logins // - , logins: { + , requests: { authorizationRedirect: function (providerUri, opts) { // TODO get own directives return OAUTH3.discover(providerUri, opts).then(function (directive) { - var prequest = OAUTH3_CORE.authorizationRedirect( + var prequest = OAUTH3_CORE.urls.authorizationRedirect( directive , opts ); @@ -169,7 +172,7 @@ , implicitGrant: function (providerUri, opts) { // TODO OAuth3 provider should use the redirect URI as the appId? return OAUTH3.discover(providerUri, opts).then(function (directive) { - var prequest = OAUTH3_CORE.implicitGrant( + var prequest = OAUTH3_CORE.urls.implicitGrant( directive // TODO OAuth3 provider should referrer / referer / origin as the appId? , opts @@ -188,7 +191,7 @@ opts = opts || {}; return OAUTH3.discover(providerUri, opts).then(function (directive) { - var prequest = OAUTH3_CORE.logout( + var prequest = OAUTH3_CORE.urls.logout( directive , opts ); @@ -228,6 +231,12 @@ }; Object.keys(browser).forEach(function (key) { + if ('requests' === key) { + Object.keys(browser.requests).forEach(function (key) { + OAUTH3.requests[key] = browser.requests[key]; + }); + return; + } OAUTH3[key] = browser[key]; }); diff --git a/oauth3.core.js b/oauth3.core.js index 61e0cae..9a1b947 100644 --- a/oauth3.core.js +++ b/oauth3.core.js @@ -4,6 +4,7 @@ // NOTE: we assume that directive.provider_uri exists var core = {}; + core.urls = core; function getDefaultAppApiBase() { console.warn('[deprecated] using window.location.host when opts.appApiBase should be used'); @@ -76,9 +77,9 @@ }; core.formatError = function (providerUri, params) { - var err = new Error(params.error_description || "Unknown error when discoving provider '" + providerUri + "'"); - err.uri = params.error_uri; - err.code = params.error; + var err = new Error(params.error_description || params.error.message || "Unknown error when discoving provider '" + providerUri + "'"); + err.uri = params.error_uri || params.error.uri; + err.code = params.error.code || params.error; return err; }; core.normalizeUri = function (providerUri) { @@ -104,7 +105,7 @@ ; }; - core.discover = function (providerUri, opts) { + core.urls.discover = function (providerUri, opts) { if (!providerUri) { throw new Error("cannot discover without providerUri"); } @@ -196,6 +197,21 @@ , signature: parts[2] // should remain url-safe base64 }; } + , getFreshness: function (meta, staletime, now) { + staletime = staletime || (15 * 60); + now = now || Date.now(); + var fresh = ((parseInt(meta.exp, 10) || 0) - (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' }; @@ -212,7 +228,7 @@ } }; - core.authorizationCode = function (/*directive, scope, redirectUri, clientId*/) { + core.urls.authorizationCode = function (/*directive, scope, redirectUri, clientId*/) { // // Example Authorization Code Request // (not for use in the browser) @@ -234,7 +250,7 @@ throw new Error("not implemented"); }; - core.authorizationRedirect = function (directive, opts) { + core.urls.authorizationRedirect = function (directive, opts) { //console.log('[authorizationRedirect]'); // // Example Authorization Redirect - from Browser to Consumer API @@ -293,7 +309,7 @@ }; }; - core.implicitGrant = function (directive, opts) { + core.urls.implicitGrant = function (directive, opts) { //console.log('[implicitGrant]'); // // Example Implicit Grant Request @@ -357,7 +373,7 @@ return result; }; - core.loginCode = function (directive, opts) { + core.urls.loginCode = function (directive, opts) { // // Example Resource Owner Password Request // (generally for 1st party and direct-partner mobile apps, and webapps) @@ -404,7 +420,7 @@ }; }; - core.resourceOwnerPassword = function (directive, opts) { + core.urls.resourceOwnerPassword = function (directive, opts) { // // Example Resource Owner Password Request // (generally for 1st party and direct-partner mobile apps, and webapps) @@ -479,7 +495,7 @@ }; }; - core.refreshToken = function (directive, opts) { + core.urls.refreshToken = function (directive, opts) { // grant_type=refresh_token // Example Refresh Token Request @@ -494,15 +510,14 @@ var grantType = 'refresh_token'; var scope = opts.scope || directive.authn_scope; - var clientId = opts.appId || opts.clientId; var clientSecret = opts.appSecret || opts.clientSecret; var args = directive[type]; var params = { "grant_type": grantType - , "refresh_token": opts.refreshToken + , "refresh_token": opts.refresh_token || opts.refreshToken || (opts.session && opts.session.refresh_token) , "response_type": 'token' - //, "client_id": undefined - //, "client_uri": undefined + , "client_id": opts.appId || opts.app_id || opts.client_id || opts.clientId || opts.client_id || opts.clientId + , "client_uri": opts.client_uri || opts.clientUri //, "scope": undefined //, "client_secret": undefined , debug: opts.debug || undefined @@ -510,14 +525,7 @@ var uri = args.url; var body; - if (opts.clientUri) { - params.client_uri = opts.clientUri; - } - - if (clientId) { - params.client_id = clientId; - } - + // TODO not allowed in the browser if (clientSecret) { params.client_secret = clientSecret; } @@ -539,7 +547,7 @@ }; }; - core.logout = function (directive, opts) { + core.urls.logout = function (directive, opts) { opts = opts || {}; var type = 'logout'; var clientId = opts.appId || opts.clientId || opts.client_id; diff --git a/oauth3.js b/oauth3.js index 7cc83cf..1dd6489 100644 --- a/oauth3.js +++ b/oauth3.js @@ -3,11 +3,10 @@ 'use strict'; var oauth3 = {}; - var logins = {}; var core = exports.OAUTH3_CORE || require('./oauth3.core.js'); - oauth3.requests = logins; + oauth3.requests = {}; if ('undefined' !== typeof Promise) { oauth3.PromiseA = Promise; @@ -27,6 +26,7 @@ return PromiseA.resolve(); }; + // TODO move recase out oauth3._recaseRequest = function (recase, req) { // convert JavaScript camelCase to oauth3/ruby snake_case if (req.data && 'object' === typeof req.data) { @@ -44,64 +44,90 @@ } return resp; }; - oauth3._lintRequest = function (preq, opts) { - var providerUri; - var fresh; - console.log('[os] request meta opts', opts); + oauth3.hooks = { + checkSession: function (preq, opts) { + if (!preq.session) { + console.error('NO SESSION to consider'); + return oauth3.PromiseA.resolve(null); + } + var freshness = oauth3.core.jwt.getFreshness(preq.session.meta, opts.staletime); + console.log('checkSession', freshness, preq.session); - // check that the JWT is not expired - // TODO check that this request applies to the aud and azp - if (!(preq.session && preq.session.accessToken)) { - console.log('[os] no session/accessTokenData'); - return oauth3.PromiseA.resolve(preq); + switch (freshness) { + case 'stale': + return oauth3.hooks.sessionStale(preq.session); + case 'expired': + console.log('expired checkSession', preq.session); + return oauth3.hooks.sessionExpired(preq.session).then(function (newSession) { + preq.session = newSession; + return newSession; + }); + //case 'fresh': + default: + return oauth3.PromiseA.resolve(preq.session); + } } - - preq.headers = preq.headers || {}; - preq.headers.Authorization = 'Bearer ' + preq.session.accessToken; - - if (!preq.session._accessTokenData) { - console.log('[os] no _accessTokenData'); - preq.session._accessTokenData = core.jwt.decode(preq.session.accessToken).payload; - } - - if (!preq.url.match(preq.session._accessTokenData.aud)) { - console.log("[os] doesn't match audience", preq.session._accessTokenData.aud); - return oauth3.PromiseA.resolve(preq); - } - - fresh = (Date.now() / 1000) >= (parseInt(preq.session._accessTokenData.exp) || 0); - if (!fresh) { - console.log("[os] isn't fresh", preq.session._accessTokenData.exp); - return oauth3.PromiseA.resolve(preq); - } - - if (!preq.session.refreshToken) { - console.log("[os] cann't refresh", preq.session); - return oauth3.PromiseA.resolve(preq); - } - - opts.refreshToken = preq.session.refreshToken; - console.log('[oauth3.js] refreshToken attempt'); - - // TODO include directive? - providerUri = preq.session.providerUri || preq.session._accessTokenData.iss; - //opts. - return oauth3.refreshToken(providerUri, opts).then(function (res) { - console.log('[oauth3.js] refreshToken result:', res); - - if (!res.data.accessToken) { - return preq; + , sessionStale: function (staleSession) { + if (oauth3.hooks._stalePromise) { + return oauth3.PromiseA.resolve(staleSession); } - // TODO fire session update event - res.data.providerUri = preq.session.providerUri; - preq.session = res.data; - preq.headers.Authorization = 'Bearer ' + preq.session.accessToken; - return preq; - }); - }; + oauth3.hooks._stalePromise = oauth3.requests.refreshToken( + staleSession.provider_uri + , staleSession + ).then(function (newSession) { + oauth3.hooks._stalePromise = null; + return newSession; // oauth3.hooks.refreshSession(staleSession, newSession); + }, function () { + oauth3.hooks._stalePromise = null; + }); + return oauth3.PromiseA.resolve(staleSession); + } + , sessionExpired: function (expiredSession) { + console.log('expiredSession'); + console.log(expiredSession); + return oauth3.requests.refreshToken(expiredSession.provider_uri, expiredSession).then(function (newSession) { + return newSession; // oauth3.hooks.refreshSession(expiredSession, newSession); + }); + } + , refreshSession: function (oldSession, newSession) { + var providerUri = oldSession.provider_uri; + var clientUri = oldSession.client_uri; + + Object.keys(oldSession).forEach(function (key) { + oldSession[key] = undefined; + }); + Object.keys(newSession).forEach(function (key) { + oldSession[key] = newSession[key]; + }); + + console.info('refreshSession', oldSession, newSession); + oldSession.meta = core.jwt.decode(oldSession.access_token).payload; + oldSession.meta.sub = oldSession.meta.sub || oldSession.meta.acx.id; + oldSession.client_uri = clientUri; + oldSession.meta.client_uri = clientUri; + oldSession.provider_uri = providerUri; + oldSession.meta.provider_uri = providerUri; + + oldSession._accessTokenData = oldSession.data = oldSession.meta; + + if (oldSession.refresh_token || oldSession.refreshToken) { + oldSession.refresh = core.jwt.decode(oldSession.refresh_token || oldSession.refreshToken).payload; + oldSession.refresh.sub = oldSession.refresh.sub || oldSession.refresh.acx.id; + oldSession.refresh.provider_uri = providerUri; + } + + return oauth3.PromiseA.resolve(oauth3.hooks.setSession(oldSession)); + } + , setSession: function (newSession) { + console.warn('oauth3.hooks.setSession is not implemented'); + //console.warn(JSON.parse(JSON.stringify(oldSession))); + console.warn(newSession); + return newSession; + } + }; oauth3.provideRequest = function (rawRequest, opts) { opts = opts || {}; var Recase = exports.Recase || require('recase'); @@ -109,9 +135,25 @@ var recase = Recase.create({ exceptions: {} }); function lintAndRequest(preq) { - return oauth3._lintRequest(preq, opts).then(function (preq) { - return rawRequest(preq); - }); + function goGetHer() { + if (!oauth3._lintRequest) { + return rawRequest(preq); + } + return oauth3._lintRequest(preq, opts).then(function (preq) { + return rawRequest(preq); + }); + } + + if (!preq.session) { + return goGetHer(); + } + + preq.headers = preq.headers || {}; + preq.headers.Authorization = 'Bearer ' + (preq.session.access_token || preq.session.accessToken); + + console.warn('lintAndRequest checkSession', preq); + return oauth3.hooks.checkSession(preq, opts).then(goGetHer); + } if (opts.rawCase) { @@ -141,9 +183,9 @@ */ }; - oauth3.loginCode = function (providerUri, opts) { + oauth3.requests.loginCode = function (providerUri, opts) { return oauth3.discover(providerUri, opts).then(function (directive) { - var prequest = core.loginCode(directive, opts); + var prequest = core.urls.loginCode(directive, opts); console.log('[DEBUG] [core] loginCode URL', prequest); @@ -157,13 +199,14 @@ }); }); }; + oauth3.loginCode = oauth3.requests.loginCode; - oauth3.resourceOwnerPassword = function (providerUri, username, passphrase, opts) { + oauth3.requests.resourceOwnerPassword = function (providerUri, opts) { console.log('DEBUG oauth3.resourceOwnerPassword opts', opts); //var scope = opts.scope; //var appId = opts.appId; return oauth3.discover(providerUri, opts).then(function (directive) { - var prequest = core.resourceOwnerPassword(directive, opts); + var prequest = core.urls.resourceOwnerPassword(directive, opts); console.log('[DEBUG] [core] resourceOwnerPassword URL', prequest); @@ -171,13 +214,25 @@ url: prequest.url , method: prequest.method , data: prequest.data + }).then(function (req) { + var data = (req.originalData || req.data); + data.provider_uri = providerUri; + if (data.error) { + return oauth3.PromiseA.reject(oauth3.core.formatError(providerUri, data.error)); + } + return oauth3.hooks.refreshSession( + opts.session || { provider_uri: providerUri, client_uri: opts.client_uri || opts.clientUri } + , data + ); }); }); }; + oauth3.resourceOwnerPassword = oauth3.requests.resourceOwnerPassword; - oauth3.refreshToken = function (providerUri, opts) { + oauth3.requests.refreshToken = function (providerUri, opts) { + console.warn('oauth3.requests.refreshToken', providerUri, opts); return oauth3.discover(providerUri, opts).then(function (directive) { - var prequest = core.refreshToken(directive, opts); + var prequest = core.urls.refreshToken(directive, opts); console.log('[DEBUG] [core] refreshToken URL', prequest); @@ -185,20 +240,28 @@ url: prequest.url , method: prequest.method , data: prequest.data + }).then(function (req) { + var data = (req.originalData || req.data); + data.provider_uri = providerUri; + if (data.error) { + return oauth3.PromiseA.reject(oauth3.core.formatError(providerUri, data)); + } + return oauth3.hooks.refreshSession(opts, data); }); }); }; + oauth3.refreshToken = oauth3.requests.refreshToken; // TODO It'll be very interesting to see if we can do some browser popup stuff from the CLI - logins._error_description = 'Not Implemented: Please override by including '; - logins.authorizationRedirect = function (/*providerUri, opts*/) { - throw new Error(logins._error_description); + oauth3.requests._error_description = 'Not Implemented: Please override by including '; + oauth3.requests.authorizationRedirect = function (/*providerUri, opts*/) { + throw new Error(oauth3.requests._error_description); }; - logins.implicitGrant = function (/*providerUri, opts*/) { - throw new Error(logins._error_description); + oauth3.requests.implicitGrant = function (/*providerUri, opts*/) { + throw new Error(oauth3.requests._error_description); }; - logins.logout = function (/*providerUri, opts*/) { - throw new Error(logins._error_description); + oauth3.requests.logout = function (/*providerUri, opts*/) { + throw new Error(oauth3.requests._error_description); }; oauth3.login = function (providerUri, opts) { @@ -226,12 +289,7 @@ } /* jshint ignore:end */ - var username = opts.username; - var password = opts.password; - delete opts.username; - delete opts.password; - - return oauth3.resourceOwnerPassword(providerUri, username, password, opts).then(function (resp) { + return oauth3.requests.resourceOwnerPassword(providerUri, opts).then(function (resp) { if (!resp || !resp.data) { var err = new Error("bad response"); err.response = resp; @@ -254,10 +312,10 @@ opts.popup = true; } if (opts.authorizationRedirect) { - promise = logins.authorizationRedirect(providerUri, opts); + promise = oauth3.requests.authorizationRedirect(providerUri, opts); } else { - promise = logins.implicitGrant(providerUri, opts); + promise = oauth3.requests.implicitGrant(providerUri, opts); } return promise; diff --git a/oauth3.lint.js b/oauth3.lint.js index 36c89a1..d956a0b 100644 --- a/oauth3.lint.js +++ b/oauth3.lint.js @@ -90,3 +90,69 @@ return OAUTH3.PromiseA.reject(err); } }; + + core.tokenState = function (session) { + var fresh; + fresh = (Date.now() / 1000) >= (parseInt(session._accessTokenData.exp) || 0); + if (!fresh) { + console.log("[os] isn't fresh", session._accessTokenData.exp); + } + }; + oauth3._lintRequest = function (preq, opts) { + var providerUri; + + console.log('[os] request meta opts', opts); + + // check that the JWT is not expired + // TODO check that this request applies to the aud and azp + if (!(preq.session && preq.session.accessToken)) { + console.log('[os] no session/accessTokenData'); + return oauth3.PromiseA.resolve(preq); + } + + preq.headers = preq.headers || {}; + preq.headers.Authorization = 'Bearer ' + preq.session.accessToken; + + if (!preq.session._accessTokenData) { + console.log('[os] no _accessTokenData'); + preq.session._accessTokenData = core.jwt.decode(preq.session.accessToken).payload; + } + + if (!preq.url.match(preq.session._accessTokenData.aud)) { + console.log("[os] doesn't match audience", preq.session._accessTokenData.aud); + return oauth3.PromiseA.resolve(preq); + } + + switch (core.tokenState(session)) { + case 'fresh': + return oauth3.PromiseA.resolve(preq); + case 'stale': + case 'useless': + break; + } + + if (!preq.session.refreshToken) { + console.log("[os] can't refresh", preq.session); + return oauth3.PromiseA.resolve(preq); + } + + opts.refreshToken = preq.session.refreshToken; + console.log('[oauth3.js] refreshToken attempt'); + + // TODO include directive? + providerUri = preq.session.providerUri || preq.session._accessTokenData.iss; + //opts. + return oauth3.refreshToken(providerUri, opts).then(function (res) { + console.log('[oauth3.js] refreshToken result:', res); + + if (!res.data.accessToken) { + return preq; + } + + // TODO fire session update event + res.data.providerUri = preq.session.providerUri; + preq.session = res.data; + preq.headers.Authorization = 'Bearer ' + preq.session.accessToken; + return preq; + }); + };