Auth1 Blog Passwordless Auth in 5 Minutes
Tutorial Quick Start · 5 min read

How to Build Passwordless Auth
in 5 Minutes

No password database. No bcrypt. No "forgot password" flows. Just phone number verification via SMS OTP. Five steps. Working code. Ship it today.

What You'll Build

By the end of this tutorial, you'll have a working passwordless authentication system where users sign in with their phone number. The flow is simple:

  1. User enters their phone number
  2. Auth1 sends them a 6-digit OTP via SMS
  3. User enters the code
  4. Your app receives a JWT token and user ID
  5. User is authenticated

No passwords stored. No password reset emails. No credential stuffing attacks. The user's phone is their identity. Auth1 handles SMS delivery, VOIP blocking, rate limiting, and fraud detection automatically.


Step 1: Sign Up and Get an API Key

1

Create your Auth1 account

Go to auth1.ai/signup and create a free account. You'll get an API key that looks like this:

Text Your API key
auth1_pk_yourapp_a1b2c3d4e5f6g7h8i9j0...

The free tier gives you 1,000 verifications per month. No credit card required.


Step 2: Install the SDK

2

Add Auth1 to your project

Shell Terminal
npm install @auth1/sdk

Or use the REST API directly — no SDK required. Every endpoint is a standard HTTP POST with JSON. The SDK is a thin wrapper for convenience.


Step 3: Backend (Express)

3

Add auth routes to your Express app

Create two routes: one to request an OTP and one to verify it. This is the complete backend code:

JavaScript server.js
const express = require('express');
const { Auth1Client } = require('@auth1/sdk');
const cookieParser = require('cookie-parser');

const app = express();
app.use(express.json());
app.use(cookieParser());

// Initialize Auth1 client
const auth1 = new Auth1Client({
  apiKey: process.env.AUTH1_API_KEY,
  baseUrl: 'https://auth-api.z101.ai'
});

// ---- Route 1: Request OTP ----
app.post('/api/auth/request', async (req, res) => {
  try {
    const { phone } = req.body;

    const result = await auth1.requestOTP({ phone });

    res.json({
      success: true,
      message: 'Verification code sent'
    });
  } catch (err) {
    res.status(err.status || 500).json({
      error: err.message
    });
  }
});

// ---- Route 2: Verify OTP ----
app.post('/api/auth/verify', async (req, res) => {
  try {
    const { phone, code } = req.body;

    const result = await auth1.verifyOTP({ phone, code });

    if (result.verified) {
      // Set httpOnly cookie with the JWT token
      res.cookie('auth_token', result.token, {
        httpOnly: true,
        secure: process.env.NODE_ENV === 'production',
        sameSite: 'lax',
        maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
      });

      res.json({
        verified: true,
        user_id: result.user_id,
        is_new_user: result.is_new
      });
    } else {
      res.status(401).json({ error: 'Invalid code' });
    }
  } catch (err) {
    res.status(err.status || 500).json({
      error: err.message
    });
  }
});

// ---- Route 3: Get current user ----
app.get('/api/auth/me', async (req, res) => {
  const token = req.cookies.auth_token;
  if (!token) {
    return res.status(401).json({ error: 'Not authenticated' });
  }

  try {
    const user = await auth1.getUser(token);
    res.json(user);
  } catch (err) {
    res.status(401).json({ error: 'Invalid token' });
  }
});

