VulpesOS/gaia/apps/email/js/cards/oauth2/fetch.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

228 lines
7.7 KiB
JavaScript
Executable file

'use strict';
define(function(require) {
var queryString = require('query_string');
var services = require('services');
var cards = require('cards');
// All of the oauthSecrets we know.
var oauthSecrets = services.oauth2;
// The secrets for the oauth we're currently doing.
var curOauthSecrets;
// Flag to indicate the work for redeeming the auth code is still in progress,
// so detection of the auth window closing should not cancel the operation.
var redeemingCode = false;
var TIMEOUT_MS = 30 * 1000;
var redirectUri = 'http://localhost';
var deferred, p, oauthSettings;
/**
* The postMessage listener called from redirect.html to indicate the final
* data returned from the oauth jump.
* @param {DOMEvent} event the event sent via redirect.html's postMessage.
*/
function onBrowserComplete(message) {
var data = message.data;
if (message.type === 'oauth2Complete') {
if (!data.code) {
reset('reject', new Error('no code returned'));
return;
}
// Okay, so we've got a code. And now we need to redeem that code for
// tokens.
console.log('oauth redirect returned with code');
redeemingCode = true;
redeemCode(data.code).then(
function(redeemed) {
// Microsoft provides expires_on too but Google only provides
// expires_in, so let's just use expires_in for now.
var expiresInMS = parseInt(redeemed.expires_in, 10) * 1000;
// Make the time absolute and let's take 30 seconds off the end to
// allow for ridiculous latency.
var expireTimeMS = Date.now() +
Math.max(0, expiresInMS - TIMEOUT_MS);
var result = {
status: 'success',
tokens: {
accessToken: redeemed.access_token,
refreshToken: redeemed.refresh_token,
expireTimeMS: expireTimeMS
},
secrets: curOauthSecrets
};
reset('resolve', result);
},
function(err) {
reset('reject', err);
});
} else {
// Treat anything else like a cancel, it failed or was stopped by the
// user.
reset('resolve', { status: 'cancel' });
}
}
/**
* Turns the code received from the oauth jump into an actual oauth token
* data.
* @param {String} code the code received from the oauth2 provider.
* @return {Promise}
*/
function redeemCode(code) {
console.log('redeeming oauth code');
return new Promise(function(xhrResolve, xhrReject) {
var xhr = new XMLHttpRequest({ mozSystem: true });
xhr.open('POST', oauthSettings.tokenEndpoint, true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.timeout = TIMEOUT_MS;
xhr.onload = function() {
if (xhr.status < 200 || xhr.status >= 300) {
console.error('token redemption failed', xhr.status,
xhr.responseText);
xhrReject('status' + xhr.status);
} else {
try {
// yeah, we could have just set responseType.
var data = JSON.parse(xhr.responseText);
console.log('oauth code redeemed. access_token? ' +
!!data.access_token + ', refresh_token? ' +
!! data.refresh_token);
xhrResolve(data);
} catch (ex) {
console.error('badly formed JSON response for token redemption:',
xhr.responseText);
xhrReject(ex);
}
}
};
xhr.onerror = function(err) {
console.error('token redemption weird error:', err);
xhrReject(err);
};
xhr.ontimeout = function() {
console.error('token redemption timeout');
xhrReject('timeout');
};
var redemptionArgs = {
code: code,
client_id: curOauthSecrets.clientId,
client_secret: curOauthSecrets.clientSecret,
// uh, this doesn't seem right, but the docs want it? hopefully it just
// gets forever ignored?
redirect_uri: redirectUri,
grant_type: 'authorization_code'
};
xhr.send(queryString.fromObject(redemptionArgs));
});
}
function reset(actionName, value) {
console.log('oauth2 fetch reset with action: ' + actionName);
var action = deferred[actionName];
deferred = p = null;
redeemingCode = false;
action(value);
}
/**
* Does the OAuth dialog dance.
*
* @param {Object} o2Settings
* @param {String} o2Settings.authEndpoint
* URL of the authorization endpoint.
* @param {String} o2Settings.scope
* The scope to request.
* @param {String} [o2Settings.secretGroup]
* Secret group identifier if this is an initial OAuth. If this is a reauth
* then the clientId should be provided instead and reused.
* @param {String} [o2Settings.clientId]
* If this is a reauth (or don't need to/want to use our internal secret
* table, the client_id to reuse. clientSecret should also be provided
* (even though we don't use it ourselves) for consistency.
* @param {Object} [extraQueryObject] extra query args to pass to the
* provider. These are typically only known at runtime vs the o2Settings.
* The property names in the extraQueryObject will be mapped to the provider's
* preferred names by using o2Settings.extraArgsMap
*
* @return {Promise} The resolved value will be an object with these
* properties:
* * status: String indicating status of the request. 'succcess' and 'cancel'
* are the possible values.
* * tokens: If status is 'success', this will be the token data in the form
* { accessToken, refreshToken, expireTimeMS }
* * secrets: The clientId and clientSecret used for this oauth.
*/
return function oauth2Fetch(o2Settings, extraQueryObject) {
// Only one auth session at a time.
if (deferred) {
reset('reject', new Error('Multiple oauth calls, starting new one'));
}
p = new Promise(function(res, rej) {
deferred = {
resolve: res,
reject: rej
};
});
oauthSettings = o2Settings;
curOauthSecrets = undefined;
if (o2Settings.clientId && o2Settings.clientSecret) {
curOauthSecrets = {
clientId: o2Settings.clientId,
clientSecret: o2Settings.clientSecret
};
} else if (o2Settings.secretGroup) {
curOauthSecrets = oauthSecrets[o2Settings.secretGroup];
}
if (!curOauthSecrets) {
reset('reject',
new Error('no secrets for group: ' + o2Settings.secretGroup));
return p;
}
var authEndpointQuery = {
client_id: curOauthSecrets.clientId,
// This must match what's in our manifest so we're not considering it
// something that's allowed to change. Because this is currently
// fairly gmail specific with the "installed app" flow, we (must) use
// localhost, but we are maintaining a redirect for our theoretical
// https://email.gaiamobile.org/ domain.
redirect_uri: redirectUri,
response_type: 'code',
scope: o2Settings.scope,
// max_auth_age set to 0 means that google will not keep the user signed
// in after completing the oauth jump. This is important so that if an
// account is deleted, someone else could not then just set up another
// account using the cached sign in cookies.
max_auth_age: 0
};
var url = o2Settings.authEndpoint + '?' +
queryString.fromObject(authEndpointQuery);
if (extraQueryObject) {
var extraArgs = queryString.fromObject(extraQueryObject);
if (extraArgs) {
url += '&' + extraArgs;
}
}
cards.pushCard('setup_oauth2', 'animate', {
url: url,
onBrowserComplete: onBrowserComplete
});
return p;
};
});