chore(server): drop api-host branch in OAuth discovery (#19768)

## Summary

Follow-up to #19755. Simplifies `OAuthDiscoveryController` by dropping
the `authorization_endpoint → frontend base URL` branch that was there
to make `api.twenty.com/mcp` paste-able in MCP clients.

We've decided not to support pasting `api.twenty.com/mcp` — users can
paste `app.twenty.com/mcp`, `<workspace>.twenty.com/mcp`, or a custom
domain, all of which serve both frontend and API. On those hosts,
`authorization_endpoint` was already pointed at the same host as
`issuer`, which is what we want.

## Change

- Remove `isApiHost` helper and the `authorizeBase` branch — use
`issuer` for `authorization_endpoint`.
- Drop now-unused `TwentyConfigService` and `DomainServerConfigService`
injections.
- Drop duplicate `DomainServerConfigModule` import from
`application-oauth.module.ts` (the module is no longer needed).

Net diff: +1 / -22 across 2 files.

## Breaking change

MCP clients configured with `https://api.twenty.com/mcp` will stop
working. They should be reconfigured with the host matching the
workspace they're connecting to (`<workspace>.twenty.com/mcp`,
`app.twenty.com/mcp`, or a custom domain).

## Test plan

- [x] `yarn jest --testPathPatterns="mcp-auth.guard"` → 2/2 passing
(unchanged)
- [x] `tsc --noEmit` clean on modified files
- [ ] Manual verification on staging: `app.twenty.com/mcp` and
`<workspace>.twenty.com/mcp` OAuth flow still works end-to-end

Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Félix Malfait
2026-04-16 17:01:55 +02:00
committed by GitHub
parent cb6953abe3
commit d3df58046c
2 changed files with 1 additions and 22 deletions
@@ -19,7 +19,6 @@ import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { DomainServerConfigModule } from 'src/engine/core-modules/domain/domain-server-config/domain-server-config.module';
@Module({
imports: [
@@ -33,13 +32,11 @@ import { DomainServerConfigModule } from 'src/engine/core-modules/domain/domain-
ApplicationCoreModule,
ApplicationInstallModule,
TokenModule,
DomainServerConfigModule,
FeatureFlagModule,
PermissionsModule,
ThrottlerModule,
TwentyConfigModule,
WorkspaceCacheStorageModule,
DomainServerConfigModule,
],
controllers: [
OAuthTokenController,
@@ -4,18 +4,13 @@ import { type Request } from 'express';
import { ALL_OAUTH_SCOPES } from 'src/engine/core-modules/application/application-oauth/constants/oauth-scopes';
import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service';
import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
import { cleanServerUrl } from 'src/utils/clean-server-url';
import { TWENTY_CLI_APPLICATION_REGISTRATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-cli-application-registration.constant';
@Controller('.well-known')
export class OAuthDiscoveryController {
constructor(
private readonly twentyConfigService: TwentyConfigService,
private readonly domainServerConfigService: DomainServerConfigService,
private readonly applicationRegistrationService: ApplicationRegistrationService,
) {}
@@ -23,13 +18,6 @@ export class OAuthDiscoveryController {
@UseGuards(PublicEndpointGuard, NoPermissionGuard)
async getAuthorizationServerMetadata(@Req() request: Request) {
const issuer = this.getRequestBaseUrl(request);
// /authorize is served by the frontend; SERVER_URL (API-only) has no such
// route, so we route the client to the default frontend base URL in that
// case. All other hosts (app.twenty.com, workspace subdomains, custom
// domains) serve both frontend and API.
const authorizeBase = this.isApiHost(request)
? cleanServerUrl(this.domainServerConfigService.getBaseUrl().toString())
: issuer;
const cliRegistration =
await this.applicationRegistrationService.findOneByUniversalIdentifier(
@@ -38,7 +26,7 @@ export class OAuthDiscoveryController {
return {
issuer,
authorization_endpoint: `${authorizeBase}/authorize`,
authorization_endpoint: `${issuer}/authorize`,
token_endpoint: `${issuer}/oauth/token`,
registration_endpoint: `${issuer}/oauth/register`,
revocation_endpoint: `${issuer}/oauth/revoke`,
@@ -80,10 +68,4 @@ export class OAuthDiscoveryController {
private getRequestBaseUrl(request: Request): string {
return `${request.protocol}://${request.get('host')}`;
}
private isApiHost(request: Request): boolean {
const serverUrl = this.twentyConfigService.get('SERVER_URL');
return request.get('host') === new URL(serverUrl).host;
}
}