Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 13 commits into from
Jan 24, 2025
Merged
6 changes: 6 additions & 0 deletions packages/twenty-front/src/modules/auth/hooks/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,12 @@ export const useAuth = () => {
url.searchParams.set('workspaceId', workspacePublicData.id);
}

if (isDefined(workspacePublicData)) {
url.searchParams.set('workspaceId', workspacePublicData.id);
AMoreaux marked this conversation as resolved.
Show resolved Hide resolved
}

url.searchParams.set('origin_url', window.origin);
AMoreaux marked this conversation as resolved.
Show resolved Hide resolved

return url.toString();
},
[workspacePublicData],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ export const useSSO = () => {
);
}

const url = new URL(
authorizationUrlForSSOResult.data?.getAuthorizationUrl.authorizationURL,
);

url.searchParams.set('origin_url', window.origin);
AMoreaux marked this conversation as resolved.
Show resolved Hide resolved

window.location.href =
authorizationUrlForSSOResult.data?.getAuthorizationUrl.authorizationURL;
return;
Expand Down
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 prefixGet = (
AMoreaux marked this conversation as resolved.
Show resolved Hide resolved
xmlDoc: Document | Element,
key: string,
prefix = 'md',
): Element | undefined => {
return (
xmlDoc.getElementsByTagName(`${prefix}:${key}`)?.[0] ??
xmlDoc.getElementsByTagName(`${key}`)?.[0]
);
};

const prefixGetAll = (
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 = prefixGet(xmlDoc, 'EntityDescriptor');
if (!entityDescriptor) throw new Error('No EntityDescriptor found');

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

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

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

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

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

const singleSignOnServices = prefixGetAll(
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 { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
import { SocialSsoService } from 'src/engine/core-modules/auth/services/social-sso.service';
import { GuardManagerModule } from 'src/engine/core-modules/guard-manager/guard-manager.module';

import { AuthResolver } from './auth.resolver';

Expand Down Expand Up @@ -81,6 +82,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
FeatureFlagModule,
WorkspaceInvitationModule,
EmailVerificationModule,
GuardManagerModule,
],
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,7 +22,7 @@ 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';

@Controller('auth/google')
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,7 +18,7 @@ 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';

@Controller('auth/microsoft')
Expand Down
Loading
Loading