Introduce the Proxmox.OAuth2 singleton supporting Google and Microsoft OAuth2. The flow is handled by opening a new window with the authorization URL, and expecting to receive the resulting authorization code from the redirect handler via a BroadcastChannel [0], which allows communication between any two browsing contexts.
[0] https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel Signed-off-by: Arthur Bied-Charreton <[email protected]> --- src/Utils.js | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/src/Utils.js b/src/Utils.js index ed88210..5b951ce 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -1742,6 +1742,105 @@ Ext.define('Proxmox.Utils', { }, }); +Ext.define('Proxmox.OAuth2', { + singleton: true, + + handleGoogleFlow: function (clientId, clientSecret, refreshTokenUrl) { + return this._handleFlow({ + authMethod: 'google-oauth2', + clientId, + clientSecret, + refreshTokenUrl, + authUrl: 'https://accounts.google.com/o/oauth2/v2/auth', + scope: 'https://mail.google.com', + extraAuthParams: { + access_type: 'offline', + prompt: 'consent', + }, + }); + }, + + handleMicrosoftFlow: function (clientId, clientSecret, tenantId, refreshTokenUrl) { + return this._handleFlow({ + authMethod: 'microsoft-oauth2', + tenantId, + clientId, + clientSecret, + refreshTokenUrl, + authUrl: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`, + scope: 'https://outlook.office.com/SMTP.Send offline_access', + extraAuthParams: { + prompt: 'consent', + }, + }); + }, + + _handleFlow: function (config) { + return new Promise((resolve, reject) => { + let redirectUri = window.location.origin; + let channelName = `oauth2_${crypto.randomUUID()}`; + let state = encodeURIComponent(JSON.stringify({ channelName })); + + let authParams = new URLSearchParams({ + client_id: config.clientId, + response_type: 'code', + redirect_uri: redirectUri, + scope: config.scope, + state, + ...config.extraAuthParams, + }); + + let authUrl = `${config.authUrl}?${authParams}`; + + let channel = new BroadcastChannel(channelName); + // Opens OAuth2 authorization window. The app's redirect handler must + // extract the authorization code from the callback URL and send it via + // the BroadcastChannel whose name we passed along as a state parameter. + let popup = window.open(authUrl); + if (!popup) { + reject(gettext('Could not open authorization window')); + return; + } + + channel.addEventListener('message', (event) => { + if (popup && !popup.closed) { + popup.close(); + } + channel.close(); + + let code = event.data.code; + if (!code) { + reject( + gettext('Did not receive any authorization code from authorization window'), + ); + return; + } + + let params = { + 'auth-method': config.authMethod, + 'client-id': config.clientId, + 'client-secret': config.clientSecret, + 'authorization-code': code, + 'redirect-uri': redirectUri, + }; + if (config.tenantId) { + params['tenant-id'] = config.tenantId; + } + + Proxmox.Async.api2({ + url: config.refreshTokenUrl, + method: 'POST', + params, + }) + .then(({ result }) => resolve(result.data)) + .catch((response) => { + reject(response.htmlStatus || gettext('Token exchange failed')); + }); + }); + }); + }, +}); + Ext.define('Proxmox.Async', { singleton: true, -- 2.47.3
