Windows LAPS with AzureAD/Intune Using PowerApps/PowerAutomate

This is all taken from Moe Kinani here: https://cloudbymoe.com/f/windows-laps—power-app, and I will be referencing the instructions there throughout. However, the instructions there are a little bare for my smooth brain and there were a few hurdles I needed to get over, either because I didn’t understand the documentation properly, or because there have been changes to the platform since this was posted. I’ll also editorialize here a bit as is my wont. Make sure to follow along over there, though.

Azure VM

We’ll need to set up an Azure VM. I won’t do into much detail here because there’s a lot of choices that go into making that that are your own. This VM is only going to be used to run some Microsoft Graph Powershell commands, so it doesn’t need to be beefy. I also know in my environment that this doesn’t need 100% uptime. I can still get to the LAPS passwords in Azure AD or Intune, so I chose basically the cheapest option of VM that also lets Microsoft shutdown my VM at any given time if they need the resources. That’s a tradeoff I’m good with. I also have the VM to shutdown every night at 6 PM, because I know I won’t have any need for it after that point using the built in tools for the VM. I also have it automatically turning back on 5 AM, which uses a different set of tools that I will detail later.

Once you’ve got a VM, get into the VM and run:

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope AllUsers
Install-Module Microsoft.Graph -Scope AllUsers

To set up VM auto-start and auto-stop, there are instructions here to set it up: Azure VM Start/Stop V2. The long and the short of it, though, is to go to their GitHub page linked in the above instructions and install it in your Azure instance. Then, in Azure, look for “Logic Apps.” In there, you’ll see some new stuff that looks like this.

Here are my settings.

Registered Apps

This part was very straightforward and you can follow Moe’s instructions on how to set up the two registered apps you’ll need. One will be used to get information on user accounts, and the other will be used to get the LAPS password for the machine.

Azure Automation Account

Again, Moe’s instructions are largely fine. However, there’s an issue with his scripts that you download from his GitHub.

In the test script, in line #19, you need to convert the string to a secure string before Connect-MGGraph will use it. Replace line #19 with the following:

$Token = $Connection.access_token | ConvertTo-SecureString -AsPlainText -Force$Token = $Connection.access_token

This also means that in his second script, you’ll need to replace line #25 with the above as well.

If you run either script, you’ll notice that when Connect-MgGraph -AccessToken $Token is run, you get a ton of stuff in your output.

All of this will mess with your PowerAutomate flow later. When Moe wrote his instructions, all that we received back was a simple “Welcome to Microsoft Graph” and so he has a compose step that gets rid of this as part of the flow. Since then, Microsoft has added more junk in here.Thankfully, we can edit line #28 and add -nowelcome to the end to get rid of all of this and the need for that compose transform.

The rest should work.

PowerAutomate

Go ahead and import WindowsLAPSStep1 from his GitHub. This can be done from the My Flows tab and choosing the Import dropdown and picking “Import Package (Legacy).” A note on this step. WindowsLAPSStep1 uses the legacy “PowerApps” app in its first step. You’ll see why this is an issue in the next step. At present it’s still working, but I imagine that at some point this will be deprecated, so keep this in mind if it’s not working.

This next step is where things start to really diverge from Moe’s instructions, and it’s entirely because the PowerApps app is in the process of being deprecated. You can’t pick it, only “PowerApps (V2),” which works differently but ultimately still gets the job done.

Start with PowerApps (V2) and add a Text input. In the first field, put in ThisItem.azureADDeviceId. Leave the rest of it default.

Next, add a Compose. Click the little lightning symbol, then choose ThisItem.azureADDeviceId. This brings that value into the flow in a way that we can use.

Next, add an Azure Automation Job

Here, you’ll specify details about the runbook you want the job to run. This is all the stuff we set up back in the “Azure Automation Account” step. The last field will use the output from the previous Compose step.

Next, we’ll create an Azure Automation Get Job Output action.

We’ll set that up to get the output of the job we created in the previous step.

Next, we’ll add another Compose action to parse the result back into a string.

Finally, we’ll add a “Respond to a Powerapp or flow” action. We’ll add a text input named “LocalPass” with a value that is the output from the previous step’s compose string.

