(function (exports) { 'use strict'; // NOTE: we assume that directive.provider_uri exists var core = {}; 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); } 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; }; // 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 b64 = b64.replace(/-/g, '+').replace(/_/g, '/'); b64 = (b64 + '===').slice(0, b64.length + (b64.length % 4)); return b64; } , base64ToUrlSafeBase64: function (b64) { // Base64 to URL-safe Base64 b64 = b64.replace(/\+/g, '-').replace(/\//g, '_'); b64 = b64.replace(/=+/g, ''); return b64; } }; 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 (b64) { var atob = exports.atob || require('atob'); return atob(core.utils.urlSafeBase64ToBase64(b64)); }); return { header: JSON.parse(jsons[0]) , payload: JSON.parse(jsons[1]) , signature: parts[2] // should remain url-safe base64 }; } // 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.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=`Math.random()` // &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.authorizationRedirect = function (directive, authorizationRedirect, 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=`Math.random()` // &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 state = Math.random().toString().replace(/^0\./, ''); var params = {}; var slimProviderUri = encodeURIComponent(providerUri.replace(/^(https?|spdy):\/\//, '')); params.state = state; 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 = 'https://' + window.location.host + '/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: state // this becomes browser_state , params: params // includes scope, final redirect_uri? }; }; core.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=`Math.random()` // &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; var args = directive[type]; var uri = args.url; var state = Math.random().toString().replace(/^0\./, ''); var params = {}; 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.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 }; 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.resourceOwnerPassword = function (directive, username, passphrase, 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 (!passphrase) { if (opts.otp) { // for backwards compat passphrase = opts.otp; // 'otp:' + opts.otpUuid + ':' + opts.otp; } } var scope = opts.scope || directive.authn_scope; var clientId = opts.appId || opts.clientId; var clientAgreeTos = opts.clientAgreeTos; var clientUri = opts.clientUri; var args = directive[type]; var params = { "grant_type": grantType , "username": username , "password": passphrase || undefined , "totp": opts.totp || opts.totpToken || undefined , "otp": opts.otp || opts.otpCode || undefined , "otp_uuid": opts.otpUuid || undefined , "user_agent": opts.userAgent || undefined // AJ's Macbook , "jwk": opts.rememberDevice && 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 }; 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.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 clientId = opts.appId || opts.clientId; var clientSecret = opts.appSecret || opts.clientSecret; var args = directive[type]; var params = { "grant_type": grantType , "refresh_token": opts.refreshToken , "response_type": 'token' //, "client_id": undefined //, "client_uri": undefined //, "scope": undefined //, "client_secret": undefined }; var uri = args.url; var body; if (opts.clientUri) { params.client_uri = opts.clientUri; } if (clientId) { params.client_id = clientId; } 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 }; }; 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));