yikart/AiToEarn · error · AppException

ChannelAuthCsrfInvalid

ChannelAuthCsrfInvalid

Error message

ResponseCode.ChannelAuthCsrfInvalid

What it means

Thrown in parseMiniAppCallback when the zod schema validation of the Douyin mini-app callback payload fails specifically on the 'state' field. 'state' carries the CSRF/session identifier, so an invalid state means the callback cannot be tied to a legitimate auth session.

Source

Thrown at project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/douyin/douyin-auth.provider.ts:238

  private parseMiniAppCallback(input: AuthCallbackInput): DouyinMiniAppCallback | undefined {
    const payload = {
      state: input.query?.state ?? input.body?.state,
      token: input.query?.token ?? input.body?.token,
      nickname: input.query?.nickname ?? input.body?.nickname,
      avatar: input.query?.avatar ?? input.body?.avatar,
      tickets: input.body?.tickets,
    }
    if (payload.token === undefined && payload.tickets === undefined) {
      return undefined
    }

    const result = DouyinMiniAppCallbackSchema.safeParse(payload)
    if (!result.success) {
      const hasInvalidState = result.error.issues.some(issue => issue.path[0] === 'state')
      const hasInvalidToken = result.error.issues.some(issue => issue.path[0] === 'token')
      if (hasInvalidState) {
        throw new AppException(ResponseCode.ChannelAuthCsrfInvalid)
      }
      if (hasInvalidToken) {
        throw new AppException(ResponseCode.ChannelAuthCodeMissing)
      }

      throw new AppException(ResponseCode.ChannelAuthorizationFailed)
    }

    assertParsedCallbackState(result.data.state, input.session.id)
    return result.data
  }

  private hasMiniAppScope(scopes: string | undefined, scope: string) {
    return !!scopes?.split(/[,\s]+/).includes(scope)
  }

  private maskOpenId(openId: string) {
    if (openId.length <= 8) {

View on GitHub (pinned to d3aa8bea5b)

Solutions

  1. Re-initiate the mini-app authorization flow to generate a fresh state bound to the current session
  2. Check that the frontend forwards the full callback query (including state) unchanged to the backend
  3. Verify DouyinMiniAppCallbackSchema's state field still matches what the Douyin flow actually sends
Defensive patterns

Strategy: validation

Validate before calling

const state = callbackPayload?.state
if (typeof state !== 'string' || state.length === 0) {
  throw new Error('Missing or invalid OAuth state parameter')
}

Type guard

function hasValidState(payload: unknown): payload is { state: string } {
  return typeof payload === 'object' && payload !== null && typeof (payload as any).state === 'string' && (payload as any).state.length > 0
}

Try / catch

try {
  await provider.miniAppCallback(payload)
} catch (e) {
  if (e instanceof AppException && e.code === 'ChannelAuthCsrfInvalid') {
    return { restartAuth: true, message: 'Authorization session expired or invalid, please retry' }
  }
  throw e
}

Prevention

When it happens

Trigger: Douyin mini-app callback POST whose state is absent, malformed, or fails assertParsedCallbackState-shaped schema checks (e.g. tampered query params, callback replayed from a different session).

Common situations: User bookmarked/refreshed the callback URL, session expired and state no longer parses, a bot hits the callback endpoint with junk parameters, or payload structure changed after a Douyin SDK/flow update.

Related errors


AI-assisted analysis of yikart/AiToEarn@d3aa8bea5b (2026-08-31). Data as JSON: /api/errors/f4f919babf2049aa. Report an issue: GitHub.