azure functions
355 TopicsBuilding a TOTP Authenticator App on Azure Functions and Azure Key Vault
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: Securely store new account secrets 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: 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. The actual TOTP secret we extracted from the URI. 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: Retrieve a secret from Azure Key Vault using its unique ID Generate a valid TOTP code based on the current time 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.2.4KViews2likes1CommentAI Transcription & Text Analytics for Health
Industry Challenge Healthcare organizations depend on qualitative research, patient interviews, and clinical documentation to improve care delivery. Traditional transcription services often create bottlenecks: Manual Processes: Require manual uploads and lack automation. Delayed Turnaround: Transcripts can take days, slowing research and decision-making. Limited Integration: Minimal interoperability with EMR systems or analytics platforms. Cost Inefficiencies: Pricing models that scale poorly for large volumes. The need for real-time, HIPAA-compliant transcription and integrated analytics has never been greater. Azure AI Solution Overview Azure provides a comprehensive, cloud-native transcription and analytics pipeline that addresses these challenges head-on. By leveraging Azure AI Services, organizations can: Transcribe audio/video recordings in real time. Process PDFs and text documents for structured data extraction. Apply Text Analytics for Health to identify medical entities and structure data into FHIR format. Generate summaries and insights using cutting edge LLMs including Azure OpenAI. This approach accelerates workflows, improves compliance, and reduces costs compared to traditional transcription vendors. Azure Speech Service Options Azure Speech Service offers multiple transcription modes to fit different scenarios: Real-Time Transcription: Converts live audio streams into text instantly for telehealth sessions and interviews. Batch Transcription: Processes large volumes of pre-recorded audio asynchronously for research studies. Fast Transcription: Optimized for quick turnaround on short recordings for rapid documentation needs. Azure Text Analytics for Health One of the most powerful components of this solution is Azure AI Language – Text Analytics for Health, which transforms raw text into structured clinical insights. Key capabilities include: Named Entity Recognition (NER): Automatically identifies clinical entities such as symptoms, diagnoses, medications, procedures, and anatomy from transcripts and documents. Relation Extraction: Detects relationships between entities (e.g., linking a medication to its dosage or a condition to its treatment), enabling richer context for clinical decision-making. Entity Linking to UMLS Codes: Maps recognized entities to Unified Medical Language System (UMLS) concepts, ensuring interoperability and standardization across healthcare systems. Assertion Detection: Determines the status of an entity (e.g., present, absent, conditional, or hypothetical), which is critical for accurate interpretation of patient data. These features allow healthcare organizations to move beyond simple transcription and unlock structured, actionable insights that can feed downstream analytics and reporting. Other Azure Resources Azure AI Document Intelligence – Extracts structured data from PDFs and scanned documents. Azure OpenAI Service – Summarizes transcripts and generates clinical insights. Azure Storage & Functions – Securely stores raw and processed data; orchestrates workflows for transcription and analytics. Integration with Microsoft Fabric OneLake Once FHIR JSON output is generated from Text Analytics for Health, it can be stored in Microsoft Fabric OneLake. This unlocks powerful downstream capabilities: Unified Data Lake: Centralized storage for structured healthcare data. Analytics & Reporting: Use Fabric’s Lakehouse and Power BI to build dashboards for clinical research trends, patient outcomes, and operational metrics. AI-Driven Insights: Combine transcription data with other datasets for predictive modeling and advanced analytics. This integration ensures that transcription and clinical insights are not siloed—they become part of a broader data ecosystem for research and decision-making. Why Azure Stands Out Compared to other transcription solutions in the market, Azure offers: Real-Time Processing: Immediate access to transcripts versus multi-day turnaround. Integrated Analytics: Built-in medical entity recognition and AI summarization. Compliance & Security: HIPAA-ready architecture with enterprise-grade governance. Cost Efficiency: Pay-as-you-go pricing with elastic scaling for large datasets. End-to-End Data Flow: From transcription to Fabric OneLake for analytics. Step-by-Step Deployment Guide As part of the Azure Field team working in the Healthcare and Life Sciences industry, this challenge has emerged as a common theme among organizations seeking to modernize transcription and analytics workflows. To assist organizations exploring Azure AI solutions to address these challenges, the following demo application was developed by Solution Engineer Samuel Tauil and Cloud & AI Platform Specialist Hannah Abbott. This application is intended to allow organizations to quickly stand up and test these Azure services for their needs and is not intended as a production-ready solution. This Azure-powered web application demonstrates how organizations can modernize transcription and clinical insights using cloud-native AI services. Users can upload audio files in multiple formats, which are stored in Azure Storage and trigger an Azure Function to perform speech-to-text transcription with speaker diarization. The transcript is then enriched through Azure Text Analytics for Health, applying advanced capabilities like named entity recognition, relation extraction, UMLS-based entity linking, and assertion detection to deliver structured clinical insights. Finally, Azure OpenAI generates a concise summary and a downloadable clinical report, while FHIR-compliant JSON output seamlessly integrates with Microsoft Fabric OneLake for downstream analytics and reporting—unlocking a complete, scalable, and secure solution for healthcare data workflows. The following video clip uses AI-generated dialog for a fictitious scenario to demonstrate the capabilities of the sample application. Sample application developed by Samuel Tauil Microsoft Solution Engineer (25) Samuel Tauil | LinkedIn Prerequisites Azure Subscription GitHub account Azure CLI installed locally (optional, for manual deployment) 1. Fork the Repository GitHub - samueltauil/transcription-services-demo: Azure Healthcare Transcription Services Demo - Speech-to-text with Text Analytics for Health for HIPAA-compliant medical transcription 2. Create Azure Service Principal for GitHub Actions Copy the JSON output. 3. Add GitHub Secrets (Settings → Secrets and variables → Actions): AZURE_CREDENTIALS: Paste the service principal JSON from step 2 4. Run the deployment workflow: Go to Actions tab → "0. Deploy All (Complete)" Click "Run workflow" Enter your resource group name and Azure region Click "Run workflow" 5. After infrastructure deploys, add these additional secrets: AZURE_FUNCTIONAPP_NAME: The function app name (shown in workflow output) AZURE_STATIC_WEB_APPS_API_TOKEN: Get from Azure Portal → Static Web App → Manage deployment token Benefits Accelerated Research: Reduce transcription time from days to minutes. Enhanced Accuracy: AI-driven entity recognition for clinical terms. Scalable & Secure: Built on Azure’s compliance-ready infrastructure. Analytics-Ready: Seamless integration with Fabric for reporting and insights. Reference Links: Transcription Service: Speech to text overview - Speech service - Foundry Tools | Microsoft Learn Batch transcription overview - Speech service - Foundry Tools | Microsoft Learn Speech to text quickstart - Foundry Tools | Microsoft Learn Real-time diarization quickstart - Speech service - Foundry Tools | Microsoft Learn Text Analytics: Watch this: Embedded Video | Microsoft Learn What is the Text Analytics for health in Azure Language in Foundry Tools? - Foundry Tools | Microso… Fast Healthcare Interoperability Resources (FHIR) structuring in Text Analytics for health - Foundr… azure-ai-docs/articles/ai-services/language-service/text-analytics-for-health/quickstart.md at main… AI Foundry: Model catalog - Azure AI Foundry296Views0likes0CommentsAnnouncing Azure Functions Durable Task Scheduler Dedicated SKU GA & Consumption SKU Public Preview
Earlier this year, we introduced the Durable Task Scheduler, our orchestration engine designed for complex workflows and intelligent agents. It automatically checkpoints progress and protects your orchestration state, enabling resilient and reliable execution. Today, we’re excited to announce a major milestone: Durable Task Scheduler is now Generally Available with the Dedicated SKU, and the Consumption SKU is entering Public Preview. These offerings provide advanced orchestration capabilities for cloud-native and AI applications, providing predictable pricing for steady workloads with the Dedicated SKU and flexible, pay-as-you-go billing for dynamic, variable workloads with the Consumption SKU. “The Durable Task Scheduler has been a game-changer for our projects. It keeps our workflows running reliably with minimal code, even as they grow in complexity. It automatically recovers from unexpected issues, so we don’t have to step in. It scales to handle millions of orchestrations, and the real-time dashboard makes it simple to monitor and manage everything as it happens.” – Pedram Rezaei, VP of Engineering for Copilot What is the durable task scheduler? If you’re new to the Durable Task Scheduler, we recommend checking out our previous blog posts for a detailed background on what it is and how/when to leverage it: https://aka.ms/dts-public-preview https://aka.ms/workflow-in-aca In brief, the Durable Task Scheduler is a fully managed backend for durable execution on Azure. It can serve as the backend for a Durable Function App using the Durable Functions extension, or as the backend for an app leveraging the Durable Task SDKs in other compute environments, such as Azure Container Apps, Azure Kubernetes Services, or Azure App Service. It simplifies the development and operation of complex, stateful, and long-running workflows by providing automatic orchestration state persistence, fault tolerance, and built-in monitoring, all freeing developers from the operational overhead of managing orchestration storage and failure recovery. The Durable Task Scheduler is designed to deliver the best possible experience by addressing the key challenges developers face when self-managing orchestration infrastructure, such as configuring storage accounts, checkpointing orchestration progress, troubleshooting unexpected orchestration behavior, and ensuring high reliability. As of today, the Durable Task Scheduler is available across all Function App SKUs and includes autoscaling support in options like Flex Consumption. “Durable Task Scheduler has significantly accelerated execution of complex business logic which requires orchestration. We are observing up to 10 times faster speed as compared to the blob storage backend. We also love the dashboard view for our taskhubs, which gives us great visibility and helps us monitor, time and manage our workflows.” – Roney Varghese, Software Engineer at Pinnacle Tech Dedicated and Consumption SKUs Dedicated SKU (GA) The Dedicated SKU, which has been available in public preview since March of this year, has now graduated to General Availability. It delivers predictable performance and high reliability with dedicated infrastructure, high throughput, and up to 90-days orchestration data retention. It’s ideal for mission-critical workloads requiring consistent, high-scale throughput and for organizations that prefer predictable billing. Key features of the Dedicated SKU include: Dedicated Infrastructure: Runs on dedicated resources guaranteeing isolation. Custom Scaling: Configure Capacity Units (CUs) to match your workload needs. High Availability: High availability with multi-CU deployments. Data Retention: Up to 90 days. Performance: Each CU supports up to 2,000 actions per second and 50GB of orchestration data. What’s new in the Dedicated SKU? More Capacity Units As of today, the Dedicated SKU enables you to purchase additional capacity units for high performance and orchestration data storage. High Availability For applications requiring even higher availability for mission-critical scenarios, the Dedicated SKU now offers a High Availability feature. To enable high availability, you need at least 3 capacity units on your scheduler instance. Learn more about the Dedicated SKU here: https://aka.ms/dts-dedicated-sku Consumption SKU (Public Preview) We’ve heard your feedback loud and clear. We understand that the Dedicated SKU isn’t the right fit for every scenario. That’s why we’re introducing a new pricing plan: the Consumption SKU, a SKU tailored for workloads that run intermittently, or scale dynamically, and for requirements where flexibility and cost efficiency matter most. The Consumption SKU is perfect for variable workloads and development/test environments. It offers: Pay-Per-Use: Only pay for actions dispatched No Upfront Costs: No minimum commitments. Data Retention: Up to 30 days. Performance: Up to 500 actions per second. Learn more about the Consumption SKU here: https://aka.ms/dts-consumption-sku Roadmap We’re excited to reach this milestone, but we also have many plans for the future. Here’s a glimpse of the features you can expect to see in the Durable Task Scheduler in the near future: Private Endpoints Zone Redundancy in the Dedicated SKU Export API – Need your orchestration data for longer than the max retention limit? Use the Export API to move data out of DTS into a storage provider of your choice. Dynamic Scaling of Capacity Units – Set a minimum and maximum and allow DTS to dynamically scale up and down depending on orchestration throughput. Ability to handle payloads larger than 1MB Get started with the Durable Task Scheduler today Documentation: https://aka.ms/dts-documentation Samples: https://aka.ms/dts-samples Getting Started: https://aka.ms/dts-getting-started901Views1like1CommentSecure Unique Default Hostnames Now GA for Functions and Logic Apps
We are pleased to announce that Secure Unique Default Hostnames are now generally available (GA) for Azure Functions and Logic Apps (Standard). This expands the security model previously available for Web Apps to the entire App Service ecosystem and provides customers with stronger, more secure, and standardized hostname behavior across all workloads. Why This Feature Matters Historically, App Service resources have used default hostname format such as: <SiteName>.azurewebsites.net While straightforward, this pattern introduced potential security risks, particularly in scenarios where DNS records were left behind after deleting a resource. In those situations, a different user could create a new resource with the same name and unintentionally receive traffic or bindings associated with the old DNS configuration, creating opportunities for issues such as subdomain takeover. Secure Unique Default Hostnames address this by assigning a unique, randomized, region‑scoped hostname to each resource, for example: <SiteName>-<Hash>.<Region>.azurewebsites.net This change ensures that: No other customer can recreate the same default hostname. Apps inherently avoid risks associated with dangling DNS entries. Customers gain a more secure, reliable baseline behavior across App Service. Adopting this model now helps organizations prepare for the long‑term direction of the platform while improving security posture today. What’s New: GA Support for Functions and Logic Apps With this release, both Azure Functions and Logic Apps (Standard) fully support the Secure Unique Default Hostname capability. This brings these services in line with Web Apps and ensures customers across all App Service workloads benefit from the same secure and consistent default hostname model. Azure CLI Support The Azure CLI for Web Apps and Function Apps now includes support for the “--domain-name-scope” parameter. This allows customers to explicitly specify the scope used when generating a unique default hostname during resource creation. Examples: az webapp create --domain-name-scope {NoReuse, ResourceGroupReuse, SubscriptionReuse, TenantReuse} az functionapp create --domain-name-scope {NoReuse, ResourceGroupReuse, SubscriptionReuse, TenantReuse} Including this parameter ensures that deployments consistently use the intended hostname scope and helps teams prepare their automation and provisioning workflows for the secure unique default hostname model. Why Customers Should Adopt This Now While existing resources will continue to function normally, customers are strongly encouraged to adopt Secure Unique Default Hostnames for all new deployments. Early adoption provides several important benefits: A significantly stronger security posture. Protection against dangling DNS and subdomain takeover scenarios. Consistent default hostname behavior as the platform evolves. Alignment with recommended deployment practices and modern DNS hygiene. This feature represents the current best practice for hostname management on App Service and adopting it now helps ensure that new deployments follow the most secure and consistent model available. Recommended Next Steps Enable Secure Unique Default Hostnames for all new Web Apps, Function Apps, and Logic Apps. Update automation and CLI scripts to include the --domain-name-scope parameter when creating new resources. Begin updating internal documentation and operational processes to reflect the new hostname pattern. Additional Resources For readers who want to explore the technical background and earlier announcements in more detail, the following articles offer deeper coverage of unique default hostnames: Public Preview: Creating Web App with a Unique Default Hostname This article introduces the foundational concepts behind unique default hostnames. It explains why the feature was created, how the hostname format works, and provides examples and guidance for enabling the feature when creating new resources. Secure Unique Default Hostnames: GA on App Service Web Apps and Public Preview on Functions This article provides the initial GA announcement for Web Apps and early preview details for Functions. It covers the security benefits, recommended usage patterns, and guidance on how to handle existing resources that were created without unique default hostnames. Conclusion Secure Unique Default Hostnames now provide a more secure and consistent default hostname model across Web Apps, Function Apps, and Logic Apps. This enhancement reduces DNS‑related risks and strengthens application security, and organizations are encouraged to adopt this feature as the standard for new deployments.288Views0likes0CommentsFrom Vibe Coding to Working App: How SRE Agent Completes the Developer Loop
The Most Common Challenge in Modern Cloud Apps There's a category of bugs that drive engineers crazy: multi-layer infrastructure issues. Your app deploys successfully. Every Azure resource shows "Succeeded." But the app fails at runtime with a vague error like Login failed for user ''. Where do you even start? You're checking the Web App, the SQL Server, the VNet, the private endpoint, the DNS zone, the identity configuration... and each one looks fine in isolation. The problem is how they connect and that's invisible in the portal. Networking issues are especially brutal. The error says "Login failed" but the actual causes could be DNS, firewall, identity, or all three. The symptom and the root causes are in completely different resources. Without deep Azure networking knowledge, you're just clicking around hoping something jumps out. Now imagine you vibe coded the infrastructure. You used AI to generate the Bicep, deployed it, and moved on. When it breaks, you're debugging code you didn't write, configuring resources you don't fully understand. This is where I wanted AI to help not just to build, but to debug. Enter SRE Agent + Coding Agent Here's what I used: Layer Tool Purpose Build VS Code Copilot Agent Mode + Claude Opus Generate code, Bicep, deploy Debug Azure SRE Agent Diagnose infrastructure issues and create developer issue with suggested fixes in source code (app code and IaC) Fix GitHub Coding Agent Create PRs with code and IaC fix from Github issue created by SRE Agent Copilot builds. SRE Agent debugs. Coding Agent fixes. What I Built I used VS Code Copilot in Agent Mode with Claude Opus to create a .NET 8 Web App connected to Azure SQL via private endpoint: Private networking (no public exposure) Entra-only authentication Managed identity (no secrets) Deployed with azd up. All green. Then I tested the health endpoint: $ curl https://app-tsdvdfdwo77hc.azurewebsites.net/health/sql {"status":"unhealthy","error":"Login failed for user ''.","errorType":"SqlException"} Deployment succeeded. App failed. One error. How I Fixed It: Step by Step Step 1: Create SRE Agent with Azure Access I created an SRE Agent with read access to my Azure subscription. You can scope it to specific resource groups. The agent builds a knowledge graph of your resources and their dependencies visible in the Resource Mapping view below. Step 2: Connect GitHub to SRE Agent using GitHub MCP server I connected the GitHub MCP server so the agent could read my repository and create issues. Step 3: Create Sub Agent to analyze source code I created a sub-agent for analyzing source code using GitHub mcp tools. this lets SRE Agent understand not just Azure resources, but also the Bicep and source code files that created them. "you are expert in analyzing source code (bicep and app code) from github repos" Step 4: Invoke Sub-Agent to Analyze the Error In the SRE Agent chat, I invoked the sub-agent to diagnose the error I received from my app end point. It correlated the runtime error with the infrastructure configuration Step 5: Watch the SRE Agent Think and Reason SRE Agent analyzed the error by tracing code in Program.cs, Bicep configurations, and Azure resource relationships Web App, SQL Server, VNet, private endpoint, DNS zone, and managed identity. Its reasoning process worked through each layer, eliminating possibilities one by one until it identified the root causes. Step 6: Agent Creates GitHub Issue Based on its analysis, SRE Agent summarized the root causes and suggested fixes in a GitHub issue: Root Causes: Private DNS Zone missing VNet link Managed identity not created as SQL user Suggested Fixes: Add virtualNetworkLinks resource to Bicep Add SQL setup script to create user with db_datareader and db_datawriter roles Step 7: Merge the PR from Coding Agent Assign the Github issue to Coding Agent which then creates a PR with the fixes. I just reviewed the fix. It made sense and I merged it. Redeployed with azd up, ran the SQL script: curl -s https://app-tsdvdfdwo77hc.azurewebsites.net/health/sql | jq . { "status": "healthy", "database": "tododb", "server": "tcp:sql-tsdvdfdwo77hc.database.windows.net,1433", "message": "Successfully connected to SQL Server" } 🎉 From error to fix in minutes without manually debugging a single Azure resource. Why This Matters If you're a developer building and deploying apps to Azure, SRE Agent changes how you work: You don't need to be a networking expert. SRE Agent understands the relationships between Azure resources private endpoints, DNS zones, VNet links, managed identities. It connects dots you didn't know existed. You don't need to guess. Instead of clicking through the portal hoping something looks wrong, the agent systematically eliminates possibilities like a senior engineer would. You don't break your workflow. SRE Agent suggests fixes in your Bicep and source code not portal changes. Everything stays version controlled. Deployed through pipelines. No hot fixes at 2 AM. You close the loop. AI helps you build fast. Now AI helps you debug fast too. Try It Yourself Do you vibe code your app, your infrastructure, or both? How do you debug when things break? Here's a challenge: Vibe code a todo app with a Web App, VNet, private endpoint, and SQL database. "Forget" to link the DNS zone to the VNet. Deploy it. Watch it fail. Then point SRE Agent at it and see how it identifies the root cause, creates a GitHub issue with the fix, and hands it off to Coding Agent for a PR. Share your experience. I'd love to hear how it goes. Learn More Azure SRE Agent documentation Azure SRE Agent blogs Azure SRE Agent community Azure SRE Agent home page Azure SRE Agent pricing594Views3likes0CommentsHost ChatGPT apps on Azure Functions
This blog post is for developers learning and building ChatGPT apps. It provides an overview of how these apps work, why build them, and how to host one on Azure Functions. Chat with ChatGPT apps OpenAI recently launched ChatGPT apps. These are apps you can chat with right inside ChatGPT, extending what ChatGPT can do beyond simple chats to actions. These apps can be invoked by starting a message with the app name, or they can be suggested by ChatGPT when relevant to the conversation. The following shows an example of invoking the Booking.com app to find hotels that meet certain criteria: OpenAI calls these “a new generation of apps” that “blend familiar interactive elements…with new ways of interacting through conversation.” For users, ChatGPT apps fit directly into an interface they’re already familiar with and can use with little to no learning. For developers, building these apps is great way to get them in the hands of ChatGPT’s 800 million users without having to build custom frontends or worry about distribution and discovery. The following summarizes key benefits of ChatGPT apps: Native Integration: Once connected, users can invoke apps with a simple @ mention. Contextual Actions: Your app doesn't just "chat"—it does. It can fetch real-time data or execute actions. Massive Distribution and Easy Discovery: ChatGPT has added an app directory and just announced that they’re accepting submissions. Apps in the directory are exposed ChatGPT’s massive user base. ChatGPT apps are remote MCP servers ChatGPT apps are simply remote MCP servers that expose tools, but with two notable distinctions: Their tools use metadata to specify UI elements that should be rendered when it returns a result The UI elements are exposed as MCP resources. ChatGPT invokes tools the same way agents invoke tools on any MCP server. The difference is the added ability to render the tool results in a custom UI that’s embedded in the chat as an iFrame. A UI can include buttons, text boxes, maps, and other components that users can interact with. Instead of calling a RESTful API, the UI can trigger additional tool calls in the MCP server as the user interacts with it. Learn more about building the custom UI . For example, when the Zillow app returns results to the user’s question, the results are home listings and a map that users can interact with: Since ChatGPT apps are just MCP servers, any existing server you may have can be turned into a ChatGPT app. To do that, you must ensure the server uses the streamable HTTP transport if it doesn’t already and then find a place to host it. Hosting remote MCP servers While there are many hosting platforms available, Azure Functions is uniquely positioned to host remote MCP servers as the platform provides several key benefits: Scalable Infrastructure: ChatGPT apps can go viral. Azure Function’s Flex Consumption plan can handle bursty traffic during high traffic times and scale back to zero when needed Built-in auth: Keep your server secured with Azure Function’s built-in server authentication and authorization feature Serverless billing: Pay for only when the app is run instead of idle time Learn more about remote MCP servers hosted on Azure Functions. Create ChatGPT app Let’s quickly create a sample ChatGPT app that returns the weather of a place. Prerequisites Ensure you have the following prerequisites before proceeding: Azure subscription for creating Azure Functions and related resources Azure Developer CLI for deploying MCP server via infrastructure as code ChatGPT Plus subscription for testing ChatGPT app in developer mode Deploy MCP server to Azure Functions Clone this sample MCP server: `git clone https://github.com/Azure-Samples/chatgpt-app-azure-function-mcp`. Open terminal, run `azd auth login` and complete the login flow in the browser. Navigate to sample root directory, run `azd up` to deploy the server and related resources. You’ll be prompted with: Enter a unique environment name: Enter a unique name. This is the name of the resource group where all deployed resources live. Select an Azure Subscription: Pick your subscription Enter a value for the ‘location’ infrastructure: East US Once deployment completes, copy the app url for the next step. It should look like: https://<your-app>.azurewebsites.net Sample code walkthrough The sample server is built using the Python FastMCP package. You can find more information and how to test server locally in this repo. We'll walkthough the code briefly here. In main.py, you find the `get_weather_widget` resource and `get_current_weather` tool (code abbreviated here): .resource("ui://widget/current-weather.html", mime_type="text/html+skybridge") def get_weather_widget() -> str: """Interactive HTML widget to display current weather data in ChatGPT.""" # some code... @mcp.tool( annotations={ "title": "Get Current Weather", "readOnlyHint": True, "openWorldHint": True, }, meta={ "openai/outputTemplate": "ui://widget/current-weather.html", "openai/toolInvocation/invoking": "Fetching weather data", "openai/toolInvocation/invoked": "Weather data retrieved" }, ) def get_current_weather(latitude: float, longitude: float) -> ToolResult: """Get current weather for a given latitude and longitude using Open-Meteo API.""" # some code... return ToolResult( content=content_text, structured_content=data ) When you ask ChatGPT a question, it calls the MCP tool which returns a `ToolResult` containing both human-readable content (for ChatGPT to understand) and machine-readable data (`structured_content`, raw data for the widget). Because the `get_current_weather` tool specifies an `outputTemplate` in the metadata, ChatGPT fetches the corresponding widget HTML from the `get_weather_widget` resource. To return results, it creates an iframe and injects the weather results (`structured_content`) into the widget's JavaScript environment (via `window.openai.toolOutput`). The widget's JavaScript then renders the weather data into a beautiful UI. Test ChatGPT app in developer mode Turn on Developer mode in ChatGPT: Go to Settings → Connectors → Advanced → Developer mode In the chat, click + → More → Add sources The Add + button should show next to Sources. Click Add + → Connect more In the Enable apps window, look for Advanced settings. Click Create app. A form should open. Fill out the form to create the new app Name: WeatherApp MCP Server URL: Enter the MCP server endpoint, which is the app URL you previously saved with /mcp appended. Example: https://<you-app>.azurewebsites.net/mcp Authentication: Choose No Auth Check the box for “I understand and want to continue” and click Create. Once connected, you should find the server listed under Enabled apps. Test by asking ChatGPT “@WeatherApp what’s the temperature in NYC today?” Submit to ChatGPT App Directory OpenAI has opened app submission recently. Submitting the app to the App Directory makes it accessible to all users on ChatGPT. You may want to read through the submission guidelines to ensure your app meets the requirements before submitting. What’s next In this blog post, we gave an overview of ChatGPT apps and showed how to host one in Azure Functions. We’ll dedicate the next blog post to elaborate on configuring authentication and authorization for apps hosted on Azure Functions. For users familiar with the Azure Functions MCP extension, we’re working on support for MCP Resources in the extension. You’ll be able to build ChatGPT apps using the extension once that support is out. For now, you need to use the official MCP SDKs. Closing thoughts ChatGPT apps extend the ability of ChatGPT beyond chat by letting users take actions like searching for an apartment, ordering groceries, and turning an outline into slide deck with just a mention of the app name in the chat. The directory OpenAI created where developers can submit their apps reminds one of the App Store in the iPhone. It seems to be a no-brainer now that such a marketplace should be provided. Would this also be the case for ChatGPT? Do you think the introduction of these apps is a gamechanger? And are they useful for your scenarios? Share with us your thoughts!657Views1like0CommentsBuilding Reliable AI Travel Agents with the Durable Task Extension for Microsoft Agent Framework
The durable task extension for Microsoft Agent Framework makes all this possible. In this post, we'll walk through the AI Travel Planner, a C# application I built that demonstrates how to build reliable, scalable multi-agent applications using the durable task extension for Microsoft agent framework. While I work on the python version, I've included code snippets that show the python equivalent. If you haven't already seen the announcement post on the durable task extension for Microsoft Agent Framework, I suggest you read that first before continuing with this post: http://aka.ms/durable-extension-for-af-blog. In brief, production AI agents face real challenges: crashes can lose conversation history, unpredictable behavior makes debugging difficult, human-in-the-loop workflows require waiting without wasting resources, and variable demand needs flexible scaling. The durable task extension addresses each of these: Serverless Hosting: Deploy agents on Azure Functions with auto-scaling from thousands of instances to zero, while retaining full control in a serverless architecture. Automatic Session Management: Agents maintain persistent sessions with full conversation context that survives process crashes, restarts, and distributed execution across instances Deterministic Multi-Agent Orchestrations: Coordinate specialized durable agents with predictable, repeatable, code-driven execution patterns Human-in-the-Loop with Serverless Cost Savings: Pause for human input without consuming compute resources or incurring costs Built-in Observability with Durable Task Scheduler: Deep visibility into agent operations and orchestrations through the Durable Task Scheduler UI dashboard AI Travel Planner Architecture Overview The Travel Planner application takes user trip preferences and starts a workflow that orchestrates three specialized agent framework agents (a Destination Recommender, an Itinerary Planner, and a Local Recommender) to build a comprehensive, personalized travel plan. Once a travel plan is created, the workflow includes human-in-the-loop approval before booking the trip (mocked), showcasing how the durable task extension handles long-running operations easily: Application Workflow User Request: User submits travel preferences via React frontend Orchestration Scheduled: Azure Functions backend receives the request and schedules a deterministic agentic workflow using the Durable Task Extension for Agent Framework. Destination Recommendation: The orchestrator first coordinates the Destination Recommender agent to analyze preferences and suggest destinations Itinerary Planning and Local Recommendations: The orchestrator then parallelizes the invocation of the Itinerary Planner agent to create detailed day-by-day plans for the given destination and the Local Recommendations agent to add insider tips and attractions Storage: Created travel plan is saved to Azure Blob Storage Approval: User reviews and approves the plan (human-in-the-loop) Booking: Upon approval, booking of the trip completes Key Components Azure Static Web Apps: Hosts the React frontend Azure Functions (.NET 9): Serverless compute hosting the agents and workflow with automatic scaling Durable Task Extension for Microsoft Agent Framework: The AI agent SDK with durable task extension Durable Task Scheduler: Manages state persistence, orchestration, and observability Azure OpenAI (GPT-4o-mini): Powers the AI agents Now let’s dive into the code. Along the way, I’ll highlight the value the durable task extension brings and patterns you can apply to your own applications. Creating Durable Agents Making the standard Agent Framework agents durable agents is simple. Include the durable task extension package and register your agents within the ConfigureDurableAgents extension method and you automatically get: Persistent conversation sessions that survive restarts HTTP endpoints for agent interactions Automatic state checkpointing that survive restarts Distributed execution across instances C# FunctionsApplication .CreateBuilder(args) .ConfigureDurableAgents(configure => { configure.AddAIAgentFactory("DestinationRecommenderAgent", sp => chatClient.CreateAIAgent( instructions: "You are a travel destination expert...", name: "DestinationRecommenderAgent", services: sp)); configure.AddAIAgentFactory("ItineraryPlannerAgent", sp => chatClient.CreateAIAgent( instructions: "You are a travel itinerary planner...", name: "ItineraryPlannerAgent", services: sp, tools: [AIFunctionFactory.Create(CurrencyConverterTool.ConvertCurrency)])); configure.AddAIAgentFactory("LocalRecommendationsAgent", sp => chatClient.CreateAIAgent( instructions: "You are a local expert...", name: "LocalRecommendationsAgent", services: sp)); }); Python # Create the Azure OpenAI chat client chat_client = AzureOpenAIChatClient( endpoint=endpoint, deployment_name=deployment_name, credential=DefaultAzureCredential() ) # Destination Recommender Agent destination_recommender_agent = chat_client.create_agent( name="DestinationRecommenderAgent", instructions="You are a travel destination expert..." ) # Itinerary Planner Agent (with tools) itinerary_planner_agent = chat_client.create_agent( name="ItineraryPlannerAgent", instructions="You are a travel itinerary planner...", tools=[get_exchange_rate, convert_currency] ) # Local Recommendations Agent local_recommendations_agent = chat_client.create_agent( name="LocalRecommendationsAgent", instructions="You are a local expert..." ) # Configure Function App with Durable Agents. AgentFunctionApp is where the magic happens app = AgentFunctionApp(agents=[ destination_recommender_agent, itinerary_planner_agent, local_recommendations_agent ]) The Orchestration Programming Model The durable task extension uses an intuitive async/await programming model for deterministic orchestration. You write orchestration logic as ordinary imperative code (if/else, try/catch), and the framework handles all the complexity of coordination, durability, retries, and distributed execution. The Travel Planner Orchestration Here's the actual orchestration from the application that coordinates all three agents, runs tasks in parallel, handles human approval, and books the trip: C# [Function(nameof(RunTravelPlannerOrchestration))] public async Task<TravelPlanResult> RunTravelPlannerOrchestration( [OrchestrationTrigger] TaskOrchestrationContext context) { var travelRequest = context.GetInput<TravelRequest>()!; // Get durable agents and create conversation threads DurableAIAgent destinationAgent = context.GetAgent("DestinationRecommenderAgent"); DurableAIAgent itineraryAgent = context.GetAgent("ItineraryPlannerAgent"); DurableAIAgent localAgent = context.GetAgent("LocalRecommendationsAgent"); // Step 1: Get destination recommendations var destinations = await destinationAgent.RunAsync<DestinationRecommendations>( $"Recommend destinations for {travelRequest.Preferences}", destinationAgent.GetNewThread()); var topDestination = destinations.Result.Recommendations.First(); // Steps 2 & 3: Run itinerary and local recommendations IN PARALLEL var itineraryTask = itineraryAgent.RunAsync<TravelItinerary>( $"Create itinerary for {topDestination.Name}", itineraryAgent.GetNewThread()); var localTask = localAgent.RunAsync<LocalRecommendations>( $"Local recommendations for {topDestination.Name}", localAgent.GetNewThread()); await Task.WhenAll(itineraryTask, localTask); // Step 4: Save to blob storage await context.CallActivityAsync(nameof(SaveTravelPlanToBlob), travelPlan); // Step 5: Wait for human approval (NO COMPUTE COSTS while waiting!) var approval = await context.WaitForExternalEvent<ApprovalResponse>( "ApprovalEvent", TimeSpan.FromDays(7)); // Step 6: Book if approved if (approval.Approved) await context.CallActivityAsync(nameof(BookTrip), travelPlan); return new TravelPlanResult(travelPlan, approval.Approved); } Python app.orchestration_trigger(context_name="context") def travel_planner_orchestration(context: df.DurableOrchestrationContext): travel_request_data = context.get_input() travel_request = TravelRequest(**travel_request_data) # Get durable agents and create conversation threads destination_agent = app.get_agent(context, "DestinationRecommenderAgent") itinerary_agent = app.get_agent(context, "ItineraryPlannerAgent") local_agent = app.get_agent(context, "LocalRecommendationsAgent") # Step 1: Get destination recommendations destinations_result = yield destination_agent.run( messages=f"Recommend destinations for {travel_request.preferences}", thread=destination_agent.get_new_thread(), response_format=DestinationRecommendations ) destinations = cast(DestinationRecommendations, destinations_result.value) top_destination = destinations.recommendations[0] # Steps 2 & 3: Run itinerary and local recommendations IN PARALLEL itinerary_task = itinerary_agent.run( messages=f"Create itinerary for {top_destination.destination_name}", thread=itinerary_agent.get_new_thread(), response_format=Itinerary ) local_task = local_agent.run( messages=f"Local recommendations for {top_destination.destination_name}", thread=local_agent.get_new_thread(), response_format=LocalRecommendations ) results = yield context.task_all([itinerary_task, local_task]) itinerary = cast(Itinerary, results[0].value) local_recs = cast(LocalRecommendations, results[1].value) # Step 4: Save to blob storage yield context.call_activity("save_travel_plan_to_blob", travel_plan) # Step 5: Wait for human approval (NO COMPUTE COSTS while waiting!) approval_task = context.wait_for_external_event("ApprovalEvent") timeout_task = context.create_timer( context.current_utc_datetime + timedelta(days=7)) winner = yield context.task_any([approval_task, timeout_task]) if winner == approval_task: timeout_task.cancel() approval = approval_task.result # Step 6: Book if approved if approval.get("approved"): yield context.call_activity("book_trip", travel_plan) return TravelPlanResult(plan=travel_plan, approved=approval.get("approved")) return TravelPlanResult(plan=travel_plan, approved=False) Notice how the orchestration combines: Agent calls (await agent.RunAsync(...)) for AI-driven decisions Parallel execution (Task.WhenAll) for running multiple agents concurrently Activity calls (await context.CallActivityAsync(...)) for non-intelligent business tasks Human-in-the-loop (await context.WaitForExternalEvent(...)) for approval workflows The orchestration automatically checkpoints after each step. If a failure occurs, completed steps aren't re-executed. The orchestration resumes exactly where it left off, no need for manual intervention. Agent Patterns in Action Agent Chaining: Sequential Handoffs The Travel Planner demonstrates agent chaining where the Destination Recommender's output feeds into both the Itinerary Planner and Local Recommendations agents: C# // Agent 1: Get destination recommendations var destinations = await destinationAgent.RunAsync<DestinationRecommendations>(prompt, thread); var topDestination = destinations.Result.Recommendations.First(); // Agent 2: Create itinerary based on Agent 1's output var itinerary = await itineraryAgent.RunAsync<TravelItinerary>( $"Create itinerary for {topDestination.Name}", thread); Python # Agent 1: Get destination recommendations destinations_result = yield destination_agent.run( messages=prompt, thread=thread, response_format=DestinationRecommendations ) destinations = cast(DestinationRecommendations, destinations_result.value) top_destination = destinations.recommendations[0] # Agent 2: Create itinerary based on Agent 1's output itinerary_result = yield itinerary_agent.run( messages=f"Create itinerary for {top_destination.destination_name}", thread=thread, response_format=Itinerary ) itinerary = cast(Itinerary, itinerary_result.value) Agent Parallelization: Concurrent Execution The app runs the Itinerary Planner and Local Recommendations agents in parallel to reduce latency: C# // Launch both agent calls simultaneously var itineraryTask = itineraryAgent.RunAsync<TravelItinerary>(itineraryPrompt, thread1); var localTask = localAgent.RunAsync<LocalRecommendations>(localPrompt, thread2); // Wait for both to complete await Task.WhenAll(itineraryTask, localTask); Python # Launch both agent calls simultaneously itinerary_task = itinerary_agent.run( messages=itinerary_prompt, thread=thread1, response_format=Itinerary ) local_task = local_agent.run( messages=local_prompt, thread=thread2, response_format=LocalRecommendations ) # Wait for both to complete results = yield context.task_all([itinerary_task, local_task]) itinerary = cast(Itinerary, results[0].value) local_recs = cast(LocalRecommendations, results[1].value) Human-in-the-Loop: Approval Workflows The Travel Planner includes a complete human-in-the-loop pattern. After generating the travel plan, the workflow pauses for user approval: C# // Send approval request notification await context.CallActivityAsync(nameof(RequestApproval), travelPlan); // Wait for approval - NO COMPUTE COSTS OR LLM TOKENS while waiting! var approval = await context.WaitForExternalEvent<ApprovalResponse>( "ApprovalEvent", TimeSpan.FromDays(7)); if (approval.Approved) await context.CallActivityAsync(nameof(BookTrip), travelPlan); Python # Send approval request notification await context.CallActivityAsync(nameof(RequestApproval), travelPlan); # Wait for approval - NO COMPUTE COSTS OR LLM TOKENS while waiting! var approval = await context.WaitForExternalEvent<ApprovalResponse>( "ApprovalEvent", TimeSpan.FromDays(7)); if (approval.Approved) await context.CallActivityAsync(nameof(BookTrip), travelPlan); The API endpoint to handle approval responses: C# [Function(nameof(HandleApprovalResponse))] public async Task HandleApprovalResponse( [HttpTrigger("post", Route = "approve/{instanceId}")] HttpRequestData req, string instanceId, [DurableClient] DurableTaskClient client) { var approval = await req.ReadFromJsonAsync<ApprovalResponse>(); await client.RaiseEventAsync(instanceId, "ApprovalEvent", approval); } Python app.function_name(name="ApproveTravelPlan") app.route(route="travel-planner/approve/{instance_id}", methods=["POST"]) app.durable_client_input(client_name="client") async def approve_travel_plan(req: func.HttpRequest, client) -> func.HttpResponse: instance_id = req.route_params.get("instance_id") approval = req.get_json() await client.raise_event(instance_id, "ApprovalEvent", approval) return func.HttpResponse( json.dumps({"message": "Approval processed"}), status_code=200, mimetype="application/json" ) The workflow generates a complete travel plan and waits up to 7 days for user approval. During this entire waiting period, since we're hosting this application on the Functions Flex Consumption plan, the app scales down and zero compute resources or LLM tokens are consumed. When the user approves (or the timeout expires), the app scales back up and the orchestration automatically resumes with full context intact. Real-Time Monitoring with the Durable Task Scheduler Since we're using the Durable Task Scheduler as the backend for our durable agents, we're provided with a built-in dashboard for monitoring our agents and orchestrations in real-time. Agent Thread Insights Conversation history: View complete conversation threads for each agent session, including all messages, tool calls, and agent decisions Task timing: Monitor how long specific tasks and agent interactions take to complete Orchestration Insights Multi-agent visualization: See the execution flow across multiple agents with visual representation of parallel executions and branching Real-time monitoring: Track active orchestrations, queued work items, and agent states Performance metrics: Monitor response times, token usage, and orchestration duration Debugging Capabilities View structured inputs and outputs for activities, agents, and tool calls Trace tool invocations and their outcomes Monitor external event handling for human-in-the-loop scenarios The dashboard enables you to understand exactly what your agents and workflows are doing, diagnose issues quickly, and optimize performance, all without adding custom logging to your code. Try The Travel Planner Application That’s it! That’s the gist of how the AI Travel Planner application is put together and some of the key components that it took to build it. I'd love for you to try the application out for yourself. It's fully instrumented with the Azure Developer CLI and Bicep, so you can deploy it to Azure with a few simple CLI commands. Click here to try the AI Travel Planner sample Python version coming soon! Demo Video Learn More Durable Task Extension Overview Durable Agent Features Durable Task Scheduler AI Travel Planner GitHub Repository705Views2likes0CommentsCall Function App from Azure Data Factory with Managed Identity Authentication
Integrating Azure Function Apps into your Azure Data Factory (ADF) workflows is a common practice. To enhance security beyond the use of function API keys, leveraging managed identity authentication is strongly recommended. Given the fact that many existing guides were outdated with recent updates to Azure services, this article provides a comprehensive, up-to-date walkthrough on configuring managed identity in ADF to securely call Function Apps. The provided methods can also be adapted to other Azure services that need to call Function Apps with managed identity authentication. The high level process is: Enable Managed Identity on Data Factory Configure Microsoft Entra Sign-in on Azure Function App Configure Linked Service in Data Factory Assign Permissions to the Data Factory in Azure Function Step 1: Enable Managed Identity on Data Factory On the Data Factory’s portal, go to Managed Identities, and enable a system assigned managed identity. Step 2: Configure Microsoft Entra Sign-in on Azure Function App 1. Go to Function App portal and enable Authentication. Choose "Microsoft" as the identity provider. 2. Add an app registration to the app, it could be an existing one or you can choose to let the platform create a new app registration. 3. Next, allow the ADF as a client application to authenticate to the function app. This step is a new requirement from previous guides. If these settings are not correctly set, the 403 response will be returned: Add the Application ID of the ADF managed identity in Allowed client application and Object ID of the ADF managed identity in the Allowed identities. If the requests are only allowed from specific tenants, add the Tenant ID of the managed identity in the last box. 4. This part sets the response from function app for the unauthenticated requests. We should set the response as "HTTP 401 Unauthorized: recommended for APIs" as sign-in page is not feasible for API calls from ADF. 5. Then, click next and use the default permission option. 6. After everything is set, click "Add" to complete the configuration. Copy the generated App (client) id, as this is used in data factory to handle authorization. Step 3: Configure Linked Service in Data Factory 1. To use an Azure Function activity in a pipeline, follow the steps here: Create an Azure Function activity with UI 2. Then Edit or New a Azure Function Linked Service. 3. Change authentication method to System Assigned Managed Identity, and paste the copied client ID of function app identity provider from Step 2 into Resource ID. This step is necessary as authorization does not work without this. Step 4: Assign Permissions to the Data Factory in Azure Function 1. On the function app portal, go to Access control (IAM), and Add a new role assignment. 2. Assign reader role. 3. Assign the Data Factory’s Managed Identity to that role. After everything is set, test that the function app can be called from Azure Data Factory successfully. Reference: https://prodata.ie/2022/06/16/enabling-managed-identity-authentication-on-azure-functions-in-data-factory/ https://learn.microsoft.com/en-us/azure/data-factory/control-flow-azure-function-activity https://docs.azure.cn/en-us/app-service/overview-authentication-authorization1.9KViews0likes2CommentsIndustry-Wide Certificate Changes Impacting Azure App Service Certificates
Executive Summary In early 2026, industry-wide changes mandated by browser applications and the CA/B Forum will affect both how TLS certificates are issued as well as their validity period. The CA/B Forum is a vendor body that establishes standards for securing websites and online communications through SSL/TLS certificates. Azure App Service is aligning with these standards for both App Service Managed Certificates (ASMC, free, DigiCert-issued) and App Service Certificates (ASC, paid, GoDaddy-issued). Most customers will experience no disruption. Action is required only if you pin certificates or use them for client authentication (mTLS). Who Should Read This? App Service administrators Security and compliance teams Anyone responsible for certificate management or application security Quick Reference: What’s Changing & What To Do Topic ASMC (Managed, free) ASC (GoDaddy, paid) Required Action New Cert Chain New chain (no action unless pinned) New chain (no action unless pinned) Remove certificate pinning Client Auth EKU Not supported (no action unless cert is used for mTLS) Not supported (no action unless cert is used for mTLS) Transition from mTLS Validity No change (already compliant) Two overlapping certs issued for the full year None (automated) If you do not pin certificates or use them for mTLS, no action is required. Timeline of Key Dates Date Change Action Required Mid-Jan 2026 and after ASMC migrates to new chain ASMC stops supporting client auth EKU Remove certificate pinning if used Transition to alternative authentication if the certificate is used for mTLS Mar 2026 and after ASC validity shortened ASC migrates to new chain ASC stops supporting client auth EKU Remove certificate pinning if used Transition to alternative authentication if the certificate is used for mTLS Actions Checklist For All Users Review your use of App Service certificates. If you do not pin these certificates and do not use them for mTLS, no action is required. If You Pin Certificates (ASMC or ASC) Remove all certificate or chain pinning before their respective key change dates to avoid service disruption. See Best Practices: Certificate Pinning. If You Use Certificates for Client Authentication (mTLS) Switch to an alternative authentication method before their respective key change dates to avoid service disruption, as client authentication EKU will no longer be supported for these certificates. See Sunsetting the client authentication EKU from DigiCert public TLS certificates. See Set Up TLS Mutual Authentication - Azure App Service Details & Rationale Why Are These Changes Happening? These updates are required by major browser programs (e.g., Chrome) and apply to all public CAs. They are designed to enhance security and compliance across the industry. Azure App Service is automating updates to minimize customer impact. What’s Changing? New Certificate Chain Certificates will be issued from a new chain to maintain browser trust. Impact: Remove any certificate pinning to avoid disruption. Removal of Client Authentication EKU Newly issued certificates will not support client authentication EKU. This change aligns with Google Chrome’s root program requirements to enhance security. Impact: If you use these certificates for mTLS, transition to an alternate authentication method. Shortening of Certificate Validity Certificate validity is now limited to a maximum of 200 days. Impact: ASMC is already compliant; ASC will automatically issue two overlapping certificates to cover one year. No billing impact. Frequently Asked Questions (FAQs) Will I lose coverage due to shorter validity? No. For App Service Certificate, App Service will issue two certificates to span the full year you purchased. Is this unique to DigiCert and GoDaddy? No. This is an industry-wide change. Do these changes impact certificates from other CAs? Yes. These changes are an industry-wide change. We recommend you reach out to your certificates’ CA for more information. Do I need to act today? If you do not pin or use these certs for mTLS, no action is required. Glossary ASMC: App Service Managed Certificate (free, DigiCert-issued) ASC: App Service Certificate (paid, GoDaddy-issued) EKU: Extended Key Usage mTLS: Mutual TLS (client certificate authentication) CA/B Forum: Certification Authority/Browser Forum Additional Resources Changes to the Managed TLS Feature Set Up TLS Mutual Authentication Azure App Service Best Practices – Certificate pinning DigiCert Root and Intermediate CA Certificate Updates 2023 Sunsetting the client authentication EKU from DigiCert public TLS certificates Feedback & Support If you have questions or need help, please visit our official support channels or the Microsoft Q&A, where our team and the community can assist you.1.2KViews1like0Comments