Introduction

This documentation will go over SMS Check's architecture, as well as how to use the API.

PLEASE NOTE: When your clients authenticate for the first time, our system securely stores their information. This offers two key benefits:

  • Convenience for your clients: They only need to complete SMS verification once to access your site indefinitely.
  • Cost savings for you: You'll reduce expenses on SMS verifications.

Additionally, this system helps prevent multiple accounts per user. Once a phone number is verified and associated with an account, that same number cannot be used to create additional accounts. It's important to note that while this approach enhances user experience and security, it is not currently suitable for Single Sign-On (SSO) implementations. However, we may add SSO support in the future if there's sufficient demand from our users.

Authentication Flow

The authentication architecture is based on signed JWTs with GET requests only, as depicted below:

Authentication Flow

When the user sends a request to a protected page:

  1. Your server will redirect the user to a signed SMS check URL. Tokens are signed with a secret key, which you can generate in the configure sites page.
  2. The user will either be:
    • Not yet authenticated, and will authenticate using SMS verification.
    • Already authenticated from a previous session, and will be redirected to the protected page.
  3. If authentication succeeds, the user will be redirected to the protected page. If authentication fails, the user will be redirected to a failed page you specify.
    • All attempts will be logged for your reference.
    • If the user is already authenticated, the user will be redirected to the protected page.

To Auth Server

Redirects client to an SMS authentication session. Signed with a secret key and HS256 algorithm. Below is implemented in a Django standalone view for ease of understanding. Decorators are recommended (see "Django Decorator Example" section below) for ease of implementation. For backends like Express.js, there is already a reusable middleware function that can be used to protect your views.

Endpoint
auth/phone_auth/?token=<your_token>&domain=<your_domain>
Token Parameters
Name Type Description Required
unique_user_identifier string (512 characters max) An identifier that uniquely identifies your user. User ID is recommended. Identifying information NOT recommended. Yes
failed_url string The URL to redirect the user to if authentication fails. Yes
gated_url string The URL to redirect the user to after authentication. Yes
exp integer The expiration time of the token, in seconds since the Unix epoch. Yes
GET Parameters
Name Type Description Required
token string Token with above token parameters payload, signed with a secret key and HS256 algorithm. Yes
domain string The domain of your website. This is required to verify the token. Must include protocol (https://). Yes
Request example
Copied!
const express = require('express');
const jwt = require('jsonwebtoken');
const dotenv = require('dotenv');

dotenv.config();

const router = express.Router();
const secret = process.env.YOUR_SECRET_KEY;

function initiatePhoneVerification(req, res) {
  // Get the protected URL
  const protectedUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
  const authFailedUrl = 'YOUR_AUTH_FAILED_URL';

  // Prepare the payload
  const exp = Math.floor(Date.now() / 1000) + 5 * 60; // recommended 5 mins, or less.
  const payload = {
    unique_user_identifier: req.user ? req.user.id : 'anonymous',
    failed_url: authFailedUrl,
    gated_url: protectedUrl,
    exp: exp,
  };

  // Create the JWT token
  const token = jwt.sign(payload, secret, { algorithm: 'HS256' });

  const domain = 'https://yourwebsite.com'; // MUST include protocol (https://)

  // Redirect to the SMS check service
  res.redirect(`https://mysmscheck.com/auth/phone_auth/?token=${token}&domain=${domain}`);
}

From Auth Server

Triggered on successful authentication, and redirects the client back to your app's protected / gated page.

Endpoint
{gated_url}?token=<your_token>
Token Parameters
Name Type Description Required
success boolean True when the user has successfully authenticated. Always true as this is only triggered on successful authentication. Yes
unique_user_identifier string (512 characters max) An identifier that uniquely identifies your user. The same one that you used to generate the auth session. Yes
GET Parameters
Name Type Description Required
token string Token with above token parameters payload, signed with your site's secret key and HS256 algorithm. Yes
Request example
Copied!

const express = require('express');
const jwt = require('jsonwebtoken');

const router = express.Router();

// Middleware to verify JWT token
const protectedRouteAuthMiddleware = (req, res, next) => {
  const token = req.query.token;
  const AUTH_FAILED_URL = '/your-auth-failed-url';

  if (!token) {
    return initiatePhoneVerification(req, res);
  }

  try {
    const payload = jwt.verify(token, process.env.YOUR_SECRET_KEY, { algorithms: ["HS256"] });
    
    if (!payload.success || !payload.unique_user_identifier) {
      return res.redirect(AUTH_FAILED_URL);
    }

    req.user = payload;
    next();
  } catch (error) {
    if (error instanceof jwt.TokenExpiredError) {
      return initiatePhoneVerification(req, res);
    } else {
      return res.redirect(AUTH_FAILED_URL);
    }
  }
};

// Protected route handler
const protectedRouteHandler = (req, res) => {
  // Access to protected route granted
  res.json({ 
    message: 'Access to protected route granted',
    user: req.user.unique_user_identifier
  });
};

// Mount the route with middleware
router.get('/your/protected/route', protectedRouteAuthMiddleware, protectedRouteHandler);

Django Decorator Example

This example shows how to use an example @phone_verify decorator to protect a view.

Decorator File

Create a phone_verify decorator in your your/app/path/decorators.py. Use of initiate_phone_verification below is identical to the above example.

Copied!
BYPASS_IDS = ("123",)
def phone_verify(view_func):
    from functools import wraps
    @wraps(view_func)
    def wrapper(request, *args, **kwargs):
        if not request.user.is_authenticated or (request.user.is_authenticated and str(request.user.id) in BYPASS_IDS):
            return view_func(request, *args, **kwargs)
        token = request.GET.get("token")

        # if no token, send to auth server.
        if not token:
            return initiate_phone_verification(request)

        # get secret to decode token.
        try:
            # Decode and verify the token
            payload = jwt.decode(token, secret, algorithms=["HS256"])

            # Check if verification was successful
            if not payload.get("success") or not payload.get("unique_user_identifier"):
                return HttpResponseRedirect(YOUR_AUTH_FAILED_URL)

            # If everything is okay, proceed to the view
            return view_func(request, *args, **kwargs)

        except jwt.ExpiredSignatureError:
            # Token has expired, initiate new verification
            return initiate_phone_verification(request)
        except jwt.InvalidTokenError:
            # Invalid token, redirect to authentication failed page
            return HttpResponseRedirect(YOUR_AUTH_FAILED_URL)
        
    return wrapper
Using Your Decorator

Once you have your decorator, just attach to any view you want to protect.

Copied!
from your.app.path.decorators import phone_verify

@phone_verify
def your_protected_view(request):
...

@phone_verify
def create_stripe_checkout_session(request):
...

@phone_verify
def another_protected_view(request):
...