Connecting to Exchange Online PowerShell via client secret

Microsoft just released a new version of the Exchange Online (V2) PowerShell module, which brings support for much awaited feature – seamless connectivity that satisfies MFA requirements thanks to using the certificate-based authentication flow. Now, one can argue that this isn’t “true” MFA and point to the inherit auditing issues when using this flow, but that’s true for all other apps leveraging it, and not something the Exchange team can be blamed for.

We did a thorough review/guide of the new method in our recent post over at the QuadrotechĀ blog. Here I’ll add few more details that were left out for brevity or simply border the “unsupported” frontier and should not be used unless you really want to get your hands dirty. We start by getting the obvious thing out of the way – yes, you can actually use a client secret as well, although it is considered less secure and Microsoft chose not to support it via the module. But nothing is stopping you from generating a client secret for your app, then using it to connect via the “manual” method outlined below. But more on that later.

First, what is this “manual” method I talk about? Well, if you are familiar with the way Exchange Online Remote PowerShell implements support for modern authentication (or have seen my previous articles on the subject), you are aware that the bulk of the client-side changes are introduced to handle the token request/renewal process. Once the token is obtained, it is “proxied” via the good old basic authentication WinRM endpoint, to a slightly different URI – one that has the “BasicAuthToOAuthConversion=true” value added. The backend of course has also received some updates in order to handle this, but those are out of our hands, and thus not as interesting.

The important thing is that once you understand the process, you can bypass the built-in cmdlets and create your own “connect to Exchange Online via modern authentication” function, which might suit your needs better. Same applies to the features released in the new version of the module. Once you have configured the Azure AD application, it’s permissions and the certificate to be used for authentication, you can use any number of methods to obtain a token, for example:

Add-Type -Path 'C:\Program Files\WindowsPowerShell\Modules\AzureAD\2.0.1.10\Microsoft.IdentityModel.Clients.ActiveDirectory.dll'

$authContext45 = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList "https://login.windows.net/tenant.onmicrosoft.com"
$secret = Get-ChildItem cert://currentuser/my/"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
$CAC = [Microsoft.IdentityModel.Clients.ActiveDirectory.ClientAssertionCertificate]::new("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",$secret)
$authenticationResult = $authContext45.AcquireTokenAsync("https://outlook.office365.com",$CAC)

where I have pointed to a certificate stored in the Personal store for the current user and also provided the client_id of the application registered in Azure AD. If everything checks out and a valid token is obtained, you are ready to establish the remote PowerShell session. One small detail you need to know first – when passing the token, you need to specify the username in the following format: “OAuthUser@tenantGUID”. Here’s how it works:

$token = $authenticationResult.Result.AccessToken
$Authorization = "Bearer {0}" -f $Token
$Password = ConvertTo-SecureString -AsPlainText $Authorization -Force
$Ctoken = New-Object System.Management.Automation.PSCredential -ArgumentList "OAuthUser@923712ba-352a-4eda-bece-09d0684d0cfb",$Password

$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/PowerShell-LiveId?BasicAuthToOAuthConversion=true -Credential $Ctoken -Authentication Basic -AllowRedirection -Verbose
Import-PSSession $Session

And with that, you should have an Exchange Online Remote PowerShell session established and all the cmdlets corresponding to the admin role granted to the application’s service principal available.

A variation of the above can be used to point to a certificate file, instead of using the certificate store provider. Assuming we have a cert.pfx file, protected with a password, the following example can be used:

$authContext45 = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList "https://login.windows.net/tenant.onmicrosoft.com"

$certFile = "C:\cert.pfx"
$certFilePassword = "P4$$w0rd"
$secret = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate -ArgumentList $certFile,$certFilePassword
$CAC = [Microsoft.IdentityModel.Clients.ActiveDirectory.ClientAssertionCertificate]::new("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx",$secret)

$authenticationResult = $authContext45.AcquireTokenAsync("https://outlook.office365.com", $CAC)

Or if you don’t want to get bothered by certificates and don’t care about the increased security they provide, you can simply add a client secret to the application you registered in Azure AD and use the client credentials flow instead:

$authContext4 = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList "https://login.windows.net/tenant.onmicrosoft.com"
$ccred4 = New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.ClientCredential -ArgumentList "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "very_long_string"
$authenticationResult4 = $authContext4.AcquireTokenAsync("https://outlook.office365.com", $ccred4)

And that’s pretty much it. Once you have a valid token, use it to establish the remote PowerShell session via the sample above. Don’t forget to add some proper error handling and most importantly, logic to detect token expiry and renew/obtain new tokens as needed!

One additional detail you might need when troubleshooting connectivity issues – the token must contain the wid claim and within it, the admin role(s) to which the service principal object corresponding to your Azure AD application has been assigned. You already know how to get the token, as for decoding it use a site such as jwt.ms or just do it directly via PowerShell as detailed in this article. Here’s how a sample valid token would look like:

