diff --git a/packages/backend/server/src/base/error/def.ts b/packages/backend/server/src/base/error/def.ts index c91d8e27e9..e44f4f61bf 100644 --- a/packages/backend/server/src/base/error/def.ts +++ b/packages/backend/server/src/base/error/def.ts @@ -301,6 +301,12 @@ export const USER_FRIENDLY_ERRORS = { type: 'bad_request', message: 'Invalid callback state parameter.', }, + invalid_oauth_callback_code: { + type: 'bad_request', + args: { status: 'number', body: 'string' }, + message: ({ status, body }) => + `Invalid callback code parameter, provider response status: ${status} and body: ${body}.`, + }, missing_oauth_query_parameter: { type: 'bad_request', args: { name: 'string' }, diff --git a/packages/backend/server/src/base/error/errors.gen.ts b/packages/backend/server/src/base/error/errors.gen.ts index b8cb1dff81..6bbad70b39 100644 --- a/packages/backend/server/src/base/error/errors.gen.ts +++ b/packages/backend/server/src/base/error/errors.gen.ts @@ -105,6 +105,17 @@ export class InvalidOauthCallbackState extends UserFriendlyError { } } @ObjectType() +class InvalidOauthCallbackCodeDataType { + @Field() status!: number + @Field() body!: string +} + +export class InvalidOauthCallbackCode extends UserFriendlyError { + constructor(args: InvalidOauthCallbackCodeDataType, message?: string | ((args: InvalidOauthCallbackCodeDataType) => string)) { + super('bad_request', 'invalid_oauth_callback_code', message, args); + } +} +@ObjectType() class MissingOauthQueryParameterDataType { @Field() name!: string } @@ -820,6 +831,7 @@ export enum ErrorNames { UNKNOWN_OAUTH_PROVIDER, OAUTH_STATE_EXPIRED, INVALID_OAUTH_CALLBACK_STATE, + INVALID_OAUTH_CALLBACK_CODE, MISSING_OAUTH_QUERY_PARAMETER, OAUTH_ACCOUNT_ALREADY_CONNECTED, INVALID_EMAIL, @@ -916,5 +928,5 @@ registerEnumType(ErrorNames, { export const ErrorDataUnionType = createUnionType({ name: 'ErrorDataUnion', types: () => - [GraphqlBadRequestDataType, QueryTooLongDataType, WrongSignInCredentialsDataType, UnknownOauthProviderDataType, MissingOauthQueryParameterDataType, InvalidEmailDataType, InvalidPasswordLengthDataType, WorkspacePermissionNotFoundDataType, SpaceNotFoundDataType, MemberNotFoundInSpaceDataType, NotInSpaceDataType, AlreadyInSpaceDataType, SpaceAccessDeniedDataType, SpaceOwnerNotFoundDataType, SpaceShouldHaveOnlyOneOwnerDataType, DocNotFoundDataType, DocActionDeniedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, ExpectToGrantDocUserRolesDataType, ExpectToRevokeDocUserRolesDataType, ExpectToUpdateDocUserRoleDataType, UnsupportedSubscriptionPlanDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, CopilotDocNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderSideErrorDataType, CopilotInvalidContextDataType, CopilotContextFileNotSupportedDataType, CopilotFailedToModifyContextDataType, CopilotFailedToMatchContextDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType, InvalidLicenseUpdateParamsDataType, WorkspaceMembersExceedLimitToDowngradeDataType, UnsupportedClientVersionDataType] as const, + [GraphqlBadRequestDataType, QueryTooLongDataType, WrongSignInCredentialsDataType, UnknownOauthProviderDataType, InvalidOauthCallbackCodeDataType, MissingOauthQueryParameterDataType, InvalidEmailDataType, InvalidPasswordLengthDataType, WorkspacePermissionNotFoundDataType, SpaceNotFoundDataType, MemberNotFoundInSpaceDataType, NotInSpaceDataType, AlreadyInSpaceDataType, SpaceAccessDeniedDataType, SpaceOwnerNotFoundDataType, SpaceShouldHaveOnlyOneOwnerDataType, DocNotFoundDataType, DocActionDeniedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, ExpectToGrantDocUserRolesDataType, ExpectToRevokeDocUserRolesDataType, ExpectToUpdateDocUserRoleDataType, UnsupportedSubscriptionPlanDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, CopilotDocNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderSideErrorDataType, CopilotInvalidContextDataType, CopilotContextFileNotSupportedDataType, CopilotFailedToModifyContextDataType, CopilotFailedToMatchContextDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType, InvalidLicenseUpdateParamsDataType, WorkspaceMembersExceedLimitToDowngradeDataType, UnsupportedClientVersionDataType] as const, }); diff --git a/packages/backend/server/src/plugins/oauth/controller.ts b/packages/backend/server/src/plugins/oauth/controller.ts index a9033b9c18..af2329e0b6 100644 --- a/packages/backend/server/src/plugins/oauth/controller.ts +++ b/packages/backend/server/src/plugins/oauth/controller.ts @@ -3,7 +3,9 @@ import { Controller, HttpCode, HttpStatus, + Logger, Post, + type RawBodyRequest, Req, Res, } from '@nestjs/common'; @@ -27,6 +29,8 @@ import { OAuthService } from './service'; @Controller('/api/oauth') export class OAuthController { + private readonly logger = new Logger(OAuthController.name); + constructor( private readonly auth: AuthService, private readonly oauth: OAuthService, @@ -69,7 +73,7 @@ export class OAuthController { @Post('/callback') @HttpCode(HttpStatus.OK) async callback( - @Req() req: Request, + @Req() req: RawBodyRequest, @Res() res: Response, @Body('code') code?: string, @Body('state') stateStr?: string @@ -102,7 +106,20 @@ export class OAuthController { throw new UnknownOauthProvider({ name: state.provider ?? 'unknown' }); } - const tokens = await provider.getToken(code); + let tokens: Tokens; + try { + tokens = await provider.getToken(code); + } catch (err) { + let rayBodyString = ''; + if (req.rawBody) { + // only log the first 4096 bytes of the raw body + rayBodyString = req.rawBody.subarray(0, 4096).toString('utf-8'); + } + this.logger.warn( + `Error getting oauth token for ${state.provider}, callback code: ${code}, stateStr: ${stateStr}, rawBody: ${rayBodyString}, error: ${err}` + ); + throw err; + } const externAccount = await provider.getUser(tokens.accessToken); const user = await this.loginFromOauth( state.provider, diff --git a/packages/backend/server/src/plugins/oauth/providers/github.ts b/packages/backend/server/src/plugins/oauth/providers/github.ts index a7727da791..bff59262f3 100644 --- a/packages/backend/server/src/plugins/oauth/providers/github.ts +++ b/packages/backend/server/src/plugins/oauth/providers/github.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { Config, URLHelper } from '../../../base'; +import { Config, InvalidOauthCallbackCode, URLHelper } from '../../../base'; import { OAuthProviderName } from '../config'; import { AutoRegisteredOAuthProvider } from '../register'; @@ -64,10 +64,12 @@ export class GithubOAuthProvider extends AutoRegisteredOAuthProvider { scope: ghToken.scope, }; } else { + const body = await response.text(); + if (response.status < 500) { + throw new InvalidOauthCallbackCode({ status: response.status, body }); + } throw new Error( - `Server responded with non-success code ${ - response.status - }, ${JSON.stringify(await response.json())}` + `Server responded with non-success status ${response.status}, body: ${body}` ); } } diff --git a/packages/backend/server/src/plugins/oauth/providers/google.ts b/packages/backend/server/src/plugins/oauth/providers/google.ts index 56c45eb5cf..8bb2f15360 100644 --- a/packages/backend/server/src/plugins/oauth/providers/google.ts +++ b/packages/backend/server/src/plugins/oauth/providers/google.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { Config, URLHelper } from '../../../base'; +import { Config, InvalidOauthCallbackCode, URLHelper } from '../../../base'; import { OAuthProviderName } from '../config'; import { AutoRegisteredOAuthProvider } from '../register'; @@ -69,10 +69,12 @@ export class GoogleOAuthProvider extends AutoRegisteredOAuthProvider { scope: ghToken.scope, }; } else { + const body = await response.text(); + if (response.status < 500) { + throw new InvalidOauthCallbackCode({ status: response.status, body }); + } throw new Error( - `Server responded with non-success code ${ - response.status - }, ${JSON.stringify(await response.json())}` + `Server responded with non-success status ${response.status}, body: ${body}` ); } } diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index 20df8bf2a8..1d4342faa9 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -324,7 +324,7 @@ type EditorType { name: String! } -union ErrorDataUnion = AlreadyInSpaceDataType | BlobNotFoundDataType | CopilotContextFileNotSupportedDataType | CopilotDocNotFoundDataType | CopilotFailedToMatchContextDataType | CopilotFailedToModifyContextDataType | CopilotInvalidContextDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderSideErrorDataType | DocActionDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | ExpectToGrantDocUserRolesDataType | ExpectToRevokeDocUserRolesDataType | ExpectToUpdateDocUserRoleDataType | GraphqlBadRequestDataType | InvalidEmailDataType | InvalidHistoryTimestampDataType | InvalidLicenseUpdateParamsDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | MemberNotFoundInSpaceDataType | MissingOauthQueryParameterDataType | NotInSpaceDataType | QueryTooLongDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SpaceAccessDeniedDataType | SpaceNotFoundDataType | SpaceOwnerNotFoundDataType | SpaceShouldHaveOnlyOneOwnerDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | UnsupportedClientVersionDataType | UnsupportedSubscriptionPlanDataType | VersionRejectedDataType | WorkspaceMembersExceedLimitToDowngradeDataType | WorkspacePermissionNotFoundDataType | WrongSignInCredentialsDataType +union ErrorDataUnion = AlreadyInSpaceDataType | BlobNotFoundDataType | CopilotContextFileNotSupportedDataType | CopilotDocNotFoundDataType | CopilotFailedToMatchContextDataType | CopilotFailedToModifyContextDataType | CopilotInvalidContextDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderSideErrorDataType | DocActionDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | ExpectToGrantDocUserRolesDataType | ExpectToRevokeDocUserRolesDataType | ExpectToUpdateDocUserRoleDataType | GraphqlBadRequestDataType | InvalidEmailDataType | InvalidHistoryTimestampDataType | InvalidLicenseUpdateParamsDataType | InvalidOauthCallbackCodeDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | MemberNotFoundInSpaceDataType | MissingOauthQueryParameterDataType | NotInSpaceDataType | QueryTooLongDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SpaceAccessDeniedDataType | SpaceNotFoundDataType | SpaceOwnerNotFoundDataType | SpaceShouldHaveOnlyOneOwnerDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | UnsupportedClientVersionDataType | UnsupportedSubscriptionPlanDataType | VersionRejectedDataType | WorkspaceMembersExceedLimitToDowngradeDataType | WorkspacePermissionNotFoundDataType | WrongSignInCredentialsDataType enum ErrorNames { ACCESS_DENIED @@ -382,6 +382,7 @@ enum ErrorNames { INVALID_LICENSE_SESSION_ID INVALID_LICENSE_TO_ACTIVATE INVALID_LICENSE_UPDATE_PARAMS + INVALID_OAUTH_CALLBACK_CODE INVALID_OAUTH_CALLBACK_STATE INVALID_PASSWORD_LENGTH INVALID_RUNTIME_CONFIG_TYPE @@ -503,6 +504,11 @@ type InvalidLicenseUpdateParamsDataType { reason: String! } +type InvalidOauthCallbackCodeDataType { + body: String! + status: Int! +} + type InvalidPasswordLengthDataType { max: Int! min: Int!