|
|
|
@@ -10,16 +10,30 @@ import { ProfilesRepository } from "@/modules/profiles/profiles.repository";
|
|
|
|
|
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
|
|
|
|
import { UsersService } from "@/modules/users/services/users.service";
|
|
|
|
|
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
|
|
|
|
import { Injectable, InternalServerErrorException, UnauthorizedException } from "@nestjs/common";
|
|
|
|
|
import {
|
|
|
|
|
HttpException,
|
|
|
|
|
Injectable,
|
|
|
|
|
InternalServerErrorException,
|
|
|
|
|
UnauthorizedException,
|
|
|
|
|
} from "@nestjs/common";
|
|
|
|
|
import { Logger } from "@nestjs/common";
|
|
|
|
|
import { ConfigService } from "@nestjs/config";
|
|
|
|
|
import { PassportStrategy } from "@nestjs/passport";
|
|
|
|
|
import type { Request } from "express";
|
|
|
|
|
import * as jwt from "jsonwebtoken";
|
|
|
|
|
import { getToken } from "next-auth/jwt";
|
|
|
|
|
|
|
|
|
|
import { INVALID_ACCESS_TOKEN, X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
|
|
|
|
|
|
|
|
|
import type { AllowedAuthMethod } from "../../decorators/api-auth-guard-only-allow.decorator";
|
|
|
|
|
|
|
|
|
|
interface OAuthTokenPayload {
|
|
|
|
|
userId?: number;
|
|
|
|
|
teamId?: number;
|
|
|
|
|
scope: string[];
|
|
|
|
|
token_type: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type ApiAuthGuardUser = UserWithProfile & { isSystemAdmin: boolean };
|
|
|
|
|
export type ApiAuthGuardRequest = Request & {
|
|
|
|
|
authMethod: AuthMethods;
|
|
|
|
@@ -29,8 +43,11 @@ export type ApiAuthGuardRequest = Request & {
|
|
|
|
|
};
|
|
|
|
|
export const NO_AUTH_PROVIDED_MESSAGE =
|
|
|
|
|
"No authentication method provided. Either pass an API key as 'Bearer' header or OAuth client credentials as 'x-cal-secret-key' and 'x-cal-client-id' headers";
|
|
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth") {
|
|
|
|
|
private readonly logger = new Logger("ApiAuthStrategy");
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
private readonly deploymentsService: DeploymentsService,
|
|
|
|
|
private readonly config: ConfigService,
|
|
|
|
@@ -54,22 +71,52 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
|
|
|
|
|
|
|
|
|
const allowedMethods = request.allowedAuthMethods;
|
|
|
|
|
const noSpecificAuthExpected = !allowedMethods || !allowedMethods.length;
|
|
|
|
|
|
|
|
|
|
const oAuthAllowed = noSpecificAuthExpected || allowedMethods.includes("OAUTH_CLIENT_CREDENTIALS");
|
|
|
|
|
const apiKeyAllowed = noSpecificAuthExpected || allowedMethods.includes("API_KEY");
|
|
|
|
|
const accessTokenAllowed = noSpecificAuthExpected || allowedMethods.includes("ACCESS_TOKEN");
|
|
|
|
|
const nextAuthAllowed = noSpecificAuthExpected || allowedMethods.includes("NEXT_AUTH");
|
|
|
|
|
const thirdPartyAccessTokenAllowed =
|
|
|
|
|
noSpecificAuthExpected || allowedMethods.includes("THIRD_PARTY_ACCESS_TOKEN");
|
|
|
|
|
|
|
|
|
|
if (oAuthClientId && oAuthClientSecret && oAuthAllowed) {
|
|
|
|
|
request.authMethod = AuthMethods["OAUTH_CLIENT"];
|
|
|
|
|
return await this.authenticateOAuthClient(oAuthClientId, oAuthClientSecret, request);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (bearerToken && (apiKeyAllowed || accessTokenAllowed)) {
|
|
|
|
|
const requestOrigin = request.get("Origin");
|
|
|
|
|
request.authMethod = isApiKey(bearerToken, this.config.get<string>("api.apiKeyPrefix") ?? "cal_")
|
|
|
|
|
? AuthMethods["API_KEY"]
|
|
|
|
|
: AuthMethods["ACCESS_TOKEN"];
|
|
|
|
|
return await this.authenticateBearerToken(bearerToken, request, requestOrigin);
|
|
|
|
|
if (bearerToken) {
|
|
|
|
|
if (!apiKeyAllowed && !accessTokenAllowed && thirdPartyAccessTokenAllowed) {
|
|
|
|
|
request.authMethod = AuthMethods["THIRD_PARTY_ACCESS_TOKEN"];
|
|
|
|
|
const result = await this.validateThirdPartyAccessToken(bearerToken, request);
|
|
|
|
|
if (result.success) {
|
|
|
|
|
return this.success(this.getSuccessUser(result.data));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (apiKeyAllowed || accessTokenAllowed) {
|
|
|
|
|
try {
|
|
|
|
|
const requestOrigin = request.get("Origin");
|
|
|
|
|
request.authMethod = isApiKey(bearerToken, this.config.get<string>("api.apiKeyPrefix") ?? "cal_")
|
|
|
|
|
? AuthMethods["API_KEY"]
|
|
|
|
|
: AuthMethods["ACCESS_TOKEN"];
|
|
|
|
|
return await this.authenticateBearerToken(bearerToken, request, requestOrigin);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
// failed to validate access token, try to validate third party token
|
|
|
|
|
if (thirdPartyAccessTokenAllowed && request.authMethod === AuthMethods["ACCESS_TOKEN"]) {
|
|
|
|
|
request.authMethod = AuthMethods["THIRD_PARTY_ACCESS_TOKEN"];
|
|
|
|
|
const result = await this.validateThirdPartyAccessToken(bearerToken, request);
|
|
|
|
|
if (result.success) {
|
|
|
|
|
return this.success(this.getSuccessUser(result.data));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// token was not third party token, rethrow error from authenticateBearerToken
|
|
|
|
|
if (err instanceof HttpException) {
|
|
|
|
|
return this.error(err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw new UnauthorizedException(`ApiAuthStrategy - Invalid Bearer token`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nextAuthSecret = this.config.get("next.authSecret", { infer: true });
|
|
|
|
@@ -83,8 +130,11 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
|
|
|
|
if (noAuthProvided) {
|
|
|
|
|
throw new UnauthorizedException(`ApiAuthStrategy - ${NO_AUTH_PROVIDED_MESSAGE}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw new UnauthorizedException(
|
|
|
|
|
`ApiAuthStrategy - Invalid authentication method. Please provide one of the allowed methods: ${allowedMethods}`
|
|
|
|
|
`ApiAuthStrategy - Invalid authentication method. Please provide one of the allowed methods: ${
|
|
|
|
|
allowedMethods && allowedMethods.length > 0 ? allowedMethods.join(", ") : "Any supported method"
|
|
|
|
|
}`
|
|
|
|
|
);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (err instanceof Error) {
|
|
|
|
@@ -271,4 +321,55 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
|
|
|
|
|
|
|
|
|
return user;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async validateThirdPartyAccessToken(
|
|
|
|
|
token: string,
|
|
|
|
|
request: ApiAuthGuardRequest
|
|
|
|
|
): Promise<{ success: true; data: UserWithProfile } | { success: false }> {
|
|
|
|
|
// Removed requiredScopes parameter
|
|
|
|
|
const encryptionKey = this.config.get<string>("CALENDSO_ENCRYPTION_KEY");
|
|
|
|
|
if (!encryptionKey) {
|
|
|
|
|
throw new InternalServerErrorException("CALENDSO_ENCRYPTION_KEY environment variable is not set.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let decodedToken: OAuthTokenPayload;
|
|
|
|
|
try {
|
|
|
|
|
decodedToken = jwt.verify(token, encryptionKey) as OAuthTokenPayload;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return { success: false };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!decodedToken || decodedToken.token_type !== "Access Token") {
|
|
|
|
|
return { success: false };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let user: UserWithProfile | null = null;
|
|
|
|
|
let organizationId: number | null = null;
|
|
|
|
|
|
|
|
|
|
if (decodedToken.userId) {
|
|
|
|
|
user = await this.userRepository.findByIdWithProfile(decodedToken.userId);
|
|
|
|
|
if (user) {
|
|
|
|
|
organizationId = this.usersService.getUserMainOrgId(user) as number;
|
|
|
|
|
}
|
|
|
|
|
} else if (decodedToken.teamId) {
|
|
|
|
|
const teamOwner = await this.userRepository.findOwnerByTeamIdWithProfile(decodedToken.teamId);
|
|
|
|
|
if (!teamOwner) {
|
|
|
|
|
throw new UnauthorizedException(
|
|
|
|
|
"ApiAuthStrategy - third-party token - No owner found for the associated team."
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
user = teamOwner;
|
|
|
|
|
organizationId =
|
|
|
|
|
teamOwner.profiles?.find((p) => p.organizationId === decodedToken.teamId)?.organizationId ?? null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new UnauthorizedException(
|
|
|
|
|
"ApiAuthStrategy - third-party token - No user or team owner associated with the token."
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
request.organizationId = organizationId;
|
|
|
|
|
return { success: true, data: user };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|