Blog Post

Apps on Azure Blog
19 MIN READ

Building a TOTP Authenticator App on Azure Functions and Azure Key Vault

StephenMS's avatar
StephenMS
Icon for Microsoft rankMicrosoft
Jan 31, 2025

Two-factor authentication (2FA) has become a cornerstone of modern digital security, serving as a crucial defense against unauthorized access and account compromises. While many organizations rely on popular authenticator apps like Microsoft Authenticator, there's significant value in understanding how to build and customize your own TOTP (Time-based One-Time Password) solution. This becomes particularly relevant for those requiring specific customizations, enhanced security controls, or seamless integration with existing systems.

In this blog, I'll walk through building a TOTP authenticator application using Azure's modern cloud services. Our solution demonstrates using Azure Functions for server-side operations with Azure Key Vault for secrets management. A bonus section covers integrating with Azure Static Web Apps for the frontend. The solution supports the standard TOTP protocol (RFC 6238), ensuring compatibility with services like GitHub and Microsoft's own authentication systems.

While this implementation serves as a proof of concept rather than a production-ready system, it provides a solid foundation for understanding how authenticator apps work under the hood. By walking through the core components - from secret management to token generation - it will share valuable insights into both authentication systems and cloud architecture. This knowledge proves especially valuable for teams considering building custom authentication solutions or those looking to better understand the security principles behind 2FA.

Understanding TOTP

Time-based One-Time Password (TOTP) is an algorithm that generates temporary passwords based on a shared secret key and the current time.

The foundation of TOTP lies in its use of a shared secret key. When a user first enables 2FA with a service like GitHub, a unique secret key is generated. This key is then encoded into a QR code that the user scans with their authenticator app. This initial exchange of the secret is the only time it's transmitted between the service and the authenticator.

For example, a service will provide a QR code that looks like this:

On decoding that, we see that the text encoded within this QR code is:

otpauth://totp/Test%20Token?secret=2FASTEST&issuer=2FAS

When we break down this URI:

  • otpauth:// specifies this is an OTP authentication
  • totp/ indicates this is time-based (as opposed to counter-based HOTP)
  • Test%20Token is the account name (URL encoded)
  • secret=2FASTEST is the shared secret key
  • issuer=2FAS identifies the service providing the 2FA

Once the user scans this, the secret is shared with the app and both the service and authenticator app use it in combination with the current time to generate codes. The process divides time into 30-second intervals. For each interval, the current Unix timestamp is combined with the secret key using a cryptographic hash function (HMAC-SHA1), which produces a consistent 6-digit code that both sides can generate independently.

Security in TOTP comes from several key design principles. The short 30-second validity window means that even if an attacker intercepts a code, they have very limited time to use it. The one-way nature of the hash function means that even with a valid code, an attacker cannot work backwards to discover the secret key. Additionally, since the system relies on UTC time, it works seamlessly across different time zones.

Most services implement a small amount of time drift tolerance. Since device clocks may not be perfectly synchronized, services typically accept codes from adjacent 30 second time windows. This provides a balance between security and usability, ensuring that slight time differences don't prevent legitimate authentication attempts while maintaining the security benefits of time-based codes.

TOTP has become the de facto standard for two-factor authentication across the internet. Its implementation in RFC 6238 ensures compatibility between different services and authenticator apps. This means that whether you're using Google Authenticator, Microsoft Authenticator, or building your own solution like we are, the underlying mechanics remain the same, providing a consistent and secure experience for users.

Architecture

Our TOTP authenticator is built with security and scalability in mind, leveraging Azure's managed services to handle sensitive authentication data. The system consists of two main components: the web frontend, the backend API, and the secret storage.

Backend API: Implemented as Azure Functions, our backend provides endpoints for managing TOTP secrets and generating tokens. We use Azure Functions because they provide excellent security features through managed identities, automatic HTTPS enforcement, and built-in scaling capabilities. The API will contain endpoints for adding new 2FA accounts and retrieving tokens.

