MSP Automator

HaloPSA One-Click SMS Identity Verification (2025 Edition)

Upgraded with Azure Functions, Runbooks, and Webhooks!

These fucking vendors, man.

So some vendor saw this original article dated AUGUST OF 2022 and were apparently driven to some kind of epiphany on how they could use this concept to swindle as many gullible MSPs out of as many dollarie doos as possible. Well, we don’t stand for that nincompoopery in the HaloPSA community.

I have, admittedly, failed to protect you (rubes?) from predatory Kaseya wannabees by not updating the original script as Halo development has marched on. I sincerely apologize for my transgressions. So, as penance for my sins, I am updating the original script to be faster, better, stronger, prettier, and simple to deploy!

On with the show!

Old and Busted: HaloPSA Automation driven by Azure Runbooks

If you hang out in the HaloPSA Discord or on any of the MSP subreddits, you’ve probably seen me shill unapologetically for Azure Runbooks. I do this for good reason – Azure Automation Runbooks are highly accessible and very easy to work with. They are the best way to break into powerful automation techniques because they have a very low barrier to entry and very low skill ceiling. As you master basic multi-tenant automation, a few problems eventually become clear:

New (old?) Hotness: HaloPSA automation driven by Azure Functions

These days I’m primarily a C# .NET Blazor developer and have largely neglected by PowerShell skills in favor of .NETs more powerful lower level languages. PowerShell is hands down the most accessible and powerful (I see what you did there, Microsoft) scripting language that is purpose built for IT professionals to automate IT processes with. The fact that it rides on top of .NET and is semantically very similar to C# is what made PowerShell a springboard for me to launch deeper into actual software development.

With that expanding software development experience came the expectation for my automations to be FAST AS FUCK BOIIIIII. So as I’ve had to go back and fix or update previous runbook based automations, I’ve found myself converting them to Azure Functions instead. Azure Functions are the perfect mix of PowerShell, webhook triggers, and always-on run speed. While a little bit more difficult to configure than a runbook, they offer a huge number of benefits:

Have I convinced you yet? No? Well, enjoy paying $30k a year to run your SMS verification in Rewst I guess.

Prerequisites

  1. An Azure Subscription
  2. A Twilio Account
  3. GDAP permissions correctly configured in your tenant
  4. Your users must have SMS set up as either a backup authentication method OR as an SSPR method (Entra is configured this way by default, so unless you’ve specifically configured your client tenants to not allow this, you don’t need to make any changes)
  5. Cyberdrain Improved Partner Portal (CIPP) – optional, but makes consenting our app registration much easier

Step 1: Configure the App Registration in Entra

They can. They totally can eat a bag of dicks.

*Note: The first time you do this you may receive an error. Give it a Microsoft Minute and try again in about an hour. It should work.

Do this. Don’t be a lazy dickhead.
Granular permissions. Get it?

Optional Sub Step: Consent the App Registration in your client tenants with CIPP (Thanks Kelvin <3)

If you use CIPP (and if you don’t, what are you waiting for?), you can navigate to the Overview tab of your new app registration, copy the Application ID, and go over to CIPP -> Tools -> Tenant Tools -> Application Approval. Leave the selection on all tenants -> click next -> paste in your App ID -> check the toggle to copy permissions from the current app -> click next -> click submit. Your application registration will be installed and consented in all your client tenants.

And just like magic – your app is consented and installed in all your client tenants.

If you’re a heathen and don’t use CIPP, you will need to manually consent this application registration in all your client tenants for it to work. That’ll be annoying as fuck. Enjoy.

Step 2: Set up certificate authentication

Why certificate authentication? Because Microsoft fucking said so, that’s why. In order to authenticate correctly against our app registration and invoke the full power of our GDAP permissions, we need to connect to Graph using certificate authentication. This is not only the most secure method, it’s also smart and convenient. Let’s walk through the process to create a self signed key-pair to be able to authenticate with.

Fire up PowerShell on your local computer and paste the following command, updating the subject name and years to your liking:

$Certificate=New-SelfSignedCertificate –Subject automation.kaseyasucks.com -CertStoreLocation Cert:CurrentUserMy -NotAfter (Get-Date).AddYears(5) 

Next, we need to export TWO different types of certificates from this. One is a .CER that contains only the public key – this one will be uploaded to Entra. The second will be a PFX including the private key – that will be uploaded to our Key Vault later.

Export-Certificate -Cert $Certificate -FilePath "C:certname.cer" 

