* 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>
62 lines
1.6 KiB
TypeScript
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;
|
|
}
|
|
}
|