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:
co-authored by
CarinaWolli
parent
9c223fc718
commit
73b276015c
@@ -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, {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user