feat(sso): allow to use OIDC and SAML (#7246)

## What it does
### Backend
- [x] Add a mutation to create OIDC and SAML configuration
- [x] Add a mutation to delete an SSO config
- [x] Add a feature flag to toggle SSO
- [x] Add a mutation to activate/deactivate an SSO config
- [x] Add a mutation to delete an SSO config
- [x] Add strategy to use OIDC or SAML
- [ ] Improve error management

### Frontend
- [x] Add section "security" in settings
- [x] Add page to list SSO configurations
- [x] Add page and forms to create OIDC or SAML configuration
- [x] Add field to "connect with SSO" in the signin/signup process
- [x] Trigger auth when a user switch to a workspace with SSO enable
- [x] Add an option on the security page to activate/deactivate the
global invitation link
- [ ] Add new Icons for SSO Identity Providers (okta, Auth0, Azure,
Microsoft)

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Antoine Moreaux
2024-10-21 20:07:08 +02:00
committed by GitHub
parent 11c3f1c399
commit 0f0a7966b1
132 changed files with 5245 additions and 306 deletions

View File

@ -17,4 +17,6 @@ export enum AuthExceptionCode {
INVALID_DATA = 'INVALID_DATA',
INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR',
OAUTH_ACCESS_DENIED = 'OAUTH_ACCESS_DENIED',
SSO_AUTH_FAILED = 'SSO_AUTH_FAILED',
USE_SSO_AUTH = 'USE_SSO_AUTH',
}

View File

@ -27,7 +27,13 @@ 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 { WorkspaceSSOModule } from 'src/engine/core-modules/sso/sso.module';
import { SSOAuthController } from 'src/engine/core-modules/auth/controllers/sso-auth.controller';
import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
import { SamlAuthStrategy } from 'src/engine/core-modules/auth/strategies/saml.auth.strategy';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { WorkspaceInvitationModule } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.module';
import { AuthResolver } from './auth.resolver';
@ -43,7 +49,14 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
WorkspaceManagerModule,
TypeORMModule,
TypeOrmModule.forFeature(
[Workspace, User, AppToken, FeatureFlagEntity],
[
Workspace,
User,
AppToken,
FeatureFlagEntity,
WorkspaceSSOIdentityProvider,
KeyValuePair,
],
'core',
),
HttpModule,
@ -52,7 +65,9 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
WorkspaceModule,
OnboardingModule,
WorkspaceDataSourceModule,
WorkspaceInvitationModule,
ConnectedAccountModule,
WorkspaceSSOModule,
FeatureFlagModule,
],
controllers: [
@ -60,11 +75,13 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
MicrosoftAuthController,
GoogleAPIsAuthController,
VerifyAuthController,
SSOAuthController,
],
providers: [
SignInUpService,
AuthService,
JwtAuthStrategy,
SamlAuthStrategy,
AuthResolver,
TokenService,
GoogleAPIsService,

View File

@ -24,6 +24,11 @@ import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import {
GenerateJWTOutput,
GenerateJWTOutputWithAuthTokens,
GenerateJWTOutputWithSSOAUTH,
} from 'src/engine/core-modules/auth/dto/generateJWT.output';
import { ChallengeInput } from './dto/challenge.input';
import { ImpersonateInput } from './dto/impersonate.input';
@ -159,18 +164,41 @@ export class AuthResolver {
return authorizedApp;
}
@Mutation(() => AuthTokens)
@Mutation(() => GenerateJWTOutput)
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
async generateJWT(
@AuthUser() user: User,
@Args() args: GenerateJwtInput,
): Promise<AuthTokens> {
const token = await this.tokenService.generateSwitchWorkspaceToken(
): Promise<GenerateJWTOutputWithAuthTokens | GenerateJWTOutputWithSSOAUTH> {
const result = await this.tokenService.switchWorkspace(
user,
args.workspaceId,
);
return token;
if (result.useSSOAuth) {
return {
success: true,
reason: 'WORKSPACE_USE_SSO_AUTH',
availableSSOIDPs: result.availableSSOIdentityProviders.map(
(identityProvider) => ({
...identityProvider,
workspace: {
id: result.workspace.id,
displayName: result.workspace.displayName,
},
}),
),
};
}
return {
success: true,
reason: 'WORKSPACE_AVAILABLE_FOR_SWITCH',
authTokens: await this.tokenService.generateSwitchWorkspaceToken(
user,
result.workspace,
),
};
}
@Mutation(() => AuthTokens)

View File

@ -0,0 +1,161 @@
/* @license Enterprise */
import {
Controller,
Get,
Post,
Req,
Res,
UseFilters,
UseGuards,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { generateServiceProviderMetadata } from '@node-saml/node-saml';
import { Response } from 'express';
import { Repository } from 'typeorm';
import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
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 { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
import { TokenService } from 'src/engine/core-modules/auth/token/services/token.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
import {
IdentityProviderType,
WorkspaceSSOIdentityProvider,
} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
@Controller('auth')
@UseFilters(AuthRestApiExceptionFilter)
export class SSOAuthController {
constructor(
private readonly tokenService: TokenService,
private readonly authService: AuthService,
private readonly workspaceInvitationService: WorkspaceInvitationService,
private readonly environmentService: EnvironmentService,
private readonly userWorkspaceService: UserWorkspaceService,
private readonly ssoService: SSOService,
@InjectRepository(WorkspaceSSOIdentityProvider, 'core')
private readonly workspaceSSOIdentityProviderRepository: Repository<WorkspaceSSOIdentityProvider>,
) {}
@Get('saml/metadata/:identityProviderId')
@UseGuards(SSOProviderEnabledGuard)
async generateMetadata(@Req() req: any): Promise<string> {
return generateServiceProviderMetadata({
wantAssertionsSigned: false,
issuer: this.ssoService.buildIssuerURL({
id: req.params.identityProviderId,
type: IdentityProviderType.SAML,
}),
callbackUrl: this.ssoService.buildCallbackUrl({
type: IdentityProviderType.SAML,
}),
});
}
@Get('oidc/login/:identityProviderId')
@UseGuards(SSOProviderEnabledGuard, OIDCAuthGuard)
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)
async samlAuth() {
// As this method is protected by SAML Auth guard, it will trigger SAML SSO flow
return;
}
@Get('oidc/callback')
@UseGuards(SSOProviderEnabledGuard, OIDCAuthGuard)
async oidcAuthCallback(@Req() req: any, @Res() res: Response) {
try {
const loginToken = await this.generateLoginToken(req.user);
return res.redirect(
this.tokenService.computeRedirectURI(loginToken.token),
);
} catch (err) {
// TODO: improve error management
res.status(403).send(err.message);
}
}
@Post('saml/callback')
@UseGuards(SSOProviderEnabledGuard, SAMLAuthGuard)
async samlAuthCallback(@Req() req: any, @Res() res: Response) {
try {
const loginToken = await this.generateLoginToken(req.user);
return res.redirect(
this.tokenService.computeRedirectURI(loginToken.token),
);
} catch (err) {
// TODO: improve error management
res.status(403).send(err.message);
res.redirect(`${this.environmentService.get('FRONT_BASE_URL')}/verify`);
}
}
private async generateLoginToken({
user,
identityProviderId,
}: {
identityProviderId?: string;
user: { email: string } & Record<string, string>;
}) {
const identityProvider =
await this.workspaceSSOIdentityProviderRepository.findOne({
where: { id: identityProviderId },
relations: ['workspace'],
});
if (!identityProvider) {
throw new AuthException(
'Identity provider not found',
AuthExceptionCode.INVALID_DATA,
);
}
const invitation =
await this.workspaceInvitationService.getOneWorkspaceInvitation(
identityProvider.workspaceId,
user.email,
);
if (invitation) {
await this.authService.signInUp({
...user,
workspacePersonalInviteToken: invitation.value,
workspaceInviteHash: identityProvider.workspace.inviteHash,
fromSSO: true,
});
}
const isUserExistInWorkspace =
await this.userWorkspaceService.checkUserWorkspaceExistsByEmail(
user.email,
identityProvider.workspaceId,
);
if (!isUserExistInWorkspace) {
throw new AuthException(
'User not found in workspace',
AuthExceptionCode.FORBIDDEN_EXCEPTION,
);
}
return this.tokenService.generateLoginToken(user.email);
}
}

View File

@ -0,0 +1,43 @@
import { Field, ObjectType, createUnionType } from '@nestjs/graphql';
import { AuthTokens } from 'src/engine/core-modules/auth/dto/token.entity';
import { FindAvailableSSOIDPOutput } from 'src/engine/core-modules/sso/dtos/find-available-SSO-IDP.output';
@ObjectType()
export class GenerateJWTOutputWithAuthTokens {
@Field(() => Boolean)
success: boolean;
@Field(() => String)
reason: 'WORKSPACE_AVAILABLE_FOR_SWITCH';
@Field(() => AuthTokens)
authTokens: AuthTokens;
}
@ObjectType()
export class GenerateJWTOutputWithSSOAUTH {
@Field(() => Boolean)
success: boolean;
@Field(() => String)
reason: 'WORKSPACE_USE_SSO_AUTH';
@Field(() => [FindAvailableSSOIDPOutput])
availableSSOIDPs: Array<FindAvailableSSOIDPOutput>;
}
export const GenerateJWTOutput = createUnionType({
name: 'GenerateJWT',
types: () => [GenerateJWTOutputWithAuthTokens, GenerateJWTOutputWithSSOAUTH],
resolveType(value) {
if (value.reason === 'WORKSPACE_AVAILABLE_FOR_SWITCH') {
return GenerateJWTOutputWithAuthTokens;
}
if (value.reason === 'WORKSPACE_USE_SSO_AUTH') {
return GenerateJWTOutputWithSSOAUTH;
}
return null;
},
});

View File

@ -0,0 +1,73 @@
/* @license Enterprise */
import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Issuer } from 'openid-client';
import {
AuthException,
AuthExceptionCode,
} 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';
@Injectable()
export class OIDCAuthGuard extends AuthGuard('openidconnect') {
constructor(private readonly ssoService: SSOService) {
super();
}
private getIdentityProviderId(request: any): string {
if (request.params.identityProviderId) {
return request.params.identityProviderId;
}
if (
request.query.state &&
typeof request.query.state === 'string' &&
request.query.state.startsWith('{') &&
request.query.state.endsWith('}')
) {
const state = JSON.parse(request.query.state);
return state.identityProviderId;
}
throw new Error('Invalid OIDC identity provider params');
}
async canActivate(context: ExecutionContext): Promise<boolean> {
try {
const request = context.switchToHttp().getRequest();
const identityProviderId = this.getIdentityProviderId(request);
const identityProvider =
await this.ssoService.findSSOIdentityProviderById(identityProviderId);
if (!identityProvider) {
throw new AuthException(
'Identity provider not found',
AuthExceptionCode.INVALID_DATA,
);
}
const issuer = await Issuer.discover(identityProvider.issuer);
new OIDCAuthStrategy(
this.ssoService.getOIDCClient(identityProvider, issuer),
identityProvider.id,
);
return (await super.canActivate(context)) as boolean;
} catch (err) {
if (err instanceof AuthException) {
return false;
}
// TODO AMOREAUX: trigger sentry error
return false;
}
}
}

View File

@ -0,0 +1,48 @@
/* @license Enterprise */
import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import {
AuthException,
AuthExceptionCode,
} 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';
@Injectable()
export class SAMLAuthGuard extends AuthGuard('saml') {
constructor(private readonly sSOService: SSOService) {
super();
}
async canActivate(context: ExecutionContext) {
try {
const request = context.switchToHttp().getRequest();
const RelayState =
'RelayState' in request.body ? JSON.parse(request.body.RelayState) : {};
request.params.identityProviderId =
request.params.identityProviderId ?? RelayState.identityProviderId;
if (!request.params.identityProviderId) {
throw new AuthException(
'Invalid SAML identity provider',
AuthExceptionCode.INVALID_DATA,
);
}
new SamlAuthStrategy(this.sSOService);
return (await super.canActivate(context)) as boolean;
} catch (err) {
if (err instanceof AuthException) {
return false;
}
// TODO AMOREAUX: trigger sentry error
return false;
}
}
}

View File

@ -0,0 +1,27 @@
/* @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.FORBIDDEN_EXCEPTION,
);
}
return true;
}
}

View File

@ -35,7 +35,6 @@ import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-u
import { TokenService } from 'src/engine/core-modules/auth/token/services/token.service';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@ -43,7 +42,6 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
export class AuthService {
constructor(
private readonly tokenService: TokenService,
private readonly userService: UserService,
private readonly signInUpService: SignInUpService,
@InjectRepository(Workspace, 'core')
private readonly workspaceRepository: Repository<Workspace>,

View File

@ -225,23 +225,45 @@ export class SignInUpService {
email,
}) {
if (!workspacePersonalInviteToken && !workspaceInviteHash) {
throw new Error('No invite token or hash provided');
}
if (!workspacePersonalInviteToken && workspaceInviteHash) {
return (
(await this.workspaceRepository.findOneBy({
inviteHash: workspaceInviteHash,
})) ?? undefined
throw new AuthException(
'No invite token or hash provided',
AuthExceptionCode.FORBIDDEN_EXCEPTION,
);
}
const appToken = await this.userWorkspaceService.validateInvitation(
workspacePersonalInviteToken,
email,
);
const workspace = await this.workspaceRepository.findOneBy({
inviteHash: workspaceInviteHash,
});
return appToken?.workspace;
if (!workspace) {
throw new AuthException(
'Workspace not found',
AuthExceptionCode.WORKSPACE_NOT_FOUND,
);
}
if (!workspacePersonalInviteToken && !workspace.isPublicInviteLinkEnabled) {
throw new AuthException(
'Workspace does not allow public invites',
AuthExceptionCode.FORBIDDEN_EXCEPTION,
);
}
if (workspacePersonalInviteToken && workspace.isPublicInviteLinkEnabled) {
try {
await this.userWorkspaceService.validateInvitation(
workspacePersonalInviteToken,
email,
);
} catch (err) {
throw new AuthException(
err.message,
AuthExceptionCode.FORBIDDEN_EXCEPTION,
);
}
}
return workspace;
}
private async activateOnboardingForNewUser(

View File

@ -0,0 +1,86 @@
/* @license Enterprise */
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import {
Strategy,
StrategyOptions,
StrategyVerifyCallbackReq,
} from 'openid-client';
import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
@Injectable()
export class OIDCAuthStrategy extends PassportStrategy(
Strategy,
'openidconnect',
) {
constructor(
private client: StrategyOptions['client'],
sessionKey: string,
) {
super({
params: {
scope: 'openid email profile',
code_challenge_method: 'S256',
},
client,
usePKCE: true,
passReqToCallback: true,
sessionKey,
});
}
async authenticate(req: any, options: any) {
return super.authenticate(req, {
...options,
state: JSON.stringify({
identityProviderId: req.params.identityProviderId,
}),
});
}
validate: StrategyVerifyCallbackReq<{
identityProviderId: string;
user: {
email: string;
firstName?: string | null;
lastName?: string | null;
};
}> = async (req, tokenset, done) => {
try {
const state = JSON.parse(
'query' in req &&
req.query &&
typeof req.query === 'object' &&
'state' in req.query &&
req.query.state &&
typeof req.query.state === 'string'
? req.query.state
: '{}',
);
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 } : {}),
...(userinfo.family_name ? { lastName: userinfo.family_name } : {}),
};
done(null, { user, identityProviderId: state.identityProviderId });
} catch (err) {
done(err);
}
};
}

