Twenty sdk cli oauth (#18638)
<img width="1418" height="804" alt="image" src="https://github.com/user-attachments/assets/de6c8222-6496-4a71-bc21-7e5e1269d5cb" /> --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
co-authored by
Félix Malfait
Félix Malfait
parent
111debc1ce
commit
731e297147
+6
@@ -35,6 +35,8 @@ import { FileStorageService } from 'src/engine/core-modules/file-storage/file-st
|
||||
import { FileDTO } from 'src/engine/core-modules/file/dtos/file.dto';
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { DevelopmentGuard } from 'src/engine/guards/development.guard';
|
||||
import {
|
||||
@@ -109,10 +111,14 @@ export class ApplicationDevelopmentResolver {
|
||||
async generateApplicationToken(
|
||||
@Args() { applicationId }: GenerateApplicationTokenInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
@AuthUser({ allowUndefined: true }) user?: { id: string },
|
||||
@AuthUserWorkspaceId() userWorkspaceId?: string,
|
||||
): Promise<ApplicationTokenPairDTO> {
|
||||
return this.applicationTokenService.generateApplicationTokenPair({
|
||||
workspaceId,
|
||||
applicationId,
|
||||
userId: user?.id,
|
||||
userWorkspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -13,13 +13,13 @@ import { OAuthTokenController } from 'src/engine/core-modules/application/applic
|
||||
import { OAuthService } from 'src/engine/core-modules/application/application-oauth/oauth.service';
|
||||
import { ApplicationRegistrationModule } from 'src/engine/core-modules/application/application-registration/application-registration.module';
|
||||
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
|
||||
import { DomainServerConfigModule } from 'src/engine/core-modules/domain/domain-server-config/domain-server-config.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
|
||||
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
|
||||
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: [
|
||||
@@ -39,6 +39,7 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
|
||||
ThrottlerModule,
|
||||
TwentyConfigModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
DomainServerConfigModule,
|
||||
],
|
||||
controllers: [
|
||||
OAuthTokenController,
|
||||
|
||||
+14
-2
@@ -1,24 +1,33 @@
|
||||
import { Controller, Get, UseGuards } from '@nestjs/common';
|
||||
|
||||
import { ALL_OAUTH_SCOPES } from 'src/engine/core-modules/application/application-oauth/constants/oauth-scopes';
|
||||
import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service';
|
||||
import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.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 { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service';
|
||||
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,
|
||||
) {}
|
||||
|
||||
@Get('oauth-authorization-server')
|
||||
@UseGuards(PublicEndpointGuard, NoPermissionGuard)
|
||||
getAuthorizationServerMetadata() {
|
||||
async getAuthorizationServerMetadata() {
|
||||
const serverUrl = this.twentyConfigService.get('SERVER_URL');
|
||||
|
||||
const frontUrl = this.domainServerConfigService.getFrontUrl().toString();
|
||||
|
||||
const cliRegistration =
|
||||
await this.applicationRegistrationService.findOneByUniversalIdentifier(
|
||||
TWENTY_CLI_APPLICATION_REGISTRATION.universalIdentifier,
|
||||
);
|
||||
|
||||
return {
|
||||
issuer: serverUrl,
|
||||
authorization_endpoint: `${frontUrl.replace(/\/$/, '')}/authorize`,
|
||||
@@ -37,6 +46,9 @@ export class OAuthDiscoveryController {
|
||||
token_endpoint_auth_methods_supported: ['client_secret_post', 'none'],
|
||||
revocation_endpoint_auth_methods_supported: ['client_secret_post'],
|
||||
introspection_endpoint_auth_methods_supported: ['client_secret_post'],
|
||||
...(cliRegistration
|
||||
? { cli_client_id: cliRegistration.oAuthClientId }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+24
-5
@@ -10,6 +10,7 @@ import { v4 } from 'uuid';
|
||||
|
||||
import { ALL_OAUTH_SCOPES } from 'src/engine/core-modules/application/application-oauth/constants/oauth-scopes';
|
||||
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
|
||||
import { TWENTY_CLI_APPLICATION_REGISTRATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-cli-application-registration.constant';
|
||||
import {
|
||||
ApplicationRegistrationException,
|
||||
ApplicationRegistrationExceptionCode,
|
||||
@@ -327,12 +328,30 @@ export class ApplicationRegistrationService {
|
||||
await this.applicationRegistrationRepository.save(registration);
|
||||
}
|
||||
|
||||
async findManyBySourceType(
|
||||
sourceType: ApplicationRegistrationSourceType,
|
||||
): Promise<ApplicationRegistrationEntity[]> {
|
||||
return this.applicationRegistrationRepository.find({
|
||||
where: { sourceType },
|
||||
async createCliRegistrationIfNotExists(): Promise<ApplicationRegistrationEntity | null> {
|
||||
const existing = await this.findOneByUniversalIdentifier(
|
||||
TWENTY_CLI_APPLICATION_REGISTRATION.universalIdentifier,
|
||||
);
|
||||
|
||||
if (isDefined(existing)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const registration = this.applicationRegistrationRepository.create({
|
||||
universalIdentifier:
|
||||
TWENTY_CLI_APPLICATION_REGISTRATION.universalIdentifier,
|
||||
name: TWENTY_CLI_APPLICATION_REGISTRATION.name,
|
||||
description: TWENTY_CLI_APPLICATION_REGISTRATION.description,
|
||||
oAuthClientId: v4(),
|
||||
oAuthClientSecretHash: null,
|
||||
oAuthRedirectUris: [],
|
||||
oAuthScopes: TWENTY_CLI_APPLICATION_REGISTRATION.oAuthScopes,
|
||||
ownerWorkspaceId: null,
|
||||
sourceType: ApplicationRegistrationSourceType.OAUTH_ONLY,
|
||||
createdByUserId: null,
|
||||
});
|
||||
|
||||
return this.applicationRegistrationRepository.save(registration);
|
||||
}
|
||||
|
||||
async findManyListed(): Promise<ApplicationRegistrationEntity[]> {
|
||||
|
||||
@@ -519,15 +519,45 @@ export class AuthService {
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!applicationRegistration.oAuthRedirectUris.includes(
|
||||
authorizeAppInput.redirectUrl,
|
||||
)
|
||||
) {
|
||||
throw new AuthException(
|
||||
`redirectUrl mismatch for '${clientId}'`,
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
// RFC 8252 §7.3: Native apps using loopback redirect URIs may use any port.
|
||||
// When a registration has no explicit redirect URIs (e.g. the seeded CLI registration),
|
||||
// allow any loopback redirect URI.
|
||||
const hasRegisteredRedirectUris =
|
||||
applicationRegistration.oAuthRedirectUris.length > 0;
|
||||
|
||||
if (hasRegisteredRedirectUris) {
|
||||
if (
|
||||
!applicationRegistration.oAuthRedirectUris.includes(
|
||||
authorizeAppInput.redirectUrl,
|
||||
)
|
||||
) {
|
||||
throw new AuthException(
|
||||
`redirectUrl mismatch for '${clientId}'`,
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let redirectUrl: URL;
|
||||
|
||||
try {
|
||||
redirectUrl = new URL(authorizeAppInput.redirectUrl);
|
||||
} catch {
|
||||
throw new AuthException(
|
||||
`Invalid redirectUrl for '${clientId}'`,
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
const isLoopback =
|
||||
redirectUrl.hostname === 'localhost' ||
|
||||
redirectUrl.hostname === '127.0.0.1';
|
||||
|
||||
if (!isLoopback) {
|
||||
throw new AuthException(
|
||||
`redirectUrl mismatch for '${clientId}'`,
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate requested scopes are a subset of the registration's allowed scopes
|
||||
|
||||
-3
@@ -76,9 +76,6 @@ const createSignInUpServiceForTests = () => {
|
||||
{
|
||||
emitCustomBatchEvent: jest.fn(),
|
||||
} as any,
|
||||
{
|
||||
getHttpClient: jest.fn(),
|
||||
} as any,
|
||||
mockTwentyConfigService as any,
|
||||
{
|
||||
generateSubdomain: jest.fn(),
|
||||
|
||||
@@ -33,7 +33,6 @@ import { FileCorePictureService } from 'src/engine/core-modules/file/file-core-p
|
||||
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
|
||||
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
|
||||
import { TelemetryEventType } from 'src/engine/core-modules/telemetry/telemetry-event.type';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
@@ -59,7 +58,6 @@ export class SignInUpService {
|
||||
private readonly userWorkspaceService: UserWorkspaceService,
|
||||
private readonly onboardingService: OnboardingService,
|
||||
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
|
||||
private readonly secureHttpClientService: SecureHttpClientService,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
private readonly subdomainManagerService: SubdomainManagerService,
|
||||
private readonly userService: UserService,
|
||||
|
||||
Reference in New Issue
Block a user