Secret storage: Azure Key Vault serves as our secure storage for TOTP secrets. This choice provides several crucial benefits: hardware-level encryption for secrets, detailed access auditing, and automatic key rotation capabilities. Azure Key Vault's managed identity integration with Azure Functions ensures secure, certificate-free access to secrets, while its global redundancy guarantees high availability.

Prerequisites

To follow along this blog, you'll need the following:

  • Azure subscription: You will need an active subscription to host the services we will use. Make sure you have appropriate permissions to create and manage resources. If you don't have one, you can sign up here: https://azure.microsoft.com/en-us/pricing/purchase-options/azure-account
  • Visual Studio Code: For the development environment, install Visual Studio Code. Other IDEs are available, though we will be benefiting from the extensions within this IDE. Download VS Code here: https://code.visualstudio.com/
  • VS Code Azure extensions (optional): There are many different ways to deploy to Azure Static Web Apps and Azure Functions, but having one-click deploy functionality inside our IDE is extremely useful. To install on VS Code, head to Extensions > Search Azure Static Web Apps > Click Install and do the same for the Azure Functions extension.

Building the app

Deploying the resources

We will need to create at least an Azure Key Vault resource, and if you want to test the Function in the cloud (not just locally) then an Azure Function App too.

I've attached the Azure CLI commands to deploy these resources, though it can be done through the portal if that's more comfortable.

Firstly, create an Azure Key Vault resource:

az keyvault create \
  --name <your-kv-name> \
  --resource-group <your-rg> \
  --location <region>

Enable RBAC for your Azure Key Vault:

az keyvault update \
 --name <your-kv-name> \
 --enable-rbac-authorization true

Create new Azure Function App:

az functionapp create \
 --name <app-name> \
 --storage-account <storage-name> \
 --consumption-plan-location <region> \
 --runtime node \
 --runtime-version 18 \
 --functions-version 4

Set Azure Key Vault name environment variable in Azure Function App:

az functionapp config appsettings set \
 --name <app-name> \
 --resource-group <your-rg> \
 --settings "KEY_VAULT_NAME=<your-kv-name>"

Grant your Azure Function App's managed identity access to Azure Key Vault:

az role assignment create \
 --assignee-object-id <function-app-managed-identity> \
 --role "Key Vault Secrets Officer" \
 --scope /subscriptions/<subscription-id>/resourceGroups/<rg-name>/providers/Microsoft.KeyVault/vaults/<your-kv-name>

 

Building the API

The backend of our authenticator app serves as the secure foundation for managing 2FA secrets and generating TOTP tokens. While it might be tempting to handle TOTP generation entirely in the frontend (as some authenticator apps do), our cloud-based approach offers several advantages. By keeping secrets server-side, we can provide secure backup and recovery options, implement additional security controls, and protect against client-side vulnerabilities.

The backend API will have two key responsibilities which the frontend will trigger:

  1. Securely store new account secrets 
  2. Generating valid TOTP tokens on demand

First, we need to create an Azure Functions project in VS Code. The creation wizard will ask you to create a trigger, so let's start with (1) and create a trigger for processing new accounts:

Go to Azure tab > Click the Azure Functions icon > Click Create New Project > Choose a folder > Choose JavaScript > Choose Model V4 > Choose HTTP trigger > Provide a name ('accounts') > Click Open in new window.

Let's make a few modifications to this base Function:

  • Ensure that the only allowed HTTP method is POST, as there is no need to support both and we will make use of the request body allowed in POST requests.
  • Clear everything inside that function to make way for our upcoming code.

Now, let's work forward from this adjusted base:

const { app } = require("@azure/functions");

app.http("accounts", {
  methods: ["POST"],
  authLevel: "anonymous",
  handler: async (request, context) => {
    
  }
});

This accounts endpoint will be responsible for securely storing new TOTP secrets when users add accounts to their authenticator. Here's what we need this endpoint to do:

  • Receive the new account details: the TOTP secret, account name and issuer (extracted from the QR code on the frontend)
  • Validate the request, ensuring proper formatting of all fields and that the user is authenticated
  • Store the secret in Azure Key Vault with appropriate metadata
  • Return success/failure status to allow the frontend to update accordingly.

