WIP implicit-grant-only in a single file
This commit is contained in:
parent
4657fcdb12
commit
bbc557c349
|
@ -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));
|
Loading…
Reference in New Issue