Skip to content

In-app wallet email/phone OTP login bypasses getClientFetch — no x-bundle-id header, breaks Bundle ID allowlist #8774

@antondrob

Description

@antondrob

Summary

sendOtp / verifyOtp for in-app wallet email & phone authentication issue a bare fetch() instead of going through getClientFetch(). As a result the requests to embedded-wallet.thirdweb.com/api/2024-05-05/login/email (and .../login/email/callback) carry only Content-Type and x-client-id — no x-bundle-id, no x-sdk-* headers.

On React Native this makes the in-app wallet impossible to use with a Bundle ID access restriction: if the project's client ID has anything other than "Allow all bundle IDs", the OTP request is rejected with:

{ "message": "UNAUTHORIZED - The keys are invalid. Please check the secret-key/clientId and try again.", "code": "UNAUTHORIZED" }

The only workaround today is to disable bundle-ID restrictions entirely ("Allow all bundle IDs"), which defeats the purpose of the allowlist.

Environment

  • thirdweb: 5.120.0 (also reproduced on 5.119.4)
  • Platform: React Native (Expo), thirdweb/react-native
  • Auth: in-app wallet, strategy: "email" (same applies to "phone")

Steps to reproduce

  1. In the thirdweb dashboard, on the project's client ID, set Access Restrictions → Bundle IDs to a specific value (uncheck "Allow all bundle IDs").
  2. In a React Native app, call:
    import { preAuthenticate } from "thirdweb/wallets/in-app";
    
    await preAuthenticate({ client, strategy: "email", email: "user@example.com" });
  3. Request fails with 401 UNAUTHORIZED – The keys are invalid.
  4. Inspect the request headers — only content-type and x-client-id are present; x-bundle-id is missing.
  5. Re-check "Allow all bundle IDs" → request succeeds with 200.

Root cause

thirdweb/dist/esm/wallets/in-app/web/lib/auth/otp.jssendOtp and verifyOtp:

export const sendOtp = async (args) => {
  const { client, ecosystem } = args;
  const url = getLoginUrl({ authOption: args.strategy, client, ecosystem });
  const headers = {
    "Content-Type": "application/json",
    "x-client-id": client.clientId,
  };
  // ...ecosystem headers...
  const response = await fetch(url, {           // <-- bare fetch
    body: stringify(body),
    headers,
    method: "POST",
  });
  // ...
};

verifyOtp is identical (POST to .../login/email/callback).

The bundle ID is only attached by getPlatformHeaders() (in utils/fetch.js), which is invoked exclusively from getClientFetch():

// utils/fetch.js
for (const [key, value] of getPlatformHeaders()) {
  headers.set(key, value);
}
// getPlatformHeaders():
let bundleId;
if (typeof globalThis !== "undefined" && "Application" in globalThis) {
  bundleId = globalThis.Application.applicationId;
}
// ...
...(bundleId ? { "x-bundle-id": bundleId } : {})

Because sendOtp / verifyOtp never call getClientFetch(), x-bundle-id is never sent, even when the bundle ID is correctly available via globalThis.Application.applicationId.

Note that other in-app wallet calls (wallets/in-app/native/helpers/api/fetchers.jsauthFetchEmbeddedWalletUser, verifyClientId) do use getClientFetch() correctly. The OTP login path is the inconsistency.

The thirdweb backend does enforce the Bundle ID restriction on this endpoint — so the SDK and the backend disagree: the backend requires x-bundle-id, the SDK never sends it.

Expected behavior

sendOtp / verifyOtp should send the same identification headers as every other thirdweb service request — i.e. route through getClientFetch() so x-bundle-id (and x-sdk-*) are included. In-app wallet email/phone login should then work with Bundle ID access restrictions enabled.

Proposed fix

Route both functions through getClientFetch(client, ecosystem) instead of the global fetch:

import { getClientFetch } from "../../../../../utils/fetch.js";

// sendOtp
const response = await getClientFetch(client, ecosystem)(url, {
  body: stringify(body),
  headers,
  method: "POST",
});

// verifyOtp
const response = await getClientFetch(client, ecosystem)(url, {
  body: stringify(body),
  headers,
  method: "POST",
});

getClientFetch already sets x-client-id and the ecosystem headers, so the manual headers object can be slimmed down to just Content-Type if desired.

Impact

Any React Native / mobile app using in-app wallet email or phone OTP cannot enforce a Bundle ID allowlist on its client ID. Since the client ID ships publicly inside the app binary, the Bundle ID allowlist is the intended anti-abuse control — and it is currently unusable for this auth method.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions