fix: v2 ignore permissions check for 3rd party oauth (#22588)

This commit is contained in:
Lauris Skraucis authored and GitHub committed 2025-07-18 13:53:51 +00:00
1 parent f67522dbbd
commit 2115a045ec
7 files changed
+75 -38

No files matched your search

@@ -1,6 +1,7 @@
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthClientsOutputService } from "@/modules/oauth-clients/services/oauth-clients/oauth-clients-output.service";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { TokensService } from "@/modules/tokens/tokens.service";
import { createMock } from "@golevelup/ts-jest";
import { ExecutionContext } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
@@ -19,6 +20,7 @@ describe("PermissionsGuard", () => {
guard = new PermissionsGuard(
reflector,
createMock<TokensRepository>(),
createMock<TokensService>(),
createMock<ConfigService>({
get: jest.fn().mockImplementation((key: string) => {
switch (key) {
@@ -118,6 +120,7 @@ describe("PermissionsGuard", () => {
jest
.spyOn(guard, "getOAuthClientByAccessToken")
.mockResolvedValue(getMockOAuthClient(oAuthClientPermissions));
jest.spyOn(guard, "getDecodedThirdPartyAccessToken").mockReturnValue(null);
await expect(guard.canActivate(mockContext)).rejects.toThrow(
"PermissionsGuard - oAuth client with id=100 does not have the required permissions=SCHEDULE_WRITE"
@@ -133,11 +136,22 @@ describe("PermissionsGuard", () => {
jest
.spyOn(guard, "getOAuthClientByAccessToken")
.mockResolvedValue(getMockOAuthClient(oAuthClientPermissions));
jest.spyOn(guard, "getDecodedThirdPartyAccessToken").mockReturnValue(null);
await expect(guard.canActivate(mockContext)).rejects.toThrow(
"PermissionsGuard - oAuth client with id=100 does not have the required permissions=SCHEDULE_WRITE, SCHEDULE_READ"
);
});
it("should return true for 3rd party access token", async () => {
const mockContext = createMockExecutionContext({ Authorization: "Bearer token" });
jest.spyOn(guard, "getDecodedThirdPartyAccessToken").mockReturnValue({
scope: ["scope"],
token_type: "Bearer",
});
await expect(guard.canActivate(mockContext)).resolves.toBe(true);
});
});
describe("when OAuth id is provided", () => {
@@ -3,6 +3,7 @@ import { Permissions } from "@/modules/auth/decorators/permissions/permissions.d
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthClientsOutputService } from "@/modules/oauth-clients/services/oauth-clients/oauth-clients-output.service";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { TokensService } from "@/modules/tokens/tokens.service";
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Reflector } from "@nestjs/core";
@@ -17,6 +18,7 @@ export class PermissionsGuard implements CanActivate {
constructor(
private reflector: Reflector,
private tokensRepository: TokensRepository,
private tokensService: TokensService,
private readonly config: ConfigService,
private readonly oAuthClientRepository: OAuthClientRepository,
private readonly oAuthClientsOutputService: OAuthClientsOutputService
@@ -35,9 +37,10 @@ export class PermissionsGuard implements CanActivate {
const nextAuthToken = await getToken({ req: request, secret: nextAuthSecret });
const oAuthClientId = request.params?.clientId || request.get(X_CAL_CLIENT_ID);
const apiKey = bearerToken && isApiKey(bearerToken, this.config.get("api.apiKeyPrefix") ?? "cal_");
const isThirdPartyBearerToken = bearerToken && this.getDecodedThirdPartyAccessToken(bearerToken);
// only check permissions for accessTokens attached to an oAuth Client or oAuth credentials, not for next token or api key
if (nextAuthToken || apiKey) {
// only check permissions for accessTokens attached to platform oAuth Client or platform oAuth credentials, not for next token or api key or third party oauth client
if (nextAuthToken || apiKey || isThirdPartyBearerToken) {
return true;
}
@@ -87,4 +90,8 @@ export class PermissionsGuard implements CanActivate {
}
return oAuthClient;
}
getDecodedThirdPartyAccessToken(bearerToken: string) {
return this.tokensService.getDecodedThirdPartyAccessToken(bearerToken);
}
}
@@ -9,7 +9,7 @@ import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.se
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { ProfilesModule } from "@/modules/profiles/profiles.module";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersService } from "@/modules/users/services/users.service";
import { UsersRepository } from "@/modules/users/users.repository";
import { ExecutionContext, HttpException } from "@nestjs/common";
@@ -64,6 +64,7 @@ describe("ApiAuthStrategy", () => {
load: [appConfig],
}),
ProfilesModule,
TokensModule,
],
providers: [
MockedRedisService,
@@ -77,7 +78,6 @@ describe("ApiAuthStrategy", () => {
OAuthClientRepository,
PrismaReadService,
PrismaWriteService,
TokensRepository,
JwtService,
DeploymentsRepository,
NestJwtService,
@@ -8,32 +8,20 @@ import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repo
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
import { ProfilesRepository } from "@/modules/profiles/profiles.repository";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { TokensService } from "@/modules/tokens/tokens.service";
import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
import {
HttpException,
Injectable,
InternalServerErrorException,
UnauthorizedException,
} from "@nestjs/common";
import { 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;
@@ -53,6 +41,7 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
private readonly config: ConfigService,
private readonly oauthFlowService: OAuthFlowService,
private readonly tokensRepository: TokensRepository,
private readonly tokensService: TokensService,
private readonly userRepository: UsersRepository,
private readonly apiKeyRepository: ApiKeysRepository,
private readonly oauthRepository: OAuthClientRepository,
@@ -323,20 +312,8 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
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") {
const decodedToken = this.tokensService.getDecodedThirdPartyAccessToken(token);
if (!decodedToken) {
return { success: false };
}
@@ -9,12 +9,12 @@ import { CredentialsRepository } from "@/modules/credentials/credentials.reposit
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersRepository } from "@/modules/users/users.repository";
import { Module } from "@nestjs/common";
@Module({
imports: [],
imports: [TokensModule],
providers: [
GCalService,
GoogleCalendarService,
@@ -22,7 +22,6 @@ import { Module } from "@nestjs/common";
BookingReferencesRepository_2024_08_13,
CredentialsRepository,
CalendarsService,
TokensRepository,
SelectedCalendarsRepository,
PrismaReadService,
PrismaWriteService,
@@ -1,10 +1,12 @@
import { JwtModule } from "@/modules/jwt/jwt.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { TokensService } from "@/modules/tokens/tokens.service";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule],
providers: [TokensRepository],
exports: [TokensRepository],
imports: [PrismaModule, JwtModule],
providers: [TokensRepository, TokensService],
exports: [TokensRepository, TokensService],
})
export class TokensModule {}
@@ -0,0 +1,38 @@
import { Injectable, InternalServerErrorException } from "@nestjs/common";
import { Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import * as jwt from "jsonwebtoken";
type OAuthTokenPayload = {
userId?: number;
teamId?: number;
scope: string[];
token_type: string;
};
@Injectable()
export class TokensService {
private readonly logger = new Logger("TokensService");
constructor(private readonly config: ConfigService) {}
getDecodedThirdPartyAccessToken(token: string): OAuthTokenPayload | null {
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 null;
}
if (!decodedToken || decodedToken.token_type !== "Access Token") {
return null;
}
return decodedToken;
}
}