fix(auth): Improve error management with sso + fix microsoft saml (#9799)

Fix #9760 #9758
This commit is contained in:
Antoine Moreaux
2025-01-24 10:36:18 +01:00
committed by GitHub
parent 3c85516f77
commit 5783c41df2
49 changed files with 505 additions and 309 deletions

View File

@ -62,12 +62,12 @@ export const SettingsSSOSAMLForm = () => {
if (isDefined(e.target.files)) {
const text = await e.target.files[0].text();
const samlMetadataParsed = parseSAMLMetadataFromXMLFile(text);
e.target.value = '';
if (!samlMetadataParsed.success) {
enqueueSnackBar('Invalid File', {
return enqueueSnackBar('Invalid File', {
variant: SnackBarVariant.Error,
duration: 2000,
});
return;
}
setValue('ssoURL', samlMetadataParsed.data.ssoUrl);
setValue('certificate', samlMetadataParsed.data.certificate);

View File

@ -8,6 +8,29 @@ const validator = z.object({
certificate: z.string().min(1),
});
const getByPrefixAndKey = (
xmlDoc: Document | Element,
key: string,
prefix = 'md',
): Element | undefined => {
return (
xmlDoc.getElementsByTagName(`${prefix}:${key}`)?.[0] ??
xmlDoc.getElementsByTagName(`${key}`)?.[0]
);
};
const getAllByPrefixAndKey = (
xmlDoc: Document | Element,
key: string,
prefix = 'md',
) => {
const withPrefix = xmlDoc.getElementsByTagName(`${prefix}:${key}`);
if (withPrefix.length !== 0) {
return Array.from(withPrefix);
}
return Array.from(xmlDoc.getElementsByTagName(`${key}`));
};
export const parseSAMLMetadataFromXMLFile = (
xmlString: string,
):
@ -20,33 +43,44 @@ export const parseSAMLMetadataFromXMLFile = (
throw new Error('Error parsing XML');
}
const entityDescriptor = xmlDoc.getElementsByTagName(
'md:EntityDescriptor',
)?.[0];
const idpSSODescriptor = xmlDoc.getElementsByTagName(
'md:IDPSSODescriptor',
)?.[0];
const keyDescriptor = xmlDoc.getElementsByTagName('md:KeyDescriptor')[0];
const keyInfo = keyDescriptor?.getElementsByTagName('ds:KeyInfo')[0];
const x509Data = keyInfo?.getElementsByTagName('ds:X509Data')[0];
const x509Certificate = x509Data
?.getElementsByTagName('ds:X509Certificate')?.[0]
.textContent?.trim();
const entityDescriptor = getByPrefixAndKey(xmlDoc, 'EntityDescriptor');
if (!entityDescriptor) throw new Error('No EntityDescriptor found');
const singleSignOnServices = Array.from(
idpSSODescriptor.getElementsByTagName('md:SingleSignOnService'),
).map((service) => ({
Binding: service.getAttribute('Binding'),
Location: service.getAttribute('Location'),
}));
const IDPSSODescriptor = getByPrefixAndKey(xmlDoc, 'IDPSSODescriptor');
if (!IDPSSODescriptor) throw new Error('No IDPSSODescriptor found');
const keyDescriptors = getByPrefixAndKey(IDPSSODescriptor, 'KeyDescriptor');
if (!keyDescriptors) throw new Error('No KeyDescriptor found');
const keyInfo = getByPrefixAndKey(keyDescriptors, 'KeyInfo', 'ds');
if (!keyInfo) throw new Error('No KeyInfo found');
const x509Data = getByPrefixAndKey(keyInfo, 'X509Data', 'ds');
if (!x509Data) throw new Error('No X509Data found');
const x509Certificate = getByPrefixAndKey(
x509Data,
'X509Certificate',
'ds',
)?.textContent?.trim();
if (!x509Certificate) throw new Error('No X509Certificate found');
const singleSignOnServices = getAllByPrefixAndKey(
IDPSSODescriptor,
'SingleSignOnService',
);
const result = {
ssoUrl: singleSignOnServices.find((singleSignOnService) => {
return (
singleSignOnService.Binding ===
'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
);
})?.Location,
ssoUrl: singleSignOnServices
.map((service) => ({
Binding: service.getAttribute('Binding'),
Location: service.getAttribute('Location'),
}))
.find(
(singleSignOnService) =>
singleSignOnService.Binding ===
'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
)?.Location,
certificate: x509Certificate,
entityID: entityDescriptor?.getAttribute('entityID'),
};

View File

@ -5,17 +5,17 @@ import { z } from 'zod';
export const SSOIdentitiesProvidersOIDCParamsSchema = z
.object({
type: z.literal('OIDC'),
clientID: z.string().optional(),
clientSecret: z.string().optional(),
clientID: z.string().nonempty(),
clientSecret: z.string().nonempty(),
})
.required();
export const SSOIdentitiesProvidersSAMLParamsSchema = z
.object({
type: z.literal('SAML'),
id: z.string().optional(),
ssoURL: z.string().url().optional(),
certificate: z.string().optional(),
id: z.string().nonempty(),
ssoURL: z.string().url().nonempty(),
certificate: z.string().nonempty(),
})
.required();
@ -27,8 +27,8 @@ export const SSOIdentitiesProvidersParamsSchema = z
.and(
z
.object({
name: z.string().min(1),
issuer: z.string().url().optional(),
name: z.string().nonempty(),
issuer: z.string().url().nonempty(),
})
.required(),
);

View File

@ -21,7 +21,7 @@ import { Query } from 'src/engine/api/rest/core/types/query.type';
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
@Injectable()
export class CoreQueryBuilderFactory {

View File

@ -1,7 +1,6 @@
import { CustomException } from 'src/utils/custom-exception';
export class AuthException extends CustomException {
code: AuthExceptionCode;
constructor(message: string, code: AuthExceptionCode) {
super(message, code);
}

View File

@ -44,6 +44,7 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
import { GuardRedirectModule } from 'src/engine/core-modules/guard-redirect/guard-redirect.module';
import { AuthResolver } from './auth.resolver';
@ -81,6 +82,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
FeatureFlagModule,
WorkspaceInvitationModule,
EmailVerificationModule,
GuardRedirectModule,
],
controllers: [
GoogleAuthController,

View File

@ -3,7 +3,7 @@ import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { CaptchaGuard } from 'src/engine/core-modules/captcha/captcha.guard';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EmailVerificationService } from 'src/engine/core-modules/email-verification/services/email-verification.service';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { UserService } from 'src/engine/core-modules/user/services/user.service';

View File

@ -31,7 +31,7 @@ import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/l
import { RenewTokenService } from 'src/engine/core-modules/auth/token/services/renew-token.service';
import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service';
import { CaptchaGuard } from 'src/engine/core-modules/captcha/captcha.guard';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EmailVerificationService } from 'src/engine/core-modules/email-verification/services/email-verification.service';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { UserService } from 'src/engine/core-modules/user/services/user.service';

View File

@ -24,7 +24,7 @@ import { GoogleAPIsRequest } from 'src/engine/core-modules/auth/types/google-api
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
@Controller('auth/google-apis')
@UseFilters(AuthRestApiExceptionFilter)

View File

@ -22,8 +22,9 @@ import { GoogleProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/
import { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
import { GoogleRequest } from 'src/engine/core-modules/auth/strategies/google.auth.strategy';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
@Controller('auth/google')
@UseFilters(AuthRestApiExceptionFilter)
@ -32,6 +33,7 @@ export class GoogleAuthController {
private readonly loginTokenService: LoginTokenService,
private readonly authService: AuthService,
private readonly domainManagerService: DomainManagerService,
private readonly environmentService: EnvironmentService,
@InjectRepository(User, 'core')
private readonly userRepository: Repository<User>,
) {}
@ -120,9 +122,11 @@ export class GoogleAuthController {
} catch (err) {
if (err instanceof AuthException) {
return res.redirect(
this.domainManagerService.computeRedirectErrorUrl(err.message, {
subdomain: currentWorkspace?.subdomain,
}),
this.domainManagerService.computeRedirectErrorUrl(
err.message,
currentWorkspace?.subdomain ??
this.environmentService.get('DEFAULT_SUBDOMAIN'),
),
);
}
throw new AuthException(err, AuthExceptionCode.INTERNAL_SERVER_ERROR);

View File

@ -25,7 +25,7 @@ import { EnvironmentService } from 'src/engine/core-modules/environment/environm
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
@Controller('auth/microsoft-apis')
@UseFilters(AuthRestApiExceptionFilter)

View File

@ -18,8 +18,9 @@ import { MicrosoftProviderEnabledGuard } from 'src/engine/core-modules/auth/guar
import { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
import { MicrosoftRequest } from 'src/engine/core-modules/auth/strategies/microsoft.auth.strategy';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
@Controller('auth/microsoft')
@UseFilters(AuthRestApiExceptionFilter)
@ -28,6 +29,7 @@ export class MicrosoftAuthController {
private readonly loginTokenService: LoginTokenService,
private readonly authService: AuthService,
private readonly domainManagerService: DomainManagerService,
private readonly environmentService: EnvironmentService,
@InjectRepository(User, 'core')
private readonly userRepository: Repository<User>,
) {}
@ -119,9 +121,11 @@ export class MicrosoftAuthController {
} catch (err) {
if (err instanceof AuthException) {
return res.redirect(
this.domainManagerService.computeRedirectErrorUrl(err.message, {
subdomain: currentWorkspace?.subdomain,
}),
this.domainManagerService.computeRedirectErrorUrl(
err.message,
currentWorkspace?.subdomain ??
this.environmentService.get('DEFAULT_SUBDOMAIN'),
),
);
}
throw err;

View File

@ -22,7 +22,7 @@ import {
import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter';
import { OIDCAuthGuard } from 'src/engine/core-modules/auth/guards/oidc-auth.guard';
import { SAMLAuthGuard } from 'src/engine/core-modules/auth/guards/saml-auth.guard';
import { SSOProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/sso-provider-enabled.guard';
import { EnterpriseFeaturesEnabledGuard } from 'src/engine/core-modules/auth/guards/enterprise-features-enabled.guard';
import { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
@ -30,17 +30,19 @@ import {
IdentityProviderType,
WorkspaceSSOIdentityProvider,
} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { AuthOAuthExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-oauth-exception.filter';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
@Controller('auth')
@UseFilters(AuthRestApiExceptionFilter)
export class SSOAuthController {
constructor(
private readonly loginTokenService: LoginTokenService,
private readonly authService: AuthService,
private readonly domainManagerService: DomainManagerService,
private readonly ssoService: SSOService,
private readonly environmentService: EnvironmentService,
private readonly sSOService: SSOService,
@InjectRepository(User, 'core')
private readonly userRepository: Repository<User>,
@InjectRepository(WorkspaceSSOIdentityProvider, 'core')
@ -48,15 +50,16 @@ export class SSOAuthController {
) {}
@Get('saml/metadata/:identityProviderId')
@UseGuards(SSOProviderEnabledGuard)
@UseGuards(EnterpriseFeaturesEnabledGuard)
@UseFilters(AuthRestApiExceptionFilter)
async generateMetadata(@Req() req: any): Promise<string | void> {
return generateServiceProviderMetadata({
wantAssertionsSigned: false,
issuer: this.ssoService.buildIssuerURL({
issuer: this.sSOService.buildIssuerURL({
id: req.params.identityProviderId,
type: IdentityProviderType.SAML,
}),
callbackUrl: this.ssoService.buildCallbackUrl({
callbackUrl: this.sSOService.buildCallbackUrl({
id: req.params.identityProviderId,
type: IdentityProviderType.SAML,
}),
@ -64,29 +67,40 @@ export class SSOAuthController {
}
@Get('oidc/login/:identityProviderId')
@UseGuards(SSOProviderEnabledGuard, OIDCAuthGuard)
@UseGuards(EnterpriseFeaturesEnabledGuard, OIDCAuthGuard)
@UseFilters(AuthRestApiExceptionFilter)
async oidcAuth() {
// As this method is protected by OIDC Auth guard, it will trigger OIDC SSO flow
return;
}
@Get('saml/login/:identityProviderId')
@UseGuards(SSOProviderEnabledGuard, SAMLAuthGuard)
@UseGuards(EnterpriseFeaturesEnabledGuard, SAMLAuthGuard)
@UseFilters(AuthRestApiExceptionFilter)
async samlAuth() {
// As this method is protected by SAML Auth guard, it will trigger SAML SSO flow
return;
}
@Get('oidc/callback')
@UseGuards(SSOProviderEnabledGuard, OIDCAuthGuard)
@UseGuards(EnterpriseFeaturesEnabledGuard, OIDCAuthGuard)
@UseFilters(AuthOAuthExceptionFilter)
async oidcAuthCallback(@Req() req: any, @Res() res: Response) {
return this.authCallback(req, res);
return await this.authCallback(req, res);
}
@Post('saml/callback/:identityProviderId')
@UseGuards(SSOProviderEnabledGuard, SAMLAuthGuard)
@UseGuards(EnterpriseFeaturesEnabledGuard, SAMLAuthGuard)
@UseFilters(AuthOAuthExceptionFilter)
async samlAuthCallback(@Req() req: any, @Res() res: Response) {
return this.authCallback(req, res);
try {
return await this.authCallback(req, res);
} catch (err) {
return new AuthException(
err.message ?? 'Access denied',
AuthExceptionCode.OAUTH_ACCESS_DENIED,
);
}
}
private async authCallback({ user }: any, res: Response) {
@ -95,21 +109,21 @@ export class SSOAuthController {
user.identityProviderId,
);
if (!workspaceIdentityProvider) {
throw new AuthException(
'Identity provider not found',
AuthExceptionCode.INVALID_DATA,
);
}
if (!user.user.email) {
throw new AuthException(
'Email not found',
AuthExceptionCode.INVALID_DATA,
);
}
try {
if (!workspaceIdentityProvider) {
throw new AuthException(
'Identity provider not found',
AuthExceptionCode.OAUTH_ACCESS_DENIED,
);
}
if (!user.user.email) {
throw new AuthException(
'Email not found from identity provider.',
AuthExceptionCode.OAUTH_ACCESS_DENIED,
);
}
const { loginToken, identityProvider } = await this.generateLoginToken(
user.user,
workspaceIdentityProvider,
@ -122,14 +136,13 @@ export class SSOAuthController {
}),
);
} catch (err) {
if (err instanceof AuthException) {
return res.redirect(
this.domainManagerService.computeRedirectErrorUrl(err.message, {
subdomain: workspaceIdentityProvider.workspace.subdomain,
}),
);
}
throw err;
return res.redirect(
this.domainManagerService.computeRedirectErrorUrl(
err.message,
workspaceIdentityProvider?.workspace.subdomain ??
this.environmentService.get('DEFAULT_SUBDOMAIN'),
),
);
}
}

View File

@ -6,7 +6,7 @@ import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { HttpExceptionHandlerService } from 'src/engine/core-modules/exception-handler/http-exception-handler.service';
@Catch(AuthException)

View File

@ -0,0 +1,39 @@
/* @license Enterprise */
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
@Injectable()
export class EnterpriseFeaturesEnabledGuard implements CanActivate {
constructor(
private readonly guardRedirectService: GuardRedirectService,
private readonly environmentService: EnvironmentService,
) {}
canActivate(context: ExecutionContext): boolean {
try {
if (!this.environmentService.get('ENTERPRISE_KEY')) {
throw new AuthException(
'Enterprise key missing',
AuthExceptionCode.MISSING_ENVIRONMENT_VARIABLE,
);
}
return true;
} catch (err) {
this.guardRedirectService.dispatchErrorFromGuard(
context,
err,
this.guardRedirectService.getSubdomainFromContext(context),
);
return false;
}
}
}

View File

@ -1,14 +1,25 @@
import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
@Injectable()
export class GoogleOauthGuard extends AuthGuard('google') {
constructor() {
constructor(
private readonly guardRedirectService: GuardRedirectService,
private readonly environmentService: EnvironmentService,
@InjectRepository(Workspace, 'core')
private readonly workspaceRepository: Repository<Workspace>,
) {
super({
prompt: 'select_account',
});
@ -16,43 +27,59 @@ export class GoogleOauthGuard extends AuthGuard('google') {
async canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
const workspaceInviteHash = request.query.inviteHash;
const workspacePersonalInviteToken = request.query.inviteToken;
let workspace: Workspace | null = null;
if (request.query.error === 'access_denied') {
throw new AuthException(
'Google OAuth access denied',
AuthExceptionCode.OAUTH_ACCESS_DENIED,
try {
if (
request.query.workspaceId &&
typeof request.query.workspaceId === 'string'
) {
request.params.workspaceId = request.query.workspaceId;
workspace = await this.workspaceRepository.findOneBy({
id: request.query.workspaceId,
});
}
const workspaceInviteHash = request.query.inviteHash;
const workspacePersonalInviteToken = request.query.inviteToken;
if (request.query.error === 'access_denied') {
throw new AuthException(
'Google OAuth access denied',
AuthExceptionCode.OAUTH_ACCESS_DENIED,
);
}
if (workspaceInviteHash && typeof workspaceInviteHash === 'string') {
request.params.workspaceInviteHash = workspaceInviteHash;
}
if (
workspacePersonalInviteToken &&
typeof workspacePersonalInviteToken === 'string'
) {
request.params.workspacePersonalInviteToken =
workspacePersonalInviteToken;
}
if (
request.query.billingCheckoutSessionState &&
typeof request.query.billingCheckoutSessionState === 'string'
) {
request.params.billingCheckoutSessionState =
request.query.billingCheckoutSessionState;
}
return (await super.canActivate(context)) as boolean;
} catch (err) {
this.guardRedirectService.dispatchErrorFromGuard(
context,
err,
workspace?.subdomain ??
this.environmentService.get('DEFAULT_SUBDOMAIN'),
);
}
if (workspaceInviteHash && typeof workspaceInviteHash === 'string') {
request.params.workspaceInviteHash = workspaceInviteHash;
return false;
}
if (
workspacePersonalInviteToken &&
typeof workspacePersonalInviteToken === 'string'
) {
request.params.workspacePersonalInviteToken =
workspacePersonalInviteToken;
}
if (
request.query.workspaceId &&
typeof request.query.workspaceId === 'string'
) {
request.params.workspaceId = request.query.workspaceId;
}
if (
request.query.billingCheckoutSessionState &&
typeof request.query.billingCheckoutSessionState === 'string'
) {
request.params.billingCheckoutSessionState =
request.query.billingCheckoutSessionState;
}
return (await super.canActivate(context)) as boolean;
}
}

View File

@ -1,6 +1,4 @@
import { CanActivate, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import {
AuthException,
@ -8,21 +6,35 @@ import {
} from 'src/engine/core-modules/auth/auth.exception';
import { GoogleStrategy } from 'src/engine/core-modules/auth/strategies/google.auth.strategy';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
@Injectable()
export class GoogleProviderEnabledGuard implements CanActivate {
constructor(private readonly environmentService: EnvironmentService) {}
constructor(
private readonly environmentService: EnvironmentService,
private readonly guardRedirectService: GuardRedirectService,
) {}
canActivate(): boolean | Promise<boolean> | Observable<boolean> {
if (!this.environmentService.get('AUTH_GOOGLE_ENABLED')) {
throw new AuthException(
'Google auth is not enabled',
AuthExceptionCode.GOOGLE_API_AUTH_DISABLED,
canActivate(context: ExecutionContext): boolean {
try {
if (!this.environmentService.get('AUTH_GOOGLE_ENABLED')) {
throw new AuthException(
'Google auth is not enabled',
AuthExceptionCode.GOOGLE_API_AUTH_DISABLED,
);
}
new GoogleStrategy(this.environmentService);
return true;
} catch (err) {
this.guardRedirectService.dispatchErrorFromGuard(
context,
err,
this.guardRedirectService.getSubdomainFromContext(context),
);
return false;
}
new GoogleStrategy(this.environmentService);
return true;
}
}

View File

@ -1,9 +1,21 @@
import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@Injectable()
export class MicrosoftOAuthGuard extends AuthGuard('microsoft') {
constructor() {
constructor(
private readonly guardRedirectService: GuardRedirectService,
private readonly environmentService: EnvironmentService,
@InjectRepository(Workspace, 'core')
private readonly workspaceRepository: Repository<Workspace>,
) {
super({
prompt: 'select_account',
});
@ -11,36 +23,52 @@ export class MicrosoftOAuthGuard extends AuthGuard('microsoft') {
async canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
const workspaceInviteHash = request.query.inviteHash;
const workspacePersonalInviteToken = request.query.inviteToken;
let workspace: Workspace | null = null;
if (workspaceInviteHash && typeof workspaceInviteHash === 'string') {
request.params.workspaceInviteHash = workspaceInviteHash;
try {
if (
request.query.workspaceId &&
typeof request.query.workspaceId === 'string'
) {
request.params.workspaceId = request.query.workspaceId;
workspace = await this.workspaceRepository.findOneBy({
id: request.query.workspaceId,
});
}
const workspaceInviteHash = request.query.inviteHash;
const workspacePersonalInviteToken = request.query.inviteToken;
if (workspaceInviteHash && typeof workspaceInviteHash === 'string') {
request.params.workspaceInviteHash = workspaceInviteHash;
}
if (
workspacePersonalInviteToken &&
typeof workspacePersonalInviteToken === 'string'
) {
request.params.workspacePersonalInviteToken =
workspacePersonalInviteToken;
}
if (
request.query.billingCheckoutSessionState &&
typeof request.query.billingCheckoutSessionState === 'string'
) {
request.params.billingCheckoutSessionState =
request.query.billingCheckoutSessionState;
}
return (await super.canActivate(context)) as boolean;
} catch (err) {
this.guardRedirectService.dispatchErrorFromGuard(
context,
err,
workspace?.subdomain ??
this.environmentService.get('DEFAULT_SUBDOMAIN'),
);
return false;
}
if (
workspacePersonalInviteToken &&
typeof workspacePersonalInviteToken === 'string'
) {
request.params.workspacePersonalInviteToken =
workspacePersonalInviteToken;
}
if (
request.query.workspaceSubdomain &&
typeof request.query.workspaceSubdomain === 'string'
) {
request.params.workspaceSubdomain = request.query.workspaceSubdomain;
}
if (
request.query.billingCheckoutSessionState &&
typeof request.query.billingCheckoutSessionState === 'string'
) {
request.params.billingCheckoutSessionState =
request.query.billingCheckoutSessionState;
}
return (await super.canActivate(context)) as boolean;
}
}

View File

@ -1,6 +1,4 @@
import { CanActivate, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import {
AuthException,
@ -8,21 +6,35 @@ import {
} from 'src/engine/core-modules/auth/auth.exception';
import { MicrosoftStrategy } from 'src/engine/core-modules/auth/strategies/microsoft.auth.strategy';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
@Injectable()
export class MicrosoftProviderEnabledGuard implements CanActivate {
constructor(private readonly environmentService: EnvironmentService) {}
constructor(
private readonly environmentService: EnvironmentService,
private readonly guardRedirectService: GuardRedirectService,
) {}
canActivate(): boolean | Promise<boolean> | Observable<boolean> {
if (!this.environmentService.get('AUTH_MICROSOFT_ENABLED')) {
throw new AuthException(
'Microsoft auth is not enabled',
AuthExceptionCode.MICROSOFT_API_AUTH_DISABLED,
canActivate(context: ExecutionContext): boolean {
try {
if (!this.environmentService.get('AUTH_MICROSOFT_ENABLED')) {
throw new AuthException(
'Microsoft auth is not enabled',
AuthExceptionCode.MICROSOFT_API_AUTH_DISABLED,
);
}
new MicrosoftStrategy(this.environmentService);
return true;
} catch (err) {
this.guardRedirectService.dispatchErrorFromGuard(
context,
err,
this.guardRedirectService.getSubdomainFromContext(context),
);
return false;
}
new MicrosoftStrategy(this.environmentService);
return true;
}
}

View File

@ -11,10 +11,18 @@ import {
} from 'src/engine/core-modules/auth/auth.exception';
import { OIDCAuthStrategy } from 'src/engine/core-modules/auth/strategies/oidc.auth.strategy';
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { SSOConfiguration } from 'src/engine/core-modules/sso/types/SSOConfigurations.type';
import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
@Injectable()
export class OIDCAuthGuard extends AuthGuard('openidconnect') {
constructor(private readonly ssoService: SSOService) {
constructor(
private readonly sSOService: SSOService,
private readonly guardRedirectService: GuardRedirectService,
private readonly environmentService: EnvironmentService,
) {
super();
}
@ -38,13 +46,17 @@ export class OIDCAuthGuard extends AuthGuard('openidconnect') {
}
async canActivate(context: ExecutionContext): Promise<boolean> {
try {
const request = context.switchToHttp().getRequest();
const request = context.switchToHttp().getRequest();
let identityProvider:
| (SSOConfiguration & WorkspaceSSOIdentityProvider)
| null = null;
try {
const identityProviderId = this.getIdentityProviderId(request);
const identityProvider =
await this.ssoService.findSSOIdentityProviderById(identityProviderId);
identityProvider =
await this.sSOService.findSSOIdentityProviderById(identityProviderId);
if (!identityProvider) {
throw new AuthException(
@ -56,17 +68,19 @@ export class OIDCAuthGuard extends AuthGuard('openidconnect') {
const issuer = await Issuer.discover(identityProvider.issuer);
new OIDCAuthStrategy(
this.ssoService.getOIDCClient(identityProvider, issuer),
this.sSOService.getOIDCClient(identityProvider, issuer),
identityProvider.id,
);
return (await super.canActivate(context)) as boolean;
} catch (err) {
if (err instanceof AuthException) {
return false;
}
this.guardRedirectService.dispatchErrorFromGuard(
context,
err,
identityProvider?.workspace.subdomain ??
this.environmentService.get('DEFAULT_SUBDOMAIN'),
);
// TODO AMOREAUX: trigger sentry error
return false;
}
}

View File

@ -9,33 +9,50 @@ import {
} from 'src/engine/core-modules/auth/auth.exception';
import { SamlAuthStrategy } from 'src/engine/core-modules/auth/strategies/saml.auth.strategy';
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { SSOConfiguration } from 'src/engine/core-modules/sso/types/SSOConfigurations.type';
import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
@Injectable()
export class SAMLAuthGuard extends AuthGuard('saml') {
constructor(private readonly sSOService: SSOService) {
constructor(
private readonly sSOService: SSOService,
private readonly guardRedirectService: GuardRedirectService,
private readonly environmentService: EnvironmentService,
) {
super();
}
async canActivate(context: ExecutionContext) {
try {
const request = context.switchToHttp().getRequest();
const request = context.switchToHttp().getRequest();
if (!request.params.identityProviderId) {
let identityProvider:
| (SSOConfiguration & WorkspaceSSOIdentityProvider)
| null = null;
try {
identityProvider = await this.sSOService.findSSOIdentityProviderById(
request.params.identityProviderId,
);
if (!identityProvider) {
throw new AuthException(
'Invalid SAML identity provider',
'Identity provider not found',
AuthExceptionCode.INVALID_DATA,
);
}
new SamlAuthStrategy(this.sSOService);
return (await super.canActivate(context)) as boolean;
} catch (err) {
if (err instanceof AuthException) {
return false;
}
this.guardRedirectService.dispatchErrorFromGuard(
context,
err,
identityProvider?.workspace.subdomain ??
this.environmentService.get('DEFAULT_SUBDOMAIN'),
);
// TODO AMOREAUX: trigger sentry error
return false;
}
}

View File

@ -1,27 +0,0 @@
/* @license Enterprise */
import { CanActivate, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
@Injectable()
export class SSOProviderEnabledGuard implements CanActivate {
constructor(private readonly environmentService: EnvironmentService) {}
canActivate(): boolean | Promise<boolean> | Observable<boolean> {
if (!this.environmentService.get('ENTERPRISE_KEY')) {
throw new AuthException(
'Enterprise key must be defined to use SSO',
AuthExceptionCode.MISSING_ENVIRONMENT_VARIABLE,
);
}
return true;
}
}

View File

@ -9,7 +9,7 @@ import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';

View File

@ -37,7 +37,7 @@ import { WorkspaceInviteHashValid } from 'src/engine/core-modules/auth/dto/works
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
@ -460,7 +460,7 @@ export class AuthService {
billingCheckoutSessionState,
}: {
loginToken: string;
subdomain?: string;
subdomain: string;
billingCheckoutSessionState?: string;
}) {
const url = this.domainManagerService.buildWorkspaceURL({

View File

@ -13,7 +13,7 @@ import { EmailService } from 'src/engine/core-modules/email/email.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { ResetPasswordService } from './reset-password.service';

View File

@ -21,7 +21,7 @@ import { EmailPasswordResetLink } from 'src/engine/core-modules/auth/dto/email-p
import { InvalidatePassword } from 'src/engine/core-modules/auth/dto/invalidate-password.entity';
import { PasswordResetToken } from 'src/engine/core-modules/auth/dto/token.entity';
import { ValidatePasswordResetToken } from 'src/engine/core-modules/auth/dto/validate-password-reset-token.entity';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { User } from 'src/engine/core-modules/user/user.entity';

View File

@ -12,7 +12,7 @@ import {
ExistingUserOrPartialUserWithPicture,
SignInUpBaseParams,
} from 'src/engine/core-modules/auth/types/signInUp.type';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';

View File

@ -29,7 +29,7 @@ import {
SignInUpBaseParams,
SignInUpNewUserPayload,
} from 'src/engine/core-modules/auth/types/signInUp.type';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';

View File

@ -9,11 +9,6 @@ import {
StrategyVerifyCallbackReq,
} from 'openid-client';
import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
@Injectable()
export class OIDCAuthStrategy extends PassportStrategy(
Strategy,
@ -47,7 +42,7 @@ export class OIDCAuthStrategy extends PassportStrategy(
validate: StrategyVerifyCallbackReq<{
identityProviderId: string;
user: {
email: string;
email?: string;
firstName?: string | null;
lastName?: string | null;
};
@ -66,12 +61,6 @@ export class OIDCAuthStrategy extends PassportStrategy(
const userinfo = await this.client.userinfo(tokenset);
if (!userinfo || !userinfo.email) {
return done(
new AuthException('Email not found', AuthExceptionCode.INVALID_DATA),
);
}
const user = {
email: userinfo.email,
...(userinfo.given_name ? { firstName: userinfo.given_name } : {}),

View File

@ -14,11 +14,11 @@ import { StripeBillingPortalService } from 'src/engine/core-modules/billing/stri
import { StripeCheckoutService } from 'src/engine/core-modules/billing/stripe/services/stripe-checkout.service';
import { BillingGetPricesPerPlanResult } from 'src/engine/core-modules/billing/types/billing-get-prices-per-plan-result.type';
import { BillingPortalCheckoutSessionParameters } from 'src/engine/core-modules/billing/types/billing-portal-checkout-session-parameters.type';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { assert } from 'src/utils/assert';
@Injectable()

View File

@ -3,7 +3,7 @@ import { Injectable, Logger } from '@nestjs/common';
import Stripe from 'stripe';
import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
@Injectable()

View File

@ -1,6 +1,6 @@
import { Test, TestingModule } from '@nestjs/testing';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { ClientConfigResolver } from './client-config.resolver';

View File

@ -1,6 +1,6 @@
import { Query, Resolver } from '@nestjs/graphql';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { PUBLIC_FEATURE_FLAGS } from 'src/engine/core-modules/feature-flag/constants/public-feature-flag.const';

View File

@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@Module({

View File

@ -125,6 +125,7 @@ describe('DomainManagerService', () => {
});
const result = domainManagerService.buildWorkspaceURL({
subdomain: 'subdomain',
pathname: '/path/to/resource',
});
@ -144,6 +145,8 @@ describe('DomainManagerService', () => {
});
const result = domainManagerService.buildWorkspaceURL({
subdomain: 'subdomain',
searchParams: {
foo: 'bar',
baz: 123,

View File

@ -62,14 +62,14 @@ export class DomainManagerService {
buildEmailVerificationURL({
emailVerificationToken,
email,
workspaceSubdomain,
subdomain,
}: {
emailVerificationToken: string;
email: string;
workspaceSubdomain?: string;
subdomain: string;
}) {
return this.buildWorkspaceURL({
subdomain: workspaceSubdomain,
subdomain,
pathname: 'verify-email',
searchParams: { emailVerificationToken, email },
});
@ -80,28 +80,14 @@ export class DomainManagerService {
pathname,
searchParams,
}: {
subdomain?: string;
subdomain: string;
pathname?: string;
searchParams?: Record<string, string | number>;
}) {
const url = this.getBaseUrl();
const url = this.getFrontUrl();
if (
this.environmentService.get('IS_MULTIWORKSPACE_ENABLED') &&
!subdomain
) {
throw new Error('subdomain is required when multiworkspace is enable');
}
if (
subdomain &&
subdomain.length > 0 &&
this.environmentService.get('IS_MULTIWORKSPACE_ENABLED')
) {
url.hostname = url.hostname.replace(
this.environmentService.get('DEFAULT_SUBDOMAIN'),
subdomain,
);
if (this.environmentService.get('IS_MULTIWORKSPACE_ENABLED')) {
url.hostname = `${subdomain}.${url.hostname}`;
}
if (pathname) {
@ -119,18 +105,18 @@ export class DomainManagerService {
return url;
}
getWorkspaceSubdomainByOrigin = (origin: string) => {
const { hostname: originHostname } = new URL(origin);
getWorkspaceSubdomainFromUrl = (url: string) => {
const { hostname: originHostname } = new URL(url);
if (!originHostname.endsWith(this.getFrontUrl().hostname)) {
return null;
}
const frontDomain = this.getFrontUrl().hostname;
const subdomain = originHostname.replace(`.${frontDomain}`, '');
if (this.isDefaultSubdomain(subdomain)) {
return;
}
return subdomain;
return this.isDefaultSubdomain(subdomain) ? null : subdomain;
};
async getWorkspaceBySubdomainOrDefaultWorkspace(subdomain?: string) {
@ -145,16 +131,9 @@ export class DomainManagerService {
return subdomain === this.environmentService.get('DEFAULT_SUBDOMAIN');
}
computeRedirectErrorUrl(
errorMessage: string,
{
subdomain,
}: {
subdomain?: string;
},
) {
computeRedirectErrorUrl(errorMessage: string, subdomain: string) {
const url = this.buildWorkspaceURL({
subdomain: subdomain ?? this.environmentService.get('DEFAULT_SUBDOMAIN'),
subdomain: subdomain,
pathname: '/verify',
searchParams: { errorMessage },
});
@ -206,7 +185,7 @@ export class DomainManagerService {
return this.getDefaultWorkspace();
}
const subdomain = this.getWorkspaceSubdomainByOrigin(origin);
const subdomain = this.getWorkspaceSubdomainFromUrl(origin);
if (!isDefined(subdomain)) return;

View File

@ -1,6 +1,6 @@
import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { ResendEmailVerificationTokenInput } from 'src/engine/core-modules/email-verification/dtos/resend-email-verification-token.input';
import { ResendEmailVerificationTokenOutput } from 'src/engine/core-modules/email-verification/dtos/resend-email-verification-token.output';
import { EmailVerificationService } from 'src/engine/core-modules/email-verification/services/email-verification.service';

View File

@ -12,7 +12,7 @@ import {
AppTokenType,
} from 'src/engine/core-modules/app-token/app-token.entity';
import { EmailVerificationTokenService } from 'src/engine/core-modules/auth/token/services/email-verification-token.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import {
EmailVerificationException,
EmailVerificationExceptionCode,
@ -37,7 +37,7 @@ export class EmailVerificationService {
async sendVerificationEmail(
userId: string,
email: string,
workspaceSubdomain?: string,
subdomain: string,
) {
if (!this.environmentService.get('IS_EMAIL_VERIFICATION_REQUIRED')) {
return { success: false };
@ -50,7 +50,7 @@ export class EmailVerificationService {
this.domainManagerService.buildEmailVerificationURL({
emailVerificationToken,
email,
workspaceSubdomain,
subdomain,
});
const emailData = {
@ -80,10 +80,7 @@ export class EmailVerificationService {
return { success: true };
}
async resendEmailVerificationToken(
email: string,
workspaceSubdomain?: string,
) {
async resendEmailVerificationToken(email: string, subdomain: string) {
if (!this.environmentService.get('IS_EMAIL_VERIFICATION_REQUIRED')) {
throw new EmailVerificationException(
'Email verification token cannot be sent because email verification is not required',
@ -124,7 +121,7 @@ export class EmailVerificationService {
await this.appTokenRepository.delete(existingToken.id);
}
await this.sendVerificationEmail(user.id, email, workspaceSubdomain);
await this.sendVerificationEmail(user.id, email, subdomain);
return { success: true };
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
@Module({
imports: [DomainManagerModule],
providers: [GuardRedirectService],
exports: [GuardRedirectService],
})
export class GuardRedirectModule {}

View File

@ -0,0 +1,38 @@
import { ExecutionContext, Injectable } from '@nestjs/common';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
@Injectable()
export class GuardRedirectService {
constructor(
private readonly domainManagerService: DomainManagerService,
private readonly environmentService: EnvironmentService,
) {}
dispatchErrorFromGuard(context: any, err: any, subdomain: string) {
if ('contextType' in context && context.contextType === 'graphql') {
throw err;
}
context
.switchToHttp()
.getResponse()
.redirect(
this.domainManagerService
.computeRedirectErrorUrl(err.message ?? 'Unknown error', subdomain)
.toString(),
);
}
getSubdomainFromContext(context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
const subdomainFromUrl =
this.domainManagerService.getWorkspaceSubdomainFromUrl(
request.headers.referer,
);
return subdomainFromUrl ?? this.environmentService.get('DEFAULT_SUBDOMAIN');
}
}

View File

@ -129,13 +129,10 @@ export class SSOService {
};
}
async findSSOIdentityProviderById(identityProviderId?: string) {
// if identityProviderId is not provide, typeorm return a random idp instead of undefined
if (!identityProviderId) return undefined;
async findSSOIdentityProviderById(identityProviderId: string) {
return (await this.workspaceSSOIdentityProviderRepository.findOne({
where: { id: identityProviderId },
})) as (SSOConfiguration & WorkspaceSSOIdentityProvider) | undefined;
})) as (SSOConfiguration & WorkspaceSSOIdentityProvider) | null;
}
buildCallbackUrl(

View File

@ -11,6 +11,8 @@ import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
import { SSOResolver } from 'src/engine/core-modules/sso/sso.resolver';
import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
import { User } from 'src/engine/core-modules/user/user.entity';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { GuardRedirectModule } from 'src/engine/core-modules/guard-redirect/guard-redirect.module';
@Module({
imports: [
@ -19,6 +21,8 @@ import { User } from 'src/engine/core-modules/user/user.entity';
'core',
),
BillingModule,
DomainManagerModule,
GuardRedirectModule,
],
exports: [SSOService],
providers: [SSOService, SSOResolver],

View File

@ -3,7 +3,7 @@
import { UseGuards } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { SSOProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/sso-provider-enabled.guard';
import { EnterpriseFeaturesEnabledGuard } from 'src/engine/core-modules/auth/guards/enterprise-features-enabled.guard';
import { DeleteSsoInput } from 'src/engine/core-modules/sso/dtos/delete-sso.input';
import { DeleteSsoOutput } from 'src/engine/core-modules/sso/dtos/delete-sso.output';
import { EditSsoInput } from 'src/engine/core-modules/sso/dtos/edit-sso.input';
@ -26,7 +26,7 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
export class SSOResolver {
constructor(private readonly sSOService: SSOService) {}
@UseGuards(WorkspaceAuthGuard, SSOProviderEnabledGuard)
@UseGuards(WorkspaceAuthGuard, EnterpriseFeaturesEnabledGuard)
@Mutation(() => SetupSsoOutput)
async createOIDCIdentityProvider(
@Args('input') setupSsoInput: SetupOIDCSsoInput,
@ -38,7 +38,7 @@ export class SSOResolver {
);
}
@UseGuards(SSOProviderEnabledGuard)
@UseGuards(EnterpriseFeaturesEnabledGuard)
@Query(() => [FindAvailableSSOIDPOutput])
async listSSOIdentityProvidersByWorkspaceId(
@AuthWorkspace() { id: workspaceId }: Workspace,
@ -53,7 +53,7 @@ export class SSOResolver {
return this.sSOService.getAuthorizationUrl(identityProviderId);
}
@UseGuards(WorkspaceAuthGuard, SSOProviderEnabledGuard)
@UseGuards(WorkspaceAuthGuard, EnterpriseFeaturesEnabledGuard)
@Mutation(() => SetupSsoOutput)
async createSAMLIdentityProvider(
@Args('input') setupSsoInput: SetupSAMLSsoInput,
@ -65,7 +65,7 @@ export class SSOResolver {
);
}
@UseGuards(WorkspaceAuthGuard, SSOProviderEnabledGuard)
@UseGuards(WorkspaceAuthGuard, EnterpriseFeaturesEnabledGuard)
@Mutation(() => DeleteSsoOutput)
async deleteSSOIdentityProvider(
@Args('input') { identityProviderId }: DeleteSsoInput,
@ -77,7 +77,7 @@ export class SSOResolver {
);
}
@UseGuards(WorkspaceAuthGuard, SSOProviderEnabledGuard)
@UseGuards(WorkspaceAuthGuard, EnterpriseFeaturesEnabledGuard)
@Mutation(() => EditSsoOutput)
async editSSOIdentityProvider(
@Args('input') input: EditSsoInput,

View File

@ -45,7 +45,7 @@ import {
} from 'src/engine/core-modules/auth/auth.exception';
import { userValidator } from 'src/engine/core-modules/user/user.validate';
import { OriginHeader } from 'src/engine/decorators/auth/origin-header.decorator';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
const getHMACKey = (email?: string, key?: string | null) => {

View File

@ -14,7 +14,7 @@ import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-works
import { User } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceInvitationException } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.exception';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
import { WorkspaceInvitationService } from './workspace-invitation.service';

View File

@ -17,7 +17,7 @@ import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';

View File

@ -3,7 +3,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';

View File

@ -16,7 +16,7 @@ import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';

View File

@ -16,7 +16,7 @@ export class UnhandledExceptionFilter implements ExceptionFilter {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
if (!response.header) {
if (!response.header || response.headersSent) {
return;
}