Connecting to Exchange Online PowerShell by passing an access token

December 2022 has certainly been an eventful month for Exchange Online PowerShell, with bunch of new improvements to cover, and the news of the planned deprecation of RPS-based modules and methods hitting last week. We will leave the Remote PowerShell deprecation news for another article, and will focus on covering one small, but impactful addition introduced with the latest (preview) version of the “V3” Exchange Online PowerShell module – namely the addition of the -AccessToken parameter and support for passing access tokens in general.

To start of, you will need the latest preview version of the module installed. At the time of writing, this is the 3.1.0-Preview1 version, which you can download over at the PowerShell Gallery. As you can see from the release notes, not much has changed in this release:

v3.1.0-Preview1 :
1. Support for providing an Access Token with Connect-ExchangeOnline.
2.  Bug fixes in Connect-ExchangeOnline and Get-ConnectionInformation.

Unfortunately, that’s pretty much all the information you can find about the new -AccessToken parameter, as no documentation has been published on how to use it. The only clue we can get is from the Connect-ExchangeOnline cmdlet help:

-AccessToken

Note: This parameter is available in version 3.1.0-Preview1 or later of the module.

The AccessToken parameter specifies the OAuth JSON Web Token (JWT) that’s used to connect to ExchangeOnline.

Depending on the type of access token, you need to use this parameter with the Organization, DelegatedOrganization, or UserPrincipalName parameter.

In this article, I will describe the steps required to connect to Exchange Online via this new method. Those of you that have already played with certificate-based authentication or used the unsupported variations (such as the methods detailed here or here) will find the process quite familiar, as the introduction of the –AccessToken parameter basically makes such methods officially supported. For example, you can now use the -AccessToken parameter together with the -Organization one in order to connect via a client secret, instead of using the unsupported methods outlined in the second article above.

Connecting to Exchange Online PowerShell via an access token

You can use any existing method in order to obtain the access token, such as the ADAL/MSAL-based snippets used in the articles above. The important thing is that once the token is obtained, you can now pass it directly to the Connect-ExchangeOnline cmdlet, instead of using the unsupported, and likely to be deprecated, method based on the New-PSSession cmdlet used in said articles. And, depending on the flow used, you will either have to provide some additional details, such as the -Organization identifier when connecting as a service principal using the application permissions model, or the -UserPrincipalName when connecting in the context of a user within the delegate permissions model.

But I digress. The idea behind this article was to show you how to enable the latter scenario, namely connecting in the context of a given user while passing an access token. To start with, you will need an Azure AD application with the necessary permissions granted. When connecting in the context of a user, you can and you should be leveraging the built-in Microsoft application (clientID of fb78d390-0c51-40cd-8e17-fdbfab77341b), unless you have a valid reason to use your own. If you are reading this article, I will assume you do, so let’s continue with creating the Azure AD app registration.

Open the Azure AD blade, go to App registrations and hit the New registration button. Enter a Name for you app, and for Supported account types, select the first option, Accounts in this organizational directory only. Other values can also work, but that’s not relevant for the current scenario. Lastly, under Redirect URI, select Public client/native (mobile & desktop) from the dropdown menu and enter a value for the URI. Again, some caveats apply here, but for the purposes of our scenario we can enter something like https://blabla. Hit the Register button to complete the process.

The app registration will now be created and you will be redirected to the Overview page for the new object, where you can see some important details we will need later on. Take a note of the Application (client) ID value and copy it. You can also copy the Directory (tenant) ID value, if you don’t already know it. At this point we can already fetch an Access token for our newly registered app, however, the permissions that will allow us to access Exchange Online PowerShell have not been granted yet. In order to ensure proper access, go to API permissions, hit the Add a permission button, select APIs my organization uses, then search for and select the Office 365 Exchange Online entry. Next, select Delegated permissions, expand the Exchange node and select Exchange.Manage entry. Finally, hit the Update permissions button.

