Teams Remote PowerShell updates and new API endpoints

With the deadline for Basic authentication deprecation rapidly approaching, Microsoft is busy updating various endpoints and tools to properly support OAuth. Remote PowerShell connectivity is one such example, and important one, as for many admins PowerShell remains the preferred method to manage Teams objects and policies. While Modern authentication support has been available for a while for the corresponding module(s), it is facilitated via the WinRM basic authentication endpoint, and a better solution is needed.

A proper solution would need a complete rework of all cmdlets, in a manner similar to the REST-based ExO V2 ones, which essentially translates in providing Graph API-like endpoints. Unfortunately, this is a massive undertaking, given the sheer number of cmdlets that need to be reworked. As an interim solution, the Exchange team introduced the InvokeCommand method, which we covered here. Teams PG seems to have settled on a different approach, driven by AutoRest-generated cmdlets and in this article we will explore it in more detail.

Changes in the latest Teams module versions

There are quite few similarities between the approaches taken by the two teams, which should come as no surprise. For example, both the 2.0.6 (Preview) ExO module and the latest Teams module (version 4.3.0 at the time of writing this article) no longer establish a Remote PowerShell session by default. Whilst the Exchange module requires you to establish a new connection in order to run any of the “old” cmdlets (as in run Connect-ExchangeOnline -UseRPSSession), the Teams module will automatically generate a new Remote PowerShell session the first time you run a cmdlet that hasn’t been “modernized” yet.

Once a Remote PowerShell session has been established, you can expect to see the same old behavior, including using the same endpoint and passing an access token over the WinRM basic authentication endpoint. This is all illustrated on the screenshots below, with the first one providing the details of the PSSession object, and the second one showcasing the set of credentials used – “oauth” as the username and an access token as the password.

051122 1153 TeamsRemote1

051122 1153 TeamsRemote2

Interestingly though, the Teams module no longer downloads and imports cmdlets from a temporary file. Instead, all the cmdlet definitions are contained within the “original” MicrosoftTeams module, with some behind-the-scenes magic happening to “translate” them as needed. If you poke around a bit, you will notice that few additional “containers” are utilized, such as the C:\Program Files\WindowsPowerShell\Modules\MicrosoftTeams\4.2.0\net472\bin\Microsoft.Teams.ConfigAPI.Cmdlets.private.dll library. Loading it exposes a total of 940 (!) cmdlets.

Get-Command -Module Microsoft.Teams.ConfigAPI.Cmdlets.private | measure
Count : 940

If you browse the list of cmdlets however, you will notice many “duplicate” ones, with additional suffixes added. For example, apart from the New-CsAutoAttendant cmdlet, you will see two additional variants: New-CsAutoAttendant_New and New-CsAutoAttendant_NewExpanded. We will cover the logic here in more detail later, for now it’s interesting to find out the “real” number of cmdlets exposed in said library, by trimming out the “duplicates”:

051122 1153 TeamsRemote3

OK, that’s much lower. Even more interesting facts are revealed when you look into cmdlets definition. We can distinguish between two sets of cmdlets: the “old” Remoting ones, which are yet to be reworked, and the “new” AutoRest-generated ones, which no longer depend on PowerShell remoting. Apart from those, a few Custom cmdlets can be found, which mostly serve as helper functions.

Old cmdlets

Let’s start with a cmdlet that does not have any of these newly introduced suffixes, say something innocent looking like Get-CsOnlineSipDomain. If you run said cmdlet and observe the network traffic, you will notice that it leverages standard PowerShell remoting techniques, and in fact if you run the cmdlet right after loading the module, it will result in the creation of a new remote PSSession (remember, by default no session is created now). If you look at the cmdlet definition however, you will notice that in comparison with the old “remoting” cmdlets, things have changed a bit:

051122 1153 TeamsRemote4

Gone is the old InvokeCommand method, used by all “remoting” cmdlets in both Exchange Online and SfBO Remote PowerShell. Instead, we have a “mapping” with a similarly named cmdlet within the private module, exposed via the DLL. Instead of complicating things further, we can simply observe the Fiddler trace and conclude that the cmdlet still executes Get-CsOnlineSipDomain on the backend.

Meet the Modern cmdlets

When it comes to the “modern” cmdlets, things get a bit more interesting. Mostly, because we get to see the set of new, REST-based endpoints that are now utilized. With the modern cmdlets, PowerShell basically serves as proxy, or a wrapper, for the request. Unfortunately, we cannot observe this directly within the PowerShell console, as unlike their Exchange Online counterparts, Teams cmdlets don’t spill out this info when invoked with the -Verbose switch.

051122 1153 TeamsRemote5

Instead, we either have to dig up the cmdlet definition or use a traffic capture tool, such as Fiddler. I strongly recommend the latter approach, as otherwise you not only have to deal with DLL decompiling, but all the annoying AutoRest crap. Anyway, here’s what’s actually send over the network when you run the Get-CSOnlineUSer cmdlet:

