Back to guides

Phone login with SMS codes


auth.requestSmsOtp sends a short-lived code through the configured SMS provider. For phone login, auth.verifySmsOtp consumes it and returns a session or an MFA challenge.

Configure the SMS provider

Pass the send function from the application's SMS service when the backend creates its Own Auth instance.

auth.ts
import { createOwnAuth, type SmsProvider } from "own-auth";

type SendText = (input: {
  to: string;
  body: string;
}) => Promise<void>;

export function createAuth(sendText: SendText) {
  const smsProvider: SmsProvider = {
    async send({ to, code }) {
      await sendText({
        to,
        body: `Your sign-in code is ${code}`,
      });
    },
  };

  return createOwnAuth({
    tokenPepper: process.env.OWN_AUTH_TOKEN_PEPPER,
    smsProvider,
  });
}

Request the code

request-phone-login.ts
await auth.requestSmsOtp({ phone });

return {
  message: "If that number can sign in, a code is on its way.",
};

Verify the code

verify-phone-login.ts
export async function verifyPhoneLogin(phone: string, code: string) {
  const result = await auth.verifySmsOtp({
    phone,
    code,
  });

  if (result.status === "mfa_required") {
    return {
      status: result.status,
      challengeToken: result.challengeToken,
      methods: result.methods,
      expiresAt: result.expiresAt,
    };
  }

  if (result.status === "verified") {
    throw new Error("Unexpected non-login SMS result");
  }

  return {
    status: result.status,
    userId: result.user.id,
    sessionToken: result.sessionToken,
    expiresAt: result.session.expiresAt,
  };
}

Own Auth normalizes the phone number, stores a protected code hash, limits requests and verification attempts, and consumes the code after successful verification. A complete result contains the new aal1 session; an account with another factor returns mfa_required.