feat(auth): centralize SSO error handling logic (#9832)
Introduce `SsoErrorRedirectService` to handle SSO error redirection and exception capturing across the authentication controllers. Refactor Microsoft, Google, and SSO authentication controllers to utilize this service, replacing the previous direct calls to `DomainManagerService`. Added unit tests for the new service to ensure robust error handling.
This commit is contained in:
@ -25,6 +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 { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
|
||||
|
||||
@Controller('auth/google-apis')
|
||||
@UseFilters(AuthRestApiExceptionFilter)
|
||||
@ -35,6 +36,7 @@ export class GoogleAPIsAuthController {
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly onboardingService: OnboardingService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
private readonly guardRedirectService: GuardRedirectService,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
) {}
|
||||
@ -52,75 +54,89 @@ export class GoogleAPIsAuthController {
|
||||
@Req() req: GoogleAPIsRequest,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
const { user } = req;
|
||||
let workspace: Workspace | null = null;
|
||||
|
||||
const {
|
||||
emails,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
transientToken,
|
||||
redirectLocation,
|
||||
calendarVisibility,
|
||||
messageVisibility,
|
||||
} = user;
|
||||
try {
|
||||
const { user } = req;
|
||||
|
||||
const { workspaceMemberId, userId, workspaceId } =
|
||||
await this.transientTokenService.verifyTransientToken(transientToken);
|
||||
const {
|
||||
emails,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
transientToken,
|
||||
redirectLocation,
|
||||
calendarVisibility,
|
||||
messageVisibility,
|
||||
} = user;
|
||||
|
||||
const demoWorkspaceIds = this.environmentService.get('DEMO_WORKSPACE_IDS');
|
||||
const { workspaceMemberId, userId, workspaceId } =
|
||||
await this.transientTokenService.verifyTransientToken(transientToken);
|
||||
|
||||
if (demoWorkspaceIds.includes(workspaceId)) {
|
||||
throw new AuthException(
|
||||
'Cannot connect Google account to demo workspace',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
const demoWorkspaceIds =
|
||||
this.environmentService.get('DEMO_WORKSPACE_IDS');
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new AuthException(
|
||||
'Workspace not found',
|
||||
AuthExceptionCode.WORKSPACE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
if (demoWorkspaceIds.includes(workspaceId)) {
|
||||
throw new AuthException(
|
||||
'Cannot connect Google account to demo workspace',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
const handle = emails[0].value;
|
||||
if (!workspaceId) {
|
||||
throw new AuthException(
|
||||
'Workspace not found',
|
||||
AuthExceptionCode.WORKSPACE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
await this.googleAPIsService.refreshGoogleRefreshToken({
|
||||
handle,
|
||||
workspaceMemberId: workspaceMemberId,
|
||||
workspaceId: workspaceId,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
calendarVisibility,
|
||||
messageVisibility,
|
||||
});
|
||||
|
||||
if (userId) {
|
||||
await this.onboardingService.setOnboardingConnectAccountPending({
|
||||
userId,
|
||||
workspaceId,
|
||||
value: false,
|
||||
workspace = await this.workspaceRepository.findOneBy({
|
||||
id: workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
const workspace = await this.workspaceRepository.findOneBy({
|
||||
id: workspaceId,
|
||||
});
|
||||
const handle = emails[0].value;
|
||||
|
||||
if (!workspace) {
|
||||
throw new AuthException(
|
||||
'Workspace not found',
|
||||
AuthExceptionCode.WORKSPACE_NOT_FOUND,
|
||||
await this.googleAPIsService.refreshGoogleRefreshToken({
|
||||
handle,
|
||||
workspaceMemberId: workspaceMemberId,
|
||||
workspaceId: workspaceId,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
calendarVisibility,
|
||||
messageVisibility,
|
||||
});
|
||||
|
||||
if (userId) {
|
||||
await this.onboardingService.setOnboardingConnectAccountPending({
|
||||
userId,
|
||||
workspaceId,
|
||||
value: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (!workspace) {
|
||||
throw new AuthException(
|
||||
'Workspace not found',
|
||||
AuthExceptionCode.WORKSPACE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return res.redirect(
|
||||
this.domainManagerService
|
||||
.buildWorkspaceURL({
|
||||
subdomain: workspace.subdomain,
|
||||
pathname: redirectLocation || '/settings/accounts',
|
||||
})
|
||||
.toString(),
|
||||
);
|
||||
} catch (err) {
|
||||
return res.redirect(
|
||||
this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions(
|
||||
err,
|
||||
workspace ?? {
|
||||
subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return res.redirect(
|
||||
this.domainManagerService
|
||||
.buildWorkspaceURL({
|
||||
subdomain: workspace.subdomain,
|
||||
pathname: redirectLocation || '/settings/accounts',
|
||||
})
|
||||
.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,10 +11,6 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Response } from 'express';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { AuthOAuthExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-oauth-exception.filter';
|
||||
import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter';
|
||||
import { GoogleOauthGuard } from 'src/engine/core-modules/auth/guards/google-oauth.guard';
|
||||
@ -22,9 +18,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/services/domain-manager.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
|
||||
|
||||
@Controller('auth/google')
|
||||
@UseFilters(AuthRestApiExceptionFilter)
|
||||
@ -32,8 +28,8 @@ export class GoogleAuthController {
|
||||
constructor(
|
||||
private readonly loginTokenService: LoginTokenService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly guardRedirectService: GuardRedirectService,
|
||||
@InjectRepository(User, 'core')
|
||||
private readonly userRepository: Repository<User>,
|
||||
) {}
|
||||
@ -120,16 +116,14 @@ export class GoogleAuthController {
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof AuthException) {
|
||||
return res.redirect(
|
||||
this.domainManagerService.computeRedirectErrorUrl(
|
||||
err.message,
|
||||
currentWorkspace?.subdomain ??
|
||||
this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
),
|
||||
);
|
||||
}
|
||||
throw new AuthException(err, AuthExceptionCode.INTERNAL_SERVER_ERROR);
|
||||
return res.redirect(
|
||||
this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions(
|
||||
err,
|
||||
currentWorkspace ?? {
|
||||
subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,8 +24,8 @@ import { MicrosoftAPIsRequest } from 'src/engine/core-modules/auth/types/microso
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
|
||||
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
|
||||
|
||||
@Controller('auth/microsoft-apis')
|
||||
@UseFilters(AuthRestApiExceptionFilter)
|
||||
@ -34,9 +34,9 @@ export class MicrosoftAPIsAuthController {
|
||||
private readonly microsoftAPIsService: MicrosoftAPIsService,
|
||||
private readonly transientTokenService: TransientTokenService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly workspaceService: WorkspaceService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
private readonly onboardingService: OnboardingService,
|
||||
private readonly guardRedirectService: GuardRedirectService,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
) {}
|
||||
@ -54,80 +54,96 @@ export class MicrosoftAPIsAuthController {
|
||||
@Req() req: MicrosoftAPIsRequest,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
const { user } = req;
|
||||
let workspace: Workspace | null = null;
|
||||
|
||||
const {
|
||||
emails,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
transientToken,
|
||||
redirectLocation,
|
||||
calendarVisibility,
|
||||
messageVisibility,
|
||||
} = user;
|
||||
try {
|
||||
const { user } = req;
|
||||
|
||||
const { workspaceMemberId, userId, workspaceId } =
|
||||
await this.transientTokenService.verifyTransientToken(transientToken);
|
||||
const {
|
||||
emails,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
transientToken,
|
||||
redirectLocation,
|
||||
calendarVisibility,
|
||||
messageVisibility,
|
||||
} = user;
|
||||
|
||||
const demoWorkspaceIds = this.environmentService.get('DEMO_WORKSPACE_IDS');
|
||||
const { workspaceMemberId, userId, workspaceId } =
|
||||
await this.transientTokenService.verifyTransientToken(transientToken);
|
||||
|
||||
if (demoWorkspaceIds.includes(workspaceId)) {
|
||||
throw new AuthException(
|
||||
'Cannot connect Microsoft account to demo workspace',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
if (!workspaceId) {
|
||||
throw new AuthException(
|
||||
'Workspace not found',
|
||||
AuthExceptionCode.WORKSPACE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
const demoWorkspaceIds =
|
||||
this.environmentService.get('DEMO_WORKSPACE_IDS');
|
||||
|
||||
if (emails.length === 0) {
|
||||
throw new AuthException(
|
||||
'No email - Ask your Azure Entra Admin to add you one on top of your User Principal Name',
|
||||
AuthExceptionCode.USER_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
const handle = emails[0].value;
|
||||
if (demoWorkspaceIds.includes(workspaceId)) {
|
||||
throw new AuthException(
|
||||
'Cannot connect Microsoft account to demo workspace',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
await this.microsoftAPIsService.refreshMicrosoftRefreshToken({
|
||||
handle,
|
||||
workspaceMemberId: workspaceMemberId,
|
||||
workspaceId: workspaceId,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
calendarVisibility,
|
||||
messageVisibility,
|
||||
});
|
||||
if (!workspaceId) {
|
||||
throw new AuthException(
|
||||
'Workspace not found',
|
||||
AuthExceptionCode.WORKSPACE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
await this.onboardingService.setOnboardingConnectAccountPending({
|
||||
userId,
|
||||
workspaceId,
|
||||
value: false,
|
||||
workspace = await this.workspaceRepository.findOneBy({
|
||||
id: workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
const workspace = await this.workspaceRepository.findOneBy({
|
||||
id: workspaceId,
|
||||
});
|
||||
if (emails.length === 0) {
|
||||
throw new AuthException(
|
||||
'No email - Ask your Azure Entra Admin to add you one on top of your User Principal Name',
|
||||
AuthExceptionCode.USER_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (!workspace) {
|
||||
throw new AuthException(
|
||||
'Workspace not found',
|
||||
AuthExceptionCode.WORKSPACE_NOT_FOUND,
|
||||
const handle = emails[0].value;
|
||||
|
||||
await this.microsoftAPIsService.refreshMicrosoftRefreshToken({
|
||||
handle,
|
||||
workspaceMemberId: workspaceMemberId,
|
||||
workspaceId: workspaceId,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
calendarVisibility,
|
||||
messageVisibility,
|
||||
});
|
||||
|
||||
if (userId) {
|
||||
await this.onboardingService.setOnboardingConnectAccountPending({
|
||||
userId,
|
||||
workspaceId,
|
||||
value: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (!workspace) {
|
||||
throw new AuthException(
|
||||
'Workspace not found',
|
||||
AuthExceptionCode.WORKSPACE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return res.redirect(
|
||||
this.domainManagerService
|
||||
.buildWorkspaceURL({
|
||||
subdomain: workspace.subdomain,
|
||||
pathname: redirectLocation || '/settings/accounts',
|
||||
})
|
||||
.toString(),
|
||||
);
|
||||
} catch (err) {
|
||||
return res.redirect(
|
||||
this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions(
|
||||
err,
|
||||
workspace ?? {
|
||||
subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return res.redirect(
|
||||
this.domainManagerService
|
||||
.buildWorkspaceURL({
|
||||
subdomain: workspace.subdomain,
|
||||
pathname: redirectLocation || '/settings/accounts',
|
||||
})
|
||||
.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,16 +11,15 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Response } from 'express';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter';
|
||||
import { MicrosoftOAuthGuard } from 'src/engine/core-modules/auth/guards/microsoft-oauth.guard';
|
||||
import { MicrosoftProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/microsoft-provider-enabled.guard';
|
||||
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/services/domain-manager.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
|
||||
|
||||
@Controller('auth/microsoft')
|
||||
@UseFilters(AuthRestApiExceptionFilter)
|
||||
@ -28,7 +27,7 @@ export class MicrosoftAuthController {
|
||||
constructor(
|
||||
private readonly loginTokenService: LoginTokenService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
private readonly guardRedirectService: GuardRedirectService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
@InjectRepository(User, 'core')
|
||||
private readonly userRepository: Repository<User>,
|
||||
@ -119,16 +118,14 @@ export class MicrosoftAuthController {
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof AuthException) {
|
||||
return res.redirect(
|
||||
this.domainManagerService.computeRedirectErrorUrl(
|
||||
err.message,
|
||||
currentWorkspace?.subdomain ??
|
||||
this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
),
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
return res.redirect(
|
||||
this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions(
|
||||
err,
|
||||
currentWorkspace ?? {
|
||||
subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,17 +30,17 @@ import {
|
||||
IdentityProviderType,
|
||||
WorkspaceSSOIdentityProvider,
|
||||
} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
||||
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';
|
||||
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
|
||||
|
||||
@Controller('auth')
|
||||
export class SSOAuthController {
|
||||
constructor(
|
||||
private readonly loginTokenService: LoginTokenService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
private readonly guardRedirectService: GuardRedirectService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly sSOService: SSOService,
|
||||
@InjectRepository(User, 'core')
|
||||
@ -137,10 +137,11 @@ export class SSOAuthController {
|
||||
);
|
||||
} catch (err) {
|
||||
return res.redirect(
|
||||
this.domainManagerService.computeRedirectErrorUrl(
|
||||
err.message,
|
||||
workspaceIdentityProvider?.workspace.subdomain ??
|
||||
this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions(
|
||||
err,
|
||||
workspaceIdentityProvider?.workspace ?? {
|
||||
subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -27,11 +27,9 @@ export class EnterpriseFeaturesEnabledGuard implements CanActivate {
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
this.guardRedirectService.dispatchErrorFromGuard(
|
||||
context,
|
||||
err,
|
||||
this.guardRedirectService.getSubdomainFromContext(context),
|
||||
);
|
||||
this.guardRedirectService.dispatchErrorFromGuard(context, err, {
|
||||
subdomain: this.guardRedirectService.getSubdomainFromContext(context),
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -8,41 +8,53 @@ import {
|
||||
import { GoogleAPIsOauthExchangeCodeForTokenStrategy } from 'src/engine/core-modules/auth/strategies/google-apis-oauth-exchange-code-for-token.auth.strategy';
|
||||
import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util';
|
||||
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 GoogleAPIsOauthExchangeCodeForTokenGuard extends AuthGuard(
|
||||
'google-apis',
|
||||
) {
|
||||
constructor(private readonly environmentService: EnvironmentService) {
|
||||
constructor(
|
||||
private readonly guardRedirectService: GuardRedirectService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async canActivate(context: ExecutionContext) {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const state = JSON.parse(request.query.state);
|
||||
try {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const state = JSON.parse(request.query.state);
|
||||
|
||||
if (
|
||||
!this.environmentService.get('MESSAGING_PROVIDER_GMAIL_ENABLED') &&
|
||||
!this.environmentService.get('CALENDAR_PROVIDER_GOOGLE_ENABLED')
|
||||
) {
|
||||
throw new AuthException(
|
||||
'Google apis auth is not enabled',
|
||||
AuthExceptionCode.GOOGLE_API_AUTH_DISABLED,
|
||||
if (
|
||||
!this.environmentService.get('MESSAGING_PROVIDER_GMAIL_ENABLED') &&
|
||||
!this.environmentService.get('CALENDAR_PROVIDER_GOOGLE_ENABLED')
|
||||
) {
|
||||
throw new AuthException(
|
||||
'Google apis auth is not enabled',
|
||||
AuthExceptionCode.GOOGLE_API_AUTH_DISABLED,
|
||||
);
|
||||
}
|
||||
|
||||
new GoogleAPIsOauthExchangeCodeForTokenStrategy(
|
||||
this.environmentService,
|
||||
{},
|
||||
);
|
||||
|
||||
setRequestExtraParams(request, {
|
||||
transientToken: state.transientToken,
|
||||
redirectLocation: state.redirectLocation,
|
||||
calendarVisibility: state.calendarVisibility,
|
||||
messageVisibility: state.messageVisibility,
|
||||
});
|
||||
|
||||
return (await super.canActivate(context)) as boolean;
|
||||
} catch (err) {
|
||||
this.guardRedirectService.dispatchErrorFromGuard(context, err, {
|
||||
subdomain: this.guardRedirectService.getSubdomainFromContext(context),
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
new GoogleAPIsOauthExchangeCodeForTokenStrategy(
|
||||
this.environmentService,
|
||||
{},
|
||||
);
|
||||
|
||||
setRequestExtraParams(request, {
|
||||
transientToken: state.transientToken,
|
||||
redirectLocation: state.redirectLocation,
|
||||
calendarVisibility: state.calendarVisibility,
|
||||
messageVisibility: state.messageVisibility,
|
||||
});
|
||||
|
||||
return (await super.canActivate(context)) as boolean;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
AuthException,
|
||||
@ -9,14 +12,17 @@ import { GoogleAPIsOauthRequestCodeStrategy } from 'src/engine/core-modules/auth
|
||||
import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service';
|
||||
import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
@Injectable()
|
||||
export class GoogleAPIsOauthRequestCodeGuard extends AuthGuard('google-apis') {
|
||||
constructor(
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly transientTokenService: TransientTokenService,
|
||||
private readonly guardRedirectService: GuardRedirectService,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
) {
|
||||
super({
|
||||
prompt: 'select_account',
|
||||
@ -24,37 +30,53 @@ export class GoogleAPIsOauthRequestCodeGuard extends AuthGuard('google-apis') {
|
||||
}
|
||||
|
||||
async canActivate(context: ExecutionContext) {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
let workspace: Workspace | null = null;
|
||||
|
||||
const { workspaceId, userId } =
|
||||
await this.transientTokenService.verifyTransientToken(
|
||||
request.query.transientToken,
|
||||
try {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
|
||||
const { workspaceId, userId } =
|
||||
await this.transientTokenService.verifyTransientToken(
|
||||
request.query.transientToken,
|
||||
);
|
||||
|
||||
workspace = await this.workspaceRepository.findOneBy({
|
||||
id: workspaceId,
|
||||
});
|
||||
|
||||
setRequestExtraParams(request, {
|
||||
transientToken: request.query.transientToken,
|
||||
redirectLocation: request.query.redirectLocation,
|
||||
calendarVisibility: request.query.calendarVisibility,
|
||||
messageVisibility: request.query.messageVisibility,
|
||||
loginHint: request.query.loginHint,
|
||||
userId: userId,
|
||||
workspaceId: workspaceId,
|
||||
});
|
||||
|
||||
if (
|
||||
!this.environmentService.get('MESSAGING_PROVIDER_GMAIL_ENABLED') &&
|
||||
!this.environmentService.get('CALENDAR_PROVIDER_GOOGLE_ENABLED')
|
||||
) {
|
||||
throw new AuthException(
|
||||
'Google apis auth is not enabled',
|
||||
AuthExceptionCode.GOOGLE_API_AUTH_DISABLED,
|
||||
);
|
||||
}
|
||||
|
||||
new GoogleAPIsOauthRequestCodeStrategy(this.environmentService, {});
|
||||
|
||||
return (await super.canActivate(context)) as boolean;
|
||||
} catch (err) {
|
||||
this.guardRedirectService.dispatchErrorFromGuard(
|
||||
context,
|
||||
err,
|
||||
workspace ?? {
|
||||
subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
},
|
||||
);
|
||||
|
||||
setRequestExtraParams(request, {
|
||||
transientToken: request.query.transientToken,
|
||||
redirectLocation: request.query.redirectLocation,
|
||||
calendarVisibility: request.query.calendarVisibility,
|
||||
messageVisibility: request.query.messageVisibility,
|
||||
loginHint: request.query.loginHint,
|
||||
userId: userId,
|
||||
workspaceId: workspaceId,
|
||||
});
|
||||
|
||||
if (
|
||||
!this.environmentService.get('MESSAGING_PROVIDER_GMAIL_ENABLED') &&
|
||||
!this.environmentService.get('CALENDAR_PROVIDER_GOOGLE_ENABLED')
|
||||
) {
|
||||
throw new AuthException(
|
||||
'Google apis auth is not enabled',
|
||||
AuthExceptionCode.GOOGLE_API_AUTH_DISABLED,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
new GoogleAPIsOauthRequestCodeStrategy(this.environmentService, {});
|
||||
|
||||
const activate = (await super.canActivate(context)) as boolean;
|
||||
|
||||
return activate;
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,8 +75,9 @@ export class GoogleOauthGuard extends AuthGuard('google') {
|
||||
this.guardRedirectService.dispatchErrorFromGuard(
|
||||
context,
|
||||
err,
|
||||
workspace?.subdomain ??
|
||||
this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
workspace ?? {
|
||||
subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
},
|
||||
);
|
||||
|
||||
return false;
|
||||
|
||||
@ -28,11 +28,9 @@ export class GoogleProviderEnabledGuard implements CanActivate {
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
this.guardRedirectService.dispatchErrorFromGuard(
|
||||
context,
|
||||
err,
|
||||
this.guardRedirectService.getSubdomainFromContext(context),
|
||||
);
|
||||
this.guardRedirectService.dispatchErrorFromGuard(context, err, {
|
||||
subdomain: this.guardRedirectService.getSubdomainFromContext(context),
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -8,12 +8,16 @@ import {
|
||||
import { MicrosoftAPIsOauthExchangeCodeForTokenStrategy } from 'src/engine/core-modules/auth/strategies/microsoft-apis-oauth-exchange-code-for-token.auth.strategy';
|
||||
import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util';
|
||||
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 MicrosoftAPIsOauthExchangeCodeForTokenGuard extends AuthGuard(
|
||||
'microsoft-apis',
|
||||
) {
|
||||
constructor(private readonly environmentService: EnvironmentService) {
|
||||
constructor(
|
||||
private readonly guardRedirectService: GuardRedirectService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@ -35,12 +39,18 @@ export class MicrosoftAPIsOauthExchangeCodeForTokenGuard extends AuthGuard(
|
||||
|
||||
return (await super.canActivate(context)) as boolean;
|
||||
} catch (error) {
|
||||
if (error?.oauthError?.statusCode === 403) {
|
||||
throw new AuthException(
|
||||
`Insufficient privileges to access this microsoft resource. Make sure you have the correct scopes or ask your admin to update your scopes. ${error?.message}`,
|
||||
AuthExceptionCode.INSUFFICIENT_SCOPES,
|
||||
);
|
||||
}
|
||||
this.guardRedirectService.dispatchErrorFromGuard(
|
||||
context,
|
||||
error?.oauthError?.statusCode === 403
|
||||
? new AuthException(
|
||||
`Insufficient privileges to access this microsoft resource. Make sure you have the correct scopes or ask your admin to update your scopes. ${error?.message}`,
|
||||
AuthExceptionCode.INSUFFICIENT_SCOPES,
|
||||
)
|
||||
: error,
|
||||
{
|
||||
subdomain: this.guardRedirectService.getSubdomainFromContext(context),
|
||||
},
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
AuthException,
|
||||
@ -11,6 +14,8 @@ import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google
|
||||
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 { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
|
||||
|
||||
@Injectable()
|
||||
export class MicrosoftAPIsOauthRequestCodeGuard extends AuthGuard(
|
||||
@ -20,6 +25,9 @@ export class MicrosoftAPIsOauthRequestCodeGuard extends AuthGuard(
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly transientTokenService: TransientTokenService,
|
||||
private readonly guardRedirectService: GuardRedirectService,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
) {
|
||||
super({
|
||||
prompt: 'select_account',
|
||||
@ -27,36 +35,53 @@ export class MicrosoftAPIsOauthRequestCodeGuard extends AuthGuard(
|
||||
}
|
||||
|
||||
async canActivate(context: ExecutionContext) {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
let workspace: Workspace | null = null;
|
||||
|
||||
const { workspaceId } =
|
||||
await this.transientTokenService.verifyTransientToken(
|
||||
request.query.transientToken,
|
||||
);
|
||||
const isMicrosoftSyncEnabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IsMicrosoftSyncEnabled,
|
||||
workspaceId,
|
||||
try {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
|
||||
const { workspaceId } =
|
||||
await this.transientTokenService.verifyTransientToken(
|
||||
request.query.transientToken,
|
||||
);
|
||||
|
||||
workspace = await this.workspaceRepository.findOneBy({
|
||||
id: workspaceId,
|
||||
});
|
||||
|
||||
const isMicrosoftSyncEnabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IsMicrosoftSyncEnabled,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (!isMicrosoftSyncEnabled) {
|
||||
throw new AuthException(
|
||||
'Microsoft sync is not enabled',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
new MicrosoftAPIsOauthRequestCodeStrategy(this.environmentService);
|
||||
setRequestExtraParams(request, {
|
||||
transientToken: request.query.transientToken,
|
||||
redirectLocation: request.query.redirectLocation,
|
||||
calendarVisibility: request.query.calendarVisibility,
|
||||
messageVisibility: request.query.messageVisibility,
|
||||
loginHint: request.query.loginHint,
|
||||
});
|
||||
|
||||
return (await super.canActivate(context)) as boolean;
|
||||
} catch (err) {
|
||||
this.guardRedirectService.dispatchErrorFromGuard(
|
||||
context,
|
||||
err,
|
||||
workspace ?? {
|
||||
subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
},
|
||||
);
|
||||
|
||||
if (!isMicrosoftSyncEnabled) {
|
||||
throw new AuthException(
|
||||
'Microsoft sync is not enabled',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
new MicrosoftAPIsOauthRequestCodeStrategy(this.environmentService);
|
||||
setRequestExtraParams(request, {
|
||||
transientToken: request.query.transientToken,
|
||||
redirectLocation: request.query.redirectLocation,
|
||||
calendarVisibility: request.query.calendarVisibility,
|
||||
messageVisibility: request.query.messageVisibility,
|
||||
loginHint: request.query.loginHint,
|
||||
});
|
||||
|
||||
const activate = (await super.canActivate(context)) as boolean;
|
||||
|
||||
return activate;
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,8 +64,9 @@ export class MicrosoftOAuthGuard extends AuthGuard('microsoft') {
|
||||
this.guardRedirectService.dispatchErrorFromGuard(
|
||||
context,
|
||||
err,
|
||||
workspace?.subdomain ??
|
||||
this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
workspace ?? {
|
||||
subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
},
|
||||
);
|
||||
|
||||
return false;
|
||||
|
||||
@ -28,11 +28,9 @@ export class MicrosoftProviderEnabledGuard implements CanActivate {
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
this.guardRedirectService.dispatchErrorFromGuard(
|
||||
context,
|
||||
err,
|
||||
this.guardRedirectService.getSubdomainFromContext(context),
|
||||
);
|
||||
this.guardRedirectService.dispatchErrorFromGuard(context, err, {
|
||||
subdomain: this.guardRedirectService.getSubdomainFromContext(context),
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -77,8 +77,9 @@ export class OIDCAuthGuard extends AuthGuard('openidconnect') {
|
||||
this.guardRedirectService.dispatchErrorFromGuard(
|
||||
context,
|
||||
err,
|
||||
identityProvider?.workspace.subdomain ??
|
||||
this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
identityProvider?.workspace ?? {
|
||||
subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
},
|
||||
);
|
||||
|
||||
return false;
|
||||
|
||||
@ -49,8 +49,9 @@ export class SAMLAuthGuard extends AuthGuard('saml') {
|
||||
this.guardRedirectService.dispatchErrorFromGuard(
|
||||
context,
|
||||
err,
|
||||
identityProvider?.workspace.subdomain ??
|
||||
this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
identityProvider?.workspace ?? {
|
||||
subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
},
|
||||
);
|
||||
|
||||
return false;
|
||||
|
||||
@ -0,0 +1,114 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { SocialSsoService } from 'src/engine/core-modules/auth/services/social-sso.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
describe('SocialSsoService', () => {
|
||||
let socialSsoService: SocialSsoService;
|
||||
let workspaceRepository: Repository<Workspace>;
|
||||
let environmentService: EnvironmentService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
SocialSsoService,
|
||||
{
|
||||
provide: getRepositoryToken(Workspace, 'core'),
|
||||
useClass: Repository,
|
||||
},
|
||||
{
|
||||
provide: EnvironmentService,
|
||||
useValue: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
socialSsoService = module.get<SocialSsoService>(SocialSsoService);
|
||||
workspaceRepository = module.get<Repository<Workspace>>(
|
||||
getRepositoryToken(Workspace, 'core'),
|
||||
);
|
||||
environmentService = module.get<EnvironmentService>(EnvironmentService);
|
||||
});
|
||||
|
||||
describe('findWorkspaceFromWorkspaceIdOrAuthProvider', () => {
|
||||
it('should return a workspace by workspaceId', async () => {
|
||||
const workspaceId = 'workspace-id-123';
|
||||
const mockWorkspace = { id: workspaceId } as Workspace;
|
||||
|
||||
jest
|
||||
.spyOn(workspaceRepository, 'findOneBy')
|
||||
.mockResolvedValue(mockWorkspace);
|
||||
|
||||
const result =
|
||||
await socialSsoService.findWorkspaceFromWorkspaceIdOrAuthProvider(
|
||||
{ authProvider: 'google', email: 'test@example.com' },
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockWorkspace);
|
||||
expect(workspaceRepository.findOneBy).toHaveBeenCalledWith({
|
||||
id: workspaceId,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a workspace from authProvider and email when multi-workspace mode is enabled', async () => {
|
||||
const authProvider = 'google';
|
||||
const email = 'test@example.com';
|
||||
const mockWorkspace = { id: 'workspace-id-456' } as Workspace;
|
||||
|
||||
jest.spyOn(environmentService, 'get').mockReturnValue(true);
|
||||
jest
|
||||
.spyOn(workspaceRepository, 'findOne')
|
||||
.mockResolvedValue(mockWorkspace);
|
||||
|
||||
const result =
|
||||
await socialSsoService.findWorkspaceFromWorkspaceIdOrAuthProvider({
|
||||
authProvider,
|
||||
email,
|
||||
});
|
||||
|
||||
expect(result).toEqual(mockWorkspace);
|
||||
expect(workspaceRepository.findOne).toHaveBeenCalledWith({
|
||||
where: {
|
||||
isGoogleAuthEnabled: true,
|
||||
workspaceUsers: {
|
||||
user: {
|
||||
email,
|
||||
},
|
||||
},
|
||||
},
|
||||
relations: ['workspaceUsers', 'workspaceUsers.user'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined if no workspace is found when multi-workspace mode is enabled', async () => {
|
||||
jest.spyOn(environmentService, 'get').mockReturnValue(true);
|
||||
jest.spyOn(workspaceRepository, 'findOne').mockResolvedValue(null);
|
||||
|
||||
const result =
|
||||
await socialSsoService.findWorkspaceFromWorkspaceIdOrAuthProvider({
|
||||
authProvider: 'google',
|
||||
email: 'notfound@example.com',
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should throw an error for an invalid authProvider', async () => {
|
||||
jest.spyOn(environmentService, 'get').mockReturnValue(true);
|
||||
|
||||
await expect(
|
||||
socialSsoService.findWorkspaceFromWorkspaceIdOrAuthProvider({
|
||||
authProvider: 'invalid-provider' as any,
|
||||
email: 'test@example.com',
|
||||
}),
|
||||
).rejects.toThrow('invalid-provider is not a valid auth provider.');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -2,27 +2,31 @@ 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';
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
|
||||
@Injectable()
|
||||
export class GuardRedirectService {
|
||||
constructor(
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly exceptionHandlerService: ExceptionHandlerService,
|
||||
) {}
|
||||
|
||||
dispatchErrorFromGuard(context: any, err: any, subdomain: string) {
|
||||
dispatchErrorFromGuard(
|
||||
context: ExecutionContext,
|
||||
error: Error | CustomException,
|
||||
workspace: { id?: string; subdomain: string },
|
||||
) {
|
||||
if ('contextType' in context && context.contextType === 'graphql') {
|
||||
throw err;
|
||||
throw error;
|
||||
}
|
||||
|
||||
context
|
||||
.switchToHttp()
|
||||
.getResponse()
|
||||
.redirect(
|
||||
this.domainManagerService
|
||||
.computeRedirectErrorUrl(err.message ?? 'Unknown error', subdomain)
|
||||
.toString(),
|
||||
);
|
||||
.redirect(this.getRedirectErrorUrlAndCaptureExceptions(error, workspace));
|
||||
}
|
||||
|
||||
getSubdomainFromContext(context: ExecutionContext) {
|
||||
@ -35,4 +39,26 @@ export class GuardRedirectService {
|
||||
|
||||
return subdomainFromUrl ?? this.environmentService.get('DEFAULT_SUBDOMAIN');
|
||||
}
|
||||
|
||||
private captureException(err: Error | CustomException, workspaceId?: string) {
|
||||
if (err instanceof AuthException) return;
|
||||
|
||||
this.exceptionHandlerService.captureExceptions([err], {
|
||||
workspace: {
|
||||
id: workspaceId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getRedirectErrorUrlAndCaptureExceptions(
|
||||
err: Error | CustomException,
|
||||
workspace: { id?: string; subdomain: string },
|
||||
) {
|
||||
this.captureException(err, workspace.id);
|
||||
|
||||
return this.domainManagerService.computeRedirectErrorUrl(
|
||||
err instanceof AuthException ? err.message : 'Unknown error',
|
||||
workspace.subdomain,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ import {
|
||||
OIDCResponseType,
|
||||
WorkspaceSSOIdentityProvider,
|
||||
} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
|
||||
@Injectable()
|
||||
export class SSOService {
|
||||
@ -32,6 +33,7 @@ export class SSOService {
|
||||
private readonly workspaceSSOIdentityProviderRepository: Repository<WorkspaceSSOIdentityProvider>,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly billingService: BillingService,
|
||||
private readonly exceptionHandlerService: ExceptionHandlerService,
|
||||
) {}
|
||||
|
||||
private async isSSOEnabled(workspaceId: string) {
|
||||
@ -48,6 +50,17 @@ export class SSOService {
|
||||
}
|
||||
}
|
||||
|
||||
private async getIssuerForOIDC(issuerUrl: string) {
|
||||
try {
|
||||
return await Issuer.discover(issuerUrl);
|
||||
} catch (err) {
|
||||
throw new SSOException(
|
||||
'Invalid issuer',
|
||||
SSOExceptionCode.INVALID_ISSUER_URL,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async createOIDCIdentityProvider(
|
||||
data: Pick<
|
||||
WorkspaceSSOIdentityProvider,
|
||||
@ -58,21 +71,7 @@ export class SSOService {
|
||||
try {
|
||||
await this.isSSOEnabled(workspaceId);
|
||||
|
||||
if (!data.issuer) {
|
||||
throw new SSOException(
|
||||
'Invalid issuer URL',
|
||||
SSOExceptionCode.INVALID_ISSUER_URL,
|
||||
);
|
||||
}
|
||||
|
||||
const issuer = await Issuer.discover(data.issuer);
|
||||
|
||||
if (!issuer.metadata.issuer) {
|
||||
throw new SSOException(
|
||||
'Invalid issuer URL from discovery',
|
||||
SSOExceptionCode.INVALID_ISSUER_URL,
|
||||
);
|
||||
}
|
||||
const issuer = await this.getIssuerForOIDC(data.issuer);
|
||||
|
||||
const identityProvider =
|
||||
await this.workspaceSSOIdentityProviderRepository.save({
|
||||
@ -96,6 +95,8 @@ export class SSOService {
|
||||
return err;
|
||||
}
|
||||
|
||||
this.exceptionHandlerService.captureExceptions([err]);
|
||||
|
||||
return new SSOException(
|
||||
'Unknown SSO configuration error',
|
||||
SSOExceptionCode.UNKNOWN_SSO_CONFIGURATION_ERROR,
|
||||
@ -131,6 +132,7 @@ export class SSOService {
|
||||
async findSSOIdentityProviderById(identityProviderId: string) {
|
||||
return (await this.workspaceSSOIdentityProviderRepository.findOne({
|
||||
where: { id: identityProviderId },
|
||||
relations: ['workspace'],
|
||||
})) as (SSOConfiguration & WorkspaceSSOIdentityProvider) | null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user