First, let's validate the incoming request data. When setting up two-factor authentication, services provide a QR code containing a URI in the otpauth:// format. This standardized format includes all the information we need to set up TOTP authentication. Assuming the frontend has decoded the QR code and sent us the resulting data, let's add some code to parse and validate this URI format. We'll use JavaScript's built-in URL class to handle the parsing, which will also take care of URL encoding/decoding for us. Add the following code to the function:

// First, ensure we have a JSON payload
let requestBody;
try {
    requestBody = await request.json();
} catch (error) {
    context.log('Error parsing request body:', error);
    return {
        status: 400,
        jsonBody: {
            error: 'Invalid request format',
            details: 'Request body must be valid JSON containing a TOTP URI'
        }
    };
}

// Check for the URI in the request
const { uri } = requestBody;
if (!uri || typeof uri !== 'string') {
    return {
        status: 400,
        jsonBody: {
            error: 'Missing or invalid TOTP URI',
            details: 'Request must include a "uri" field containing the TOTP setup URI'
        }
    };
}

This first section of code handles the basic validation of our incoming request data. We start by attempting to parse the request body as JSON using request.json(), wrapping it in a try-catch block to handle any parsing failures gracefully. If the parsing fails, we return a 400 Bad Request status with a clear error message. After successfully parsing the JSON, we check for the presence of a uri field in the request body and ensure it's a string value. This validation ensures we have the minimum required data before we attempt to parse the actual TOTP URI in the next step.

Let's now move on to parsing and validating the TOTP URI itself. This URI should contain all the important information: the type of OTP (TOTP in our case), the account name, the secret key, and optionally the issuer. Here's an example of a valid URI which would be provided by services:

otpauth://totp/Test%20Token?secret=2FASTEST&issuer=2FAS

To parse this, add the following code after our initial validation:

// Parse and validate the TOTP URI
try {
  const totpUrl = new URL(uri);

  // Validate it's a TOTP URI
  if (totpUrl.protocol !== "otpauth:") {
    throw new Error("URI must use otpauth:// protocol");
  }

  if (totpUrl.host !== "totp") {
    throw new Error("URI must be for TOTP authentication");
  }

  // Extract the components
  const accountName = decodeURIComponent(totpUrl.pathname.split("/")[1]);
  const secret = totpUrl.searchParams.get("secret");
  const issuer = totpUrl.searchParams.get("issuer");

  // Validate required components
  if (!secret) {
    throw new Error("Missing secret in URI");
  }

  // Store the parsed data for the next step
  const validatedData = {
    accountName,
    secret,
    issuer: issuer || accountName, // Fall back to account name if issuer not specified
  };

  ...
} catch (error) {
  context.log("Error validating TOTP URI:", error);
  return {
    status: 400,
    jsonBody: {
      error: "Invalid TOTP URI",
      details: error.message,
    },
  };
}

We use JavaScript's built-in URL class to do the heavy lifting of parsing the URI components. We first verify this is actually a TOTP URI by checking the protocol and path. Then we extract the three key pieces of information: the account name (from the path), the secret key, and the issuer (both from the query parameters). We validate that the essential secret is present and store all this information in a validatedData object.

Now that we have our TOTP data properly validated and parsed, let's move on to setting up our Azure Key Vault integration. Firstly, we must install the required Azure SDK packages:

npm install azure/identity azure/keyvault-secrets

Now we can add the Azure Key Vault integration to our function. Add these imports at the top of your file:

const { DefaultAzureCredential } = require('@azure/identity');
const { SecretClient } = require('@azure/keyvault-secrets');
const { randomUUID } = require('crypto');

// Initialize Key Vault client
const credential = new DefaultAzureCredential();
const vaultName = process.env.KEY_VAULT_NAME;
const vaultUrl = `https://${vaultName}.vault.azure.net`;
const secretClient = new SecretClient(vaultUrl, credential);

This code sets up our connection to Azure Key Vault using Azure's managed identity authentication. The DefaultAzureCredential will automatically handle authentication when deployed to Azure, and our vault name comes from an environment variable to keep it configurable.

