Configure an autoreply (OOO) message for Microsoft 365 Groups

Microsoft 365 Groups have long been positioned as the better version of a distribution list, and there is some merit to this. Although as much as Microsoft’s marketing team would want us to just forget about the good old DL/DG, in some cases they remain a viable, lightweight (as in no extra baggage added in terms of a SharePoint site or other artifacts), and cheaper alternative (dynamic DGs cost nothing, Microsoft 365 Groups with dynamic membership require Azure AD Premium).

In theory, the combination of a distribution list and a shared mailbox, to which we can approximate a Microsoft 365 Group to, should offer best of both worlds. In practice, Microsoft long neglected this part of the Group experience. Things seems to have changed recently, with the introduction of features such as Moderation. Most recently, Microsoft added a feature that sits on the other side of the spectrum, as in it is technically part of the “mailbox” experience. Namely, we can now configure automatic replies, also known as OOO, for a Microsoft 365 Group. Here’s how.

The bad news is, none of the UI bits currently expose this, so you will have to use PowerShell. The good news is that you can use the good old Set-MailboxAutoReplyConfiguration cmdlet and the same set of parameters as available for regular mailboxes. You don’t even need to specify the –GroupMailbox switch, which is required by some of the “mailbox” cmdlets when used against a Microsoft 365 Group. So, to check the current autoreply configuration for a given Group, run the Get-MailboxAutoReplyConfiguration cmdlet:

Get-MailboxAutoReplyConfiguration TeamSite_12c96e98-45fd-4591-93e5-cca4fd91666b

RunspaceId : cb1e9511-0fea-4259-9fde-8f5e010ce0b5
AutoDeclineFutureRequestsWhenOOF : False
AutoReplyState : Disabled
CreateOOFEvent : False
DeclineAllEventsForScheduledOOF : False
DeclineEventsForScheduledOOF : False
EventsToDeleteIDs :
EndTime : 10/06/2022 08:00:00
ExternalAudience : All
ExternalMessage :
InternalMessage :
DeclineMeetingMessage :
OOFEventSubject :
StartTime : 09/06/2022 08:00:00
Recipients :
ReminderMinutesBeforeStart : 0
ReminderMessage :
MailboxOwnerId : TeamSite_12c96e98-45fd-4591-93e5-cca4fd91666b
Identity : TeamSite_12c96e98-45fd-4591-93e5-cca4fd91666b
IsValid : True
ObjectState : Unchanged

As expected, no autoreply is configured, which is the default state. To configure the OOO reply, we can now use the Set-MailboxAutoReplyConfiguration cmdlet, with the set of parameters as follows:

  • AutoReplyStateEnabled to toggle autoreplies ON, Disabled to toggle it OFF, Scheduled to specify a start and end time between which to send OOO replies.
  • InternalMessage – the message that will be sent to internal recipients (as in users within your own tenant).
  • ExternalMessage – the message that will be sent to external recipients (as in users outside of your tenant). Remember that by default, Microsoft 365 Groups restrict external delivery, so configuring an external message only makes sense when you’ve toggled the RequireSenderAuthenticationEnabled flag.
  • ExternalAudience – optionally limit the set of external recipients to reply to. Valid values are None, Known (as in entries present in the mailbox’s Contacts folder) and All. However, as Microsoft 365 Groups still do not expose the Contacts experience, the Known value will not have the desired effect.
  • EndTime and StartTime – used when AutoReplyState is set to Scheduled.

For example, this cmdlet will enable OOO replies for the specified Microsoft 365 Group, with a simple message:

Set-MailboxAutoReplyConfiguration -Identity TeamSite_12c96e98-45fd-4591-93e5-cca4fd91666b -InternalMessage "This is an OOO reply for Microsoft 365 Group" -ExternalMessage "Not enabled" -AutoReplyState Enabled

As you’d expect, the internal and external message you specify is saved in HTML format, as evident from the Get-MailboxAutoReplyConfiguration output:

Get-MailboxAutoReplyConfiguration TeamSite_12c96e98-45fd-4591-93e5-cca4fd91666b

