From bbc557c3496d7349defd3f26a81338e1a964bb07 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 13 Feb 2017 15:22:06 -0700 Subject: [PATCH] WIP implicit-grant-only in a single file --- oauth3.implicit.js | 276 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 oauth3.implicit.js diff --git a/oauth3.implicit.js b/oauth3.implicit.js new file mode 100644 index 0000000..3f35c5c --- /dev/null +++ b/oauth3.implicit.js @@ -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 = '