View File

@ -0,0 +1,98 @@
/* @license Enterprise */
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import {
MultiSamlStrategy,
MultiStrategyConfig,
PassportSamlConfig,
SamlConfig,
VerifyWithRequest,
} from '@node-saml/passport-saml';
import { AuthenticateOptions } from '@node-saml/passport-saml/lib/types';
import { isEmail } from 'class-validator';
import { Request } from 'express';
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
@Injectable()
export class SamlAuthStrategy extends PassportStrategy(
MultiSamlStrategy,
'saml',
) {
constructor(private readonly sSOService: SSOService) {
super({
getSamlOptions: (req, callback) => {
this.sSOService
.findSSOIdentityProviderById(req.params.identityProviderId)
.then((identityProvider) => {
if (
identityProvider &&
this.sSOService.isSAMLIdentityProvider(identityProvider)
) {
const config: SamlConfig = {
entryPoint: identityProvider.ssoURL,
issuer: this.sSOService.buildIssuerURL(identityProvider),
callbackUrl: this.sSOService.buildCallbackUrl(identityProvider),
idpCert: identityProvider.certificate,
wantAssertionsSigned: false,
// TODO: Improve the feature by sign the response
wantAuthnResponseSigned: false,
signatureAlgorithm: 'sha256',
};
return callback(null, config);
}
// TODO: improve error management
return callback(new Error('Invalid SAML identity provider'));
})
.catch((err) => {
// TODO: improve error management
return callback(err);
});
},
passReqToCallback: true,
} as PassportSamlConfig & MultiStrategyConfig);
}
authenticate(req: Request, options: AuthenticateOptions) {
super.authenticate(req, {
...options,
additionalParams: {
RelayState: JSON.stringify({
identityProviderId: req.params.identityProviderId,
}),
},
});
}
validate: VerifyWithRequest = async (request, profile, done) => {
if (!profile) {
return done(new Error('Profile is must be provided'));
}
const email = profile.email ?? profile.mail ?? profile.nameID;
if (!isEmail(email)) {
return done(new Error('Invalid email'));
}
const result: {
user: Record<string, string>;
identityProviderId?: string;
} = { user: { email } };
if (
'RelayState' in request.body &&
typeof request.body.RelayState === 'string'
) {
const RelayState = JSON.parse(request.body.RelayState);
result.identityProviderId = RelayState.identityProviderId;
}
done(null, result);
};
}

