calcom/cal.diy · error · UnauthorizedException

No valid credentials available for Google Calendar

Error message

No valid credentials available for Google Calendar

What it means

Thrown by getAuthorizedCalendarInstance when neither delegated auth nor direct OAuth can produce a Google Calendar client. The method first attempts service-account delegation (getDelegatedCalendarInstance) when a userEmail and delegationCredential.id are supplied; if that returns null, it falls through and requires oAuthCredentials on the credential record. When credential.key is missing/empty the UnauthorizedException (HTTP 401) fires.

Source

Thrown at apps/api/v2/src/modules/cal-unified-calendars/services/google-calendar.service.ts:121

  /**
   * Gets an authorized Google Calendar instance
   * Tries delegation credentials first, falls back to direct OAuth
   */
  private async getAuthorizedCalendarInstance(
    userEmail?: string,
    oAuthCredentials?: Prisma.JsonValue | undefined,
    delegationCredential?: { id: string } | null
  ): Promise<calendar_v3.Calendar> {
    if (userEmail && delegationCredential?.id) {
      const delegatedCalendar = await this.getDelegatedCalendarInstance(delegationCredential, userEmail);
      if (delegatedCalendar) {
        return delegatedCalendar;
      }
    }

    // Fall back to direct OAuth authentication
    if (!oAuthCredentials) {
      throw new UnauthorizedException("No valid credentials available for Google Calendar");
    }
    const parsedOAuthCredentials = OAuth2UniversalSchema.parse(oAuthCredentials);
    const oAuth2Client = await this.gCalService.getOAuthClient(this.gCalService.redirectUri);
    oAuth2Client.setCredentials(parsedOAuthCredentials);

    return new calendar_v3.Calendar({ auth: oAuth2Client });
  }

  private async getDelegatedCalendarInstance(
    delegationCredential: { id: string },
    emailToImpersonate: string
  ): Promise<calendar_v3.Calendar | null> {
    try {
      const oauthClientIdAliasRegex = /\+[a-zA-Z0-9]{25}/;
      const cleanEmail = emailToImpersonate.replace(oauthClientIdAliasRegex, "");

      const serviceAccountCreds =
        await DelegationCredentialRepository.findByIdIncludeSensitiveServiceAccountKey({

View on GitHub (pinned to 176037d0af)

Solutions

  1. Inspect the credential row (SELECT key, "delegationCredentialId", invalid FROM "AppCredential" WHERE id=?) and confirm whether key is populated or delegationCredentialId points to a live DelegationCredential with a serviceAccountKey.
  2. If the user is meant to use delegation, ensure the DelegationCredential row has a complete serviceAccountKey (client_email + private_key); otherwise re-run domain-wide delegation setup.
  3. If the user should use direct OAuth, have them reconnect Google Calendar so credential.key is populated with a valid OAuth token JSON.
  4. As a caller, pre-check that either credential.key is truthy or (credential.user.email and credential.delegationCredentialId) are set before invoking calendar operations.

Example fix

// before: caller blindly calls the service
const cal = await googleCalendarService.getCalendarClientForUser(userId);

// after: guard before calling
const cred = await credentialsRepository.findCredentialWithDelegationByTypeAndUserId(GOOGLE_CALENDAR_TYPE, userId);
if (!cred || cred.invalid || (!cred.key && !(cred.user?.email && cred.delegationCredentialId))) {
  throw new UnauthorizedException('Reconnect Google Calendar before proceeding.');
}
Defensive patterns

Strategy: validation

Validate before calling

// Validate a credential can yield a calendar client before calling the service.
function canAuthorizeCredential(cred: { key: unknown; user?: { email?: string | null }; delegationCredentialId?: string | null; invalid?: boolean }): boolean {
  if (cred.invalid) return false;
  const hasOAuth = Boolean(cred.key);
  const hasDelegation = Boolean(cred.user?.email && cred.delegationCredentialId);
  return hasOAuth || hasDelegation;
}

// usage
const cred = await credentialsRepository.findCredentialWithDelegationByTypeAndUserId(GOOGLE_CALENDAR_TYPE, userId);
if (!cred || !canAuthorizeCredential(cred)) {
  throw new UnauthorizedException('Reconnect Google Calendar before proceeding.');
}

Type guard

import type { AppCredential } from '@prisma/client';

function hasUsableOAuthKey(cred: AppCredential): boolean {
  return cred.key != null && typeof cred.key === 'object' && Object.keys(cred.key).length > 0;
}

function hasDelegationContext(cred: AppCredential & { user?: { email?: string | null } }): boolean {
  return Boolean(cred.delegationCredentialId && cred.user?.email);
}

function isCalendarClientReady(cred: AppCredential & { user?: { email?: string | null } }): cred is AppCredential & { user: { email: string } } {
  return !cred.invalid && (hasUsableOAuthKey(cred) || hasDelegationContext(cred));
}

Try / catch

try {
  const cal = await googleCalendarService.getCalendarClientForUser(userId);
} catch (e) {
  if (e instanceof UnauthorizedException && e.message.includes('No valid credentials')) {
    // prompt reconnect; this is unrecoverable without user action
    return { status: 'reconnect_required', provider: 'google_calendar' };
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling getEventDetails/updateEventDetails with a booking reference whose credential.key is null, or calling getCalendarClientForUser/getCalendarClientByCredentialId for a credential that has a delegationCredentialId pointing at a delegation record whose serviceAccountKey is missing (getDelegatedCalendarInstance swallows the error and returns null) and whose own key field is also empty.

Common situations: A credential row was created for delegation but the service-account JSON was never stored; the delegation credential was deleted out of band but delegationCredentialId on the credential still points at it; a migration left credential.key null for users moved to domain-wide delegation.

Related errors


AI-assisted analysis of calcom/cal.diy@176037d0af (2026-08-12). Data as JSON: /api/errors/dccc9ff6f78edced. Report an issue: GitHub.