Skip to content

Commit

Permalink
fix(auth): Improve error management with sso + fix microsoft saml (#9799
Browse files Browse the repository at this point in the history
)

Fix #9760 #9758
  • Loading branch information
AMoreaux authored Jan 24, 2025
1 parent 3c85516 commit 5783c41
Show file tree
Hide file tree
Showing 49 changed files with 499 additions and 303 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,12 @@ export const SettingsSSOSAMLForm = () => {
if (isDefined(e.target.files)) {
const text = await e.target.files[0].text();
const samlMetadataParsed = parseSAMLMetadataFromXMLFile(text);
e.target.value = '';
if (!samlMetadataParsed.success) {
enqueueSnackBar('Invalid File', {
return enqueueSnackBar('Invalid File', {
variant: SnackBarVariant.Error,
duration: 2000,
});
return;
}
setValue('ssoURL', samlMetadataParsed.data.ssoUrl);
setValue('certificate', samlMetadataParsed.data.certificate);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,29 @@ const validator = z.object({
certificate: z.string().min(1),
});

const getByPrefixAndKey = (
xmlDoc: Document | Element,
key: string,
prefix = 'md',
): Element | undefined => {
return (
xmlDoc.getElementsByTagName(`${prefix}:${key}`)?.[0] ??
xmlDoc.getElementsByTagName(`${key}`)?.[0]
);
};

const getAllByPrefixAndKey = (
xmlDoc: Document | Element,
key: string,
prefix = 'md',
) => {
const withPrefix = xmlDoc.getElementsByTagName(`${prefix}:${key}`);
if (withPrefix.length !== 0) {
return Array.from(withPrefix);
}
return Array.from(xmlDoc.getElementsByTagName(`${key}`));
};

export const parseSAMLMetadataFromXMLFile = (
xmlString: string,
):
Expand All @@ -20,33 +43,44 @@ export const parseSAMLMetadataFromXMLFile = (
throw new Error('Error parsing XML');
}

const entityDescriptor = xmlDoc.getElementsByTagName(
'md:EntityDescriptor',
)?.[0];
const idpSSODescriptor = xmlDoc.getElementsByTagName(
'md:IDPSSODescriptor',
)?.[0];
const keyDescriptor = xmlDoc.getElementsByTagName('md:KeyDescriptor')[0];
const keyInfo = keyDescriptor?.getElementsByTagName('ds:KeyInfo')[0];
const x509Data = keyInfo?.getElementsByTagName('ds:X509Data')[0];
const x509Certificate = x509Data
?.getElementsByTagName('ds:X509Certificate')?.[0]
.textContent?.trim();

const singleSignOnServices = Array.from(
idpSSODescriptor.getElementsByTagName('md:SingleSignOnService'),
).map((service) => ({
Binding: service.getAttribute('Binding'),
Location: service.getAttribute('Location'),
}));
const entityDescriptor = getByPrefixAndKey(xmlDoc, 'EntityDescriptor');
if (!entityDescriptor) throw new Error('No EntityDescriptor found');

const IDPSSODescriptor = getByPrefixAndKey(xmlDoc, 'IDPSSODescriptor');
if (!IDPSSODescriptor) throw new Error('No IDPSSODescriptor found');

const keyDescriptors = getByPrefixAndKey(IDPSSODescriptor, 'KeyDescriptor');
if (!keyDescriptors) throw new Error('No KeyDescriptor found');

const keyInfo = getByPrefixAndKey(keyDescriptors, 'KeyInfo', 'ds');
if (!keyInfo) throw new Error('No KeyInfo found');

const x509Data = getByPrefixAndKey(keyInfo, 'X509Data', 'ds');
if (!x509Data) throw new Error('No X509Data found');

const x509Certificate = getByPrefixAndKey(
x509Data,
'X509Certificate',
'ds',
)?.textContent?.trim();
if (!x509Certificate) throw new Error('No X509Certificate found');

const singleSignOnServices = getAllByPrefixAndKey(
IDPSSODescriptor,
'SingleSignOnService',
);

const result = {
ssoUrl: singleSignOnServices.find((singleSignOnService) => {
return (
singleSignOnService.Binding ===
'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
);
})?.Location,
ssoUrl: singleSignOnServices
.map((service) => ({
Binding: service.getAttribute('Binding'),
Location: service.getAttribute('Location'),
}))
.find(
(singleSignOnService) =>
singleSignOnService.Binding ===
'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
)?.Location,
certificate: x509Certificate,
entityID: entityDescriptor?.getAttribute('entityID'),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@ import { z } from 'zod';
export const SSOIdentitiesProvidersOIDCParamsSchema = z
.object({
type: z.literal('OIDC'),
clientID: z.string().optional(),
clientSecret: z.string().optional(),
clientID: z.string().nonempty(),
clientSecret: z.string().nonempty(),
})
.required();

export const SSOIdentitiesProvidersSAMLParamsSchema = z
.object({
type: z.literal('SAML'),
id: z.string().optional(),
ssoURL: z.string().url().optional(),
certificate: z.string().optional(),
id: z.string().nonempty(),
ssoURL: z.string().url().nonempty(),
certificate: z.string().nonempty(),
})
.required();

Expand All @@ -27,8 +27,8 @@ export const SSOIdentitiesProvidersParamsSchema = z
.and(
z
.object({
name: z.string().min(1),
issuer: z.string().url().optional(),
name: z.string().nonempty(),
issuer: z.string().url().nonempty(),
})
.required(),
);
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { Query } from 'src/engine/api/rest/core/types/query.type';
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';

@Injectable()
export class CoreQueryBuilderFactory {
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -81,6 +82,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
FeatureFlagModule,
WorkspaceInvitationModule,
EmailVerificationModule,
GuardRedirectModule,
],
controllers: [
GoogleAuthController,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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>,
) {}
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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>,
) {}
Expand Down Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 5783c41

Please sign in to comment.