/* 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 = '