The screenshot above illustrates how the API permissions should look like for our newly registered app. This is an important step – without the Exchange.Manage permission you will be presented with an UnAuthorized error when trying to connect to Exchange Online PowerShell. Also note that the Exchange.Manage permission does not require any sort of admin consent, so every user will be able to leverage our new app to obtain a valid access token for Exchange Online PowerShell. The usual RBAC and protocol-level controls apply though, so this is not a security issue. In addition, you can also restrict who gets access to our new app, but toggling the Assignment Required property for the corresponding service principal object, and adding Users and groups as needed.

So, we now have an application we can leverage to obtain an access token for Exchange Online management. All that is left now is actually fetching the token and then passing it to the Connect-ExchangeOnline cmdlet. To obtain the token, you can use a variety of methods, such as the MSAL- and ADAL-based snippets shown below, the various SDKs, direct HTTPS requests, even “borrowing” a token obtained from the module itself. The method doesn’t really matter, as long as the token is a valid one and contains the Exchange.Manage permission.

#Obtain an access token via MSAL

Add-Type -Path "C:\Program Files\WindowsPowerShell\Modules\MSAL\Microsoft.Identity.Client.dll"

$app2 = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create("59bd1626-xxxx-xxxx-xxxx-4b5ec0b5532b").WithTenantId("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx").WithRedirectUri("https://ExOPSApp").WithBroker().Build #Azure Android app

$Scopes = New-Object System.Collections.Generic.List[string]
$Scope = "https://outlook.office365.com/.default"
$Scopes.Add($Scope)

$token = $app2.Invoke().AcquireTokenInteractive($Scopes).WithLoginHint("user@tenant.onmicrosoft.com").ExecuteAsync().Result


#Obtain an access token via ADAL
Add-Type -Path 'C:\Program Files\WindowsPowerShell\Modules\AzureAD\2.0.1.10\Microsoft.IdentityModel.Clients.ActiveDirectory.dll' #-PassThru

$authContext3 = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList "https://login.windows.net/tenant.onmicrosoft.com"
$plat = New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters -ArgumentList "Auto"
$authenticationResult = $authContext3.AcquireTokenAsync("https://outlook.office365.com", "59bd1626-xxxx-xxxx-xxxx-4b5ec0b5532b", "https://ExOPSApp",$plat);

$token = $authenticationResult.Result.AccessToken

For the sake of completeness, here’s how a decoded token should look like. Note the presence of the Exchange.Manage permission under the scp claim. And since we’re obtaining a token in the context of a given user, you can also find the relevant user claims, including the list of admin roles assigned to the user.

As mentioned above, no admin consent is required for the Exchange.Manage permission. Still, the first time a given user tries to obtain a token via our app, a consent screen will popup, screenshot of which I’m also adding for the sake of completeness:

Now that we have a valid token, all that’s left is to pass it to the Connect-ExchangeOnline cmdlet, by providing it as value for the -AccessToken parameter. And since we obtained a token in the context of a user, we will also use the -UserPrincipalName parameter. And that’s it:

From here on, it’s business as usual – the user will get access to the set of cmdlets granted to him based on the roles/admin permissions assigned. It is important to understand however that this method DOES NOT cater to token expiration, session renewal and so on. Lots of the goodness added with the V2/V3 versions of the Exchange Online PowerShell module revolves around making things easier, more reliable and performant, and by leveraging this semi-automated method, you will be impacting your experience, in a negative way. While it’s a good thing to have the added flexibility of using this method, I wouldn’t recommend doing so, unless you have a valid reason/requirements.

And of course, we can pass a parameter obtained in the context of an application, combined with the –Organization parameter, to emulate the CBA-based experience, with a token obtained outside of the module (such as one leveraging the client credentials flow). The process is the same – obtain an access token with sufficient permissions, but this time in the application context, then pass it to the –AccessToken parameter. Refer to the article above for all the details on how to set up an app and obtain a token for this scenario.

