yikart/AiToEarn · error · AppException
ChannelAuthRefreshTokenMissing
ChannelAuthRefreshTokenMissing
Error message
ChannelAuthRefreshTokenMissing
What it means
Google Business credential refresh was attempted without a stored refresh token. OAuth refresh requires the long-lived refresh token obtained at initial consent; without it the provider cannot mint a new access token.
Source
Thrown at project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/google-business/google-business-auth.provider.ts:46
return { url, state: input.state, redirectUri: this.config.redirectUri }
}
async exchangeCode(input: AuthCallbackInput): Promise<CredentialResult> {
const callback = parseOAuthCallback(input)
const result = await this.googleBusinessService.exchangeCode(callback.code)
return {
accessToken: result.accessToken,
refreshToken: result.refreshToken,
expiresAt: result.expiresAt,
scope: result.scope,
}
}
async refresh(input: RefreshCredentialInput): Promise<CredentialResult> {
if (!input.refreshToken) {
throw new AppException(ResponseCode.ChannelAuthRefreshTokenMissing)
}
const result = await this.googleBusinessService.refreshAccessToken(input.refreshToken)
return {
accessToken: result.accessToken,
refreshToken: result.refreshToken ?? input.refreshToken,
expiresAt: result.expiresAt,
scope: result.scope,
}
}
async revoke(input: RevokeCredentialInput): Promise<void> {
await this.googleBusinessService.revokeToken(input.accessToken)
}
async getProfile(input: CredentialContext): Promise<PlatformAccountProfile> {
const userInfo = await this.googleBusinessService.getUserInfo(input.accessToken)View on GitHub (pinned to d3aa8bea5b)
Solutions
- Re-run the Google OAuth connect flow with access_type=offline&prompt=consent to obtain and store a refresh token
- Check the credential storage/migration didn't drop the refreshToken field
- Ensure the auth callback persists result.refreshToken before responding
- Verify the Google Cloud app is in production (or the user is a test user) so refresh tokens aren't expiring in 7 days
Example fix
// before
await credsRepo.save({ accessToken: result.accessToken })
// after
await credsRepo.save({ accessToken: result.accessToken, refreshToken: result.refreshToken }) Defensive patterns
Strategy: validation
Validate before calling
if (!credential.refreshToken) {
throw new Error('No Google refresh token stored; user must reconnect the Google Business channel')
} Type guard
function hasRefreshToken(c: { refreshToken?: string | null }): c is { refreshToken: string } {
return typeof c.refreshToken === 'string' && c.refreshToken.length > 0
} Try / catch
try {
await provider.refresh(input)
} catch (e) {
if (e.code === 'ChannelAuthRefreshTokenMissing') {
await markChannelNeedsReauth(accountId, 'google-business')
} else throw e
} Prevention
- Always request access_type=offline&prompt=consent in the Google OAuth URL
- Persist refreshToken in the same transaction as accessToken
- Audit credential records for null refreshToken columns
- Remember Google omits refresh_token on repeat consents unless prompt=consent
When it happens
Trigger: refresh() is called with input.refreshToken undefined/empty — the credential record was saved without offline access or the token field was lost.
Common situations: Initial OAuth consent omitted access_type=offline / prompt=consent so no refresh token was issued; stored credential was partially migrated or overwritten; Google only returns a refresh token on first consent and it was never persisted.
Related errors
- ChannelAuthRefreshTokenMissing
- ChannelRefreshTokenFailed
- ChannelAuthRefreshTokenMissing
- ChannelAuthPlatformUidMissing
- Twitter user profile not found
AI-assisted analysis of yikart/AiToEarn@d3aa8bea5b (2026-08-31).
Data as JSON: /api/errors/b262b41a39f479c0.
Report an issue: GitHub.