Access token claims (client secret flow)///UPDATE.

I remembered one thing I forgot to add in this post – make sure you pass the “anchor” when connecting. The module cmdlet does that for you, but when you create the session yourself, you need to add “&email=SystemMailbox%7bbb558c35-97f1-4cb9-8ff7-d53741dc928c%7d%40tenantname.onmicrosoft.com” to the connection string.

So the end result should be something like this:

"https://outlook.office365.com/PowerShell-LiveId?BasicAuthToOAuthConversion=true&email=SystemMailbox%7bbb558c35-97f1-4cb9-8ff7-d53741dc928c%7d%40tenantname.onmicrosoft.com

This will fix some of the issues you get with Azure AD-related operations, such as creating a Shared mailbox. Some, but not all, as some cmdlets are simply not designed to work in the context of an app.

33 thoughts on “Connecting to Exchange Online PowerShell via client secret

  1. Justin Grote says:

    Great work! Have you looked at the new 2.0.6 module that uses a REST API to proxy all the cmdlets? It creates a connection context that you can totally build yourself with the factory, and shim in a token provider. I’ve been trying to use this to get it to work with a Managed Service Identity.

    Reply
    1. Vasil Michev says:

      The goal there is to get rid of the WinRM dependency, it doesn’t change much else. They should be releasing the GA version soon, we’ll see what’s in there…

      Reply
  2. Jurgen says:

    Hi Vasil,

    We are trying to connect by using the script described.
    We are able to retrieve a token which contains the WIDs related to the Template ID of the directory roles.

    “iss”: “https://sts.windows.net/b5bca53a-fe43-4c16-98a7-/”,
    “iat”: 1618912680,
    “nbf”: 1618912680,
    “exp”: 1618916580,
    “aio”: “E2ZgYKjstL+4Zf4l3lmTt8yOuPrsAQA=”,
    “app_displayname”: “PowerShell”,
    “appid”: “xxxxxxxxxxxxxxxxxxxxxxxxx”,
    “appidacr”: “2”,
    “idp”: “https://sts.windows.net/b5bca53a-fe43-4c16-98a7-/”,
    “oid”: “3e9e3ab4-a068-4214-b7d9-1bfc0b713dea”,
    “rh”: “0.AQIAOqW8tUP-FkyYp21Q4_hxNvknHBqBeSZEu_dKxPURwzcCAAA.”,
    “sid”: “38ab1eed-6528-4ac8-b182-cf0bdeb7b1a2”,
    “sub”: “3e9e3ab4-a068-4214-b7d9-1bfc0b713dea”,
    “tid”: “xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx”,
    “uti”: “BDefE6MoAEqIvL8RYuRUAA”,
    “ver”: “1.0”,
    “wids”: [
    “88d8e3e3-8f55-4a1e-953a-9b9898b8876b”,
    “62e90394-69f5-4237-9190-012177145e10”,
    “0997a1d0-0d1d-4acb-b408-d5ca73121e90”
    ]
    }

    Allthough the role information is missing but the WID is there. We have assigned the Global Admin permissions to the Service Principa for testing purposes.

    When we try to connect the session by using the step:
    $Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/PowerShell-LiveId?BasicAuthToOAuthConversion=true -Credential $Ctoken -Authentication Basic -AllowRedirection -Verbose
    The output is: “Connecting to remote server outlook.office365.com failed with the following error message : Access is denied.”

    I have something similar with the user account as described in your previous post and it works fine.
    Also, I have logged in using the certificate and the connect-azuread cmdlet and it works fine.

    What are we doing wrong?

    Thanks.

    J

    Reply
    1. Vasil Michev says:

      You need the permissions/scope as well, make sure you’ve added it for your application under the Azure AD blade and consent to the permissions as well.

      Reply
  3. Aaron says:

    Hi Vasil!

    Thank you for such a helpful article.

    I’m trying to use this method to connect to “https://ps.compliance.protection.outlook.com” however I always receive a 401. I believe it’s because my app or service principal is missing the following scopes:

    AdminApi.AccessAsUser.All, FfoPowerShell.AccessAsUser.All, RemotePowerShell.AccessAsUser.All

    How do I assign these permissions in order to be able to access this? I cannot see them in the Azure portal.

    Reply
    1. Vasil Michev says:

      This method is not supported for the SCC PowerShell module, just Exchange Online.

      Reply
  4. Patrick Schmidt says:

    This is really working, NICE!!
    One question is left ( for me ). With this i can execute the “old” CMDlets like Get-Mailbox. But if i use Get-ExoMailbox ( the new rest based calls), it still says

    “Get-EXOMailbox : You must call Connect-ExchangeOnline before calling any other cmdlet.”
    Any solution on that?

    Reply
      1. Patrick Schmidt says:

        Thank you for the quick response! Nice “hack” , but like you said, we should not use this in production currently, thats what the “beta” also suggests.. I stick with the “old” ones for now, even though they are slower and hope the EXO cmdlets will be updates soon or the API (adminAPI) released publicly so we can use REST directly.

        There seems to be no mechanism to get the endpoint ID to do that automatically. Wonder how the Cmdlet does obtain this!

        Also, this would not be possible with the given approach
        $mb | Get-EXOMailboxFolderStatistics

        Reply
        1. Justin Grote says:

          The irony is that the “production” endpoints still use the “beta” API, you can confirm this in fiddler…

    1. Vasil Michev says:

      Afaik no, the new EAC is not PowerShell based, and the APIs aren’t officially supported in any way just yet (even if you can get them to work).

      Reply
    1. Vasil Michev says:

      Not yet, the ExO Remote PowerShell endpoint is the only one currently supporting CBA, afaik.

      Reply
  5. Brad says:

    Vasil, can you provide (from start to finish) what’s necessary to connect to Exchange online via a app id and secret? I’m having trouble understanding which of each block of code needs to be used to get this working, as I can’t get it to work. Thank you

    Reply
    1. Brad says:

      I’m getting a 403, access denied with this. Where I have [xxx] I’ve replaced specific user values

      Add-Type -Path ‘C:\Users\[xxx]\Documents\Powershell\ExOnline\Microsoft.IdentityModel.Clients.ActiveDirectory.dll’
      $authContext4 = New-Object “Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext” -ArgumentList “https://login.windows.net/[xxx].onmicrosoft.com”
      $ccred4 = New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.ClientCredential -ArgumentList “[xxx]”, “[xxx]”
      $authenticationResult4 = $authContext4.AcquireTokenAsync(“https://outlook.office365.com”, $ccred4)
      $token = $authenticationResult4.Result.AccessToken
      $Authorization = “Bearer {0}” -f $Token
      $Password = ConvertTo-SecureString -AsPlainText $Authorization -Force
      $Ctoken = New-Object System.Management.Automation.PSCredential -ArgumentList “OAuthUser@[xxx]”,$Password

      $sessionCloud = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/PowerShell-LiveId?BasicAuthToOAuthConversion=true -Credential $Ctoken -Authentication Basic -AllowRedirection -Verbose
      Import-PSSession $sessionCloud

      Reply
      1. Vasil Michev says:

        Check the token, most likely you don’t have a proper Azure AD role assigned to the service principal object.

        Reply
  6. Mauro says:

    Hi Vasil,

    I’ve had issues with certain cmdlets. Specifically when running Add-RecipientPermission I get an error that says:

    ‘Active Directory operation failed on xxxxx.prod.outlook.com. This error is not retriable. Additional information: Access is denied.
    Active directory response: 00000005: SecErr: DSID-03152612, problem 4003 (INSUFF_ACCESS_RIGHTS), data 0

    However, if I run the cmdlet Add-MailboxPermission that one goes through fine without permissions issues. what’s going on here? I’m authing with a cert and granted global administrator and Exchange Admin roles to the applicationā€™s service principal.

    Thank you for any help that you can provide.

    Reply
    1. Vasil Michev says:

      Seems to run fine here. Perhaps you have some scopes restricting the use of Add-RecipientPermission? Can you run it fine when connecting via “regular” PS? In any case, you can report it to MS.

      Reply
      1. Mauro says:

        Hi Vasil,

        Any idea what scopes would restrict it?

        Thanks
        Youcef

        Reply
        1. Vasil Michev says:

          Custom management scopes, Get-ManagementScope.

        2. Anthony Lopez says:

          Hey Mauro, Vasil, I am having the exact same issue, same commands, same error. It’s actually intermittent, Add-RecipientPermission fails most of time, but sometimes it works. Did you ever figure it out? I have an active case with MS now.

          Thanks,
          Anthony

        3. Mauro says:

          I can’t seem to reply to Anthony, but after trying a couple of days ago it seems to be working. I had it on my backlog for a while. I wonder if it was a bug, and if you’re ticket with microsoft fixed it for all of us.

        4. Mauro says:

          @Anthony,

          Please ignore my previous comment. This is still an issue and it was not reasolved. please let me know if you found a solution.

          Thanks
          Mauro

  7. Anishka says:

    Hi Michev,

    I am able to using the client credentials and client id for MFA enabled account and connect to exchange powershell using below command:-
    $Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/PowerShell-LiveId?BasicAuthToOAuthConversion=true -Credential $Ctoken -Authentication Basic -AllowRedirection -Verbose
    Im

    Can you please tell me that this approach is recommended by Microsoft for connection or they are going to disable this approach in future?

    Reply
    1. Vasil Michev says:

      It’s neither recommended not supported, use at your own risk. What’s supported is to use the module.

      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.