Next, lets export the PFX:

$Pwd = ConvertTo-SecureString -String "FredVCanGargleMuhBallz" -Force -AsPlainText 
Export-PfxCertificate -Cert $Certificate -FilePath "C:certname.pfx" -Password $Pwd 

Then we head over to our App Registration in Entra -> Certificates and Secrets -> Certificates -> Upload Certificate

Upload the .CER file here
We did it, Reddit!

Step 3: Set up the HaloPSA Runbook and Webhook

The HaloPSA setup is rather straightforward. It consists of two components. An outgoing webhook to trigger the Azure function, and an incoming webhook for HaloPSA to receive the data from our function on the way back in. We’ll start by setting up the incoming runbook.

Incoming Runbook Configuration

First, navigate to Configuration -> Integrations -> Custom Integrations -> Custom Integration Runbooks -> New. Name it anything you like and save it. Then edit again and click the “Import from JSON” button. Paste the below:

{
  "id": null,
  "name": "SMS User Verification for Twilio (MSP Automator)",
  "type": 1,
  "content_type": "application/json",
  "authentication_type": 0,
  "method": 0,
  "certificate_id": 0,
  "certificate_name": "",
  "active": true,
  "events": [],
  "last_status": 1,
  "systemuse": "",
  "runbook_start_type": 1,
  "inbound_authentication_type": 0,
  "algorithm": 0,
  "digest": 0,
  "custom_payload": false,
  "payload_type": 0,
  "library_licence_name": "",
  "major_version_number": 0,
  "minor_version_number": 0,
  "patch_version_number": 0,
  "version_number": "0.0.0",
  "note": "MSPAutomator.com",
  "steps": [
    {
      "step_id": 1,
      "flow_id": 0,
      "chatprofile_id": null,
      "name": "Step 1",
      "isstart": true,
      "isend": false,
      "islaststep": false,
      "stage_number": 0,
      "pipeline_stage_name": "",
      "actions": [
        {
          "id": null,
          "flow_id": 0,
          "chatprofile_id": null,
          "start_step": 1,
          "end_step": 2,
          "action_type": 18,
          "action_id": -18,
          "action_name": "Successful",
          "action_outcome": "",
          "use_work_hours": true,
          "time_limit_action_name": "",
          "automation_action_name": "",
          "automation_runbook_name": "",
          "seq": 1,
          "approval_result": 1,
          "restricted": false,
          "conditions": [],
          "conditions_exec": [],
          "restrictions": [],
          "todo_group_name": "",
          "chat_selection_order": 1
        },
        {
          "id": null,
          "flow_id": 0,
          "chatprofile_id": null,
          "start_step": 1,
          "end_step": 3,
          "action_type": 18,
          "action_id": -18,
          "action_name": "Unsuccessful",
          "action_outcome": "",
          "use_work_hours": true,
          "time_limit_action_name": "",
          "automation_action_name": "",
          "automation_runbook_name": "",
          "seq": 2,
          "approval_result": 0,
          "restricted": false,
          "conditions": [],
          "conditions_exec": [],
          "restrictions": [],
          "todo_group_name": "",
          "chat_selection_order": 1
        }
      ],
      "steptype": 2,
      "message": "{n  "ticket_id": <<SMSTicketID>>,n  "outcome": "Private Note",n  "who": "Your MSPs Automation Services",n  "hiddenfromuser": true,n  "note_html": "<<VerificationResponse!>>"n}n",
      "auto_action": 8,
      "auto_action_type": 3,
      "input_field_id": 0,
      "chat_image_type": 0,
      "newticket_service_id": 0,
      "start_new_chat_flow_id": "",
      "iteration_type": 0,
      "iteration_batch_size": 1,
      "output_variables": [],
      "runbook_variable_mappings": []
    },
    {
      "step_id": 2,
      "flow_id": 0,
      "chatprofile_id": null,
      "name": "Step 2",
      "isstart": false,
      "isend": true,
      "islaststep": false,
      "stage_number": 0,
      "pipeline_stage_name": "",
      "actions": [],
      "steptype": 3,
      "auto_action": 0,
      "input_field_id": 0,
      "chat_image_type": 0,
      "newticket_service_id": 0,
      "start_new_chat_flow_id": "",
      "iteration_type": 0,
      "iteration_batch_size": 1
    },
    {
      "step_id": 3,
      "flow_id": 0,
      "chatprofile_id": null,
      "name": "Step 3",
      "isstart": false,
      "isend": true,
      "islaststep": false,
      "stage_number": 0,
      "pipeline_stage_name": "",
      "actions": [],
      "steptype": 3,
      "auto_action": 1,
      "input_field_id": 0,
      "chat_image_type": 0,
      "newticket_service_id": 0,
      "start_new_chat_flow_id": "",
      "iteration_type": 0,
      "iteration_batch_size": 1
    }
  ],
  "input_variables": [
    {
      "method_id": 0,
      "type": 3,
      "data_type": 3,
      "key": "SMSTicketID",
      "value": "<<request^TicketID!>>",
      "value_mappings": [],
      "extra_process": 0,
      "step_id": 0,
      "step_name": "",
      "mapping_type": 0
    },
    {
      "method_id": 0,
      "type": 3,
      "data_type": 2,
      "key": "VerificationResponse",
      "value": "<<request^VerificationResponse!>>",
      "value_mappings": [],
      "extra_process": 0,
      "step_id": 0,
      "step_name": "",
      "mapping_type": 0
    }
  ],
  "disabled": false,
  "log_retention_policy_days": 30,
  "batch_method": 0,
  "batch_delay_seconds": 30,
  "batch_limit": 0,
  "infinite_loop_threshold": 5,
  "access_control": null
}

