;(function (exports) { 'use strict'; // NOTE: we assume that directive.provider_uri exists var core = {}; core.urls = core; function getDefaultAppApiBase() { console.warn('[deprecated] using window.location.host when opts.appApiBase should be used'); return 'https://' + window.location.host; } core.parsescope = function (scope) { return (scope||'').split(/[+, ]/g); }; core.stringifyscope = function (scope) { if (Array.isArray(scope)) { scope = scope.join(' '); } return scope; }; core.querystringify = function (params) { var qs = []; Object.keys(params).forEach(function (key) { // TODO nullify instead? if ('undefined' === typeof params[key]) { return; } if ('scope' === key) { params[key] = core.stringifyscope(params[key]); } qs.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key])); }); return qs.join('&'); }; // Modified from http://stackoverflow.com/a/7826782 core.queryparse = function (search) { // parse a query or a hash if (-1 !== ['#', '?'].indexOf(search[0])) { search = search.substring(1); } // Solve for case of search within hash // example: #/authorization_dialog/?state=...&redirect_uri=... var queryIndex = search.indexOf('?'); if (-1 !== queryIndex) { search = search.substr(queryIndex + 1); } var args = search.split('&'); var argsParsed = {}; var i, arg, kvp, key, value; for (i = 0; i < args.length; i += 1) { arg = args[i]; if (-1 === arg.indexOf('=')) { argsParsed[decodeURIComponent(arg).trim()] = true; } else { kvp = arg.split('='); key = decodeURIComponent(kvp[0]).trim(); value = decodeURIComponent(kvp[1]).trim(); argsParsed[key] = value; } } return argsParsed; }; core.formatError = function (providerUri, params) { var err = new Error(params.error_description || params.error.message || "Unknown error when discoving provider '" + providerUri + "'"); err.uri = params.error_uri || params.error.uri; err.code = params.error.code || params.error; return err; }; core.normalizePath = function (path) { return path.replace(/^\//, '').replace(/\/$/, ''); }; core.normalizeUri = function (providerUri) { // tested with // example.com // example.com/ // http://example.com // https://example.com/ return providerUri .replace(/^(https?:\/\/)?/i, '') .replace(/\/?$/, '') ; }; core.normalizeUrl = function (providerUri) { // tested with // example.com // example.com/ // http://example.com // https://example.com/ return providerUri .replace(/^(https?:\/\/)?/i, 'https://') .replace(/\/?$/, '') ; }; // these might not really belong in core... not sure // there should be node.js- and browser-specific versions probably core.utils = { 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; } , base64ToUrlSafeBase64: function (b64) { // Base64 to URL-safe Base64 b64 = b64.replace(/\+/g, '-').replace(/\//g, '_'); b64 = b64.replace(/=+/g, ''); return b64; } , randomState: function () { var i; var ch; var str; // 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) { // 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; } } }; core.jwt = { // decode only (no verification) decode: function (str) { // 'abc.qrs.xyz' // [ 'abc', 'qrs', 'xyz' ] // [ {}, {}, 'foo' ] // { header: {}, payload: {}, signature: } var parts = str.split(/\./g); var jsons = parts.slice(0, 2).map(function (urlsafe64) { var atob = exports.atob || require('atob'); var b64 = core.utils.urlSafeBase64ToBase64(urlsafe64); return atob(b64); }); return { header: JSON.parse(jsons[0]) , payload: JSON.parse(jsons[1]) , signature: parts[2] // should remain url-safe base64 }; } , getFreshness: function (meta, staletime, now) { staletime = staletime || (15 * 60); now = now || Date.now(); var fresh = ((parseInt(meta.exp, 10) || 0) - Math.round(now / 1000)); if (fresh >= staletime) { return 'fresh'; } if (fresh <= 0) { return 'expired'; } return 'stale'; } // encode-only (no signature) , encode: function (parts) { parts.header = parts.header || { alg: 'none', typ: 'jwt' }; parts.signature = parts.signature || ''; var btoa = exports.btoa || require('btoa'); var result = [ core.utils.base64ToUrlSafeBase64(btoa(JSON.stringify(parts.header, null))) , core.utils.base64ToUrlSafeBase64(btoa(JSON.stringify(parts.payload, null))) , parts.signature // should already be url-safe base64 ].join('.'); return result; } }; core.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 params = { action: 'directives' , state: core.utils.randomState() , redirect_uri: opts.client_id + (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/#/?' + core.querystringify(params) , state: params.state , method: 'GET' , query: params }; return result; }; core.urls.authorizationCode = function (/*directive, scope, redirectUri, clientId*/) { // // Example Authorization Code Request // (not for use in the browser) // // GET https://example.com/api/org.oauth3.provider/authorization_dialog // ?response_type=code // &scope=`encodeURIComponent('profile.login profile.email')` // &state=`cryptoutil.random().toString('hex')` // &client_id=xxxxxxxxxxx // &redirect_uri=`encodeURIComponent('https://myapp.com/oauth3.html')` // // NOTE: `redirect_uri` itself may also contain URI-encoded components // // NOTE: This probably shouldn't be done in the browser because the server // needs to initiate the state. If it is done in a browser, the browser // should probably request 'state' from the server beforehand // throw new Error("not implemented"); }; core.urls.authorizationRedirect = function (directive, opts) { //console.log('[authorizationRedirect]'); // // Example Authorization Redirect - from Browser to Consumer API // (for generating a session securely on your own server) // // i.e. GET https://<>.com/api/org.oauth3.consumer/authorization_redirect/<>.com // // GET https://myapp.com/api/org.oauth3.consumer/authorization_redirect/`encodeURIComponent('example.com')` // &scope=`encodeURIComponent('profile.login profile.email')` // // (optional) // &state=`cryptoutil.random().toString('hex')` // &redirect_uri=`encodeURIComponent('https://myapp.com/oauth3.html')` // // NOTE: This is not a request sent to the provider, but rather a request sent to the // consumer (your own API) which then sets some state and redirects. // This will initiate the `authorization_code` request on your server // opts = opts || {}; var scope = opts.scope || directive.authn_scope; var providerUri = directive.provider_uri; var params = { state: core.utils.randomState() , debug: opts.debug || undefined }; var slimProviderUri = encodeURIComponent(providerUri.replace(/^(https?|spdy):\/\//, '')); var authorizationRedirect = opts.authorizationRedirect; if (scope) { params.scope = scope; } if (opts.redirectUri) { // this is really only for debugging params.redirect_uri = opts.redirectUri; } // Note: the type check is necessary because we allow 'true' // as an automatic mechanism when it isn't necessary to specify if ('string' !== typeof authorizationRedirect) { // TODO oauth3.json for self? authorizationRedirect = (opts.appApiBase || getDefaultAppApiBase()) + '/api/org.oauth3.consumer/authorization_redirect/:provider_uri'; } authorizationRedirect = authorizationRedirect .replace(/!(provider_uri)/, slimProviderUri) .replace(/:provider_uri/, slimProviderUri) .replace(/#{provider_uri}/, slimProviderUri) .replace(/{{provider_uri}}/, slimProviderUri) ; return { url: authorizationRedirect + '?' + core.querystringify(params) , method: 'GET' , state: params.state // this becomes browser_state , params: params // includes scope, final redirect_uri? }; }; core.urls.implicitGrant = function (directive, opts) { //console.log('[implicitGrant]'); // // Example Implicit Grant Request // (for generating a browser-only session, not a session on your server) // // GET https://example.com/api/org.oauth3.provider/authorization_dialog // ?response_type=token // &scope=`encodeURIComponent('profile.login profile.email')` // &state=`cryptoutil.random().toString('hex')` // &client_id=xxxxxxxxxxx // &redirect_uri=`encodeURIComponent('https://myapp.com/oauth3.html')` // // NOTE: `redirect_uri` itself may also contain URI-encoded components // opts = opts || {}; var type = 'authorization_dialog'; var responseType = 'token'; var redirectUri = opts.redirectUri; var scope = opts.scope || directive.authn_scope; var clientId = core.normalizeUri(opts.client_id || opts.client_uri || opts.appId || opts.clientId || opts.clientUri); var args = directive[type]; var uri = args.url; var state = core.utils.randomState(); var params = { debug: opts.debug || undefined , client_uri: opts.client_uri || opts.clientUri || undefined }; var loc; var result; params.state = state; params.response_type = responseType; if (scope) { params.scope = core.stringifyscope(scope); } if (clientId) { // In OAuth3 client_id is optional for implicit grant params.client_id = clientId; } if (!redirectUri) { loc = window.location; redirectUri = loc.protocol + '//' + loc.host + loc.pathname; if ('/' !== redirectUri[redirectUri.length - 1]) { redirectUri += '/'; } redirectUri += 'oauth3.html'; } params.redirect_uri = redirectUri; uri += '?' + core.querystringify(params); result = { url: uri , state: state , method: args.method , query: params }; return result; }; core.urls.resolve = function (base, next) { if (/^https:\/\//i.test(next)) { return next; } return core.normalizeUrl(base) + '/' + core.normalizePath(next); }; core.urls.refreshToken = function (directive, opts) { // grant_type=refresh_token // Example Refresh Token Request // (generally for 1st or 3rd party server-side, mobile, and desktop apps) // // POST https://example.com/api/oauth3/access_token // { "grant_type": "refresh_token", "client_id": "<>", "scope": "<>" // , "username": "<>", "password": "password" } // opts = opts || {}; var type = 'access_token'; var grantType = 'refresh_token'; var scope = opts.scope || directive.authn_scope; var clientSecret = opts.appSecret || opts.clientSecret; var args = directive[type]; var params = { "grant_type": grantType , "refresh_token": opts.refresh_token || opts.refreshToken || (opts.session && opts.session.refresh_token) , "response_type": 'token' , "client_id": opts.appId || opts.app_id || opts.client_id || opts.clientId || opts.client_id || opts.clientId , "client_uri": opts.client_uri || opts.clientUri //, "scope": undefined //, "client_secret": undefined , debug: opts.debug || undefined }; var uri = args.url; var body; // TODO not allowed in the browser if (clientSecret) { params.client_secret = clientSecret; } if (scope) { params.scope = core.stringifyscope(scope); } if ('GET' === args.method.toUpperCase()) { uri += '?' + core.querystringify(params); } else { body = params; } return { url: uri , method: args.method , data: body }; }; core.urls.logout = function (directive, opts) { opts = opts || {}; var type = 'logout'; var clientId = opts.appId || opts.clientId || opts.client_id; var args = directive[type]; var params = { client_id: opts.clientUri || opts.client_uri , debug: opts.debug || undefined }; var uri = args.url; var body; if (opts.clientUri) { params.client_uri = opts.clientUri; } if (clientId) { params.client_id = clientId; } args.method = (args.method || 'GET').toUpperCase(); if ('GET' === args.method) { uri += '?' + core.querystringify(params); } else { body = params; } return { url: uri , method: args.method || 'GET' , data: body }; }; exports.OAUTH3 = exports.OAUTH3 || { core: core }; exports.OAUTH3_CORE = core.OAUTH3_CORE = core; if ('undefined' !== typeof module) { module.exports = core; } }('undefined' !== typeof exports ? exports : window));