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;
+ });
+ };