oauth3.js/oauth3.core.js

1195 lines
42 KiB
JavaScript
Raw Normal View History

/* global Promise */
;(function (exports) {
'use strict';
if ('undefined' !== typeof window && 'https:' !== window.location.protocol) {
window.alert("You must use https. We suggest using caddy as your webserver (or serve-https if testing locally)");
}
var OAUTH3 = exports.OAUTH3 = {
clientUri: function (location) {
2017-03-21 05:29:03 +00:00
return OAUTH3.uri.normalize(location.host + (location.pathname || ''));
}
, error: {
parse: function (providerUri, params) {
var err = new Error(params.error_description || params.error.message || "Unknown error with provider '" + providerUri + "'");
err.uri = params.error_uri || params.error.uri;
err.code = params.error.code || params.error;
return err;
}
}
, _binStr: {
bufferToBinStr: function (buf) {
return Array.prototype.map.call(new Uint8Array(buf), function(ch) {
return String.fromCharCode(ch);
}).join('');
}
, binStrToBuffer: function (str) {
var buf;
if ('undefined' !== typeof Uint8Array) {
buf = new Uint8Array(str.length);
} else {
buf = [];
}
Array.prototype.forEach.call(str, function (ch, ind) {
buf[ind] = ch.charCodeAt(0);
});
return buf;
}
}
, _base64: {
atob: function (base64) {
// atob must be called from the global context
// http://stackoverflow.com/questions/9677985/uncaught-typeerror-illegal-invocation-in-chrome
return (exports.atob || require('atob'))(base64);
}
2017-02-21 23:54:37 +00:00
, btoa: function (b64) {
// for directive passing in .well-known/oauth3
// http://stackoverflow.com/questions/9677985/uncaught-typeerror-illegal-invocation-in-chrome
return (exports.btoa || require('btoa'))(b64);
}
, decodeUrlSafe: function (b64) {
// URL-safe Base64 to Base64
// https://en.wikipedia.org/wiki/Base64
// https://gist.github.com/catwell/3046205
var mod = b64.length % 4;
if (2 === mod) { b64 += '=='; }
if (3 === mod) { b64 += '='; }
b64 = b64.replace(/-/g, '+').replace(/_/g, '/');
return OAUTH3._base64.atob(b64);
}
2017-02-21 23:54:37 +00:00
, encodeUrlSafe: function (b64) {
// for directive passing in .well-known/oauth3
// Base64 to URL-safe Base64
b64 = OAUTH3._base64.btoa(b64);
2017-02-21 23:54:37 +00:00
b64 = b64.replace(/\+/g, '-').replace(/\//g, '_');
b64 = b64.replace(/=+/g, '');
return b64;
2017-02-21 23:54:37 +00:00
}
, urlSafeToBuffer: function (str) {
return OAUTH3._binStr.binStrToBuffer(OAUTH3._base64.decodeUrlSafe(str));
}
, bufferToUrlSafe: function (buf) {
return OAUTH3._base64.encodeUrlSafe(OAUTH3._binStr.bufferToBinStr(buf));
}
}
, uri: {
normalize: function (uri) {
if ('string' !== typeof uri) {
console.error((new Error('stack')).stack);
}
// tested with
// example.com
// example.com/
// http://example.com
// https://example.com/
return uri
.replace(/^(https?:\/\/)?/i, '')
.replace(/\/?$/, '')
;
}
}
, url: {
normalize: function (url) {
if ('string' !== typeof url) {
console.error((new Error('stack')).stack);
}
// tested with
// example.com
// example.com/
// http://example.com
// https://example.com/
return url
.replace(/^(https?:\/\/)?/i, 'https://')
.replace(/\/?$/, '')
;
}
, resolve: function (base, next) {
if (/^https:\/\//i.test(next)) {
return next;
}
return this.normalize(base) + '/' + this._normalizePath(next);
}
, _normalizePath: function (path) {
return path.replace(/^\//, '').replace(/\/$/, '');
}
}
, query: {
2017-02-21 23:54:37 +00:00
parse: function (search) {
// needed for .well-known/oauth3
// parse a query or a hash
if (-1 !== ['#', '?'].indexOf(search[0])) {
search = search.substring(1);
}
// Solve for case of search within hash
// example: #/authorization_dialog/?state=...&redirect_uri=...
var queryIndex = search.indexOf('?');
if (-1 !== queryIndex) {
search = search.substr(queryIndex + 1);
}
var args = search.split('&');
var argsParsed = {};
var i, arg, kvp, key, value;
for (i = 0; i < args.length; i += 1) {
arg = args[i];
if (-1 === arg.indexOf('=')) {
argsParsed[decodeURIComponent(arg).trim()] = true;
}
else {
kvp = arg.split('=');
key = decodeURIComponent(kvp[0]).trim();
value = decodeURIComponent(kvp[1]).trim();
argsParsed[key] = value;
}
}
return argsParsed;
}
, stringify: function (params) {
var qs = [];
Object.keys(params).forEach(function (key) {
// TODO nullify instead?
if ('undefined' === typeof params[key]) {
return;
}
if ('scope' === key) {
params[key] = OAUTH3.scope.stringify(params[key]);
}
qs.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key]));
});
return qs.join('&');
}
}
, scope: {
stringify: function (scope) {
if (Array.isArray(scope)) {
scope = scope.join(' ');
}
return scope;
}
}
, randomState: function () {
// TODO put in different file for browser vs node
try {
return Array.prototype.slice.call(
2017-03-20 23:55:00 +00:00
OAUTH3._browser.window.crypto.getRandomValues(new Uint8Array(16))
).map(function (ch) { return (ch).toString(16); }).join('');
} catch(e) {
return OAUTH3.utils._insecureRandomState();
}
}
, _insecureRandomState: function () {
var i;
var ch;
var str;
// TODO use fisher-yates on 0..255 and select [0] 16 times
// [security] https://medium.com/@betable/tifu-by-using-math-random-f1c308c4fd9d#.5qx0bf95a
// https://github.com/v8/v8/blob/b0e4dce6091a8777bda80d962df76525dc6c5ea9/src/js/math.js#L135-L144
// Note: newer versions of v8 do not have this bug, but other engines may still
console.warn('[security] crypto.getRandomValues() failed, falling back to Math.random()');
str = '';
for (i = 0; i < 32; i += 1) {
ch = Math.round(Math.random() * 255).toString(16);
if (ch.length < 2) { ch = '0' + ch; }
str += ch;
}
return str;
}
, jwt: {
// decode only (no verification)
decode: function (str) {
// 'abc.qrs.xyz'
// [ 'abc', 'qrs', 'xyz' ]
// [ {}, {}, 'foo' ]
// { header: {}, payload: {}, signature: '' }
var parts = str.split(/\./g);
var jsons = parts.slice(0, 2).map(function (urlsafe64) {
return JSON.parse(OAUTH3._base64.decodeUrlSafe(urlsafe64));
});
return { header: jsons[0], payload: jsons[1] };
}
, verify: function (jwk, token) {
var parts = token.split(/\./g);
var data = OAUTH3._binStr.binStrToBuffer(parts.slice(0, 2).join('.'));
var signature = OAUTH3._base64.urlSafeToBuffer(parts[2]);
return OAUTH3.crypto.core.verify(jwk, data, signature);
}
, freshness: function (tokenMeta, staletime, _now) {
staletime = staletime || (15 * 60);
var now = _now || Date.now();
var fresh = ((parseInt(tokenMeta.exp, 10) || 0) - Math.round(now / 1000));
if (fresh >= staletime) {
return 'fresh';
}
if (fresh <= 0) {
return 'expired';
}
return 'stale';
}
}
, urls: {
discover: function (providerUri, opts) {
if (!providerUri) {
throw new Error("cannot discover without providerUri");
}
if (!opts.client_id) {
throw new Error("cannot discover without options.client_id");
}
var clientId = OAUTH3.url.normalize(opts.client_id || opts.client_uri);
providerUri = OAUTH3.url.normalize(providerUri);
var params = {
action: 'directives'
, state: opts.state || OAUTH3.utils.randomState()
, redirect_uri: clientId + (opts.client_callback_path || '/.well-known/oauth3/callback.html#/')
, response_type: 'rpc'
, _method: 'GET'
, _pathname: '.well-known/oauth3/directives.json'
, debug: opts.debug || undefined
};
var result = {
url: providerUri + '/.well-known/oauth3/#/?' + OAUTH3.query.stringify(params)
, state: params.state
, method: 'GET'
, query: params
};
return result;
}
, implicitGrant: function (directive, opts) {
//
// Example Implicit Grant Request
// (for generating a browser-only session, not a session on your server)
//
// GET https://example.com/api/org.oauth3.provider/authorization_dialog
// ?response_type=token
// &scope=`encodeURIComponent('profile.login profile.email')`
// &state=`cryptoutil.random().toString('hex')`
// &client_id=xxxxxxxxxxx
// &redirect_uri=`encodeURIComponent('https://myapp.com/oauth3.html')`
//
// NOTE: `redirect_uri` itself may also contain URI-encoded components
//
opts = opts || {};
var type = 'authorization_dialog';
var responseType = 'token';
var scope = opts.scope || directive.authn_scope;
var args = directive[type];
var uri = args.url;
var state = opts.state || OAUTH3.utils.randomState();
var params = {
debug: opts.debug || undefined
, client_uri: opts.client_uri || opts.clientUri || undefined
, client_id: opts.client_id || opts.client_uri || undefined
, state: state
};
var result;
params.response_type = responseType;
if (scope) {
params.scope = OAUTH3.scope.stringify(scope);
}
if (!opts.redirect_uri) {
// TODO consider making this optional
//console.warn("auto-generating redirect_uri from hard-coded callback.html"
// + " (should be configurable... but then redirect_uri could just be manually-generated)");
opts.redirect_uri = OAUTH3.url.resolve(
OAUTH3.url.normalize(params.client_uri)
, '.well-known/oauth3/callback.html'
);
}
params.redirect_uri = opts.redirect_uri;
uri += '?' + OAUTH3.query.stringify(params);
result = {
url: uri
, state: state
, method: args.method
, query: params
};
return result;
}
, refreshToken: function (directive, opts) {
// grant_type=refresh_token
// Example Refresh Token Request
// (generally for 1st or 3rd party server-side, mobile, and desktop apps)
//
// POST https://example.com/api/oauth3/access_token
// { "grant_type": "refresh_token", "client_id": "<<id>>", "scope": "<<scope>>"
// , "username": "<<username>>", "password": "password" }
//
opts = opts || {};
var type = 'access_token';
var grantType = 'refresh_token';
var scope = opts.scope || directive.authn_scope;
var clientSecret = opts.client_secret;
var args = directive[type];
var params = {
"grant_type": grantType
, "refresh_token": opts.refresh_token || (opts.session && opts.session.refresh_token)
, "response_type": 'token'
, "client_id": opts.client_id || opts.client_uri
, "client_uri": opts.client_uri
//, "scope": undefined
//, "client_secret": undefined
, debug: opts.debug || undefined
};
var uri = args.url;
var body;
if (clientSecret) {
// TODO not allowed in the browser
console.warn("if this is a browser, you must not use client_secret");
params.client_secret = clientSecret;
}
if (scope) {
params.scope = OAUTH3.scope.stringify(scope);
}
if ('GET' === args.method.toUpperCase()) {
uri += '?' + OAUTH3.query.stringify(params);
} else {
body = params;
}
return {
url: uri
, method: args.method
, data: body
};
}
, logout: function (directive, opts) {
// action=logout
// Example Logout Request
// (generally for 1st or 3rd party server-side, mobile, and desktop apps)
//
// GET https://example.com/#/logout/
// ?client_id=<<id>>
// &access_token=<<token>>
// &sub=<<ppid>>
//
// Note that the use of # keeps certain parameters from traveling across
// the network at all (and we use https anyway)
//
opts = opts || {};
var action = 'logout';
var args = directive[action];
var state = opts.state || OAUTH3.utils.randomState();
var params = {
action: action
//, response_type: 'confirmation'
, client_id: opts.client_id || opts.client_uri
, client_uri: opts.client_uri || opts.client_id
, state: state
, redirect_uri: opts.redirect_uri = OAUTH3.url.resolve(
OAUTH3.url.normalize(opts.client_uri || opts.client_id)
, '.well-known/oauth3/callback.html'
)
, debug: opts.debug
};
var uri = args.url;
var body;
if ('GET' === args.method.toUpperCase()) {
uri += '?' + OAUTH3.query.stringify(params);
} else {
body = params;
}
return {
url: OAUTH3.url.resolve(directive.issuer, uri)
, method: args.method
, state: state
, data: body
};
}
}
, hooks: {
directives: {
2017-02-24 19:18:45 +00:00
get: function (providerUri) {
providerUri = OAUTH3.uri.normalize(providerUri);
2017-03-02 20:08:14 +00:00
return OAUTH3.PromiseA.resolve(OAUTH3.hooks.directives._getCached(providerUri)
2017-02-24 19:18:45 +00:00
|| OAUTH3.hooks.directives._get(providerUri))
.then(function (directives) {
// or do .then(this._set) to keep DRY?
OAUTH3.hooks.directives._cache[providerUri] = directives;
return directives;
});
}
, _getCached: function (providerUri) {
providerUri = OAUTH3.uri.normalize(providerUri);
2017-03-02 20:08:14 +00:00
if (!OAUTH3.hooks.directives._cache) { OAUTH3.hooks.directives._cache = {}; }
return OAUTH3.hooks.directives._cache[providerUri];
}
2017-02-24 19:18:45 +00:00
, set: function (providerUri, directives) {
providerUri = OAUTH3.uri.normalize(providerUri);
if (!OAUTH3.hooks.directives._cache) { OAUTH3.hooks.directives._cache = {}; }
OAUTH3.hooks.directives._cache[providerUri] = directives;
2017-02-24 19:18:45 +00:00
return OAUTH3.PromiseA.resolve(OAUTH3.hooks.directives._set(providerUri, directives));
}
2017-02-24 19:18:45 +00:00
, _get: function (providerUri) {
2017-03-21 23:33:03 +00:00
if (!OAUTH3._hooks || !OAUTH3._hooks.directives || !OAUTH3._hooks.directives.get) {
console.warn('[Warn] Please implement OAUTH3._hooks.directives.get = function (providerUri) { return PromiseA<directives>; }');
return JSON.parse(window.localStorage.getItem('directives-' + providerUri) || '{}');
}
return OAUTH3._hooks.directives.get(providerUri);
2017-02-24 19:18:45 +00:00
}
, _set: function (providerUri, directives) {
2017-03-21 23:33:03 +00:00
if (!OAUTH3._hooks || !OAUTH3._hooks.directives || !OAUTH3._hooks.directives.set) {
console.warn('[Warn] Please implement OAUTH3._hooks.directives.set = function (providerUri, directives) { return PromiseA<directives>; }');
window.localStorage.setItem('directives-' + providerUri, JSON.stringify(directives));
return directives;
}
return OAUTH3._hooks.directives.set(providerUri, directives);
}
}
, session: {
refresh: 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];
});
// 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 = OAUTH3.jwt.decode(oldSession.access_token).payload;
oldSession.token.sub = oldSession.token.sub || oldSession.token.acx.id;
oldSession.token.client_uri = clientUri;
oldSession.token.provider_uri = providerUri;
if (oldSession.refresh_token) {
oldSession.refresh = OAUTH3.jwt.decode(oldSession.refresh_token).payload;
oldSession.refresh.sub = oldSession.refresh.sub || oldSession.refresh.acx.id;
oldSession.refresh.provider_uri = providerUri;
}
// set for a set of audiences
return OAUTH3.PromiseA.resolve(OAUTH3.hooks.session.set(providerUri, oldSession));
}
, check: function (preq, opts) {
2017-02-28 01:19:01 +00:00
opts = opts || {};
if (!preq.session) {
return OAUTH3.PromiseA.resolve(null);
}
var freshness = OAUTH3.jwt.freshness(preq.session.token, opts.staletime);
switch (freshness) {
case 'stale':
return OAUTH3.hooks.session.stale(preq.session);
case 'expired':
return OAUTH3.hooks.session.expired(preq.session).then(function (newSession) {
preq.session = newSession;
return newSession;
});
//case 'fresh':
default:
return OAUTH3.PromiseA.resolve(preq.session);
}
}
, stale: function (staleSession) {
if (OAUTH3.hooks.session._stalePromise) {
return OAUTH3.PromiseA.resolve(staleSession);
}
OAUTH3.hooks.session._stalePromise = OAUTH3._refreshToken(
staleSession.provider_uri
, { client_uri: staleSession.client_uri
, session: staleSession
, debug: staleSession.debug
}
).then(function (newSession) {
OAUTH3.hooks.session._stalePromise = null;
return newSession; // oauth3.hooks.refreshSession(staleSession, newSession);
}, function () {
OAUTH3.hooks.session._stalePromise = null;
});
return OAUTH3.PromiseA.resolve(staleSession);
}
, expired: function (expiredSession) {
return OAUTH3._refreshToken(
expiredSession.provider_uri
, { client_uri: expiredSession.client_uri
, session: expiredSession
, debug: expiredSession.debug
}
).then(function (newSession) {
return newSession; // oauth3.hooks.refreshSession(expiredSession, newSession);
});
}
2017-03-21 23:33:03 +00:00
, _getCached: function (providerUri, id) {
2017-02-24 19:18:45 +00:00
providerUri = OAUTH3.uri.normalize(providerUri);
2017-03-02 20:08:14 +00:00
if (!OAUTH3.hooks.session._cache) { OAUTH3.hooks.session._cache = {}; }
2017-03-21 23:33:03 +00:00
if (id) {
return OAUTH3.hooks.session._cache[providerUri + id];
}
return OAUTH3.hooks.session._cache[providerUri];
2017-02-24 19:18:45 +00:00
}
2017-03-21 23:33:03 +00:00
, set: function (providerUri, newSession, id) {
if (!providerUri) {
console.error(new Error('no providerUri').stack);
throw new Error("providerUri is not set");
}
providerUri = OAUTH3.uri.normalize(providerUri);
if (!OAUTH3.hooks.session._cache) { OAUTH3.hooks.session._cache = {}; }
2017-03-21 23:33:03 +00:00
OAUTH3.hooks.session._cache[providerUri + (id || newSession.id || newSession.token.id || '')] = newSession;
if (!id) {
OAUTH3.hooks.session._cache[providerUri] = newSession;
}
2017-02-24 19:18:45 +00:00
return OAUTH3.PromiseA.resolve(OAUTH3.hooks.session._set(providerUri, newSession));
}
2017-03-21 23:33:03 +00:00
, get: function (providerUri, id) {
providerUri = OAUTH3.uri.normalize(providerUri);
if (!providerUri) {
throw new Error("providerUri is not set");
}
2017-02-24 19:18:45 +00:00
2017-03-21 23:33:03 +00:00
return OAUTH3.PromiseA.resolve(
OAUTH3.hooks.session._getCached(providerUri, id) || OAUTH3.hooks.session._get(providerUri, id)
).then(function (session) {
var s = session || { token: {} };
OAUTH3.hooks.session._cache[providerUri + (id || s.id || s.token.id || '')] = session;
2017-03-21 23:33:03 +00:00
if (!id) {
OAUTH3.hooks.session._cache[providerUri] = session;
}
return session;
});
2017-02-24 19:18:45 +00:00
}
2017-03-21 23:33:03 +00:00
, _get: function (providerUri, id) {
if (!OAUTH3._hooks || !OAUTH3._hooks.sessions || !OAUTH3._hooks.sessions.all) {
console.warn('[Warn] Please implement OAUTH3._hooks.sessions.all = function ([providerUri]) { return PromiseA<sessions>; }');
}
if (!OAUTH3._hooks || !OAUTH3._hooks.sessions || !OAUTH3._hooks.sessions.get) {
console.warn('[Warn] Please implement OAUTH3._hooks.sessions.get = function (providerUri[, id]) { return PromiseA<session>; }');
return JSON.parse(window.sessionStorage.getItem('session-' + providerUri + (id || '')) || 'null');
}
return OAUTH3._hooks.directives.get(providerUri, id);
2017-02-24 19:18:45 +00:00
}
2017-03-21 23:33:03 +00:00
, _set: function (providerUri, newSession, id) {
if (!OAUTH3._hooks || !OAUTH3._hooks.sessions || !OAUTH3._hooks.sessions.set) {
console.warn('[Warn] Please implement OAUTH3._hooks.sessions.set = function (providerUri, newSession[, id]) { return PromiseA<newSession>; }');
window.sessionStorage.setItem('session-' + providerUri, JSON.stringify(newSession));
window.sessionStorage.setItem('session-' + providerUri + (id || newSession.id || newSession.token.id || ''), JSON.stringify(newSession));
return newSession;
}
return OAUTH3._hooks.directives.set(providerUri, newSession, id);
}
}
}
, discover: function (providerUri, opts) {
if (!providerUri) {
throw new Error('oauth3.discover(providerUri, opts) received providerUri as ' + providerUri);
}
2017-02-24 19:18:45 +00:00
return OAUTH3.hooks.directives.get(providerUri).then(function (directives) {
if (directives && directives.issuer) {
return directives;
}
2017-02-24 19:18:45 +00:00
return OAUTH3._discoverHelper(providerUri, opts).then(function (directives) {
directives.azp = directives.azp || OAUTH3.url.normalize(providerUri);
directives.issuer = directives.issuer || OAUTH3.url.normalize(providerUri);
// OAUTH3.PromiseA.resolve() is taken care of because this is wrapped
2017-02-24 19:18:45 +00:00
return OAUTH3.hooks.directives.set(providerUri, directives);
});
});
}
, _discoverHelper: function(providerUri, opts) {
return OAUTH3._browser.discover(providerUri, opts);
}
, request: function (preq, opts) {
function fetch() {
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 || preq.session.accessToken);
}
return OAUTH3._requestHelper(preq, opts);
}
if (!preq.session) {
return fetch();
}
return OAUTH3.hooks.session.check(preq, opts).then(fetch);
}
, _requestHelper: function (preq, opts) {
2017-03-16 21:41:33 +00:00
/*
if (opts && opts.directives) {
preq.url = OAUTH3.url.resolve(opts.directives.issuer, preq.url);
}
*/
return OAUTH3._browser.request(preq, opts);
}
, implicitGrant: function(directives, opts) {
var promise;
var providerUri = directives.azp || directives.issuer || directives;
if (opts.broker) {
// Discovery can happen in-flow because we know that this is
// a valid oauth3 provider
promise = OAUTH3._discoverThenImplicitGrant(providerUri, opts);
}
else {
// Discovery must take place before calling implicitGrant
promise = OAUTH3._implicitGrant(OAUTH3.hooks.directives._getCached(providerUri), opts);
}
return promise.then(function (tokens) {
// TODO abstract browser bits away
try {
OAUTH3._browser.closeFrame(tokens.state || opts._state, opts);
} catch(e) {
console.warn("[implicitGrant] TODO abstract browser bits away");
}
opts._state = undefined;
return OAUTH3.hooks.session.refresh(
opts.session || {
provider_uri: providerUri
, client_id: opts.client_id
, client_uri: opts.client_uri || opts.clientUri
}
, tokens
);
});
}
, _discoverThenImplicitGrant: function(providerUri, opts) {
opts.windowType = opts.windowType || 'popup';
return OAUTH3.discover(providerUri, opts).then(function (directives) {
return OAUTH3._implicitGrant(directives, opts).then(function (tokens) {
return tokens;
});
});
}
, _implicitGrant: function(directives, opts) {
// TODO this may need to be synchronous for browser security policy
// Do some stuff
var authReq = OAUTH3.urls.implicitGrant(
directives
, { redirect_uri: opts.redirect_uri
, client_id: opts.client_id || opts.client_uri
, client_uri: opts.client_uri || opts.client_id
2017-05-08 23:39:56 +00:00
, scope: opts.scope
, state: opts._state || undefined
, debug: opts.debug
}
);
if (opts.debug) {
window.alert("DEBUG MODE: Pausing so you can look at logs and whatnot :) Fire at will!");
}
return OAUTH3._browser.frameRequest(
OAUTH3.url.resolve(directives.issuer, authReq.url)
, authReq.state // state should recycle params
, { windowType: opts.windowType
, reuseWindow: opts.broker && '-broker'
, debug: opts.debug
}
).then(function (tokens) {
if (tokens.error) {
// TODO directives.audience
return OAUTH3.PromiseA.reject(OAUTH3.error.parse(directives.issuer /*providerUri*/, tokens));
}
return tokens;
});
}
, _refreshToken: function (providerUri, opts) {
return OAUTH3.discover(providerUri, opts).then(function (directive) {
var prequest = OAUTH3.urls.refreshToken(directive, opts);
2017-03-16 21:41:33 +00:00
prequest.url = OAUTH3.url.resolve(providerUri/*directives.issuer*/, prequest.url);
return OAUTH3.request(prequest/*, { directives: directive }*/).then(function (req) {
var data = req.data;
data.provider_uri = providerUri;
if (data.error) {
return OAUTH3.PromiseA.reject(OAUTH3.error.parse(providerUri, data));
}
2017-02-24 20:05:07 +00:00
return OAUTH3.hooks.session.refresh(
opts.session || { provider_uri: providerUri, client_uri: opts.client_uri || opts.clientUri }
, data
);
});
});
}
, logout: function(providerUri, opts) {
return OAUTH3._logoutHelper(OAUTH3.hooks.directives._getCached(providerUri), opts);
}
, _logoutHelper: function(directives, opts) {
var logoutReq = OAUTH3.urls.logout(
directives
, { client_id: (opts.client_id || opts.client_uri || OAUTH3.clientUri(OAUTH3._browser.window.location))
, windowType: 'popup' // we'll figure out background later
, broker: opts.broker
//, state: opts._state
, debug: opts.debug
}
);
return OAUTH3._browser.frameRequest(
OAUTH3.url.resolve(directives.issuer, logoutReq.url)
, logoutReq.state // state should recycle params
, { windowType: 'popup'
, reuseWindow: opts.broker && '-broker'
, debug: opts.debug
}
).then(function (params) {
OAUTH3._browser.closeFrame(params.state || opts._state, opts);
if (params.error) {
// TODO directives.audience
return OAUTH3.PromiseA.reject(OAUTH3.error.parse(directives.issuer /*providerUri*/, params));
}
OAUTH3.hooks.session._cache = {};
return params;
});
}
//
// Let the Code Waste begin!!
//
, _browser: {
2017-03-20 23:55:00 +00:00
window: 'undefined' !== typeof window ? window : null
// TODO we don't need to include this if we're using jQuery or angular
, discover: function(providerUri, opts) {
opts = opts || {};
providerUri = OAUTH3.url.normalize(providerUri);
if (providerUri.match(OAUTH3._browser.window.location.hostname)) {
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.url.normalize(providerUri) + '/.well-known/oauth3/directives.json'
}).then(function (resp) {
return resp.data;
});
}
if (!(opts.client_id || opts.client_uri).match(OAUTH3._browser.window.location.hostname)) {
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, OAUTH3._browser.window.location.hostname);
}
var discReq = OAUTH3.urls.discover(
providerUri
, { client_id: (opts.client_id || opts.client_uri || OAUTH3.clientUri(OAUTH3._browser.window.location))
, windowType: opts.broker && opts.windowType || 'background'
, broker: opts.broker
, state: opts._state || undefined
, debug: opts.debug
}
);
opts._state = discReq.state;
//var discReq = OAUTH3.urls.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
// TODO allow node to open a desktop browser window
opts._windowType = opts.windowType;
opts.windowType = opts.windowType || 'background';
return OAUTH3._browser.frameRequest(
OAUTH3.url.resolve(providerUri, discReq.url)
, discReq.state
// why not just pass opts whole?
, { windowType: opts.windowType
, reuseWindow: opts.broker && '-broker'
, debug: opts.debug
}
).then(function (params) {
opts.windowType = opts._windowType;
// caller will call OAUTH3._browser.closeFrame(discReq.state, { debug: opts.debug || params.debug });
if (params.error) {
// TODO directives.issuer || directives.audience
return OAUTH3.PromiseA.reject(OAUTH3.error.parse(providerUri, params));
}
// TODO params should have response_type indicating json, binary, etc
var directives = JSON.parse(OAUTH3._base64.decodeUrlSafe(params.result || params.directives));
2017-02-24 19:18:45 +00:00
// caller will call OAUTH3.hooks.directives.set(providerUri, directives);
return directives;
});
}
, request: function (preq, _sys) {
return new OAUTH3.PromiseA(function (resolve, reject) {
var xhr;
try {
xhr = new XMLHttpRequest(_sys);
} catch(e) {
xhr = new XMLHttpRequest();
}
xhr.onreadystatechange = function () {
var data;
if (xhr.readyState !== XMLHttpRequest.DONE) {
// nothing to do here
return;
}
if (xhr.status !== 200) {
reject(new Error('bad status code: ' + xhr.status));
return;
}
try {
data = JSON.parse(xhr.responseText);
} catch(e) {
data = xhr.responseText;
}
resolve({
2017-03-21 07:02:41 +00:00
_request: xhr
, headers: null // TODO
, data: data
, status: xhr.status
});
};
2017-02-21 23:54:37 +00:00
xhr.open(preq.method || 'GET', preq.url, true);
var headers = preq.headers || {};
2017-02-22 01:38:45 +00:00
if (preq.data) {
headers['Content-Type'] = 'application/json'; // TODO XXX TODO utf8
}
Object.keys(headers).forEach(function (key) {
xhr.setRequestHeader(key, headers[key]);
});
2017-02-22 01:38:45 +00:00
xhr.send(JSON.stringify(preq.data));
});
}
, frameRequest: function (url, state, opts) {
opts = opts || {};
var previousFrame = OAUTH3._browser._frames[state];
var windowType = opts.windowType;
if (!windowType) {
windowType = 'popup';
}
var timeout = opts.timeout;
if (opts.debug) {
timeout = timeout || 3 * 60 * 1000;
}
else {
timeout = timeout || ('background' === windowType ? 15 * 1000 : 3 * 60 * 1000);
}
return new OAUTH3.PromiseA(function (resolve, reject) {
// TODO periodically garbage collect expired handlers from window object
var tok;
function cleanup() {
delete window['--oauth3-callback-' + state];
clearTimeout(tok);
tok = null;
// the actual close is done later (by the caller) so that the window/frame
// can be reused or self-closes synchronously itself / by parent
// (probably won't ever happen, but that's a negotiable implementation detail)
}
window['--oauth3-callback-' + state] = function (params) {
resolve(params);
cleanup();
};
tok = setTimeout(function () {
var err = new Error(
"the '" + windowType + "' request did not complete within " + Math.round(timeout / 1000) + "s"
);
err.code = "E_TIMEOUT";
reject(err);
cleanup();
}, timeout);
setTimeout(function () {
if (!OAUTH3._browser._frames[state]) {
reject(new Error("TODO: open the iframe first and discover oauth3 directives before popup"));
cleanup();
}
}, 0);
if ('background' === windowType) {
if (previousFrame) {
previousFrame.location = url;
//promise = previousFrame.promise;
}
else {
OAUTH3._browser._frames[state] = OAUTH3._browser.iframe(url, state, opts);
}
} else if ('popup' === windowType) {
if (previousFrame) {
previousFrame.location = url;
if (opts.debug) {
previousFrame.focus();
}
}
else {
OAUTH3._browser._frames[state] = OAUTH3._browser.frame(url, state, opts);
}
} else if ('inline' === windowType) {
// callback function will never execute and would need to redirect back to current page
// rather than the callback.html
url += '&original_url=' + OAUTH3._browser.window.location.href;
OAUTH3._browser.window.location = url;
//promise = OAUTH3.PromiseA.resolve({ url: url });
return;
} else {
throw new Error("login framing method options.windowType="
+ opts.windowType + " not type yet implemented");
}
}).then(function (params) {
if (params.error) {
// TODO directives.issuer || directives.audience
return OAUTH3.PromiseA.reject(OAUTH3.error.parse('https://oauth3.org', params));
}
return params;
});
}
, closeFrame: function (state, opts) {
opts = opts || {};
function close() {
try {
OAUTH3._browser._frames[state].close();
} catch(e) {
try {
OAUTH3._browser._frames[state].remove();
} catch(e) {
console.error(new Error("Could not clase window/iframe. closeFrame may have been called twice."));
}
}
delete OAUTH3._browser._frames[state];
}
if (opts.debug) {
if (window.confirm("DEBUG MODE: okay to close oauth3 window?")) {
close();
}
}
else {
close();
}
}
, _frames: {}
, iframe: function (url, state, opts) {
var framesrc = '<iframe class="js-oauth3-iframe" src="' + url + '" ';
if (opts.debug) {
framesrc += ' width="' + (opts.height || 600) + 'px"'
+ ' height="' + (opts.width || 720) + 'px"'
+ ' style="opacity: 0.8;" frameborder="1"';
}
else {
framesrc += ' width="1px" height="1px" frameborder="0"';
}
framesrc += '></iframe>';
var frame = OAUTH3._browser.window.document.createElement('div');
frame.innerHTML = framesrc;
OAUTH3._browser.window.document.body.appendChild(frame);
return frame;
}
, frame: function (url, state, opts) {
// TODO allow size changes (via directive even)
return window.open(
url
, 'oauth3-login-' + (opts.reuseWindow || state)
, 'height=' + (opts.height || 720) + ',width=' + (opts.width || 620)
);
}
}
2017-02-28 01:19:01 +00:00
, api: function (providerUri, opts) {
if (!OAUTH3.api[opts.api]) {
return OAUTH3.PromiseA.reject(new Error("No API for '" + opts.api + "'"));
2017-02-28 01:19:01 +00:00
}
return OAUTH3.api[opts.api](providerUri, opts);
}
};
OAUTH3.login = OAUTH3.implicitGrant;
// TODO get rid of these
OAUTH3.utils = {
clientUri: OAUTH3.clientUri
, query: OAUTH3.query
, scope: OAUTH3.scope
, uri: OAUTH3.uri
, url: OAUTH3.url
, _error: OAUTH3.error
, _formatError: OAUTH3.error
, _urlSafeBase64ToBase64: OAUTH3._urlSafeBase64ToBase64
, randomState: OAUTH3.randomState
, _insecureRandomState: OAUTH3._insecureRandomState
};
if ('undefined' !== typeof Promise) {
OAUTH3.PromiseA = Promise;
}
// this is not necessary, but it's relatively small
// and gives people the 3-line examples they love so much
OAUTH3.create = function (location, opts) {
if (!location) {
location = OAUTH3._browser.window.location;
}
2017-02-21 22:32:45 +00:00
var result = {
_clientUri: OAUTH3.clientUri(location)
, _providerUri: null
, _init: function (location, opts) {
var me = this;
if (location) {
me._clientUri = OAUTH3.clientUri(location);
}
if (opts) {
if (opts.providerUri) {
me._providerUri = opts.providerUri;
}
if (opts.session) {
2017-03-24 16:15:03 +00:00
if (!me._providerUri) {
throw new Error("'providerUri' was not supplied");
}
2017-04-14 22:52:00 +00:00
opts.session.provider_uri = me._providerUri;
opts.session.client_uri = me._clientUri;
me.session(opts.session, opts.sessionId);
}
}
}
, init: function (location/*, opts*/) {
var me = this;
var p = OAUTH3.PromiseA.resolve();
me._init(location, opts);
if (me._providerUri) {
2017-03-21 07:02:41 +00:00
// returns directives
p = OAUTH3.discover(me._providerUri, { client_id: this._clientUri });
}
return p.then(function () {
return OAUTH3.discover(me._clientUri, { client_id: me._clientUri }).then(function (clientDirectives) {
me._clientDirectives = clientDirectives;
return clientDirectives;
});
});
}
, setProvider: function (providerUri) {
var me = this;
me._providerUri = providerUri;
return me.init().then(function () {
// this should be synchronous the second time around
return OAUTH3.discover(me._providerUri, { client_id: me._clientUri }).then(function (directives) {
me._providerDirectives = directives;
return directives;
});
});
}
, checkSession: function () {
return OAUTH3.hooks.session.get(this._providerUri);
}
, login: function (opts) {
var me = this;
if (me.session()) {
me._session = true;
return OAUTH3.PromiseA.resolve(me.session());
}
opts = opts || {};
opts.client_uri = me._clientUri;
return OAUTH3.implicitGrant(me._providerDirectives, opts).then(function (session) {
me._session = true;
2017-03-26 07:43:06 +00:00
me.__session = session;
return session;
});
}
2017-03-24 15:59:22 +00:00
, session: function (session, id) {
if (!session) {
return JSON.parse(JSON.stringify(OAUTH3.hooks.session._getCached(this._providerUri) || null));
}
return OAUTH3.hooks.session.set(this._providerUri, session, id);
}
2017-02-28 01:19:01 +00:00
, request: function (preq, opts) {
opts = opts || {};
preq.client_uri = this._clientUri;
preq.client_id = this._clientUri;
2017-02-21 23:54:37 +00:00
preq.method = preq.method || 'GET';
if (this._session) {
2017-03-26 07:43:06 +00:00
preq.session = preq.session || this.session(); // OAUTH3.hooks.session._getCached(this._providerUri);
}
2017-02-21 23:54:37 +00:00
// TODO maybe use a baseUrl from the directives file?
preq.url = OAUTH3.url.resolve(this._providerUri, preq.url);
2017-03-16 21:41:33 +00:00
2017-02-28 01:19:01 +00:00
return OAUTH3.request(preq, opts);
}
, logout: function (opts) {
2017-03-26 07:43:06 +00:00
this.__session = false;
this._session = false;
opts = opts || {};
opts.client_uri = this._clientUri;
opts.client_id = this._clientUri;
opts.session = OAUTH3.hooks.session._getCached(this._providerUri);
return OAUTH3.logout(this._providerUri, opts);
}
2017-02-28 01:19:01 +00:00
, api: function (api, opts) {
opts = opts || {};
opts.api = api;
2017-03-26 07:43:06 +00:00
opts.session = this.__session || OAUTH3.hooks.session._getCached(this._providerUri);
2017-02-28 01:21:35 +00:00
return OAUTH3.api(this._providerUri, opts);
2017-02-28 01:19:01 +00:00
}
};
2017-02-21 22:32:45 +00:00
result.authenticate = result.login;
result.authorize = result.login;
result.expire = result.logout;
result._init(location, opts);
2017-02-21 22:32:45 +00:00
return result;
};
}('undefined' !== typeof exports ? exports : window));