calcom/cal.diy · error · UnauthorizedException

Invalid Access token.

Error message

Invalid Access token.

What it means

Thrown by StripeService.saveStripeAccount if the userId argument is falsy. This is a 401 Unauthorized defense-in-depth guard inside the service; the controller normally resolves userId from the access token and passes a real number, so reaching this throw means the service was called without a valid user.

Source

Thrown at apps/api/v2/src/modules/stripe/stripe.service.ts:102

  async getStripeAppKeys() {
    const app = await this.appsRepository.getAppBySlug("stripe");

    const { client_id, client_secret } = stripeKeysResponseSchema.parse(app?.keys);

    if (!client_id) {
      throw new NotFoundException("Stripe app not found");
    }

    if (!client_secret) {
      throw new NotFoundException("Stripe app not found");
    }

    return { client_id, client_secret };
  }

  async saveStripeAccount(state: OAuthCallbackState, code: string, userId: number): Promise<{ url: string }> {
    if (!userId) {
      throw new UnauthorizedException("Invalid Access token.");
    }

    const response = await stripeInstance.oauth.token({
      grant_type: "authorization_code",
      code: code?.toString(),
    });

    const data: StripeData = { ...response, default_currency: "" };
    if (response["stripe_user_id"]) {
      const account = await stripeInstance.accounts.retrieve(response["stripe_user_id"]);
      data["default_currency"] = account.default_currency;
    }

    const existingCredentials = await this.credentialsRepository.findAllCredentialsByTypeAndUserId(
      "stripe_payment",
      userId
    );

View on GitHub (pinned to 176037d0af)

Solutions

  1. Always resolve a non-zero userId (via getAccessTokenOwnerId) before calling saveStripeAccount.
  2. Route Stripe connect completion through the StripeController.save endpoint rather than calling the service directly.
  3. Type the service parameter as a branded/non-zero number so a falsy value is a compile error.

Example fix

// before
await stripeService.saveStripeAccount(state, code, maybeUserId);

// after
if (!userId) throw new UnauthorizedException('Sign in required');
await stripeService.saveStripeAccount(state, code, userId);
Defensive patterns

Strategy: validation

Validate before calling

async function resolveUserForStripe(tokensRepository, accessToken) {
  const userId = await tokensRepository.getAccessTokenOwnerId(accessToken);
  if (!userId) throw new UnauthorizedException('Sign in required');
  return userId;
}

Type guard

function isResolvedUserId(userId: number | null | undefined): userId is number {
  return typeof userId === 'number' && userId > 0;
}

Try / catch

try {
  await stripeService.saveStripeAccount(state, code, userId);
} catch (e) {
  if (e.status === 401 && /Invalid Access token/.test(e.message)) {
    // ensure caller resolves userId before invoking the service
  } else throw e;
}

Prevention

When it happens

Trigger: saveStripeAccount(state, code, userId) is invoked with userId = 0, undefined, or null. In normal flow the controller resolves a userId first; this guard fires only when the service is called directly or the controller passed a falsy value through.

Common situations: A code path calls saveStripeAccount directly (bypassing the controller) without resolving a user. A refactor changed the controller to pass an optional userId that can be undefined. A test invokes the service without a user fixture.

Related errors


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