Before closing the article, a small warning – pay attention to the value you provide for the -UserPrincipalName parameter. Why Microsoft does not validate the value, or leverage the corresponding claim directly from the access token, if beyond me. But a mistake therein can have quite interesting repercussions… more on that in another article 🙂

34 thoughts on “Connecting to Exchange Online PowerShell by passing an access token

  1. Paul says:

    Hi Visil,

    I got an error : The role assigned to application xxxxxxxxxxxxxxxxxxxxxxxxxxxxx isn’t supported in this scenario. Please check online documentation for assigning correct Directory Roles to Azure AD Application for EXO App-Only Authentication.
    The token is Ok (I can connect to MgGraph with the same) and the EXO module is updated. ManageApps role is granted (delegated and application)

    Could you help me please ?

    Reply
  2. Martin says:

    Vasil, can you go into more detail on
    “When connecting in the context of a user, you can and you should be leveraging the built-in Microsoft application (clientID of fb78d390-0c51-40cd-8e17-fdbfab77341b), unless you have a valid reason to use your own.”
    If I don’t have a specific reason for using my own app, e.g. because of specific permissions, is it then best practice to use “Microsoft Exchange REST API Based Powershell” application which is associated with the ID you mentioned (https://learn.microsoft.com/en-us/troubleshoot/azure/active-directory/verify-first-party-apps-sign-in)?
    Thanks, Martin

    Reply
    1. Vasil Michev says:

      Yes, as said SP is already registered in any tenant using Exchange Online, so there’s no reason to duplicate it. Smaller attack service and all. Plus, if the required permissions ever change, the built-in SP will likely continue to work, whereas any custom ones you create will have to be manually updated.

      Reply
      1. Martin says:

        Thanks Vasil.
        Do you know if there is SP for managing Exchange Online connectors and transport rules?
        “Exchange.Manage” (delegated) or “Exchange.ManageAsApp” (application) needs to be included.

        Reply
        1. Vasil Michev says:

          That would depend on the type of application you plan to use. There are no separate permissions required for management of connectors and transport rules, the Graph API is effectively only used for authentication. The permissions granted to the user (or SP) within Exchange Online determine which specific operations/cmdlets they will be able to use.

      2. David Homer says:

        I did wonder whether Microsoft would have a problem with you pretending to be their application. It is a bit weird because if you write an application that uses the EXO module for authentication then your application would appear to be the EXO module to the Microsoft online servers. I wonder if there’s an official Microsoft position on this.

        Reply
        1. Vasil Michev says:

          There’s nothing wrong with running PowerShell cmdlets by passing an auth token you obtained for the built-in ExO PS app. Nothing wrong with using a custom app either, if Microsoft didn’t want this to happen, they wouldn’t have made the corresponding scopes available to the generic public.

  3. Yevgen Dyuburg says:

    Hi Vasil, thank you so much for such for the article, but how can you populate
    Connect-ExchangeOnline -AccessToken?
    I tried to reproduce your scenario, but all I got is this error message:
    Connect-ExchangeOnline : A parameter cannot be found that matches parameter name ‘accesstoken’.
    At line:1 char:24
    + Connect-ExchangeOnline -accesstoken eyJ0eXAiOiJKV1QiLCJub25jZSI6ImRha …
    + ~~~~~~~~~~~~
    + CategoryInfo : InvalidArgument: (:) [Connect-ExchangeOnline], ParameterBindingException
    + FullyQualifiedErrorId : NamedParameterNotFound,Connect-ExchangeOnline

    Reply
  4. Ponith says:

    Hi, Vasil

    How do I implement OAuth flow URL for registered app with graph permission and Office365 Exchange Permission.
    Permissions –
    Graph – Application.Read.All, AuditLog.Read.All, Directory.Read.All, Domain.Read.All
    Office 365 Exchange Online – Exchange.ManageAsApp
    All these are application permissions
    How do I include in URL for OAuth Consent these permission, when I include all the permission I am getting an error – invalid_client&error_description=AADSTS650053: The application ‘TestMutliTenantApp’ asked for scope ‘Exchange.ManageAsApp’ that doesn’t exist on the resource ‘00000003-0000-0000-c000-000000000000’. Contact the app vendor.Trace

    Reply
    1. Vasil Michev says:

      00000003-0000-0000-c000-000000000000 is the Graph API, you need to add the permissions on the Exchange Online API instead.

      Reply
      1. Ponith says:

        Yes, I have added in Office365 Exchange Online API – Exchange.ManageAsApp
        I was success while using OAuth 2.0 with only graph permissions with URL
        https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=$ClientID&response_type=code&redirect_uri=https://google.com&response_mode=query&scope=offline_access%20Directory.Read.All%20User.Read.All&state=12345&prompt=consent

        But here I also want Exchange.ManageAsApp, so I have added them in the scope of the URL as
        https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=352a4b84-14ce-4c43-954d-0e3a17b31ae8&response_type=code&redirect_uri=https://checkred.com&response_mode=query&scope=offline_access%20Directory.Read.All%20User.Read.All%20Exchange.ManageAsApp&state=12345&prompt=consent

        I have tired adding both, https://outlook.office365.com/Exchange.ManageAsApp and also only Exchange.ManageAsApp, the result is same error
        I was using all these permissions as Application Permissions
        However this is my app’s manifest of required resource access:
        “requiredResourceAccess”: [
        {
        “resourceAppId”: “00000003-0000-0000-c000-000000000000”,
        “resourceAccess”: [
        {
        “id”: “5b567255-7703-4780-807c-7be8301ae99b”,
        “type”: “Role”
        },
        {
        “id”: “498476ce-e0fe-48b0-b801-37ba7e2685c6”,
        “type”: “Role”
        },
        {
        “id”: “dc377aa6-52d8-4e23-b271-2a7ae04cedf3”,
        “type”: “Role”
        }
        ]
        },
        {
        “resourceAppId”: “00000002-0000-0ff1-ce00-000000000000”,
        “resourceAccess”: [
        {
        “id”: “dc50a0fb-09a3-484d-be87-e023b12c6440”,
        “type”: “Role”
        }
        ]
        }
        ]

        I gave necessary permission to my app and also client have those permissions. The scopes that comes under Graph API does not go through any error. I also gone through various references for that but got no solution found till now.
        How should I had Exchange.ManageAsApp in scope of the URL with graph permissions so that my in any env can use all the graph and exchange permissions.

        Reply
  5. Ponith says:

    Hi Vasil,
    I had implemeted you approch and it worked well before but then,
    Till last week this worked with access token with application permission, but from last 3 days it’s throughing a error

    OperationStopped: The role assigned to application 6f4867d1-7d57-4c66-8bec-e261b82e7f64 isn’t supported in this scenario. Please check online documentation for assigning correct Directory Roles to Azure AD Application for
    EXO App-Only Authentication.

    I don’t know what I have changed, I also tired with the same old app, even that app was throughing the same error.

    I had Application permission : (Exchange.ManageAsApp) – Manage Exchange As Application
    and I am generating token with script which is as follow :
    def get_access_token(tenant_id, client_secret, client_id):
    r = requests.post(“https://login.microsoftonline.com/” + tenant_id + “/oauth2/v2.0/token”,
    data={“grant_type”: “client_credentials”,
    “client_secret”: client_secret,
    “client_id”: client_id,
    “scope”: “https://outlook.office365.com/.default”}) # access token for ExchangeModule
    ret_body = r.json()
    print(ret_body[‘access_token’])

    What am I doing wrong I wasn’t able to figure it out, My PowerShell Version is – 7.3.4 and ExchangeOnlineModule Version is – 3.1.0

    Note: – Last week I had updated my PowerShell from 7.2.1 to 7.3.4 version, is this effects?

    Reply
      1. Ponith says:

        I don’t know why, but I am still encountering the same error – OperationStopped: The assigned role for application APPLICATIONID is not supported in this scenario. Please refer to the online documentation to correctly assign Directory Roles to Azure AD Application for EXO App-Only Authentication.

        And, the token includes an audience and roles that appear as follows:
        {
        “aud”: “https://outlook.office365.com”,
        “iat”: 1685356305,
        “nbf”: 1685356305,
        “exp”: 1685360205,
        “app_displayname”: “TestMutiTenantApp”,
        “appid”: “kl3413423-o93b-4c43-954d-934scjn3282”,
        “appidacr”: “1”,
        “roles”: [
        “Exchange.ManageAsApp”
        ]
        }

        I don’t know where I went wrong.

        Reply
        1. Ponith says:

          Hi Vasil,
          Thanks for all the info, It’s really helpful
          One last question as it is multi tenant application we need to add Add Azure AD role in each tenant to service principal?
          Do we have a way where we can assing a role to Published App and those roles would be applicable to all other client in which the application is present?

        2. Vasil Michev says:

          For MT apps, each tenant will have to consent to the permissions required, no other way around it.

  6. ax says:

    I followed your directions and ran into problem after problem. The very first line Add-Type -Path “C:\Program Files\WindowsPowerShell\Modules\MSAL\Microsoft.Identity.Client.dll” errors out saying the .dll doesn’t exist. Sure enough it doesn’t. I did find a MSAL.PS folder and put that path in the command and it took it. Then then very next line errors with this “Exception calling “WithBroker” with “0” argument(s): “The Windows broker is not directly available on MSAL for .NET Framework To use it, please install the nuget package named Microsoft.Identity.Client.Desktop and call the extension method .WithDesktopFeatures() first.””.
    Since that didn’t work I looked around and found this Install-Module -Name MSAL.PS but have no idea where it installed it unless it put it in the same place as the other which is “C:\Users\user1\Documents\WindowsPowerShell\Modules\MSAL.PS\4.36.1.2\Microsoft.Identity.Client.4.36.1\net45\Microsoft.Identity.Client.dll” and “C:\Users\user1\Documents\WindowsPowerShell\Modules\MSAL.PS\4.36.1.2\Microsoft.Identity.Client.Desktop.4.36.1\net461\Microsoft.Identity.Client.Desktop.dll” in which none of them work. What am I missing?

    Reply
    1. Vasil Michev says:

      There are gazillion versions of those binaries circulating the internet, as different versions of the PowerShell modules (and MS apps in general) use different ADAL/MSAL builds, so it’s hard to get an uniform experience. Consider the examples above as just that, examples. It’s the idea that counts – the fact that you can obtain a token outside of PowerShell and pass it to the Connect-ExchangeOnline cmdlet.

      If you simply want to test this, you can use existing connections to Exchange Online to obtain a token:

      Connect-ExchangeOnline -UseRPSSession -UserPrincipalName vasil@michevdev3.onmicrosoft.com
      (Get-PSSession).Runspace.ConnectionInfo.Credential.GetNetworkCredential().Password

      and for a non-RPS session:

      #Get any existing contexts 
      $context = [Microsoft.Exchange.Management.ExoPowershellSnapin.ConnectionContextFactory]::GetAllConnectionContexts() 
      
      #Get an existing token from the cache
      $context[0].TokenProvider.GetValidTokenFromCache("Get-Mailbox").AuthorizationHeader
      
      #Or generate a new one 
      $context[0].TokenProvider.GetAccessToken()
      Reply
      1. Ax says:

        What worked for me was to connect to ExhangeOnline like you suggested, then used these two commands you gave me…

        $context = [Microsoft.Exchange.Management.ExoPowershellSnapin.ConnectionContextFactory]::GetAllConnectionContexts()
        $context[0].TokenProvider.GetValidTokenFromCache(“Get-Mailbox”).AuthorizationHeader

        Then I copied the output and pasted it into a script using $token as the variable to capture it. Removed the word Bearer and all the line feeds it included and it worked.
        I’ve been trying to connect from a headless Linux server, but it would always want to open a browser to authenticate. Since there was no GUI much less a browser, it would fail. This solved the problem.
        Thank you

        Reply
      2. Ax says:

        Now it doesn’t work. I came back a week later, ran the script and I get this error.

        UnAuthorized
        At C:\Program Files\WindowsPowerShell\Modules\ExchangeOnlineManagement\3.2.0\netFramework\ExchangeOnlineManagementBeta.psm1:733 char:21
        + throw $_.Exception;
        + ~~~~~~~~~~~~~~~~~~
        + CategoryInfo : OperationStopped: (:) [], UnauthorizedAccessException
        + FullyQualifiedErrorId : UnAuthorized

        Why is it so damn difficult to connect to Exchange Online without being prompted to sign in each time?

        Reply
  7. Piyumi Perera says:

    Hi,
    I have implemented a .net 6 console app to connect with EXO using Powershell using app only authentication. I went through this article and tried to use access token to connect exchange online. Since here we are adding Delegate permission I obtained the access token using Auth Code flow using following request.
    https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token
    scope: https://ps.outlook.com/Exchange.Manage
    code:{code}
    redirect_uri:{redirect_uri}
    grant_type:authorization_code
    client_secret:{client_secret}
    client_id:{client_id}

    Then execute following PS command using Windows Powershell
    Connect-ExchangeOnline -UserPrincipalName {user_email} -AccessToken {access_token}

    But I am getting an error as follows;
    UnAuthorized
    At C:\Program Files\WindowsPowerShell\Modules\ExchangeOnlineManagement\3.1.0\netFramework\ExchangeOnlineManagementBeta.psm1:733 char:21
    + throw $_.Exception;
    + ~~~~~~~~~~~~~~~~~~
    + CategoryInfo : OperationStopped: (:) [], UnauthorizedAccessException
    + FullyQualifiedErrorId : UnAuthorized

    Can some one help to solve this?

    Reply
    1. Vasil Michev says:

      Decode your token and make sure it contains the required scopes. Also check if the user has sufficient permissions to connect to PowerShell, as it might have been blocked (i.e. try connecting directly via the module instead of passing token).

      Reply
      1. Piyumi Perera says:

        Yes scope is there.
        “scp”: “Exchange.Manage User.Read”

        And using the windows powershell user can connect to exchange Online.

        Reply
        1. Vasil Michev says:

          “UnAuthorized” is usually token related, so I’d double-check on that. Can you paste the decoded token?

        2. Vasil Michev says:

          The audience is wrong, try using the default “https://outlook.office365.com/.default” scope instead.
          Also, you can share a decoded token by parsing it via JWT.ms or JWT.io tools, no need to share the actual token.

  8. Volodymyr says:

    Hi, Vasil

    Thanks for you article!

    I faced a problem with connecting to Exchange Online using Connect-ExchangeOnline cmdlet.
    I obtained an access token for my application via an application ID and secret and than called Connect-ExchangeOnline with the token. Everything worked fine. Connect-ExchangeOnline showed me information of the connection. There was the property TokenExpiryTimeUTC which was 12/31/9999 11:59:59 PM +00:00. In fact after about an hour the token expired and any cmdlet failed with “Maximum retry count reached.” error.
    Is any way to prolong the session?
    Another question why the failed request is retried? I think there is no sense to retry a request with an expired access token.

    Reply
    1. Vasil Michev says:

      If you pass a token manually, it’s your responsibility to monitor its validity and renew it – the module will do nothing about this.As for the error message, you can ignore it, the root cause is likely the token expiration.

      Reply
      1. Volodymyr says:

        Thanks

        I found another issue. I use this module from a .NET application. In the application I need to connect to different Azure tenants. I create a new PowerShell runspace for each tenant and call Connect-ExchangeOnline in each runspace. All cmdlets except Exo-cmdlets (Get-ExoMailbox for example) work correctly. All Exo-cmdlets from all runspaces use the last opened connection.

        May be you know where it is better to write the bug report?

        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.