Microsoft Graph and Twilio and HaloPSA, oh my!
Introduction
How do you know that person calling your helpdesk is who they say they are? Social engineering a helpdesk employee is a highly effective method of bypassing physical and logical access controls to breach an environment. This is a big enough problem in single silo organizations that have internal IT teams, but it presents a much larger attack surface for an MSP. You can’t “know” every one of your thousands of end users at clients, and that’s especially true for new employees joining your helpdesk team and starting from zero. Today we’re going to take a look at a creative way to make your own user identity verification system that avoids some of the pitfalls of commercially available products and harnesses Twilio, Microsoft Graph, and Azure Automation runbooks all from one click inside HaloPSA.
User identity verification? Why?
Our friends over at TheHackerNews put it best in their article from 2021:
“Consider an example in which an attacker contacts an organization’s helpdesk pretending to be an employee who needs their password reset. Several things could conceivably happen during that conversation. Some possible outcomes include:
- The attacker answers the security question using stolen information sourced from social media or from the Dark Web
- The attacker tries to gain the technician’s trust through friendly conversation to gain favor with the technician. The attacker hopes that the technician will overlook the rules and go ahead and reset the password, even in the absence of the required security information. In some situations, the attacker might also try to make the helpdesk technician feel sorry for them.
- The attacker might try to intimidate the helpdesk technician by posing as a CEO who is extremely upset that they cannot log in. When the helpdesk technician asks a security question, the attacker might scream that they do not have time to answer a bunch of stupid questions, and demand that the password be reset right now (this technique has succeeded many times in the real world).”
Nightmare fuel. Got it. Isn’t there already a product for this?
There’s an entire app industry that supports itself trying to fill this need. Nothing I’ve found has really hit the mark of usability and flexibility to make it widely adoptable. QuickPass and Duo are two of the biggest players in this market for MSPs and both have a really solid product. Before you even get to a cost per user (for Quickpass, Duo’s helpdesk verification product is free), the problem is threefold:
- It requires another app that needs to be installed during onboarding. It introduces additional support work when users switch phones and need to have Duo reactivated.
- It is dependent on the app being installed, updated, logged in (for Quickpass), and working correctly. Users delete things from phones, change permissions on apps because an article told them to, etc.
- It is driven by data provided by the user and dependent on that data being updated when it changes. Users who change phone numbers will continue to use Duo in push mode after recovering with their master password without realizing they need to update their associated phone number to get an SMS auth request.
(Nightmare fuel intensifies) Graph, Twilio, HaloPSA, and Azure Automation to the rescue!
I will start this paragraph by saying I don’t think there is an all-in-one perfect solution for this problem. I will follow that statement by saying we can get really fucking close by invoking the holy trinity of APIs: HaloPSA, Twilio, and Microsoft Graph. Today we will implement a solution that does the following:
- Agent pushes an action button in HaloPSA named “Verify User” that sends an Azure Automation webhook to our script with the user’s email address and Ticket ID.
- We extract the tenant ID from the user’s email address and piggyback off the HaloPSA CSP AppID to call Microsoft Graph.
- Microsoft Graph looks up the user’s currently registered SMS MFA method in Azure AD and returns it to our script.
- Our script generates a 6 digit verification code and sends it via SMS to the registered number using the Twilio API.
- Post the code and details back to HaloPSA as a private note so the Agent can verify the information.
- (Optionally) Log the details to an Azure SQL DB.
Prerequisites
- An Azure Automation 5.1 Runbook or PowerShell Universal Dashboard with an API Endpoint configured. PowerShell Universal is great for this task but will run the script much slower than Azure Automation.
- The HaloPSA integration for Microsoft CSP enabled and configured (unless you want to make a separate app registration, which is fine too).
- Alternatively, you can add your Azure RunAs Account service principal to the AdminAgents group and use that service principal to call MS Graph.
- If you want to create a new AppID for this, the scopes you’ll need in Graph are “UserAuthenticationMethod.Read.All”, “User.Read.All”
- A HaloPSA API AppID and ClientSecret (services)
- A Twilio account, a number to send from, and your SID and Token (from the main dashboard page)
- An Azure Keyvault with your RunAs granted permission to read secrets.
- My simplified HaloAPI module for private notes uploaded to your Azure Automation account
- The following modules available in your automation account (PowerShell 5.1):
- MSAL.PS
- Sqlserver (if logging to SQL)
- Az.Keyvault
- Az.Accounts
- Az.Automation
- Microsoft.Graph.Authentication
- Microsoft.Graph.Users
- Microsoft.Graph.Identity.DirectoryManagement
- Microsoft.Graph.Identity.SignIns
Step 1: Keyvault and secret setup
The first step is to set up our Application Registration in our CSP Azure AD tenant. When you configure the HaloPSA CSP integration it generates a multi-tenant application in your partner tenant. Instead of creating a second Application, we can add a secret to our HaloPSA CSP app instead. This can come in handy later on if you need to scale your script out to login across multiple tenants natively. You’ll need the AppID and to create a new secret. Store your new secret in your keyvault and name it something you’ll recognize later.

