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
- 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
- If internal access is not intended, resend the request with a valid x-api-key or Bearer JWT instead
- Check that no proxy/gateway middleware strips or modifies the Authorization header
- 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
- Load the internal token from one shared secret/config source on all services
- Rotate internal tokens with coordinated rolling deploys
- Log token fingerprint (not the token) on mismatch to diagnose env drift
- Add an integration test that calls each service with the shared token
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
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Unauthorized
- userId is required
- ChannelAuthRefreshTokenMissing
- ChannelAccessTokenFailed
- ChannelRefreshTokenFailed
AI-assisted analysis of yikart/AiToEarn@d3aa8bea5b (2026-08-31).
Data as JSON: /api/errors/a4d7f868027c6325.
Report an issue: GitHub.