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
- In the thirdweb dashboard, on the project's client ID, set Access Restrictions → Bundle IDs to a specific value (uncheck "Allow all bundle IDs").
- In a React Native app, call:
import { preAuthenticate } from "thirdweb/wallets/in-app";
await preAuthenticate({ client, strategy: "email", email: "user@example.com" });
- Request fails with
401 UNAUTHORIZED – The keys are invalid.
- Inspect the request headers — only
content-type and x-client-id are present; x-bundle-id is missing.
- Re-check "Allow all bundle IDs" → request succeeds with
200.
Root cause
thirdweb/dist/esm/wallets/in-app/web/lib/auth/otp.js — sendOtp 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.js → authFetchEmbeddedWalletUser, 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.
Summary
sendOtp/verifyOtpfor in-app wallet email & phone authentication issue a barefetch()instead of going throughgetClientFetch(). As a result the requests toembedded-wallet.thirdweb.com/api/2024-05-05/login/email(and.../login/email/callback) carry onlyContent-Typeandx-client-id— nox-bundle-id, nox-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 on5.119.4)thirdweb/react-nativestrategy: "email"(same applies to"phone")Steps to reproduce
401 UNAUTHORIZED – The keys are invalid.content-typeandx-client-idare present;x-bundle-idis missing.200.Root cause
thirdweb/dist/esm/wallets/in-app/web/lib/auth/otp.js—sendOtpandverifyOtp:verifyOtpis identical (POST to.../login/email/callback).The bundle ID is only attached by
getPlatformHeaders()(inutils/fetch.js), which is invoked exclusively fromgetClientFetch():Because
sendOtp/verifyOtpnever callgetClientFetch(),x-bundle-idis never sent, even when the bundle ID is correctly available viaglobalThis.Application.applicationId.Note that other in-app wallet calls (
wallets/in-app/native/helpers/api/fetchers.js→authFetchEmbeddedWalletUser,verifyClientId) do usegetClientFetch()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/verifyOtpshould send the same identification headers as every other thirdweb service request — i.e. route throughgetClientFetch()sox-bundle-id(andx-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 globalfetch:getClientFetchalready setsx-client-idand the ecosystem headers, so the manualheadersobject can be slimmed down to justContent-Typeif 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.