PowerApp

Once again, Moe’s got a nice little package for WindowsLAPSStep1 that can be downloaded from his GitHub and can be imported into PowerApp. Something that confused me was that, once you upload the Zip file, you’ll need to do some things the on the resulting page before you can click Import. You’ll need to click the spots marked with arrows and perform the required actions.

You can follow the rest of Moe’s instructions from here, but I made some significant tweaks to his imported app that I’ll detail below. But, at this point, if you finish out Moe’s instructions, you should have a working app that will pull Windows LAPS passwords for computers in AzureAD/Intune. I have it published and shared with specific users, and have it published to Microsoft Teams. Users are then able to go to the Apps section of Teams and install the app both on their desktop client and on their phone apps.

Licensing

You will need some kind of licensing for this app, since it uses Premium sources (Azure AD). At present, you either get licensing for each individual user that will be using this app. Or, you can get Per App licenses, which will then let any user this app is shared with use the app as long as there is an available license. In other words, you can either get named licenses (license per user) or a concurrent license (the Per App licenses). In my case, we so very rarely get passwords that it’s unlikely we’ll need to lookup passwords simultaneously enough as to need anything except a Per App license so the entire helpdesk staff can use the app.

PowerApps Customization

Spinning Load Icon

Probably as a result of using such a low power VM, my search takes a really long time when you click “Get LAPS.” On average it’s about a minute of waiting. Not a major problem for my use case, but sometimes you start to wonder if it’s really working. While there is a very very subtle little thing going on at the top of the screen, it’s way too subtle. So instead, I went to Loading.io and grabbed a loading spinner I liked that was free. I saved it as an SVG and uploaded it to the Media tab of the app. I then placed it on the screen, along with two text boxes, one that says the app is running, and another that says “No really, it’s running” after 10 seconds.

And while I’m at it, I also added a spinning wheel when clicking “Find the Device.” It’s a pretty inconsequential amount of time, but it provides continuity and trust in the app seeing the same elements when waiting.

Now, before I move on, I want to note that I changed the names of a bunch of the elements on the screen from the defaults that Moe used. This was to help me to understand what was doing what. I’m not going to change my stuff back, so just know that you might need to go searching for the elements I’m referencing. I hope I gave them fairly obvious names.

Add the spinner, and the two text boxes onto your screen and position them where you’d like them. The key here will be using the “Visible” property of elements in the advanced tab. In the spinner’s visible property, set it to locShowSpinner. Set the visible property on each of the two text boxes to ShowTimer1 and ShowTimer2 respectively, based on which will show first before the other. These elements should now disappear.

Now add a Timer. Set it somewhere on the screen, but set the visible property to false. This can be set to true just for troubleshooting. In the timer’s advanced tab, set the following:

OnTimerEnd:
UpdateContext({ShowTimer1: false});
UpdateContext({ShowTimer2: true});


Duration:
(Whatever you want in ms)

Now, on the “Find the Device” button element, in the advanced tab, put the following in the onSelect field:

// show the spinner
UpdateContext({locShowSpinner: true});
// Get Computer info
ClearCollect(MK6,WindowsLAPSStep1.Run(TextInput_FQDN.Text));
// hide the spinner
UpdateContext({locShowSpinner: false});

Then, on the “Get LAPS” button, put the following in the onSelect field:

// show the spinner
UpdateContext({locShowSpinner: true});
UpdateContext({ShowTimer1: true});
// start timer
UpdateContext({TimerGo: true});
// load data before going to next screen
Set(LABS_VAR,WindowsLAPSStep2.Run(ThisItem.azureADDeviceId).localpass);
// reset timer
UpdateContext({TimerGo:false});
Reset(Timer);
// hide the spinner
UpdateContext({locShowSpinner: false});
UpdateContext({ShowTimer1: false});
UpdateContext({ShowTimer2: false});

Finally, create a Text box that fills the entire screen is placed between all of these new elements and the rest of what Moe has. Name it something like “ClickShield” and in the visible field set it to locShowSpinner. This will prevent people from interacting with things while the app is searching.

Reset Button

