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



Reply via email to