WIP refactor (refreshToken works)

This commit is contained in:
AJ ONeal 2017-02-08 04:18:15 -05:00
parent 7ae4d83cfe
commit 9af2f574c0
4 changed files with 247 additions and 106 deletions

View File

@ -14,11 +14,14 @@
var browser = exports.OAUTH3_BROWSER = { var browser = exports.OAUTH3_BROWSER = {
discover: function (providerUri, opts) { discover: function (providerUri, opts) {
if (!providerUri) {
throw new Error('oauth3.discover(providerUri, opts) received providerUri as ' + providerUri);
}
opts = opts || {}; opts = opts || {};
opts.debug = true; opts.debug = true;
console.log('discover providerUri', providerUri); console.log('discover providerUri', providerUri);
providerUri = OAUTH3_CORE.normalizeUrl(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) { return browser.insertIframe(discObj.url, discObj.state, opts).then(function (params) {
if (params.error) { if (params.error) {
@ -147,12 +150,12 @@
// //
// Logins // Logins
// //
, logins: { , requests: {
authorizationRedirect: function (providerUri, opts) { authorizationRedirect: function (providerUri, opts) {
// TODO get own directives // TODO get own directives
return OAUTH3.discover(providerUri, opts).then(function (directive) { return OAUTH3.discover(providerUri, opts).then(function (directive) {
var prequest = OAUTH3_CORE.authorizationRedirect( var prequest = OAUTH3_CORE.urls.authorizationRedirect(
directive directive
, opts , opts
); );
@ -169,7 +172,7 @@
, implicitGrant: function (providerUri, opts) { , implicitGrant: function (providerUri, opts) {
// TODO OAuth3 provider should use the redirect URI as the appId? // TODO OAuth3 provider should use the redirect URI as the appId?
return OAUTH3.discover(providerUri, opts).then(function (directive) { return OAUTH3.discover(providerUri, opts).then(function (directive) {
var prequest = OAUTH3_CORE.implicitGrant( var prequest = OAUTH3_CORE.urls.implicitGrant(
directive directive
// TODO OAuth3 provider should referrer / referer / origin as the appId? // TODO OAuth3 provider should referrer / referer / origin as the appId?
, opts , opts
@ -188,7 +191,7 @@
opts = opts || {}; opts = opts || {};
return OAUTH3.discover(providerUri, opts).then(function (directive) { return OAUTH3.discover(providerUri, opts).then(function (directive) {
var prequest = OAUTH3_CORE.logout( var prequest = OAUTH3_CORE.urls.logout(
directive directive
, opts , opts
); );
@ -228,6 +231,12 @@
}; };
Object.keys(browser).forEach(function (key) { 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]; OAUTH3[key] = browser[key];
}); });

View File

@ -4,6 +4,7 @@
// NOTE: we assume that directive.provider_uri exists // NOTE: we assume that directive.provider_uri exists
var core = {}; var core = {};
core.urls = core;
function getDefaultAppApiBase() { function getDefaultAppApiBase() {
console.warn('[deprecated] using window.location.host when opts.appApiBase should be used'); console.warn('[deprecated] using window.location.host when opts.appApiBase should be used');
@ -76,9 +77,9 @@
}; };
core.formatError = function (providerUri, params) { core.formatError = function (providerUri, params) {
var err = new Error(params.error_description || "Unknown error when discoving provider '" + providerUri + "'"); var err = new Error(params.error_description || params.error.message || "Unknown error when discoving provider '" + providerUri + "'");
err.uri = params.error_uri; err.uri = params.error_uri || params.error.uri;
err.code = params.error; err.code = params.error.code || params.error;
return err; return err;
}; };
core.normalizeUri = function (providerUri) { core.normalizeUri = function (providerUri) {
@ -104,7 +105,7 @@
; ;
}; };
core.discover = function (providerUri, opts) { core.urls.discover = function (providerUri, opts) {
if (!providerUri) { if (!providerUri) {
throw new Error("cannot discover without providerUri"); throw new Error("cannot discover without providerUri");
} }
@ -196,6 +197,21 @@
, signature: parts[2] // should remain url-safe base64 , 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-only (no signature)
, encode: function (parts) { , encode: function (parts) {
parts.header = parts.header || { alg: 'none', typ: 'jwt' }; 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 // Example Authorization Code Request
// (not for use in the browser) // (not for use in the browser)
@ -234,7 +250,7 @@
throw new Error("not implemented"); throw new Error("not implemented");
}; };
core.authorizationRedirect = function (directive, opts) { core.urls.authorizationRedirect = function (directive, opts) {
//console.log('[authorizationRedirect]'); //console.log('[authorizationRedirect]');
// //
// Example Authorization Redirect - from Browser to Consumer API // 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]'); //console.log('[implicitGrant]');
// //
// Example Implicit Grant Request // Example Implicit Grant Request
@ -357,7 +373,7 @@
return result; return result;
}; };
core.loginCode = function (directive, opts) { core.urls.loginCode = function (directive, opts) {
// //
// Example Resource Owner Password Request // Example Resource Owner Password Request
// (generally for 1st party and direct-partner mobile apps, and webapps) // (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 // Example Resource Owner Password Request
// (generally for 1st party and direct-partner mobile apps, and webapps) // (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 // grant_type=refresh_token
// Example Refresh Token Request // Example Refresh Token Request
@ -494,15 +510,14 @@
var grantType = 'refresh_token'; var grantType = 'refresh_token';
var scope = opts.scope || directive.authn_scope; var scope = opts.scope || directive.authn_scope;
var clientId = opts.appId || opts.clientId;
var clientSecret = opts.appSecret || opts.clientSecret; var clientSecret = opts.appSecret || opts.clientSecret;
var args = directive[type]; var args = directive[type];
var params = { var params = {
"grant_type": grantType "grant_type": grantType
, "refresh_token": opts.refreshToken , "refresh_token": opts.refresh_token || opts.refreshToken || (opts.session && opts.session.refresh_token)
, "response_type": 'token' , "response_type": 'token'
//, "client_id": undefined , "client_id": opts.appId || opts.app_id || opts.client_id || opts.clientId || opts.client_id || opts.clientId
//, "client_uri": undefined , "client_uri": opts.client_uri || opts.clientUri
//, "scope": undefined //, "scope": undefined
//, "client_secret": undefined //, "client_secret": undefined
, debug: opts.debug || undefined , debug: opts.debug || undefined
@ -510,14 +525,7 @@
var uri = args.url; var uri = args.url;
var body; var body;
if (opts.clientUri) { // TODO not allowed in the browser
params.client_uri = opts.clientUri;
}
if (clientId) {
params.client_id = clientId;
}
if (clientSecret) { if (clientSecret) {
params.client_secret = clientSecret; params.client_secret = clientSecret;
} }
@ -539,7 +547,7 @@
}; };
}; };
core.logout = function (directive, opts) { core.urls.logout = function (directive, opts) {
opts = opts || {}; opts = opts || {};
var type = 'logout'; var type = 'logout';
var clientId = opts.appId || opts.clientId || opts.client_id; var clientId = opts.appId || opts.clientId || opts.client_id;

210
oauth3.js
View File

@ -3,11 +3,10 @@
'use strict'; 'use strict';
var oauth3 = {}; var oauth3 = {};
var logins = {};
var core = exports.OAUTH3_CORE || require('./oauth3.core.js'); var core = exports.OAUTH3_CORE || require('./oauth3.core.js');
oauth3.requests = logins; oauth3.requests = {};
if ('undefined' !== typeof Promise) { if ('undefined' !== typeof Promise) {
oauth3.PromiseA = Promise; oauth3.PromiseA = Promise;
@ -27,6 +26,7 @@
return PromiseA.resolve(); return PromiseA.resolve();
}; };
// TODO move recase out
oauth3._recaseRequest = function (recase, req) { oauth3._recaseRequest = function (recase, req) {
// convert JavaScript camelCase to oauth3/ruby snake_case // convert JavaScript camelCase to oauth3/ruby snake_case
if (req.data && 'object' === typeof req.data) { if (req.data && 'object' === typeof req.data) {
@ -44,64 +44,90 @@
} }
return resp; return resp;
}; };
oauth3._lintRequest = function (preq, opts) {
var providerUri;
var fresh;
console.log('[os] request meta opts', opts); oauth3.hooks = {
checkSession: function (preq, opts) {
// check that the JWT is not expired if (!preq.session) {
// TODO check that this request applies to the aud and azp console.error('NO SESSION to consider');
if (!(preq.session && preq.session.accessToken)) { return oauth3.PromiseA.resolve(null);
console.log('[os] no session/accessTokenData');
return oauth3.PromiseA.resolve(preq);
} }
var freshness = oauth3.core.jwt.getFreshness(preq.session.meta, opts.staletime);
console.log('checkSession', freshness, preq.session);
preq.headers = preq.headers || {}; switch (freshness) {
preq.headers.Authorization = 'Bearer ' + preq.session.accessToken; case 'stale':
return oauth3.hooks.sessionStale(preq.session);
if (!preq.session._accessTokenData) { case 'expired':
console.log('[os] no _accessTokenData'); console.log('expired checkSession', preq.session);
preq.session._accessTokenData = core.jwt.decode(preq.session.accessToken).payload; return oauth3.hooks.sessionExpired(preq.session).then(function (newSession) {
} preq.session = newSession;
return newSession;
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;
}
// TODO fire session update event
res.data.providerUri = preq.session.providerUri;
preq.session = res.data;
preq.headers.Authorization = 'Bearer ' + preq.session.accessToken;
return preq;
}); });
}; //case 'fresh':
default:
return oauth3.PromiseA.resolve(preq.session);
}
}
, sessionStale: function (staleSession) {
if (oauth3.hooks._stalePromise) {
return oauth3.PromiseA.resolve(staleSession);
}
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) { oauth3.provideRequest = function (rawRequest, opts) {
opts = opts || {}; opts = opts || {};
var Recase = exports.Recase || require('recase'); var Recase = exports.Recase || require('recase');
@ -109,11 +135,27 @@
var recase = Recase.create({ exceptions: {} }); var recase = Recase.create({ exceptions: {} });
function lintAndRequest(preq) { function lintAndRequest(preq) {
function goGetHer() {
if (!oauth3._lintRequest) {
return rawRequest(preq);
}
return oauth3._lintRequest(preq, opts).then(function (preq) { return oauth3._lintRequest(preq, opts).then(function (preq) {
return rawRequest(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) { if (opts.rawCase) {
oauth3.request = lintAndRequest; oauth3.request = lintAndRequest;
return; return;
@ -141,9 +183,9 @@
*/ */
}; };
oauth3.loginCode = function (providerUri, opts) { oauth3.requests.loginCode = function (providerUri, opts) {
return oauth3.discover(providerUri, opts).then(function (directive) { 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); 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); console.log('DEBUG oauth3.resourceOwnerPassword opts', opts);
//var scope = opts.scope; //var scope = opts.scope;
//var appId = opts.appId; //var appId = opts.appId;
return oauth3.discover(providerUri, opts).then(function (directive) { 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); console.log('[DEBUG] [core] resourceOwnerPassword URL', prequest);
@ -171,13 +214,25 @@
url: prequest.url url: prequest.url
, method: prequest.method , method: prequest.method
, data: prequest.data , 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) { 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); console.log('[DEBUG] [core] refreshToken URL', prequest);
@ -185,20 +240,28 @@
url: prequest.url url: prequest.url
, method: prequest.method , method: prequest.method
, data: prequest.data , 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 // 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 <script src="oauth3.browser.js"></script>'; oauth3.requests._error_description = 'Not Implemented: Please override by including <script src="oauth3.browser.js"></script>';
logins.authorizationRedirect = function (/*providerUri, opts*/) { oauth3.requests.authorizationRedirect = function (/*providerUri, opts*/) {
throw new Error(logins._error_description); throw new Error(oauth3.requests._error_description);
}; };
logins.implicitGrant = function (/*providerUri, opts*/) { oauth3.requests.implicitGrant = function (/*providerUri, opts*/) {
throw new Error(logins._error_description); throw new Error(oauth3.requests._error_description);
}; };
logins.logout = function (/*providerUri, opts*/) { oauth3.requests.logout = function (/*providerUri, opts*/) {
throw new Error(logins._error_description); throw new Error(oauth3.requests._error_description);
}; };
oauth3.login = function (providerUri, opts) { oauth3.login = function (providerUri, opts) {
@ -226,12 +289,7 @@
} }
/* jshint ignore:end */ /* jshint ignore:end */
var username = opts.username; return oauth3.requests.resourceOwnerPassword(providerUri, opts).then(function (resp) {
var password = opts.password;
delete opts.username;
delete opts.password;
return oauth3.resourceOwnerPassword(providerUri, username, password, opts).then(function (resp) {
if (!resp || !resp.data) { if (!resp || !resp.data) {
var err = new Error("bad response"); var err = new Error("bad response");
err.response = resp; err.response = resp;
@ -254,10 +312,10 @@
opts.popup = true; opts.popup = true;
} }
if (opts.authorizationRedirect) { if (opts.authorizationRedirect) {
promise = logins.authorizationRedirect(providerUri, opts); promise = oauth3.requests.authorizationRedirect(providerUri, opts);
} }
else { else {
promise = logins.implicitGrant(providerUri, opts); promise = oauth3.requests.implicitGrant(providerUri, opts);
} }
return promise; return promise;

View File

@ -90,3 +90,69 @@
return OAUTH3.PromiseA.reject(err); 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;
});
};