RunspaceId : cb1e9511-0fea-4259-9fde-8f5e010ce0b5
AutoDeclineFutureRequestsWhenOOF : False
AutoReplyState : Enabled
CreateOOFEvent : False
DeclineAllEventsForScheduledOOF : False
DeclineEventsForScheduledOOF : False
EventsToDeleteIDs :
EndTime : 10/06/2022 08:00:00
ExternalAudience : All
ExternalMessage : <html>
<body>
Not enabled
</html>

InternalMessage : <html>
<body>
This is an OOO reply for Microsoft 365 Group
</body>
</html>

DeclineMeetingMessage :
OOFEventSubject :
StartTime : 09/06/2022 08:00:00
Recipients :
ReminderMinutesBeforeStart : 0
ReminderMessage :
MailboxOwnerId : TeamSite_12c96e98-45fd-4591-93e5-cca4fd91666b
Identity : TeamSite_12c96e98-45fd-4591-93e5-cca4fd91666b
IsValid : True
ObjectState : Unchanged

At this point, you can send a test message to the Group and observe the result:

Interestingly, no MailTip is presented when an OOO message is configured for a Microsoft 365 Group, in contrast with the mailbox experience. But hey, you cannot have it all!

Now, there are a bunch of additional parameters you can use with the Set-MailboxAutoReplyConfiguration, which control the availability info and meeting responses for the mailbox over the period for which OOO is scheduled. For example, you can use the –CreateOOFEvent switch together with the –OOFEventSubject parameter to “block” the time scheduled as OOO within the Group’s calendar. However, those parameters do not seem to work as expected (or at all) with Microsoft 365 Groups, at least in my experience. In any case, you can get additional information on them via the official documentation. Do let me know if you manage to get them working!

Posted in Azure AD, Exchange Online, Microsoft 365, Office 365, PowerShell | Leave a comment

Updating your profile photo as Guest via the Microsoft Graph SDK for PowerShell

June is a busy month for the authors of the Office 365 for IT Pros book, as not only they have to prepare updates for the next incremental release, but rework them to better fit in the overall vision for the next edition, scheduled for July 2022 release. My job as a tech editor is even tougher, as I need to go over each and every one of the chapters and flesh them out (mostly because I slack during the reminder of the year). On the other hand, this can be a rewarding experience, as it serves as a refresher for many of the new functionalities Microsoft has released over the course of the year. Another big part of the experience is replacing old code with newer examples, and this year in particular we’re doubling down on this effort, due to some impending PowerShell module deprecation.

After this long introduction, cue the topic at hand. One of the examples in the Groups chapter revolves around sample code that Guests can use in order to update their profile picture within the host tenant. The current version involves the use of the Set-AzureADUserThumbNailPhoto cmdlet, which is no good anymore. Instead, a replacement is needed, either via the Microsoft Graph SDK for PowerShell, or a direct Graph API call. Long story short, here’s how you can achieve this task via the former.

First, you will need to authenticate in order to obtain a valid access token for the tenant you’d like to make the change in. Remember, we are doing this as Guest, so we need an access token valid for the corresponding resource tenant. The Connect-MgGraph cmdlet allows you to specify the -TenantId parameter, however it continues to disappoint with the overall experience, as it will reuse the token cache even when you specify additional parameters, such as -ForceRefresh or -UseDeviceAuthentication. To ensure you will obtain the proper token, it pays to issue the Disconnect-MgGraph cmdlet first. Then, use the following to connect as a Guest:

Connect-MgGraph -TenantId 922712ba-352a-4eda-bece-09d1684d0cfb -ForceRefresh -UseDeviceAuthentication
To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code D48YXLLW3 to authenticate.
Welcome To Microsoft Graph!

Since we’re utilizing the -UseDeviceAuthentication parameter here, you will need to complete the authentication process by opening the /devicelogin endpoint in a separate browser window, where you need to paste the provided code, and then provide credentials for the account you want to use. At this point, you can specify the UPN of an account that already has a matching Guest user object within the resource tenant. If everything goes as planned, you will be connected in the context of the guest user, which you can verify by issuing the Get-MgContext cmdlet.