Be sure to go and set the KEY_VAULT_NAME variable inside of your local.settings.json file.

Now let's add the code to store our TOTP secret in Azure Key Vault. Add this after our URI validation:

// Create a unique name for this secret
const secretName = `totp-${Date.now()}`;

// Store the secret in Key Vault with metadata
try {
   await secretClient.setSecret(secretName, validatedData.secret, {
       contentType: 'application/json',
       tags: {
           accountName: validatedData.accountName,
           issuer: validatedData.issuer,
           type: 'totp-secret'
       }
   });

   context.log(`Stored new TOTP secret for account ${validatedData.accountName}`);
   
   return {
       status: 201,
       jsonBody: {
           message: 'TOTP secret stored successfully',
           secretName: secretName,
           accountName: validatedData.accountName,
           issuer: validatedData.issuer
       }
   };
} catch (error) {
   context.error('Error storing secret in Key Vault:', error);
   return {
       status: 500,
       jsonBody: {
           error: 'Failed to store TOTP secret'
       }
   };
}

When storing the secret, we use setSecret with three important parameters:

  1. A unique name generated using a UUID (totp-${randomUUID()}). This ensures each secret has a globally unique identifier with no possibility of collisions, even across distributed systems. The resulting name looks like totp-123e4567-e89b-12d3-a456-426614174000.
  2. The actual TOTP secret we extracted from the URI.
  3. Metadata about the secret, including:
    • contentType marking this as JSON data
    • tags containing the account name and issuer, which helps us identify the purpose of each secret without needing to retrieve its actual value
    • A type tag marking this specifically as a TOTP secret.

If the storage succeeds, we return a 201 Created status with details about the stored secret (but never the secret itself). The returned secretName is particularly important as it will be used later when we need to retrieve this secret to generate TOTP codes.

Now that we can securely store TOTP secrets, let's create our second endpoint that generates the 6-digit codes. This endpoint will:

  1. Retrieve a secret from Azure Key Vault using its unique ID
  2. Generate a valid TOTP code based on the current time
  3. Return the code along with its remaining validity time

Follow the same setup steps as earlier, and ensure you have an empty function. I've named it tokens and set it as a GET request:

app.http('tokens', {
   methods: ['GET'],
   authLevel: 'anonymous',
   handler: async (request, context) => {
       
   }
});

Let's add the code to validate the query parameter and retrieve the secret from Azure Key Vault. A valid request will look like this:

/api/tokens?id=totp-123e4567-e89b-12d3-a456-426614174000

 We want to ensure the ID parameter exists and has the correct format:

// Get the secret ID from query parameters
const secretId = request.query.get('id');

// Validate the secret ID format
if (!secretId || !secretId.startsWith('totp-')) {
   return {
       status: 400,
       jsonBody: {
           error: 'Invalid or missing secret ID. Must be in format: totp-{uuid}'
       }
   };
}

This code first checks if we have a properly formatted secret ID in our query parameters. The ID should start with totp- and be followed by a UUID, matching the format we used when storing secrets in our first endpoint. If the ID is missing or invalid, we return a 400 Bad Request with a helpful error message.

Now if the ID is valid, we should attempt to retrieve the secret from Azure Key Vault:

try {
   // Retrieve the secret from Key Vault
   const secret = await secretClient.getSecret(secretId);
   
   ...
} catch (error) {
   context.error('Error retrieving secret:', error);
   return {
       status: 500,
       jsonBody: {
           error: 'Failed to retrieve secret'
       }
   };
}

