Skip to content

Conversation

@ThyMinimalDev
Copy link
Contributor

@ThyMinimalDev ThyMinimalDev commented Dec 17, 2025

Fixes #26470

What does this PR do?

Adds a new OAuth2 controller for API v2 to support migrating OAuth endpoints from Next.js to the NestJS API. This includes fully functional implementations of client validation, PKCE verification, and JWT token generation/verification.

New endpoints created:

  • GET /v2/auth/oauth2/clients/:clientId - Returns OAuth client info (mirrors getClient.handler.ts)
  • POST /v2/auth/oauth2/clients/:clientId/authorize - Generates authorization code and redirects (mirrors generateAuthCode.handler.ts)
  • POST /v2/auth/oauth2/clients/:clientId/exchange - Exchanges code for tokens (mirrors token/route.ts)
  • POST /v2/auth/oauth2/clients/:clientId/refresh - Refreshes access token (mirrors refreshToken/route.ts)

Files created:

  • Controller: apps/api/v2/src/modules/auth/oauth2/controllers/oauth2.controller.ts
  • Service: packages/features/oauth/services/OAuthService.ts (shared for tRPC and API v2)
  • Repositories: packages/features/oauth/repositories/OAuthClientRepository.ts, AccessCodeRepository.ts
  • Input DTOs: authorize.input.ts, exchange.input.ts, refresh.input.ts
  • Output DTOs: oauth2-client.output.ts, oauth2-tokens.output.ts
  • Module: oauth2.module.ts
  • E2E Tests: oauth2.controller.e2e-spec.ts
  • DI modules: packages/features/oauth/di/ (tokens, modules, container)

Updates since last revision

Latest changes (PR comment fixes):

  • Removed unused GetOAuth2ClientInput class
  • Changed @ApiTags("Auth / OAuth2") to @ApiTags("OAuth2") for cleaner API organization
  • Moved OAuthClientFixture to separate fixture file (test/fixtures/repository/oauth2-client.repository.fixture.ts)
  • Added type property to OAuth2ClientDto to expose confidential/public client type
  • Renamed clientId to id in OAuth2ClientDto output (using @Expose({ name: "clientId" }) mapping)
  • Cleaned up duplicate provider declarations in oauth2.module.ts
  • Updated e2e tests to use new fixture and verify type property

Previous changes (tRPC handler fix):

  • Fixed generateAuthCode.handler.ts to check for ErrorWithCode instead of HttpError - the handler was still checking for HttpError in its catch block, but OAuthService now throws ErrorWithCode, causing error messages to be lost and replaced with "server_error"

Earlier updates:

  • Replaced HttpError with ErrorWithCode throughout OAuth files for consistent error handling
  • Updated OAuthService.ts to throw ErrorWithCode with appropriate ErrorCode enum values
  • Authorize endpoint returns HTTP 303 redirect with authorization code and state in query params
  • Made redirectUri a required input parameter (exact match validation)
  • Fixed P0 critical bug: token_type field name mismatch in DecodedRefreshToken interface
  • Added @UseGuards(ApiAuthGuard) to getClient and authorize endpoints

Mandatory Tasks (DO NOT REMOVE)

  • I have self-reviewed the code (A decent size PR without self-review might be rejected).
  • I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. N/A - new internal API endpoints hidden from public docs
  • I confirm automated tests are in place that prove my fix is effective or that my feature works.

How should this be tested?

  1. E2E Tests - Run TZ=UTC yarn jest --config ./jest-e2e.ts --testPathPattern="oauth2.controller.e2e-spec" in apps/api/v2
  2. Unit Tests - Run TZ=UTC yarn vitest run packages/trpc/server/routers/viewer/oAuth/generateAuthCode.handler.test.ts
  3. Redirect behavior - Verify authorize endpoint returns 303 with Location header containing code and state params
  4. Error redirects - Verify errors redirect to redirect URI with error and error_description query params
  5. DI wiring - Verify getOAuthService() from packages/features/oauth/di/OAuthService.container.ts returns a properly instantiated service

Checklist

  • My code follows the style guidelines of this project
  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new warnings

Human Review Checklist

  • IMPORTANT: getClient.handler.ts still checks for HttpError but OAuthService now throws ErrorWithCode - verify this doesn't cause error messages to be lost (may need same fix as generateAuthCode.handler.ts)
  • Verify E2E tests pass with the getToken mock approach (mocks next-auth/jwt for ApiAuthStrategy)
  • Verify authorize endpoint returns HTTP 303 redirect (not JSON)
  • Verify redirectUri is required and exact match validation works correctly
  • Verify no debug console.log statements remain in production code
  • Review as unknown as PrismaClient type casting in prisma-access-code.repository.ts and prisma-oauth-client.repository.ts
  • Verify ErrorWithCode is properly exported from platform-libraries and error handling correctly maps ErrorCode to HTTP status codes via getHttpStatusCode
  • Verify mapErrorToOAuthError correctly handles ErrorWithCode instances (checks error.code instead of error.statusCode)
  • Confirm @ApiExcludeController(true) correctly hides endpoints from Swagger docs
  • Verify @Expose({ name: "clientId" }) correctly maps id property to clientId in JSON output
  • Verify type property correctly exposes clientType enum value in OAuth2ClientDto