Once you’ve searched for something, everything is left in the all of the fields where they were. This can make running another search confusing. So, I created a reset button at the bottom of the page.

Create a button, and in the onSelect field. put the following:

Set(LABS_VAR, "");
Clear(MK6);
Set(varReset, true);
Set(varReset, false);

Then edit the Reset property of the ComboBox dropdown with all your user’s names with varReset. Now when you click that button, everything should clear and reset back to the defaults.

Grey Out “Find The Device” when FQDN Field is Empty

During normal workflow, you should be finding a user in your drop down, and then when you select them that should fill in the field next to it with their FQDN, which is what the app uses to find assigned machines. But what if somehow that field gets blanked out? When you click “Find The Device” you’ll get an error about the upstream server not responding, since you gave it a null value. To make sure this doesn’t happen and to streamline the user experience a bit, we can “disable” the “Find The Device” button until there’s a value in there. Find the “DisplayMode” property of the “Find The Device” button and add the following:

If(IsBlank(TextInput_FQDN.Text),DisplayMode.Disabled,DisplayMode.Edit)

Connect-MGGraph “Invalid provider type specified”

If you’re setting up a certificate-based connection to Microsoft Graph Powershell (or whatever they’ve decided to name it at the point you’re reading this. You know what I’m talking about) and you’re getting an error when running:

$cert = Get-ChildItem Cert:\LocalMachine\My\$CertThumbnail
Connect-MgGraph -certificate $cert -ClientId $ClientID -TenantId $TenantID

don’t worry, that just means you need to run powershell as admin. You’re accessing the local machine’s cert store, so you need admin rights to do this.

Credential Persistence / Azure AD SSO Synchronization Customization

The company I work for has our Exchange service hosted with Intermedia, which at the time of the transition from an on-premise solution seemed great. Why deal with the headache of an Exchange server when you can make someone else do it for you? Fast forward three years and Microsoft’s Exchange Online service has become both ubiquitous and quite stable, but for Reasons we aren’t in the market to move our e-mail servers again.

That being said, we’re happy to be leveraging Microsoft’s other Office 365 offerings (because we don’t have a choice in the matter. Thanks Microsoft). This has created a pretty interesting dilemma that should be applicable any time you have your Exchange service hosted outside Office 365, and are also looking to leverage Office 365. Intermedia provides Active Directory synchronization via their proprietary HostPilot software, and Microsoft provides Acive Directory synchronization via Azure AD Sync. Intermedia and Azure both look to our UPN for our e-mails (and more importantly, our usernames), which it turns out is a problem!

Local Computer Credential Persistence

For some reason, if the “username” e-mail address used for your Exchange mailbox is the same as your UPN, your computer will save the credentials for that account with “Local Computer” persistence. This means that the credentials will not persist through sessions. If you log out, then log back in, you will need to re-enter your password. You can check this in the Credential Manager. What you want is “Enterprise” persistence.

Okay, so what did we do? Well, we thought we were being pretty clever by changing our UPN to include a subdomain. Instead of [email protected], we changed everyone’s UPN to be [email protected]. We were then able to synchronize that UPN to Office 365, and ensure all of our users continued to be able to have their mailbox credential persist between sessions. The theory was that users would only need to use their @sub.domain.com account once, when activating Office licensing, and one of our technicians would be doing that anyways, so the impact would be minimal.

SAML Single Sign On

Fast forward another year, and we have a new dilemma. We have an on-prem application that previously did not have SSO enabled, but a security audit brought up doing so to ensure that user accounts are disabled in a timely fashion. Reasonable. When weighing our options, we considered setting up AD FS on-premise, but my boss brought up the idea of using Azure AD. We’re already synchronizing everything up to Azure, why not continue to leverage it.

Great idea, except we ran into the issue of minimal impact. We had assumed that we would only ever use the Azure AD accounts for Office activation, and users would never need to remember when to use sub.domain.com instead of domain.com. But here we are, hoisted upon our own pitard.

