Seem all I blog about lately is PowerShell… so here’s another article. Kind of important one at that – the Microsoft Teams PowerShell module has added (preview) support for application context authentication!
Let’s start with why this is important. Basically, it allows you to run scripts unattended, without a logged in user. We covered many such examples for Exchange Online PowerShell, where the V2 module has had support for certificate-based authentication for a while now. Other methods, such as authenticating via a client secret can also be leveraged, although not in “supported” manner. The situation with the Microsoft Teams PowerShell module is largely the same, with only certificate-based auth officially supported. However, the module does allow you to pass access tokens (yes, plural) directly, which means you can leverage other flows as needed.
So how do we get this working? First, you need the latest preview version of the Microsoft Teams PowerShell module, namely 4.7.1-preview, which you can get from the usual location over at the PowerShell gallery. Once the module is updated, you can use the newly introduced –CertificateThumbprint, –ApplicationId, –TenantId and/or –AccessTokens parameters for the Connect-MicrosoftTeams cmdlet. The official documentation walks you over all the details, but we will of course cover them here too.
As with all other things Graph, in order to connect you will have to obtain a valid access token. Now, since the Microsoft Teams PowerShell module incorporates both the old-style Skype for Business and new-style Teams cmdlets, under the cover it effectively uses two different access tokens. Why? Because each “resource” needs a separate token. Thus, we need a token for the Skype and Teams Tenant Admin API resource, also represented via the https://*.api.interfaces.records.teams.microsoft.com URI or 48ac35b8-9aa8-4d74-927d-1f4a14a0b239 GUID. And another one for the Graph API resource, represented via https://graph.microsoft.com/ and 00000003-0000-0000-c000-000000000000.
An important requirement, not properly detailed in the documentation, is that both tokens need to have the same AppId claim, in other words you need to have a single app registration with both these resources added. You cannot obtain a Graph API token for one app, and use a different app to obtain the Skype and Teams Tenant Admin API token. We’ll come back to this point later.
You will also need to have the corresponding permissions/scopes for each of these APIs. In the case of the Graph API, the full set of scopes includes: AppCatalog.ReadWrite.All, User.Read.All, Group.ReadWrite.All, Channel.Delete.All, ChannelSettings.ReadWrite.All, ChannelMember.ReadWrite.All, TeamSettings.ReadWrite.All. Having all of these will ensure that all of the Graph API-based Teams cmdlets will work. There is no requirement to have all these scopes granted to however, and the module will not prevent you from connecting if one or more of these is missing. The corresponding cmdlet however might not work as expected. Another thing to keep in mind is that the Group.ReadWrite.All scope “trumps” many of the individual scopes listed above, thus allowing you to perform operations such as update settings or delete a channel, without having the individual scopes listed explicitly.
The situation is a bit more peculiar when it comes to the Skype and Teams Tenant Admin API. If you have bothered to ever look at the scopes defined for said resource, you will notice that two application permission scopes exists, and none of them pertains to PowerShell cmdlet. In fact, the documentation also states this fact, as follows: “For *-Cs cmdlets – no API permissions are needed.” Effectively, you do not need to add any scopes for the Skype and Teams Tenant Admin API resource in your app registration, or even add said resource at all. It also means, that any app that has been granted Graph API scopes can potentially be used to connect to and execute *-CS* cmdlets, without any checks in place.
No admin roles are required to be assigned to the matching service principal object either.
With all the above in mind, here’s how to connect to Teams PowerShell via a certificate:
Connect-MicrosoftTeams -CertificateThumbprint "ABCDABCDABCDCBDAABCDABCDABCDACBDABCDABCD" -ApplicationId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -TenantId tenant.onmicrosoft.com
Of course, doing things the easy way is no fun, so let’s also see how to connect by passing the access tokens instead. The examples below use the client credentials flow, i.e. we are connecting via client secret. As mentioned already, two separate tokens need to be obtained first, one for the Graph API and one for the Skype and Teams Tenant Admin API resource, and both need to be using the same appID. The example below does just that by leveraging the MSAL library, and then passes the two tokens to the Connect-MicrosoftTeams cmdlet:
#Teams PS application context scenario $app = [Microsoft.Identity.Client.ConfidentialClientApplicationBuilder]::Create("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx").WithClientSecret("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx").WithTenantId("tenant.onmicrosoft.com").Build() $Scopes = New-Object System.Collections.Generic.List[string] $Scope = "https://graph.microsoft.com/.default" $Scopes.Add($Scope) $token = $app.AcquireTokenForClient($Scopes).ExecuteAsync().Result $graphToken = $token.AccessToken $app = [Microsoft.Identity.Client.ConfidentialClientApplicationBuilder]::Create("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx").WithClientSecret("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx").WithTenantId("tenant.onmicrosoft.com").Build() $Scopes = New-Object System.Collections.Generic.List[string] $Scope = "48ac35b8-9aa8-4d74-927d-1f4a14a0b239/.default" $Scopes.Add($Scope) $token = $app.AcquireTokenForClient($Scopes).ExecuteAsync().Result $teamsToken = $token.AccessToken Disconnect-MicrosoftTeams; Connect-MicrosoftTeams -AccessTokens @("$graphToken", "$teamsToken")
As noted above, it’s important to understand that we can now use any of the *-CS* cmdlets which are currently supported. The list includes: Get-CsTenant, Get-CsOnlineUser, Get-CsOnlineVoiceUser and *-CsOnlineSipDomain. While there’s not much bad you can do by having access to said cmdlets, as more and more cmdlets get ported to support application context scenarios, the lack of resource-specific controls might pose to be a challenge here.
Another interesting thing to note is that we can use the –AccessTokens parameter to connect within the user context, too. The process remains largely the same – have an app registration for the two resources, obtain tokens for a given user, pass them. Since the delegate permissions model does enforce additional constraints on the effective permissions though, you will not be able to run all the cmdlets, unless you also have the relevant Teams admin role(s) assigned. Here’s the relevant code:
$app2 = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx").WithRedirectUri("https://blabla").WithBroker().Build() $Scopes = New-Object System.Collections.Generic.List[string] $Scope = "48ac35b8-9aa8-4d74-927d-1f4a14a0b239/.default" $Scopes.Add($Scope) $token = $app2.AcquireTokenInteractive($Scopes).ExecuteAsync().Result $teamsToken = $token.AccessToken $app2 = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx").WithRedirectUri("https://blabla").WithBroker().Build() $Scopes = New-Object System.Collections.Generic.List[string] $Scope = "https://graph.microsoft.com/.default" $Scopes.Add($Scope) $token = $app2.AcquireTokenInteractive($Scopes).ExecuteAsync().Result $graphToken = $token.AccessToken Disconnect-MicrosoftTeams; Connect-MicrosoftTeams -AccessTokens @("$graphToken", "$teamsToken")
Since we’re connected in the user context, we will now be able to run any and all cmdlets that the user is privy to – not limited to the list above. The difference stems from the fact that before all cmdlets can support application context, some changed need to be made on the backend. The same situation happened with the Exchange Online cmdlets – it took a while before (most of) them can be run as a service principal object, and not a user.
Interestingly enough, when using the delegate permissions model (user context), we cannot obtain an access token unless the resource is specifically added in the app manifest. Failing to do so will result in an error such as the below:
Exception : System.AggregateException: One or more errors occurred. —> Microsoft.Identity.Client.MsalServiceException: AADSTS650057: Invalid resource. The client has requested access to a resource which is not listed in the requested permissions in the client’s application registration. Client app ID:
9dd50c8b-0eb9-47e9-af9e-80d200b11505(Reporting API Application). Resource value from request: 48ac35b8-9aa8-4d74-927d-1f4a14a0b239. Resource app ID: 48ac35b8-9aa8-4d74-927d-1f4a14a0b239. List of valid resources from app registration: 00000003-0000-0000-c000-000000000000, 00000002-0000-0ff1-ce00-000000000000,
I might be late to the party, but I never noticed this difference in behavior between the delegate and application permissions context. And I definitely prefer the way the former one is handling things. But anyway, I’ll update the article once I have more clarity on that front.
UPDATE: I seem to have overlooked the fact that the service principal corresponding to my test app had the Global reader admin role assigned, which explains why I’m able to run the Get-*CS* cmdlets currently supported within the app context. Removing the Global reader role results in the cmdlets throwing “Access denied” errors, so that’s good. I’m still a bit puzzled as to why am I able to obtain a valid access token for said resource in the first place, but that’s not an issue with the module itself. I’ll circle back once I have more clarity on that part.