Link to Devin run: https://app.devin.ai/sessions/35eff6e6fcb340eea1ecaec74bf006f7
Requested by: Volnei Munhoz (volnei@cal.com) (@volnei)

- Add new OAuth2 module under /auth with controller, service, repository, and DTOs
- Implement skeleton endpoints:
  - GET /v2/auth/oauth2/clients/:clientId - Get OAuth client info
  - POST /v2/auth/oauth2/clients/:clientId/authorize - Generate authorization code
  - POST /v2/auth/oauth2/clients/:clientId/exchange - Exchange code for tokens
  - POST /v2/auth/oauth2/clients/:clientId/refresh - Refresh access token
- Create input DTOs for authorize, exchange, and refresh operations
- Create output DTOs for client info, authorization code, and tokens
- Register OAuth2Module in endpoints.module.ts

Co-Authored-By: morgan@cal.com <morgan@cal.com>
@devin-ai-integration
Copy link
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@vercel
Copy link

vercel bot commented Dec 17, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

3 Skipped Deployments
Project Deployment Review Updated (UTC)
cal Ignored Ignored Jan 7, 2026 10:38am
cal-companion Ignored Ignored Preview Jan 7, 2026 10:38am
cal-eu Ignored Ignored Jan 7, 2026 10:38am

…th2 controller

- Add OAuthService export to platform-libraries
- Refactor OAuth2Service to use OAuthService.validateClient() and OAuthService.verifyPKCE()
- Remove duplicate local implementations of validateClient and verifyPKCE methods

Co-Authored-By: morgan@cal.com <morgan@cal.com>
- Remove OAuthService export from platform-libraries (will be deprecated)
- Reimplement validateClient and verifyPKCE methods locally in OAuth2Service
- Use verifyCodeChallenge and generateSecret from platform-libraries directly

Co-Authored-By: morgan@cal.com <morgan@cal.com>
… teams

- Create AccessCodeRepository for access code Prisma operations
- Add findTeamBySlugWithAdminRole method to TeamsRepository
- Move generateAuthorizationCode, createTokens, verifyRefreshToken to OAuth2Service
- Update OAuth2Repository to only contain OAuth client operations
- Update OAuth2Module to import TeamsModule and provide AccessCodeRepository

Addresses PR review comments to properly separate concerns

Co-Authored-By: morgan@cal.com <morgan@cal.com>
- Implement createTokens method using jsonwebtoken to sign access and refresh tokens
- Implement verifyRefreshToken method to verify JWT refresh tokens
- Add ConfigService injection for CALENDSO_ENCRYPTION_KEY access
- Import ConfigModule in OAuth2Module for dependency injection

Based on logic from apps/web/app/api/auth/oauth/token/route.ts and
apps/web/app/api/auth/oauth/refreshToken/route.ts

Co-Authored-By: morgan@cal.com <morgan@cal.com>
…r OAuth2

- Add @UseGuards(ApiAuthGuard) to getClient endpoint for authentication
- Add comprehensive e2e tests for all OAuth2 endpoints:
  - GET /v2/auth/oauth2/clients/:clientId
  - POST /v2/auth/oauth2/clients/:clientId/authorize
  - POST /v2/auth/oauth2/clients/:clientId/exchange
  - POST /v2/auth/oauth2/clients/:clientId/refresh
- Tests cover happy paths and error cases (invalid client, invalid code, etc.)

Co-Authored-By: morgan@cal.com <morgan@cal.com>
Co-Authored-By: morgan@cal.com <morgan@cal.com>
hbjORbj
hbjORbj previously approved these changes Dec 22, 2025
Copy link
Contributor

@hbjORbj hbjORbj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great stuff

@PeerRich PeerRich requested a review from a team as a code owner December 24, 2025 14:38
@volnei volnei changed the title feat: OAuth2 controller api v2 + refactor oAuth Trpc handlers [block] feat: OAuth2 controller api v2 + refactor oAuth Trpc handlers - merge blocked until manual tests completed Dec 24, 2025
@github-actions

This comment was marked as resolved.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 1, 2026

This PR has been marked as stale due to inactivity. If you're still working on it or need any help, please let us know or update the PR to keep it active.

@github-actions github-actions bot added the Stale label Jan 1, 2026
Copy link
Contributor

