Rui Qiu's Tech Blog
← Back to all posts

July 29, 2026

Automating Hardware OATH (FOB) MFA Provisioning with Azure Automation & Microsoft Graph

Automating Hardware OATH (FOB) Token Provisioning

with Microsoft Forms, SharePoint, Azure Automation & Microsoft Graph

A practical, end-to-end architecture & build guide.

This guide shows how to fully automate the issuance of hardware OATH-TOTP security tokens (“FOBs”) to users — ideal for populations that can’t use phone-based MFA (call centers, offshore staff, secure areas). It removes manual seed handling and per-user registration entirely.

Note: All names, domains, IDs, and links in this article are generic placeholders — replace them with your own environment’s values.


Table of Contents

  1. What This Solution Does
  2. Architecture
  3. Key Gotcha: Read the Seed from seeds.txt, not the XML
  4. Prerequisites
  5. SharePoint Setup
  6. Microsoft Form & Live Workbook
  7. Robustness Lessons Worth Copying
  8. Testing & Go-Live
  9. Troubleshooting
  10. Security & Compliance Notes

1. What This Solution Does

A user submits their token’s serial number via a Microsoft Form. Vendor “seed” files (the secrets behind each token) arrive by email and are ingested into a permanent vault. A scheduled Azure Automation runbook matches the serial to its secret, registers and binds the token to the user in Microsoft Entra ID via Microsoft Graph, and emails the user activation instructions. Admins get a per-run digest.

1.1 Design Principles


2. Architecture

The diagram below shows the end-to-end flow across Microsoft Forms, SharePoint, the Azure Automation runbook, Microsoft Graph, and Entra ID.

Hardware OATH (FOB) provisioning architecture

Figure 1 — Hardware OATH (FOB) provisioning architecture

2.1 Components

ComponentRole
Microsoft FormsIntake for the token serial number and a “replacement?” flag. Own it via an M365 group so its live Excel lands in team SharePoint.
Live Excel workbookAuto-synced Forms responses in the team site’s Documents (tab ‘Sheet1’). The runbook reads new rows here.
Seeds/Inbox folderLanding folder for vendor seed .eml pairs (a ‘Seed Delivery’ email + a ‘Password’ email).
SeedVault (list)Permanent store of serial → Base32 secret. Source of truth for matching.
ProcessedResponses (list)Dedup by Forms response Id, so each submission is handled once.
Azure Automation runbookScheduled PowerShell 7.2 job (Managed Identity) that orchestrates the flow.
Microsoft Graph (beta)Hardware OATH endpoints: create the device in the tenant pool + bind it to the user.
Entra IDWhere the activated Hardware OATH method lives on each user.
Notifications moduleSends user confirmation emails and the admin digest via Graph sendMail.

2.2 Per-run Flow

  1. Connect to Graph with the Automation Account’s managed identity.
  2. Read new responses from the live Excel; skip any Id already in ProcessedResponses.
  3. Load SeedVault; parse any new .eml seeds and append new serials to the vault.
  4. For each pending response: resolve the UPN, validate the serial, match it to a seed.
  5. Create + bind the token via Graph (self-healing: delete & recreate a stale device, retry once).
  6. If flagged as a replacement, retire the old token.
  7. Email the user activation steps; record the response as processed.
  8. Move processed .eml to an archive folder (the vault already holds the data).
  9. Send the admin digest with a CSV attachment.

3. Key Gotcha: Read the Seed from seeds.txt, not the XML

Vendor seed archives (a password-protected zip, sometimes disguised with a .pdf extension) typically contain three files: seeds.xml, seeds.txt, and key.bin.

The seeds.xml stores the secret inside an EncryptedValue (AES-128-CBC) — you cannot use it without the vendor’s decryption key. The seeds.txt contains the same secrets in plaintext Base32, ready for Entra:

Serial Number,Base32 Secret
2307532800604,YE4VLPC735ZJH3LPCCDCLJGMA6VJ6354
2307532800605,TUSZT7DQ5K2JHE7YP53LFZ4C2VUBJDKO

