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:
+2
-2
@@ -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;
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+20
-24
@@ -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);
|
||||
|
||||
+21
-18
@@ -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);
|
||||
|
||||
+17
-23
@@ -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);
|
||||
|
||||
+16
-16
@@ -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);
|
||||
|
||||
+1
-1
@@ -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;
|
||||
};
|
||||
-24
@@ -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;
|
||||
};
|
||||
};
|
||||
+5
@@ -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;
|
||||
};
|
||||
+3
-3
@@ -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, {
|
||||
|
||||
+4
-4
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user