Important: When you save this JSON, a URL will appear that can be used to start the runbook externally. This is the webhook URL for the runbook that you will be required to enter in Step 4 when deploying the function app.

HaloPSA Outgoing Webhook Configuration

Next, we’ll create the outgoing webhook that triggers the SMS verification. You won’t have a URL yet, so just type anything into the URL box for now. Set up the custom payload exactly as depicted here:

It is critically important that you do not fuck this up.

Create an action in HaloPSA to trigger this webhook

Mosey on over to Configuration -> Tickets -> Actions and create a new action. Set it up like this:

Perform the needful in the HaloPSA action config.

Add this action to any workflow(s) you want to make it accessible to your agents, and now we can move on to the fun part!

Step 4: Deploy the Function App

You have two choices – deploy the function app manually using VSCode and the Azure Functions Extension (Find the solution and deployment instructions here in my Github)

Or, just Deploy to Azure using my template by clicking the shiny blue button below

The ARM template will ask you to fill in the following information:

  1. A name for the Keyvault that will be created
  2. A name for the function app that will be created
  3. Your MSP Name (used in the outbound SMS message)
  4. Your Twilio SID
  5. Your Twilio Token
  6. Your Twilio send from number in E.164 format (e.g. +18008675309)
  7. Your Halo Runbook Webhook URL
  8. The name you plan to upload the PFX to the keyvault as (we will do this in the next step – just fill something in here and remember what you entered)
  9. The App Registration ID for the application we made in Step 1

Step 5: Upload the PFX to the Keyvault

Part of the deployment in step 4 created a keyvault to hold secrets and prepopulated a bunch of secrets from the information you entered as part of the template. One thing it didn’t do is upload the PFX. Remember when I told you to remember what you entered in the template for certificate name? This is where that matters.

Navigate to the Azure portal -> the new Keyvault that was created -> Objects -> Certificates and click “Generate/Import” on the top bar.

Select “Import” from the dropdown menu instead of “Generate” and select your PFX we made earlier. Be sure to name the certificate whatever you entered in the template during deployment or nothing we have done will work.

I cannot stress to you how crucially important it is to not fuck this up.

Step 6: Retrieve the Function App URL and populate the HaloPSA Webhook

Navigate to the function app in Azure that was created as part of deployment. On the Overview screen you’ll see a URL. This is the base URL for your function app.

Default Domain is the root of your function app, HttpTrigger1 is the function name.

Now that we know the default domain of our function app, we can go back to the webhook we set up previously in HaloPSA and enter it in the URL box.

https://<yourfunctionappname&gt;.azurewebsites.net/api/HttpTrigger1?

Setup is now complete. Now the fun part.

Testing the function app in HaloPSA

Click the button you added to the workflow and watch the magic happen.

Bitchin’

The original incarnation of this script when operated via Azure Runbooks took up to 1-3 minutes to send the SMS and return the data to Halo. This new function app takes about 3-5 seconds from start to finish. I would say that’s a pretty damn good improvement. In fact, I’d wager that this is significantly faster than any of the paid products on the market.

So please folks, don’t pay some scumbag vendor for SMS identity verification. Use this fully functional, extremely low cost, and totally fast option instead.

As always, happy automating!

Exit mobile version