* 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>
110 lines
3.6 KiB
TypeScript
110 lines
3.6 KiB
TypeScript
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
|
import { parseUrlFormData } from "app/api/parseRequestData";
|
|
import jwt from "jsonwebtoken";
|
|
import { NextResponse } from "next/server";
|
|
import type { NextRequest } from "next/server";
|
|
|
|
import { AccessCodeRepository } from "@calcom/features/oauth/repositories/AccessCodeRepository";
|
|
import { OAuthClientRepository } from "@calcom/features/oauth/repositories/OAuthClientRepository";
|
|
import { OAuthService } from "@calcom/features/oauth/services/OAuthService";
|
|
import prisma from "@calcom/prisma";
|
|
import type { OAuthTokenPayload } from "@calcom/types/oauth";
|
|
|
|
async function handler(req: NextRequest) {
|
|
const { code, client_id, client_secret, grant_type, redirect_uri, code_verifier } = await parseUrlFormData(
|
|
req
|
|
);
|
|
|
|
if (!process.env.CALENDSO_ENCRYPTION_KEY) {
|
|
return NextResponse.json({ message: "CALENDSO_ENCRYPTION_KEY is not set" }, { status: 500 });
|
|
}
|
|
|
|
if (!client_id || !code || !redirect_uri) {
|
|
return NextResponse.json({ error: "invalid_request" }, { status: 400 });
|
|
}
|
|
|
|
if (grant_type !== "authorization_code") {
|
|
return NextResponse.json({ error: "invalid_request" }, { status: 400 });
|
|
}
|
|
|
|
const oAuthClientRepository = new OAuthClientRepository(prisma);
|
|
const accessCodeRepository = new AccessCodeRepository(prisma);
|
|
|
|
const client = await oAuthClientRepository.findByClientId(client_id);
|
|
|
|
if (!client) {
|
|
return NextResponse.json({ error: "invalid_client" }, { status: 401 });
|
|
}
|
|
|
|
if (client.redirectUri !== redirect_uri) {
|
|
return NextResponse.json({ error: "invalid_grant" }, { status: 400 });
|
|
}
|
|
|
|
const isValidClient = OAuthService.validateClient(client, client_secret);
|
|
|
|
if (!isValidClient) {
|
|
return NextResponse.json({ error: "invalid_client" }, { status: 401 });
|
|
}
|
|
|
|
const accessCode = await accessCodeRepository.findValidCode(code, client_id);
|
|
|
|
// Delete all expired accessCodes + the one that is used here
|
|
await accessCodeRepository.deleteExpiredAndUsedCodes(code, client_id);
|
|
|
|
if (!accessCode) {
|
|
return NextResponse.json({ error: "invalid_grant" }, { status: 400 });
|
|
}
|
|
|
|
const pkceError = OAuthService.verifyPKCE(client, accessCode, code_verifier);
|
|
if (pkceError) {
|
|
return NextResponse.json({ error: pkceError.error }, { status: pkceError.status });
|
|
}
|
|
|
|
const secretKey = process.env.CALENDSO_ENCRYPTION_KEY;
|
|
|
|
const payloadAuthToken: OAuthTokenPayload = {
|
|
userId: accessCode.userId,
|
|
teamId: accessCode.teamId,
|
|
scope: accessCode.scopes,
|
|
token_type: "Access Token",
|
|
clientId: client_id,
|
|
};
|
|
|
|
const payloadRefreshToken: OAuthTokenPayload = {
|
|
userId: accessCode.userId,
|
|
teamId: accessCode.teamId,
|
|
scope: accessCode.scopes,
|
|
token_type: "Refresh Token",
|
|
clientId: client_id,
|
|
// Include PKCE information for clients that used PKCE (PUBLIC mandatory, CONFIDENTIAL optional)
|
|
...(accessCode.codeChallenge && {
|
|
codeChallenge: accessCode.codeChallenge,
|
|
codeChallengeMethod: accessCode.codeChallengeMethod,
|
|
}),
|
|
};
|
|
|
|
const accessTokenExpiresIn = 1800; // 30 minutes
|
|
|
|
const access_token = jwt.sign(payloadAuthToken, secretKey, {
|
|
expiresIn: accessTokenExpiresIn,
|
|
});
|
|
|
|
const refresh_token = jwt.sign(payloadRefreshToken, secretKey, {
|
|
expiresIn: 30 * 24 * 60 * 60, // 30 days
|
|
});
|
|
|
|
return NextResponse.json(
|
|
{ access_token, token_type: "bearer", refresh_token, expires_in: accessTokenExpiresIn },
|
|
{
|
|
status: 200,
|
|
headers: {
|
|
"Content-Type": "application/json;charset=UTF-8",
|
|
"Cache-Control": "no-store",
|
|
Pragma: "no-cache",
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
export const POST = defaultResponderForAppDir(handler);
|