@supalarry supalarry left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review:

providers: [PrismaAccessCodeRepository, PrismaOAuthClientRepository, PrismaTeamRepository, OAuthService],
exports: [OAuthService],
})
export class oAuthServiceModule {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have the new code for oauth module, but we have the controllers related to platform oauth:

  1. 'apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller.ts'
  2. 'apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.ts'

and then services and repositories they call all point to platform oauth clients.

Which is why I think we need to rename controllers above, their services and repositories with a "platform" prefix not only in file names but also class names with "Platform", because otherwise it will be confusing to find files and then code within them.


import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";

export class OAuth2ClientDto {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also return whether the oauth client is confidential or public under a new "type" property ?

@ThyMinimalDev ThyMinimalDev marked this pull request as draft January 5, 2026 11:56
auto-merge was automatically disabled January 5, 2026 11:56

Pull request was converted to draft

@github-actions github-actions bot removed the Stale label Jan 7, 2026
devin-ai-integration bot and others added 2 commits January 7, 2026 10:18
Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>
- Remove unused GetOAuth2ClientInput class
- Change API tag from 'Auth / OAuth2' to 'OAuth2'
- Move OAuthClientFixture to separate fixture file (oauth2-client.repository.fixture.ts)
- Add 'type' property to OAuth2ClientDto for confidential/public client type
- Rename 'clientId' to 'id' in OAuth2ClientDto (using @expose mapping)
- Clean up duplicate provider declarations in oauth2.module.ts
- Update e2e tests to use new fixture and verify type property

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>
@github-actions github-actions bot added the High priority Created by Linear-GitHub Sync label Jan 7, 2026
@volnei volnei changed the title [block] feat: OAuth2 controller api v2 + refactor oAuth Trpc handlers - merge blocked until manual tests completed feat: OAuth2 controller api v2 + refactor oAuth Trpc handlers - merge blocked until manual tests completed Jan 7, 2026
@volnei volnei changed the title feat: OAuth2 controller api v2 + refactor oAuth Trpc handlers - merge blocked until manual tests completed feat: OAuth2 controller api v2 + refactor oAuth Trpc handlers Jan 7, 2026
@volnei volnei marked this pull request as ready for review January 7, 2026 10:52
@volnei volnei requested review from hbjORbj and supalarry January 7, 2026 10:53
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 44 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/features/oauth/services/OAuthService.ts">

<violation number="1" location="packages/features/oauth/services/OAuthService.ts:193">
P2: Property mismatch: errors are thrown with `{ reason: "..." }` but the handler reads `error.data?.cause`. This causes detailed error descriptions to be lost, always falling back to the generic OAuth error message.</violation>
</file>

<file name="packages/features/ee/teams/repositories/TeamRepository.ts">

<violation number="1" location="packages/features/ee/teams/repositories/TeamRepository.ts:609">
P2: Consider adding `accepted: true` to the membership filter. Without this check, users with pending admin invitations (who haven't accepted yet) could be treated as having admin access.</violation>
</file>

<file name="apps/api/v2/src/modules/auth/oauth2/controllers/oauth2.controller.ts">

<violation number="1" location="apps/api/v2/src/modules/auth/oauth2/controllers/oauth2.controller.ts:102">
P1: Potential open redirect: If `getClient` throws an unexpected error (e.g., database error), the code redirects to the unvalidated `body.redirectUri`. Per OAuth 2.0 spec, invalid/unverified redirect URIs should not receive redirects. Consider throwing an HTTP exception for all errors from `getClient` instead of redirecting to an unvalidated URI.</violation>
</file>

<file name="apps/api/v2/src/modules/auth/oauth2/outputs/oauth2-client.output.ts">

<violation number="1" location="apps/api/v2/src/modules/auth/oauth2/outputs/oauth2-client.output.ts:14">
P2: Swagger documentation will show `id` as the property name, but the actual JSON output will use `clientId` due to `@Expose({ name: "clientId" })`. Add `name: "clientId"` to the `@ApiProperty` decorator to align documentation with runtime behavior.</violation>

<violation number="2" location="apps/api/v2/src/modules/auth/oauth2/outputs/oauth2-client.output.ts:56">
P2: Swagger documentation will show `type` as the property name, but the actual JSON output will use `clientType` due to `@Expose({ name: "clientType" })`. Add `name: "clientType"` to the `@ApiProperty` decorator to align documentation with runtime behavior.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

throw new HttpException(err.message, statusCode);
}
}
const errorRedirectUrl = this.oAuthService.buildErrorRedirectUrl(body.redirectUri, err, body.state);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Potential open redirect: If getClient throws an unexpected error (e.g., database error), the code redirects to the unvalidated body.redirectUri. Per OAuth 2.0 spec, invalid/unverified redirect URIs should not receive redirects. Consider throwing an HTTP exception for all errors from getClient instead of redirecting to an unvalidated URI.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/v2/src/modules/auth/oauth2/controllers/oauth2.controller.ts, line 102:

<comment>Potential open redirect: If `getClient` throws an unexpected error (e.g., database error), the code redirects to the unvalidated `body.redirectUri`. Per OAuth 2.0 spec, invalid/unverified redirect URIs should not receive redirects. Consider throwing an HTTP exception for all errors from `getClient` instead of redirecting to an unvalidated URI.</comment>

<file context>
@@ -0,0 +1,168 @@
+          throw new HttpException(err.message, statusCode);
+        }
+      }
+      const errorRedirectUrl = this.oAuthService.buildErrorRedirectUrl(body.redirectUri, err, body.state);
+      return res.redirect(303, errorRedirectUrl);
+    }
</file context>
Fix with Cubic

