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

@ -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 } : {}),