Files
calendar/packages/features/oauth/services/OAuthService.ts
T
Carina WollendorferGitHubcubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>CarinaWollicubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
4e0798577a feat: OAuth PKCE (#25313)
* add public client

* implement PKCE

* pass codeChallenge and codeChallengeMethod to handler

* fixes for secure oauth flow

* fix type error

* clean up refresh token endpoint

* only support S256

* fix type error

* remove comment

* add tests

* fix type errors in route.test.ts

* add missing support for refresh token

* add e2e test for public client refresh tokens

* allow pkce for confidential clients

* fix type error

* fix e2e

* fix option pkce for confidential clients

* e2e test improvements

* fix test

* remove only

* add delay

* fix e2e tests

* remove only

* don't skip pkce if codeChallenge is set

* add service functions for token endpoint

* use service function in refreshToken endpoint

* use repository

* remove return types

* e2e test fixes

* fix e2e test

* remove .only in e2e test

* remove pause

* fix error responses in token endpoints

* adjust tests to new error responses

* fix error responses

* e2e improvements

* redirect on error

* adjust tests

* Update apps/web/modules/auth/oauth2/authorize-view.tsx

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2025-11-26 17:02:42 +01:00

62 lines
1.6 KiB
TypeScript

import { verifyCodeChallenge } from "@calcom/lib/pkce";
import { generateSecret } from "@calcom/trpc/server/routers/viewer/oAuth/addClient.handler";
interface OAuthClient {
clientType: "CONFIDENTIAL" | "PUBLIC";
clientSecret?: string | null;
}
type OAuthErrorCode = "invalid_request" | "invalid_grant";
interface PKCESource {
codeChallenge?: string | null;
codeChallengeMethod?: string | null;
}
interface OAuthErrorResult {
error: OAuthErrorCode;
status: number;
}
export class OAuthService {
static validateClient(client: OAuthClient, client_secret?: string): boolean {
if (client.clientType === "CONFIDENTIAL") {
if (!client_secret) return false;
const [hashedSecret] = generateSecret(client_secret);
if (client.clientSecret !== hashedSecret) return false;
}
return true; // PUBLIC has no client_secret
}
/**
* PKCE validator for BOTH:
* - exchanging authorization code
* - exchanging refresh token
*/
static verifyPKCE(
client: OAuthClient,
source: PKCESource,
code_verifier?: string
): OAuthErrorResult | null {
// Determine if PKCE should be enforced
const shouldEnforcePKCE =
client.clientType === "PUBLIC" || (client.clientType === "CONFIDENTIAL" && source.codeChallenge);
if (!shouldEnforcePKCE) return null;
const method = source.codeChallengeMethod || "S256";
if (!source.codeChallenge || !code_verifier || method !== "S256") {
return { error: "invalid_request", status: 400 };
}
if (!verifyCodeChallenge(code_verifier, source.codeChallenge, method)) {
return { error: "invalid_grant", status: 400 };
}
return null;
}
}