561 lines
19 KiB
JavaScript
561 lines
19 KiB
JavaScript
;(function (exports) {
|
|
'use strict';
|
|
|
|
var OAUTH3 = exports.OAUTH3;
|
|
var OAUTH3_CORE = exports.OAUTH3_CORE;
|
|
|
|
function getDefaultAppUrl() {
|
|
console.warn('[deprecated] using window.location.{protocol, host, pathname} when opts.client_id should be used');
|
|
return window.location.protocol
|
|
+ '//' + window.location.host
|
|
+ (window.location.pathname).replace(/\/?$/, '')
|
|
;
|
|
}
|
|
|
|
var browser = exports.OAUTH3_BROWSER = {
|
|
window: window
|
|
, clientUri: function (location) {
|
|
return OAUTH3_CORE.normalizeUri(location.host + location.pathname);
|
|
}
|
|
, discover: function (providerUri, opts) {
|
|
if (!providerUri) {
|
|
throw new Error('oauth3.discover(providerUri, opts) received providerUri as ' + providerUri);
|
|
}
|
|
var directives = OAUTH3.hooks.getDirectives(providerUri);
|
|
if (directives && directives.issuer) {
|
|
return OAUTH3.PromiseA.resolve(directives);
|
|
}
|
|
return browser._discoverHelper(providerUri, opts).then(function (directives) {
|
|
directives.issuer = directives.issuer || OAUTH3_CORE.normalizeUrl(providerUri);
|
|
console.log('discoverHelper', directives);
|
|
return OAUTH3.hooks.setDirectives(providerUri, directives);
|
|
});
|
|
}
|
|
, _discoverHelper: function (providerUri, opts) {
|
|
opts = opts || {};
|
|
//opts.debug = true;
|
|
providerUri = OAUTH3_CORE.normalizeUrl(providerUri);
|
|
if (window.location.hostname.match(providerUri)) {
|
|
console.warn("It looks like you're a provider checking for your own directive,"
|
|
+ " so we we're just gonna use OAUTH3.request({ method: 'GET', url: '.well-known/oauth3/directive.json' })");
|
|
return OAUTH3.request({
|
|
method: 'GET'
|
|
, url: OAUTH3.core.normalizeUrl(providerUri) + '/.well-known/oauth3/directives.json'
|
|
});
|
|
}
|
|
|
|
if (!window.location.hostname.match(opts.client_id || opts.client_uri)) {
|
|
console.warn("It looks like your client_id doesn't match your current window... this probably won't end well");
|
|
console.warn(opts.client_id || opts.client_uri, window.location.hostname);
|
|
}
|
|
var discObj = OAUTH3_CORE.urls.discover(providerUri, { client_id: (opts.client_id || opts.client_uri || getDefaultAppUrl()), debug: opts.debug });
|
|
|
|
// TODO ability to reuse iframe instead of closing
|
|
return browser.insertIframe(discObj.url, discObj.state, opts).then(function (params) {
|
|
if (params.error) {
|
|
return OAUTH3_CORE.formatError(providerUri, params.error);
|
|
}
|
|
var directives = JSON.parse(atob(OAUTH3_CORE.utils.urlSafeBase64ToBase64(params.result || params.directives)));
|
|
return directives;
|
|
}, function (err) {
|
|
return OAUTH3.PromiseA.reject(err);
|
|
});
|
|
}
|
|
|
|
, discoverAuthorizationDialog: function(providerUri, opts) {
|
|
var discObj = OAUTH3.core.discover(providerUri, opts);
|
|
|
|
// hmm... we're gonna need a broker for this since switching windows is distracting,
|
|
// popups are obnoxious, iframes are sometimes blocked, and most servers don't implement CORS
|
|
// eventually it should be the browser (and postMessage may be a viable option now), but whatever...
|
|
|
|
// TODO allow postMessage from providerUri in addition to callback
|
|
var discWin = OAUTH3.openWindow(discObj.url, discObj.state, { reuseWindow: 'conquerer' });
|
|
return discWin.then(function (params) {
|
|
console.log('discwin params');
|
|
console.log(params);
|
|
// discWin.child
|
|
// TODO params should have response_type indicating json, binary, etc
|
|
var directives = JSON.parse(atob(OAUTH3.core.utils.urlSafeBase64ToBase64(params.result || params.directives)));
|
|
console.log('directives');
|
|
console.log(directives);
|
|
|
|
// Do some stuff
|
|
var authObj = OAUTH3.core.implicitGrant(
|
|
directives
|
|
, { redirect_uri: opts.redirect_uri
|
|
, debug: opts.debug
|
|
, client_id: opts.client_id || opts.client_uri
|
|
, client_uri: opts.client_uri || opts.client_id
|
|
}
|
|
);
|
|
|
|
if (params.debug) {
|
|
window.alert("DEBUG MODE: Pausing so you can look at logs and whatnot :) Fire at will!");
|
|
}
|
|
|
|
return new OAUTH3.PromiseA(function (resolve, reject) {
|
|
// TODO check if authObj.url is relative or full
|
|
discWin.child.location = OAUTH3.core.urls.resolve(providerUri, authObj.url);
|
|
|
|
if (params.debug) {
|
|
discWin.child.focus();
|
|
}
|
|
|
|
window['--oauth3-callback-' + authObj.state] = function (tokens) {
|
|
if (tokens.error) {
|
|
return reject(OAUTH3.core.formatError(tokens.error));
|
|
}
|
|
|
|
if (params.debug || tokens.debug) {
|
|
if (window.confirm("DEBUG MODE: okay to close oauth3 window?")) {
|
|
discWin.child.close();
|
|
}
|
|
}
|
|
else {
|
|
discWin.child.close();
|
|
}
|
|
|
|
resolve(tokens);
|
|
};
|
|
});
|
|
}).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
|
|
);
|
|
});
|
|
}
|
|
|
|
, frameRequest: function (url, state, opts) {
|
|
var promise;
|
|
|
|
if (!opts.windowType) {
|
|
opts.windowType = 'popup';
|
|
}
|
|
|
|
if ('background' === opts.windowType) {
|
|
promise = browser.insertIframe(url, state, opts);
|
|
} else if ('popup' === opts.windowType) {
|
|
promise = browser.openWindow(url, state, opts);
|
|
} else if ('inline' === opts.windowType) {
|
|
// callback function will never execute and would need to redirect back to current page
|
|
// rather than the callback.html
|
|
url += '&original_url=' + browser.window.location.href;
|
|
promise = browser.window.location = url;
|
|
} else {
|
|
throw new Error("login framing method options.windowType not specified or not type yet implemented");
|
|
}
|
|
|
|
return promise.then(function (params) {
|
|
var err;
|
|
|
|
if (params.error || params.error_description) {
|
|
err = new Error(params.error_description || "Unknown response error");
|
|
err.code = params.error || "E_UKNOWN_ERROR";
|
|
err.params = params;
|
|
return OAUTH3.PromiseA.reject(err);
|
|
}
|
|
|
|
return params;
|
|
});
|
|
}
|
|
|
|
, insertIframe: function (url, state, opts) {
|
|
opts = opts || {};
|
|
if (opts.debug) {
|
|
opts.timeout = opts.timeout || 15 * 60 * 1000;
|
|
}
|
|
var promise = new OAUTH3.PromiseA(function (resolve, reject) {
|
|
var tok;
|
|
var iframeDiv;
|
|
|
|
function cleanup() {
|
|
delete window['--oauth3-callback-' + state];
|
|
iframeDiv.remove();
|
|
clearTimeout(tok);
|
|
tok = null;
|
|
}
|
|
|
|
window['--oauth3-callback-' + state] = function (params) {
|
|
resolve(params);
|
|
cleanup();
|
|
};
|
|
|
|
tok = setTimeout(function () {
|
|
var err = new Error("the iframe request did not complete within 15 seconds");
|
|
err.code = "E_TIMEOUT";
|
|
reject(err);
|
|
cleanup();
|
|
}, opts.timeout || 15 * 1000);
|
|
|
|
// TODO hidden / non-hidden (via directive even)
|
|
var framesrc = '<iframe class="js-oauth3-iframe" src="' + url + '" ';
|
|
if (opts.debug) {
|
|
framesrc += ' width="800px" height="800px" style="opacity: 0.8;" frameborder="1"';
|
|
}
|
|
else {
|
|
framesrc += ' width="1px" height="1px" frameborder="0"';
|
|
}
|
|
framesrc += '></iframe>';
|
|
|
|
iframeDiv = window.document.createElement('div');
|
|
iframeDiv.innerHTML = framesrc;
|
|
window.document.body.appendChild(iframeDiv);
|
|
});
|
|
|
|
// TODO periodically garbage collect expired handlers from window object
|
|
return promise;
|
|
}
|
|
|
|
, openWindow: function (url, state, opts) {
|
|
if (opts.debug) {
|
|
opts.timeout = opts.timeout || 15 * 60 * 1000;
|
|
}
|
|
var promise = new OAUTH3.PromiseA(function (resolve, reject) {
|
|
var tok;
|
|
|
|
function cleanup() {
|
|
clearTimeout(tok);
|
|
tok = null;
|
|
delete window['--oauth3-callback-' + state];
|
|
// this is last in case the window self-closes synchronously
|
|
// (should never happen, but that's a negotiable implementation detail)
|
|
if (!opts.reuseWindow) {
|
|
promise.child.close();
|
|
}
|
|
}
|
|
|
|
window['--oauth3-callback-' + state] = function (params) {
|
|
console.log('YOLO!!');
|
|
resolve(params);
|
|
cleanup();
|
|
};
|
|
|
|
tok = setTimeout(function () {
|
|
var err = new Error("the windowed request did not complete within 3 minutes");
|
|
err.code = "E_TIMEOUT";
|
|
reject(err);
|
|
cleanup();
|
|
}, opts.timeout || 3 * 60 * 1000);
|
|
|
|
setTimeout(function () {
|
|
if (!promise.child) {
|
|
reject("TODO: open the iframe first and discover oauth3 directives before popup");
|
|
cleanup();
|
|
}
|
|
}, 0);
|
|
});
|
|
|
|
// TODO allow size changes (via directive even)
|
|
promise.child = window.open(
|
|
url
|
|
, 'oauth3-login-' + (opts.reuseWindow || state)
|
|
, 'height=' + (opts.height || 720) + ',width=' + (opts.width || 620)
|
|
);
|
|
// TODO periodically garbage collect expired handlers from window object
|
|
return promise;
|
|
}
|
|
|
|
//
|
|
// Logins
|
|
//
|
|
, authn: {
|
|
authorizationRedirect: function (providerUri, opts) {
|
|
// TODO get own directives
|
|
|
|
return OAUTH3.discover(providerUri, opts).then(function (directive) {
|
|
var prequest = OAUTH3_CORE.urls.authorizationRedirect(
|
|
directive
|
|
, opts
|
|
);
|
|
|
|
if (!prequest.state) {
|
|
throw new Error("[Devolper Error] [authorization redirect] prequest.state is empty");
|
|
}
|
|
|
|
return browser.frameRequest(prequest.url, prequest.state, opts);
|
|
}).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
|
|
);
|
|
});
|
|
}
|
|
, implicitGrant: function (providerUri, opts) {
|
|
// TODO let broker=true change behavior to open discover inline with frameRequest
|
|
// TODO OAuth3 provider should use the redirect URI as the appId?
|
|
return OAUTH3.discover(providerUri, opts).then(function (directive) {
|
|
var prequest = OAUTH3_CORE.urls.implicitGrant(
|
|
directive
|
|
// TODO OAuth3 provider should referrer / referer / origin as the appId?
|
|
, opts
|
|
);
|
|
|
|
if (!prequest.state) {
|
|
throw new Error("[Devolper Error] [implicit grant] prequest.state is empty");
|
|
}
|
|
|
|
return browser.frameRequest(prequest.url, prequest.state, opts);
|
|
}).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
|
|
);
|
|
});
|
|
}
|
|
, logout: function (providerUri, opts) {
|
|
opts = opts || {};
|
|
|
|
return OAUTH3.discover(providerUri, opts).then(function (directive) {
|
|
var prequest = OAUTH3_CORE.urls.logout(
|
|
directive
|
|
, opts
|
|
);
|
|
// Oauth3.init({ logout: function () {} });
|
|
//return Oauth3.logout();
|
|
|
|
var redirectUri = opts.redirect_uri || opts.redirectUri
|
|
|| (window.location.protocol + '//' + (window.location.host + window.location.pathname) + 'oauth3.html')
|
|
;
|
|
var params = {
|
|
// logout=true for all logins/accounts
|
|
// logout=app-scoped-login-id for a single login
|
|
action: 'logout'
|
|
// TODO specify specific accounts / logins to delete from session
|
|
, accounts: true
|
|
, logins: true
|
|
, redirect_uri: redirectUri
|
|
, state: prequest.state
|
|
, debug: opts.debug
|
|
};
|
|
|
|
if (prequest.url === params.redirect_uri) {
|
|
return OAUTH3.PromiseA.resolve();
|
|
}
|
|
|
|
prequest.url += '#' + OAUTH3_CORE.querystringify(params);
|
|
|
|
return OAUTH3.insertIframe(prequest.url, prequest.state, opts);
|
|
});
|
|
}
|
|
}
|
|
, isIframe: function isIframe () {
|
|
try {
|
|
return window.self !== window.top;
|
|
} catch (e) {
|
|
return true;
|
|
}
|
|
}
|
|
, parseUrl: function (url) {
|
|
var parser = document.createElement('a');
|
|
parser.href = url;
|
|
return parser;
|
|
}
|
|
, isRedirectHostSafe: function (referrerUrl, redirectUrl) {
|
|
var src = browser.parseUrl(referrerUrl);
|
|
var dst = browser.parseUrl(redirectUrl);
|
|
|
|
// TODO how should we handle subdomains?
|
|
// It should be safe for api.example.com to redirect to example.com
|
|
// But it may not be safe for to example.com to redirect to aj.example.com
|
|
// It is also probably not safe for sally.example.com to redirect to john.example.com
|
|
// The client should have a list of allowed URLs to choose from and perhaps a wildcard will do
|
|
//
|
|
// api.example.com.evil.com SHOULD NOT match example.com
|
|
return dst.hostname === src.hostname;
|
|
}
|
|
, checkRedirect: function (client, query) {
|
|
console.warn("[security] URL path checking not yet implemented");
|
|
|
|
var clientUrl = OAUTH3.core.normalizeUrl(client.url);
|
|
var redirectUrl = OAUTH3.core.normalizeUrl(query.redirect_uri);
|
|
|
|
// General rule:
|
|
// I can callback to a shorter domain (fewer subs) or a shorter path (on the same domain)
|
|
// but not a longer (more subs) or different domain or a longer path (on the same domain)
|
|
|
|
|
|
// We can callback to an explicitly listed domain (TODO and path)
|
|
if (browser.isRedirectHostSafe(clientUrl, redirectUrl)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
/*
|
|
, redirect: function (redirect) {
|
|
if (parser.search) {
|
|
parser.search += '&';
|
|
} else {
|
|
parser.search += '?';
|
|
}
|
|
|
|
parser.search += 'error=E_NO_SESSION';
|
|
redirectUri = parser.href;
|
|
|
|
window.location.href = redirectUri;
|
|
}
|
|
*/
|
|
|
|
, hackFormSubmit: function (opts) {
|
|
opts = opts || {};
|
|
scope.authorizationDecisionUri = DaplieApiConfig.providerUri + '/api/org.oauth3.provider/authorization_decision';
|
|
scope.updateScope();
|
|
|
|
var redirectUri = scope.appQuery.redirect_uri.replace(/^https?:\/\//i, 'https://');
|
|
var separator;
|
|
|
|
// TODO check that we appropriately use '#' for implicit and '?' for code
|
|
// (server-side) in an OAuth2 backwards-compatible way
|
|
if ('token' === scope.appQuery.response_type) {
|
|
separator = '#';
|
|
}
|
|
else if ('code' === scope.appQuery.response_type) {
|
|
separator = '?';
|
|
}
|
|
else {
|
|
separator = '#';
|
|
}
|
|
|
|
if (scope.pendingScope.length && !opts.allow) {
|
|
redirectUri += separator + Oauth3.querystringify({
|
|
error: 'access_denied'
|
|
, error_description: 'None of the permissions were accepted'
|
|
, error_uri: 'https://oauth3.org/docs/errors#access_denied'
|
|
, state: scope.appQuery.state
|
|
});
|
|
window.location.href = redirectUri;
|
|
return;
|
|
}
|
|
|
|
// TODO move to Oauth3? or not?
|
|
// this could be implementation-specific,
|
|
// but it may still be nice to provide it as de-facto
|
|
var url = DaplieApiConfig.apiBaseUri + '/api/org.oauth3.provider/grants/:client_id/:account_id'
|
|
.replace(/:client_id/g, scope.appQuery.client_id || scope.appQuery.client_uri)
|
|
.replace(/:account_id/g, scope.selectedAccountId)
|
|
;
|
|
|
|
var account = scope.sessionAccount;
|
|
var session = { accessToken: account.token, refreshToken: account.refreshToken };
|
|
var preq = {
|
|
url: url
|
|
, method: 'POST'
|
|
, data: {
|
|
scope: updateAccepted()
|
|
, response_type: scope.appQuery.response_type
|
|
, referrer: document.referrer || document.referer || ''
|
|
, referer: document.referrer || document.referer || ''
|
|
, tenant_id: scope.appQuery.tenant_id
|
|
, client_id: scope.appQuery.client_id
|
|
, client_uri: scope.appQuery.client_uri
|
|
}
|
|
, session: session
|
|
};
|
|
preq.clientId = preq.appId = DaplieApiConfig.appId || DaplieApiConfig.clientId;
|
|
preq.clientUri = preq.appUri = DaplieApiConfig.appUri || DaplieApiConfig.clientUri;
|
|
// TODO need a way to have middleware in Oauth3.request for TherapySession et al
|
|
|
|
return Oauth3.request(preq).then(function (resp) {
|
|
var err;
|
|
var data = resp.data || {};
|
|
|
|
if (data.error) {
|
|
err = new Error(data.error.message || data.errorDescription);
|
|
err.message = data.error.message || data.errorDescription;
|
|
err.code = resp.data.error.code || resp.data.error;
|
|
err.uri = 'https://oauth3.org/docs/errors#' + (resp.data.error.code || resp.data.error);
|
|
return $q.reject(err);
|
|
}
|
|
|
|
if (!(data.code || data.accessToken)) {
|
|
err = new Error("No grant code");
|
|
return $q.reject(err);
|
|
}
|
|
|
|
return data;
|
|
}).then(function (data) {
|
|
redirectUri += separator + Oauth3.querystringify({
|
|
state: scope.appQuery.state
|
|
|
|
, code: data.code
|
|
|
|
, access_token: data.access_token
|
|
, expires_at: data.expires_at
|
|
, expires_in: data.expires_in
|
|
, scope: data.scope
|
|
|
|
, refresh_token: data.refresh_token
|
|
, refresh_expires_at: data.refresh_expires_at
|
|
, refresh_expires_in: data.refresh_expires_in
|
|
});
|
|
|
|
if ('token' === scope.appQuery.response_type) {
|
|
window.location.href = redirectUri;
|
|
return;
|
|
}
|
|
else if ('code' === scope.appQuery.response_type) {
|
|
scope.hackFormSubmitHelper(redirectUri);
|
|
return;
|
|
}
|
|
else {
|
|
console.warn("Grant Code NOT IMPLEMENTED for '" + scope.appQuery.response_type + "'");
|
|
console.warn(redirectUri);
|
|
throw new Error("Grant Code NOT IMPLEMENTED for '" + scope.appQuery.response_type + "'");
|
|
}
|
|
}, function (err) {
|
|
redirectUri += separator + Oauth3.querystringify({
|
|
error: err.code || 'server_error'
|
|
, error_description: err.message || "Server Error: It's not your fault"
|
|
, error_uri: err.uri || 'https://oauth3.org/docs/errors#server_error'
|
|
, state: scope.appQuery.state
|
|
});
|
|
|
|
console.error('Grant Code Error NOT IMPLEMENTED');
|
|
console.error(err);
|
|
console.error(redirectUri);
|
|
//window.location.href = redirectUri;
|
|
});
|
|
}
|
|
|
|
, hackFormSubmitHelper: function (uri) {
|
|
// TODO de-jQuerify
|
|
//window.location.href = redirectUri;
|
|
//return;
|
|
|
|
// the only way to do a POST that redirects the current window
|
|
window.jQuery('form.js-hack-hidden-form').attr('action', uri);
|
|
|
|
// give time for the apply to take place
|
|
window.setTimeout(function () {
|
|
window.jQuery('form.js-hack-hidden-form').submit();
|
|
}, 50);
|
|
}
|
|
};
|
|
browser.requests = browser.authn;
|
|
|
|
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];
|
|
});
|
|
|
|
}('undefined' !== typeof exports ? exports : window));
|