yikart/AiToEarn · error · UnauthorizedException

Unauthorized

Error message

Unauthorized

What it means

The AitoearnAuthGuard's canActivate throws UnauthorizedException when a request presents the internal-token path (isInternal) but the extracted token does not equal options.internalToken. It means internal service-to-service authentication failed before API key or JWT resolution is attempted.

Source

Thrown at project/aitoearn-backend/libs/aitoearn-auth/src/aitoearn-auth.guard.ts:41

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ])
    const isInternal = this.reflector.getAllAndOverride<boolean>(IS_INTERNAL_KEY, [
      context.getHandler(),
      context.getClass(),
    ])

    const request = context.switchToHttp().getRequest()

    if (isInternal) {
      const token = this.extractTokenFromHeader(request)
      if (token === this.options.internalToken) {
        return true
      }
      throw new UnauthorizedException()
    }

    // 1. API Key 认证(x-api-key,既有行为,对所有路由生效)
    const apiKey = request.headers['x-api-key'] as string | undefined
    if (apiKey) {
      await this.resolveApiKey(request, apiKey)
      return true
    }

    // 2. 额外声明的 header(@ApiKeyHeader)按 API Key 解析
    const apiKeyHeader = this.reflector.getAllAndOverride<string>(API_KEY_HEADER_KEY, [
      context.getHandler(),
      context.getClass(),
    ])
    if (apiKeyHeader) {
      // Authorization 特殊处理:优先按 JWT 校验,失败再兜底按 API Key 解析
      if (apiKeyHeader.toLowerCase() === 'authorization') {
        const token = this.extractTokenFromHeader(request)

View on GitHub (pinned to d3aa8bea5b)

Solutions

  1. Compare the token sent by the caller against the internalToken configured in the receiving service's options; make both read the exact same env var
  2. If internal access is not intended, resend the request with a valid x-api-key or Bearer JWT instead
  3. Check that no proxy/gateway middleware strips or modifies the Authorization header
  4. Rotate/regenerate the shared internal token and redeploy both sides together

Example fix

// before (.env of caller)
INTERNAL_TOKEN=staging-token
// after
INTERNAL_TOKEN=same-value-as-receiver-options.internalToken
Defensive patterns

Strategy: validation

Validate before calling

const token = extractBearer(req.headers.authorization)
if (!token || token !== process.env.INTERNAL_TOKEN) {
  throw new Error('Internal token mismatch before calling API')
}

Type guard

function hasValidInternalToken(authHeader: unknown, expected: string): boolean {
  return typeof authHeader === 'string' && authHeader === `Bearer ${expected}`
}

Try / catch

try {
  await internalCall(url, { headers: { Authorization: `Bearer ${process.env.INTERNAL_TOKEN}` } })
} catch (e) {
  if (e.response?.status === 401) {
    // refresh shared token from config service and retry once
  }
  throw e
}

Prevention

When it happens

Trigger: A request routed as internal (isInternal true) carries a missing, malformed, or stale internal token in the Authorization header, or the caller's token was generated for a different environment than the one configured via options.internalToken.

Common situations: Environment variable holding the internal token differs between the calling service and the receiving service (e.g. staging vs prod token), the token was rotated on one side only, or a gateway/proxy strips or rewrites the Authorization header.

Understand the failure class

Related errors


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