if (validOAuthErrors.includes(error.message)) {
return {
error: error.message,
errorDescription: (error.data?.cause as string | undefined) ?? error.message,
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Property mismatch: errors are thrown with { reason: "..." } but the handler reads error.data?.cause. This causes detailed error descriptions to be lost, always falling back to the generic OAuth error message.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/features/oauth/services/OAuthService.ts, line 193:

<comment>Property mismatch: errors are thrown with `{ reason: "..." }` but the handler reads `error.data?.cause`. This causes detailed error descriptions to be lost, always falling back to the generic OAuth error message.</comment>

<file context>
@@ -1,61 +1,454 @@
+      if (validOAuthErrors.includes(error.message)) {
+        return {
+          error: error.message,
+          errorDescription: (error.data?.cause as string | undefined) ?? error.message,
+        };
+      }
</file context>
Fix with Cubic

where: {
slug: teamSlug,
members: {
some: {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Consider adding accepted: true to the membership filter. Without this check, users with pending admin invitations (who haven't accepted yet) could be treated as having admin access.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/features/ee/teams/repositories/TeamRepository.ts, line 609:

<comment>Consider adding `accepted: true` to the membership filter. Without this check, users with pending admin invitations (who haven't accepted yet) could be treated as having admin access.</comment>

<file context>
@@ -599,4 +599,21 @@ export class TeamRepository {
+      where: {
+        slug: teamSlug,
+        members: {
+          some: {
+            userId,
+            role: {
</file context>
Fix with Cubic

enum: OAuthClientType,
})
@IsEnum(OAuthClientType)
@Expose({ name: "clientType" })
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Swagger documentation will show type as the property name, but the actual JSON output will use clientType due to @Expose({ name: "clientType" }). Add name: "clientType" to the @ApiProperty decorator to align documentation with runtime behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/v2/src/modules/auth/oauth2/outputs/oauth2-client.output.ts, line 56:

<comment>Swagger documentation will show `type` as the property name, but the actual JSON output will use `clientType` due to `@Expose({ name: "clientType" })`. Add `name: "clientType"` to the `@ApiProperty` decorator to align documentation with runtime behavior.</comment>

<file context>
@@ -0,0 +1,74 @@
+    enum: OAuthClientType,
+  })
+  @IsEnum(OAuthClientType)
+  @Expose({ name: "clientType" })
+  type!: OAuthClientType;
+}
</file context>
Fix with Cubic

example: "clxxxxxxxxxxxxxxxx",
})
@IsString()
@Expose({ name: "clientId" })
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Swagger documentation will show id as the property name, but the actual JSON output will use clientId due to @Expose({ name: "clientId" }). Add name: "clientId" to the @ApiProperty decorator to align documentation with runtime behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/v2/src/modules/auth/oauth2/outputs/oauth2-client.output.ts, line 14:

<comment>Swagger documentation will show `id` as the property name, but the actual JSON output will use `clientId` due to `@Expose({ name: "clientId" })`. Add `name: "clientId"` to the `@ApiProperty` decorator to align documentation with runtime behavior.</comment>

<file context>
@@ -0,0 +1,74 @@
+    example: "clxxxxxxxxxxxxxxxx",
+  })
+  @IsString()
+  @Expose({ name: "clientId" })
+  id!: string;
+
</file context>
Fix with Cubic

@keithwillcode keithwillcode dismissed supalarry’s stale review January 7, 2026 11:04

Changes addressed

@keithwillcode keithwillcode merged commit 61a932f into main Jan 7, 2026
78 of 82 checks passed
@keithwillcode keithwillcode deleted the devin/oauth2-controller-skeleton-1765988792 branch January 7, 2026 11:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core area: core, team members only foundation High priority Created by Linear-GitHub Sync ready-for-e2e size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: OAuth v2 endpoints

7 participants