/* global Promise */ ;(function (exports) { 'use strict'; var OAUTH3 = exports.OAUTH3 = { utils: { clientUri: function (location) { return OAUTH3.utils.uri.normalize(location.host + location.pathname); } , 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(/\/?$/, '') ; } } , 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 directives; } return OAUTH3._discoverHelper(providerUri, opts).then(function (directives) { directives.issuer = directives.issuer || OAUTH3.utils.url.normalize(providerUri); // 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); } , request: function (preq) { return OAUTH3._browser.request(preq); } , implicitGrant: function(providerUri, opts) { var promise; if (opts.broker) { promise = OAUTH3._discoverThenImplicitGrant(providerUri, opts); } else { promise = OAUTH3._implicitGrant(providerUri, opts); } return promise.then(function (tokens) { return OAUTH3.hooks.refreshSession( 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) { OAUTH3._browser.closeFrame(tokens.state || opts._state); opts._state = undefined; }); }); } , _discover: function(providerUri, opts) { providerUri = OAUTH3.utils.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.utils.url.normalize(providerUri) + '/.well-known/oauth3/directives.json' }); } 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 , 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 return OAUTH3._browser.frameRequest( discReq.url , discReq.state , { windowType: opts.windowType , reuseWindow: opts.broker && '-broker' , debug: opts.debug } ).then(function (params) { // discWin.child.close() // TODO params should have response_type indicating json, binary, etc var directives = JSON.parse(OAUTH3.utils.atob(OAUTH3.utils.urlSafeBase64ToBase64(params.result || params.directives))); return OAUTH3.hooks.directives.set(providerUri, directives); }); } , _implicitGrant: function(providerUri, opts) { // TODO this may need to be synchronous for browser security policy return OAUTH3.PromiseA.resolve(OAUTH3.hooks.directives.get(providerUri)).then(function (directives) { // 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 , state: opts._state , debug: opts.debug } ); if (opts.debug) { window.alert("DEBUG MODE: Pausing so you can look at logs and whatnot :) Fire at will!"); } return new OAUTH3.PromiseA(function (resolve, reject) { return OAUTH3._browser.frameRequest( authReq.url , authReq.state // state should recycle params , { windowType: opts.windowType , reuseWindow: opts.broker && '-broker' , debug: opts.debug } ).then(function (tokens) { if (tokens.error) { return reject(OAUTH3.utils._formatError(tokens.error)); } OAUTH3._browser.closeFrame(authReq.state, { debug: opts.debug || tokens.debug }); return tokens; }); }); }); } // // Let the Code Waste begin!! // , _browser: { window: window // TODO we don't need to include this if we're using jQuery or angular , 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 () { console.error('state change'); 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({ request: xhr , data: data , status: xhr.status }); }; xhr.open(preq.method, preq.url, true); var headers = preq.headers || {}; Object.keys(headers).forEach(function (key) { xhr.setRequestHeader(key, headers[key]); }); xhr.send(); }); } , discover: function (providerUri, opts) { opts = opts || {}; //opts.debug = true; providerUri = OAUTH3.utils.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.utils.url.normalize(providerUri) + '/.well-known/oauth3/directives.json' }); } 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 discObj = OAUTH3.urls.discover( providerUri , { client_id: (opts.client_id || opts.client_uri || OAUTH3.clientUri(OAUTH3._browser.window.location)), 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.closeFrame(discObj.state, { debug: opts.debug || params.debug }); 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.closeFrame(discObj.state, { debug: opts.debug || err.debug }); return OAUTH3.PromiseA.reject(err); }); } , frameRequest: function (url, state, opts) { var previousFrame = OAUTH3._browser._frames[state]; if (!opts.windowType) { opts.windowType = 'popup'; } opts = opts || {}; if (opts.debug) { opts.timeout = opts.timeout || 15 * 60 * 1000; } return 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); if ('background' === opts.windowType) { if (previousFrame) { previousFrame.location = url; //promise = previousFrame.promise; } else { OAUTH3._browser._iframe.insert(url, state, opts); } } else if ('popup' === opts.windowType) { if (previousFrame) { previousFrame.location = url; if (opts.debug) { previousFrame.focus(); } } else { OAUTH3._browser.frame.open(url, state, opts); } } else if ('inline' === opts.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) { var err; if (params.error || params.error_description) { err = new Error(params.error_description || "Unknown response error"); err.code = params.error || "E_UKNOWN_ERROR"; err.params = params; //_formatError return OAUTH3.PromiseA.reject(err); } return params; }); } , closeFrame: function (state, opts) { function close() { try { OAUTH3._browser._frames[state].close(); } catch(e) { try { OAUTH3._browser._frames[state].remove(); } catch(e) { } } delete OAUTH3._browser._frames[state]; } if (opts.debug) { if (window.confirm("DEBUG MODE: okay to close oauth3 window?")) { close(); } } else { close(); } } , _frames: {} , iframe: { insert: function (url, state, opts) { // TODO hidden / non-hidden (via directive even) var framesrc = '