From a68833089f902ba25abca59bf0b952d37aeb0b59 Mon Sep 17 00:00:00 2001 From: naman-bruno Date: Fri, 11 Jul 2025 13:55:03 +0530 Subject: [PATCH] Feat: OAuth2 implicit grant type (#4307) * add: implicit grant type --- .../CollectionSettings/Auth/OAuth2/index.js | 4 + .../components/FolderSettings/Auth/index.js | 3 + .../Auth/OAuth2/GrantTypeSelector/index.js | 9 + .../Auth/OAuth2/Implicit/StyledWrapper.js | 61 ++++ .../RequestPane/Auth/OAuth2/Implicit/index.js | 281 ++++++++++++++++++ .../Auth/OAuth2/Implicit/inputsConfig.js | 24 ++ .../Auth/OAuth2/Oauth2ActionButtons/index.js | 48 ++- .../RequestPane/Auth/OAuth2/index.js | 4 + .../bruno-app/src/utils/collections/index.js | 44 ++- packages/bruno-electron/src/ipc/collection.js | 56 +++- .../ipc/network/authorize-user-in-window.js | 28 +- .../bruno-electron/src/ipc/network/index.js | 19 +- .../src/ipc/network/interpolate-vars.js | 12 + .../src/ipc/network/prepare-request.js | 30 ++ packages/bruno-electron/src/utils/oauth2.js | 264 +++++++++++++++- packages/bruno-lang/v2/src/bruToJson.js | 14 + .../bruno-lang/v2/src/collectionBruToJson.js | 14 + packages/bruno-lang/v2/src/jsonToBru.js | 19 ++ .../bruno-lang/v2/src/jsonToCollectionBru.js | 19 ++ .../bruno-schema/src/collections/index.js | 22 +- 20 files changed, 919 insertions(+), 56 deletions(-) create mode 100644 packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Implicit/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Implicit/index.js create mode 100644 packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Implicit/inputsConfig.js diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/index.js index 474e44717..e8d20f25c 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/index.js @@ -7,6 +7,7 @@ import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/in import { useDispatch } from 'react-redux'; import OAuth2PasswordCredentials from 'components/RequestPane/Auth/OAuth2/PasswordCredentials/index'; import OAuth2ClientCredentials from 'components/RequestPane/Auth/OAuth2/ClientCredentials/index'; +import OAuth2Implicit from 'components/RequestPane/Auth/OAuth2/Implicit/index'; import GrantTypeSelector from 'components/RequestPane/Auth/OAuth2/GrantTypeSelector/index'; const GrantTypeComponentMap = ({collection }) => { @@ -29,6 +30,9 @@ const GrantTypeComponentMap = ({collection }) => { case 'client_credentials': return ; break; + case 'implicit': + return ; + break; default: return
TBD
; break; diff --git a/packages/bruno-app/src/components/FolderSettings/Auth/index.js b/packages/bruno-app/src/components/FolderSettings/Auth/index.js index 89b109f82..cbdbba57d 100644 --- a/packages/bruno-app/src/components/FolderSettings/Auth/index.js +++ b/packages/bruno-app/src/components/FolderSettings/Auth/index.js @@ -7,6 +7,7 @@ import { updateFolderAuth } from 'providers/ReduxStore/slices/collections'; import { useDispatch } from 'react-redux'; import OAuth2PasswordCredentials from 'components/RequestPane/Auth/OAuth2/PasswordCredentials/index'; import OAuth2ClientCredentials from 'components/RequestPane/Auth/OAuth2/ClientCredentials/index'; +import OAuth2Implicit from 'components/RequestPane/Auth/OAuth2/Implicit/index'; import GrantTypeSelector from 'components/RequestPane/Auth/OAuth2/GrantTypeSelector/index'; import AuthMode from '../AuthMode'; import BasicAuth from 'components/RequestPane/Auth/BasicAuth'; @@ -35,6 +36,8 @@ const GrantTypeComponentMap = ({ collection, folder }) => { return ; case 'client_credentials': return ; + case 'implicit': + return ; default: return
TBD
; } diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/GrantTypeSelector/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/GrantTypeSelector/index.js index e468845e4..f89aa9579 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/GrantTypeSelector/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/GrantTypeSelector/index.js @@ -101,6 +101,15 @@ const GrantTypeSelector = ({ item = {}, request, updateAuth, collection }) => { > Authorization Code +
{ + dropdownTippyRef.current.hide(); + onGrantTypeChange('implicit'); + }} + > + Implicit +
{ diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Implicit/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Implicit/StyledWrapper.js new file mode 100644 index 000000000..273806001 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Implicit/StyledWrapper.js @@ -0,0 +1,61 @@ +import styled from 'styled-components'; + +const Wrapper = styled.div` + label { + font-size: 0.8125rem; + } + .oauth2-input-wrapper { + max-width: 400px; + padding: 0.15rem 0.4rem; + border-radius: 3px; + border: solid 1px ${(props) => props.theme.input.border}; + background-color: ${(props) => props.theme.input.bg}; + } + + .token-placement-selector { + padding: 0.5rem 0px; + border-radius: 3px; + border: solid 1px ${(props) => props.theme.input.border}; + background-color: ${(props) => props.theme.input.bg}; + min-width: 100px; + + .dropdown { + width: fit-content; + min-width: 100px; + + div[data-tippy-root] { + width: fit-content; + min-width: 100px; + } + .tippy-box { + width: fit-content; + max-width: none !important; + min-width: 100px; + + .tippy-content { + width: fit-content; + max-width: none !important; + min-width: 100px; + } + } + } + + .token-placement-label { + width: fit-content; + justify-content: space-between; + padding: 0 0.5rem; + min-width: 100px; + } + + .dropdown-item { + padding: 0.2rem 0.6rem !important; + } + } + + .checkbox-label { + color: ${(props) => props.theme.colors.text.primary}; + user-select: none; + } +`; + +export default Wrapper; \ No newline at end of file diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Implicit/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Implicit/index.js new file mode 100644 index 000000000..275e6b040 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Implicit/index.js @@ -0,0 +1,281 @@ +import React, { useRef, forwardRef, useState, useMemo } from 'react'; +import get from 'lodash/get'; +import { useTheme } from 'providers/Theme'; +import { useDispatch } from 'react-redux'; +import { IconCaretDown, IconLoader2, IconSettings, IconKey, IconHelp, IconAdjustmentsHorizontal } from '@tabler/icons'; +import Dropdown from 'components/Dropdown'; +import SingleLineEditor from 'components/SingleLineEditor'; +import { clearOauth2Cache, fetchOauth2Credentials } from 'providers/ReduxStore/slices/collections/actions'; +import Wrapper from './StyledWrapper'; +import { inputsConfig } from './inputsConfig'; +import toast from 'react-hot-toast'; +import Oauth2TokenViewer from '../Oauth2TokenViewer/index'; +import { cloneDeep } from 'lodash'; +import { getAllVariables } from 'utils/collections/index'; +import { interpolate } from '@usebruno/common'; + +const OAuth2Implicit = ({ save, item = {}, request, handleRun, updateAuth, collection, folder }) => { + const dispatch = useDispatch(); + const { storedTheme } = useTheme(); + const dropdownTippyRef = useRef(); + const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); + const [fetchingToken, toggleFetchingToken] = useState(false); + + const oAuth = get(request, 'auth.oauth2', {}); + const { + callbackUrl, + authorizationUrl, + clientId, + scope, + state, + credentialsId, + tokenPlacement, + tokenHeaderPrefix, + tokenQueryKey, + autoFetchToken + } = oAuth; + + const interpolatedAuthUrl = useMemo(() => { + const variables = getAllVariables(collection, item); + return interpolate(authorizationUrl, variables); + }, [collection, item, authorizationUrl]); + + const TokenPlacementIcon = forwardRef((props, ref) => { + return ( +
+ {tokenPlacement == 'url' ? 'URL' : 'Headers'} + +
+ ); + }); + + const handleFetchOauth2Credentials = async () => { + let requestCopy = cloneDeep(request); + requestCopy.oauth2 = requestCopy?.auth.oauth2; + requestCopy.headers = {}; + toggleFetchingToken(true); + try { + const result = await dispatch(fetchOauth2Credentials({ + itemUid: item.uid, + request: requestCopy, + collection, + folderUid: folder?.uid || null, + forceGetToken: true + })); + + toggleFetchingToken(false); + + // Check if the result contains error or if access_token is missing + if (result?.error || !result?.access_token) { + const errorMessage = result?.error || 'No access token received from authorization server'; + toast.error(errorMessage); + return; + } + + toast.success('Token fetched successfully!'); + } + catch (error) { + console.error(error); + toggleFetchingToken(false); + toast.error(error?.message || 'An error occurred while fetching token!'); + } + } + + const handleSave = () => { save(); }; + + const handleChange = (key, value) => { + dispatch( + updateAuth({ + mode: 'oauth2', + collectionUid: collection.uid, + itemUid: item.uid, + content: { + grantType: 'implicit', + callbackUrl, + authorizationUrl, + clientId, + state, + scope, + credentialsId, + tokenPlacement, + tokenHeaderPrefix, + tokenQueryKey, + autoFetchToken, + [key]: value, + } + }) + ); + }; + + const handleAutoFetchTokenToggle = (e) => { + handleChange('autoFetchToken', e.target.checked); + }; + + const handleClearCache = (e) => { + dispatch(clearOauth2Cache({ collectionUid: collection?.uid, url: interpolatedAuthUrl, credentialsId })) + .then(() => { + toast.success('cleared cache successfully'); + }) + .catch((err) => { + toast.error(err.message); + }); + }; + + return ( + + +
+
+ +
+ + Configuration + +
+ {inputsConfig.map((input) => { + const { key, label, isSecret } = input; + return ( +
+ +
+ handleChange(key, val)} + onRun={handleRun} + collection={collection} + item={item} + isSecret={isSecret} + /> +
+
+ ); + })} + +
+
+ +
+ + Token + +
+ +
+ +
+ handleChange('credentialsId', val)} + onRun={handleRun} + collection={collection} + item={item} + /> +
+
+ +
+ +
+ } placement="bottom-end"> +
{ + dropdownTippyRef.current.hide(); + handleChange('tokenPlacement', 'header'); + }} + > + Headers +
+
{ + dropdownTippyRef.current.hide(); + handleChange('tokenPlacement', 'url'); + }} + > + URL +
+
+
+
+ + {tokenPlacement == 'header' ? ( +
+ +
+ handleChange('tokenHeaderPrefix', val)} + onRun={handleRun} + collection={collection} + item={item} + /> +
+
+ ) : ( +
+ +
+ handleChange('tokenQueryKey', val)} + onRun={handleRun} + collection={collection} + item={item} + /> +
+
+ )} + +
+
+ +
+ + Advanced Options + +
+ +
+ + +
+
+ + + Automatically fetch a new token when the current one expires. + +
+
+
+ +
+ + +
+
+ ); +}; + +export default OAuth2Implicit; \ No newline at end of file diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Implicit/inputsConfig.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Implicit/inputsConfig.js new file mode 100644 index 000000000..86040b838 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Implicit/inputsConfig.js @@ -0,0 +1,24 @@ +const inputsConfig = [ + { + key: 'callbackUrl', + label: 'Callback URL' + }, + { + key: 'authorizationUrl', + label: 'Authorization URL' + }, + { + key: 'clientId', + label: 'Client ID' + }, + { + key: 'scope', + label: 'Scope' + }, + { + key: 'state', + label: 'State' + } +]; + +export { inputsConfig }; \ No newline at end of file diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Oauth2ActionButtons/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Oauth2ActionButtons/index.js index 7b45f03ea..ee38fa1f2 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Oauth2ActionButtons/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Oauth2ActionButtons/index.js @@ -28,20 +28,30 @@ const Oauth2ActionButtons = ({ item, request, collection, url: accessTokenUrl, c requestCopy.headers = {}; toggleFetchingToken(true); try { - const credentials = await dispatch(fetchOauth2Credentials({ itemUid: item.uid, request: requestCopy, collection })); + const result = await dispatch(fetchOauth2Credentials({ + itemUid: item.uid, + request: requestCopy, + collection, + forceGetToken: true + })); + toggleFetchingToken(false); - if (credentials?.access_token) { - toast.success('token fetched successfully!'); - } - else { - toast.error('An error occurred while fetching token!'); + + // Check if the result contains error or if access_token is missing + if (!result || !result.access_token) { + const errorMessage = result?.error || 'No access token received from authorization server'; + console.error(errorMessage); + toast.error(errorMessage); + return; } + + toast.success('Token fetched successfully!'); } catch (error) { console.error('could not fetch the token!'); console.error(error); toggleFetchingToken(false); - toast.error('An error occurred while fetching token!'); + toast.error(error?.message || 'An error occurred while fetching token!'); } } @@ -51,19 +61,29 @@ const Oauth2ActionButtons = ({ item, request, collection, url: accessTokenUrl, c requestCopy.headers = {}; toggleRefreshingToken(true); try { - const credentials = await dispatch(refreshOauth2Credentials({ itemUid: item.uid, request: requestCopy, collection })); + const result = await dispatch(refreshOauth2Credentials({ + itemUid: item.uid, + request: requestCopy, + collection, + forceGetToken: true + })); + toggleRefreshingToken(false); - if (credentials?.access_token) { - toast.success('token refreshed successfully!'); - } - else { - toast.error('An error occurred while refreshing token!'); + + // Check if the result contains error or if access_token is missing + if (!result || !result.access_token) { + const errorMessage = result?.error || 'No access token received from authorization server'; + console.error(errorMessage); + toast.error(errorMessage); + return; } + + toast.success('Token refreshed successfully!'); } catch(error) { console.error(error); toggleRefreshingToken(false); - toast.error('An error occurred while refreshing token!'); + toast.error(error?.message || 'An error occurred while refreshing token!'); } }; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/index.js index bbee9d4f5..98b435f1d 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/index.js @@ -4,6 +4,7 @@ import StyledWrapper from './StyledWrapper'; import GrantTypeSelector from './GrantTypeSelector/index'; import OAuth2PasswordCredentials from './PasswordCredentials/index'; import OAuth2AuthorizationCode from './AuthorizationCode/index'; +import OAuth2Implicit from './Implicit/index'; import OAuth2ClientCredentials from './ClientCredentials/index'; import { updateAuth } from 'providers/ReduxStore/slices/collections'; import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions'; @@ -31,6 +32,9 @@ const GrantTypeComponentMap = ({ item, collection }) => { case 'authorization_code': return ; break; + case 'implicit': + return ; + break; case 'client_credentials': return ; break; diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index acb867ebe..3f391c6e3 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -340,6 +340,21 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {} autoRefreshToken: get(si.request, 'auth.oauth2.autoRefreshToken', true), }; break; + case 'implicit': + di.request.auth.oauth2 = { + grantType: grantType, + callbackUrl: get(si.request, 'auth.oauth2.callbackUrl', ''), + authorizationUrl: get(si.request, 'auth.oauth2.authorizationUrl', ''), + clientId: get(si.request, 'auth.oauth2.clientId', ''), + scope: get(si.request, 'auth.oauth2.scope', ''), + state: get(si.request, 'auth.oauth2.state', ''), + credentialsId: get(si.request, 'auth.oauth2.credentialsId', 'credentials'), + tokenPlacement: get(si.request, 'auth.oauth2.tokenPlacement', 'header'), + tokenHeaderPrefix: get(si.request, 'auth.oauth2.tokenHeaderPrefix', 'Bearer'), + tokenQueryKey: get(si.request, 'auth.oauth2.tokenQueryKey', ''), + autoFetchToken: get(si.request, 'auth.oauth2.autoFetchToken', true), + }; + break; case 'client_credentials': di.request.auth.oauth2 = { grantType: grantType, @@ -710,23 +725,22 @@ export const humanizeRequestAPIKeyPlacement = (placement) => { }; export const humanizeGrantType = (mode) => { - let label = 'No Auth'; - switch (mode) { - case 'password': { - label = 'Password Credentials'; - break; - } - case 'authorization_code': { - label = 'Authorization Code'; - break; - } - case 'client_credentials': { - label = 'Client Credentials'; - break; - } + if (!mode || typeof mode !== 'string') { + return ''; } - return label; + switch (mode) { + case 'password': + return 'Password Credentials'; + case 'authorization_code': + return 'Authorization Code'; + case 'client_credentials': + return 'Client Credentials'; + case 'implicit': + return 'Implicit'; + default: + return mode; + } }; export const refreshUidsInItem = (item) => { diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index f76590768..96322e4b4 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -40,7 +40,7 @@ const UiStateSnapshotStore = require('../store/ui-state-snapshot'); const interpolateVars = require('./network/interpolate-vars'); const { getEnvVars, getTreePathFromCollectionToItem, mergeVars, parseBruFileMeta, hydrateRequestWithUuid, transformRequestToSaveToFilesystem } = require('../utils/collection'); const { getProcessEnvVars } = require('../store/process-env'); -const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials, refreshOauth2Token } = require('../utils/oauth2'); +const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials, getOAuth2TokenUsingImplicitGrant, refreshOauth2Token } = require('../utils/oauth2'); const { getCertsAndProxyConfig } = require('./network'); const collectionWatcher = require('../app/collection-watcher'); @@ -994,22 +994,59 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection collectionPath }); const { oauth2: { grantType }} = requestCopy || {}; - let credentials, url, credentialsId; + + const handleOAuth2Response = (response) => { + if (response.error && !response.debugInfo) { + throw new Error(response.error); + } + return response; + }; + switch (grantType) { case 'authorization_code': interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); - ({ credentials, url, credentialsId, debugInfo } = await getOAuth2TokenUsingAuthorizationCode({ request: requestCopy, collectionUid, forceFetch: true, certsAndProxyConfig })); - break; + return await getOAuth2TokenUsingAuthorizationCode({ + request: requestCopy, + collectionUid, + forceFetch: true, + certsAndProxyConfig + }).then(handleOAuth2Response); + case 'client_credentials': interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); - ({ credentials, url, credentialsId, debugInfo } = await getOAuth2TokenUsingClientCredentials({ request: requestCopy, collectionUid, forceFetch: true, certsAndProxyConfig })); - break; + return await getOAuth2TokenUsingClientCredentials({ + request: requestCopy, + collectionUid, + forceFetch: true, + certsAndProxyConfig + }).then(handleOAuth2Response); + case 'password': interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); - ({ credentials, url, credentialsId, debugInfo } = await getOAuth2TokenUsingPasswordCredentials({ request: requestCopy, collectionUid, forceFetch: true, certsAndProxyConfig })); - break; + return await getOAuth2TokenUsingPasswordCredentials({ + request: requestCopy, + collectionUid, + forceFetch: true, + certsAndProxyConfig + }).then(handleOAuth2Response); + + case 'implicit': + interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); + return await getOAuth2TokenUsingImplicitGrant({ + request: requestCopy, + collectionUid, + forceFetch: true + }).then(handleOAuth2Response); + + default: + return { + error: `Unsupported grant type: ${grantType}`, + credentials: null, + url: null, + collectionUid, + credentialsId: null + }; } - return { credentials, url, collectionUid, credentialsId, debugInfo }; } } catch (error) { return Promise.reject(error); @@ -1082,6 +1119,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection processEnvVars, collectionPath }); + let { credentials, url, credentialsId, debugInfo } = await refreshOauth2Token({ requestCopy, collectionUid, certsAndProxyConfig }); return { credentials, url, collectionUid, credentialsId, debugInfo }; } diff --git a/packages/bruno-electron/src/ipc/network/authorize-user-in-window.js b/packages/bruno-electron/src/ipc/network/authorize-user-in-window.js index 7d2e23abc..0cf45d70c 100644 --- a/packages/bruno-electron/src/ipc/network/authorize-user-in-window.js +++ b/packages/bruno-electron/src/ipc/network/authorize-user-in-window.js @@ -5,7 +5,7 @@ const matchesCallbackUrl = (url, callbackUrl) => { return url ? url.href.startsWith(callbackUrl.href) : false; }; -const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session }) => { +const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session, grantType = 'authorization_code' }) => { return new Promise(async (resolve, reject) => { let finalUrl = null; let debugInfo = { @@ -166,9 +166,29 @@ const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session }) => { if (finalUrl) { try { - const callbackUrlWithCode = new URL(finalUrl); - const authorizationCode = callbackUrlWithCode.searchParams.get('code'); - return resolve({ authorizationCode, debugInfo }); + // Handle different grant types differently + if (grantType === 'implicit') { + // For implicit flow, tokens are in the URL hash fragment + const urlWithHash = new URL(finalUrl); + const hash = urlWithHash.hash.substring(1); // Remove the leading # + const hashParams = new URLSearchParams(hash); + + // Extract tokens from hash fragment + const implicitTokens = { + access_token: hashParams.get('access_token'), + token_type: hashParams.get('token_type'), + expires_in: hashParams.get('expires_in'), + state: hashParams.get('state'), + scope: hashParams.get('scope') + }; + + return resolve({ implicitTokens, debugInfo }); + } else { + // Default case - authorization code flow + const callbackUrlWithCode = new URL(finalUrl); + const authorizationCode = callbackUrlWithCode.searchParams.get('code'); + return resolve({ authorizationCode, debugInfo }); + } } catch (error) { return reject(error); } diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 21ae36d2b..4f17c06cf 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -25,7 +25,8 @@ const { chooseFileToSave, writeBinaryFile, writeFile } = require('../../utils/fi const { addCookieToJar, getDomainsWithCookies, getCookieStringForUrl } = require('../../utils/cookies'); const { createFormData } = require('../../utils/form-data'); const { findItemInCollectionByPathname, sortFolder, getAllRequestsInFolderRecursively, getEnvVars, getTreePathFromCollectionToItem, mergeVars } = require('../../utils/collection'); -const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials } = require('../../utils/oauth2'); +const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials, getOAuth2TokenUsingImplicitGrant } = require('../../utils/oauth2'); +const { setupProxyAgents } = require('../../utils/proxy-util'); const { preferencesUtil } = require('../../store/preferences'); const { getProcessEnvVars } = require('../../store/process-env'); const { getBrunoConfig } = require('../../store/bruno-config'); @@ -218,6 +219,22 @@ const configureRequest = async ( catch(error) {} } break; + case 'implicit': + interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); + ({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingImplicitGrant({ request: requestCopy, collectionUid })); + request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid }; + if (tokenPlacement == 'header') { + request.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials?.access_token}`; + } + else { + try { + const url = new URL(request.url); + url?.searchParams?.set(tokenQueryKey, credentials?.access_token); + request.url = url?.toString(); + } + catch(error) {} + } + break; case 'client_credentials': interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); ({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingClientCredentials({ request: requestCopy, collectionUid, certsAndProxyConfig })); diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index a7206ec22..5d80cf567 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -188,6 +188,18 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc request.oauth2.autoFetchToken = _interpolate(request.oauth2.autoFetchToken); request.oauth2.autoRefreshToken = _interpolate(request.oauth2.autoRefreshToken); break; + case 'implicit': + request.oauth2.callbackUrl = _interpolate(request.oauth2.callbackUrl) || ''; + request.oauth2.authorizationUrl = _interpolate(request.oauth2.authorizationUrl) || ''; + request.oauth2.clientId = _interpolate(request.oauth2.clientId) || ''; + request.oauth2.scope = _interpolate(request.oauth2.scope) || ''; + request.oauth2.state = _interpolate(request.oauth2.state) || ''; + request.oauth2.credentialsId = _interpolate(request.oauth2.credentialsId) || ''; + request.oauth2.tokenPlacement = _interpolate(request.oauth2.tokenPlacement) || ''; + request.oauth2.tokenHeaderPrefix = _interpolate(request.oauth2.tokenHeaderPrefix) || ''; + request.oauth2.tokenQueryKey = _interpolate(request.oauth2.tokenQueryKey) || ''; + request.oauth2.autoFetchToken = _interpolate(request.oauth2.autoFetchToken); + break; case 'authorization_code': request.oauth2.callbackUrl = _interpolate(request.oauth2.callbackUrl) || ''; request.oauth2.authorizationUrl = _interpolate(request.oauth2.authorizationUrl) || ''; diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 8f2e89f0d..e51192f3b 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -111,6 +111,21 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { autoRefreshToken: get(collectionAuth, 'oauth2.autoRefreshToken') }; break; + case 'implicit': + axiosRequest.oauth2 = { + grantType: grantType, + callbackUrl: get(collectionAuth, 'oauth2.callbackUrl'), + authorizationUrl: get(collectionAuth, 'oauth2.authorizationUrl'), + clientId: get(collectionAuth, 'oauth2.clientId'), + scope: get(collectionAuth, 'oauth2.scope'), + state: get(collectionAuth, 'oauth2.state'), + credentialsId: get(collectionAuth, 'oauth2.credentialsId'), + tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'), + tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'), + tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey'), + autoFetchToken: get(collectionAuth, 'oauth2.autoFetchToken') + }; + break; case 'client_credentials': axiosRequest.oauth2 = { grantType: grantType, @@ -209,6 +224,21 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { autoRefreshToken: get(request, 'auth.oauth2.autoRefreshToken') }; break; + case 'implicit': + axiosRequest.oauth2 = { + grantType: grantType, + callbackUrl: get(request, 'auth.oauth2.callbackUrl'), + authorizationUrl: get(request, 'auth.oauth2.authorizationUrl'), + clientId: get(request, 'auth.oauth2.clientId'), + scope: get(request, 'auth.oauth2.scope'), + state: get(request, 'auth.oauth2.state'), + credentialsId: get(request, 'auth.oauth2.credentialsId'), + tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'), + tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'), + tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey'), + autoFetchToken: get(request, 'auth.oauth2.autoFetchToken') + }; + break; case 'client_credentials': axiosRequest.oauth2 = { grantType: grantType, diff --git a/packages/bruno-electron/src/utils/oauth2.js b/packages/bruno-electron/src/utils/oauth2.js index dd8594728..d288f8a45 100644 --- a/packages/bruno-electron/src/utils/oauth2.js +++ b/packages/bruno-electron/src/utils/oauth2.js @@ -67,6 +67,44 @@ const getOAuth2TokenUsingAuthorizationCode = async ({ request, collectionUid, fo autoFetchToken, } = oAuth; const url = requestCopy?.oauth2?.accessTokenUrl; + + // Validate required fields + if (!authorizationUrl) { + return { + error: 'Authorization URL is required for OAuth2 authorization code flow', + credentials: null, + url, + credentialsId + }; + } + + if (!url) { + return { + error: 'Access Token URL is required for OAuth2 authorization code flow', + credentials: null, + url: authorizationUrl, + credentialsId + }; + } + + if (!callbackUrl) { + return { + error: 'Callback URL is required for OAuth2 authorization code flow', + credentials: null, + url, + credentialsId + }; + } + + if (!clientId) { + return { + error: 'Client ID is required for OAuth2 authorization code flow', + credentials: null, + url, + credentialsId + }; + } + if (!forceFetch) { const storedCredentials = getStoredOauth2Credentials({ collectionUid, url, credentialsId }); @@ -279,6 +317,34 @@ const getOAuth2TokenUsingClientCredentials = async ({ request, collectionUid, fo const url = requestCopy?.oauth2?.accessTokenUrl; + // Validate required fields + if (!url) { + return { + error: 'Access Token URL is required for OAuth2 client credentials flow', + credentials: null, + url, + credentialsId + }; + } + + if (!clientId) { + return { + error: 'Client ID is required for OAuth2 client credentials flow', + credentials: null, + url, + credentialsId + }; + } + + if (!clientSecret) { + return { + error: 'Client Secret is required for OAuth2 client credentials flow', + credentials: null, + url, + credentialsId + }; + } + if (!forceFetch) { const storedCredentials = getStoredOauth2Credentials({ collectionUid, url, credentialsId }); @@ -447,6 +513,43 @@ const getOAuth2TokenUsingPasswordCredentials = async ({ request, collectionUid, } = oAuth; const url = requestCopy?.oauth2?.accessTokenUrl; + // Validate required fields + if (!url) { + return { + error: 'Access Token URL is required for OAuth2 password credentials flow', + credentials: null, + url, + credentialsId + }; + } + + if (!username) { + return { + error: 'Username is required for OAuth2 password credentials flow', + credentials: null, + url, + credentialsId + }; + } + + if (!password) { + return { + error: 'Password is required for OAuth2 password credentials flow', + credentials: null, + url, + credentialsId + }; + } + + if (!clientId) { + return { + error: 'Client ID is required for OAuth2 password credentials flow', + credentials: null, + url, + credentialsId + }; + } + if (!forceFetch) { const storedCredentials = getStoredOauth2Credentials({ collectionUid, url, credentialsId }); @@ -726,10 +829,167 @@ const generateCodeChallenge = (codeVerifier) => { return base64Hash; }; +const getOAuth2TokenUsingImplicitGrant = async ({ request, collectionUid, forceFetch = false }) => { + const { oauth2 = {} } = request; + const { + authorizationUrl, + clientId, + scope, + state = '', + callbackUrl, + credentialsId = 'credentials', + autoFetchToken = true + } = oauth2; + + // Validate required fields + if (!authorizationUrl) { + return { + error: 'Authorization URL is required for OAuth2 implicit flow', + credentials: null, + url: authorizationUrl, + credentialsId + }; + } + + if (!callbackUrl) { + return { + error: 'Callback URL is required for OAuth2 implicit flow', + credentials: null, + url: authorizationUrl, + credentialsId + }; + } + + // Check if we already have valid credentials + if (!forceFetch) { + try { + const storedCredentials = getStoredOauth2Credentials({ + collectionUid, + url: authorizationUrl, + credentialsId + }); + + if (storedCredentials) { + // Token exists + if (!isTokenExpired(storedCredentials)) { + // Token is valid, use it + return { + collectionUid, + credentials: storedCredentials, + url: authorizationUrl, + credentialsId + }; + } else { + // Token is expired - unlike other grant types, implicit flow doesn't support refresh tokens + if (autoFetchToken) { + // Proceed to fetch new token + clearOauth2Credentials({ collectionUid, url: authorizationUrl, credentialsId }); + } else { + // Proceed with expired token + return { + collectionUid, + credentials: storedCredentials, + url: authorizationUrl, + credentialsId + }; + } + } + } else { + // No stored credentials + if (!autoFetchToken) { + // Don't fetch token if autoFetchToken is disabled + return { + collectionUid, + credentials: null, + url: authorizationUrl, + credentialsId + }; + } + // Otherwise proceed to fetch new token + } + } catch (error) { + console.error('Error retrieving oauth2 credentials from cache', error); + clearOauth2Credentials({ collectionUid, url: authorizationUrl, credentialsId }); + } + } + + const authorizationUrlWithQueryParams = new URL(authorizationUrl); + authorizationUrlWithQueryParams.searchParams.append('response_type', 'token'); + authorizationUrlWithQueryParams.searchParams.append('client_id', clientId); + authorizationUrlWithQueryParams.searchParams.append('redirect_uri', callbackUrl); + if (scope) { + authorizationUrlWithQueryParams.searchParams.append('scope', scope); + } + if (state) { + authorizationUrlWithQueryParams.searchParams.append('state', state); + } + + const authorizeUrl = authorizationUrlWithQueryParams.toString(); + + try { + const { implicitTokens, debugInfo } = await authorizeUserInWindow({ + authorizeUrl, + callbackUrl, + session: oauth2Store.getSessionIdOfCollection({ collectionUid, url: authorizationUrl }), + grantType: 'implicit' + }); + + if (!implicitTokens || !implicitTokens.access_token) { + return { + error: 'No access token received from authorization server', + credentials: null, + url: authorizationUrl, + credentialsId, + debugInfo + }; + } + + const credentials = { + access_token: implicitTokens.access_token, + token_type: implicitTokens.token_type || 'Bearer', + state: implicitTokens.state || '', + ...(implicitTokens.expires_in ? { expires_in: parseInt(implicitTokens.expires_in) } : {}), + created_at: Date.now() + }; + + if (implicitTokens.scope) { + credentials.scope = implicitTokens.scope; + } + + // Store the credentials + persistOauth2Credentials({ + collectionUid, + url: authorizationUrl, + credentials, + credentialsId + }); + + return { + collectionUid, + credentials, + url: authorizationUrl, + credentialsId, + debugInfo + }; + } catch (error) { + return { + error: error.message || 'Failed to obtain token', + credentials: null, + url: authorizationUrl, + credentialsId + }; + } +}; + module.exports = { + persistOauth2Credentials, + clearOauth2Credentials, + getStoredOauth2Credentials, getOAuth2TokenUsingAuthorizationCode, - getOAuth2AuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials, - refreshOauth2Token + getOAuth2TokenUsingImplicitGrant, + refreshOauth2Token, + generateCodeVerifier, + generateCodeChallenge }; \ No newline at end of file diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index e99b690c2..844e21e79 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -584,6 +584,20 @@ const sem = grammar.createSemantics().addAttribute('ast', { autoFetchToken: autoFetchTokenKey ? safeParseJson(autoFetchTokenKey?.value) ?? true : true, autoRefreshToken: autoRefreshTokenKey ? safeParseJson(autoRefreshTokenKey?.value) ?? false : false } + : grantTypeKey?.value && grantTypeKey?.value == 'implicit' + ? { + grantType: grantTypeKey ? grantTypeKey.value : '', + callbackUrl: callbackUrlKey ? callbackUrlKey.value : '', + authorizationUrl: authorizationUrlKey ? authorizationUrlKey.value : '', + clientId: clientIdKey ? clientIdKey.value : '', + scope: scopeKey ? scopeKey.value : '', + state: stateKey ? stateKey.value : '', + credentialsId: credentialsIdKey?.value ? credentialsIdKey.value : 'credentials', + tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header', + tokenHeaderPrefix: tokenHeaderPrefixKey?.value ? tokenHeaderPrefixKey.value : '', + tokenQueryKey: tokenQueryKeyKey?.value ? tokenQueryKeyKey.value : 'access_token', + autoFetchToken: autoFetchTokenKey ? safeParseJson(autoFetchTokenKey?.value) ?? true : true, + } : {} } }; diff --git a/packages/bruno-lang/v2/src/collectionBruToJson.js b/packages/bruno-lang/v2/src/collectionBruToJson.js index 73f5af1a8..d50926014 100644 --- a/packages/bruno-lang/v2/src/collectionBruToJson.js +++ b/packages/bruno-lang/v2/src/collectionBruToJson.js @@ -328,6 +328,20 @@ const sem = grammar.createSemantics().addAttribute('ast', { autoFetchToken: autoFetchTokenKey ? safeParseJson(autoFetchTokenKey?.value) ?? true : true, autoRefreshToken: autoRefreshTokenKey ? safeParseJson(autoRefreshTokenKey?.value) ?? false : false } + : grantTypeKey?.value && grantTypeKey?.value == 'implicit' + ? { + grantType: grantTypeKey ? grantTypeKey.value : '', + callbackUrl: callbackUrlKey ? callbackUrlKey.value : '', + authorizationUrl: authorizationUrlKey ? authorizationUrlKey.value : '', + clientId: clientIdKey ? clientIdKey.value : '', + scope: scopeKey ? scopeKey.value : '', + state: stateKey ? stateKey.value : '', + credentialsId: credentialsIdKey?.value ? credentialsIdKey.value : 'credentials', + tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header', + tokenHeaderPrefix: tokenHeaderPrefixKey?.value ? tokenHeaderPrefixKey.value : '', + tokenQueryKey: tokenQueryKeyKey?.value ? tokenQueryKeyKey.value : 'access_token', + autoFetchToken: autoFetchTokenKey ? safeParseJson(autoFetchTokenKey?.value) ?? true : true, + } : grantTypeKey?.value && grantTypeKey?.value == 'client_credentials' ? { grantType: grantTypeKey ? grantTypeKey.value : '', diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index c7395e2ff..9013599b7 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -246,6 +246,25 @@ ${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken ?? true).toStr ${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken ?? false).toString()}`)} } +`; + break; + case 'implicit': + bru += `auth:oauth2 { +${indentString(`grant_type: implicit`)} +${indentString(`callback_url: ${auth?.oauth2?.callbackUrl || ''}`)} +${indentString(`authorization_url: ${auth?.oauth2?.authorizationUrl || ''}`)} +${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)} +${indentString(`scope: ${auth?.oauth2?.scope || ''}`)} +${indentString(`state: ${auth?.oauth2?.state || ''}`)} +${indentString(`credentials_id: ${auth?.oauth2?.credentialsId || ''}`)} +${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${ + auth?.oauth2?.tokenPlacement == 'header' ? '\n' + indentString(`token_header_prefix: ${auth?.oauth2?.tokenHeaderPrefix || ''}`) : '' +}${ + auth?.oauth2?.tokenPlacement !== 'header' ? '\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : '' +} +${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken ?? true).toString()}`)} +} + `; break; } diff --git a/packages/bruno-lang/v2/src/jsonToCollectionBru.js b/packages/bruno-lang/v2/src/jsonToCollectionBru.js index 2812798a5..adcdf6b06 100644 --- a/packages/bruno-lang/v2/src/jsonToCollectionBru.js +++ b/packages/bruno-lang/v2/src/jsonToCollectionBru.js @@ -191,6 +191,25 @@ ${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken ?? true).toStr ${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken ?? false).toString()}`)} } +`; + break; + case 'implicit': + bru += `auth:oauth2 { +${indentString(`grant_type: implicit`)} +${indentString(`callback_url: ${auth?.oauth2?.callbackUrl || ''}`)} +${indentString(`authorization_url: ${auth?.oauth2?.authorizationUrl || ''}`)} +${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)} +${indentString(`scope: ${auth?.oauth2?.scope || ''}`)} +${indentString(`state: ${auth?.oauth2?.state || ''}`)} +${indentString(`credentials_id: ${auth?.oauth2?.credentialsId || ''}`)} +${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${ + auth?.oauth2?.tokenPlacement == 'header' ? '\n' + indentString(`token_header_prefix: ${auth?.oauth2?.tokenHeaderPrefix || ''}`) : '' +}${ + auth?.oauth2?.tokenPlacement !== 'header' ? '\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : '' +} +${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken ?? true).toString()}`)} +} + `; break; case 'client_credentials': diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index af4b13434..7a03141d5 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -159,7 +159,7 @@ const authApiKeySchema = Yup.object({ const oauth2Schema = Yup.object({ grantType: Yup.string() - .oneOf(['client_credentials', 'password', 'authorization_code']) + .oneOf(['client_credentials', 'password', 'authorization_code', 'implicit']) .required('grantType is required'), username: Yup.string().when('grantType', { is: (val) => ['client_credentials', 'password'].includes(val), @@ -172,12 +172,12 @@ const oauth2Schema = Yup.object({ otherwise: Yup.string().nullable().strip() }), callbackUrl: Yup.string().when('grantType', { - is: (val) => ['authorization_code'].includes(val), + is: (val) => ['authorization_code', 'implicit'].includes(val), then: Yup.string().nullable(), otherwise: Yup.string().nullable().strip() }), authorizationUrl: Yup.string().when('grantType', { - is: (val) => ['authorization_code'].includes(val), + is: (val) => ['authorization_code', 'implicit'].includes(val), then: Yup.string().nullable(), otherwise: Yup.string().nullable().strip() }), @@ -187,7 +187,7 @@ const oauth2Schema = Yup.object({ otherwise: Yup.string().nullable().strip() }), clientId: Yup.string().when('grantType', { - is: (val) => ['client_credentials', 'password', 'authorization_code'].includes(val), + is: (val) => ['client_credentials', 'password', 'authorization_code', 'implicit'].includes(val), then: Yup.string().nullable(), otherwise: Yup.string().nullable().strip() }), @@ -197,12 +197,12 @@ const oauth2Schema = Yup.object({ otherwise: Yup.string().nullable().strip() }), scope: Yup.string().when('grantType', { - is: (val) => ['client_credentials', 'password', 'authorization_code'].includes(val), + is: (val) => ['client_credentials', 'password', 'authorization_code', 'implicit'].includes(val), then: Yup.string().nullable(), otherwise: Yup.string().nullable().strip() }), state: Yup.string().when('grantType', { - is: (val) => ['authorization_code'].includes(val), + is: (val) => ['authorization_code', 'implicit'].includes(val), then: Yup.string().nullable(), otherwise: Yup.string().nullable().strip() }), @@ -217,24 +217,24 @@ const oauth2Schema = Yup.object({ otherwise: Yup.string().nullable().strip() }), credentialsId: Yup.string().when('grantType', { - is: (val) => ['client_credentials', 'password', 'authorization_code'].includes(val), + is: (val) => ['client_credentials', 'password', 'authorization_code', 'implicit'].includes(val), then: Yup.string().nullable(), otherwise: Yup.string().nullable().strip() }), tokenPlacement: Yup.string().when('grantType', { - is: (val) => ['client_credentials', 'password', 'authorization_code'].includes(val), + is: (val) => ['client_credentials', 'password', 'authorization_code', 'implicit'].includes(val), then: Yup.string().nullable(), otherwise: Yup.string().nullable().strip() }), tokenHeaderPrefix: Yup.string().when(['grantType', 'tokenPlacement'], { is: (grantType, tokenPlacement) => - ['client_credentials', 'password', 'authorization_code'].includes(grantType) && tokenPlacement === 'header', + ['client_credentials', 'password', 'authorization_code', 'implicit'].includes(grantType) && tokenPlacement === 'header', then: Yup.string().nullable(), otherwise: Yup.string().nullable().strip() }), tokenQueryKey: Yup.string().when(['grantType', 'tokenPlacement'], { is: (grantType, tokenPlacement) => - ['client_credentials', 'password', 'authorization_code'].includes(grantType) && tokenPlacement === 'url', + ['client_credentials', 'password', 'authorization_code', 'implicit'].includes(grantType) && tokenPlacement === 'url', then: Yup.string().nullable(), otherwise: Yup.string().nullable().strip() }), @@ -249,7 +249,7 @@ const oauth2Schema = Yup.object({ otherwise: Yup.boolean() }), autoFetchToken: Yup.boolean().when('grantType', { - is: (val) => ['authorization_code'].includes(val), + is: (val) => ['authorization_code', 'implicit'].includes(val), then: Yup.boolean().default(true), otherwise: Yup.boolean() })