View File

@ -17,6 +17,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
import { TokenService } from './token.service';
@ -50,6 +51,12 @@ describe('TokenService', () => {
send: jest.fn(),
},
},
{
provide: SSOService,
useValue: {
send: jest.fn(),
},
},
{
provide: getRepositoryToken(User, 'core'),
useValue: {

View File

@ -46,6 +46,7 @@ import {
} from 'src/engine/core-modules/workspace/workspace.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
@Injectable()
export class TokenService {
@ -60,6 +61,7 @@ export class TokenService {
@InjectRepository(Workspace, 'core')
private readonly workspaceRepository: Repository<Workspace>,
private readonly emailService: EmailService,
private readonly sSSOService: SSOService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
@ -341,10 +343,7 @@ export class TokenService {
};
}
async generateSwitchWorkspaceToken(
user: User,
workspaceId: string,
): Promise<AuthTokens> {
async switchWorkspace(user: User, workspaceId: string) {
const userExists = await this.userRepository.findBy({ id: user.id });
if (!userExists) {
@ -356,7 +355,7 @@ export class TokenService {
const workspace = await this.workspaceRepository.findOne({
where: { id: workspaceId },
relations: ['workspaceUsers'],
relations: ['workspaceUsers', 'workspaceSSOIdentityProviders'],
});
if (!workspace) {
@ -377,12 +376,44 @@ export class TokenService {
);
}
if (workspace.workspaceSSOIdentityProviders.length > 0) {
return {
useSSOAuth: true,
workspace,
availableSSOIdentityProviders:
await this.sSSOService.listSSOIdentityProvidersByWorkspaceId(
workspaceId,
),
} as {
useSSOAuth: true;
workspace: Workspace;
availableSSOIdentityProviders: Awaited<
ReturnType<
typeof this.sSSOService.listSSOIdentityProvidersByWorkspaceId
>
>;
};
}
return {
useSSOAuth: false,
workspace,
} as {
useSSOAuth: false;
workspace: Workspace;
};
}
async generateSwitchWorkspaceToken(
user: User,
workspace: Workspace,
): Promise<AuthTokens> {
await this.userRepository.save({
id: user.id,
defaultWorkspace: workspace,
});
const token = await this.generateAccessToken(user.id, workspaceId);
const token = await this.generateAccessToken(user.id, workspace.id);
const refreshToken = await this.generateRefreshToken(user.id);
return {

View File

@ -11,6 +11,7 @@ import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { WorkspaceSSOModule } from 'src/engine/core-modules/sso/sso.module';
@Module({
imports: [
@ -19,6 +20,7 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s
TypeORMModule,
DataSourceModule,
EmailModule,
WorkspaceSSOModule,
],
providers: [TokenService, JwtAuthStrategy],
exports: [TokenService],