;(function (exports) { 'use strict'; var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3; OAUTH3.query.parse = 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; }; OAUTH3.scope.parse = function (scope) { return (scope||'').split(/[, ]/g); }; OAUTH3.url.parse = function (url) { // TODO browser // Node should replace this var parser = document.createElement('a'); parser.href = url; return parser; }; OAUTH3.url._isRedirectHostSafe = function (referrerUrl, redirectUrl) { var src = OAUTH3.url.parse(referrerUrl); var dst = OAUTH3.url.parse(redirectUrl); // TODO how should we handle subdomains? // It should be safe for api.example.com to redirect to example.com // But it may not be safe for to example.com to redirect to aj.example.com // It is also probably not safe for sally.example.com to redirect to john.example.com // The client should have a list of allowed URLs to choose from and perhaps a wildcard will do // // api.example.com.evil.com SHOULD NOT match example.com return dst.hostname === src.hostname; }; OAUTH3.url.checkRedirect = function (client, query) { console.warn("[security] URL path checking not yet implemented"); var clientUrl = OAUTH3.url.normalize(client.url); var redirectUrl = OAUTH3.url.normalize(query.redirect_uri); // General rule: // I can callback to a shorter domain (fewer subs) or a shorter path (on the same domain) // but not a longer (more subs) or different domain or a longer path (on the same domain) // We can callback to an explicitly listed domain (TODO and path) if (OAUTH3.url._isRedirectHostSafe(clientUrl, redirectUrl)) { return true; } return false; }; OAUTH3.url.redirect = function (clientParams, grants, tokenOrError) { // TODO OAUTH3.redirect(clientParams, grants, tokenOrError) // TODO check redirect safeness right here with grants.client.urls // TODO check for '#' and '?'. If none, issue warning and use '?' (for backwards compat) var authz = { access_token: tokenOrError.access_token , token_type: tokenOrError.token_type // 'Bearer' , refresh_token: tokenOrError.refresh_token , expires_in: tokenOrError.expires_in // 1800 (but superceded by jwt.exp) , scope: tokenOrError.scope // superceded by jwt.scp , state: clientParams.state , debug: clientParams.debug }; if (tokenOrError.error) { authz.error = tokenOrError.error.code || tokenOrError.error; authz.error_description = tokenOrError.error.message || tokenOrError.error_description; authz.error_uri = tokenOrError.error.uri || tokenOrError.error_uri; } var redirect = clientParams.redirect_uri + '#' + window.OAUTH3.query.stringify(authz); if (clientParams.debug) { console.info('final redirect_uri:', redirect); window.alert("You're in debug mode so we've taken a pause. Hit OK to continue"); } window.location = redirect; }; OAUTH3.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 clientAgreeTos = 'oauth3.org/tos/draft'; // opts.clientAgreeTos || opts.client_agree_tos; var clientUri = opts.client_uri; var args = directive[type]; var otpCode = opts.otp || opts.otpCode || opts.otp_code || opts.otpToken || opts.otp_token || undefined; var params = { client_id: opts.client_id || opts.client_uri , client_uri: opts.client_uri , grant_type: grantType , username: opts.username , password: opts.password || otpCode || undefined , totp: opts.totp || opts.totpToken || opts.totp_token || undefined , otp: otpCode , password_type: otpCode && 'otp' , 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 (clientUri) { params.clientAgreeTos = clientAgreeTos; if (!clientAgreeTos) { throw new Error('Developer Error: missing clientAgreeTos uri'); } } if (scope) { params.scope = OAUTH3.scope.stringify(scope); } if ('GET' === args.method.toUpperCase()) { uri += '?' + OAUTH3.query.stringify(params); } else { body = params; } return { url: OAUTH3.url.resolve(directive.issuer, uri) , method: args.method , data: body }; }; OAUTH3.urls.grants = function (directive, opts) { // directive = { issuer, authorization_decision } // opts = { response_type, scopes{ granted, requested, pending, accepted } } if (!opts) { throw new Error("You must supply a directive and an options object."); } if (!opts.client_id) { throw new Error("You must supply options.client_id."); } if (!opts.session) { throw new Error("You must supply options.session."); } if (!opts.referrer) { console.warn("You should supply options.referrer"); } if (!opts.method) { console.warn("You must supply options.method as either 'GET', or 'POST'"); } if ('POST' === opts.method) { if ('string' !== typeof opts.scope) { console.warn("You should supply options.scope as a space-delimited string of scopes"); } if (-1 === ['token', 'code'].indexOf(opts.response_type)) { throw new Error("You must supply options.response_type as 'token' or 'code'"); } } var url = OAUTH3.url.resolve(directive.issuer, directive.grants.url) .replace(/(:azp|:client_id)/g, OAUTH3.uri.normalize(opts.client_id || opts.client_uri)) .replace(/(:sub|:account_id)/g, opts.session.token.sub || 'ISSUER:GRANT:TOKEN_SUB:UNDEFINED') ; var data = { client_id: opts.client_id , client_uri: opts.client_uri , referrer: opts.referrer , response_type: opts.response_type , scope: opts.scope , tenant_id: opts.tenant_id }; var body; if ('GET' === opts.method) { url += '?' + OAUTH3.query.stringify(data); } else { body = data; } return { method: opts.method , url: url , data: body , session: opts.session }; }; OAUTH3.authn = {}; OAUTH3.authn.loginMeta = function (directive, opts) { return OAUTH3.request({ method: directive.credential_meta.method || 'GET' // TODO lint urls , url: OAUTH3.url.resolve(directive.issuer, directive.credential_meta.url) .replace(':type', 'email') .replace(':id', opts.email) }); }; OAUTH3.authn.otp = function (directive, opts) { var preq = { method: directive.credential_otp.method || 'POST' , url: OAUTH3.url.resolve(directive.issuer, directive.credential_otp.url) , data: { // TODO replace with signed hosted file client_agree_tos: 'oauth3.org/tos/draft' , client_id: directive.issuer // In this case, the issuer is its own client , client_uri: directive.issuer , request_otp: true , username: opts.email } }; return OAUTH3.request(preq); }; OAUTH3.authn.resourceOwnerPassword = function (directive, opts) { var providerUri = directive.issuer; //var scope = opts.scope; //var appId = opts.appId; return OAUTH3.discover(providerUri, opts).then(function (directive) { var prequest = OAUTH3.urls.resourceOwnerPassword(directive, opts); return OAUTH3.request(prequest).then(function (req) { var data = req.data; data.provider_uri = providerUri; if (data.error) { return OAUTH3.PromiseA.reject(OAUTH3.error.parse(providerUri, data)); } return OAUTH3.hooks.session.refresh( opts.session || { provider_uri: providerUri, client_uri: opts.client_uri || opts.clientUri } , data ); }); }); }; OAUTH3.authz = {}; OAUTH3.authz.scopes = function (providerUri, session, clientParams) { // OAuth3.requests.grants(providerUri, {}); // return list of grants // OAuth3.checkGrants(providerUri, {}); // var clientUri = OAUTH3.uri.normalize(clientParams.client_uri || OAUTH3._browser.window.document.referrer); var scope = clientParams.scope || ''; var clientObj = clientParams; if (!scope) { scope = 'oauth3_authn'; } return OAUTH3.authz.grants(providerUri, { method: 'GET' , client_id: clientUri , client_uri: clientUri , session: session }).then(function (grantResults) { var grants; var grantedScopes; var grantedScopesMap; var pendingScopes; var acceptedScopes; var scopes = scope.split(/[+, ]/g); var callbackUrl; // it doesn't matter who the referrer is as long as the destination // is an authorized destination for the client in question // (though it may not hurt to pass the referrer's info on to the client) if (!OAUTH3.url.checkRedirect(grantResults.client, clientObj)) { callbackUrl = 'https://oauth3.org/docs/errors#E_REDIRECT_ATTACK' + '?redirect_uri=' + clientObj.redirect_uri + '&allowed_urls=' + grantResults.client.url + '&client_id=' + clientUri + '&referrer_uri=' + OAUTH3.uri.normalize(window.document.referrer) ; if (clientParams.debug) { console.log('grantResults Redirect Attack'); console.log(grantResults); console.log(clientObj); window.alert("DEBUG MODE checkRedirect error encountered. Check the console."); } location.href = callbackUrl; return; } if ('oauth3_authn' === scope) { // implicit ppid grant is automatic console.warn('[security] fix scope checking on backend so that we can do automatic grants'); // TODO check user preference if implicit ppid grant is allowed //return generateToken(session, clientObj); } grants = (grantResults).grants.filter(function (grant) { if (clientUri === (grant.azp || grant.oauth_client_id || grant.oauthClientId)) { return true; } }); grantedScopesMap = {}; acceptedScopes = []; pendingScopes = scopes.filter(function (requestedScope) { return grants.every(function (grant) { if (!grant.scope) { grant.scope = 'oauth3_authn'; } var gscopes = grant.scope.split(/[+, ]/g); gscopes.forEach(function (s) { grantedScopesMap[s] = true; }); if (-1 !== gscopes.indexOf(requestedScope)) { // already accepted in the past acceptedScopes.push(requestedScope); } else { // true, is pending return true; } }); }); grantedScopes = Object.keys(grantedScopesMap); return { pending: pendingScopes // not yet accepted , granted: grantedScopes // all granted, ever , requested: scopes // all requested, now , accepted: acceptedScopes // granted (ever) and requested (now) }; }); }; OAUTH3.authz.grants = function (providerUri, opts) { return OAUTH3.discover(providerUri, { client_id: providerUri , debug: opts.debug }).then(function (directive) { return OAUTH3.request(OAUTH3.urls.grants(directive, opts), opts).then(function (grantsResult) { if ('POST' === opts.method) { // TODO this is clientToken return grantsResult.originalData || grantsResult.data; } var grants = grantsResult.originalData || grantsResult.data; // TODO if (grants.error) { return OAUTH3.PromiseA.reject(OAUTH3.error.parse(providerUri, grants)); } OAUTH3.hooks.grants.set(opts.client_id + '-client', grants.client); grants.grants.forEach(function (grant) { var clientId = grant.client_id || grant.oauth_client_id || grant.oauthClientId; // TODO should save as an array OAUTH3.hooks.grants.set(clientId, [ grant ]); }); return { client: OAUTH3.hooks.grants.get(opts.client_id + '-client') , grants: OAUTH3.hooks.grants.get(opts.client_id) || [] }; }); }); }; OAUTH3.authz.redirectWithToken = function (providerUri, session, clientParams, scopes) { scopes.new = scopes.new || []; if ('token' === clientParams.response_type) { // get token and redirect client-side return OAUTH3.authz.grants(providerUri, { method: 'POST' , client_id: clientParams.client_uri , client_uri: clientParams.client_uri , scope: scopes.granted.concat(scopes.new).join(',') , response_type: clientParams.response_type , referrer: clientParams.referrer , session: session , debug: clientParams.debug }).then(function (results) { // TODO limit refresh token to an expirable token // TODO inform client not to persist token /* if (clientParams.dnsTxt) { Object.keys(results).forEach(function (key) { if (/refresh/.test(key)) { results[key] = undefined; } }); } */ OAUTH3.url.redirect(clientParams, scopes, results); }); } else if ('code' === clientParams.response_type) { // get token and redirect server-side // (requires insecure form post as per spec) //OAUTH3.requests.authorizationDecision(); window.alert("Authorization Code Redirect NOT IMPLEMENTED"); throw new Error("Authorization Code Redirect NOT IMPLEMENTED"); } }; OAUTH3.requests = {}; OAUTH3.requests.accounts = {}; OAUTH3.requests.accounts.update = function (directive, session, opts) { var dir = directive.update_account || { method: 'POST' , url: 'https://' + directive.provider_url + '/api/org.oauth3.provider/accounts/:accountId' , bearer: 'Bearer' }; var url = dir.url .replace(/:accountId/, opts.accountId) ; return OAUTH3.request({ method: dir.method || 'POST' , url: url , headers: { 'Authorization': (dir.bearer || 'Bearer') + ' ' + session.accessToken } , json: { name: opts.name , comment: opts.comment , displayName: opts.displayName , priority: opts.priority } }); }; OAUTH3.requests.accounts.create = function (directive, session, account) { var dir = directive.create_account || { method: 'POST' , url: 'https://' + directive.issuer + '/api/org.oauth3.provider/accounts' , bearer: 'Bearer' }; var data = { // TODO fix the server to just use one scheme // account = { nick, self: { comment, username } } // account = { name, comment, display_name, priority } account: { nick: account.display_name , name: account.name , comment: account.comment , display_name: account.display_name , priority: account.priority , self: { nick: account.display_name , name: account.name , comment: account.comment , display_name: account.display_name , priority: account.priority } } , logins: [ { token: session.access_token } ] }; return OAUTH3.request({ method: dir.method || 'POST' , url: dir.url , session: session , data: data }); }; OAUTH3.hooks.grants = { // Provider Only set: function (clientUri, newGrants) { clientUri = OAUTH3.uri.normalize(clientUri); console.warn('[oauth3.hooks.setGrants] PLEASE IMPLEMENT -- Your Fault'); console.warn(newGrants); if (!this._cache) { this._cache = {}; } console.log('clientUri, newGrants'); console.log(clientUri, newGrants); this._cache[clientUri] = newGrants; return newGrants; } , get: function (clientUri) { clientUri = OAUTH3.uri.normalize(clientUri); console.warn('[oauth3.hooks.getGrants] PLEASE IMPLEMENT -- Your Fault'); if (!this._cache) { this._cache = {}; } console.log('clientUri, existingGrants'); console.log(clientUri, this._cache[clientUri]); return this._cache[clientUri]; } }; OAUTH3._browser.isIframe = function isIframe () { try { return window.self !== window.top; } catch (e) { return true; } }; }('undefined' !== typeof exports ? exports : window));