Teams PowerShell module finally adds support for application authentication!

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

Connecting to Teams PowerShell via certificate-based authenticationOf 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,
00000009-0000-0000-c000-000000000000, c5393580-f805-4401-95e8-94b7a6ef2fc2.

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.

5 thoughts on “Teams PowerShell module finally adds support for application authentication!

  1. Ioannis says:

    One can also assign the app the “Skype for business” Admin role, so that the “*CS*” cmdlets function.

    What is the pros/cons in assigning an admin role in comparison to granting API permissions?

    Reply
  2. Branko says:

    Do you know what method would be best to use in order to check if “connection” to teams is still available. In other words if connect-microsoftteams is still connected/access token is still valid?

    Reply
    1. Vasil Michev says:

      For now, run a test via any of the cmdlets (preferably one that returns a single object) and check for output. Going forward, they might add the analog of Exchange’s Get-ConnectionInformation.

      Reply
  3. Oliver says:

    Dear Vasil

    I already use CBA for Exchange-Online and Microsoft Graph and it works fine.
    Now I wanted to test it for Microsoft Teams and downloaded version 4.7.1-preview
    (in the meantime 4.8.0 is out).

    I followed the Microsoft Instructions and added the API Permissions listed in the
    article under “App Registrations” (not Enterprise Applications).

    I now have the following API permissions set (Type: Application)

    API/Permissions name Type Description Admin consent required Status
    Microsoft Graph (8)
    AppCatalog.ReadWrite.All Application Read and write to all app catalogs Yes Granted for Swisscard AECS
    Channel.Delete.All Application Delete channels Yes Granted for Swisscard AECS
    ChannelMember.ReadWrite.All Application Add and remove members from all channels Yes Granted for Swisscard AECS
    ChannelSettings.ReadWrite.All Application Read and write the names, descriptions, and settings of all channels Yes Granted for Swisscard AECS
    Group.ReadWrite.All Application Read and write all groups Yes Granted for Swisscard AECS
    Organization.Read.All Application Read organization information Yes Granted for Swisscard AECS
    TeamSettings.ReadWrite.All Application Read and change all teams’ settings Yes Granted for Swisscard AECS
    User.ReadWrite.All Application Read and write all users’ full profiles Yes Granted for Swisscard AECS
    Office 365 Exchange Online (1)
    Exchange.ManageAsApp Application Manage Exchange As Application Yes Granted for Swisscard AECS

    I as well assigned my “Provisioning APP” to the Azure AD Roles “Teams Administrator” and “Skype for Business Administrator”.
    Have no idea if this is necessary.

    I can successfully connect with

    Connect-MicrosoftTeams -CertificateThumbprint …

    I can use the following cmdlets and they work as expected:

    – Get-Team
    – Get-TeamUser
    – Add-TeamUser
    – Remove-TeamUser

    But when it comes to creating a new team an error is thrown:

    PS > New-Team -DisplayName “Test Team” -Description “Test Team” -Visibility Private
    New-Team : Error occurred while executing
    Code: BadRequest
    Message: /me request is only valid with delegated authentication flow.
    InnerError:
    RequestId: 682e11bc-8140-44ec-a3a5-8141d7bc6563
    DateTimeStamp: 2022-10-13T08:10:22
    HttpStatusCode: BadRequest
    At line:1 char:1
    + New-Team -DisplayName “Test Team” -Description “Test Team” -Visibilit …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [New-Team], ApiException
    + FullyQualifiedErrorId : Microsoft.Teams.PowerShell.TeamsCmdlets.ErrorHandling.ApiException,Microsoft.Teams.Power
    Shell.TeamsCmdlets.NewTeam

    Why should new-team follow a “delegated” flow (/me) ???

    Any idea what’s wrong ?
    Is this a bug ?
    If yes, where can I file a bug report ?

    Regards,
    Oliver

    Reply
    1. Vasil Michev says:

      The issue here is with how the cmdlet is coded – if you don’t provide a value for the Owner parameter, it will try to assign the current users as owner, and this obviously cannot work in the app scenario. Thus, simply run it with the -Owner parameter:

      New-Team -DisplayName “Test Team” -Description “Test Team” -Visibility Private -Owner user@domain.com

      Another thing to keep in mind is that not all cmdlets are currently supported for app permissions, although the list is growing:

      Non *-Cs cmdlets (for example, Get-Team)
          Get-CsTenant
          Get-CsOnlineUser, Get-CsOnlineVoiceUser
          *-CsOnlineSipDomain
          *-CsPhoneNumberAssignment
          *-CsOnlineTelephoneNumberOrder, Get-CsOnlineTelephoneNumberType, Get-CsOnlineTelephoneNumberCountry
          *-CsCallQueue
          *-CsAutoAttendant, *-CsAutoAttendant*
          *-CsOnlineVoicemailUserSettings
          Find-CsOnlineApplicationInstance, *-CsOnlineApplicationInstanceAssociation, Get-CsOnlineApplicationInstanceAssociationStatus
          *-CsOnlineSchedule, New-CsOnlineTimeRange, New-CsOnlineDateTimeRange
          *-CsOnlineAudioFile
          Find-CsGroup
          *-CsOnlineDialInConferencingUser, *-CsOnlineDialInConferencingServiceNumber, *-CsOnlineDialInConferencingBridge, Get-CsOnlineDialInConferencingLanguagesSupported, Set-CsOnlineDialInConferencingUserDefaultNumber
          *-CsOnlineLisLocation, *-CsOnlineLisCivicAddress, *-CsOnlineLisWirelessAccessPoint, *-CsOnlineLisPort, *-CsOnlineLisSubnet, *-CsOnlineEnhancedEmergencyServiceDisclaimer, *-CsOnlineLisSwitch
      Reply

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.