Next, we’ll create our HaloPSA API credentials. Save these into your Keyvault and remember what you called them.

Step 2: Set up your Runbook and create a Webhook
Here’s the script we’ll be using in our Runbook. Please jump to lines 69 (nice) through 87 and edit variables to reflect your keyvault secret names. You’ll also need to include your HaloPSA Agent domain (youragentdomain.halopsa.com if you’re on the default). You can also remove any SQL variable lines and the SQL logging blocks if you don’t want to use SQL logging.
<#
.NOTES
===========================================================================
Created on: 8/27/2022 15:15
Created by: Ceej - The MSP Automator
Organization: MSPAutomator.com
Filename: VerifyUser.ps1
===========================================================================
.DESCRIPTION
Azure Runbook to verify a user identity.
#>
param ([Parameter (Mandatory = $false)]
[object]$WebHookData
)
Function Connect_MgGraph
{
$MsalToken = Get-MsalToken -TenantId $TenantId -ClientId $CSPAppId -ClientSecret ($CSPClientSecret | ConvertTo-SecureString -AsPlainText -Force)
#Connect to Graph using access token
Connect-Graph -AccessToken $MsalToken.AccessToken
Select-MgProfile -Name beta
}
Import-Module HaloAPI
Import-Module MSAL.PS
Import-Module Microsoft.Graph.Authentication
Import-Module Microsoft.Graph.Users
Import-Module Microsoft.Graph.Identity.DirectoryManagement
Import-Module Microsoft.Graph.Identity.SignIns
Import-Module SqlServer
Import-Module Az.KeyVault
Import-Module Az.Accounts
Import-Module Az.Automation
#Unpack the JSON
$Data = ConvertFrom-Json -InputObject $WebHookData.RequestBody
$HaloUser = $data[0].content.HaloUser
$TicketID = $data[0].content.TicketID
$RequestID = $data[0].id
$Timestamp = $data[0].timestamp
$RefCharacter = $HaloUser.IndexOf("@")
$TenantID = $HaloUser.Substring($RefCharacter + 1)
$random = Get-Random -Minimum 100000 -Maximum 999999
#Get Azure RunAs information so we can access the keyvault
$RunAsConnection = Get-AutomationConnection -Name "AzureRunAsConnection"
#Connect as Azure RunAs and get the Azure context info
try
{
Connect-AzAccount `
-ServicePrincipal `
-Tenant $RunAsConnection.TenantId `
-ApplicationId $RunAsConnection.ApplicationId `
-CertificateThumbprint $RunAsConnection.CertificateThumbprint | Write-Verbose
Set-AzContext -Subscription $RunAsConnection.SubscriptionID | Write-Verbose
}
catch
{
Write-Error $_.Exception.Message
}
#VARIABLE DEFINITIONS - CHANGE THESE TO SUIT YOUR ENVIRONMENT
$VaultName = '#############'
#Twilio info
$sid = Get-AzKeyVaultSecret -vaultname $VaultName -Name "###########" -AsPlainText -EA Stop
$token = Get-AzKeyVaultSecret -vaultname $VaultName -Name "##########" -AsPlainText -EA Stop
$FromNumber = "+1##############"
#CSP or your AppID info
$CSPAppId = Get-AzKeyVaultSecret -vaultname $VaultName -Name "###########" -AsPlainText -EA Stop
$CSPClientSecret = Get-AzKeyVaultSecret -vaultname $VaultName -Name "##########" -AsPlainText -EA Stop
#HaloAPI info
$HaloAppId = Get-AzKeyVaultSecret -vaultname $VaultName -Name "###########" -AsPlainText -EA Stop
$HaloSecret = Get-AzKeyVaultSecret -vaultname $VaultName -Name "###########" -AsPlainText -EA Stop
$AgentDomain = "HTTPS://YOURAGENTDOMAIN.HALOPSA.COM"
#SQL server info - remove these five lines if not logging to SQL
$SQLServer = "##########.database.windows.net"
$tableName = "dbo.##########"
$DBSecret = Get-AzKeyVaultSecret -vaultname $VaultName -Name "##########" -AsPlainText -EA Stop
$DBName = "############"
$SQLUser = "############"
#Connect to SQL for logging - remove this block only if you dont want to log to SQL
$Connection = New-Object System.Data.SQLClient.SQLConnection
$Connection.ConnectionString = "Server=$SQLServer;Database=$DBName;Uid=$SQLUser;Pwd=$DBSecret;Encrypt=yes;TrustServerCertificate=no;Connection Timeout=30;"
$Connection.Open()
$Command = New-Object System.Data.SQLClient.SQLCommand
$Command.Connection = $Connection
Connect_MgGraph
if ((Get-MgContext) -ne "")
{
Write-Host Connected to Microsoft Graph PowerShell using (Get-MgContext).Account account -ForegroundColor Yellow
}
[array]$MFAData = Get-MgUserAuthenticationMethod -UserId $HaloUser
$AuthenticationMethod = @()
$AdditionalDetails = @()
foreach ($MFA in $MFAData)
{
Switch ($MFA.AdditionalProperties["@odata.type"])
{
"#microsoft.graph.passwordAuthenticationMethod"
{
$AuthMethod = 'PasswordAuthentication'
$AuthMethodDetails = $MFA.AdditionalProperties["displayName"]
}
"#microsoft.graph.microsoftAuthenticatorAuthenticationMethod"
{
# Microsoft Authenticator App
$AuthMethod = 'AuthenticatorApp'
$AuthMethodDetails = $MFA.AdditionalProperties["displayName"]
$MicrosoftAuthenticatorDevice = $MFA.AdditionalProperties["displayName"]
}
"#microsoft.graph.phoneAuthenticationMethod"
{
# Phone authentication
$AuthMethod = 'PhoneAuthentication'
$AuthMethodDetails = $MFA.AdditionalProperties["phoneType", "phoneNumber"] -join ' '
$MFAPhone = $MFA.AdditionalProperties["phoneNumber"]
}
"#microsoft.graph.fido2AuthenticationMethod"
{
# FIDO2 key
$AuthMethod = 'Fido2'
$AuthMethodDetails = $MFA.AdditionalProperties["model"]
}
"#microsoft.graph.windowsHelloForBusinessAuthenticationMethod"
{
# Windows Hello
$AuthMethod = 'WindowsHelloForBusiness'
$AuthMethodDetails = $MFA.AdditionalProperties["displayName"]
}
"#microsoft.graph.emailAuthenticationMethod"
{
# Email Authentication
$AuthMethod = 'EmailAuthentication'
$AuthMethodDetails = $MFA.AdditionalProperties["emailAddress"]
}
"microsoft.graph.temporaryAccessPassAuthenticationMethod"
{
# Temporary Access pass
$AuthMethod = 'TemporaryAccessPass'
$AuthMethodDetails = 'Access pass lifetime (minutes): ' + $MFA.AdditionalProperties["lifetimeInMinutes"]
}
"#microsoft.graph.passwordlessMicrosoftAuthenticatorAuthenticationMethod"
{
# Passwordless
$AuthMethod = 'PasswordlessMSAuthenticator'
$AuthMethodDetails = $MFA.AdditionalProperties["displayName"]
}
"#microsoft.graph.softwareOathAuthenticationMethod"
{
$AuthMethod = 'SoftwareOath'
$Is3rdPartyAuthenticatorUsed = "True"
}
}
$AuthenticationMethod += $AuthMethod
if ($AuthMethodDetails -ne $null)
{
$AdditionalDetails += "$AuthMethod : $AuthMethodDetails"
}
}
#To remove duplicate authentication methods
$AuthenticationMethod = $AuthenticationMethod | Sort-Object | Get-Unique
$AuthenticationMethods = $AuthenticationMethod -join ","
$AdditionalDetail = $AdditionalDetails -join ", "
# Twilio API endpoint and POST params
$url = "https://api.twilio.com/2010-04-01/Accounts/$sid/Messages.json"
$params = @{ To = $MFAPhone; From = $FromNumber; Body = "Please give this code to your technician to verify your identity: $Random" }
# Create a credential object for HTTP basic auth
$p = $token | ConvertTo-SecureString -asPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential($sid, $p)
# Make API request, selecting JSON properties from response
$TwilioResponse = Invoke-WebRequest $url -Method Post -Credential $credential -Body $params -UseBasicParsing |
ConvertFrom-Json | Select sid, body
if ($TwilioResponse -ne $null)
{
$Note = "VERIFICATION CODE: $Random - Request ID: $RequestID - Timestamp: $Timestamp - TARGET NUMBER: $MFAPhone - Twilio Response: Success - Log Response: Success"
}
else
{
$Note = "VERIFICATION CODE: $Random - Request ID: $RequestID - Timestamp: $Timestamp - TARGET NUMBER: $MFAPhone - Twilio Response: Failed - Log Response: Failure"
}
$Token = Get-HaloPSAToken -ClientID $HaloAppId -ClientSecret $HaloSecret -AgentDomain $AgentDomain
New-HaloPrivateNote -TicketID $TicketID -Note $Note -Token $Token -AgentDomain $AgentDomain
#REMOVE BELOW HERE IF YOU DONT WANT TO USE SQL LOGGING
$insertquery = "
INSERT INTO $tableName
([RequestID],[Timestamp],[TicketID],[Number],[Code])
VALUES
('$RequestID','$Timestamp','$TicketID','$MFAPhone','$Random')"
$Command.CommandText = $insertquery
$Command.ExecuteNonQuery()
$Connection.Close();
Step 3: Configure the HaloPSA Azure Automation and Action
In HaloPSA we need to do two things: set up our Automation Webhook and configure an Action the Agents can use to send our verification request. Let’s look at the webhook first:

Now let’s see what an action might look like that triggers this:


Add this new action to the workflow of your choice on the steps it would make sense so that your users can trigger the action when they pick up a call.
Step 4: Testing
It’s important that you have the correct requester on a ticket before hitting Verify User, as our webhook sends the email address of the Requester to our Runbook to look up in Graph. That’s really the only prerequisite before hitting the shiny button.

If we’ve done it all correctly, we end up with this:

And the end user sees this:

The time from clicking the button in HaloPSA to the end-user receiving the code and it being posted back to the ticket is usually <45 seconds, which is pretty decent considering how much is involved. The possibilities to adapt and use this script are endless. You could automatically update the user record in HaloPSA with the mobile number you receive from Graph as a way of truing up your address book automatically, for instance.
Conclusion
Is it perfect? No. Is it free? Also no. Is it cheaper than a per-user licensed SaaS app? Yes. Is it cool that agents can push one button and all this magic happens? Also yes.
This is just a small demonstration of the power you have at your fingertips with a platform like HaloPSA. Building your own integrations with HaloPSA are trivial and you can leverage PowerShell to make any API do your bidding. Until next time, happy automating!
CeeJ, firstly great job on this! have an auth error when running the runbook VerifyUser
“Get-MgUserAuthenticationMethod : Request Authorization failed At line:89 char:1 + [array]$MFAData = Get-MgUserAuthenticationMethod -UserId $HaloUser + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : InvalidOperation: ({ UserId = Davi…ndProperty = }:f__AnonymousType93`9) [Get-MgUserAuthenticationMethod_List], RestException`1 + FullyQualifiedErrorId : accessDenied,Microsoft.Graph.PowerShell.Cmdlets.GetMgUserAuthenticationMethod_List”
This is running with a Run AS Automation Account – with Global admin and explicit allows to the MS CSP integration app. What am I missing here?
Do you have this configured as a multitenant app in Azure AD? I would recommend deploying GDAP if you haven’t already and adding the service principal to a GDAP group with permissions in each tenant to perform these operations.