feat(server): add invalid oauth callback code error handling (#10603)

close CLOUD-130
This commit is contained in:
fengmk2
2025-03-05 06:16:59 +00:00
parent 0b8fa7904d
commit b88113a2d1
6 changed files with 57 additions and 12 deletions

View File

@@ -301,6 +301,12 @@ export const USER_FRIENDLY_ERRORS = {
type: 'bad_request', type: 'bad_request',
message: 'Invalid callback state parameter.', 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: { missing_oauth_query_parameter: {
type: 'bad_request', type: 'bad_request',
args: { name: 'string' }, args: { name: 'string' },

View File

@@ -105,6 +105,17 @@ export class InvalidOauthCallbackState extends UserFriendlyError {
} }
} }
@ObjectType() @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 { class MissingOauthQueryParameterDataType {
@Field() name!: string @Field() name!: string
} }
@@ -820,6 +831,7 @@ export enum ErrorNames {
UNKNOWN_OAUTH_PROVIDER, UNKNOWN_OAUTH_PROVIDER,
OAUTH_STATE_EXPIRED, OAUTH_STATE_EXPIRED,
INVALID_OAUTH_CALLBACK_STATE, INVALID_OAUTH_CALLBACK_STATE,
INVALID_OAUTH_CALLBACK_CODE,
MISSING_OAUTH_QUERY_PARAMETER, MISSING_OAUTH_QUERY_PARAMETER,
OAUTH_ACCOUNT_ALREADY_CONNECTED, OAUTH_ACCOUNT_ALREADY_CONNECTED,
INVALID_EMAIL, INVALID_EMAIL,
@@ -916,5 +928,5 @@ registerEnumType(ErrorNames, {
export const ErrorDataUnionType = createUnionType({ export const ErrorDataUnionType = createUnionType({
name: 'ErrorDataUnion', name: 'ErrorDataUnion',
types: () => 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,
}); });

View File

@@ -3,7 +3,9 @@ import {
Controller, Controller,
HttpCode, HttpCode,
HttpStatus, HttpStatus,
Logger,
Post, Post,
type RawBodyRequest,
Req, Req,
Res, Res,
} from '@nestjs/common'; } from '@nestjs/common';
@@ -27,6 +29,8 @@ import { OAuthService } from './service';
@Controller('/api/oauth') @Controller('/api/oauth')
export class OAuthController { export class OAuthController {
private readonly logger = new Logger(OAuthController.name);
constructor( constructor(
private readonly auth: AuthService, private readonly auth: AuthService,
private readonly oauth: OAuthService, private readonly oauth: OAuthService,
@@ -69,7 +73,7 @@ export class OAuthController {
@Post('/callback') @Post('/callback')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
async callback( async callback(
@Req() req: Request, @Req() req: RawBodyRequest<Request>,
@Res() res: Response, @Res() res: Response,
@Body('code') code?: string, @Body('code') code?: string,
@Body('state') stateStr?: string @Body('state') stateStr?: string
@@ -102,7 +106,20 @@ export class OAuthController {
throw new UnknownOauthProvider({ name: state.provider ?? 'unknown' }); 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 externAccount = await provider.getUser(tokens.accessToken);
const user = await this.loginFromOauth( const user = await this.loginFromOauth(
state.provider, state.provider,

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Config, URLHelper } from '../../../base'; import { Config, InvalidOauthCallbackCode, URLHelper } from '../../../base';
import { OAuthProviderName } from '../config'; import { OAuthProviderName } from '../config';
import { AutoRegisteredOAuthProvider } from '../register'; import { AutoRegisteredOAuthProvider } from '../register';
@@ -64,10 +64,12 @@ export class GithubOAuthProvider extends AutoRegisteredOAuthProvider {
scope: ghToken.scope, scope: ghToken.scope,
}; };
} else { } else {
const body = await response.text();
if (response.status < 500) {
throw new InvalidOauthCallbackCode({ status: response.status, body });
}
throw new Error( throw new Error(
`Server responded with non-success code ${ `Server responded with non-success status ${response.status}, body: ${body}`
response.status
}, ${JSON.stringify(await response.json())}`
); );
} }
} }

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Config, URLHelper } from '../../../base'; import { Config, InvalidOauthCallbackCode, URLHelper } from '../../../base';
import { OAuthProviderName } from '../config'; import { OAuthProviderName } from '../config';
import { AutoRegisteredOAuthProvider } from '../register'; import { AutoRegisteredOAuthProvider } from '../register';
@@ -69,10 +69,12 @@ export class GoogleOAuthProvider extends AutoRegisteredOAuthProvider {
scope: ghToken.scope, scope: ghToken.scope,
}; };
} else { } else {
const body = await response.text();
if (response.status < 500) {
throw new InvalidOauthCallbackCode({ status: response.status, body });
}
throw new Error( throw new Error(
`Server responded with non-success code ${ `Server responded with non-success status ${response.status}, body: ${body}`
response.status
}, ${JSON.stringify(await response.json())}`
); );
} }
} }

View File

@@ -324,7 +324,7 @@ type EditorType {
name: String! 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 { enum ErrorNames {
ACCESS_DENIED ACCESS_DENIED
@@ -382,6 +382,7 @@ enum ErrorNames {
INVALID_LICENSE_SESSION_ID INVALID_LICENSE_SESSION_ID
INVALID_LICENSE_TO_ACTIVATE INVALID_LICENSE_TO_ACTIVATE
INVALID_LICENSE_UPDATE_PARAMS INVALID_LICENSE_UPDATE_PARAMS
INVALID_OAUTH_CALLBACK_CODE
INVALID_OAUTH_CALLBACK_STATE INVALID_OAUTH_CALLBACK_STATE
INVALID_PASSWORD_LENGTH INVALID_PASSWORD_LENGTH
INVALID_RUNTIME_CONFIG_TYPE INVALID_RUNTIME_CONFIG_TYPE
@@ -503,6 +504,11 @@ type InvalidLicenseUpdateParamsDataType {
reason: String! reason: String!
} }
type InvalidOauthCallbackCodeDataType {
body: String!
status: Int!
}
type InvalidPasswordLengthDataType { type InvalidPasswordLengthDataType {
max: Int! max: Int!
min: Int! min: Int!