;(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.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.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(/\/?$/, '') ; }; core.urls.discover = function (providerUri, opts) { if (!providerUri) { throw new Error("cannot discover without providerUri"); } if (!opts.appUrl) { throw new Error("cannot discover without opts.appUrl"); } var params = { action: 'directives' , state: core.utils.randomState() , redirect_uri: opts.appUrl + (opts.appCallbackPath || '/.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; }; // 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.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 = 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.loginCode = function (directive, opts) { // // Example Resource Owner Password Request // (generally for 1st party and direct-partner mobile apps, and webapps) // // POST https://api.example.com/api/org.oauth3.provider/otp // { "request_otp": true, "client_id": "<>", "scope": "<>" // , "username": "<>" } // opts = opts || {}; var clientId = opts.appId || opts.clientId; var args = directive.otp; if (!directive.otp) { console.log('[debug] loginCode directive:'); console.log(directive); } var params = { "username": opts.id || opts.username , "request_otp": true // opts.requestOtp || undefined //, "jwt": opts.jwt // TODO sign a proof , debug: opts.debug || undefined }; var uri = args.url; var body; if (opts.clientUri) { params.client_uri = opts.clientUri; } if (opts.clientAgreeTos) { params.client_agree_tos = opts.clientAgreeTos; } if (clientId) { params.client_id = clientId; } if ('GET' === args.method.toUpperCase()) { uri += '?' + core.querystringify(params); } else { body = params; } return { url: uri , method: args.method , data: body }; }; core.urls.resourceOwnerPassword = function (directive, opts) { // // Example Resource Owner Password Request // (generally for 1st party and direct-partner mobile apps, and webapps) // // POST https://example.com/api/org.oauth3.provider/access_token // { "grant_type": "password", "client_id": "<>", "scope": "<>" // , "username": "<>", "password": "password" } // opts = opts || {}; var type = 'access_token'; var grantType = 'password'; if (!opts.password) { if (opts.otp) { // for backwards compat opts.password = opts.otp; // 'otp:' + opts.otpUuid + ':' + opts.otp; } } var scope = opts.scope || directive.authn_scope; var clientId = opts.appId || opts.clientId || opts.client_id; var clientAgreeTos = opts.clientAgreeTos || opts.client_agree_tos; var clientUri = opts.clientUri || opts.client_uri || opts.clientUrl || opts.client_url; var args = directive[type]; var otpCode = opts.otp || opts.otpCode || opts.otp_code || opts.otpToken || opts.otp_token || undefined; var params = { "grant_type": grantType , "username": opts.username , "password": opts.password || otpCode || undefined , "totp": opts.totp || opts.totpToken || opts.totp_token || undefined , "otp": otpCode , "otp_code": otpCode , "otp_uuid": opts.otpUuid || opts.otp_uuid || undefined , "user_agent": opts.userAgent || opts.useragent || opts.user_agent || undefined // AJ's Macbook , "jwk": (opts.rememberDevice || opts.remember_device) && opts.jwk || undefined //, "public_key": opts.rememberDevice && opts.publicKey || undefined //, "public_key_type": opts.rememberDevice && opts.publicKeyType || undefined // RSA/ECDSA //, "jwt": opts.jwt // TODO sign a proof with a previously loaded public_key , debug: opts.debug || undefined }; var uri = args.url; var body; if (opts.totp) { params.totp = opts.totp; } if (clientId) { params.clientId = clientId; } if (clientUri) { params.clientUri = clientUri; params.clientAgreeTos = clientAgreeTos; if (!clientAgreeTos) { throw new Error('Developer Error: missing clientAgreeTos uri'); } } 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.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));