Once connected, you will be able to use the Set-MgUserPhotoContent cmdlet to perform the photo upload, however one vital piece of information is needed for that – your user Id. Generally speaking, you can use the UserPrincipalName value instead of the id, which is something you can obtain from various UI bits, or guesstimate based on your UPN value within your home tenant and the MOERA domain of the host organization. Getting the id value on the other hand might prove a bit more difficult. Generally speaking, you can obtain it from the access token itself. The Graph API allows you to also use the /me endpoint, which will spill out your details, however I’m not aware of any way to leverage that via the PowerShell SDK. Instad, you can use the Get-MgUser cmdlet, which even in the most restricted scenario will allow you to query your own user object.

In the example below, the first cmdlet will fail as the host tenant is using the most restrictive guest access setting, limiting guest users to only being able to see their own user object, as explained in the documentation. If we provide the correct UPN value for the guest user, we can actually get the needed details:

Get-MgUser -UserId vasil@michevdev3.onmicrosoft.com
Get-MgUser_Get1: Insufficient privileges to complete the operation.

Get-MgUser -UserId vasil_michevdev3.onmicrosoft.com#EXT#@michev.onmicrosoft.com

Id DisplayName Mail UserPrincipalName UserType
-- ----------- ---- ----------------- --------
d0b22cd9-32fc-42fd-82d1-6121d92af3fc Vasil Michev vasil@michevdev3.onmicrosoft.com vasil_michevdev3.onmicrosoft.com#EXT#@michev.onmicrosoft.com

And with that, you are ready to update your profile photo. Don’t forget that there are certain limitations in terms of the photo size! Once you have the photo prepared, simply issue the Set-MgUserPhotoContent cmdlet as follows:

Set-MgUserPhotoContent -UserId d0b22cd9-32fc-42fd-82d1-6121d92af3fc -InFile "D:\Downloads\photo.jpg"

To confirm the change was successful, use the UI or the Get-MgUserPhoto cmdlet:

Get-MgUserPhoto -UserId d0b22cd9-32fc-42fd-82d1-6121d92af3fc

Id Height Width
-- ------ -----
default 1103 1135

And in case you are wondering why we needed to provide the Id value, it’s because the PowerShell SDK is crap. Not only it does not accept the UPN as a valid input for the Set-MgUserPhotoContent cmdlet, but the error message thrown is completely off:

Set-MgUserPhotoContent -UserId "vasil_michevdev3.onmicrosoft.com#EXT#@michev.onmicrosoft.com" -InFile "D:\Downloads\photo.jpg"
Set-MgUserPhotoContent_Set2: {
"errorCode": "ImageNotFound",
"message": "Exception of type 'Microsoft.Fast.Profile.Core.Exception.ImageNotFoundException' was thrown.",
"target": null,
"details": null,
"innerError": null,
"instanceAnnotations": []
}

But that’s what you get with the great AutoRest…

One important thing to note here. Unlike the Azure AD PowerShell, which every tenant can use out of the box, the Graph SDK is NOT a built-in app, as in the resource tenant has to have added it first. And, consent needs to be granted to the corresponding scopes for the above cmdlets to work. In particular, you need the User.ReadWrite permissions in order to set the photo. And given the known caveats of the /photo endpoint, even the Get-MgUserPhoto cmdlet might fail without User.ReadWrite access.

Posted in Azure AD, Graph API, Microsoft 365, Office 365, PowerShell | 1 Comment

Exchange Online PowerShell module gets rid of the WinRM dependence

Exchange Online Remote PowerShell has been around for a long time. It is largely thanks to it that Exchange Online offers the best admin experience out of all Microsoft 365 workloads in terms of usability, breadth of operation coverage, automation capabilities, RBAC controls, auditing, and so on. One can even argue that Remote PowerShell is one of the building blocks that made the cloud service possible in the first place.

That said, the cloud did expose some of the weaknesses of Remote PowerShell, in the form or reliability and performance issues. Especially in large tenants, long running scripts are known to pose a challenge, and throttling controls imposed by Microsoft didn’t help either. As software inevitably evolves, the reliance on old components brought other problems. One such example is the dependance on WinRM, which forced Microsoft to use the basic authentication endpoint as a workaround to facilitate OAuth/OIDC implementation.

