fix: remove pkce check for refreshToken endpoint (#26050)

* remove pkce check for refreshToken endpoint

* adjust e2e tests

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
This commit is contained in:
Carina Wollendorfer
2025-12-19 15:18:49 +01:00
committed by GitHub
co-authored by CarinaWolli
parent 9c223fc718
commit 73b276015c
2 changed files with 4 additions and 225 deletions
@@ -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, {
+3 -214
View File
@@ -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);