OAuth Edge case crash + cleanup (#18326)

Fixes Sentry issue https://twenty-v7.sentry.io/issues/6603377117/

Also cleaned up the code with proper types removing `//
eslint-disable-next-line @typescript-eslint/no-explicit-any`
This commit is contained in:
neo773
2026-03-03 16:52:12 +05:30
committed by GitHub
parent 2e9624858c
commit 083df3e7ca
13 changed files with 113 additions and 117 deletions
@@ -22,7 +22,7 @@ import { GoogleAPIsOauthExchangeCodeForTokenGuard } from 'src/engine/core-module
import { GoogleAPIsOauthRequestCodeGuard } from 'src/engine/core-modules/auth/guards/google-apis-oauth-request-code.guard';
import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service';
import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service';
import { GoogleAPIsRequest } from 'src/engine/core-modules/auth/types/google-api-request.type';
import { APIsOAuthRequest } from 'src/engine/core-modules/auth/types/apis-oauth-request.type';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
@@ -63,7 +63,7 @@ export class GoogleAPIsAuthController {
NoPermissionGuard,
)
async googleAuthGetAccessToken(
@Req() req: GoogleAPIsRequest,
@Req() req: APIsOAuthRequest,
@Res() res: Response,
) {
let workspace: WorkspaceEntity | null = null;
@@ -22,7 +22,7 @@ import { MicrosoftAPIsOauthExchangeCodeForTokenGuard } from 'src/engine/core-mod
import { MicrosoftAPIsOauthRequestCodeGuard } from 'src/engine/core-modules/auth/guards/microsoft-apis-oauth-request-code.guard';
import { MicrosoftAPIsService } from 'src/engine/core-modules/auth/services/microsoft-apis.service';
import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service';
import { MicrosoftAPIsRequest } from 'src/engine/core-modules/auth/types/microsoft-api-request.type';
import { APIsOAuthRequest } from 'src/engine/core-modules/auth/types/apis-oauth-request.type';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
@@ -63,7 +63,7 @@ export class MicrosoftAPIsAuthController {
NoPermissionGuard,
)
async MicrosoftAuthGetAccessToken(
@Req() req: MicrosoftAPIsRequest,
@Req() req: APIsOAuthRequest,
@Res() res: Response,
) {
let workspace: WorkspaceEntity | null = null;
@@ -1,15 +1,16 @@
import { Injectable } from '@nestjs/common';
import { type VerifyCallback } from 'passport-google-oauth20';
import {
type Profile as GoogleProfile,
type VerifyCallback,
} from 'passport-google-oauth20';
import { parseJson } from 'twenty-shared/utils';
import { GoogleAPIsOauthCommonStrategy } from 'src/engine/core-modules/auth/strategies/google-apis-oauth-common.auth.strategy';
import { type GoogleAPIsRequest } from 'src/engine/core-modules/auth/types/google-api-request.type';
import { type APIsOAuthRequest } from 'src/engine/core-modules/auth/types/apis-oauth-request.type';
import { type APIsOAuthState } from 'src/engine/core-modules/auth/types/apis-oauth-state.type';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
export type GoogleAPIScopeConfig = {
isCalendarEnabled?: boolean;
};
@Injectable()
export class GoogleAPIsOauthExchangeCodeForTokenStrategy extends GoogleAPIsOauthCommonStrategy {
constructor(
@@ -20,32 +21,27 @@ export class GoogleAPIsOauthExchangeCodeForTokenStrategy extends GoogleAPIsOauth
}
async validate(
request: GoogleAPIsRequest,
request: APIsOAuthRequest,
accessToken: string,
refreshToken: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
profile: any,
profile: GoogleProfile,
done: VerifyCallback,
): Promise<void> {
const { name, emails, photos } = profile;
const state = parseJson<APIsOAuthState>(request.query.state as string);
const state =
typeof request.query.state === 'string'
? JSON.parse(request.query.state)
: undefined;
const user: GoogleAPIsRequest['user'] = {
emails,
firstName: name.givenName,
lastName: name.familyName,
picture: photos?.[0]?.value,
const user: APIsOAuthRequest['user'] = {
emails: emails ?? [],
firstName: name?.givenName,
lastName: name?.familyName,
picture: photos?.[0]?.value ?? null,
accessToken,
refreshToken,
transientToken: state.transientToken,
redirectLocation: state.redirectLocation,
calendarVisibility: state.calendarVisibility,
messageVisibility: state.messageVisibility,
skipMessageChannelConfiguration: state.skipMessageChannelConfiguration,
transientToken: state?.transientToken ?? '',
redirectLocation: state?.redirectLocation,
calendarVisibility: state?.calendarVisibility,
messageVisibility: state?.messageVisibility,
skipMessageChannelConfiguration: state?.skipMessageChannelConfiguration,
};
done(null, user);
@@ -2,14 +2,20 @@ import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { type Request } from 'express';
import { Strategy, type VerifyCallback } from 'passport-google-oauth20';
import {
Strategy,
type Profile as GoogleProfile,
type VerifyCallback,
} from 'passport-google-oauth20';
import { type APP_LOCALES } from 'twenty-shared/translations';
import { parseJson } from 'twenty-shared/utils';
import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { type SocialSSOSignInUpActionType } from 'src/engine/core-modules/auth/types/signInUp.type';
import { type SocialSSOState } from 'src/engine/core-modules/auth/types/social-sso-state.type';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
export type GoogleRequest = Omit<
@@ -52,6 +58,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
billingCheckoutSessionState: req.query.billingCheckoutSessionState,
workspacePersonalInviteToken: req.query.workspacePersonalInviteToken,
action: req.query.action,
locale: req.query.locale,
}),
};
@@ -62,18 +69,14 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
request: GoogleRequest,
_accessToken: string,
_refreshToken: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
profile: any,
profile: GoogleProfile,
done: VerifyCallback,
): Promise<void> {
const { name, emails, photos } = profile;
const state =
typeof request.query.state === 'string'
? JSON.parse(request.query.state)
: undefined;
const state = parseJson<SocialSSOState>(request.query.state as string);
const firstVerifiedEmail = emails.find(
(email: { verified: boolean }) => email?.verified === true,
const firstVerifiedEmail = emails?.find(
(email) => email?.verified === true,
)?.value;
if (!firstVerifiedEmail) {
@@ -85,15 +88,15 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
const user: GoogleRequest['user'] = {
email: firstVerifiedEmail,
firstName: name.givenName,
lastName: name.familyName,
picture: photos?.[0]?.value,
workspaceInviteHash: state.workspaceInviteHash,
workspacePersonalInviteToken: state.workspacePersonalInviteToken,
workspaceId: state.workspaceId,
billingCheckoutSessionState: state.billingCheckoutSessionState,
action: state.action,
locale: state.locale,
firstName: name?.givenName,
lastName: name?.familyName,
picture: photos?.[0]?.value ?? null,
workspaceInviteHash: state?.workspaceInviteHash,
workspacePersonalInviteToken: state?.workspacePersonalInviteToken,
workspaceId: state?.workspaceId,
billingCheckoutSessionState: state?.billingCheckoutSessionState,
action: state?.action ?? 'list-available-workspaces',
locale: state?.locale,
};
done(null, user);
@@ -1,15 +1,14 @@
import { Injectable } from '@nestjs/common';
import { type VerifyCallback } from 'passport-google-oauth20';
import { parseJson } from 'twenty-shared/utils';
import { MicrosoftAPIsOauthCommonStrategy } from 'src/engine/core-modules/auth/strategies/microsoft-apis-oauth-common.auth.strategy';
import { type MicrosoftAPIsRequest } from 'src/engine/core-modules/auth/types/microsoft-api-request.type';
import { type APIsOAuthRequest } from 'src/engine/core-modules/auth/types/apis-oauth-request.type';
import { type APIsOAuthState } from 'src/engine/core-modules/auth/types/apis-oauth-state.type';
import { type MicrosoftPassportProfile } from 'src/engine/core-modules/auth/types/microsoft-passport-profile.type';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
export type MicrosoftAPIScopeConfig = {
isCalendarEnabled?: boolean;
};
@Injectable()
export class MicrosoftAPIsOauthExchangeCodeForTokenStrategy extends MicrosoftAPIsOauthCommonStrategy {
constructor(twentyConfigService: TwentyConfigService) {
@@ -17,32 +16,27 @@ export class MicrosoftAPIsOauthExchangeCodeForTokenStrategy extends MicrosoftAPI
}
async validate(
request: MicrosoftAPIsRequest,
request: APIsOAuthRequest,
accessToken: string,
refreshToken: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
profile: any,
profile: MicrosoftPassportProfile,
done: VerifyCallback,
): Promise<void> {
const { name, emails, photos } = profile;
const state = parseJson<APIsOAuthState>(request.query.state as string);
const state =
typeof request.query.state === 'string'
? JSON.parse(request.query.state)
: undefined;
const user: MicrosoftAPIsRequest['user'] = {
emails,
firstName: name.givenName,
lastName: name.familyName,
picture: photos?.[0]?.value,
const user: APIsOAuthRequest['user'] = {
emails: emails ?? [],
firstName: name?.givenName,
lastName: name?.familyName,
picture: photos?.[0]?.value ?? null,
accessToken,
refreshToken,
transientToken: state.transientToken,
redirectLocation: state.redirectLocation,
calendarVisibility: state.calendarVisibility,
messageVisibility: state.messageVisibility,
skipMessageChannelConfiguration: state.skipMessageChannelConfiguration,
transientToken: state?.transientToken ?? '',
redirectLocation: state?.redirectLocation,
calendarVisibility: state?.calendarVisibility,
messageVisibility: state?.messageVisibility,
skipMessageChannelConfiguration: state?.skipMessageChannelConfiguration,
};
done(null, user);
@@ -1,15 +1,19 @@
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { type Request } from 'express';
import { type VerifyCallback } from 'passport-google-oauth20';
import { Strategy } from 'passport-microsoft';
import { type APP_LOCALES } from 'twenty-shared/translations';
import { parseJson } from 'twenty-shared/utils';
import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { type MicrosoftPassportProfile } from 'src/engine/core-modules/auth/types/microsoft-passport-profile.type';
import { type SocialSSOSignInUpActionType } from 'src/engine/core-modules/auth/types/signInUp.type';
import { type SocialSSOState } from 'src/engine/core-modules/auth/types/social-sso-state.type';
import { type TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
export type MicrosoftRequest = Omit<
@@ -30,6 +34,7 @@ export type MicrosoftRequest = Omit<
};
};
@Injectable()
export class MicrosoftStrategy extends PassportStrategy(Strategy, 'microsoft') {
constructor(twentyConfigService: TwentyConfigService) {
super({
@@ -63,16 +68,11 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy, 'microsoft') {
request: MicrosoftRequest,
_accessToken: string,
_refreshToken: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
profile: any,
profile: MicrosoftPassportProfile,
done: VerifyCallback,
): Promise<void> {
const { name, userPrincipalName, photos } = profile;
const state =
typeof request.query.state === 'string'
? JSON.parse(request.query.state)
: undefined;
const state = parseJson<SocialSSOState>(request.query.state as string);
if (!userPrincipalName) {
throw new AuthException(
@@ -83,15 +83,15 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy, 'microsoft') {
const user: MicrosoftRequest['user'] = {
email: userPrincipalName,
firstName: name.givenName,
lastName: name.familyName,
picture: photos?.[0]?.value,
workspaceInviteHash: state.workspaceInviteHash,
workspacePersonalInviteToken: state.workspacePersonalInviteToken,
workspaceId: state.workspaceId,
billingCheckoutSessionState: state.billingCheckoutSessionState,
locale: state.locale,
action: state.action,
firstName: name?.givenName,
lastName: name?.familyName,
picture: photos?.[0]?.value ?? null,
workspaceInviteHash: state?.workspaceInviteHash,
workspacePersonalInviteToken: state?.workspacePersonalInviteToken,
workspaceId: state?.workspaceId,
billingCheckoutSessionState: state?.billingCheckoutSessionState,
locale: state?.locale,
action: state?.action ?? 'list-available-workspaces',
};
done(null, user);
@@ -3,7 +3,7 @@ import { type Request } from 'express';
import { type CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { type MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
export type GoogleAPIsRequest = Omit<
export type APIsOAuthRequest = Omit<
Request,
'user' | 'workspace' | 'workspaceMetadataVersion'
> & {
@@ -0,0 +1,10 @@
import { type CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { type MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
export type APIsOAuthState = {
transientToken?: string;
redirectLocation?: string;
calendarVisibility?: CalendarChannelVisibility;
messageVisibility?: MessageChannelVisibility;
skipMessageChannelConfiguration?: boolean;
};
@@ -1,24 +0,0 @@
import { type Request } from 'express';
import { type CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { type MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
export type MicrosoftAPIsRequest = Omit<
Request,
'user' | 'workspace' | 'workspaceMetadataVersion'
> & {
user: {
firstName?: string | null;
lastName?: string | null;
emails: { value: string }[];
picture: string | null;
workspaceInviteHash?: string;
accessToken: string;
refreshToken: string;
transientToken: string;
redirectLocation?: string;
calendarVisibility?: CalendarChannelVisibility;
messageVisibility?: MessageChannelVisibility;
skipMessageChannelConfiguration?: boolean;
};
};
@@ -0,0 +1,5 @@
import { type Profile } from 'passport';
export type MicrosoftPassportProfile = Profile & {
userPrincipalName?: string;
};
@@ -0,0 +1,12 @@
import { type APP_LOCALES } from 'twenty-shared/translations';
import { type SocialSSOSignInUpActionType } from 'src/engine/core-modules/auth/types/signInUp.type';
export type SocialSSOState = {
workspaceInviteHash?: string;
workspaceId?: string;
billingCheckoutSessionState?: string;
workspacePersonalInviteToken?: string;
action?: SocialSSOSignInUpActionType;
locale?: keyof typeof APP_LOCALES;
};
@@ -1,4 +1,4 @@
import { type GoogleAPIsRequest } from 'src/engine/core-modules/auth/types/google-api-request.type';
import { type APIsOAuthRequest } from 'src/engine/core-modules/auth/types/apis-oauth-request.type';
import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util';
import { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
@@ -7,7 +7,7 @@ describe('googleApisSetRequestExtraParams', () => {
it('should set request extra params', () => {
const request = {
params: {},
} as GoogleAPIsRequest;
} as APIsOAuthRequest;
setRequestExtraParams(request, {
transientToken: 'abc',
@@ -27,7 +27,7 @@ describe('googleApisSetRequestExtraParams', () => {
it('should throw error if transientToken is not provided', () => {
const request = {
params: {},
} as GoogleAPIsRequest;
} as APIsOAuthRequest;
expect(() => {
setRequestExtraParams(request, {
@@ -2,9 +2,9 @@ import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { type GoogleAPIsRequest } from 'src/engine/core-modules/auth/types/google-api-request.type';
import { type APIsOAuthRequest } from 'src/engine/core-modules/auth/types/apis-oauth-request.type';
type GoogleAPIsRequestExtraParams = {
type APIsOAuthRequestExtraParams = {
transientToken?: string;
redirectLocation?: string;
calendarVisibility?: string;
@@ -16,8 +16,8 @@ type GoogleAPIsRequestExtraParams = {
};
export const setRequestExtraParams = (
request: GoogleAPIsRequest,
params: GoogleAPIsRequestExtraParams,
request: APIsOAuthRequest,
params: APIsOAuthRequestExtraParams,
): void => {
const {
transientToken,