With the release of the V2 Exchange Online PowerShell module, Microsoft addressed some of these shortcomings. In addition, the set of new REST-based cmdlets helped alleviate some of the stress when running operations against a large number of objects. And now, as an update to the V2 module, Microsoft is tackling the dependence on WinRM, promising to bring a slew of performance and reliability improvements in the process.

Improvements to PowerShell cmdlet execution

In order to bypass the WinRM layer, the Exchange Online V2 module (version 2.0.6 or later) by default no longer establishes a remote PowerShell session. It sounds crazy I know, but you can easily verify this. First, connect to the service via Connect-ExchangeOnline cmdlet, then run the Get-PSSession cmdlet. You should see no PSSession established, yet Exchange Online cmdlets seem to run just fine, how come? In short, Exchange Online PowerShell cmdlets have been reworked to “proxy” their payload over HTTPS, effectively using the Invoke-WebRequest cmdlet as a wrapper!

So how does it work? Just like with a “traditional” Remote PowerShell session, all the Exchange Online cmdlets you have access to as per the RBAC model will be downloaded to a temporary file and loaded as a new module. To distinguish from the “old” type of module, the name has been changed to tmpEXO_xxxxxxxx.xxx (whereas the old ones looked like tmp_xxxxxxxx.xxx). The most revealing piece of information is the cmdlet definition itself. For example, let’s take a look at the definition of Get-AcceptedDomain:

To find out what exactly this Execute-Command does, one can peek into the temp module file containing its definition. Long story short, using the cmdlet results in sending a POST request against a RESTful endpoint, with a JSON payload and an OAuth token. If all of this sounds familiar, you’re not wrong – the process is very similar to what the set of REST-based “V2” cmdlets do. And if you are too lazy to look up the cmdlet definition, using the –Verbose switch will also give you a clue:

In effect, Microsoft has “rewritten” the Exchange Online cmdlets and is now proxying them over an HTTPS REST-based endpoint, getting rid of the Remote PowerShell session and its dependencies, and reaping some benefits in the process. This solution inherently supports Modern authentication – whereas previously an OAuth token was passed over the WinRM basic auth endpoint, now all communication is done in a Graph API-like manner. This in turn brings many of the improvements we’ve already seen as part of the REST-based cmdlets, such as increased reliability thanks to the stateless nature of the protocol. Performance is also greatly improved, both in terms of establishing the initial connection and subsequent cmdlet executions, with up to 50% boosts observed.

Probably the biggest benefit of all is the fact that the reworked cmdlets are at functional parity with the “old” ones, meaning that you will be able to reuse your scripts and modules with minimal or no changes. Well, apart from some output formatting annoyances. That said, not every cmdlet has been updated to take advantage of this new method, and at the time of writing this article 421 out of 800+ Exchange Online cmdlets are available:

Keep in mind that the numbers above represent the cmdlets available to the current user, as per his role(s) within Exchange’s RBAC model. Microsoft continues to add (or occasionally remove when a fix is needed) cmdlets, and by the time the 2.0.6 version of the Exchange Online PowerShell module gets released in GA, we can expect all but the least used cmdlets to be part of it. Should you need to use any of the “missing” cmdlets or switch to the older version for some reason, you can do so by utilizing the –UseRPSSession switch:

Connect-ExchangeOnline -UserPrincipalName user@domain.com -UseRPSSession

And while we’re on the topic of new parameters, you might have noticed that few cmdlets now feature the -UseCustomRouting one, which takes a mailbox identifier as value. The idea is that using this parameter should help route requests to the most appropriate mailbox server, which should in turn increase performance in some scenarios. This parameter is not available when using the old Remote PowerShell session connectivity method. For the time being, a total of 12 cmdlets take advantage of the -UseCustomRouting parameter:

As mentioned already, the “new” cmdlets are backwards compatible for the most part and the experience when using them should remain the same. There are some minor annoyances with the output and “shortcut” notations I’ve become accustomed to, but important things such as pipeline support are taken care of. One important addition is the automatic use of batches, as hinted by the cmdlet definition screenshot above. Batching works by combining several cmdlet executions into a single request and is available out of the box for many cmdlets. For example, if you take the output of Get-Mailbox and pass it to the Get-CASMailbox cmdlet, the execution happens in batches of 10 objects, which brings noticeable performance increase compared to cmdlets that do not support batching.

