oauth3.js/prefactor/oauth3.js

446 lines
16 KiB
JavaScript

/* global Promise */
(function (exports) {
'use strict';
var oauth3 = {};
var core = exports.OAUTH3_CORE || require('./oauth3.core.js');
oauth3.requests = {};
if ('undefined' !== typeof Promise) {
oauth3.PromiseA = Promise;
} else {
console.warn("[oauth3.js] Remember to call oauth3.providePromise(Promise) with a proper Promise implementation");
}
oauth3.providePromise = function (PromiseA) {
oauth3.PromiseA = PromiseA;
if (oauth3._testPromise) {
return oauth3._testPromise(PromiseA).then(function () {
oauth3.PromiseA = PromiseA;
});
}
oauth3.PromiseA = PromiseA;
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) {
req.originalData = req.data;
req.data = recase.snakeCopy(req.data);
}
return req;
};
oauth3._recaseResponse = function (recase, resp) {
// convert oauth3/ruby snake_case to JavaScript camelCase
if (resp.data && 'object' === typeof resp.data) {
resp.originalData = resp.data;
resp.data = recase.camelCopy(resp.data);
}
return resp;
};
*/
oauth3.hooks = {
checkSession: function (preq, opts) {
if (!preq.session) {
console.warn('[oauth3.hooks.checkSession] no session');
return oauth3.PromiseA.resolve(null);
}
var freshness = oauth3.core.jwt.getFreshness(preq.session.token, opts.staletime);
console.info('[oauth3.hooks.checkSession] freshness', freshness, preq.session);
switch (freshness) {
case 'stale':
return oauth3.hooks.sessionStale(preq.session);
case 'expired':
return oauth3.hooks.sessionExpired(preq.session).then(function (newSession) {
preq.session = newSession;
return newSession;
});
//case 'fresh':
default:
return oauth3.PromiseA.resolve(preq.session);
}
}
, sessionStale: function (staleSession) {
console.info('[oauth3.hooks.sessionStale] called');
if (oauth3.hooks._stalePromise) {
return oauth3.PromiseA.resolve(staleSession);
}
oauth3.hooks._stalePromise = oauth3.requests.refreshToken(
staleSession.provider_uri
, { client_uri: staleSession.client_uri
, session: staleSession
, debug: staleSession.debug
}
).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.info('[oauth3.hooks.sessionExpired] called');
return oauth3.requests.refreshToken(
expiredSession.provider_uri
, { client_uri: expiredSession.client_uri
, session: expiredSession
, debug: expiredSession.debug
}
).then(function (newSession) {
return newSession; // oauth3.hooks.refreshSession(expiredSession, newSession);
});
}
, refreshSession: 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);
// shim for account create which does not return new refresh_token
newSession.refresh_token = newSession.refresh_token || oldSession.refresh_token;
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 = oldSession.meta = core.jwt.decode(oldSession.access_token).payload;
oldSession.token.sub = oldSession.token.sub
|| (oldSession.token.acx && oldSession.token.acx.id)
|| (oldSession.token.axs && oldSession.token.axs.length && oldSession.token.axs[0].appScopedId)
;
oldSession.token.client_uri = clientUri;
oldSession.token.provider_uri = providerUri;
if (!oldSession.token.sub) {
// TODO this is broken hard
console.warn('TODO implementation for OAUTH3.hooks.accounts.create (GUI, CLI, or API)');
}
if (oldSession.refresh_token) {
oldSession.refresh = core.jwt.decode(oldSession.refresh_token).payload;
oldSession.refresh.sub = oldSession.refresh.sub
|| (oldSession.refresh.acx && oldSession.refresh.acx.id)
|| (oldSession.refresh.axs && oldSession.refresh.axs.length && oldSession.refresh.axs[0].appScopedId)
;
oldSession.refresh.provider_uri = providerUri;
}
console.info('[oauth3.hooks.refreshSession] refreshedSession', oldSession);
// set for a set of audiences
return oauth3.PromiseA.resolve(oauth3.hooks.setSession(providerUri, oldSession));
}
, setSession: function (providerUri, newSession) {
if (!providerUri) {
console.error(new Error('no providerUri').stack);
}
providerUri = oauth3.core.normalizeUri(providerUri);
console.warn('[ERROR] Please implement OAUTH3.hooks.setSession = function (providerUri, newSession) { return newSession; }');
console.warn(newSession);
if (!oauth3.hooks._sessions) { oauth3.hooks._sessions = {}; }
oauth3.hooks._sessions[providerUri] = newSession;
return newSession;
}
, getSession: function (providerUri) {
providerUri = oauth3.core.normalizeUri(providerUri);
console.warn('[ERROR] Please implement OAUTH3.hooks.getSession = function (providerUri) { return savedSession; }');
if (!oauth3.hooks._sessions) { oauth3.hooks._sessions = {}; }
return oauth3.hooks._sessions[providerUri];
}
, setDirectives: function (providerUri, directives) {
providerUri = oauth3.core.normalizeUri(providerUri);
console.warn('[oauth3.hooks.setDirectives] PLEASE IMPLEMENT -- Your Fault');
console.warn(directives);
if (!oauth3.hooks._directives) { oauth3.hooks._directives = {}; }
window.localStorage.setItem('directives-' + providerUri, JSON.stringify(directives));
oauth3.hooks._directives[providerUri] = directives;
return directives;
}
, getDirectives: function (providerUri) {
providerUri = oauth3.core.normalizeUri(providerUri);
console.warn('[oauth3.hooks.getDirectives] PLEASE IMPLEMENT -- Your Fault');
if (!oauth3.hooks._directives) { oauth3.hooks._directives = {}; }
return JSON.parse(window.localStorage.getItem('directives-' + providerUri) || '{}');
//return oauth3.hooks._directives[providerUri];
}
// Provider Only
, setGrants: function (clientUri, newGrants) {
clientUri = oauth3.core.normalizeUri(clientUri);
console.warn('[oauth3.hooks.setGrants] PLEASE IMPLEMENT -- Your Fault');
console.warn(newGrants);
if (!oauth3.hooks._grants) { oauth3.hooks._grants = {}; }
console.log('clientUri, newGrants');
console.log(clientUri, newGrants);
oauth3.hooks._grants[clientUri] = newGrants;
return newGrants;
}
, getGrants: function (clientUri) {
clientUri = oauth3.core.normalizeUri(clientUri);
console.warn('[oauth3.hooks.getGrants] PLEASE IMPLEMENT -- Your Fault');
if (!oauth3.hooks._grants) { oauth3.hooks._grants = {}; }
console.log('clientUri, existingGrants');
console.log(clientUri, oauth3.hooks._grants[clientUri]);
return oauth3.hooks._grants[clientUri];
}
};
// TODO simplify (nix recase)
oauth3.provideRequest = function (rawRequest, opts) {
opts = opts || {};
//var Recase = exports.Recase || require('recase');
// TODO make insensitive to providing exceptions
//var recase = Recase.create({ exceptions: {} });
function lintAndRequest(preq) {
function goGetHer() {
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;
}
if (!oauth3._lintRequest) {
return rawRequest(preq);
}
return oauth3._lintRequest(preq, opts).then(function (preq) {
return rawRequest(preq);
});
}
if (!preq.session) {
return goGetHer();
}
console.warn('lintAndRequest checkSession', preq);
return oauth3.hooks.checkSession(preq, opts).then(goGetHer);
}
if (opts.rawCase) {
oauth3.request = lintAndRequest;
return;
}
// Wrap oauth3 api calls in snake_case / camelCase conversion
oauth3.request = function (req, opts) {
//console.log('[D] [oauth3 req.url]', req.url);
opts = opts || {};
if (opts.rawCase) {
return lintAndRequest(req, opts);
}
//req = oauth3._recaseRequest(recase, req);
return lintAndRequest(req, opts).then(function (res) {
//return oauth3._recaseResponse(recase, res);
return res;
});
};
/*
return oauth3._testRequest(request).then(function () {
oauth3.request = request;
});
*/
};
// TODO merge with regular token access point and new response_type=federated ?
oauth3.requests.clientToken = function (providerUri, opts) {
return oauth3.discover(providerUri, opts).then(function (directive) {
return oauth3.request(core.urls.grants(directive, opts)).then(function (grantsResult) {
return grantsResult.originalData || grantsResult.data;
});
});
};
oauth3.requests.grants = function (providerUri, opts) {
return oauth3.discover(providerUri, {
client_id: providerUri
, debug: opts.debug
}).then(function (directive) {
return oauth3.request(core.urls.grants(directive, 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.core.formatError(grants.error));
}
console.warn('requests.grants', grants);
oauth3.hooks.setGrants(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.setGrants(clientId, [ grant ]);
});
return {
client: oauth3.hooks.getGrants(opts.client_id + '-client')
, grants: oauth3.hooks.getGrants(opts.client_id) || []
};
});
});
};
oauth3.requests.loginCode = function (providerUri, opts) {
return oauth3.discover(providerUri, opts).then(function (directive) {
var prequest = core.urls.loginCode(directive, opts);
return oauth3.request(prequest).then(function (res) {
// result = { uuid, expires_at }
return {
otpUuid: res.data.uuid
, otpExpires: res.data.expires_at
};
});
});
};
oauth3.loginCode = oauth3.requests.loginCode;
oauth3.requests.resourceOwnerPassword = function (providerUri, opts) {
//var scope = opts.scope;
//var appId = opts.appId;
return oauth3.discover(providerUri, opts).then(function (directive) {
var prequest = core.urls.resourceOwnerPassword(directive, opts);
return oauth3.request(prequest).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.requests.refreshToken = function (providerUri, opts) {
console.info('[oauth3.requests.refreshToken] called', providerUri, opts);
return oauth3.discover(providerUri, opts).then(function (directive) {
var prequest = core.urls.refreshToken(directive, opts);
return oauth3.request(prequest).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
oauth3.requests._error_description = 'Not Implemented: Please override by including <script src="oauth3.browser.js"></script>';
oauth3.requests.authorizationRedirect = function (/*providerUri, opts*/) {
throw new Error(oauth3.requests._error_description);
};
oauth3.requests.implicitGrant = function (/*providerUri, opts*/) {
throw new Error(oauth3.requests._error_description);
};
oauth3.requests.logout = function (/*providerUri, opts*/) {
throw new Error(oauth3.requests._error_description);
};
oauth3.login = function (providerUri, opts) {
// Four styles of login:
// * background (hidden iframe)
// * iframe (visible iframe, needs border color and width x height params)
// * popup (needs width x height and positioning? params)
// * window (params?)
// Two strategies
// * authorization_redirect (to server authorization code)
// * implicit_grant (default, browser-only)
// If both are selected, implicit happens first and then the other happens in background
var promise;
if (opts.username || opts.password) {
/* jshint ignore:start */
// ingore "confusing use of !"
if (!opts.username !== !(opts.password || opts.otp)) {
throw new Error("you did not specify both username and password");
}
/* jshint ignore:end */
return oauth3.requests.resourceOwnerPassword(providerUri, opts).then(function (resp) {
if (!resp || !resp.data) {
var err = new Error("bad response");
err.response = resp;
err.data = resp && resp.data || undefined;
return oauth3.PromiseA.reject(err);
}
return resp.data;
});
}
// TODO support dual-strategy login
// by default, always get implicitGrant (for client)
// and optionally do authorizationCode (for server session)
if ('background' === opts.type || opts.background) {
opts.type = 'background';
opts.background = true;
}
else {
opts.type = 'popup';
opts.popup = true;
}
if (opts.authorizationRedirect) {
promise = oauth3.requests.authorizationRedirect(providerUri, opts);
}
else {
promise = oauth3.requests.implicitGrant(providerUri, opts);
}
return promise;
};
oauth3.backgroundLogin = function (providerUri, opts) {
opts = opts || {};
opts.type = 'background';
return oauth3.login(providerUri, opts);
};
oauth3.core = core;
oauth3.querystringify = core.querystringify;
oauth3.scopestringify = core.stringifyscope;
oauth3.stringifyscope = core.stringifyscope;
exports.OAUTH3 = oauth3.oauth3 = oauth3.OAUTH3 = oauth3;
exports.oauth3 = exports.OAUTH3;
if ('undefined' !== typeof module) {
module.exports = oauth3;
}
}('undefined' !== typeof exports ? exports : window));