Wicked Smart Data
LearnInsightsAboutContact
Sign InLet's Build
LearnInsightsAboutContact
Sign InLet's Build
Wicked Smart Data

Intelligence, automation, and expert execution — plus an elite library of free knowledge. We turn complexity into competitive advantage.

Start a conversation

Platform

  • Learning Paths
  • Insights
  • RSS Feed

Company

  • About
  • Contact
  • Work With Us

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Wicked Smart Data. All rights reserved.

Intelligence · Automation · Advantage

All Insights
Power Automate

Automating Two-Factor Authentication and OTP Handling in Power Automate Desktop: Intercepting SMS, Email, and Authenticator Codes for Unattended Login Flows

Two-factor authentication breaks most unattended bots — but it doesn't have to break yours. Learn how to intercept SMS OTPs via API, extract email codes with Microsoft Graph, and generate TOTP codes programmatically to complete 2FA login flows in Power Automate Desktop without any human intervention.

🔥 Expert30 min readOct 27, 2026Updated Oct 27, 2026
Automating Two-Factor Authentication and OTP Handling in Power Automate Desktop: Intercepting SMS, Email, and Authenticator Codes for Unattended Login Flows
On this page
  • Introduction
  • Prerequisites
  • Understanding the 2FA Landscape: What You're Actually Intercepting
  • SMS-Based OTP
  • Email-Based OTP
  • TOTP (Authenticator App Codes)
  • Strategy 1: Intercepting SMS OTPs via API
  • Setting Up the SMS Interception Pipeline
  • Polling the SMS Webhook Endpoint
  • Handling the OTP Timing Window
  • Strategy 2: Reading Email OTPs with Outlook and Graph API
  • Outlook COM Automation Approach
  • Microsoft Graph API Approach (Preferred for Unattended)
  • Polling Loop for Email OTPs
  • Strategy 3: Generating TOTP Codes Programmatically
  • How TOTP Works (and Why This Matters)
  • Python TOTP Generator (The Cleanest Approach)
  • PowerShell TOTP Generator (No Python Required)
  • Assembling the Complete Unattended Login Flow
  • Flow Architecture Overview
  • Detecting Which 2FA Prompt Is Active
  • Entering the OTP Code
  • Verifying Successful Login
  • Handling Edge Cases and Application-Specific Quirks
  • "Remember This Device" Checkboxes
  • Captcha-Gated 2FA Prompts
  • Push Notification 2FA (Authenticator App "Approve/Deny")
  • OTP Code Prefilled or Already Visible
  • Error Handling and Resilience Patterns
  • The Lockout Prevention Pattern
  • Handling OTP Delivery Delays
  • Clock Skew and TOTP Failures
  • Hands-On Exercise
  • Common Mistakes & Troubleshooting
  • Mistake 1: Hardcoding TOTP Secrets or API Keys
  • Mistake 2: Retrieving a Stale OTP
  • Mistake 3: Not Accounting for Different OTP Prompt Behaviors
  • Mistake 4: Trusting the Absence of an Error Message as Success
  • Mistake 5: Running TOTP Generation in a Tight Loop Without Backoff
  • Troubleshooting: The OTP Field Won't Accept Typed Text
  • Troubleshooting: TOTP Codes Consistently Wrong
  • Security and Compliance Considerations
  • Summary & Next Steps
  • Automating Two-Factor Authentication and OTP Handling in Power Automate Desktop: Intercepting SMS, Email, and Authenticator Codes to Complete Unattended Login Flows for Legacy Applications

    Introduction

    Picture this: you've built a flawless unattended bot that logs into your company's legacy ERP system every morning at 6 AM, pulls the previous day's transaction data, and pushes it into a reporting database — all before your finance team arrives. It ran perfectly for three months. Then IT Security enabled mandatory two-factor authentication on every application, and your beautiful bot started failing silently at 6:01 AM every single morning. Welcome to one of the most common and least-documented problems in enterprise RPA.

    Two-factor authentication (2FA) and one-time passwords (OTPs) exist specifically to defeat automation — they're designed to require a human to be present, holding a phone or reading an email. The challenge for RPA engineers is that legitimate automation still needs to get through these gates, especially for internal business processes running on systems where IT can't or won't create service accounts without MFA. The good news is that 2FA is a process, not magic, and processes can be automated if you understand where the OTP actually lives and how to get it programmatically before the time window expires.

    By the end of this lesson, you'll be able to build robust, unattended login flows that handle SMS-delivered OTPs, email-based verification codes, and TOTP (Time-based One-Time Password) authenticator codes — the kind generated by apps like Microsoft Authenticator or Google Authenticator. You'll understand the architecture of each approach, the PowerShell and Python scripts that extract codes programmatically, and the error-handling patterns that prevent your bot from locking out service accounts at 3 AM.

    What you'll learn:

    • How each 2FA delivery mechanism works and which interception strategy applies to each
    • Building unattended flows that retrieve SMS OTPs via SMS gateway APIs and Windows SMS forwarding
    • Extracting email OTPs programmatically using Outlook automation and Graph API calls from within a desktop flow
    • Generating TOTP codes without a phone using shared secret keys and Python/PowerShell scripts
    • Composing a resilient end-to-end unattended login flow with proper timeout handling, retry logic, and account lockout prevention

    Prerequisites

    Before working through this material, you should have solid experience with:

    • Power Automate Desktop flows at an intermediate-to-advanced level — if you're newer to the platform, work through Getting Started with Power Automate Desktop: Installing, Recording, and Running Your First Desktop Flow first
    • Handling Credentials Securely in Desktop Flows: Sensitive Variables and Azure Key Vault — OTP secrets and API keys must never be hardcoded
    • Attended vs Unattended RPA: Choosing a Run Mode and Configuring Machines in Power Automate — this lesson assumes you're building for unattended execution
    • Comfort with PowerShell and basic Python — we'll use both for cryptographic operations
    • At minimum, familiarity with REST APIs and JSON parsing

    Understanding the 2FA Landscape: What You're Actually Intercepting

    Before writing a single action in the flow designer, you need to understand the technical architecture of each 2FA type. The strategy changes completely depending on how the OTP arrives.

    SMS-Based OTP

    When a user triggers 2FA on an application, the application's backend calls an SMS gateway (Twilio, AWS SNS, Azure Communication Services, or a carrier-direct service). The gateway delivers a text message to the registered phone number. The OTP is valid for somewhere between 30 seconds and 10 minutes, depending on the application.

    For unattended automation, you have three viable paths to intercept SMS OTPs:

    1. SMS gateway webhook — If your organization controls the SMS gateway (or can configure it), you can set up a webhook that posts the incoming message body to a cloud endpoint, which your desktop flow then polls
    2. Windows SMS forwarding — A Windows 10/11 machine linked to an Android phone via the "Phone Link" app can receive SMS messages; you can read these from Outlook or the Phone Link app directly
    3. SIM-hosted virtual number with API access — Services like Twilio provide a phone number you can rent; SMS messages to that number are accessible via REST API

    Email-Based OTP

    Email OTP is architecturally simpler. The application sends an email to the registered address, and you read that email programmatically. The two main approaches are:

    1. Outlook COM automation — Power Automate Desktop can drive Outlook directly, read the inbox, and extract the OTP from the email body using text parsing or regex
    2. Microsoft Graph API — A more robust approach that calls the Graph API from a PowerShell script inside your flow, reading messages from a shared mailbox or service account inbox

    TOTP (Authenticator App Codes)

    TOTP is the most technically interesting case. Apps like Microsoft Authenticator and Google Authenticator don't receive a code — they generate one using a shared secret key and the current time. The algorithm (RFC 6238) is completely deterministic: given the secret key and the current UTC timestamp, you can calculate the same 6-digit code the authenticator app would show.

    This means if you have the TOTP secret key (the QR code's underlying value), you can generate codes programmatically without any phone, API call, or external service. This is the cleanest solution when it's available.

    Key insight

    The real challenge with TOTP isn't the cryptography — a 10-line Python script handles that. The challenge is obtaining and securely storing the TOTP secret, which must happen at enrollment time before the QR code is scanned by an authenticator app. If you miss this window, you'll need to re-enroll the service account to retrieve the secret.


    Strategy 1: Intercepting SMS OTPs via API

    Setting Up the SMS Interception Pipeline

    The most reliable SMS interception approach for enterprise use combines a virtual phone number (via Twilio or a similar service) registered to your service account, with a lightweight Azure Function or Logic App acting as a webhook receiver.

    Here's the architecture:

    1. The application texts your Twilio number with the OTP
    2. Twilio forwards the SMS body to an HTTPS webhook endpoint (your Azure Function)
    3. The Azure Function stores the most recent OTP in an Azure Storage Table or Cosmos DB record, keyed by phone number
    4. Your desktop flow, running as an unattended bot, polls that endpoint via a PowerShell HTTP call and retrieves the code

    Let's build the polling side inside Power Automate Desktop.

    Polling the SMS Webhook Endpoint

    Inside your desktop flow, after triggering the 2FA prompt (by submitting the username and password), add a Run PowerShell Script action. If you're not yet comfortable with the PowerShell action, see Scripting Inside Desktop Flows: Running PowerShell, Python, and VBScript Actions for the foundational setup.

    # Poll the SMS OTP endpoint with exponential backoff
    param(
        [string]$ApiEndpoint,
        [string]$ApiKey,
        [string]$PhoneNumber
    )
    
    $maxAttempts = 12
    $waitSeconds = 5
    $otpCode = $null
    
    for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) {
        $headers = @{
            "x-api-key"    = $ApiKey
            "Content-Type" = "application/json"
        }
    
        $uri = "$ApiEndpoint/otp/latest?phone=$PhoneNumber"
    
        try {
            $response = Invoke-RestMethod -Uri $uri -Method GET -Headers $headers -TimeoutSec 10
    
            if ($response.otp -and $response.timestamp) {
                # Check the OTP is no older than 90 seconds
                $otpAge = (Get-Date).ToUniversalTime() - [DateTime]::Parse($response.timestamp)
                if ($otpAge.TotalSeconds -lt 90) {
                    $otpCode = $response.otp
                    break
                }
            }
        }
        catch {
            # Log but continue polling
            Write-Output "Attempt $attempt failed: $_"
        }
    
        # Exponential backoff capped at 30 seconds
        $sleepTime = [Math]::Min($waitSeconds * $attempt, 30)
        Start-Sleep -Seconds $sleepTime
    }
    
    if (-not $otpCode) {
        throw "OTP not received within timeout window"
    }
    
    Write-Output $otpCode
    

    In the PowerShell action configuration, set Script output variable to %OTPResult%. Then capture the output:

    # In PAD flow after the PowerShell action:
    SET %OTPCode% TO %OTPResult%
    

    Warning

    Never pass your API key as a hardcoded string in the script. Retrieve it from Azure Key Vault at the start of your flow and pass it as a variable. A hardcoded key in a flow script is visible in plain text to anyone with flow edit access.

    Handling the OTP Timing Window

    SMS OTPs typically expire between 60 seconds and 10 minutes. Your polling loop above has a maximum runtime of approximately 5.5 minutes (12 attempts × averaging 15 seconds each with backoff), which covers most application timeout windows. However, there's a subtlety: by the time your flow retrieves the OTP and types it into the application, additional seconds have passed.

    Always retrieve the OTP as late as possible — right before typing it — and build in logic to detect expiry messages so you can retry rather than hard-fail. We'll cover this in the error-handling section.


    Strategy 2: Reading Email OTPs with Outlook and Graph API

    Outlook COM Automation Approach

    If your service account uses Outlook and the email OTP arrives in its inbox, you can drive Outlook directly. This approach works well in attended scenarios or unattended flows where Outlook is already open and the service account profile is loaded.

    Power Automate Desktop has dedicated Outlook actions under the Email category. However, for the level of filtering you need — finding the most recent unread message from a specific sender, within the last two minutes, with a subject matching a pattern — the built-in actions are often too blunt. A PowerShell COM approach gives you precise control:

    param(
        [string]$SenderAddress,
        [string]$SubjectContains
    )
    
    Add-Type -AssemblyName "Microsoft.Office.Interop.Outlook"
    $outlook = New-Object -ComObject Outlook.Application
    $namespace = $outlook.GetNamespace("MAPI")
    $inbox = $namespace.GetDefaultFolder(6)  # 6 = Inbox
    
    # Search for recent messages (last 3 minutes)
    $cutoff = (Get-Date).AddMinutes(-3)
    $otpCode = $null
    
    foreach ($mail in $inbox.Items) {
        if ($mail.ReceivedTime -ge $cutoff -and
            $mail.SenderEmailAddress -eq $SenderAddress -and
            $mail.Subject -like "*$SubjectContains*") {
    
            # Extract 6-digit OTP from email body using regex
            $body = $mail.Body
            $match = [regex]::Match($body, '\b(\d{6})\b')
            if ($match.Success) {
                $otpCode = $match.Groups[1].Value
                break
            }
        }
    }
    
    if (-not $otpCode) {
        throw "No OTP email found within the time window"
    }
    
    Write-Output $otpCode
    

    Note

    The regex \b(\d{6})\b captures any standalone 6-digit number. For applications that use 8-digit or alphanumeric OTPs, adjust accordingly. Some email templates embed the OTP in an HTML link or button — if $mail.Body returns HTML, you may need to parse $mail.HTMLBody and strip tags, or use the $mail.Body plain-text property if the email is multipart.

    Microsoft Graph API Approach (Preferred for Unattended)

    The Outlook COM approach has a critical weakness: it requires Outlook to be installed and a profile loaded under the service account running the bot. In many unattended environments, this isn't the case, or IT policy prevents it.

    The Graph API approach is architecturally cleaner: your bot authenticates to Microsoft Graph using client credentials (app registration with Mail.Read permission on a shared mailbox), and fetches the message programmatically without needing any local email client.

    param(
        [string]$TenantId,
        [string]$ClientId,
        [string]$ClientSecret,
        [string]$MailboxAddress,
        [string]$SenderFilter
    )
    
    # Acquire access token using client credentials flow
    $tokenUri = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
    $tokenBody = @{
        client_id     = $ClientId
        scope         = "https://graph.microsoft.com/.default"
        client_secret = $ClientSecret
        grant_type    = "client_credentials"
    }
    
    $tokenResponse = Invoke-RestMethod -Method POST -Uri $tokenUri -Body $tokenBody
    $accessToken = $tokenResponse.access_token
    
    # Query inbox for recent messages from the OTP sender
    $graphUri = "https://graph.microsoft.com/v1.0/users/$MailboxAddress/mailFolders/inbox/messages"
    $graphUri += "?`$filter=from/emailAddress/address eq '$SenderFilter'"
    $graphUri += " and receivedDateTime ge " + (Get-Date).AddMinutes(-5).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
    $graphUri += "&`$orderby=receivedDateTime desc&`$top=1&`$select=body,receivedDateTime,subject"
    
    $headers = @{ Authorization = "Bearer $accessToken" }
    $messages = Invoke-RestMethod -Uri $graphUri -Headers $headers
    
    if ($messages.value.Count -eq 0) {
        throw "No OTP email received in the last 5 minutes"
    }
    
    $emailBody = $messages.value[0].body.content
    $match = [regex]::Match($emailBody, '\b(\d{6})\b')
    
    if (-not $match.Success) {
        throw "Could not extract OTP from email body"
    }
    
    Write-Output $match.Groups[1].Value
    

    Store TenantId, ClientId, and ClientSecret in Azure Key Vault and retrieve them at flow start using the Key Vault action. This keeps your client secret out of the flow definition entirely.

    Tip

    Create a dedicated shared mailbox (e.g., rpa-otp@yourcompany.com) for your service accounts' OTP emails rather than using an individual's mailbox. This separates concerns, makes permissions easier to manage, and prevents personal email from cluttering the OTP search space.

    Polling Loop for Email OTPs

    Because email delivery isn't instantaneous, you need a retry loop around your OTP retrieval script. In PAD, structure this with a Loop action combined with conditional exit:

    SET %MaxAttempts% TO 10
    SET %AttemptCount% TO 0
    SET %OTPFound% TO False
    
    LOOP WHILE %OTPFound% = False AND %AttemptCount% < %MaxAttempts%
        SET %AttemptCount% TO %AttemptCount% + 1
        
        RUN PowerShell script with parameters -> %RawOTP%, %ScriptError%
        
        IF %ScriptError% IS EMPTY THEN
            SET %OTPCode% TO %RawOTP%
            SET %OTPFound% TO True
        ELSE
            WAIT 10 seconds
        END IF
    END LOOP
    
    IF %OTPFound% = False THEN
        THROW "Email OTP not received after maximum attempts"
    END IF
    

    Warning

    Be careful about how many failed attempts you're racking up against the application's lockout policy. If the OTP email is delayed and your bot retries the login trigger (not just the OTP retrieval), you could trigger the wrong re-send and confuse your timing logic. Only retry the email retrieval loop, not the login trigger itself.


    Strategy 3: Generating TOTP Codes Programmatically

    How TOTP Works (and Why This Matters)

    TOTP (RFC 6238) generates codes using HMAC-SHA1 applied to a combination of the shared secret and the current time period (typically a 30-second window). The secret is encoded in Base32 and is the same value embedded in the QR code that authenticator apps scan during enrollment.

    The critical moment is enrollment: when IT or the application presents the QR code for the user to scan, you need to capture the raw secret key before someone scans it with a personal authenticator app. Most applications also display the secret as a text string below the QR code for manual entry — copy this value.

    Once you have the secret, you never need a phone again. Store it in Azure Key Vault and generate codes on demand.

    Python TOTP Generator (The Cleanest Approach)

    Python's pyotp library makes TOTP generation trivial. If Python is available on your bot machine (and it should be — it's a standard enterprise tool), this is the most readable implementation:

    import pyotp
    import sys
    import time
    
    secret_key = sys.argv[1]  # Passed from PAD as argument
    
    # pyotp handles all the RFC 6238 math
    totp = pyotp.TOTP(secret_key)
    current_code = totp.now()
    
    # Calculate seconds remaining in current window
    remaining = 30 - (int(time.time()) % 30)
    
    # If we're in the last 5 seconds of a window, wait for the next one
    # This prevents submitting a code that expires before the server validates it
    if remaining <= 5:
        time.sleep(remaining + 1)
        current_code = totp.now()
    
    print(current_code)
    

    Call this from a Run Python Script action in PAD, passing the secret key (retrieved from Key Vault) as the script argument. The output is your 6-digit code.

    Install pyotp on your bot machine beforehand:

    pip install pyotp
    

    PowerShell TOTP Generator (No Python Required)

    If you're working in a locked-down environment where Python isn't available or isn't approved, here's a pure PowerShell implementation of RFC 6238:

    param([string]$Base32Secret)
    
    function ConvertFrom-Base32 {
        param([string]$Base32)
        $Base32 = $Base32.ToUpper().TrimEnd('=')
        $alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
        $bits = ""
        foreach ($char in $Base32.ToCharArray()) {
            $val = $alphabet.IndexOf($char)
            $bits += [Convert]::ToString($val, 2).PadLeft(5, '0')
        }
        $bytes = @()
        for ($i = 0; $i -lt $bits.Length - 7; $i += 8) {
            $bytes += [Convert]::ToByte($bits.Substring($i, 8), 2)
        }
        return [byte[]]$bytes
    }
    
    function Get-TOTP {
        param([string]$Secret)
    
        $secretBytes = ConvertFrom-Base32 -Base32 $Secret
        $epoch = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
        $timeStep = [Math]::Floor($epoch / 30)
    
        # TOTP requires big-endian 8-byte counter
        $counterBytes = [BitConverter]::GetBytes([int64]$timeStep)
        if ([BitConverter]::IsLittleEndian) {
            [Array]::Reverse($counterBytes)
        }
    
        $hmac = New-Object System.Security.Cryptography.HMACSHA1
        $hmac.Key = $secretBytes
        $hash = $hmac.ComputeHash($counterBytes)
    
        # Dynamic truncation
        $offset = $hash[19] -band 0x0F
        $truncated = (($hash[$offset] -band 0x7F) -shl 24) -bor
                     (($hash[$offset+1] -band 0xFF) -shl 16) -bor
                     (($hash[$offset+2] -band 0xFF) -shl 8) -bor
                     ($hash[$offset+3] -band 0xFF)
    
        $otp = $truncated % 1000000
        return $otp.ToString("000000")
    }
    
    # Check time remaining in current window
    $epoch = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
    $remaining = 30 - ($epoch % 30)
    if ($remaining -le 5) {
        Start-Sleep -Seconds ($remaining + 1)
    }
    
    Write-Output (Get-TOTP -Secret $Base32Secret)
    

    Key insight

    The 5-second window buffer at the end of each TOTP period is critical in unattended flows. If your bot generates a code with 3 seconds left on the clock, types it into the application over 2 seconds, and the server validates it 1 second after the window rolls over, you'll get an authentication failure even though the code was technically valid when generated. Always pad into the next window if you're close to a rollover.


    Assembling the Complete Unattended Login Flow

    Now let's put the pieces together into a production-ready flow structure. The architecture below handles all three OTP types through a subflow pattern, keeping the main login logic clean.

    Flow Architecture Overview

    MAIN FLOW
    ├── Retrieve credentials from Key Vault
    ├── Launch application (browser or desktop app)
    ├── Enter username and password
    ├── Detect 2FA prompt type
    │   ├── SMS → Call GetSMSOTP subflow
    │   ├── Email → Call GetEmailOTP subflow
    │   └── TOTP → Call GenerateTOTP subflow
    ├── Enter OTP code
    ├── Verify successful login
    └── Proceed with business logic
    

    This pattern, using subflows for modular OTP handling, follows the principles described in Subflows and Reusable Logic in Power Automate Desktop. The OTP subflows can be reused across multiple desktop flows that log into different systems.

    Detecting Which 2FA Prompt Is Active

    One of the trickier parts of the complete flow is detecting which type of 2FA the application is presenting. Most legacy applications show a prompt with static text indicating the delivery method. Use a Wait for UI Element action with a timeout to detect each possible prompt, then branch accordingly.

    In PAD, the pattern looks like this (using UI element existence checks):

    # Try to detect SMS prompt (5 second timeout each)
    UI Element Exists: [SMS verification prompt] -> %SMSPromptFound%
    UI Element Exists: [Email verification prompt] -> %EmailPromptFound%  
    UI Element Exists: [Authenticator code prompt] -> %TOTPPromptFound%
    
    IF %SMSPromptFound% = True THEN
        CALL Subflow: GetSMSOTP
        SET %OTPCode% TO %SubflowOutput%
    ELSE IF %EmailPromptFound% = True THEN
        CALL Subflow: GetEmailOTP
        SET %OTPCode% TO %SubflowOutput%
    ELSE IF %TOTPPromptFound% = True THEN
        CALL Subflow: GenerateTOTP
        SET %OTPCode% TO %SubflowOutput%
    ELSE
        THROW "Unrecognized 2FA prompt type — manual intervention required"
    END IF
    

    For selectors on legacy applications that may have fragile UI trees, review the strategies in UI Elements and Selectors in Power Automate Desktop: Building Automations That Don't Break.

    Entering the OTP Code

    Once you have the OTP code as a string variable, typing it sounds trivial — but there are subtleties:

    Field behavior: Some OTP fields accept all 6 digits in one text field. Others split into individual digit boxes (common in web applications). And some legacy thick-client apps have OTP fields that don't accept keyboard paste — only keystroke-by-keystroke input.

    For single-field entry:

    Focus UI Element: [OTP input field]
    Clear: [OTP input field]
    Populate Text Field: [OTP input field] with %OTPCode%
    

    For split digit fields (six individual boxes), loop through each character:

    SET %OTPDigits% TO Split(%OTPCode%, "")
    SET %BoxIndex% TO 1
    
    FOREACH %Digit% IN %OTPDigits%
        Focus UI Element: [OTP digit box %BoxIndex%]
        Send Keys: %Digit%
        SET %BoxIndex% TO %BoxIndex% + 1
    END FOREACH
    

    For legacy applications that reject simulated keyboard input from standard PAD actions, use the Send Keys action with physical key simulation, or fall back to clipboard-based paste. The Automating Windows Clipboard Operations in Power Automate Desktop article covers the clipboard approach in detail.

    Tip

    After entering the OTP, don't immediately click Submit. Add a 500ms wait first. Some OTP fields have JavaScript validation that runs on focus-out events, and clicking Submit too quickly can submit before validation completes, causing a silent failure that looks like an incorrect code.

    Verifying Successful Login

    After submitting the OTP, your flow must verify that authentication succeeded before proceeding. Don't assume success based on the prompt disappearing. Check for a positive indicator (a dashboard element, a welcome message, the application's main menu) rather than the absence of an error:

    Wait for UI Element to appear: [Main application dashboard]
      Timeout: 15 seconds
      On timeout: THROW "Login verification failed after OTP submission"
    

    Additionally, check for common failure states:

    UI Element Exists: [Incorrect code error message] -> %OTPError%
    UI Element Exists: [Account locked message] -> %AccountLocked%
    
    IF %AccountLocked% = True THEN
        # Critical: stop retrying immediately
        SEND Email Alert to operations team
        THROW "Service account locked — immediate intervention required"
    END IF
    
    IF %OTPError% = True THEN
        # Handle incorrect OTP (can retry once)
        ...
    END IF
    

    The account lockout check is non-negotiable. If your bot is feeding wrong OTP codes due to a timing bug or stale cached value, it will lock the service account within 3-5 attempts on most enterprise systems. Catching this early prevents an incident.


    Handling Edge Cases and Application-Specific Quirks

    "Remember This Device" Checkboxes

    Many applications offer to remember the browser or device after successful 2FA, skipping the OTP prompt for subsequent logins. For unattended bots, this is a double-edged sword:

    • Benefit: If the session persists, your bot won't need to handle 2FA on every run
    • Risk: If the session is tied to a browser profile that gets cleared, or the "remember" period expires, your bot will suddenly encounter a 2FA prompt it's not expecting

    The safer approach is to always expect 2FA might appear and handle it gracefully, rather than relying on session persistence. Use the detection pattern above and simply proceed normally if no 2FA prompt appears.

    For browser-based applications, see Web Automation in Power Automate Desktop: Browser Actions, Form Filling, and Data Extraction for managing browser sessions and profiles in unattended contexts.

    Captcha-Gated 2FA Prompts

    Some applications layer a CAPTCHA challenge on top of 2FA, particularly if multiple failed logins triggered a security check. This is outside the scope of programmatic solving (and ethically murky if it's a third-party CAPTCHA service). Your best options:

    1. Alert operations and pause the flow for attended intervention
    2. Work with the application owner to whitelist your bot's IP or service account from CAPTCHA requirements
    3. If the application is internal, work with IT to disable CAPTCHA for the service account

    Push Notification 2FA (Authenticator App "Approve/Deny")

    Microsoft Authenticator and similar apps can be configured for push notifications — instead of entering a code, the user gets a notification saying "Approve this sign-in?" This cannot be automated through Power Automate Desktop alone.

    Options for push notification 2FA:

    • Request IT to configure the service account to use TOTP (code-based) mode instead of push notifications
    • Use Microsoft Entra ID Conditional Access policies to exempt the automation service account from MFA requirements based on a Managed Device or network location condition
    • For Azure AD-integrated applications, use a service principal with client credentials instead of a user account

    Warning

    Push-based 2FA is not automatable through standard UI means. Do not attempt to automate the phone's touchscreen via an Android automation framework — this violates most terms of service and creates a massive security hole. The right solution is always a configuration change, not a technical hack.

    OTP Code Prefilled or Already Visible

    Some applications pre-populate the OTP field with a code sent via a method your bot didn't trigger, or the previous login session left a stale OTP visible. Before entering your retrieved code, always clear the field first using the Clear action or Ctrl+A followed by Delete.


    Error Handling and Resilience Patterns

    The Lockout Prevention Pattern

    Account lockout is the most dangerous failure mode in 2FA automation. Most enterprise applications lock accounts after 3-5 consecutive failed authentication attempts, and 2FA failures typically count toward this limit.

    Structure your error handling so that OTP failures are counted globally, not per-run:

    # PowerShell: Read/write failure count from shared state (Azure Storage Table)
    param([string]$AccountName, [string]$StorageConnectionString, [string]$Action)
    
    $ctx = New-AzStorageContext -ConnectionString $StorageConnectionString
    $tableName = "BotLoginState"
    
    if ($Action -eq "Read") {
        $entity = Get-AzTableRow -Table (Get-AzStorageTable -Name $tableName -Context $ctx).CloudTable `
                  -PartitionKey "OTPFailures" -RowKey $AccountName
        Write-Output ($entity.FailureCount ?? 0)
    }
    elseif ($Action -eq "Increment") {
        # Upsert failure count
        $entity = @{ PartitionKey = "OTPFailures"; RowKey = $AccountName; FailureCount = 1 }
        $existing = Get-AzTableRow -Table (Get-AzStorageTable -Name $tableName -Context $ctx).CloudTable `
                    -PartitionKey "OTPFailures" -RowKey $AccountName
        if ($existing) { $entity.FailureCount = $existing.FailureCount + 1 }
        Add-AzTableRow -Table (Get-AzStorageTable -Name $tableName -Context $ctx).CloudTable `
                       -PartitionKey $entity.PartitionKey -RowKey $entity.RowKey `
                       -Property @{FailureCount = $entity.FailureCount} -UpdateExisting
    }
    elseif ($Action -eq "Reset") {
        Remove-AzTableRow -Table (Get-AzStorageTable -Name $tableName -Context $ctx).CloudTable `
                          -PartitionKey "OTPFailures" -RowKey $AccountName
    }
    

    In your main flow:

    # Read current failure count before attempting login
    RUN PowerShell: Read failure count -> %FailureCount%
    
    IF %FailureCount% >= 2 THEN
        THROW "Lockout threshold reached — halting to protect service account"
    END IF
    
    # Attempt login with OTP...
    
    IF Login succeeded THEN
        RUN PowerShell: Reset failure count
    ELSE
        RUN PowerShell: Increment failure count
        IF %FailureCount% + 1 >= 2 THEN
            SEND Alert email
        END IF
        THROW "OTP login failed"
    END IF
    

    This pattern stops the bot from making that fatal third attempt. Better to alert operations than to lock the account and cause a service outage.

    For comprehensive error handling patterns, including On Block Error and retry policies, see Error Handling in Desktop Flows: On Block Error, Retry Policies, and Recovery Screenshots.

    Handling OTP Delivery Delays

    SMS and email delivery can lag. Carrier delays, spam filter processing, and mail server queuing all introduce latency. Your polling loops must account for this without spinning too aggressively (which wastes resources) or timing out too early (which causes false failures).

    A practical polling schedule for email OTPs:

    • Attempts 1-3: 5 second intervals (handles fast delivery)
    • Attempts 4-6: 10 second intervals (handles normal delivery)
    • Attempts 7-10: 20 second intervals (handles delayed delivery)
    • After 10 attempts: fail and alert

    Total maximum wait: 5×3 + 10×3 + 20×4 = 125 seconds ≈ 2 minutes. This fits within most OTP validity windows while not running forever.

    Clock Skew and TOTP Failures

    TOTP is time-sensitive to within ±30 seconds. If your bot machine's clock drifts more than 30 seconds from UTC, every TOTP code will be wrong and the bot will keep failing mysteriously.

    Ensure your bot machine syncs to an NTP server. Add a clock check to your flow startup:

    # Verify machine time is within acceptable skew of an NTP server
    $ntpServer = "time.windows.com"
    $ntpData = [byte[]]::new(48)
    $ntpData[0] = 0x1B  # NTP request header
    
    $udpClient = New-Object System.Net.Sockets.UdpClient
    $udpClient.Connect($ntpServer, 123)
    $udpClient.Send($ntpData, $ntpData.Length) | Out-Null
    $response = $udpClient.Receive([ref]$null)
    $udpClient.Close()
    
    $intPart = [BitConverter]::ToUInt32($response[40..43], 0)
    $fracPart = [BitConverter]::ToUInt32($response[44..47], 0)
    $ntpTime = [DateTime]::new(1900, 1, 1).AddSeconds(
        ([System.Net.IPAddress]::NetworkToHostOrder([int]$intPart)) +
        ([System.Net.IPAddress]::NetworkToHostOrder([int]$fracPart) / [Math]::Pow(2, 32))
    )
    
    $localTime = [DateTime]::UtcNow
    $skewSeconds = [Math]::Abs(($localTime - $ntpTime).TotalSeconds)
    
    if ($skewSeconds -gt 25) {
        throw "Machine clock skew of $skewSeconds seconds detected — TOTP will fail. Sync machine time before proceeding."
    }
    
    Write-Output "Clock skew: $skewSeconds seconds (acceptable)"
    

    Hands-On Exercise

    Scenario: You're automating the daily reconciliation process for a financial services company. Their legacy loan management system (a thick-client Windows application from 2009) has recently enabled mandatory 2FA. The system sends a 6-digit OTP via email to rpa-reconciliation@financecompany.com — a shared mailbox your team controls. The OTP arrives within 60 seconds and is valid for 5 minutes. Your bot runs unattended at 5:30 AM daily.

    Your task: Build a complete desktop flow that:

    1. Retrieves the Graph API credentials (Tenant ID, Client ID, Client Secret) from Azure Key Vault
    2. Launches the loan management application using the stored username and password
    3. After entering credentials, waits for the OTP prompt to appear (up to 30 seconds)
    4. Calls the Graph API PowerShell subflow to retrieve the most recent OTP email from the shared mailbox
    5. Extracts the 6-digit code using regex
    6. Enters the code into the OTP field and submits
    7. Verifies the main application dashboard loads
    8. Tracks consecutive failures and halts with an alert if 2 failures occur

    Steps to complete:

    1. Create an Azure App Registration with Mail.Read permission on the shared mailbox
    2. Store the three credentials in Azure Key Vault and connect your PAD flow to retrieve them
    3. Build the main flow with proper error handling blocks around each phase (credential retrieval, application launch, OTP retrieval, OTP entry)
    4. Create a GetEmailOTP subflow containing the PowerShell Graph API script — parameterize sender address and mailbox as subflow inputs
    5. Test in attended mode first with a live OTP email, verifying regex extraction works with your specific email template
    6. Test edge cases: delayed email (pause email delivery for 90 seconds artificially), incorrect code (modify the regex to extract the wrong number), and the application already logged in (skip the OTP entry entirely)
    7. Deploy for unattended execution and verify the first 5 AM run succeeds

    For guidance on structuring the unattended deployment, see Attended vs Unattended RPA: Choosing a Run Mode and Configuring Machines in Power Automate.


    Common Mistakes & Troubleshooting

    Mistake 1: Hardcoding TOTP Secrets or API Keys

    The TOTP secret key is functionally equivalent to the account password — anyone who has it can generate valid codes indefinitely. Storing it in a flow variable that's visible in the designer, in a plain text file on the bot machine, or in a PAD environment variable without sensitivity marking is a critical security failure. Use Azure Key Vault, full stop. See the credential handling guide for implementation details: Handling Credentials Securely in Desktop Flows: Sensitive Variables and Azure Key Vault.

    Mistake 2: Retrieving a Stale OTP

    If your email polling finds an OTP email from a previous run (because the old email is still in the inbox), your bot enters a code that was valid 24 hours ago. Always filter emails by receivedDateTime in your Graph API query, using a 5-minute lookback window. And after a successful login, mark the OTP email as read or move it to a processed folder so it can't be mistakenly retrieved again.

    Mistake 3: Not Accounting for Different OTP Prompt Behaviors

    Some applications show the 2FA prompt on the same page as the login form (appears after password validation, without navigation). Others redirect to a separate page or open a modal dialog. Your UI element selectors must be tested for both the element's presence and its interactable state. A prompt that's animating into view may be detectable but not yet focusable.

    Mistake 4: Trusting the Absence of an Error Message as Success

    After submitting the OTP, don't conclude success just because no error dialog appeared. Some applications fail silently by re-displaying the login form. Always check for a positive success indicator.

    Mistake 5: Running TOTP Generation in a Tight Loop Without Backoff

    If your TOTP code fails (perhaps due to clock skew), and your bot immediately generates another code and tries again, it's using the same 30-second window and will get the same code. Wait at least 31 seconds before retrying to get a fresh code — and fix the underlying clock issue before the next run.

    Troubleshooting: The OTP Field Won't Accept Typed Text

    Symptom: Your bot sends the OTP code, but the field stays empty or shows the wrong characters.

    Causes and fixes:

    • JavaScript-managed input field: Use the PAD Set Value of Element on Web Page action instead of Populate Text Field for browser-based fields
    • Field requires keyboard events, not just value setting: Use Send Keys with the focus set to the field
    • Field has input masking: The field may be reordering or transforming input as it arrives. Try slowing down keystroke delivery with a 50ms delay between characters
    • IME interference: If the bot machine has an Input Method Editor active, disable it before sending numeric OTP codes

    Troubleshooting: TOTP Codes Consistently Wrong

    Check these in order:

    1. Verify the Base32 secret doesn't have spaces or line breaks (copy it carefully from the enrollment screen)
    2. Check the algorithm — most apps use SHA1, but some use SHA256 or SHA512
    3. Check the digits — most apps use 6 digits, some use 8
    4. Check the period — most apps use 30 seconds, some use 60
    5. Run w32tm /query /status on the bot machine to check NTP sync status

    Security and Compliance Considerations

    Before deploying any of these patterns, have an honest conversation with your security and compliance teams. Automating 2FA isn't inherently wrong, but it does change the risk profile:

    What you're doing: Moving the second factor from "something the user physically possesses (phone)" to "something stored in a key vault." This is actually a reasonable trade-off for a service account — service accounts don't have physical possession of anything anyway.

    What you must document: Your security audit trail should show that OTP secrets are stored in Key Vault with access logging enabled, that only the bot's managed identity (not individual engineers) has Get Secret permission, and that the Key Vault access is reviewed quarterly.

    Regulatory considerations: In environments subject to SOX, HIPAA, or PCI-DSS, automating 2FA for service accounts often requires a formal risk acceptance sign-off. The argument that "the bot has no physical presence, so SMS/app-based 2FA is architecturally impossible for service accounts" is well-understood and generally accepted, but it needs to be in writing.

    The cleanest long-term solution — and the one you should be pushing for — is service accounts that authenticate via certificate-based or service principal authentication, with MFA exempted at the platform level via Conditional Access. Automating 2FA interception is a tactical solution for systems that can't support proper service account authentication. Treat it as technical debt, not a permanent pattern.


    Summary & Next Steps

    You've now built a complete mental model and implementation toolkit for handling 2FA in unattended Power Automate Desktop flows. The three strategies — SMS gateway API polling, email OTP extraction via Graph API, and programmatic TOTP generation — cover the vast majority of 2FA configurations you'll encounter in enterprise environments. The lockout prevention pattern and clock skew detection are the production-hardening details that separate a proof-of-concept from something you'd trust to run at 5:30 AM without supervision.

    The key architectural insight to carry forward: treat OTP retrieval as a service with a defined interface (call subflow, receive code string) rather than inline logic. This allows you to swap interception strategies when applications change their 2FA delivery method — and they will change it — without rewriting your core login flow.

    Next steps to deepen your expertise:

    • Explore Building a Resilient Unattended RPA Orchestration Framework in Power Automate Desktop to understand how OTP-secured logins fit into larger queue-driven automation architectures
    • Review Monitoring and Troubleshooting Desktop Flow Runs at Scale to build alerting around OTP failure events before they become lockout incidents
    • Study Triggering Desktop Flows from Cloud Flows: Passing Inputs and Returning Outputs if you want to move the SMS or email OTP polling logic into a cloud flow trigger and pass the pre-resolved code to your desktop flow as an input — this is an elegant alternative that offloads the polling to cloud infrastructure
    • If you're working with systems that use certificate-based authentication instead of passwords, investigate Power Platform service principals as a path away from user-credential automation entirely

    The tools exist. The patterns are proven. Build it right, document the security trade-offs, and your bots will sail through MFA walls that stop lesser automations cold.

    Work With Us

    From insight to implementation

    Reading is the start. When you're ready to build the data, automation, or AI systems behind it, our team turns strategy into shipped results.

    Let's Build

    Power Automate Desktop & RPA

    Previous

    Automating Structured Data Extraction from Web Tables and Dynamic Grids in Power Automate Desktop: Scraping, Paginating, and Writing Results to Excel Without Manual Copying

    Related Insights

    Power AutomateExpert

    Automating Multi-Step Data Collection with Microsoft Forms, SharePoint, and Outlook in Power Automate: Building a Complete Request Intake, Storage, and Confirmation Flow

    29 min
    Power AutomatePractitioner

    Implementing Power Automate Suspension and Drain Strategies for Zero-Downtime Solution Upgrades

    21 min
    Power AutomatePractitioner

    Automating Structured Data Extraction from Web Tables and Dynamic Grids in Power Automate Desktop: Scraping, Paginating, and Writing Results to Excel Without Manual Copying

    24 min

    On this page

    • Introduction
    • Prerequisites
    • Understanding the 2FA Landscape: What You're Actually Intercepting
    • SMS-Based OTP
    • Email-Based OTP
    • TOTP (Authenticator App Codes)
    • Strategy 1: Intercepting SMS OTPs via API
    • Setting Up the SMS Interception Pipeline
    • Polling the SMS Webhook Endpoint
    • Handling the OTP Timing Window
    • Strategy 2: Reading Email OTPs with Outlook and Graph API
    • Outlook COM Automation Approach
    • Microsoft Graph API Approach (Preferred for Unattended)
    • Polling Loop for Email OTPs
    • Strategy 3: Generating TOTP Codes Programmatically
    • How TOTP Works (and Why This Matters)
    • Python TOTP Generator (The Cleanest Approach)
    • PowerShell TOTP Generator (No Python Required)
    • Assembling the Complete Unattended Login Flow
    • Flow Architecture Overview
    • Detecting Which 2FA Prompt Is Active
    • Entering the OTP Code
    • Verifying Successful Login
    • Handling Edge Cases and Application-Specific Quirks
    • "Remember This Device" Checkboxes
    • Captcha-Gated 2FA Prompts
    • Push Notification 2FA (Authenticator App "Approve/Deny")
    • OTP Code Prefilled or Already Visible
    • Error Handling and Resilience Patterns
    • The Lockout Prevention Pattern
    • Handling OTP Delivery Delays
    • Clock Skew and TOTP Failures
    • Hands-On Exercise
    • Common Mistakes & Troubleshooting
    • Mistake 1: Hardcoding TOTP Secrets or API Keys
    • Mistake 2: Retrieving a Stale OTP
    • Mistake 3: Not Accounting for Different OTP Prompt Behaviors
    • Mistake 4: Trusting the Absence of an Error Message as Success
    • Mistake 5: Running TOTP Generation in a Tight Loop Without Backoff
    • Troubleshooting: The OTP Field Won't Accept Typed Text
    • Troubleshooting: TOTP Codes Consistently Wrong
    • Security and Compliance Considerations
    • Summary & Next Steps