Files
calendar/apps/web/playwright/oauth-provider.e2e.ts
T
Rajiv SahalGitHubamritDevanshu SharmaDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Sean Brydonbot_apk
e2add3f2c9 feat: enable microsoft sign ups (#28080)
* fix: trigger lingo.dev by removing duplicate value

* under progress

* wow this worked

* migrate schema

* fix types

* fix import for google login

* Add onboarding tests for Azure (Microsoft sign up)

* add comments back

* fix failing test

* fix: update Outlook login configuration and improve type safety in authentication adapter

- Set OUTLOOK_LOGIN_ENABLED to false in .env.example
- Refactor getServerSideProps to directly use samlTenantID and samlProductID
- Update linkAccount method in next-auth-custom-adapter for better type handling
- Remove redundant comment in next-auth-options related to Azure AD email verification

* remove log

* remove debug log from signin callback in next-auth options

* fixup

* chore: standardize naming

* chore: add primary calendar for outlook, verify email and auto link org for outlook

* chore: helper fns

* chore: implement cubic feedback

* cleanup

* chore: implement cubic feedback again

* WIP design#

* feat: login design

* fix: map identity provider names correctly

* 32px of mt

* fix: login UI

* fix: type check

* fix: fix type check again

* chore: update OAuth login tests

* fixup

* fix: bad import

* chore: update tests

* fixup

* fix: locales test

* chore: implement PR feedback and fix minor issues

* fix: revert token spreading change

* fix: merge conflicts

* chore: revert signup view changes

* fixup: bring back reverted changes because of merge conflicts

* fix: disable email input when microsoft sign in is in progress

* chore: implement cubic feedback

* cleanup: unused variables

* fix: address Cubic AI review feedback (confidence >= 9/10)

- Remove userId (PII) from log payloads in updateProfilePhotoMicrosoft.ts
- Replace text selectors with data-testid in locale.e2e.ts and oauth-provider.e2e.ts
- Restore callbackUrl redirect parameter in signup link in login-view.tsx
- Add data-testid='login-subtitle' to login page subtitle element

Co-Authored-By: unknown <>

* fix: use empty alt for decorative icon images in login view

MicrosoftIcon and GoogleIcon are decorative (adjacent to text labels),
so they should have empty alt attributes per accessibility best practices.

Co-Authored-By: unknown <>

* chore: implement cubic feedback

* cleanup

* fixup

* chore: implement PR feedback

* chore: implement feedback

* fix: address PR review feedback - type safety and centralize constants

- Replace non-null assertions (!) with proper null checks for OUTLOOK_CLIENT_ID/SECRET
- Replace `as any` casting with `Record<string, unknown>` for OAuth profile claims
- Remove non-null assertion on account.access_token by adding conditional check
- Centralize Outlook env constants in @calcom/lib/constants alongside MICROSOFT_CALENDAR_SCOPES
- Add explanatory comment for getNextAuthProviderName usage in get.handler.ts

Co-Authored-By: unknown <>

* Revert "fix: address PR review feedback - type safety and centralize constants"

This reverts commit 91ace141e6a28a23deea5897f7f9d6ad80319d84.

* chore: implement feedback

* chore: cleanup

* chore: implement feedback

* fix: merge conflicts

* fix: revert formatting-only changes in packages/lib/constants.ts

Co-Authored-By: unknown <>

* fix: revert IdentityProvider enum location change in schema.prisma

Co-Authored-By: unknown <>

* chore: implement more PR feedback

* fix: restore database-derived profileId from determineProfile in OAuth JWT

The profileId regression was identified by Cubic AI (confidence 9/10).

Previously, determineProfile's returned id was used to set profileId in the
JWT via 'profileResult.id ?? token.profileId ?? null'. A recent refactor
changed this to 'token.profileId ?? null', which drops the database-derived
profile ID. On first OAuth login (or when profile switcher is disabled),
token.profileId is likely null, so profileId would incorrectly be set to
null even though determineProfile returned a valid profile with an id.

This commit restores the correct priority chain:
  visitorProfileId ?? token.profileId ?? null

Co-Authored-By: bot_apk <apk@cognition.ai>

* refactor: revert pure formatting and import reordering changes

Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com>

* fix: normalize determineProfile return type to use consistent 'id' field

The determineProfile function returned a union type where one branch used
'id' and the other used 'profileId'. This caused TS2339 when destructuring
'id' from the result. Normalize the token.upId branch to also return 'id'
(mapped from token.profileId) so the return type is consistent.

Co-Authored-By: bot_apk <apk@cognition.ai>

* chore: add tests

* reveret: profileId changes should be in a separate PR

* fix: avoid logging entire existingUser object in OAuth JWT callback

Revert to logging only { userId, upId } instead of the full existingUser
object, which contains PII (email, name, identity provider details).
This restores the previous safe logging pattern.

Co-Authored-By: bot_apk <apk@cognition.ai>

* chore: remove profileId related tests

---------

Co-authored-by: amrit <iamamrit27@gmail.com>
Co-authored-by: Devanshu Sharma <devanshusharma658@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Sean Brydon <sean@cal.com>
Co-authored-by: bot_apk <apk@cognition.ai>
2026-03-05 16:47:40 +05:30

720 lines
26 KiB
TypeScript

import { expect } from "@playwright/test";
import { createHash, randomBytes } from "node:crypto";
import { OAUTH_ERROR_REASONS } from "@calcom/features/oauth/services/OAuthService";
import { generateSecret } from "@calcom/features/oauth/utils/generateSecret";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { prisma } from "@calcom/prisma";
import { test } from "./lib/fixtures";
test.afterEach(async ({ users }) => {
await users.deleteAll();
});
let client: {
clientId: string;
redirectUri: string;
orginalSecret: string;
name: string;
clientSecret: string | null;
logo: string | null;
};
let publicClient: {
clientId: string;
redirectUri: string;
name: string;
clientSecret: string | null;
logo: string | null;
};
// Helper function to generate PKCE values
function generatePKCE() {
const codeVerifier = randomBytes(32).toString("base64url");
const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
return {
codeVerifier,
codeChallenge,
codeChallengeMethod: "S256" as const,
};
}
test.describe("OAuth Provider", () => {
test.beforeAll(async () => {
client = await createTestCLient();
});
test("should create valid access token & refresh token for user", async ({ page, users }) => {
const user = await users.create({ username: "test user", name: "test user" });
await user.apiLogin();
await page.goto(
`auth/oauth2/authorize?client_id=${client.clientId}&redirect_uri=${client.redirectUri}&response_type=code&scope=READ_PROFILE&state=1234`
);
await page.getByTestId("allow-button").click();
await page.waitForFunction(() => {
return window.location.href.startsWith("https://example.com");
});
const url = new URL(page.url());
// authorization code that is returned to client with redirect uri
const code = url.searchParams.get("code");
// request token with authorization code
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);
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();
// test if token is valid
const meResponse = await fetch(`${WEBAPP_URL}/api/auth/oauth/me`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokenData.access_token}`,
},
});
const meData = await meResponse.json();
// check if user access token is valid
expect(meData.username.startsWith("test user")).toBe(true);
// request new token with refresh token
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");
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(refreshTokenData.access_token).toBeDefined();
const validTokenResponse = await fetch(`${WEBAPP_URL}/api/auth/oauth/me`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokenData.access_token}`,
},
});
const validTokenData = await validTokenResponse.json();
expect(validTokenData.username.startsWith("test user")).toBe(true);
});
test("should show signed-in user name instead of account selector when show_account_selector is not set", async ({
page,
users,
}) => {
const user = await users.create({ username: "test user", name: "test user" });
await user.apiLogin();
await page.goto(
`auth/oauth2/authorize?client_id=${client.clientId}&redirect_uri=${client.redirectUri}&response_type=code&scope=READ_PROFILE&state=1234`
);
await expect(page).toHaveURL(/auth\/oauth2\/authorize/);
await page.waitForSelector('[data-testid="allow-button"]');
// Should show "Signed in as" with the user's name
await expect(page.getByTestId("signed-in-user")).toBeVisible();
// Account selector should not be present
await expect(page.locator("#account-select")).not.toBeVisible();
});
test("should create valid access token & refresh token for team", async ({ page, users }) => {
const user = await users.create({ username: "test user", name: "test user" }, { hasTeam: true });
await user.apiLogin();
await page.goto(
`auth/oauth2/authorize?client_id=${client.clientId}&redirect_uri=${client.redirectUri}&response_type=code&scope=READ_PROFILE&state=1234&show_account_selector=true`
);
await page.locator("#account-select").click();
const teamOption = page
.locator('[id*="react-select-"][id*="-option-"]')
.filter({ hasText: /Team/i })
.first();
await teamOption.waitFor({ state: "visible" });
await teamOption.click();
await page.getByTestId("allow-button").click();
await page.waitForFunction(() => {
return window.location.href.startsWith("https://example.com");
});
const url = new URL(page.url());
// authorization code that is returned to client with redirect uri
const code = url.searchParams.get("code");
// request token with authorization code
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);
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();
// test if token is valid
const meResponse = await fetch(`${WEBAPP_URL}/api/auth/oauth/me`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokenData.access_token}`,
},
});
const meData = await meResponse.json();
// Check if team access token is valid
expect(meData.username).toEqual(`user-id-${user.id}'s Team`);
// request new token with refresh token
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");
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(refreshTokenData.access_token).toBeDefined();
const validTokenResponse = await fetch(`${WEBAPP_URL}/api/auth/oauth/me`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokenData.access_token}`,
},
});
const validTokenData = await validTokenResponse.json();
expect(validTokenData.username).toEqual(`user-id-${user.id}'s Team`);
});
test("redirect not logged-in users to login page and after forward to authorization page", async ({
page,
users,
}) => {
const user = await users.create({ username: "test-user", name: "test user" });
await page.goto(
`auth/oauth2/authorize?client_id=${client.clientId}&redirect_uri=${client.redirectUri}&response_type=code&scope=READ_PROFILE&state=1234`
);
// check if user is redirected to login page
await expect(page.getByRole("heading", { name: "Cal.com" })).toBeVisible();
await expect(page.getByTestId("login-subtitle")).toBeVisible();
await page.locator("#email").fill(user.email);
await page.locator("#password").fill(user.username || "");
await page.locator('[type="submit"]').click();
await page.waitForSelector('[data-testid="allow-button"]');
await expect(page.getByText("test user")).toBeVisible();
});
});
test.describe("OAuth Provider - PKCE (Public Clients)", () => {
test.beforeAll(async () => {
publicClient = await createTestPublicClient();
});
test("should create valid access token for PUBLIC client with valid PKCE", async ({ page, users }) => {
const user = await users.create({ username: "test user pkce", name: "test user pkce" });
await user.apiLogin();
// Generate PKCE values
const pkce = generatePKCE();
// Authorization request with PKCE challenge
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");
// Token exchange with PKCE verifier
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);
expect(tokenData.access_token).toBeDefined();
expect(tokenData.token_type).toBe("bearer");
expect(tokenData.refresh_token).toBeDefined();
expect(tokenData.expires_in).toBe(1800);
// Verify token is valid
const meResponse = await fetch(`${WEBAPP_URL}/api/auth/oauth/me`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokenData.access_token}`,
},
});
const meData = await meResponse.json();
expect(meData.username.startsWith("test user pkce")).toBe(true);
});
test("should reject PUBLIC client with invalid PKCE verifier", async ({ page, users }) => {
const user = await users.create({ username: "test user pkce invalid", name: "test user pkce invalid" });
await user.apiLogin();
// Generate PKCE values
const pkce = generatePKCE();
const wrongVerifier = randomBytes(32).toString("base64url");
// Authorization request with PKCE challenge
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");
// Token exchange with WRONG PKCE verifier
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", wrongVerifier); // Wrong verifier!
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(400);
expect(tokenData.error).toBe("invalid_grant");
});
test("should reject PUBLIC client without code_verifier", async ({ page, users }) => {
const user = await users.create({ username: "test user pkce missing", name: "test user pkce missing" });
await user.apiLogin();
// Generate PKCE values
const pkce = generatePKCE();
// Authorization request with PKCE challenge
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");
// Token exchange WITHOUT code_verifier (should fail)
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);
// Missing code_verifier!
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(400);
expect(tokenData.error).toBe("invalid_request");
});
test("should reject PUBLIC client authorization without code_challenge", async ({ page, users }) => {
const user = await users.create({ username: "test user no challenge", name: "test user no challenge" });
await user.apiLogin();
// Try to authorize without PKCE challenge (should fail at authorization step)
await page.goto(
`auth/oauth2/authorize?client_id=${publicClient.clientId}&redirect_uri=${publicClient.redirectUri}&response_type=code&scope=READ_PROFILE&state=1234`
);
await page.waitForSelector('[data-testid="allow-button"]');
// Clicking allow should fail because PUBLIC clients require PKCE
await page.getByTestId("allow-button").click();
// Wait for redirect to callback URL with error parameters
await page.waitForFunction(() => {
return window.location.href.startsWith("https://example.com");
});
// Should redirect to callback URL with error parameters (OAuth2 error redirect)
await expect(page).toHaveURL(/^https:\/\/example\.com/);
const url = new URL(page.url());
expect(url.searchParams.get("error")).toBe("invalid_request");
expect(url.searchParams.get("error_description")).toBe(OAUTH_ERROR_REASONS["pkce_required"]);
expect(url.searchParams.get("state")).toBe("1234");
// Should not contain authorization code
expect(url.searchParams.get("code")).toBeNull();
});
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();
// Generate PKCE values
const pkce = generatePKCE();
// Authorization request with PKCE challenge
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");
// Token exchange with PKCE verifier
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);
expect(tokenData.access_token).toBeDefined();
expect(tokenData.refresh_token).toBeDefined();
// 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");
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(200);
expect(refreshTokenData.access_token).toBeDefined();
expect(refreshTokenData.token_type).toBe("bearer");
expect(refreshTokenData.refresh_token).toBeDefined();
expect(refreshTokenData.expires_in).toBe(1800);
// Verify new access token works
const meResponse = await fetch(`${WEBAPP_URL}/api/auth/oauth/me`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${refreshTokenData.access_token}`,
},
});
const meData = await meResponse.json();
expect(meData.username.startsWith("test user refresh")).toBe(true);
});
});
test.describe("OAuth Provider - PKCE with CONFIDENTIAL Clients (Enhanced Security)", () => {
test.beforeAll(async () => {
// Ensure we have a confidential client for these tests
client = await createTestCLient();
});
test("should accept CONFIDENTIAL client with PKCE for defense in depth", async ({ page, users }) => {
const user = await users.create({ username: "test user conf pkce", name: "test user conf pkce" });
await user.apiLogin();
// Generate PKCE values
const pkce = generatePKCE();
// Authorization request with PKCE challenge (even for CONFIDENTIAL client)
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 client_secret AND code_verifier (dual authentication)
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);
expect(tokenData.access_token).toBeDefined();
expect(tokenData.token_type).toBe("bearer");
expect(tokenData.refresh_token).toBeDefined();
expect(tokenData.expires_in).toBe(1800);
// Verify token works
const meResponse = await fetch(`${WEBAPP_URL}/api/auth/oauth/me`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokenData.access_token}`,
},
});
const meData = await meResponse.json();
expect(meData.username.startsWith("test user conf pkce")).toBe(true);
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");
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(200);
expect(refreshTokenData.access_token).toBeDefined();
expect(refreshTokenData.refresh_token).toBeDefined();
});
test("should allow CONFIDENTIAL client refresh with only client_secret when PKCE was NOT used", async ({
page,
users,
}) => {
const user = await users.create({ username: "test user conf no pkce", name: "test user conf no pkce" });
await user.apiLogin();
// Authorization WITHOUT PKCE challenge (traditional CONFIDENTIAL client flow)
await page.goto(
`auth/oauth2/authorize?client_id=${client.clientId}&redirect_uri=${client.redirectUri}&response_type=code&scope=READ_PROFILE&state=1234`
);
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 ONLY client_secret (no PKCE)
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);
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);
// Refresh with client_secret
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");
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(200);
expect(refreshTokenData.access_token).toBeDefined();
expect(refreshTokenData.token_type).toBe("bearer");
});
});
const createTestCLient = async () => {
const [hashedSecret, secret] = generateSecret();
const clientId = randomBytes(32).toString("hex");
const client = await prisma.oAuthClient.create({
data: {
name: "Test Client",
clientId,
clientSecret: hashedSecret,
redirectUri: "https://example.com",
clientType: "CONFIDENTIAL",
},
});
return { ...client, orginalSecret: secret };
};
const createTestPublicClient = async () => {
const clientId = randomBytes(32).toString("hex");
const client = await prisma.oAuthClient.create({
data: {
name: "Test Public Client",
clientId,
clientSecret: null,
redirectUri: "https://example.com",
clientType: "PUBLIC",
},
});
return client;
};