If anything goes wrong during this process (like the secret doesn't exist or we have connection issues), we log the error and return a 500 Internal Server Error.

Now that we have the secret from Azure Key Vault, let's add the code to generate the 6-digit TOTP code. First, install otp package:

npm install otp

Then add this import at the top of your file:

const OTP = require('otp');

Now let's generate a 6-digit TOTP using this library from the data retrieved from Azure Key Vault:

const totp = new OTP({
   secret: secret.value
});

// Generate the current token
const token = totp.totp();

// Calculate remaining seconds in this 30-second window
const timeRemaining = 30 - (Math.floor(Date.now() / 1000) % 30);

return {
   status: 200,
   jsonBody: {
       token,
       timeRemaining
   }
};

Let's break down exactly how this code generates our 6-digit TOTP code. 

When we generate a TOTP code, we're using our stored secret key to create a unique 6-digit number that changes every 30 seconds. The OTP library handles this through several steps behind the scenes. First, when we create a new OTP instance with new OTP({ secret: secret.value }), we're setting up a TOTP generator with our base32-encoded secret (like 'JBSWY3DPEHPK3PXP') that we retrieved from Azure Key Vault.

When we call totp(), the library takes our secret and combines it with the current time to generate a code. It takes the current Unix timestamp, divides it by 30 to get the current time window, then uses this value and our secret in an HMAC-SHA1 operation. The resulting hash is then dynamically truncated to give us exactly 6 digits. This is why anyone with the same secret will generate the same code within the same 30-second window.

To help users know when the current code will expire, we calculate timeRemaining by finding out how far we are into the current 30-second window and subtracting that from 30. This gives users a countdown until the next code will be generated.

With both our endpoints complete, we now have a functional backend for our TOTP authenticator. The first endpoint securely stores TOTP secrets in Azure Key Vault, generating a unique ID for each one. The second endpoint uses these IDs to retrieve secrets and generate valid 6-digit TOTP codes on demand. This server-side approach offers several advantages over traditional authenticator apps: our secrets are securely stored in Azure Key Vault rather than on user devices, we can easily back up and restore access if needed, and we can add additional security controls around code generation.

Testing

First, we'll need to run the functions locally using the Azure Functions Core Tools. Open your terminal in the project directory and run:

func start

I'm using a website designed to check if your 2FA app is working correctly. It creates a valid QR code, and also calculates the TOTP on their end so you can compare results. I highly recommend using this alongside me to test our solution: https://2fas.com/check-token/

It will present you with a QR code. You can scan it in your frontend, though you can copy/paste the below which is the exact same value:

otpauth://totp/Test%20Token?secret=2FASTEST&issuer=2FAS

Now let's test our endpoints sequentially using curl (or Postman if you prefer). My functions started on port 7071, be sure to check yours before you send the request. 

Let's start with adding the above secret to Azure Key Vault:

curl -X POST http://localhost:7071/api/accounts \
   -H "Content-Type: application/json" \
   -d '{
       "uri": "otpauth://totp/Test%20Token?secret=2FASTEST&issuer=2FAS"
   }'

This should return a response containing the generated secret ID (your UUID will be different):

{
   "message": "TOTP secret stored successfully",
   "secretName": "totp-f724efb9-a0a7-441f-86c3-2cd36647bfcf",
   "accountName": "Test Token", 
   "issuer": "2FAS"
}

Sidenote: If you head to Azure Key Vault in the Azure portal, you can see the saved secret:

Now we can use this secretName to generate TOTP codes:

curl http://localhost:7071/api/tokens?id=totp-550e8400-e29b-41d4-a716-446655440000

The response will include a 6-digit code and the remaining time until it expires:

{
   "token": "530868",
   "timeRemaining": 26
}

To prove that this is accurate, quickly look again at the website, and you should see the exact same code and a very similar time remaining:

This confirms that your code is valid! You can keep generating new codes and checking them - remember that the code changes every 30 seconds, so be quick when testing and validating.

Bonus: Frontend UI

While not the focus of this blog, as bonus content I've put together a React component which provides a functional interface for our TOTP authenticator. This component allows users to upload QR codes provided by other services, processes them to extract the TOTP URI, sends it to our backend for storage, and then displays the generated 6-digit code with a countdown timer.

Here's how it looks:

As you can see, I've followed a similar style to other known and modern authenticator apps. I recommend writing your own code for the user interface, as it's highly subjective. However, the following is the full React component in case you can benefit from it:

import React, { useState, useEffect, useCallback } from "react";
import { Shield, UserCircle, Plus, Image as ImageIcon } from "lucide-react";
import jsQR from "jsqr";

const TOTPAuthenticator = () => {
  const [secretId, setSecretId] = useState(null);
  const [token, setToken] = useState(null);
  const [timeRemaining, setTimeRemaining] = useState(null);
  const [localTimer, setLocalTimer] = useState(null);
  const [error, setError] = useState(null);
  const [isPasting, setIsPasting] = useState(false);

  useEffect(() => {
    let timerInterval;
    if (timeRemaining !== null) {
      setLocalTimer(timeRemaining);
      timerInterval = setInterval(() => {
        setLocalTimer((prev) => {
          if (prev <= 0) return timeRemaining;
          return prev - 1;
        });
      }, 1000);
    }
    return () => clearInterval(timerInterval);
  }, [timeRemaining]);

  const processImage = async (imageData) => {
    try {
      const img = new Image();
      img.src = imageData;

      await new Promise((resolve, reject) => {
        img.onload = resolve;
        img.onerror = reject;
      });

      const canvas = document.createElement("canvas");
      const context = canvas.getContext("2d");
      canvas.width = img.width;
      canvas.height = img.height;
      context.drawImage(img, 0, 0);

      const imgData = context.getImageData(0, 0, canvas.width, canvas.height);
      const code = jsQR(imgData.data, canvas.width, canvas.height);

      if (!code) {
        throw new Error("No QR code found in image");
      }

      const response = await fetch(
        "http://localhost:7071/api/accounts",
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ uri: code.data }),
        }
      );

      const data = await response.json();
      if (!response.ok) throw new Error(data.error);

      setSecretId(data.secretName);
      setToken({
        issuer: data.issuer,
        accountName: data.accountName,
        code: "--",
      });
      setError(null);
    } catch (err) {
      setError(err.message);
    } finally {
      setIsPasting(false);
    }
  };

  const handlePaste = useCallback(async (e) => {
    e.preventDefault();
    setIsPasting(true);
    setError(null);

    try {
      const items = e.clipboardData.items;
      const imageItem = Array.from(items).find((item) =>
        item.type.startsWith("image/")
      );

      if (!imageItem) {
        throw new Error("No image found in clipboard");
      }

      const blob = imageItem.getAsFile();
      const reader = new FileReader();

      reader.onload = async (event) => {
        await processImage(event.target.result);
      };

      reader.onerror = () => {
        setError("Failed to read image");
        setIsPasting(false);
      };

      reader.readAsDataURL(blob);
    } catch (err) {
      setError(err.message);
      setIsPasting(false);
    }
  }, []);

  const handleDrop = useCallback(async (e) => {
    e.preventDefault();
    setIsPasting(true);
    setError(null);

    try {
      const file = e.dataTransfer.files[0];
      if (!file || !file.type.startsWith("image/")) {
        throw new Error("Please drop an image file");
      }

      const reader = new FileReader();
      reader.onload = async (event) => {
        await processImage(event.target.result);
      };

      reader.onerror = () => {
        setError("Failed to read image");
        setIsPasting(false);
      };

      reader.readAsDataURL(file);
    } catch (err) {
      setError(err.message);
      setIsPasting(false);
    }
  }, []);

  const handleDragOver = (e) => {
    e.preventDefault();
  };

  useEffect(() => {
    let interval;

    const fetchToken = async () => {
      try {
        const response = await fetch(
          `http://localhost:7071/api/tokens?id=${secretId}`
        );
        const data = await response.json();
        if (!response.ok) throw new Error(data.error);

        setToken((prevToken) => ({
          ...prevToken,
          code: data.token,
        }));
        setTimeRemaining(data.timeRemaining);

        const nextFetchDelay = data.timeRemaining * 1000 || 30000;
        interval = setTimeout(fetchToken, nextFetchDelay);
      } catch (err) {
        setError(err.message);
        interval = setTimeout(fetchToken, 30000);
      }
    };

    if (secretId) {
      fetchToken();
    }

    return () => clearTimeout(interval);
  }, [secretId]);

  if (!secretId) {
    return (
      <div className="w-[416px] max-w-full mx-auto bg-white rounded-xl shadow-md overflow-hidden">
        <div className="bg-[#0078D4] p-4 text-white flex items-center gap-2">
          <Shield className="mt-px" size={24} />
          <h2 className="text-xl font-semibold m-0">My Authenticator</h2>
        </div>
        <div className="p-6">
          <div
            className={`w-full p-10 border-2 border-dashed border-gray-300 rounded-lg text-center cursor-pointer transition-all duration-200 ${
              isPasting ? "bg-gray-100" : "bg-white"
            }`}
            onPaste={handlePaste}
            onDrop={handleDrop}
            onDragOver={handleDragOver}
            tabIndex={0}
          >
            <ImageIcon size={32} className="text-gray-600 mx-auto" />
            <p className="text-gray-600 mt-3 text-sm">
              {isPasting ? "Processing..." : "Paste or drop QR code here"}
            </p>
          </div>
          {error && <div className="text-red-600 text-sm mt-2">{error}</div>}
        </div>
      </div>
    );
  }

  return (
    <div className="w-[416px] max-w-full mx-auto bg-white rounded-xl shadow-md overflow-hidden">
      <div className="bg-[#0078D4] p-4 text-white flex items-center gap-2">
        <Shield className="mt-px" size={24} />
        <h2 className="text-xl font-semibold m-0">My Authenticator</h2>
      </div>
      <div className="flex items-center p-4 border-b">
        <div className="bg-gray-100 rounded-full w-10 h-10 flex items-center justify-center mr-4">
          <UserCircle size={24} className="text-gray-600" />
        </div>
        <div className="flex-1">
          <h3 className="text-base font-medium text-gray-800 m-0">
            {token?.issuer || "--"}
          </h3>
          <p className="text-sm text-gray-600 mt-1 m-0">
            {token?.accountName || "--"}
          </p>
        </div>
        <div className="text-right">
          <p className="text-2xl font-medium text-gray-800 m-0 mb-0.5">
            {token?.code || "--"}
          </p>
          <p className="text-xs text-gray-600 m-0">
            {localTimer || "--"} seconds
          </p>
        </div>
      </div>
      <div className="p-6">
        <div
          className={`w-full p-10 border-2 border-dashed border-gray-300 rounded-lg text-center cursor-pointer transition-all duration-200 ${
            isPasting ? "bg-gray-100" : "bg-white"
          }`}
          onPaste={handlePaste}
          onDrop={handleDrop}
          onDragOver={handleDragOver}
          tabIndex={0}
        >
          <ImageIcon size={32} className="text-gray-600 mx-auto" />
          <p className="text-gray-600 mt-3 text-sm">
            {isPasting ? "Processing..." : "Paste or drop QR code here"}
          </p>
        </div>
      </div>
      {error && <div className="text-red-600 text-sm mt-2">{error}</div>}
    </div>
  );
};

export default TOTPAuthenticator;

For deployment, I recommend Azure Static Web Apps because it offers built-in authentication, global CDN distribution, and seamless integration with our Azure Functions backend.

Summary

In this blog, we've built a TOTP authenticator that demonstrates both the inner workings of two-factor authentication and modern cloud architecture. We've demystified how TOTP actually works - from the initial QR code scanning and secret sharing, to the time-based algorithm that generates synchronized 6-digit codes. By implementing this ourselves using Azure services like Azure Key Vault and Azure Functions, we've gained deep insights into both the security protocol and cloud-native development.

While this implementation focuses on the core TOTP functionality, it serves as a foundation that you can build upon with features like authenticated multi-user support, backup codes, or audit logging. Whether you're interested in authentication protocols, cloud architecture, or both, this project provides hands-on experience with real-world security implementations.

The complete source code for this project is available on my GitHub repository: https://github.com/stephendotgg/azure-totp-authenticator

Thanks for reading! Hopefully this has helped you understand TOTP and Azure services better.

Updated Jan 31, 2025
Version 1.0
No CommentsBe the first to comment