Lesson: parse seeds.txt. A correct Base32 secret decodes to 20 bytes (SHA-1). If you try to Base64-decode a Base32 string, you get garbage and Graph returns BadRequest.


4. Prerequisites

4.1 Managed Identity — Graph Application Permissions

PermissionPurpose
Policy.ReadWrite.AuthenticationMethodCreate hardware OATH devices in the tenant pool
UserAuthenticationMethod.ReadWrite.AllBind/activate a token on a specific user
User.Read.AllResolve users by UPN / alias / proxy address
Sites.ReadWrite.AllRead Excel + folders; read/write the SharePoint lists
Mail.SendSend user + admin notifications

4.2 Managed Identity — Directory Role (the non-obvious one)

The app permission alone is not enough to write another user’s auth methods — you’ll get 403. Also assign the Authentication Administrator role (non-admin scope; least privilege).

New-MgRoleManagementDirectoryRoleAssignment -BodyParameter @{
  principalId      = "<managed-identity-object-id>"
  roleDefinitionId = "c4e39bd9-1100-46d3-8c65-fb160da0071f"  # Authentication Administrator
  directoryScopeId = "/"
}

4.3 Scope Mail.Send (least privilege)

New-ApplicationAccessPolicy -AppId "<mi-app-id>" `
  -PolicyScopeGroupId "<sender-scope-group>@example.com" `
  -AccessRight RestrictAccess -Description "Limit automation sender"

4.4 Custom PowerShell Modules (Runtime 7.2)


5. SharePoint Setup

5.1 Folders

5.2 List: SeedVault

ColumnTypeNotes
TitleSingle lineSerial number — Enforce Unique Values = Yes
SecretSingle lineBase32 secret
IntervalNumber30
HashFunctionSingle linehmacsha1
ManufacturerSingle linee.g., token vendor
ModelSingle linee.g., token model
SOSingle lineSource order (audit)
IngestedDateDate & TimeWhen added

5.3 List: ProcessedResponses

ColumnTypeNotes
TitleSingle lineForms response Id — Enforce Unique Values = Yes
UPNSingle lineResolved user
SerialSingle lineSerial registered
StatusSingle lineRegistered / Failed
RunIdSingle lineRun stamp
ProcessedDateDate & TimeWhen handled

6. Microsoft Form & Live Workbook

  1. Own the Form via an M365 group so responses land in team SharePoint (not a personal OneDrive).
  2. Questions: Serial Number, Is this a replacement? (Yes/No), plus email + login-name fields; keep ‘Record name’ on.
  3. On the Responses tab, click Open in Excel once to create the live workbook in the team site’s Documents.
  4. Note the workbook file name and the data tab name (often ‘Sheet1’) — the runbook needs both.

7. Robustness Lessons Worth Copying

These are the hard-won fixes that turn a proof-of-concept into something production-ready:


8. Testing & Go-Live

StepActionExpected
1Run with WhatIf = TrueReads responses, loads/append vault, lists would-register; no writes/emails
2Live with a small batch limitA couple users → Created + Bound → Registered; emails sent
3Verify in EntraUsers ▸ [user] ▸ Authentication methods shows Hardware OATH
4Full live runRemaining users provisioned; digest emailed to admins
5ScheduleRecurring, e.g., every 30 minutes

9. Troubleshooting

SymptomLikely CauseResolution
Import-Module failsWrong runtimeUse the 7.2 runtime that has your custom modules
BadRequest on bind, everyoneMissing directory roleAssign Authentication Administrator; wait ~15 min
Forbidden (403) on bindMissing role/permGrant UserAuthenticationMethod.ReadWrite.All + Authentication Administrator
BadRequest on a few serialsStale unassigned pool devicesSelf-heal deletes+recreates; else delete the pool device and re-run
‘Serial not found’Seed not yet in vaultAdd the vendor .eml to Inbox; retries next run
Invalid serial formatTypo / wrong deviceUser emailed to resubmit the correct serial
User not foundAlias/typo UPNAlias/proxy fallback + domain auto-correct; else fix the response
1-char / garbage secretParsed encrypted XMLParse seeds.txt (plaintext Base32), not seeds.xml

10. Security & Compliance Notes


— End of guide —