UNAUTHORIZED gmail error (#12262)
# Gmail OAuth authentication flow issues ### TLDR This error is not an error and therefore should be treated as a simple redirect with a snackbar. ### More details Fixing incomplete OAuth token exchange processes and improving error handling for empty Gmail inboxes. The changes include modifications to OAuth guards, to ensure that if a user clicks "cancel" instead of completing the authentication workflow if fails ## Before: Redirection from `/settings/accounts` to `app.twenty.com` with an `UNAUTHORIZED` error ## After : <img width="948" alt="Screenshot 2025-05-26 at 18 04 37" src="https://github.com/user-attachments/assets/62c8721e-c2b3-4e3d-ad0b-e4059dfb7a98" /> Fixes https://github.com/twentyhq/twenty/issues/11895 --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -8,6 +8,7 @@ import { ChromeExtensionSidecarProvider } from '@/chrome-extension-sidecar/compo
|
|||||||
import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider';
|
import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider';
|
||||||
import { ClientConfigProviderEffect } from '@/client-config/components/ClientConfigProviderEffect';
|
import { ClientConfigProviderEffect } from '@/client-config/components/ClientConfigProviderEffect';
|
||||||
import { MainContextStoreProvider } from '@/context-store/components/MainContextStoreProvider';
|
import { MainContextStoreProvider } from '@/context-store/components/MainContextStoreProvider';
|
||||||
|
import { ErrorMessageEffect } from '@/error-handler/components/ErrorMessageEffect';
|
||||||
import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect';
|
import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect';
|
||||||
import { ApolloMetadataClientProvider } from '@/object-metadata/components/ApolloMetadataClientProvider';
|
import { ApolloMetadataClientProvider } from '@/object-metadata/components/ApolloMetadataClientProvider';
|
||||||
import { ObjectMetadataItemsLoadEffect } from '@/object-metadata/components/ObjectMetadataItemsLoadEffect';
|
import { ObjectMetadataItemsLoadEffect } from '@/object-metadata/components/ObjectMetadataItemsLoadEffect';
|
||||||
@ -49,6 +50,7 @@ export const AppRouterProviders = () => {
|
|||||||
<PrefetchDataProvider>
|
<PrefetchDataProvider>
|
||||||
<UserThemeProviderEffect />
|
<UserThemeProviderEffect />
|
||||||
<SnackBarProvider>
|
<SnackBarProvider>
|
||||||
|
<ErrorMessageEffect />
|
||||||
<DialogManagerScope dialogManagerScopeId="dialog-manager">
|
<DialogManagerScope dialogManagerScopeId="dialog-manager">
|
||||||
<DialogManager>
|
<DialogManager>
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
|||||||
@ -5,8 +5,6 @@ import { useIsLogged } from '@/auth/hooks/useIsLogged';
|
|||||||
import { useVerifyLogin } from '@/auth/hooks/useVerifyLogin';
|
import { useVerifyLogin } from '@/auth/hooks/useVerifyLogin';
|
||||||
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
|
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
||||||
@ -14,9 +12,7 @@ import { useNavigateApp } from '~/hooks/useNavigateApp';
|
|||||||
export const VerifyLoginTokenEffect = () => {
|
export const VerifyLoginTokenEffect = () => {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const loginToken = searchParams.get('loginToken');
|
const loginToken = searchParams.get('loginToken');
|
||||||
const errorMessage = searchParams.get('errorMessage');
|
|
||||||
|
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
|
||||||
const isLogged = useIsLogged();
|
const isLogged = useIsLogged();
|
||||||
const navigate = useNavigateApp();
|
const navigate = useNavigateApp();
|
||||||
const { verifyLoginToken } = useVerifyLogin();
|
const { verifyLoginToken } = useVerifyLogin();
|
||||||
@ -26,13 +22,6 @@ export const VerifyLoginTokenEffect = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isDefined(errorMessage)) {
|
|
||||||
enqueueSnackBar(errorMessage, {
|
|
||||||
dedupeKey: 'get-auth-tokens-from-login-token-failed-dedupe-key',
|
|
||||||
variant: SnackBarVariant.Error,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!clientConfigLoaded) return;
|
if (!clientConfigLoaded) return;
|
||||||
|
|
||||||
if (isDefined(loginToken)) {
|
if (isDefined(loginToken)) {
|
||||||
|
|||||||
@ -0,0 +1,26 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
|
export const ErrorMessageEffect = () => {
|
||||||
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const errorMessage = searchParams.get('errorMessage');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDefined(errorMessage)) {
|
||||||
|
enqueueSnackBar(errorMessage, {
|
||||||
|
dedupeKey: 'error-message-dedupe-key',
|
||||||
|
variant: SnackBarVariant.Error,
|
||||||
|
});
|
||||||
|
const newSearchParams = new URLSearchParams(searchParams);
|
||||||
|
newSearchParams.delete('errorMessage');
|
||||||
|
setSearchParams(newSearchParams);
|
||||||
|
}
|
||||||
|
}, [enqueueSnackBar, errorMessage, searchParams, setSearchParams]);
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
};
|
||||||
@ -1,5 +1,3 @@
|
|||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
|
|
||||||
import { ConnectedAccount } from '@/accounts/types/ConnectedAccount';
|
import { ConnectedAccount } from '@/accounts/types/ConnectedAccount';
|
||||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
@ -14,6 +12,7 @@ import { SettingsPageContainer } from '@/settings/components/SettingsPageContain
|
|||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
import { H2Title } from 'twenty-ui/display';
|
import { H2Title } from 'twenty-ui/display';
|
||||||
import { Section } from 'twenty-ui/layout';
|
import { Section } from 'twenty-ui/layout';
|
||||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import { ValidatePasswordResetTokenInput } from 'src/engine/core-modules/auth/dt
|
|||||||
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
|
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
|
||||||
import { ApiKeyService } from 'src/engine/core-modules/auth/services/api-key.service';
|
import { ApiKeyService } from 'src/engine/core-modules/auth/services/api-key.service';
|
||||||
// import { OAuthService } from 'src/engine/core-modules/auth/services/oauth.service';
|
// import { OAuthService } from 'src/engine/core-modules/auth/services/oauth.service';
|
||||||
|
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||||
import {
|
import {
|
||||||
AuthException,
|
AuthException,
|
||||||
AuthExceptionCode,
|
AuthExceptionCode,
|
||||||
@ -53,7 +54,6 @@ 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 { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
|
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
|
||||||
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 { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
|
||||||
|
|
||||||
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';
|
||||||
|
|||||||
@ -118,15 +118,16 @@ export class GoogleAPIsAuthController {
|
|||||||
})
|
})
|
||||||
.toString(),
|
.toString(),
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
return res.redirect(
|
return res.redirect(
|
||||||
this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions(
|
this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions({
|
||||||
err,
|
error,
|
||||||
workspace ?? {
|
workspace: workspace ?? {
|
||||||
subdomain: this.twentyConfigService.get('DEFAULT_SUBDOMAIN'),
|
subdomain: this.twentyConfigService.get('DEFAULT_SUBDOMAIN'),
|
||||||
customDomain: null,
|
customDomain: null,
|
||||||
},
|
},
|
||||||
),
|
pathname: '/verify',
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -118,14 +118,16 @@ export class GoogleAuthController {
|
|||||||
billingCheckoutSessionState,
|
billingCheckoutSessionState,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
return res.redirect(
|
return res.redirect(
|
||||||
this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions(
|
this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions({
|
||||||
err,
|
error,
|
||||||
this.domainManagerService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
|
workspace:
|
||||||
currentWorkspace,
|
this.domainManagerService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
|
||||||
),
|
currentWorkspace,
|
||||||
),
|
),
|
||||||
|
pathname: '/verify',
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -125,15 +125,16 @@ export class MicrosoftAPIsAuthController {
|
|||||||
})
|
})
|
||||||
.toString(),
|
.toString(),
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
return res.redirect(
|
return res.redirect(
|
||||||
this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions(
|
this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions({
|
||||||
err,
|
error,
|
||||||
workspace ?? {
|
workspace: workspace ?? {
|
||||||
subdomain: this.twentyConfigService.get('DEFAULT_SUBDOMAIN'),
|
subdomain: this.twentyConfigService.get('DEFAULT_SUBDOMAIN'),
|
||||||
customDomain: null,
|
customDomain: null,
|
||||||
},
|
},
|
||||||
),
|
pathname: '/verify',
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -119,14 +119,16 @@ export class MicrosoftAuthController {
|
|||||||
billingCheckoutSessionState,
|
billingCheckoutSessionState,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
return res.redirect(
|
return res.redirect(
|
||||||
this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions(
|
this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions({
|
||||||
err,
|
error,
|
||||||
this.domainManagerService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
|
workspace:
|
||||||
currentWorkspace,
|
this.domainManagerService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
|
||||||
),
|
currentWorkspace,
|
||||||
),
|
),
|
||||||
|
pathname: '/verify',
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,25 +19,25 @@ import {
|
|||||||
AuthException,
|
AuthException,
|
||||||
AuthExceptionCode,
|
AuthExceptionCode,
|
||||||
} from 'src/engine/core-modules/auth/auth.exception';
|
} from 'src/engine/core-modules/auth/auth.exception';
|
||||||
|
import { AuthOAuthExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-oauth-exception.filter';
|
||||||
import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter';
|
import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter';
|
||||||
|
import { EnterpriseFeaturesEnabledGuard } from 'src/engine/core-modules/auth/guards/enterprise-features-enabled.guard';
|
||||||
import { OIDCAuthGuard } from 'src/engine/core-modules/auth/guards/oidc-auth.guard';
|
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 { SAMLAuthGuard } from 'src/engine/core-modules/auth/guards/saml-auth.guard';
|
||||||
import { EnterpriseFeaturesEnabledGuard } from 'src/engine/core-modules/auth/guards/enterprise-features-enabled.guard';
|
|
||||||
import { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
|
import { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
|
||||||
|
import { OIDCRequest } from 'src/engine/core-modules/auth/strategies/oidc.auth.strategy';
|
||||||
|
import { SAMLRequest } from 'src/engine/core-modules/auth/strategies/saml.auth.strategy';
|
||||||
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||||
|
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||||
|
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
|
||||||
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
|
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
|
||||||
import {
|
import {
|
||||||
IdentityProviderType,
|
IdentityProviderType,
|
||||||
WorkspaceSSOIdentityProvider,
|
WorkspaceSSOIdentityProvider,
|
||||||
} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { AuthOAuthExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-oauth-exception.filter';
|
|
||||||
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
|
|
||||||
import { SAMLRequest } from 'src/engine/core-modules/auth/strategies/saml.auth.strategy';
|
|
||||||
import { OIDCRequest } from 'src/engine/core-modules/auth/strategies/oidc.auth.strategy';
|
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
|
||||||
|
|
||||||
@Controller('auth')
|
@Controller('auth')
|
||||||
export class SSOAuthController {
|
export class SSOAuthController {
|
||||||
@ -157,14 +157,16 @@ export class SSOAuthController {
|
|||||||
workspace: currentWorkspace,
|
workspace: currentWorkspace,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
return res.redirect(
|
return res.redirect(
|
||||||
this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions(
|
this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions({
|
||||||
err,
|
error,
|
||||||
this.domainManagerService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
|
workspace:
|
||||||
workspaceIdentityProvider?.workspace,
|
this.domainManagerService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
|
||||||
),
|
workspaceIdentityProvider?.workspace,
|
||||||
),
|
),
|
||||||
|
pathname: '/verify',
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,10 +14,18 @@ export class AuthRestApiExceptionFilter implements ExceptionFilter {
|
|||||||
private readonly httpExceptionHandlerService: HttpExceptionHandlerService,
|
private readonly httpExceptionHandlerService: HttpExceptionHandlerService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
catch(exception: AuthException, host: ArgumentsHost) {
|
catch(exception: AuthException | Error, host: ArgumentsHost) {
|
||||||
const ctx = host.switchToHttp();
|
const ctx = host.switchToHttp();
|
||||||
const response = ctx.getResponse<Response>();
|
const response = ctx.getResponse<Response>();
|
||||||
|
|
||||||
|
if (!(exception instanceof AuthException)) {
|
||||||
|
return this.httpExceptionHandlerService.handleError(
|
||||||
|
exception,
|
||||||
|
response,
|
||||||
|
500,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
switch (exception.code) {
|
switch (exception.code) {
|
||||||
case AuthExceptionCode.USER_NOT_FOUND:
|
case AuthExceptionCode.USER_NOT_FOUND:
|
||||||
case AuthExceptionCode.CLIENT_NOT_FOUND:
|
case AuthExceptionCode.CLIENT_NOT_FOUND:
|
||||||
@ -43,6 +51,7 @@ export class AuthRestApiExceptionFilter implements ExceptionFilter {
|
|||||||
case AuthExceptionCode.GOOGLE_API_AUTH_DISABLED:
|
case AuthExceptionCode.GOOGLE_API_AUTH_DISABLED:
|
||||||
case AuthExceptionCode.MICROSOFT_API_AUTH_DISABLED:
|
case AuthExceptionCode.MICROSOFT_API_AUTH_DISABLED:
|
||||||
case AuthExceptionCode.SIGNUP_DISABLED:
|
case AuthExceptionCode.SIGNUP_DISABLED:
|
||||||
|
case AuthExceptionCode.WORKSPACE_NOT_FOUND:
|
||||||
return this.httpExceptionHandlerService.handleError(
|
return this.httpExceptionHandlerService.handleError(
|
||||||
exception,
|
exception,
|
||||||
response,
|
response,
|
||||||
|
|||||||
@ -1,14 +1,20 @@
|
|||||||
import { ExecutionContext, Injectable } from '@nestjs/common';
|
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AuthException,
|
AuthException,
|
||||||
AuthExceptionCode,
|
AuthExceptionCode,
|
||||||
} from 'src/engine/core-modules/auth/auth.exception';
|
} from 'src/engine/core-modules/auth/auth.exception';
|
||||||
import { GoogleAPIsOauthExchangeCodeForTokenStrategy } from 'src/engine/core-modules/auth/strategies/google-apis-oauth-exchange-code-for-token.auth.strategy';
|
import { GoogleAPIsOauthExchangeCodeForTokenStrategy } from 'src/engine/core-modules/auth/strategies/google-apis-oauth-exchange-code-for-token.auth.strategy';
|
||||||
|
import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service';
|
||||||
import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util';
|
import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util';
|
||||||
|
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||||
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
|
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
|
||||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||||
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GoogleAPIsOauthExchangeCodeForTokenGuard extends AuthGuard(
|
export class GoogleAPIsOauthExchangeCodeForTokenGuard extends AuthGuard(
|
||||||
@ -17,6 +23,10 @@ export class GoogleAPIsOauthExchangeCodeForTokenGuard extends AuthGuard(
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly guardRedirectService: GuardRedirectService,
|
private readonly guardRedirectService: GuardRedirectService,
|
||||||
private readonly twentyConfigService: TwentyConfigService,
|
private readonly twentyConfigService: TwentyConfigService,
|
||||||
|
private readonly transientTokenService: TransientTokenService,
|
||||||
|
private readonly domainManagerService: DomainManagerService,
|
||||||
|
@InjectRepository(Workspace, 'core')
|
||||||
|
private readonly workspaceRepository: Repository<Workspace>,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@ -47,6 +57,30 @@ export class GoogleAPIsOauthExchangeCodeForTokenGuard extends AuthGuard(
|
|||||||
|
|
||||||
return (await super.canActivate(context)) as boolean;
|
return (await super.canActivate(context)) as boolean;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (err.status === 401) {
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
const state = JSON.parse(request.query.state);
|
||||||
|
|
||||||
|
const workspace = await this.getWorkspaceFromTransientToken(
|
||||||
|
state.transientToken,
|
||||||
|
);
|
||||||
|
|
||||||
|
const redirectErrorUrl =
|
||||||
|
this.domainManagerService.computeRedirectErrorUrl(
|
||||||
|
'We cannot connect to your Google account, please try again with more permissions, or a valid account',
|
||||||
|
{
|
||||||
|
subdomain: workspace.subdomain,
|
||||||
|
customDomain: workspace.customDomain,
|
||||||
|
isCustomDomainEnabled: workspace.isCustomDomainEnabled ?? false,
|
||||||
|
},
|
||||||
|
'/settings/accounts',
|
||||||
|
);
|
||||||
|
|
||||||
|
context.switchToHttp().getResponse().redirect(redirectErrorUrl);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
this.guardRedirectService.dispatchErrorFromGuard(
|
this.guardRedirectService.dispatchErrorFromGuard(
|
||||||
context,
|
context,
|
||||||
err,
|
err,
|
||||||
@ -58,4 +92,24 @@ export class GoogleAPIsOauthExchangeCodeForTokenGuard extends AuthGuard(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getWorkspaceFromTransientToken(
|
||||||
|
transientToken: string,
|
||||||
|
): Promise<Workspace> {
|
||||||
|
const { workspaceId } =
|
||||||
|
await this.transientTokenService.verifyTransientToken(transientToken);
|
||||||
|
|
||||||
|
const workspace = await this.workspaceRepository.findOneBy({
|
||||||
|
id: workspaceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!workspace) {
|
||||||
|
throw new AuthException(
|
||||||
|
`Error extracting workspace from transientToken for Google APIs connect for ${workspaceId}`,
|
||||||
|
AuthExceptionCode.WORKSPACE_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return workspace;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -128,10 +128,11 @@ export class DomainManagerService {
|
|||||||
computeRedirectErrorUrl(
|
computeRedirectErrorUrl(
|
||||||
errorMessage: string,
|
errorMessage: string,
|
||||||
workspace: WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType,
|
workspace: WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType,
|
||||||
|
pathname: string,
|
||||||
) {
|
) {
|
||||||
const url = this.buildWorkspaceURL({
|
const url = this.buildWorkspaceURL({
|
||||||
workspace,
|
workspace,
|
||||||
pathname: '/verify',
|
pathname,
|
||||||
searchParams: { errorMessage },
|
searchParams: { errorMessage },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,10 @@ import { ExecutionContext, Injectable } from '@nestjs/common';
|
|||||||
|
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
|
|
||||||
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
|
import {
|
||||||
|
AuthException,
|
||||||
|
AuthExceptionCode,
|
||||||
|
} from 'src/engine/core-modules/auth/auth.exception';
|
||||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||||
@ -25,15 +28,19 @@ export class GuardRedirectService {
|
|||||||
customDomain: string | null;
|
customDomain: string | null;
|
||||||
isCustomDomainEnabled?: boolean;
|
isCustomDomainEnabled?: boolean;
|
||||||
},
|
},
|
||||||
|
pathname = '/verify',
|
||||||
) {
|
) {
|
||||||
if ('contextType' in context && context.contextType === 'graphql') {
|
if ('contextType' in context && context.contextType === 'graphql') {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
context
|
context.switchToHttp().getResponse().redirect(
|
||||||
.switchToHttp()
|
this.getRedirectErrorUrlAndCaptureExceptions({
|
||||||
.getResponse()
|
error,
|
||||||
.redirect(this.getRedirectErrorUrlAndCaptureExceptions(error, workspace));
|
workspace,
|
||||||
|
pathname,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSubdomainAndCustomDomainFromContext(context: ExecutionContext) {
|
getSubdomainAndCustomDomainFromContext(context: ExecutionContext) {
|
||||||
@ -58,7 +65,11 @@ export class GuardRedirectService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private captureException(err: Error | CustomException, workspaceId?: string) {
|
private captureException(err: Error | CustomException, workspaceId?: string) {
|
||||||
if (err instanceof AuthException) return;
|
if (
|
||||||
|
err instanceof AuthException &&
|
||||||
|
err.code !== AuthExceptionCode.INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
this.exceptionHandlerService.captureExceptions([err], {
|
this.exceptionHandlerService.captureExceptions([err], {
|
||||||
workspace: {
|
workspace: {
|
||||||
@ -67,24 +78,30 @@ export class GuardRedirectService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getRedirectErrorUrlAndCaptureExceptions(
|
getRedirectErrorUrlAndCaptureExceptions({
|
||||||
err: Error | CustomException,
|
error,
|
||||||
|
workspace,
|
||||||
|
pathname,
|
||||||
|
}: {
|
||||||
|
error: Error | AuthException;
|
||||||
workspace: {
|
workspace: {
|
||||||
id?: string;
|
id?: string;
|
||||||
subdomain: string;
|
subdomain: string;
|
||||||
customDomain: string | null;
|
customDomain: string | null;
|
||||||
isCustomDomainEnabled?: boolean;
|
isCustomDomainEnabled?: boolean;
|
||||||
},
|
};
|
||||||
) {
|
pathname: string;
|
||||||
this.captureException(err, workspace.id);
|
}) {
|
||||||
|
this.captureException(error, workspace.id);
|
||||||
|
|
||||||
return this.domainManagerService.computeRedirectErrorUrl(
|
return this.domainManagerService.computeRedirectErrorUrl(
|
||||||
err instanceof AuthException ? err.message : 'Unknown error',
|
error instanceof AuthException ? error.message : 'Unknown error',
|
||||||
{
|
{
|
||||||
subdomain: workspace.subdomain,
|
subdomain: workspace.subdomain,
|
||||||
customDomain: workspace.customDomain,
|
customDomain: workspace.customDomain,
|
||||||
isCustomDomainEnabled: workspace.isCustomDomainEnabled ?? false,
|
isCustomDomainEnabled: workspace.isCustomDomainEnabled ?? false,
|
||||||
},
|
},
|
||||||
|
pathname,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user