refactor(auth/sso): rename GetAuthorizationUrl for clarity (#10173)
- Rename `GetAuthorizationUrl` to `GetAuthorizationUrlForSSO` - Move `GetAuthorizationUrlForSSO` from `sso.resolver.ts` to `auth.resolver.ts` to avoid the permission guard and let users use an SSO provider. - Fix an issue in OIDC guard that breaks the connection if you have multiple SSO providers + add tests for OIDC guard.
This commit is contained in:
@ -648,13 +648,13 @@ export type FullName = {
|
|||||||
lastName: Scalars['String']['output'];
|
lastName: Scalars['String']['output'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetAuthorizationUrlInput = {
|
export type GetAuthorizationUrlForSsoInput = {
|
||||||
identityProviderId: Scalars['String']['input'];
|
identityProviderId: Scalars['String']['input'];
|
||||||
workspaceInviteHash?: InputMaybe<Scalars['String']['input']>;
|
workspaceInviteHash?: InputMaybe<Scalars['String']['input']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetAuthorizationUrlOutput = {
|
export type GetAuthorizationUrlForSsoOutput = {
|
||||||
__typename?: 'GetAuthorizationUrlOutput';
|
__typename?: 'GetAuthorizationUrlForSSOOutput';
|
||||||
authorizationURL: Scalars['String']['output'];
|
authorizationURL: Scalars['String']['output'];
|
||||||
id: Scalars['String']['output'];
|
id: Scalars['String']['output'];
|
||||||
type: Scalars['String']['output'];
|
type: Scalars['String']['output'];
|
||||||
@ -840,7 +840,7 @@ export type Mutation = {
|
|||||||
generateApiKeyToken: ApiKeyToken;
|
generateApiKeyToken: ApiKeyToken;
|
||||||
generateTransientToken: TransientToken;
|
generateTransientToken: TransientToken;
|
||||||
getAuthTokensFromLoginToken: AuthTokens;
|
getAuthTokensFromLoginToken: AuthTokens;
|
||||||
getAuthorizationUrl: GetAuthorizationUrlOutput;
|
getAuthorizationUrlForSSO: GetAuthorizationUrlForSsoOutput;
|
||||||
getLoginTokenFromCredentials: LoginToken;
|
getLoginTokenFromCredentials: LoginToken;
|
||||||
getLoginTokenFromEmailVerificationToken: LoginToken;
|
getLoginTokenFromEmailVerificationToken: LoginToken;
|
||||||
impersonate: ImpersonateOutput;
|
impersonate: ImpersonateOutput;
|
||||||
@ -1031,8 +1031,8 @@ export type MutationGetAuthTokensFromLoginTokenArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationGetAuthorizationUrlArgs = {
|
export type MutationGetAuthorizationUrlForSsoArgs = {
|
||||||
input: GetAuthorizationUrlInput;
|
input: GetAuthorizationUrlForSsoInput;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -573,13 +573,13 @@ export type FullName = {
|
|||||||
lastName: Scalars['String'];
|
lastName: Scalars['String'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetAuthorizationUrlInput = {
|
export type GetAuthorizationUrlForSsoInput = {
|
||||||
identityProviderId: Scalars['String'];
|
identityProviderId: Scalars['String'];
|
||||||
workspaceInviteHash?: InputMaybe<Scalars['String']>;
|
workspaceInviteHash?: InputMaybe<Scalars['String']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetAuthorizationUrlOutput = {
|
export type GetAuthorizationUrlForSsoOutput = {
|
||||||
__typename?: 'GetAuthorizationUrlOutput';
|
__typename?: 'GetAuthorizationUrlForSSOOutput';
|
||||||
authorizationURL: Scalars['String'];
|
authorizationURL: Scalars['String'];
|
||||||
id: Scalars['String'];
|
id: Scalars['String'];
|
||||||
type: Scalars['String'];
|
type: Scalars['String'];
|
||||||
@ -761,7 +761,7 @@ export type Mutation = {
|
|||||||
generateApiKeyToken: ApiKeyToken;
|
generateApiKeyToken: ApiKeyToken;
|
||||||
generateTransientToken: TransientToken;
|
generateTransientToken: TransientToken;
|
||||||
getAuthTokensFromLoginToken: AuthTokens;
|
getAuthTokensFromLoginToken: AuthTokens;
|
||||||
getAuthorizationUrl: GetAuthorizationUrlOutput;
|
getAuthorizationUrlForSSO: GetAuthorizationUrlForSsoOutput;
|
||||||
getLoginTokenFromCredentials: LoginToken;
|
getLoginTokenFromCredentials: LoginToken;
|
||||||
getLoginTokenFromEmailVerificationToken: LoginToken;
|
getLoginTokenFromEmailVerificationToken: LoginToken;
|
||||||
impersonate: ImpersonateOutput;
|
impersonate: ImpersonateOutput;
|
||||||
@ -918,8 +918,8 @@ export type MutationGetAuthTokensFromLoginTokenArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationGetAuthorizationUrlArgs = {
|
export type MutationGetAuthorizationUrlForSsoArgs = {
|
||||||
input: GetAuthorizationUrlInput;
|
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 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<{
|
export type GetAuthorizationUrlForSsoMutationVariables = Exact<{
|
||||||
input: GetAuthorizationUrlInput;
|
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<{
|
export type GetLoginTokenFromCredentialsMutationVariables = Exact<{
|
||||||
email: Scalars['String'];
|
email: Scalars['String'];
|
||||||
@ -3072,41 +3072,41 @@ export function useGetAuthTokensFromLoginTokenMutation(baseOptions?: Apollo.Muta
|
|||||||
export type GetAuthTokensFromLoginTokenMutationHookResult = ReturnType<typeof useGetAuthTokensFromLoginTokenMutation>;
|
export type GetAuthTokensFromLoginTokenMutationHookResult = ReturnType<typeof useGetAuthTokensFromLoginTokenMutation>;
|
||||||
export type GetAuthTokensFromLoginTokenMutationResult = Apollo.MutationResult<GetAuthTokensFromLoginTokenMutation>;
|
export type GetAuthTokensFromLoginTokenMutationResult = Apollo.MutationResult<GetAuthTokensFromLoginTokenMutation>;
|
||||||
export type GetAuthTokensFromLoginTokenMutationOptions = Apollo.BaseMutationOptions<GetAuthTokensFromLoginTokenMutation, GetAuthTokensFromLoginTokenMutationVariables>;
|
export type GetAuthTokensFromLoginTokenMutationOptions = Apollo.BaseMutationOptions<GetAuthTokensFromLoginTokenMutation, GetAuthTokensFromLoginTokenMutationVariables>;
|
||||||
export const GetAuthorizationUrlDocument = gql`
|
export const GetAuthorizationUrlForSsoDocument = gql`
|
||||||
mutation GetAuthorizationUrl($input: GetAuthorizationUrlInput!) {
|
mutation GetAuthorizationUrlForSSO($input: GetAuthorizationUrlForSSOInput!) {
|
||||||
getAuthorizationUrl(input: $input) {
|
getAuthorizationUrlForSSO(input: $input) {
|
||||||
id
|
id
|
||||||
type
|
type
|
||||||
authorizationURL
|
authorizationURL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
export type GetAuthorizationUrlMutationFn = Apollo.MutationFunction<GetAuthorizationUrlMutation, GetAuthorizationUrlMutationVariables>;
|
export type GetAuthorizationUrlForSsoMutationFn = Apollo.MutationFunction<GetAuthorizationUrlForSsoMutation, GetAuthorizationUrlForSsoMutationVariables>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* __useGetAuthorizationUrlMutation__
|
* __useGetAuthorizationUrlForSsoMutation__
|
||||||
*
|
*
|
||||||
* To run a mutation, you first call `useGetAuthorizationUrlMutation` within a React component and pass it any options that fit your needs.
|
* 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, `useGetAuthorizationUrlMutation` returns a tuple that includes:
|
* When your component renders, `useGetAuthorizationUrlForSsoMutation` returns a tuple that includes:
|
||||||
* - A mutate function that you can call at any time to execute the mutation
|
* - 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
|
* - 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;
|
* @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
|
* @example
|
||||||
* const [getAuthorizationUrlMutation, { data, loading, error }] = useGetAuthorizationUrlMutation({
|
* const [getAuthorizationUrlForSsoMutation, { data, loading, error }] = useGetAuthorizationUrlForSsoMutation({
|
||||||
* variables: {
|
* variables: {
|
||||||
* input: // value for 'input'
|
* input: // value for 'input'
|
||||||
* },
|
* },
|
||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
export function useGetAuthorizationUrlMutation(baseOptions?: Apollo.MutationHookOptions<GetAuthorizationUrlMutation, GetAuthorizationUrlMutationVariables>) {
|
export function useGetAuthorizationUrlForSsoMutation(baseOptions?: Apollo.MutationHookOptions<GetAuthorizationUrlForSsoMutation, GetAuthorizationUrlForSsoMutationVariables>) {
|
||||||
const options = {...defaultOptions, ...baseOptions}
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
return Apollo.useMutation<GetAuthorizationUrlMutation, GetAuthorizationUrlMutationVariables>(GetAuthorizationUrlDocument, options);
|
return Apollo.useMutation<GetAuthorizationUrlForSsoMutation, GetAuthorizationUrlForSsoMutationVariables>(GetAuthorizationUrlForSsoDocument, options);
|
||||||
}
|
}
|
||||||
export type GetAuthorizationUrlMutationHookResult = ReturnType<typeof useGetAuthorizationUrlMutation>;
|
export type GetAuthorizationUrlForSsoMutationHookResult = ReturnType<typeof useGetAuthorizationUrlForSsoMutation>;
|
||||||
export type GetAuthorizationUrlMutationResult = Apollo.MutationResult<GetAuthorizationUrlMutation>;
|
export type GetAuthorizationUrlForSsoMutationResult = Apollo.MutationResult<GetAuthorizationUrlForSsoMutation>;
|
||||||
export type GetAuthorizationUrlMutationOptions = Apollo.BaseMutationOptions<GetAuthorizationUrlMutation, GetAuthorizationUrlMutationVariables>;
|
export type GetAuthorizationUrlForSsoMutationOptions = Apollo.BaseMutationOptions<GetAuthorizationUrlForSsoMutation, GetAuthorizationUrlForSsoMutationVariables>;
|
||||||
export const GetLoginTokenFromCredentialsDocument = gql`
|
export const GetLoginTokenFromCredentialsDocument = gql`
|
||||||
mutation GetLoginTokenFromCredentials($email: String!, $password: String!, $captchaToken: String) {
|
mutation GetLoginTokenFromCredentials($email: String!, $password: String!, $captchaToken: String) {
|
||||||
getLoginTokenFromCredentials(
|
getLoginTokenFromCredentials(
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@ -8,6 +8,7 @@ import { HorizontalSeparator, MainButton } from 'twenty-ui';
|
|||||||
|
|
||||||
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
|
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
|
||||||
import { isDefined } from 'twenty-shared';
|
import { isDefined } from 'twenty-shared';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
const StyledContentContainer = styled.div`
|
const StyledContentContainer = styled.div`
|
||||||
margin-bottom: ${({ theme }) => theme.spacing(8)};
|
margin-bottom: ${({ theme }) => theme.spacing(8)};
|
||||||
@ -24,16 +25,15 @@ export const SignInUpSSOIdentityProviderSelection = () => {
|
|||||||
<StyledContentContainer>
|
<StyledContentContainer>
|
||||||
{isDefined(workspaceAuthProviders?.sso) &&
|
{isDefined(workspaceAuthProviders?.sso) &&
|
||||||
workspaceAuthProviders?.sso.map((idp) => (
|
workspaceAuthProviders?.sso.map((idp) => (
|
||||||
<>
|
<React.Fragment key={idp.id}>
|
||||||
<MainButton
|
<MainButton
|
||||||
key={idp.id}
|
|
||||||
title={idp.name}
|
title={idp.name}
|
||||||
onClick={() => redirectToSSOLoginPage(idp.id)}
|
onClick={() => redirectToSSOLoginPage(idp.id)}
|
||||||
Icon={guessSSOIdentityProviderIconByUrl(idp.issuer)}
|
Icon={guessSSOIdentityProviderIconByUrl(idp.issuer)}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<HorizontalSeparator visible={false} />
|
<HorizontalSeparator visible={false} />
|
||||||
</>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</StyledContentContainer>
|
</StyledContentContainer>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -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 { useSSO } from '@/auth/sign-in-up/hooks/useSSO';
|
||||||
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
|
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
@ -23,7 +23,7 @@ const mockRedirect = jest.fn();
|
|||||||
const apolloMocks = [
|
const apolloMocks = [
|
||||||
{
|
{
|
||||||
request: {
|
request: {
|
||||||
query: GET_AUTHORIZATION_URL,
|
query: GET_AUTHORIZATION_URL_FOR_SSO,
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
identityProviderId: 'success-id',
|
identityProviderId: 'success-id',
|
||||||
@ -32,13 +32,13 @@ const apolloMocks = [
|
|||||||
},
|
},
|
||||||
result: {
|
result: {
|
||||||
data: {
|
data: {
|
||||||
getAuthorizationUrl: { authorizationURL: 'http://example.com' },
|
getAuthorizationUrlForSSO: { authorizationURL: 'http://example.com' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
request: {
|
request: {
|
||||||
query: GET_AUTHORIZATION_URL,
|
query: GET_AUTHORIZATION_URL_FOR_SSO,
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
identityProviderId: 'error-id',
|
identityProviderId: 'error-id',
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/* @license Enterprise */
|
/* @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 { useRedirect } from '@/domain-manager/hooks/useRedirect';
|
||||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
@ -17,7 +17,7 @@ export const useSSO = () => {
|
|||||||
let authorizationUrlForSSOResult;
|
let authorizationUrlForSSOResult;
|
||||||
try {
|
try {
|
||||||
authorizationUrlForSSOResult = await apolloClient.mutate({
|
authorizationUrlForSSOResult = await apolloClient.mutate({
|
||||||
mutation: GET_AUTHORIZATION_URL,
|
mutation: GET_AUTHORIZATION_URL_FOR_SSO,
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
identityProviderId,
|
identityProviderId,
|
||||||
@ -32,7 +32,8 @@ export const useSSO = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
redirect(
|
redirect(
|
||||||
authorizationUrlForSSOResult.data?.getAuthorizationUrl.authorizationURL,
|
authorizationUrlForSSOResult.data?.getAuthorizationUrlForSSO
|
||||||
|
.authorizationURL,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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 { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
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';
|
import { AuthResolver } from './auth.resolver';
|
||||||
|
|
||||||
@ -95,6 +96,10 @@ describe('AuthResolver', () => {
|
|||||||
provide: FeatureFlagService,
|
provide: FeatureFlagService,
|
||||||
useValue: {},
|
useValue: {},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: SSOService,
|
||||||
|
useValue: {},
|
||||||
|
},
|
||||||
// {
|
// {
|
||||||
// provide: OAuthService,
|
// provide: OAuthService,
|
||||||
// useValue: {},
|
// useValue: {},
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
|
|
||||||
import { SettingsFeatures, SOURCE_LOCALE } from 'twenty-shared';
|
import { SettingsFeatures, SOURCE_LOCALE } from 'twenty-shared';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
import omit from 'lodash.omit';
|
||||||
|
|
||||||
import { ApiKeyTokenInput } from 'src/engine/core-modules/auth/dto/api-key-token.input';
|
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';
|
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 { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
||||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-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 { 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 { GetAuthTokensFromLoginTokenInput } from './dto/get-auth-tokens-from-login-token.input';
|
||||||
import { GetLoginTokenFromCredentialsInput } from './dto/get-login-token-from-credentials.input';
|
import { GetLoginTokenFromCredentialsInput } from './dto/get-login-token-from-credentials.input';
|
||||||
@ -77,6 +81,7 @@ export class AuthResolver {
|
|||||||
private domainManagerService: DomainManagerService,
|
private domainManagerService: DomainManagerService,
|
||||||
private userWorkspaceService: UserWorkspaceService,
|
private userWorkspaceService: UserWorkspaceService,
|
||||||
private emailVerificationTokenService: EmailVerificationTokenService,
|
private emailVerificationTokenService: EmailVerificationTokenService,
|
||||||
|
private sSOService: SSOService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@UseGuards(CaptchaGuard)
|
@UseGuards(CaptchaGuard)
|
||||||
@ -87,6 +92,16 @@ export class AuthResolver {
|
|||||||
return await this.authService.checkUserExists(checkUserExistsInput.email);
|
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)
|
@Query(() => WorkspaceInviteHashValid)
|
||||||
async checkWorkspaceInviteHashIsValid(
|
async checkWorkspaceInviteHashIsValid(
|
||||||
@Args() workspaceInviteHashValidInput: WorkspaceInviteHashValidInput,
|
@Args() workspaceInviteHashValidInput: WorkspaceInviteHashValidInput,
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { Field, InputType } from '@nestjs/graphql';
|
|||||||
import { IsOptional, IsString } from 'class-validator';
|
import { IsOptional, IsString } from 'class-validator';
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
export class GetAuthorizationUrlInput {
|
export class GetAuthorizationUrlForSSOInput {
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
@IsString()
|
@IsString()
|
||||||
identityProviderId: string;
|
identityProviderId: string;
|
||||||
@ -5,7 +5,7 @@ import { Field, ObjectType } from '@nestjs/graphql';
|
|||||||
import { SSOConfiguration } from 'src/engine/core-modules/sso/types/SSOConfigurations.type';
|
import { SSOConfiguration } from 'src/engine/core-modules/sso/types/SSOConfigurations.type';
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class GetAuthorizationUrlOutput {
|
export class GetAuthorizationUrlForSSOOutput {
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
authorizationURL: string;
|
authorizationURL: string;
|
||||||
|
|
||||||
@ -28,7 +28,9 @@ export class OIDCAuthGuard extends AuthGuard('openidconnect') {
|
|||||||
identityProviderId: string;
|
identityProviderId: string;
|
||||||
} {
|
} {
|
||||||
if (request.params.identityProviderId) {
|
if (request.params.identityProviderId) {
|
||||||
return request.params.identityProviderId;
|
return {
|
||||||
|
identityProviderId: request.params.identityProviderId,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -57,6 +59,13 @@ export class OIDCAuthGuard extends AuthGuard('openidconnect') {
|
|||||||
try {
|
try {
|
||||||
const state = this.getStateByRequest(request);
|
const state = this.getStateByRequest(request);
|
||||||
|
|
||||||
|
if (!state.identityProviderId) {
|
||||||
|
throw new AuthException(
|
||||||
|
'identityProviderId missing',
|
||||||
|
AuthExceptionCode.INVALID_DATA,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
identityProvider = await this.sSOService.findSSOIdentityProviderById(
|
identityProvider = await this.sSOService.findSSOIdentityProviderById(
|
||||||
state.identityProviderId,
|
state.identityProviderId,
|
||||||
);
|
);
|
||||||
@ -67,7 +76,6 @@ export class OIDCAuthGuard extends AuthGuard('openidconnect') {
|
|||||||
AuthExceptionCode.INVALID_DATA,
|
AuthExceptionCode.INVALID_DATA,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const issuer = await Issuer.discover(identityProvider.issuer);
|
const issuer = await Issuer.discover(identityProvider.issuer);
|
||||||
|
|
||||||
new OIDCAuthStrategy(
|
new OIDCAuthStrategy(
|
||||||
|
|||||||
@ -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>(OIDCAuthGuard);
|
||||||
|
sSOService = module.get<SSOService>(SSOService);
|
||||||
|
guardRedirectService =
|
||||||
|
module.get<GuardRedirectService>(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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -198,7 +198,7 @@ export class SSOService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAuthorizationUrl(
|
async getAuthorizationUrlForSSO(
|
||||||
identityProviderId: string,
|
identityProviderId: string,
|
||||||
searchParams: Record<string, string | boolean>,
|
searchParams: Record<string, string | boolean>,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
import { UseFilters, UseGuards } from '@nestjs/common';
|
import { UseFilters, UseGuards } from '@nestjs/common';
|
||||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||||
|
|
||||||
import omit from 'lodash.omit';
|
|
||||||
import { SettingsFeatures } from 'twenty-shared';
|
import { SettingsFeatures } from 'twenty-shared';
|
||||||
|
|
||||||
import { EnterpriseFeaturesEnabledGuard } from 'src/engine/core-modules/auth/guards/enterprise-features-enabled.guard';
|
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 { EditSsoInput } from 'src/engine/core-modules/sso/dtos/edit-sso.input';
|
||||||
import { EditSsoOutput } from 'src/engine/core-modules/sso/dtos/edit-sso.output';
|
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 { 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 {
|
import {
|
||||||
SetupOIDCSsoInput,
|
SetupOIDCSsoInput,
|
||||||
SetupSAMLSsoInput,
|
SetupSAMLSsoInput,
|
||||||
@ -53,14 +50,6 @@ export class SSOResolver {
|
|||||||
return this.sSOService.listSSOIdentityProvidersByWorkspaceId(workspaceId);
|
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)
|
@UseGuards(WorkspaceAuthGuard, EnterpriseFeaturesEnabledGuard)
|
||||||
@Mutation(() => SetupSsoOutput)
|
@Mutation(() => SetupSsoOutput)
|
||||||
async createSAMLIdentityProvider(
|
async createSAMLIdentityProvider(
|
||||||
|
|||||||
@ -14,6 +14,6 @@ export const EXCLUDED_MIDDLEWARE_OPERATIONS = [
|
|||||||
'UpdatePasswordViaResetToken',
|
'UpdatePasswordViaResetToken',
|
||||||
'IntrospectionQuery',
|
'IntrospectionQuery',
|
||||||
'ExchangeAuthorizationCode',
|
'ExchangeAuthorizationCode',
|
||||||
'GetAuthorizationUrl',
|
'GetAuthorizationUrlForSSO',
|
||||||
'GetPublicWorkspaceDataByDomain',
|
'GetPublicWorkspaceDataByDomain',
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
Reference in New Issue
Block a user