SMSTunnel for Auth0
MFA and passwordless SMS codes from your own phone
@narcisbodea/smstunnel-otp Your codes, your number
Auth0 can hand its phone messages to an Action. With this package, the Action passes them to your SMSTunnel account and your own Android phone sends them from its own SIM, so people get their code from a number they know. One provider covers MFA, passwordless SMS and phone sign-up.
Before you start
- 1. Install the SMSTunnel app from Google Play on your Android phone and sign in.
- 2. In your dashboard, open Apps → Add an app, pick Sign-in codes, name it (for example "Auth0") and click Create the key.
- 3. Copy the key right away: it's shown only once. You can stop or delete the app there anytime, and Auth0 can't send texts anymore.
Get the steps with your key in them
It opens Add an app with Sign-in codes picked. Once the key is made, the dashboard shows every step and command with the key already filled in, ready to copy.
Keep the key in your platform's secrets, never in code that runs in the browser. Codes must be 4 to 10 digits: the default 6-digit codes are fine; codes with letters aren't accepted.
1. Custom Phone Provider (recommended)
- 1. Branding → Phone Provider → Custom, delivery method Text.
- 2. In the Action editor, add the dependency
@narcisbodea/smstunnel-otpand the secretSMSTUNNEL_API_KEY, then paste the code below. - 3. Save, then use Send Test Message.
- 4. For MFA, also turn on Use Tenant-Level Messaging Provider under Security → Multi-factor Auth → Phone Message.
const { createAuth0CustomPhoneProvider } = require('@narcisbodea/smstunnel-otp');
const sendWithSmsTunnel = createAuth0CustomPhoneProvider();
exports.onExecuteCustomPhoneProvider = async (event, api) => {
await sendWithSmsTunnel(event, api);
}; -
Codes (
otp_verify,otp_enroll) go through the code endpoint, using the text Auth0 rendered: your template and the user's language. Pass{ template: 'Your Acme code is {code}' }tocreateAuth0CustomPhoneProvider()to write your own. -
Notices without a code (
blocked_account,change_password,password_breach) are sent as regular texts. Pass{ nonCodeMessages: 'skip' }to ignore them. -
Failures are reported to Auth0 as documented. Rate limits and network errors call
api.notification.retry(reason), which Auth0 retries up to 5 times. A bad key or a bad number callsapi.notification.drop(reason). - SMSTunnel sends text messages only. A voice delivery is dropped with a clear reason; set the delivery method to Text.
- Rather not add a dependency? custom-phone-provider.no-dependency.js does the same in about twenty lines.
2. Or: Send Phone Message (MFA only)
Security → Multi-factor Auth → Phone Message, delivery method SMS, provider Custom. Same dependency and secret, then:
const { createAuth0SendPhoneMessage } = require('@narcisbodea/smstunnel-otp');
const sendWithSmsTunnel = createAuth0SendPhoneMessage();
exports.onExecuteSendPhoneMessage = async (event, api) => {
await sendWithSmsTunnel(event, api);
};
It throws when the code can't be sent, so Auth0 logs the failure; a voice message
throws a clear error. Optional secrets for both triggers: SMSTUNNEL_DEVICE_ID (send
from one phone of your account) and SMSTUNNEL_SERVER_URL (self-hosted SMSTunnel).
Worth knowing
- Codes have their own allowance. They go through SMSTunnel's code endpoint,
POST /api/v1/sms/send-2fa, which counts them apart from your other texts. See Pricing. - What counts as a code: 4 to 10 digits standing on their own, like
482913, or two groups of 3 digits, like123-456. A text without one is refused. Details - One number per text, up to 320 characters. The package refuses a longer text before calling SMSTunnel.
- 30 sends a minute. Past that, SMSTunnel answers 429 and asks to retry a little later.
- Phone offline? The code waits and goes out as soon as the phone is back.
- Nothing in the package logs codes or keys.
Build it with an AI assistant
Copy this prompt into Claude, ChatGPT, Cursor or Copilot and replace the part in [brackets]. Don't paste your API key into the chat: the prompt doesn't need it.
Help me send my Auth0 tenant's SMS codes (MFA, passwordless SMS, phone sign-up) from my own phone, with SMSTunnel.
About SMSTunnel (https://smstunnel.io): my own apps and tools hand text messages (SMS) to my SMSTunnel account, and my own Android phone sends them from my own number, on my own SIM and plan. The phone runs the SMSTunnel app from Google Play (https://play.google.com/store/apps/details?id=io.smstunnel.app), signed in to my account.
What I want: [describe what should send a text, to whom, and when]
How it works:
- Use the npm package @narcisbodea/smstunnel-otp in an Auth0 Action. Recommended: the custom-phone-provider trigger (one provider for MFA, passwordless SMS and phone sign-up). The older send-phone-message trigger, for MFA only, works too.
- Custom Phone Provider: Branding → Phone Provider → Custom, delivery method Text. In the Action editor, add the dependency @narcisbodea/smstunnel-otp and the secret SMSTUNNEL_API_KEY, then:
const { createAuth0CustomPhoneProvider } = require('@narcisbodea/smstunnel-otp');
const sendWithSmsTunnel = createAuth0CustomPhoneProvider();
exports.onExecuteCustomPhoneProvider = async (event, api) => {
await sendWithSmsTunnel(event, api);
};
- Then Save and use Send Test Message. For MFA, also turn on "Use Tenant-Level Messaging Provider" under Security → Multi-factor Auth → Phone Message.
- Codes (otp_verify, otp_enroll) are sent with the text Auth0 rendered (my template, the user's language); createAuth0CustomPhoneProvider({ template: 'Your Acme code is {code}' }) writes my own text. Notices without a code (blocked_account, change_password, password_breach) go out as regular texts; { nonCodeMessages: 'skip' } ignores them.
- Failures: rate limits and network errors call api.notification.retry(reason) (Auth0 retries up to 5 times); a bad key or a bad number calls api.notification.drop(reason). Voice delivery is not possible: SMSTunnel sends text messages only.
- Send Phone Message (MFA only): Security → Multi-factor Auth → Phone Message, delivery method SMS, provider Custom, same dependency and secret, with createAuth0SendPhoneMessage() and exports.onExecuteSendPhoneMessage. It throws when the code can't be sent, so Auth0 logs the failure.
- Optional Action secrets: SMSTUNNEL_DEVICE_ID (send from one phone of my account) and SMSTUNNEL_SERVER_URL (self-hosted SMSTunnel).
- Codes go through SMSTunnel's POST https://smstunnel.io/api/v1/sms/send-2fa: the text must contain the code as 4 to 10 digits and can be up to 320 characters. Codes have their own daily allowance. If the phone is offline, the code waits and goes out when it is back.
- The API key comes from my SMSTunnel dashboard (https://dashboard.smstunnel.io): Apps → Add an app → Create the key. It is shown only once and can send texts and check whether they went out.
Rules for your answer:
- Keep the API key in an environment variable or the platform's secret store (for example SMSTUNNEL_API_KEY). Never put it in source code, in a repository, or in code that runs in a browser or a mobile app: call SMSTunnel from a server. I will not paste my key in this chat; use a placeholder.
- Validate phone numbers before sending (international format, like +14155550123) and skip or report the invalid ones.
- Handle errors: log or show what went wrong, retry only after 429, 5xx or network errors and only a few times, and never send the same text twice.
- Only send texts people expect from me: alerts to myself, reminders, sign-in codes, updates they asked for. No unsolicited texts. Auth0 is a trademark of its owner, Okta. SMSTunnel is a third-party integration and is not affiliated with, endorsed by, or sponsored by Auth0 or Okta.
Sign-in codes for other platforms
Another platform? sendCode() from the same package
sends any code: see its README.