// ---- Route 4: Logout ----
app.post('/api/auth/logout', (req, res) => {
  res.clearCookie('auth_token');
  res.json({ success: true });
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

That's the entire backend. Four routes, ~60 lines. Auth1 handles OTP generation, SMS delivery, rate limiting, VOIP blocking, user creation, and JWT signing.


Step 4: Frontend

4

Build a simple login form

Here's a minimal React component (works with Next.js, Vite, or Create React App):

JSX LoginForm.jsx
import { useState } from 'react';

export default function LoginForm() {
  const [phone, setPhone] = useState('');
  const [code, setCode] = useState('');
  const [step, setStep] = useState('phone'); // 'phone' | 'code' | 'done'
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);

  async function requestOTP(e) {
    e.preventDefault();
    setLoading(true);
    setError('');

    const res = await fetch('/api/auth/request', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ phone })
    });

    const data = await res.json();
    setLoading(false);

    if (res.ok) {
      setStep('code');
    } else {
      setError(data.error);
    }
  }

  async function verifyOTP(e) {
    e.preventDefault();
    setLoading(true);
    setError('');

    const res = await fetch('/api/auth/verify', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ phone, code })
    });

    const data = await res.json();
    setLoading(false);

    if (data.verified) {
      setStep('done');
      // Redirect to dashboard or reload page
      window.location.href = '/dashboard';
    } else {
      setError(data.error || 'Invalid code');
    }
  }

  if (step === 'done') {
    return <p>Authenticated! Redirecting...</p>;
  }

  return (
    <div style={{ maxWidth: 400, margin: '100px auto', padding: 24 }}>
      <h1>Sign In</h1>

      {step === 'phone' ? (
        <form onSubmit={requestOTP}>
          <label>Phone Number</label>
          <input
            type="tel"
            placeholder="+1 (555) 123-4567"
            value={phone}
            onChange={e => setPhone(e.target.value)}
            required
          />
          <button type="submit" disabled={loading}>
            {loading ? 'Sending...' : 'Send Code'}
          </button>
        </form>
      ) : (
        <form onSubmit={verifyOTP}>
          <p>Enter the 6-digit code sent to {phone}</p>
          <input
            type="text"
            placeholder="000000"
            maxLength={6}
            value={code}
            onChange={e => setCode(e.target.value)}
            autoFocus
            required
          />
          <button type="submit" disabled={loading}>
            {loading ? 'Verifying...' : 'Verify'}
          </button>
          <button type="button" onClick={() => setStep('phone')}>
            Back
          </button>
        </form>
      )}

      {error && <p style={{ color: 'red' }}>{error}</p>}
    </div>
  );
}

Step 5: Deploy

5

Set the environment variable and ship

Shell .env
AUTH1_API_KEY=auth1_pk_yourapp_a1b2c3d4e5f6g7h8i9j0...

That's it. Deploy to Vercel, Railway, Render, or anywhere that runs Node.js. The AUTH1_API_KEY environment variable is the only configuration needed.

Shell Deploy to Vercel
# Set the environment variable
vercel env add AUTH1_API_KEY

# Deploy
vercel --prod

Alternative: Next.js App Router

If you're using Next.js 14+ with the App Router, here's the server-side implementation using Server Actions:

TypeScript app/auth/actions.ts
'use server';

import { cookies } from 'next/headers';

const AUTH1_URL = 'https://auth-api.z101.ai';
const API_KEY = process.env.AUTH1_API_KEY!;

export async function requestOTP(phone: string) {
  const res = await fetch(`${AUTH1_URL}/api/auth/request`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': API_KEY
    },
    body: JSON.stringify({ phone })
  });

  if (!res.ok) {
    const err = await res.json();
    throw new Error(err.message || 'Failed to send OTP');
  }

  return { success: true };
}

export async function verifyOTP(phone: string, code: string) {
  const res = await fetch(`${AUTH1_URL}/api/auth/verify`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': API_KEY
    },
    body: JSON.stringify({ phone, code })
  });

  const data = await res.json();

  if (data.verified) {
    // Set httpOnly cookie
    cookies().set('auth_token', data.token, {
      httpOnly: true,
      secure: true,
      sameSite: 'lax',
      maxAge: 60 * 60 * 24 * 7
    });
  }

  return data;
}

Without the SDK (Raw HTTP)

Don't want to install the SDK? The API is two HTTP calls:

Shell cURL
# 1. Request OTP
curl -X POST https://auth-api.z101.ai/api/auth/request \
  -H "Content-Type: application/json" \
  -H "x-api-key: auth1_pk_yourapp_..." \
  -d '{"phone": "+15551234567"}'

# Response: {"success": true, "message": "OTP sent"}

# 2. Verify OTP
curl -X POST https://auth-api.z101.ai/api/auth/verify \
  -H "Content-Type: application/json" \
  -H "x-api-key: auth1_pk_yourapp_..." \
  -d '{"phone": "+15551234567", "code": "482913"}'

# Response:
# {
#   "verified": true,
#   "token": "eyJhbGciOiJIUzI1NiIs...",
#   "user_id": "usr_abc123",
#   "is_new": true
# }

That's it. Two endpoints. Works from any language, any framework. Python, Go, Ruby, PHP — if it can make HTTP requests, it can use Auth1.

What You Get For Free

Every request through Auth1 automatically gets: VOIP phone blocking (no Google Voice signups), rate limiting (5 OTPs per hour per phone), 6-digit secure random OTP generation, 10-minute expiry, 5 attempt maximum, constant-time comparison, and one-time use (replay prevention). You don't configure any of this — it's on by default.

Build Passwordless Auth Today

Two API calls. Built-in fraud protection. No password database to secure. Free tier includes 1,000 verifications per month.

Get Free API Key → Read the Docs
Free tier · No credit card required · 5 minutes to production