WIP implicit-grant-only in a single file

This commit is contained in:
AJ ONeal 2017-02-13 15:22:06 -07:00
parent 4657fcdb12
commit bbc557c349
1 changed files with 276 additions and 0 deletions

276
oauth3.implicit.js Normal file
View File

@ -0,0 +1,276 @@
/* global Promise */
;(function (exports) {
'use strict';
var OAUTH3 = exports.OAUTH3 = {
utils: {
atob: function (base64) {
return (exports.atob || require('atob'))(base64);
}
, urlSafeBase64ToBase64: 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 b64;
}
, uri: {
normalize: function (uri) {
// tested with
// example.com
// example.com/
// http://example.com
// https://example.com/
return uri
.replace(/^(https?:\/\/)?/i, '')
.replace(/\/?$/, '')
;
}
}
, url: {
normalize: function (url) {
// tested with
// example.com
// example.com/
// http://example.com
// https://example.com/
return url
.replace(/^(https?:\/\/)?/i, 'https://')
.replace(/\/?$/, '')
;
}
}
, getDefaultAppUrl: function () {
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(/\/?$/, '')
;
}
, query: {
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.utils.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(
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;
}
}
, 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.utils.url.normalize(opts.client_id || opts.client_uri);
providerUri = OAUTH3.utils.url.normalize(providerUri);
var params = {
action: 'directives'
, 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.utils.query.stringify(params)
, state: params.state
, method: 'GET'
, query: params
};
return result;
}
}
, hooks: {
directives: {
get: function (providerUri) {
providerUri = OAUTH3.utils.uri.normalize(providerUri);
console.warn('[Warn] You should implement: OAUTH3.hooks.directives.get = function (providerUri) { return directives; }');
if (!OAUTH3.hooks.directives._cache) { OAUTH3.hooks.directives._cache = {}; }
return JSON.parse(window.localStorage.getItem('directives-' + providerUri) || '{}');
}
, set: function (providerUri, directives) {
providerUri = OAUTH3.utils.uri.normalize(providerUri);
console.warn('[Warn] You should implement: OAUTH3.hooks.directives.set = function (providerUri, directives) { return directives; }');
console.warn(directives);
if (!OAUTH3.hooks.directives._cache) { OAUTH3.hooks.directives._cache = {}; }
window.localStorage.setItem('directives-' + providerUri, JSON.stringify(directives));
OAUTH3.hooks.directives._cache[providerUri] = directives;
return directives;
}
}
}
, discover: function (providerUri, opts) {
if (!providerUri) {
throw new Error('oauth3.discover(providerUri, opts) received providerUri as ' + providerUri);
}
return OAUTH3.PromiseA.resolve(OAUTH3.hooks.directives.get(providerUri)).then(function (directives) {
if (directives && directives.issuer) {
return OAUTH3.PromiseA.resolve(directives);
}
return OAUTH3._discoverHelper(providerUri, opts).then(function (directives) {
directives.issuer = directives.issuer || OAUTH3.utils.url.normalize(providerUri);
console.log('discoverHelper', directives);
// OAUTH3.PromiseA.resolve() is taken care of because this is wrapped
return OAUTH3.hooks.directives.set(providerUri, directives);
});
});
}
// this is the browser version
, _discoverHelper: function (providerUri, opts) {
return OAUTH3._browser.discover(providerUri, opts);
}
, _browser: {
discover: function (providerUri, opts) {
opts = opts || {};
//opts.debug = true;
providerUri = OAUTH3.utils.url.normalize(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.utils.url.normalize(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.urls.discover(
providerUri
, { client_id: (opts.client_id || opts.client_uri || OAUTH3.utils.getDefaultAppUrl()), debug: opts.debug }
);
// TODO ability to reuse iframe instead of closing
return OAUTH3._browser.iframe.insert(discObj.url, discObj.state, opts).then(function (params) {
OAUTH3._browser.iframe.remove(discObj.state);
if (params.error) {
return OAUTH3.utils._formatError(providerUri, params.error);
}
var directives = JSON.parse(OAUTH3.utils.atob(OAUTH3.utils.urlSafeBase64ToBase64(params.result || params.directives)));
return directives;
}, function (err) {
OAUTH3._browser.iframe.remove(discObj.state);
return OAUTH3.PromiseA.reject(err);
});
}
, iframe: {
_frames: {}
, insert: 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;
function cleanup() {
delete window['--oauth3-callback-' + state];
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>';
OAUTH3._browser.iframe._frames[state] = window.document.createElement('div');
OAUTH3._browser.iframe._frames[state].innerHTML = framesrc;
window.document.body.appendChild(OAUTH3._browser.iframe._frames[state]);
});
// TODO periodically garbage collect expired handlers from window object
return promise;
}
, remove: function (state) {
if (OAUTH3._browser.iframe._frames[state]) {
OAUTH3._browser.iframe._frames[state].remove();
}
delete OAUTH3._browser.iframe._frames[state];
}
}
}
};
if ('undefined' !== typeof Promise) {
OAUTH3.PromiseA = Promise;
}
}('undefined' !== typeof exports ? exports : window));