[PATCH proxmox-widget-toolkit v4 11/24] utils: Add OAuth2 flow handlers

Arthur Bied-Charreton
2026-04-21T12:02:07Z

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 <a.bied-charreton@proxmox.com>
---
src/Utils.js | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 91 insertions(+)

diff --git a/src/Utils.js b/src/Utils.js
index 5457ffa..ceed4c8 100644
--- a/src/Utils.js
+++ b/src/Utils.js
@@ -1723,6 +1723,97 @@ Ext.define('Proxmox.Utils', {
},
});

+Ext.define('Proxmox.OAuth2', {
+ singleton: true,
+
+ handleGoogleFlow: function (clientId, clientSecret) {
+ return this._handleFlow({
+ clientId,
+ clientSecret,
+ authUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
+ tokenUrl: 'https://oauth2.googleapis.com/token',
+ scope: 'https://mail.google.com',
+ extraAuthParams: {
+ access_type: 'offline',
+ prompt: 'consent',
+ },
+ });
+ },
+
+ handleMicrosoftFlow: function (clientId, clientSecret, tenantId) {
+ return this._handleFlow({
+ clientId,
+ clientSecret,
+ authUrl: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`,
+ tokenUrl: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
+ 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}`;
+
+ // 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 channel = new BroadcastChannel(channelName);
+ let popup = window.open(authUrl);
+ if (!popup) {
+ reject(new Error(gettext('Could not open authentication window')));
+ return;
+ }
+
+ channel.addEventListener('message', async (event) => {
+ if (popup && !popup.closed) {
+ popup.close();
+ }
+ channel.close();
+
+ try {
+ let response = await fetch(config.tokenUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ body: new URLSearchParams({
+ grant_type: 'authorization_code',
+ code: event.data.code,
+ client_id: config.clientId,
+ client_secret: config.clientSecret,
+ redirect_uri: redirectUri,
+ }),
+ });
+
+ let tokens = await response.json();
+ if (!tokens.refresh_token) {
+ reject(tokens.error_description || gettext('Token exchange failed'));
+ }
+ resolve(tokens.refresh_token);
+ } catch (error) {
+ reject(error);
+ }
+ });
+ });
+ },
+});
+
Ext.define('Proxmox.Async', {
singleton: true,

--
2.47.3