This got me thinking about how the Azure AD Synchronization works. The AD Sync Service Manager uses a bunch of rules to match the data in your local user object with the data in your cloud user object. For all intents and purposes, you have two identical Active Directory forests that are synchronized byt this service. That means that the service needs to know what attributes to synchronize with what attributes, and more importantly, the rules for how it does that is exposed to us. We can set each user’s extensionAttribute1 attribute to be their [email protected] e-mail address and retain [email protected] as their UPN, continuing to bypass the previous issue of credential persistence. There are a number of reasons I went with the extensionAttribute1 attribute and not, say, proxyAddresses and mail, but I’ll talk about that later.

The broad strokes for the process were aped from evotech at the link below, but I ran into a few differences that are worth noting. https://evotec.xyz/azure-ad-connect-synchronizing-mail-field-with-userprincipalname-in-azure/

Open the Synchronization Rules editor and find the two rules shown in the screenshot.

When editing the rules, you’re prompted to clone and disable the default rule. I highly recommend you do this, as this is a very simple failsafe. Make sure to rename the clone, and make sure to change the priority. Default rules all have priorities over 100, so choose something below 100.

Navigate to the “Transformations” tab on the left, and find the line for userPrincipalName. The userPrincipalName is, you guessed it, the attribute that is used as your as “username” for Azure AD, so we’ll need to change this expression. Do this for both rules above.

Here is what the default is, followed by what I changed it to be.

Default:
IIF(IsPresent([userPrincipalName]),[userPrincipalName], IIF(IsPresent([sAMAccountName]),([sAMAccountName]&"@"&%Domain.FQDN%),Error("AccountName is not present")))
Modified:
IIF(IsPresent([extensionAttribute1]),[extensionAttribute1], IIF(IsPresent([userPrincipalName]),[userPrincipalName], IIF(IsPresent([sAMAccountName]),([sAMAccountName]&"@"&%Domain.FQDN%),Error("AccountName is not present"))))

If you take a minute to break it down, it makes a lot of sense. The default checks if there is a userPrincipalName for the object, and if it does it users that. Otherwise, it will check for a sAMAccountName and use that plus the domain from your FQDN, and finally error if none of those are around. What we want, though, is to add the extensionAttribute1 attribute as the first thing it checks, and have it use that. I liked keeping the UPN as a fallback so that worst comes to worst, I can direct a user to use that if I can’t immediately fix the problem.

However, that’s not the end. It turns out that when Azure AD Sync Service pulls the user object, it does not pull all attributes on the object, but rather a subset of them. You’ll need to find the pictured rule below and clone and edit that as well.

This is the rule that queries local AD and pulls what it needs. Go to the Join Rules tab on the left and add what’s highlighted in red. Now, it will pull in the extensionAttribute1 value and give the previous rules something to work with.

Once you’re done, open up Powershell and run the following to start a sync to match the value in your local user object’s extensionAttribute1 attribute with the UPN for your Azure AD user object.

 Start-ADSyncSyncCycle –PolicyType Initial 

Considerations

I said earlier there were reasons we didn’t sync with certain attributes, so below are the attributes I thought about syncing with and why that didn’t work.

mail – Our mailboxes are set up funny-like with Intermedia, and so they all have to synchronize using another e-mail address, which turns out to be in the mail field. This is something I may try to untangle later, but it may also be that we had run into credential persistence issue during the migration.

proxyaddresses – Azure AD Sync really did not like using the proxyaddresses field. The UPN value accepts strings, and while the proxyaddresses field is stated to be a string, I’m not sure it’s string enough to be accepted. Also, our previous on-prem Exchange server meant that a lot of older user accounts still had a lot of X500 data in that attribute, so it was better to just avoid it.

otherMailbox – I had thought about using this earlier on, and probably could have, but in troubleshooting why the sync wasn’t working, I decided to use the extensionAttribute1 field instead. In hindsight, it probably didn’t work because of needing to add the join rule. If you’ve got a bee in your bonnet to use this attribute, it’s probably fine.

Conclusion

This was a real long post, and a culmination of about two months total of beating my head against a wall, mostly with regards to the credential persistence. That’s what I get for opening a support ticket with Microsoft. If this helped you, let me know. If there’s anything that isn’t technically correct or just straight up wrong, let me know as well. All in all, it was pretty cool bending Azure AD to my will.