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:
When the user sends a request to a protected page:
- 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.
- 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.
- 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!
import jwt from datetime import datetime, timedelta import os from dotenv import load_dotenv load_dotenv() from django.http import HttpResponseRedirect secret = os.getenv("YOUR_SECRET_KEY") domain = "https://yourwebsite.com" # MUST include protocol (https://) YOUR_AUTH_FAILED_URL = "https://yourwebsite.com/403/" def initiate_phone_verification(request): # Get the protected URL protected_url = request.build_absolute_uri() auth_failed_url = YOUR_AUTH_FAILED_URL # Prepare the payload exp = datetime.utcnow() + timedelta(minutes=5) # recommended 5 mins, or less. exp = int(exp.timestamp()) # Unix timestamp in seconds payload = { "unique_user_identifier": ( request.user.id if request.user.is_authenticated else "anonymous" ), "failed_url": auth_failed_url, "gated_url": protected_url, "exp": exp, } # sign the payload with secret / HS256 algorithm token = jwt.encode(payload, secret, algorithm="HS256") return HttpResponseRedirect(f"https://mysmscheck.com/auth/phone_auth/?token={token}&domain={domain}")
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!
import jwt from datetime import datetime, timedelta import os from dotenv import load_dotenv load_dotenv() def your_protected_view(request): token = request.GET.get("token") if not token: return initiate_phone_verification(request) secret = os.getenv("YOUR_SECRET_KEY") 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(auth_failed_url) # If auth has passed, proceed to the protected view. return render(request, ...) except jwt.ExpiredSignatureError: # Token has expired, initiate new verification return initiate_phone_verification(request) except jwt.InvalidTokenError: print(f"Invalid token: {token}") print(f"Payload: {payload}") # Invalid token, redirect to authentication failed page return HttpResponseRedirect(auth_failed_url)
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 youryour/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): ...