Lastly, it’s worth mentioning that none of the changes detailed above affect any of the REST-based cmdlets. Those are still available in any version of the V2 module and any connectivity mode.

Executing Exchange Online cmdlets outside of PowerShell

Now that we have some idea on how the new Exchange Online cmdlets work, let’s get to the best part – bypassing them completely! Much like we did back when the original REST-based cmdlets were first released, we can obtain an access token and pass it along with a web request against the REST endpoint. All the details we need were revealed from the quick exploration we did in the previous section, so let’s get started.

First, we will need an access token. To obtain one, we can either use the built-in “Microsoft Exchange REST API Based PowerShell” application, with appID of fb78d390-0c51-40cd-8e17-fdbfab77341b, or use our own app, with at least Exchange.ManageAsApp permissions. Here’s an example of how to obtain an access token for the current user by leveraging the MSAL binaries:

Add-Type -Path "C:\Program Files\WindowsPowerShell\Modules\MSAL\Microsoft.Identity.Client.dll" #Load the MSAL binaries
$app =  [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create("fb78d390-0c51-40cd-8e17-fdbfab77341b").WithRedirectUri("https://login.microsoftonline.com/organizations/oauth2/nativeclient").WithBroker().Build()
$Scopes = New-Object System.Collections.Generic.List[string]
$Scope = "https://outlook.office365.com/.default"
$Scopes.Add($Scope)
$token = $app.AcquireTokenInteractive($Scopes).ExecuteAsync().Result

Other methods will also work, including methods leveraging other flows. If you plan to use the client credentials flow, you must use your own app registration. Once a valid token is obtained, we have to prepare the web request body and headers. Apart from providing the token as part of the Authorization header, few additional headers can be added. The X-ResponseFormat header can be used to specify the format in which we want to obtain any output, with available values of JSON or CliXML. The latter is what’s used by default for any of the “reworked” cmdlets. For most other purposes, you will likely want a JSON-formatted output.

It’s recommended to also include the X-AnchorMailbox header, which serves as hint to properly route the request. The value you need to specify for it is in the UPN:user@domain.com format. The Accept-Language header can be used to specify the locale, though it doesn’t seem to make much difference. The Accept-Encoding header is also supported, with a gzip value. Here’s how a sample set of headers should look like:

$authHeader = @{
     'Content-Type'='application\json'
     'Authorization'="Bearer $($token.AccessToken)"
     'X-ResponseFormat'= "json" 
     'X-AnchorMailbox' = "UPN:user@domain.com"
}

Now, we need to construct the request body, which represents the cmdlet we want to execute and any relevant parameters, all packaged in JSON formatted string. The CmdletName element specifies the name of the cmdlet we want to run, and the Parameters array lists each individual parameter and their corresponding values. It’s all then packaged in a CmdletInput element and looks something like this:

$body = @{
     CmdletInput = @{
          CmdletName="Get-Mailbox"
          Parameters=@{Identity="vasil"}
     }
}

Two more pieces of information are needed. First, the endpoint URL, which as we saw in the previous section is https://outlook.office365.com/adminapi/beta/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/InvokeCommand, where the GUID represents your tenant identifier (you can also use tenant.onmicrosoft.com). Lastly, we need to know the type of request, which is POST. Putting it all together, we can now run our simple Get-Mailbox cmdlet without having to load the Exchange Online PowerShell module, or PowerShell itself.

$uri = "https://outlook.office365.com/adminapi/beta/michev.onmicrosoft.com/InvokeCommand"
$res = Invoke-WebRequest -Method POST -Uri $uri -Headers $authHeader -Body ($body | ConvertTo-Json -Depth 5) -Verbose -Debug -ContentType 'application/json'
($res.Content | ConvertFrom-Json).Value

The JSON-formatted output we requested is not very suitable for display purposes, so we can transform it a bit. The end result will be a PSCustomObject, containing all the mailbox properties as displayed below. One thing to keep in mind is that the format data entries will be visible in the output for anything but string properties, so some additional handling might be needed:

And that’s all it there is to it. We’re now effectively running Exchange Online PowerShell cmdlets against an HTTPS endpoint, in a manner similar to the Graph API. Which is likely what many ISVs will end up doing, until Microsoft provides proper Graph API endpoints for Exchange Online management. Well, assuming the method outlined above is considered a “supported” one, that is. For the time being, this is all in preview, so no official support (as also hinted by the /beta endpoint notation). Things might change once the 2.0.6 version of the module GA’s.

In addition, you can also batch multiple requests together, then send them as a single JSON payload over the https://outlook.office365.com/adminapi/beta/{tenantid}/$batch endpoint. But that’s a topic for another article 🙂

UPDATE 21/05/2022: The next preview version of the module, namely 2.0.6-Preview6 was just released. Among other things, it brings the number of cmdlets to 799 (again, subject to your permissions). Which in turn means GA should be just around the corner now 🙂

Posted in Exchange Online, Graph API, Microsoft 365, Office 365, PowerShell | 11 Comments

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.

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”:

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:

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.

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.

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

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.

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.

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:

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!

Posted in Graph API, Microsoft 365, Microsoft Teams, Office 365, PowerShell, Skype for Business Online | Leave a comment

It is no longer possible to set the WindowsEmailAddress property on a synchronized user

In the world of Office 365, there are certain “gotchas” and workarounds we have accustomed ourselves to over the course of the last decade. One such useful workaround involved using the -WindowsEmailAddress parameter for the Set-Mailbox cmdlet. Invoking said parameter results in the provided value being stamped as the new Primary SMTP address of the user/mailbox, while preserving the previous PrimarySMTPAddress value as secondary alias. Oh, and it also updates the WindowsEmailAddress property, though we don’t much care for it in Exchange Online. Here’s an example use:

C:\> (Get-Mailbox room).Emailaddresses
SMTP:room@michevdev2.onmicrosoft.com

C:\> Set-Mailbox room -WindowsEmailAddress room@nativeCBA.michev.info

C:\> (Get-Mailbox room).Emailaddresses
SMTP:room@nativeCBA.michev.info
smtp:room@michevdev2.onmicrosoft.com

Now why is this useful, you might ask? Surely, one can simply update the PrimarySMTPAddress value directly? Thing is, this nifty parameter worked even for synchronized users, as in objects for which the SOA was an on-premises AD. So you could use it to change the SMTP address without having any access to the on-premises environment. And in most cases, the change was not overwritten by subsequent directory synchronization cycles. This in turn made the parameter quite useful, although Microsoft has never ever stated that this is a supported scenario.

Until now, that is. As alerted by a post over at the Q&A platform, it looks like Microsoft has now “fixed” this behavior, for lack of a better term. While you can still use the -WindowsEmailAddress parameter to make changes against cloud-authored objects, it is no longer possible to use it as workaround to update the SMTP address of an on-premises synchronized user. Trying this will now result in the following error message:

C:\> Set-Mailbox gosho -WindowsEmailAddress gosho@nativeCBA.michev.info
An Azure Active Directory call was made to keep object in sync between Azure Active Directory and Exchange Online. However, it failed. Detailed error message:
Unable to update the specified properties for on-premises mastered Directory Sync objects or objects currently undergoing migration. DualWrite (Graph) RequestId: 7b9c0db9-4b85-4da5-b0fc-5e3d56cbd783
The issue may be transient and please retry a couple of minutes later. If issue persists, please see exception members for more information.
+ CategoryInfo : NotSpecified: (:) [Set-Mailbox], UnableToWriteToAadException
+ FullyQualifiedErrorId : [Server=PR3PR09MB5346,RequestId=5bf0f47c-481d-42c8-b8ee-f7aae9ba8b26,TimeStamp=5/9/2022 8:31:37 AM] [FailureCategory=Cmdlet-UnableToWriteToAadException] 2A2BD17F,Micros
oft.Exchange.Management.RecipientTasks.SetMailbox
+ PSComputerName : outlook.office365.com

Bummer, but long time coming I suppose. As clearly stated by Microsoft, any changes to attributes for synchronized users should be made on-premises, as this is the only supported scenario. At least until they introduce some way to “transferring” the SOA, either on a per-object or per-attribute scope.

Posted in Azure AD, Exchange Online, Microsoft 365, Office 365, PowerShell | Leave a comment