From 9702469d14c61fc7d5f04782ee098c1813715280 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 15 Aug 2025 12:59:58 +0300 Subject: [PATCH] feat: add support for client credentials flow in OAuth2 authorization --- packages/server/src/routes/oauth2/index.ts | 77 +++++++++++++++++-- .../credentials/AddEditCredentialDialog.jsx | 17 ++++ 2 files changed, 87 insertions(+), 7 deletions(-) diff --git a/packages/server/src/routes/oauth2/index.ts b/packages/server/src/routes/oauth2/index.ts index b5c5f571b76..118a85af5bf 100644 --- a/packages/server/src/routes/oauth2/index.ts +++ b/packages/server/src/routes/oauth2/index.ts @@ -77,9 +77,7 @@ router.post('/authorize/:credentialId', async (req: Request, res: Response, next const credentialRepository = appServer.AppDataSource.getRepository(Credential) // Find credential by ID - const credential = await credentialRepository.findOneBy({ - id: credentialId - }) + const credential = await credentialRepository.findOneBy({ id: credentialId }) if (!credential) { return res.status(404).json({ @@ -93,7 +91,9 @@ router.post('/authorize/:credentialId', async (req: Request, res: Response, next const { clientId, + clientSecret, authorizationUrl, + accessTokenUrl, redirect_uri, scope, response_type = 'code', @@ -101,6 +101,9 @@ router.post('/authorize/:credentialId', async (req: Request, res: Response, next additionalParameters = '' } = decryptedData + // Support both "grant_type" and "grantType" + const grantType: string = (decryptedData.grant_type || decryptedData.grantType || '').toString() + if (!clientId) { return res.status(400).json({ success: false, @@ -108,6 +111,69 @@ router.post('/authorize/:credentialId', async (req: Request, res: Response, next }) } + // === Client Credentials flow (no browser redirect) === + if (grantType === 'client_credentials') { + if (!clientSecret) { + return res.status(400).json({ + success: false, + message: 'Missing clientSecret in credential data' + }) + } + if (!accessTokenUrl) { + return res.status(400).json({ + success: false, + message: 'No Access Token URL specified in credential data' + }) + } + + // Build token request + const tokenRequestData: Record = { + client_id: clientId, + client_secret: clientSecret, + grant_type: 'client_credentials' + } + + // Include scope if provided + if (scope) tokenRequestData.scope = scope + + // Also merge additionalParameters (e.g., "scope=https://graph.microsoft.com/.default" or "resource=...") + if (additionalParameters) { + const ap = new URLSearchParams(additionalParameters.toString()) + ap.forEach((value, key) => { + if (!(key in tokenRequestData)) tokenRequestData[key] = value + }) + } + // Exchange for token + const tokenResponse = await axios.post(accessTokenUrl, new URLSearchParams(tokenRequestData).toString(), { + headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' } + }) + + const tokenData = tokenResponse.data + // Update credential data + const updatedCredentialData: any = { + ...decryptedData, + ...tokenData, + token_received_at: new Date().toISOString() + } + if (tokenData.expires_in) { + const expiryTime = new Date(Date.now() + tokenData.expires_in * 1000) + updatedCredentialData.expires_at = expiryTime.toISOString() + } + + const encryptedData = await encryptCredentialData(updatedCredentialData) + await credentialRepository.update(credential.id, { encryptedData, updatedDate: new Date() }) + + // For client_credentials we return success immediately (no redirect URL) + return res.json({ + success: true, + message: 'Client credentials token obtained successfully', + credentialId, + authorizationUrl: '#', + redirectUri: '#' + }) + } + + // === Authorization Code flow (original behavior) === if (!authorizationUrl) { return res.status(400).json({ success: false, @@ -126,12 +192,9 @@ router.post('/authorize/:credentialId', async (req: Request, res: Response, next redirect_uri: finalRedirectUri }) - if (scope) { - authParams.append('scope', scope) - } + if (scope) authParams.append('scope', scope) let fullAuthorizationUrl = `${authorizationUrl}?${authParams.toString()}` - if (additionalParameters) { fullAuthorizationUrl += `&${additionalParameters.toString()}` } diff --git a/packages/ui/src/views/credentials/AddEditCredentialDialog.jsx b/packages/ui/src/views/credentials/AddEditCredentialDialog.jsx index fecdb30a74f..317876f2b66 100644 --- a/packages/ui/src/views/credentials/AddEditCredentialDialog.jsx +++ b/packages/ui/src/views/credentials/AddEditCredentialDialog.jsx @@ -257,6 +257,23 @@ const AddEditCredentialDialog = ({ show, dialogProps, onCancel, onConfirm, setEr const authResponse = await oauth2Api.authorize(credentialId) if (authResponse.data && authResponse.data.success && authResponse.data.authorizationUrl) { + // If the authorization URL is '#', it means the OAuth2 flow is complete + if (authResponse.data.authorizationUrl === '#') { + enqueueSnackbar({ + message: 'OAuth2 authorization completed successfully', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + onConfirm(credentialId) + return + } // Open the authorization URL in a new window/tab const authWindow = window.open( authResponse.data.authorizationUrl,