diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index dbe3ff45a..eaf7622bd 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -648,13 +648,13 @@ export type FullName = { lastName: Scalars['String']['output']; }; -export type GetAuthorizationUrlInput = { +export type GetAuthorizationUrlForSsoInput = { identityProviderId: Scalars['String']['input']; workspaceInviteHash?: InputMaybe; }; -export type GetAuthorizationUrlOutput = { - __typename?: 'GetAuthorizationUrlOutput'; +export type GetAuthorizationUrlForSsoOutput = { + __typename?: 'GetAuthorizationUrlForSSOOutput'; authorizationURL: Scalars['String']['output']; id: Scalars['String']['output']; type: Scalars['String']['output']; @@ -840,7 +840,7 @@ export type Mutation = { generateApiKeyToken: ApiKeyToken; generateTransientToken: TransientToken; getAuthTokensFromLoginToken: AuthTokens; - getAuthorizationUrl: GetAuthorizationUrlOutput; + getAuthorizationUrlForSSO: GetAuthorizationUrlForSsoOutput; getLoginTokenFromCredentials: LoginToken; getLoginTokenFromEmailVerificationToken: LoginToken; impersonate: ImpersonateOutput; @@ -1031,8 +1031,8 @@ export type MutationGetAuthTokensFromLoginTokenArgs = { }; -export type MutationGetAuthorizationUrlArgs = { - input: GetAuthorizationUrlInput; +export type MutationGetAuthorizationUrlForSsoArgs = { + input: GetAuthorizationUrlForSsoInput; }; diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index f6e03fe35..2a31968f2 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -573,13 +573,13 @@ export type FullName = { lastName: Scalars['String']; }; -export type GetAuthorizationUrlInput = { +export type GetAuthorizationUrlForSsoInput = { identityProviderId: Scalars['String']; workspaceInviteHash?: InputMaybe; }; -export type GetAuthorizationUrlOutput = { - __typename?: 'GetAuthorizationUrlOutput'; +export type GetAuthorizationUrlForSsoOutput = { + __typename?: 'GetAuthorizationUrlForSSOOutput'; authorizationURL: Scalars['String']; id: Scalars['String']; type: Scalars['String']; @@ -761,7 +761,7 @@ export type Mutation = { generateApiKeyToken: ApiKeyToken; generateTransientToken: TransientToken; getAuthTokensFromLoginToken: AuthTokens; - getAuthorizationUrl: GetAuthorizationUrlOutput; + getAuthorizationUrlForSSO: GetAuthorizationUrlForSsoOutput; getLoginTokenFromCredentials: LoginToken; getLoginTokenFromEmailVerificationToken: LoginToken; impersonate: ImpersonateOutput; @@ -918,8 +918,8 @@ export type MutationGetAuthTokensFromLoginTokenArgs = { }; -export type MutationGetAuthorizationUrlArgs = { - input: GetAuthorizationUrlInput; +export type MutationGetAuthorizationUrlForSsoArgs = { + input: GetAuthorizationUrlForSsoInput; }; @@ -2090,12 +2090,12 @@ export type GetAuthTokensFromLoginTokenMutationVariables = Exact<{ export type GetAuthTokensFromLoginTokenMutation = { __typename?: 'Mutation', getAuthTokensFromLoginToken: { __typename?: 'AuthTokens', tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; -export type GetAuthorizationUrlMutationVariables = Exact<{ - input: GetAuthorizationUrlInput; +export type GetAuthorizationUrlForSsoMutationVariables = Exact<{ + input: GetAuthorizationUrlForSsoInput; }>; -export type GetAuthorizationUrlMutation = { __typename?: 'Mutation', getAuthorizationUrl: { __typename?: 'GetAuthorizationUrlOutput', id: string, type: string, authorizationURL: string } }; +export type GetAuthorizationUrlForSsoMutation = { __typename?: 'Mutation', getAuthorizationUrlForSSO: { __typename?: 'GetAuthorizationUrlForSSOOutput', id: string, type: string, authorizationURL: string } }; export type GetLoginTokenFromCredentialsMutationVariables = Exact<{ email: Scalars['String']; @@ -3072,41 +3072,41 @@ export function useGetAuthTokensFromLoginTokenMutation(baseOptions?: Apollo.Muta export type GetAuthTokensFromLoginTokenMutationHookResult = ReturnType; export type GetAuthTokensFromLoginTokenMutationResult = Apollo.MutationResult; export type GetAuthTokensFromLoginTokenMutationOptions = Apollo.BaseMutationOptions; -export const GetAuthorizationUrlDocument = gql` - mutation GetAuthorizationUrl($input: GetAuthorizationUrlInput!) { - getAuthorizationUrl(input: $input) { +export const GetAuthorizationUrlForSsoDocument = gql` + mutation GetAuthorizationUrlForSSO($input: GetAuthorizationUrlForSSOInput!) { + getAuthorizationUrlForSSO(input: $input) { id type authorizationURL } } `; -export type GetAuthorizationUrlMutationFn = Apollo.MutationFunction; +export type GetAuthorizationUrlForSsoMutationFn = Apollo.MutationFunction; /** - * __useGetAuthorizationUrlMutation__ + * __useGetAuthorizationUrlForSsoMutation__ * - * To run a mutation, you first call `useGetAuthorizationUrlMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useGetAuthorizationUrlMutation` returns a tuple that includes: + * To run a mutation, you first call `useGetAuthorizationUrlForSsoMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useGetAuthorizationUrlForSsoMutation` returns a tuple that includes: * - A mutate function that you can call at any time to execute the mutation * - An object with fields that represent the current status of the mutation's execution * * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; * * @example - * const [getAuthorizationUrlMutation, { data, loading, error }] = useGetAuthorizationUrlMutation({ + * const [getAuthorizationUrlForSsoMutation, { data, loading, error }] = useGetAuthorizationUrlForSsoMutation({ * variables: { * input: // value for 'input' * }, * }); */ -export function useGetAuthorizationUrlMutation(baseOptions?: Apollo.MutationHookOptions) { +export function useGetAuthorizationUrlForSsoMutation(baseOptions?: Apollo.MutationHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useMutation(GetAuthorizationUrlDocument, options); + return Apollo.useMutation(GetAuthorizationUrlForSsoDocument, options); } -export type GetAuthorizationUrlMutationHookResult = ReturnType; -export type GetAuthorizationUrlMutationResult = Apollo.MutationResult; -export type GetAuthorizationUrlMutationOptions = Apollo.BaseMutationOptions; +export type GetAuthorizationUrlForSsoMutationHookResult = ReturnType; +export type GetAuthorizationUrlForSsoMutationResult = Apollo.MutationResult; +export type GetAuthorizationUrlForSsoMutationOptions = Apollo.BaseMutationOptions; export const GetLoginTokenFromCredentialsDocument = gql` mutation GetLoginTokenFromCredentials($email: String!, $password: String!, $captchaToken: String) { getLoginTokenFromCredentials( diff --git a/packages/twenty-front/src/modules/auth/graphql/mutations/getAuthorizationUrl.ts b/packages/twenty-front/src/modules/auth/graphql/mutations/getAuthorizationUrl.ts deleted file mode 100644 index 5492a9fad..000000000 --- a/packages/twenty-front/src/modules/auth/graphql/mutations/getAuthorizationUrl.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { gql } from '@apollo/client'; - -export const GET_AUTHORIZATION_URL = gql` - mutation GetAuthorizationUrl($input: GetAuthorizationUrlInput!) { - getAuthorizationUrl(input: $input) { - id - type - authorizationURL - } - } -`; diff --git a/packages/twenty-front/src/modules/auth/graphql/mutations/getAuthorizationUrlForSSO.ts b/packages/twenty-front/src/modules/auth/graphql/mutations/getAuthorizationUrlForSSO.ts new file mode 100644 index 000000000..55919bbb6 --- /dev/null +++ b/packages/twenty-front/src/modules/auth/graphql/mutations/getAuthorizationUrlForSSO.ts @@ -0,0 +1,11 @@ +import { gql } from '@apollo/client'; + +export const GET_AUTHORIZATION_URL_FOR_SSO = gql` + mutation GetAuthorizationUrlForSSO($input: GetAuthorizationUrlForSSOInput!) { + getAuthorizationUrlForSSO(input: $input) { + id + type + authorizationURL + } + } +`; diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpSSOIdentityProviderSelection.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpSSOIdentityProviderSelection.tsx index 96ec29b6c..5c3b2b704 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpSSOIdentityProviderSelection.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpSSOIdentityProviderSelection.tsx @@ -8,6 +8,7 @@ import { HorizontalSeparator, MainButton } from 'twenty-ui'; import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState'; import { isDefined } from 'twenty-shared'; +import React from 'react'; const StyledContentContainer = styled.div` margin-bottom: ${({ theme }) => theme.spacing(8)}; @@ -24,16 +25,15 @@ export const SignInUpSSOIdentityProviderSelection = () => { {isDefined(workspaceAuthProviders?.sso) && workspaceAuthProviders?.sso.map((idp) => ( - <> + redirectToSSOLoginPage(idp.id)} Icon={guessSSOIdentityProviderIconByUrl(idp.issuer)} fullWidth /> - + ))} diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useSSO.test.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useSSO.test.tsx index c01bcf78c..d3022d7bf 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useSSO.test.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useSSO.test.tsx @@ -1,4 +1,4 @@ -import { GET_AUTHORIZATION_URL } from '@/auth/graphql/mutations/getAuthorizationUrl'; +import { GET_AUTHORIZATION_URL_FOR_SSO } from '@/auth/graphql/mutations/getAuthorizationUrlForSSO'; import { useSSO } from '@/auth/sign-in-up/hooks/useSSO'; import { useRedirect } from '@/domain-manager/hooks/useRedirect'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; @@ -23,7 +23,7 @@ const mockRedirect = jest.fn(); const apolloMocks = [ { request: { - query: GET_AUTHORIZATION_URL, + query: GET_AUTHORIZATION_URL_FOR_SSO, variables: { input: { identityProviderId: 'success-id', @@ -32,13 +32,13 @@ const apolloMocks = [ }, result: { data: { - getAuthorizationUrl: { authorizationURL: 'http://example.com' }, + getAuthorizationUrlForSSO: { authorizationURL: 'http://example.com' }, }, }, }, { request: { - query: GET_AUTHORIZATION_URL, + query: GET_AUTHORIZATION_URL_FOR_SSO, variables: { input: { identityProviderId: 'error-id', diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSSO.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSSO.ts index 412570233..8db029fc9 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSSO.ts +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSSO.ts @@ -1,6 +1,6 @@ /* @license Enterprise */ -import { GET_AUTHORIZATION_URL } from '@/auth/graphql/mutations/getAuthorizationUrl'; +import { GET_AUTHORIZATION_URL_FOR_SSO } from '@/auth/graphql/mutations/getAuthorizationUrlForSSO'; import { useRedirect } from '@/domain-manager/hooks/useRedirect'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; @@ -17,7 +17,7 @@ export const useSSO = () => { let authorizationUrlForSSOResult; try { authorizationUrlForSSOResult = await apolloClient.mutate({ - mutation: GET_AUTHORIZATION_URL, + mutation: GET_AUTHORIZATION_URL_FOR_SSO, variables: { input: { identityProviderId, @@ -32,7 +32,8 @@ export const useSSO = () => { } redirect( - authorizationUrlForSSOResult.data?.getAuthorizationUrl.authorizationURL, + authorizationUrlForSSOResult.data?.getAuthorizationUrlForSSO + .authorizationURL, ); }; diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.spec.ts index 1d4300920..c6f46d451 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.spec.ts @@ -11,6 +11,7 @@ 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'; import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; +import { SSOService } from 'src/engine/core-modules/sso/services/sso.service'; import { AuthResolver } from './auth.resolver'; @@ -95,6 +96,10 @@ describe('AuthResolver', () => { provide: FeatureFlagService, useValue: {}, }, + { + provide: SSOService, + useValue: {}, + }, // { // provide: OAuthService, // useValue: {}, diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts index 76edf27bd..081ee2ffa 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts @@ -4,6 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { SettingsFeatures, SOURCE_LOCALE } from 'twenty-shared'; import { Repository } from 'typeorm'; +import omit from 'lodash.omit'; import { ApiKeyTokenInput } from 'src/engine/core-modules/auth/dto/api-key-token.input'; import { AppTokenInput } from 'src/engine/core-modules/auth/dto/app-token.input'; @@ -47,6 +48,9 @@ import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; +import { SSOService } from 'src/engine/core-modules/sso/services/sso.service'; +import { GetAuthorizationUrlForSSOOutput } from 'src/engine/core-modules/auth/dto/get-authorization-url-for-sso.output'; +import { GetAuthorizationUrlForSSOInput } from 'src/engine/core-modules/auth/dto/get-authorization-url-for-sso.input'; import { GetAuthTokensFromLoginTokenInput } from './dto/get-auth-tokens-from-login-token.input'; import { GetLoginTokenFromCredentialsInput } from './dto/get-login-token-from-credentials.input'; @@ -77,6 +81,7 @@ export class AuthResolver { private domainManagerService: DomainManagerService, private userWorkspaceService: UserWorkspaceService, private emailVerificationTokenService: EmailVerificationTokenService, + private sSOService: SSOService, ) {} @UseGuards(CaptchaGuard) @@ -87,6 +92,16 @@ export class AuthResolver { return await this.authService.checkUserExists(checkUserExistsInput.email); } + @Mutation(() => GetAuthorizationUrlForSSOOutput) + async getAuthorizationUrlForSSO( + @Args('input') params: GetAuthorizationUrlForSSOInput, + ) { + return await this.sSOService.getAuthorizationUrlForSSO( + params.identityProviderId, + omit(params, ['identityProviderId']), + ); + } + @Query(() => WorkspaceInviteHashValid) async checkWorkspaceInviteHashIsValid( @Args() workspaceInviteHashValidInput: WorkspaceInviteHashValidInput, diff --git a/packages/twenty-server/src/engine/core-modules/sso/dtos/get-authorization-url.input.ts b/packages/twenty-server/src/engine/core-modules/auth/dto/get-authorization-url-for-sso.input.ts similarity index 87% rename from packages/twenty-server/src/engine/core-modules/sso/dtos/get-authorization-url.input.ts rename to packages/twenty-server/src/engine/core-modules/auth/dto/get-authorization-url-for-sso.input.ts index e627a4a7e..074e435f5 100644 --- a/packages/twenty-server/src/engine/core-modules/sso/dtos/get-authorization-url.input.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/dto/get-authorization-url-for-sso.input.ts @@ -5,7 +5,7 @@ import { Field, InputType } from '@nestjs/graphql'; import { IsOptional, IsString } from 'class-validator'; @InputType() -export class GetAuthorizationUrlInput { +export class GetAuthorizationUrlForSSOInput { @Field(() => String) @IsString() identityProviderId: string; diff --git a/packages/twenty-server/src/engine/core-modules/sso/dtos/get-authorization-url.output.ts b/packages/twenty-server/src/engine/core-modules/auth/dto/get-authorization-url-for-sso.output.ts similarity index 87% rename from packages/twenty-server/src/engine/core-modules/sso/dtos/get-authorization-url.output.ts rename to packages/twenty-server/src/engine/core-modules/auth/dto/get-authorization-url-for-sso.output.ts index d0c78e37d..1727a9f00 100644 --- a/packages/twenty-server/src/engine/core-modules/sso/dtos/get-authorization-url.output.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/dto/get-authorization-url-for-sso.output.ts @@ -5,7 +5,7 @@ import { Field, ObjectType } from '@nestjs/graphql'; import { SSOConfiguration } from 'src/engine/core-modules/sso/types/SSOConfigurations.type'; @ObjectType() -export class GetAuthorizationUrlOutput { +export class GetAuthorizationUrlForSSOOutput { @Field(() => String) authorizationURL: string; diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/oidc-auth.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/oidc-auth.guard.ts index 495fb8b75..da594c10b 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/oidc-auth.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/oidc-auth.guard.ts @@ -28,7 +28,9 @@ export class OIDCAuthGuard extends AuthGuard('openidconnect') { identityProviderId: string; } { if (request.params.identityProviderId) { - return request.params.identityProviderId; + return { + identityProviderId: request.params.identityProviderId, + }; } if ( @@ -57,6 +59,13 @@ export class OIDCAuthGuard extends AuthGuard('openidconnect') { try { const state = this.getStateByRequest(request); + if (!state.identityProviderId) { + throw new AuthException( + 'identityProviderId missing', + AuthExceptionCode.INVALID_DATA, + ); + } + identityProvider = await this.sSOService.findSSOIdentityProviderById( state.identityProviderId, ); @@ -67,7 +76,6 @@ export class OIDCAuthGuard extends AuthGuard('openidconnect') { AuthExceptionCode.INVALID_DATA, ); } - const issuer = await Issuer.discover(identityProvider.issuer); new OIDCAuthStrategy( diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/oidc-auth.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/oidc-auth.spec.ts new file mode 100644 index 000000000..02139a05f --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/oidc-auth.spec.ts @@ -0,0 +1,130 @@ +import { ExecutionContext } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthGuard } from '@nestjs/passport'; + +import { Issuer } from 'openid-client'; + +import { SSOService } from 'src/engine/core-modules/sso/services/sso.service'; +import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; +import { OIDCAuthGuard } from 'src/engine/core-modules/auth/guards/oidc-auth.guard'; +import { SSOConfiguration } from 'src/engine/core-modules/sso/types/SSOConfigurations.type'; +import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; + +const createMockExecutionContext = (mockedRequest: any): ExecutionContext => { + return { + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue(mockedRequest), + }), + } as unknown as ExecutionContext; +}; + +const createMockedRequest = (params = {}, query = {}) => ({ + params, + query, +}); + +jest + .spyOn(AuthGuard('openidconnect').prototype, 'canActivate') + .mockResolvedValue(true); + +jest.mock('openid-client', () => ({ + Strategy: jest.fn(), + Issuer: { + discover: jest.fn().mockResolvedValue({} as Issuer), + }, +})); + +describe('OIDCAuthGuard', () => { + let guard: OIDCAuthGuard; + let sSOService: SSOService; + let guardRedirectService: GuardRedirectService; + let mockExecutionContext: ExecutionContext; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + OIDCAuthGuard, + { + provide: SSOService, + useValue: { + findSSOIdentityProviderById: jest.fn(), + getOIDCClient: jest.fn(), + }, + }, + { + provide: GuardRedirectService, + useValue: { + dispatchErrorFromGuard: jest.fn(), + getSubdomainAndCustomDomainFromWorkspace: jest.fn(), + }, + }, + ], + }).compile(); + + guard = module.get(OIDCAuthGuard); + sSOService = module.get(SSOService); + guardRedirectService = + module.get(GuardRedirectService); + + mockExecutionContext = { + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn(), + }), + } as unknown as ExecutionContext; + }); + + it('should activate when identity provider exists and is valid', async () => { + const mockedRequest = createMockedRequest({ + identityProviderId: 'test-id', + }); + + mockExecutionContext = createMockExecutionContext(mockedRequest); + + jest.spyOn(sSOService, 'findSSOIdentityProviderById').mockResolvedValue({ + id: 'test-id', + issuer: 'https://issuer.example.com', + workspace: {}, + } as SSOConfiguration & WorkspaceSSOIdentityProvider); + + const result = await guard.canActivate(mockExecutionContext); + + expect(result).toBe(true); + expect(guardRedirectService.dispatchErrorFromGuard).not.toHaveBeenCalled(); + expect(sSOService.findSSOIdentityProviderById).toHaveBeenCalledWith( + 'test-id', + ); + }); + + it('should throw error when identity provider is not found', async () => { + const mockedRequest = createMockedRequest({ + identityProviderId: 'non-existent-id', + }); + + mockExecutionContext = createMockExecutionContext(mockedRequest); + + jest + .spyOn(sSOService, 'findSSOIdentityProviderById') + .mockResolvedValue(null); + + await expect(guard.canActivate(mockExecutionContext)).resolves.toBe(false); + expect(sSOService.findSSOIdentityProviderById).toHaveBeenCalledWith( + 'non-existent-id', + ); + expect(guardRedirectService.dispatchErrorFromGuard).toHaveBeenCalled(); + }); + + it('should handle invalid OIDC identity provider params in request', async () => { + const mockedRequest = createMockedRequest({ + identityProviderId: undefined, + }); + + mockExecutionContext = createMockExecutionContext(mockedRequest); + + jest + .spyOn(sSOService, 'findSSOIdentityProviderById') + .mockResolvedValue(null); + + await expect(guard.canActivate(mockExecutionContext)).resolves.toBe(false); + expect(guardRedirectService.dispatchErrorFromGuard).toHaveBeenCalled(); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts b/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts index 881824581..d3d6dd503 100644 --- a/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts +++ b/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts @@ -198,7 +198,7 @@ export class SSOService { }); } - async getAuthorizationUrl( + async getAuthorizationUrlForSSO( identityProviderId: string, searchParams: Record, ) { diff --git a/packages/twenty-server/src/engine/core-modules/sso/sso.resolver.ts b/packages/twenty-server/src/engine/core-modules/sso/sso.resolver.ts index 34d69e379..d967d3c7a 100644 --- a/packages/twenty-server/src/engine/core-modules/sso/sso.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/sso/sso.resolver.ts @@ -3,7 +3,6 @@ import { UseFilters, UseGuards } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; -import omit from 'lodash.omit'; import { SettingsFeatures } from 'twenty-shared'; import { EnterpriseFeaturesEnabledGuard } from 'src/engine/core-modules/auth/guards/enterprise-features-enabled.guard'; @@ -12,8 +11,6 @@ import { DeleteSsoOutput } from 'src/engine/core-modules/sso/dtos/delete-sso.out import { EditSsoInput } from 'src/engine/core-modules/sso/dtos/edit-sso.input'; import { EditSsoOutput } from 'src/engine/core-modules/sso/dtos/edit-sso.output'; import { FindAvailableSSOIDPOutput } from 'src/engine/core-modules/sso/dtos/find-available-SSO-IDP.output'; -import { GetAuthorizationUrlInput } from 'src/engine/core-modules/sso/dtos/get-authorization-url.input'; -import { GetAuthorizationUrlOutput } from 'src/engine/core-modules/sso/dtos/get-authorization-url.output'; import { SetupOIDCSsoInput, SetupSAMLSsoInput, @@ -53,14 +50,6 @@ export class SSOResolver { return this.sSOService.listSSOIdentityProvidersByWorkspaceId(workspaceId); } - @Mutation(() => GetAuthorizationUrlOutput) - async getAuthorizationUrl(@Args('input') params: GetAuthorizationUrlInput) { - return await this.sSOService.getAuthorizationUrl( - params.identityProviderId, - omit(params, ['identityProviderId']), - ); - } - @UseGuards(WorkspaceAuthGuard, EnterpriseFeaturesEnabledGuard) @Mutation(() => SetupSsoOutput) async createSAMLIdentityProvider( diff --git a/packages/twenty-server/src/engine/middlewares/constants/excluded-middleware-operations.constant.ts b/packages/twenty-server/src/engine/middlewares/constants/excluded-middleware-operations.constant.ts index 1604611d9..a34c71b10 100644 --- a/packages/twenty-server/src/engine/middlewares/constants/excluded-middleware-operations.constant.ts +++ b/packages/twenty-server/src/engine/middlewares/constants/excluded-middleware-operations.constant.ts @@ -14,6 +14,6 @@ export const EXCLUDED_MIDDLEWARE_OPERATIONS = [ 'UpdatePasswordViaResetToken', 'IntrospectionQuery', 'ExchangeAuthorizationCode', - 'GetAuthorizationUrl', + 'GetAuthorizationUrlForSSO', 'GetPublicWorkspaceDataByDomain', ] as const;