GET https://api.interfaces.records.teams.microsoft.com/Teams.User/users/vasil?$skipuserpolicies=False&defaultpropertyset=Extended HTTP/1.1
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJS…
X-MS-Target-Uri: https://admin0e.online.lync.com/
X-MS-Correlation-Id: 431824df-be47-48fe-b075-010cfeb4fefa
User-Agent: Microsoft.Teams.ConfigAPI.Cmdlets/5.413.14/Get-CsUser_Get/MicrosoftTeams/4.2.0
X-MS-CmdletName: Get-CsUser_Get
Host: api.interfaces.records.teams.microsoft.com

Few interesting pieces of information can be spotted. First, the actual endpoint, under the https://api.interfaces.records.teams.microsoft.com/Teams.User/ namespace. The User-Agent and X-MS-CmdletName headers both give us a hint as to the actual cmdlet being executed, and as you can see, it’s not the “pure” Get-CsOnlineUser one. So here too, we have some behind the scenes magic happening. As another example, let’s take a look at the Get-CsGroupPolicyAssignment cmdlet:

GET https://api.interfaces.records.teams.microsoft.com/Skype.Policy/groupPolicyAssignments HTTP/1.1
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJS…
X-MS-Target-Uri: https://admin0e.online.lync.com/
X-MS-Correlation-Id: bb3d465c-8c50-47b4-95bf-4cf3735c2693
User-Agent: Microsoft.Teams.ConfigAPI.Cmdlets/5.413.14/Get-CsGroupPolicyAssignment_Get2/MicrosoftTeams/4.2.0
X-MS-CmdletName: Get-CsGroupPolicyAssignment_Get2
Host: api.interfaces.records.teams.microsoft.com

Here, we see another namespace being used, namely https://api.interfaces.records.teams.microsoft.com/Skype.Policy. Again we have some convoluted cmdlet name instead of the “clean” Get-CsGroupPolicyAssignment one. If we take a look at the definition of the Get-CsGroupPolicyAssignment cmdlet, it becomes clear that three different “mappings” are created, depending on the parameter set used.

051122 1153 TeamsRemote6

Going down the rabbit hole, we arrive at the following definition, which still doesn’t tell us much:

051122 1153 TeamsRemote7

Not that helpful, eh? I’m afraid it’s AutoRest territory from here on, and after some light browsing in the decompiled binaries, we can finally see what exactly the procedurally generated cmdlet is supposed to do. It’s convoluted, yes, but in a nutshell it translates into an HTTPS request being run against the https://api.interfaces.records.teams.microsoft.com/Skype.Policy/groupPolicyAssignments/ endpoint. Much like you’d expect from a Graph API equivalent.

TeamsRemote8

If you think such implementation leaves a lot to be desired, you’re not wrong. We’ve seen the “success” of AutoRest with the Microsoft Graph SDK cmdlets already, and many of the issues observed with those are present with the “new” Teams module cmdlets as well. Backwards compatibility is the prime issue here, with many of the new cmdlets failing to support the same set of functionality. In addition to lack of pipeline support, parameters not accepting null values, error handling issues, output issues, to name a few. Another downside is that unlike Exchange Online, you cannot leverage the “old” version of a cmdlet and force it to execute over a Remote PowerShell session.

On the positive side, the new cmdlets should bring some improvements, directly inherited from the RESTful approach. Those include better performance and improved reliability, relaxed throttling controls, “true” support for Modern authentication and no reliance on WinRM, and standardized implementation that should make your devs happy. The long goal is to provide proper Graph API support, and in the meanwhile we can use the new cmdlets as an interim solution that bridges the gap between PowerShell remoting and RESTful APIs.

Bypass PowerShell

Putting aside the AutoRest annoyances, the new cmdlets expose the RESTful endpoints we can leverage in order to perform operations against various Teams objects. This in turn means that we can bypass the PowerShell layer completely, much like with Exchange Online. Unlike the Exchange Online approach though, where each cmdlet can be proxied via the common InvokeCommand method, Teams uses few different endpoints, making things a bit more complicated. The process remains largely the same though: find the endpoint and syntax required, obtain an access token, build a query and run it.

Let’s start with the access token. We can either obtain one directly, by using the “built in” Teams PowerShell appID (12128f48-ec9e-42f0-b203-ea49fb6af367), or create our own app registration and grant the user_impersonation permission for the Skype and Teams Tenant Admin API resource. Here’s an example of obtaining an access token by using the built-in app:

$app2 = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create("12128f48-ec9e-42f0-b203-ea49fb6af367").WithRedirectUri("https://teamscmdlet.microsoft.com").WithBroker().Build()
$Scopes = New-Object System.Collections.Generic.List[string]
$Scope = "https://api.interfaces.records.teams.microsoft.com/user_impersonation"
$Scopes.Add($Scope)

