Automate Setting Work Location in Teams/Outlook [PoC]

Warning: This is another one of those proof of concept “because I can” kind of things. I’m going to do some things which shouldn’t be done in production. It was just for the fun of it.

Microsoft recently released the new Work location feature in Teams and the new Outlook/Outlook on the web. This allows users to set their work location so co-workers can see if their colleagues are working in the office or remotely on any specific day. You can find more information here and here.

I was extremely disappointed when I learned that there is no support for Microsoft Graph or Power Automate yet, so we could automatically set this each day based on e.g., an IP address or even through geo-fencing on a mobile device.

Off I went to try and find a way to do it anyway. First, I opened Teams in Microsoft Edge and used the browser dev tools to see what kind of requests are sent to which APIs when the work location is changed.

Here we can see that a PUT request is made to https://presence.teams.microsoft.com/v1/me/workLocation when the location is edited.

In this case the location was cleared with the following JSON payload.

JSON payload sent to the API

The value 0 clears the location. 1 is used to set it to Office and 2 will map to Remote.

Next, I inspected the token needed for this request with the help of jwt.ms.

{
"typ": "JWT",
"nonce": "xxxxxxxxxxxxxxxxxxxxxxxxx",
"alg": "RS256",
"x5t": "-xxxxxxxxxxxxxxxxxxxxxxxxx",
"kid": "-xxxxxxxxxxxxxxxxxxxxxxxxx"
}.{
"aud": "https://presence.teams.microsoft.com/",
"iss": "https://sts.windows.net/4bffbf87-53a0-4fce-b58b-4179cb3a3b7d/",
"iat": 1688763565,
"nbf": 1688763565,
"exp": 1688841846,
"acr": "1",
"aio": "xxxxxxxxxxxxxxxxxxxxxxxxx",
"amr": [
"pwd",
"mfa"
],
"appid": "5e3ce6c0-2b1f-4285-8d4b-75ee78787346",
"appidacr": "0",
"family_name": "Mozz",
"given_name": "Mozzie",
"ipaddr": "xxx.xxx.xxx.xxx",
"name": "Mozzism Admin",
"oid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"puid": "10032000330C6A66",
"rh": "xxxxxxxxxxxxxxxxxxxxxxxxx",
"scp": "user_impersonation",
"sub": "xxxxxxxxxxxxxxxxxxxxxxxxx",
"tid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"unique_name": "mozzie@mozzism.ch",
"upn": "mozzie@mozzism.ch",
"uti": "xxxxxxxxxxxxxxxxxxxxxxxxx",
"ver": "1.0",
"xms_cc": [
"CP1"
],
"xms_ssm": "1"
}.[Signature]

AADInternals

Copying the token from the browser and using it to make a web request with PowerShell is easy. The hard thing was to figure out how I can obtain that token programmatically. Luckily, there’s this awesome PowerShell module called AADInternals by one Dr. Nestori Syynimaa. This is an amazing resource to test almost anything related to Azure AD and Graph authentication.

The documentation for this module can be found here. I discovered that the following command would give me a token for Teams.

$teamsToken = Get-AADIntAccessTokenForTeams

What’s really cool is that now, with PowerShell 7 the whole login process can be done through the CLI without any browser pop-ups! Even MFA is supported!

Note: The token is redacted in this screenshot

Even though the audience for this token is https://api.spaces.skype.com and not https://presence.teams.microsoft.com it still works to change the work location in Teams.

Because everything can be done through the CLI I wondered if I could pass the credentials and the MFA secret to the AADInternals functions so that the login process could be automated to be 100% unattended.

After some trial and error, I had that figured out too. Not all functions of the AADInternals module are exposed to the user so the key is to make them available to your current PowerShell session by dot sourcing some of the *.ps1 files included in the module.

$functions = Get-ChildItem -Path "C:\Program Files\WindowsPowerShell\Modules\AADInternals\0.9.0" -Filter "*.ps1"

$functions = $functions | Where-Object {$_.Name -match "Teams" -or $_.Name -match "AccessToken" -or $_.Name -match "CommonUtils"}

foreach ($function in $functions) {

. $function.FullName

}

By executing this code which uses some of the not exposed functions from AADInternals, we can pass a credential object (and MFA secret) and then get an access token to write Teams presence.

$teamsPresenceToken = Prompt-Credentials -ClientId $ClientId -Resource "https://presence.teams.microsoft.com" -Credentials $secureCreds -OTPSecretKey $passwordDecrypted

Credentials

I’ve put together a script which does everything for you. However, you must install the AADInternals module first and you also need to clone the repo from my GitHub account since it has dependencies on other functions.

git clone https://github.com/mozziemozz/TeamsPhoneAutomation.git

Even though this was all part of some good Friday night hacking, we obviously don’t want to store the credentials and especially the MFA secret as plain text on our machines. Therefore, I’ve added a couple of functions to help handle encrypted credentials. We want to be better than UBER.

When encrypted credentials are stored on a Windows PC, they can only be decrypted using the same account and the same machine because of security identifiers (SID).

If you don’t trust me, see for yourself. Here’ I’m retrieving the password I encrypted on my main PC using my user account.

The password is encrypted and decrypted using locally

I copied the file to a virtual machine where I’m logged in with the same user. This code will let you decrypt the password if the key matches.
(I found this code years ago, unfortunately I don’t have the source anymore…)

[Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR((Get-Content -Path .\.local\SecureCreds\mozzie@mozzism.ch.txt | ConvertTo-SecureString))) | Out-String

When I run this on another machine with the same logged in user, I get an error that the Key is invalid. Which is good.

Error message when attempting to decrypt the password on another machine

My Function Get-MzzSecureCreds will automatically create a PS Credential Object when -AdminUser "user@domain.com" is specified. The username will be set as file name, and it will also be used as the username of the credential object.

The Final Script

Here’s the script to change your work location in Teams.

If you run it for the first time, you’ll need to provide the username and password and possibly the MFA secret of the user for which you want to change the work location.

At this point I want to make clear again that this is a proof of concept. Even though the credentials are encrypted, you should not store production passwords on the device and most importantly, you shouldn’t store the MFA secret in the same place as the password.

You can determine if you’re working remotely or in the office by specifying either your local or public home/remote IP address.

Since my private IP is 192.168.127.117, and I specified 192.168.1.10 my work location is set to Office.

Set Work Location to Office

When I change the IP address to 192.168.127.117 the work location is set to remote because that matches my local subnet.

Set Work Location to Remote

In case your office and your home network both use the same private address space, you can use -IpType Public and specify your home/remote public IP.

The Result

The updated work location is then visible in both Teams and Outlook.

Work Location in Teams viewed as another User
Work Location in Outlook

Even though I won’t be using this in production, I had great fun developing this and I also learned a thing or two about AADInternals, Json Web Tokens and encrypted credentials in PowerShell.

--

--

Martin Heusser | M365 Apps & Services MVP

I 'm a Microsoft MVP and work as a Microsoft Teams Voice Engineer. I like to share my knowledge about Teams, Power Automate, Azure and PowerShell on Medium.