diff --git a/apps/web/app/api/auth/oauth/refreshToken/route.ts b/apps/web/app/api/auth/oauth/refreshToken/route.ts index aafcedaa77..64c6f518ec 100644 --- a/apps/web/app/api/auth/oauth/refreshToken/route.ts +++ b/apps/web/app/api/auth/oauth/refreshToken/route.ts @@ -10,7 +10,7 @@ import prisma from "@calcom/prisma"; import type { OAuthTokenPayload } from "@calcom/types/oauth"; async function handler(req: NextRequest) { - const { client_id, client_secret, grant_type, refresh_token, code_verifier } = await parseUrlFormData(req); + const { client_id, client_secret, grant_type, refresh_token } = await parseUrlFormData(req); if (!process.env.CALENDSO_ENCRYPTION_KEY) { return NextResponse.json({ message: "CALENDSO_ENCRYPTION_KEY is not set" }, { status: 500 }); @@ -62,11 +62,6 @@ async function handler(req: NextRequest) { return NextResponse.json({ error: "invalid_grant" }, { status: 400 }); } - const pkceError = OAuthService.verifyPKCE(client, decodedRefreshToken, code_verifier); - if (pkceError) { - return NextResponse.json({ error: pkceError.error }, { status: pkceError.status }); - } - const accessTokenExpiresIn = 1800; // 30 minutes const payloadAuthToken: OAuthTokenPayload = { @@ -83,11 +78,6 @@ async function handler(req: NextRequest) { scope: decodedRefreshToken.scope, token_type: "Refresh Token", clientId: client_id, - // Preserve PKCE information for any client that used PKCE originally - ...(decodedRefreshToken.codeChallenge && { - codeChallenge: decodedRefreshToken.codeChallenge, - codeChallengeMethod: decodedRefreshToken.codeChallengeMethod, - }), }; const access_token = jwt.sign(payloadAuthToken, secretKey, { diff --git a/apps/web/playwright/oauth-provider.e2e.ts b/apps/web/playwright/oauth-provider.e2e.ts index 9df08d256f..66aa53be23 100644 --- a/apps/web/playwright/oauth-provider.e2e.ts +++ b/apps/web/playwright/oauth-provider.e2e.ts @@ -423,7 +423,7 @@ test.describe("OAuth Provider - PKCE (Public Clients)", () => { expect(url.searchParams.get("code")).toBeNull(); }); - test("should refresh tokens for PUBLIC client with valid PKCE", async ({ page, users }) => { + test("should refresh tokens for PUBLIC client", async ({ page, users }) => { const user = await users.create({ username: "test user refresh", name: "test user refresh" }); await user.apiLogin(); @@ -470,12 +470,11 @@ test.describe("OAuth Provider - PKCE (Public Clients)", () => { expect(tokenData.access_token).toBeDefined(); expect(tokenData.refresh_token).toBeDefined(); - // Now test refresh token with PKCE + // Refresh token - NO PKCE needed const refreshTokenForm = new URLSearchParams(); refreshTokenForm.append("refresh_token", tokenData.refresh_token); refreshTokenForm.append("client_id", publicClient.clientId); refreshTokenForm.append("grant_type", "refresh_token"); - refreshTokenForm.append("code_verifier", pkce.codeVerifier); // PUBLIC clients need code_verifier const refreshTokenResponse = await fetch(`${WEBAPP_URL}/api/auth/oauth/refreshToken`, { body: refreshTokenForm.toString(), @@ -505,141 +504,6 @@ test.describe("OAuth Provider - PKCE (Public Clients)", () => { const meData = await meResponse.json(); expect(meData.username.startsWith("test user refresh")).toBe(true); }); - - test("should reject PUBLIC client refresh token without code_verifier", async ({ page, users }) => { - const user = await users.create({ - username: "test user refresh no pkce", - name: "test user refresh no pkce", - }); - await user.apiLogin(); - - // Generate PKCE values - const pkce = generatePKCE(); - - // Get tokens first - await page.goto( - `auth/oauth2/authorize?client_id=${publicClient.clientId}&redirect_uri=${publicClient.redirectUri}&response_type=code&scope=READ_PROFILE&state=1234&code_challenge=${pkce.codeChallenge}&code_challenge_method=${pkce.codeChallengeMethod}` - ); - await page.getByTestId("allow-button").click(); - - await page.waitForFunction(() => { - return window.location.href.startsWith("https://example.com"); - }); - - // Assert URL to catch unexpected redirects - await expect(page).toHaveURL(/^https:\/\/example\.com/); - expect(page.url()).toContain("code="); - expect(page.url()).toContain("state=1234"); - - const url = new URL(page.url()); - const code = url.searchParams.get("code"); - - const tokenForm = new URLSearchParams(); - tokenForm.append("code", code ?? ""); - tokenForm.append("client_id", publicClient.clientId); - tokenForm.append("grant_type", "authorization_code"); - tokenForm.append("redirect_uri", publicClient.redirectUri); - tokenForm.append("code_verifier", pkce.codeVerifier); - - const tokenResponse = await fetch(`${WEBAPP_URL}/api/auth/oauth/token`, { - body: tokenForm.toString(), - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - }); - - const tokenData = await tokenResponse.json(); - expect(tokenResponse.status).toBe(200); - - // Now try refresh WITHOUT code_verifier (should fail) - const refreshTokenForm = new URLSearchParams(); - refreshTokenForm.append("refresh_token", tokenData.refresh_token); - refreshTokenForm.append("client_id", publicClient.clientId); - refreshTokenForm.append("grant_type", "refresh_token"); - // Missing code_verifier! - - const refreshTokenResponse = await fetch(`${WEBAPP_URL}/api/auth/oauth/refreshToken`, { - body: refreshTokenForm.toString(), - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - }); - - const refreshTokenData = await refreshTokenResponse.json(); - - expect(refreshTokenResponse.status).toBe(400); - expect(refreshTokenData.error).toBe("invalid_request"); - }); - - test("should reject PUBLIC client refresh token with invalid code_verifier", async ({ page, users }) => { - const user = await users.create({ - username: "test user refresh wrong pkce", - name: "test user refresh wrong pkce", - }); - await user.apiLogin(); - - // Generate PKCE values - const pkce = generatePKCE(); - const wrongVerifier = randomBytes(32).toString("base64url"); - - // Get tokens first - await page.goto( - `auth/oauth2/authorize?client_id=${publicClient.clientId}&redirect_uri=${publicClient.redirectUri}&response_type=code&scope=READ_PROFILE&state=1234&code_challenge=${pkce.codeChallenge}&code_challenge_method=${pkce.codeChallengeMethod}` - ); - await page.getByTestId("allow-button").click(); - - await page.waitForFunction(() => { - return window.location.href.startsWith("https://example.com"); - }); - - // Assert URL to catch unexpected redirects - await expect(page).toHaveURL(/^https:\/\/example\.com/); - expect(page.url()).toContain("code="); - expect(page.url()).toContain("state=1234"); - - const url = new URL(page.url()); - const code = url.searchParams.get("code"); - - const tokenForm = new URLSearchParams(); - tokenForm.append("code", code ?? ""); - tokenForm.append("client_id", publicClient.clientId); - tokenForm.append("grant_type", "authorization_code"); - tokenForm.append("redirect_uri", publicClient.redirectUri); - tokenForm.append("code_verifier", pkce.codeVerifier); - - const tokenResponse = await fetch(`${WEBAPP_URL}/api/auth/oauth/token`, { - body: tokenForm.toString(), - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - }); - - const tokenData = await tokenResponse.json(); - expect(tokenResponse.status).toBe(200); - - // Now try refresh with WRONG code_verifier (should fail) - const refreshTokenForm = new URLSearchParams(); - refreshTokenForm.append("refresh_token", tokenData.refresh_token); - refreshTokenForm.append("client_id", publicClient.clientId); - refreshTokenForm.append("grant_type", "refresh_token"); - refreshTokenForm.append("code_verifier", wrongVerifier); // Wrong verifier! - - const refreshTokenResponse = await fetch(`${WEBAPP_URL}/api/auth/oauth/refreshToken`, { - body: refreshTokenForm.toString(), - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - }); - - const refreshTokenData = await refreshTokenResponse.json(); - - expect(refreshTokenResponse.status).toBe(400); - expect(refreshTokenData.error).toBe("invalid_grant"); - }); }); test.describe("OAuth Provider - PKCE with CONFIDENTIAL Clients (Enhanced Security)", () => { @@ -710,13 +574,11 @@ test.describe("OAuth Provider - PKCE with CONFIDENTIAL Clients (Enhanced Securit const meData = await meResponse.json(); expect(meData.username.startsWith("test user conf pkce")).toBe(true); - // Test refresh with both client_secret and code_verifier (enhanced security) const refreshTokenForm = new URLSearchParams(); refreshTokenForm.append("refresh_token", tokenData.refresh_token); refreshTokenForm.append("client_id", client.clientId); refreshTokenForm.append("client_secret", client.orginalSecret); refreshTokenForm.append("grant_type", "refresh_token"); - refreshTokenForm.append("code_verifier", pkce.codeVerifier); const refreshTokenResponse = await fetch(`${WEBAPP_URL}/api/auth/oauth/refreshToken`, { body: refreshTokenForm.toString(), @@ -733,79 +595,6 @@ test.describe("OAuth Provider - PKCE with CONFIDENTIAL Clients (Enhanced Securit expect(refreshTokenData.refresh_token).toBeDefined(); }); - test("should reject CONFIDENTIAL client refresh without code_verifier when PKCE was used", async ({ - page, - users, - }) => { - const user = await users.create({ - username: "test user conf no verifier", - name: "test user conf no verifier", - }); - await user.apiLogin(); - - // Generate PKCE values for initial authorization - const pkce = generatePKCE(); - - // Authorization with PKCE - await page.goto( - `auth/oauth2/authorize?client_id=${client.clientId}&redirect_uri=${client.redirectUri}&response_type=code&scope=READ_PROFILE&state=1234&code_challenge=${pkce.codeChallenge}&code_challenge_method=${pkce.codeChallengeMethod}` - ); - await page.getByTestId("allow-button").click(); - - await page.waitForFunction(() => { - return window.location.href.startsWith("https://example.com"); - }); - - // Assert URL to catch unexpected redirects - await expect(page).toHaveURL(/^https:\/\/example\.com/); - expect(page.url()).toContain("code="); - expect(page.url()).toContain("state=1234"); - - const url = new URL(page.url()); - const code = url.searchParams.get("code"); - - // Token exchange with both credentials - const tokenForm = new URLSearchParams(); - tokenForm.append("code", code ?? ""); - tokenForm.append("client_id", client.clientId); - tokenForm.append("client_secret", client.orginalSecret); - tokenForm.append("grant_type", "authorization_code"); - tokenForm.append("redirect_uri", client.redirectUri); - tokenForm.append("code_verifier", pkce.codeVerifier); - - const tokenResponse = await fetch(`${WEBAPP_URL}/api/auth/oauth/token`, { - body: tokenForm.toString(), - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - }); - - const tokenData = await tokenResponse.json(); - expect(tokenResponse.status).toBe(200); - - // Test refresh with ONLY client_secret (no code_verifier) - should FAIL since PKCE was used originally - const refreshTokenForm = new URLSearchParams(); - refreshTokenForm.append("refresh_token", tokenData.refresh_token); - refreshTokenForm.append("client_id", client.clientId); - refreshTokenForm.append("client_secret", client.orginalSecret); - refreshTokenForm.append("grant_type", "refresh_token"); - // Intentionally not providing code_verifier - - const refreshTokenResponse = await fetch(`${WEBAPP_URL}/api/auth/oauth/refreshToken`, { - body: refreshTokenForm.toString(), - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - }); - - const refreshTokenData = await refreshTokenResponse.json(); - - expect(refreshTokenResponse.status).toBe(400); - expect(refreshTokenData.error).toBe("invalid_request"); - }); - test("should allow CONFIDENTIAL client refresh with only client_secret when PKCE was NOT used", async ({ page, users, @@ -850,7 +639,7 @@ test.describe("OAuth Provider - PKCE with CONFIDENTIAL Clients (Enhanced Securit const tokenData = await tokenResponse.json(); expect(tokenResponse.status).toBe(200); - // Refresh with ONLY client_secret - should work since PKCE was never used + // Refresh with client_secret const refreshTokenForm = new URLSearchParams(); refreshTokenForm.append("refresh_token", tokenData.refresh_token); refreshTokenForm.append("client_id", client.clientId);