$token = $app2.AcquireTokenInteractive($Scopes).ExecuteAsync().Result

Do make sure to use the proper scope here! The built-in app also allows you to obtain a token for the Graph API (https://graph.microsoft.com/.default scope), which you can use to execute requests against the /teams endpoint, however it will not help you with the endpoints corresponding to the SfBO PowerShell cmdlets!

Anyway, once the token is obtained, we can build our query. For example, let’s get the list of Teams app permission policies in our tenant, the one you can obtain via the Get-CsTeamsAppPermissionPolicy cmdlet. Here we have already discovered the corresponding endpoint, by using the methods outlined above. Once we have all the building blocks, we simply issue a GET request:

$authHeader1 = @{
'Content-Type'='application\json'
'Authorization'="Bearer $($token.AccessToken)"
}

$uri = "https://api.interfaces.records.teams.microsoft.com/Skype.Policy/configurations/TeamsAppPermissionPolicy"
$Gr = Invoke-WebRequest -Headers $AuthHeader1 -Uri $uri -Verbose -Debug
$result = ($gr.Content | ConvertFrom-Json)
$result

DefaultCatalogApps : {}
GlobalCatalogApps : {}
PrivateCatalogApps : {}
Description : 
DefaultCatalogAppsType : BlockedAppList
GlobalCatalogAppsType : BlockedAppList
PrivateCatalogAppsType : BlockedAppList
DataSource : 
Key : @{ScopeClass=Global; SchemaId=; AuthorityId=; DefaultXml=; XmlRoot=}
Identity : Global

DefaultCatalogApps : {@{Id=com.microsoft.teamspace.tab.planner}}
GlobalCatalogApps : {}
PrivateCatalogApps : {}
Description : 
DefaultCatalogAppsType : BlockedAppList
GlobalCatalogAppsType : BlockedAppList
PrivateCatalogAppsType : BlockedAppList
DataSource : Memory
Key : @{ScopeClass=Tag; SchemaId=; AuthorityId=; DefaultXml=; XmlRoot=}
Identity : Tag:Block Tasks

As another example, we can fetch the list of Call queues. Same process as above, we simply need to replace the corresponding endpoint: https://api.interfaces.records.teams.microsoft.com/Teams.VoiceApps/callqueues?FilterInvalidObos=False. Here’s the result:

PS C:\> $result

CallQueues 
---------- 
{@{Identity=062e7ab8-4653-47f5-8075-dc7b51a639a6; Name=Call queeeeeeeeeeeeeeeeeeeeeeeeeue; TenantId=923712ba-352a-4eda-bece-09d0684d0cfb; 

The examples above are all simple GET requests, with no input needed. When you do need to specify parameters and their values, package them in a JSON payload and provide it with the request. Apart from GET operations, you can of course POST, PUT, PATCH, DELETE as needed.

 

IMPORTANT! The output of some of the queries depends on the presence (and possibly value) of the ‘X-MS-CmdletName’ header! Do make sure to include it in your request. As an example, compare the output of the following query, with or without the ‘X-MS-CmdletName’ header:

$authHeader1 = @{
'Content-Type'='application\json'
'Authorization'="Bearer $($token3.AccessToken)"
'X-MS-CmdletName' = "Get-CsUser_Get"
}

$uri = "https://api.interfaces.records.teams.microsoft.com/Teams.User/users/user@domain.com?`$skipuserpolicies=False&defaultpropertyset=Extended"

 

Here’s also an example with using our own application to fetch an access token:

$app2 = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create("ae23ad05-9b02-4946-a906-78b7a0757f5f").WithRedirectUri("https://ExoPSapp2").WithBroker().Build()
$Scopes = New-Object System.Collections.Generic.List[string]
$Scope = "https://api.interfaces.records.teams.microsoft.com/user_impersonation"
$Scopes.Add($Scope)

$token = $app2.AcquireTokenInteractive($Scopes).ExecuteAsync().Result

For the sake of completeness, here’s also how the permissions should look on said app:

051122 1153 TeamsRemote9

Summary

In summary, the latest versions of the Teams PowerShell module bring some changes to existing cmdlets, reworking many of them to remove the dependence on WinRM and PowerShell remoting. This effort continues behind the scenes, as Microsoft reworks more and more cmdlets, with hopes of meeting the previously announced deadlines. And while certain annoyances exist in the current implementation, the end goal here is providing a set of RESTful endpoints that might eventually mature to a complete set of Graph API endpoints for Teams management, so let’s keep hoping.

While you wait for that, you might consider implementing your own solution that bypasses the PowerShell layer completely, as detailed in the previous section. Yes, it technically falls in the “unsupported” territory, and adds some overhead by requiring you to do the initial discovery of the largely undocumented endpoints. But hey, no pain, no gain!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.