diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts index 8d7170b01..a32bedf72 100644 --- a/backend/src/controllers/v1/integrationAuthController.ts +++ b/backend/src/controllers/v1/integrationAuthController.ts @@ -10,11 +10,11 @@ import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, INTEGRATION_BITBUCKET_API_URL, + INTEGRATION_GCP_SECRET_MANAGER, INTEGRATION_NORTHFLANK_API_URL, INTEGRATION_RAILWAY_API_URL, INTEGRATION_SET, INTEGRATION_VERCEL_API_URL, - INTEGRATION_GCP_SECRET_MANAGER, getIntegrationOptions as getIntegrationOptionsFunc } from "../../variables"; import { exchangeRefresh } from "../../integrations"; @@ -51,7 +51,12 @@ export const getIntegrationOptions = async (req: Request, res: Response) => { * @returns */ export const oAuthExchange = async (req: Request, res: Response) => { - const { workspaceId, code, integration } = req.body; + const { + workspaceId, + code, + integration, + url + } = req.body; if (!INTEGRATION_SET.has(integration)) throw new Error("Failed to validate integration"); const environments = req.membership.workspace?.environments || []; @@ -63,7 +68,8 @@ export const oAuthExchange = async (req: Request, res: Response) => { workspaceId, integration, code, - environment: environments[0].slug + environment: environments[0].slug, + url }); await EEAuditLogService.createAuditLog( diff --git a/backend/src/helpers/integration.ts b/backend/src/helpers/integration.ts index 6a948a039..485c19d7e 100644 --- a/backend/src/helpers/integration.ts +++ b/backend/src/helpers/integration.ts @@ -5,11 +5,11 @@ import { BotService } from "../services"; import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, + INTEGRATION_GCP_SECRET_MANAGER, INTEGRATION_NETLIFY, INTEGRATION_VERCEL, - INTEGRATION_GCP_SECRET_MANAGER, } from "../variables"; -import { BadRequestError, InternalServerError, UnauthorizedRequestError } from "../utils/errors"; +import { InternalServerError, UnauthorizedRequestError } from "../utils/errors"; import { IntegrationAuthMetadata } from "../models/integrationAuth/types"; interface Update { @@ -36,12 +36,14 @@ export const handleOAuthExchangeHelper = async ({ workspaceId, integration, code, - environment + environment, + url }: { workspaceId: string; integration: string; code: string; environment: string; + url?: string; }) => { const bot = await Bot.findOne({ workspace: workspaceId, @@ -53,7 +55,8 @@ export const handleOAuthExchangeHelper = async ({ // exchange code for access and refresh tokens const res = await exchangeCode({ integration, - code + code, + url }); const update: Update = { @@ -67,6 +70,7 @@ export const handleOAuthExchangeHelper = async ({ break; case INTEGRATION_NETLIFY: update.accountId = res.accountId; + break; case INTEGRATION_GCP_SECRET_MANAGER: update.metadata = { authMethod: "oauth2" diff --git a/backend/src/integrations/exchange.ts b/backend/src/integrations/exchange.ts index 641240470..a97a9db09 100644 --- a/backend/src/integrations/exchange.ts +++ b/backend/src/integrations/exchange.ts @@ -118,9 +118,11 @@ interface ExchangeCodeBitBucketResponse { const exchangeCode = async ({ integration, code, + url }: { integration: string; code: string; + url?: string; }) => { let obj = {} as any; @@ -158,6 +160,7 @@ const exchangeCode = async ({ case INTEGRATION_GITLAB: obj = await exchangeCodeGitlab({ code, + url }); break; case INTEGRATION_BITBUCKET: @@ -388,11 +391,17 @@ const exchangeCodeGithub = async ({ code }: { code: string }) => { * @returns {String} obj2.refreshToken - refresh token for Gitlab API * @returns {Date} obj2.accessExpiresAt - date of expiration for access token */ -const exchangeCodeGitlab = async ({ code }: { code: string }) => { +const exchangeCodeGitlab = async ({ + code, + url +}: { + code: string, + url?: string; +}) => { const accessExpiresAt = new Date(); const res: ExchangeCodeGitlabResponse = ( await standardRequest.post( - INTEGRATION_GITLAB_TOKEN_URL, + url ? `${url}/oauth/token` : INTEGRATION_GITLAB_TOKEN_URL, new URLSearchParams({ grant_type: "authorization_code", code: code, diff --git a/backend/src/integrations/refresh.ts b/backend/src/integrations/refresh.ts index 36cc4b930..dee4931c8 100644 --- a/backend/src/integrations/refresh.ts +++ b/backend/src/integrations/refresh.ts @@ -5,11 +5,11 @@ import { INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_BITBUCKET, INTEGRATION_BITBUCKET_TOKEN_URL, - INTEGRATION_GITLAB, - INTEGRATION_HEROKU, + INTEGRATION_GCP_CLOUD_PLATFORM_SCOPE, INTEGRATION_GCP_SECRET_MANAGER, INTEGRATION_GCP_TOKEN_URL, - INTEGRATION_GCP_CLOUD_PLATFORM_SCOPE + INTEGRATION_GITLAB, + INTEGRATION_HEROKU } from "../variables"; import { INTEGRATION_AZURE_TOKEN_URL, @@ -20,13 +20,13 @@ import { IntegrationService } from "../services"; import { getClientIdAzure, getClientIdBitBucket, + getClientIdGCPSecretManager, getClientIdGitLab, getClientSecretAzure, getClientSecretBitBucket, + getClientSecretGCPSecretManager, getClientSecretGitLab, getClientSecretHeroku, - getClientIdGCPSecretManager, - getClientSecretGCPSecretManager, getSiteURL, } from "../config"; @@ -112,6 +112,7 @@ const exchangeRefresh = async ({ break; case INTEGRATION_GITLAB: tokenDetails = await exchangeRefreshGitLab({ + integrationAuth, refreshToken, }); break; @@ -226,17 +227,21 @@ const exchangeRefreshHeroku = async ({ * @returns */ const exchangeRefreshGitLab = async ({ + integrationAuth, refreshToken, }: { + integrationAuth: IIntegrationAuth; refreshToken: string; }) => { const accessExpiresAt = new Date(); + const url = integrationAuth.url; + const { data, }: { data: RefreshTokenGitLabResponse; } = await standardRequest.post( - INTEGRATION_GITLAB_TOKEN_URL, + url ? `${url}/oauth/token` : INTEGRATION_GITLAB_TOKEN_URL, new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, @@ -329,17 +334,17 @@ const exchangeRefreshGCPSecretManager = async ({ exp: Math.floor(Date.now() / 1000) + 3600, }; - const token = jwt.sign(payload, serviceAccount.private_key, { algorithm: 'RS256' }); + const token = jwt.sign(payload, serviceAccount.private_key, { algorithm: "RS256" }); const { data }: { data: ServiceAccountAccessTokenGCPSecretManagerResponse } = await standardRequest.post( INTEGRATION_GCP_TOKEN_URL, new URLSearchParams({ - grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', + grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", assertion: token }).toString(), { headers: { - 'Content-Type': 'application/x-www-form-urlencoded' + "Content-Type": "application/x-www-form-urlencoded" } } ); diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 1f762e976..8023f74a2 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -1,4 +1,3 @@ -import jwt from "jsonwebtoken"; import { CreateSecretCommand, GetSecretValueCommand, @@ -29,8 +28,6 @@ import { INTEGRATION_FLYIO_API_URL, INTEGRATION_GCP_SECRET_MANAGER, INTEGRATION_GCP_SECRET_MANAGER_URL, - INTEGRATION_GCP_TOKEN_URL, - INTEGRATION_GCP_CLOUD_PLATFORM_SCOPE, INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_GITLAB_API_URL, diff --git a/backend/src/routes/v1/integrationAuth.ts b/backend/src/routes/v1/integrationAuth.ts index caa902072..73278a962 100644 --- a/backend/src/routes/v1/integrationAuth.ts +++ b/backend/src/routes/v1/integrationAuth.ts @@ -48,6 +48,7 @@ router.post( body("workspaceId").exists().trim().notEmpty(), body("code").exists().trim().notEmpty(), body("integration").exists().trim().notEmpty(), + body("url").optional().isString().trim(), validateRequest, integrationAuthController.oAuthExchange ); diff --git a/backend/src/services/IntegrationService.ts b/backend/src/services/IntegrationService.ts index 9523058a0..06d0426f3 100644 --- a/backend/src/services/IntegrationService.ts +++ b/backend/src/services/IntegrationService.ts @@ -32,17 +32,20 @@ class IntegrationService { integration, code, environment, + url }: { workspaceId: string; integration: string; code: string; environment: string; + url?: string; }) { return await handleOAuthExchangeHelper({ workspaceId, integration, code, environment, + url }); } diff --git a/frontend/src/hooks/api/integrationAuth/queries.tsx b/frontend/src/hooks/api/integrationAuth/queries.tsx index aa1873853..84d22ce5b 100644 --- a/frontend/src/hooks/api/integrationAuth/queries.tsx +++ b/frontend/src/hooks/api/integrationAuth/queries.tsx @@ -367,16 +367,19 @@ export const useAuthorizeIntegration = () => { mutationFn: async ({ workspaceId, code, - integration + integration, + url }: { workspaceId: string; code: string; integration: string; + url?: string; }) => { const { data: { integrationAuth } } = await apiRequest.post("/api/v1/integration-auth/oauth-token", { workspaceId, code, - integration + integration, + url }); return integrationAuth; diff --git a/frontend/src/pages/integrations/gitlab/authorize.tsx b/frontend/src/pages/integrations/gitlab/authorize.tsx index d3ec0e255..528324209 100644 --- a/frontend/src/pages/integrations/gitlab/authorize.tsx +++ b/frontend/src/pages/integrations/gitlab/authorize.tsx @@ -14,18 +14,20 @@ export default function GitLabAuthorizeIntegrationPage() { const [gitLabURL, setGitLabURL] = useState(""); const handleIntegrateWithOAuth = () => { - if (!cloudIntegrations) return; - const integrationOption = cloudIntegrations.find((integration) => integration.slug === "gitlab"); - - if (!integrationOption) return; - - const baseURL = gitLabURL.trim() === "" ? "https://gitlab.com" : gitLabURL.trim(); - - const csrfToken = crypto.randomBytes(16).toString("hex"); - localStorage.setItem("latestCSRFToken", csrfToken); - - const link = `${baseURL}/oauth/authorize?client_id=${integrationOption.clientId}&redirect_uri=${window.location.origin}/integrations/gitlab/oauth2/callback&response_type=code&state=${state}`; - window.location.assign(link); + if (!cloudIntegrations) return; + const integrationOption = cloudIntegrations.find((integration) => integration.slug === "gitlab"); + + if (!integrationOption) return; + + const baseURL = gitLabURL.trim() === "" ? "https://gitlab.com" : gitLabURL.trim(); + + const csrfToken = crypto.randomBytes(16).toString("hex"); + localStorage.setItem("latestCSRFToken", csrfToken); + + const state = `${csrfToken}|${gitLabURL.trim() === "" ? "" : gitLabURL.trim()}`; + const link = `${baseURL}/oauth/authorize?client_id=${integrationOption.clientId}&redirect_uri=${window.location.origin}/integrations/gitlab/oauth2/callback&response_type=code&state=${state}`; + + window.location.assign(link); } return ( diff --git a/frontend/src/pages/integrations/gitlab/create.tsx b/frontend/src/pages/integrations/gitlab/create.tsx index 8c4ba6cd1..6b4c5e769 100644 --- a/frontend/src/pages/integrations/gitlab/create.tsx +++ b/frontend/src/pages/integrations/gitlab/create.tsx @@ -95,7 +95,7 @@ export default function GitLabCreateIntegrationPage() { integrationAuthId: integrationAuth?._id, isActive: true, app: integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.appId === targetAppId)?.name, - appId: targetAppId, + appId: String(targetAppId), sourceEnvironment: selectedSourceEnvironment, targetEnvironment: targetEnvironment === "" ? "*" : targetEnvironment, targetEnvironmentId: null, diff --git a/frontend/src/pages/integrations/gitlab/oauth2/callback.tsx b/frontend/src/pages/integrations/gitlab/oauth2/callback.tsx index df5c98ad1..4ecf30de7 100644 --- a/frontend/src/pages/integrations/gitlab/oauth2/callback.tsx +++ b/frontend/src/pages/integrations/gitlab/oauth2/callback.tsx @@ -15,14 +15,18 @@ export default function GitLabOAuth2CallbackPage() { (async () => { try { // validate state - if (state !== localStorage.getItem("latestCSRFToken")) return; + const [csrfToken, url] = (state as string).split("|", 2); + + if (csrfToken !== localStorage.getItem("latestCSRFToken")) return; localStorage.removeItem("latestCSRFToken"); - // TODO: self-hosted url somewhere here? const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id") as string, code: code as string, - integration: "gitlab" + integration: "gitlab", + ...(url === "" ? {} : { + url + }) }); router.push(`/integrations/gitlab/create?integrationAuthId=${integrationAuth._id}`);