Modern Auth and Unattended Scripts in Exchange Online PowerShell V2

Published Jun 30 2020 08:14 AM 59.8K Views

Today, we are happy to announce the Public Preview of a Modern Auth unattended scripting option for use with Exchange Online PowerShell V2. This feature provides customers the ability to run non-interactive scripts using Modern Authentication. This feature requires version 2.0.3-Preview or later of the EXO PowerShell V2 module, available via PowerShellGallery.

Check out the detailed guide on how to install/update the new EXO PowerShell V2 Module here.

As previously announced, Basic Authentication for Exchange Online Remote PowerShell will be retired in the second half of 2021. Customers who currently use Exchange Online PowerShell cmdlets in unattended scripts should switch to adopt this new feature. This new approach uses AzureAD applications, certificates and Modern Authentication. You can find detailed step-by-step instructions available here.

It’s simple to create and use sessions using this new feature. For example, if you are currently using Basic Authentication for unattended scripting, you are probably using something like this in your scripts;


New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri -Credential $UserCredential -Authentication Basic -AllowRedirection


Once you have changed to Certificate Based Authentication, the above cmdlet pattern will need to be changed to use Connect-ExchangeOnline along with other necessary parameters. For example:


Connect-ExchangeOnline -CertificateFilePath "C:\Users\johndoe\Desktop\automation-cert.pfx" -CertificatePassword (ConvertTo-SecureString -String "<My Password>" -AsPlainText -Force) -AppID "36ee4c6c-0812-40a2-b820-b22ebd02bce3" -Organization ""


Please note the feature does not support delegation. Unattended scripting in delegation scenarios is supported with the Secure App Model which is documented here.

We hope this new feature makes it possible for you to move away from using Basic Authentication for your unattended scripting needs and appreciate the increased security this new option provides. Please do give us feedback, we really do want to hear what you think.

The Exchange Team

Regular Visitor

Great News!


Will this work with Exchange RBAC roles (custom roles) or only with Azure default service roles?

Occasional Contributor

Great News. A Welcome Addition !


But I was reading the article Friday which made no suggestion this is a public preview ... maybe worth flagging on there in case folks come across that link without reading this blog post.

Occasional Contributor

This is great with one slight amendment on the command... The AppID needs to be the Registered Apps AppID GUID not its displayname... Otherwise it does not locate the registered app in the target tenant... 


@JamesIII - We are only supporting Exchange Built-in roles which are available in AAD as those are the ones which can be assigned to an AAD App.


@Jamie BRANDWOOD - Thanks for the note. This has been corrected and docs changes should reflect in few hours.


@Paul Westlake You are right and that's what we intend to tell with the parameter name. Name is generally not considered as unique and hence GUID will be the one to use. We will call it out explicitly in example.

Senior Member

The installation path for 2.0.3-preview has been changed to %UserProfile%\Documents\WindowsPowerShell\Modules\ExchangeOnlineManagement. Compared to the GA version, it is C:\Program Files\WindowsPowerShell\Modules\ExchangeOnlineManagement.

Is this a permanent change or just for the preview version?

Frequent Contributor

@victorguo If you install the module with -Scope CurrentUser, it will go into %UserProfile%, while if you don't do it that way, it will install system-wide into Program Files.  It's a PowerShell / modules thing, but not anything exclusive to this module.


So happy to see this preview go live.

Regular Visitor

This is great Addition and will help to run auto script scheduler... hope it works with Azure Automation...

Senior Member

YOU MUST NOT USE CNG CERTIFICATES, for some bizarre reason there's no support for it in this module, and that is the default type of certificate created/imported by Windows 10


When generating your cert use New-SelfSignedCertificate -KeySpec KeyExchange to force a csp provider.

if you see

New-EXOPSSession : Invalid Provider Type Specified

as an error, this is your problem. I wasted several hours, would have been nice to know...


Also, it doesn't work in Azure Automation, which is where a lot of Exchange Tests run, throws a network dll error, you would think it would have been tested...



Frequent Contributor

@JGrote I see "New-EXOPSSession".  Could it be that you mean to be using Connect-ExchangeOnline instead?  I was doing some digging to see if I could find an answer about OAuth or JWT's requiring the KeySpec to be Signature, because most examples I see from show the New-SelfSignedCertificate command having "-KeySpec Signature", so that's what I've always used.  But now I've done a test, and can confirm I'm able to connect using both a KeySpec = KeyExchange cert and KeySpec = Signature cert.


I create my self-signed certificate from Windows 10 and have pure success for both ways.  I just updated my New-SelfSignedAzureADAppRegistrationCertificate function (link to containing module), giving it a new parameter -KeySpec [Signature|KeyExchange] so I could test.   Both work fine for me from Windows 10 / Windows PowerShell 5.1, and in Azure Automation.


Maybe you're provider has a typo?


Last thing, I checked the script file Create-SelfSignedCertificate.ps1 and see that it actually uses KeyExchange for the key spec (1 = KeyExchange, 2 = Signature):


$key = new-object -com "X509Enrollment.CX509PrivateKey.1"
    $key.ProviderName = "Microsoft RSA SChannel Cryptographic Provider"
    $key.KeySpec = 1
    $key.Length = 2048 
    $key.SecurityDescriptor = "D:PAI(A;;0xd01f01ff;;;SY)(A;;0xd01f01ff;;;BA)(A;;0x80120089;;;NS)"
    $key.MachineContext = 1
    $key.ExportPolicy = 1 # This is required to allow the private key to be exported

So with all of this, I don't think the module has an issue with KeySpec = KeyExchange for the self-signed certificate being used.


UPDATE:  And ahhh, just like that, I am now seeing what you mean.  It's that if you DON'T specify -KeySpec Signature OR KeyExchange with the New-SelfSignedCertificate cmdlet, it will default to None and yada yada ==> CNG.  Figured that out here -


So now I get your point.  Windows 10's New-SelfSignedCertificate by default will create a non-working cert for us if we don't know better to take charge with the -KeySpec parameter.


Senior Member

@Jeremy Bradshaw try it with Keyexchange none. Both the keyexchanges you specified force a CSP certificate, not a CNG certificate.

Frequent Contributor

@JGrote I tried to update my post to show that I get what you mean now.  Just realized I put the update in the middle of my original message.  Will fix that, then delete this one.


It's a valid point you raised.  I think the step by step guide on docs could stand to be updated to clarify this issue to lookout for, but in their defense, they show using the downloadable script to create the self-signed cert, and it sets KeySpec to KeyExchange, so if people follow their guide, they should be good to go.


Trying to get this working, Unfortunately, I end up with New-ExoPSSession : No certificate found for the given CertificateThumbprint
I am assuming that the cause is that the certificate is in the personal folder for the local machine (because the account using the script will be a service account) any ideas how I can redirect the module to search the local machine compartment? 

Frequent Visitor

Is there plans to support authenticating directly with access/refresh tokens like most of the new Azure/O365 cmdlets support?

Senior Member

I made a GistBlog about how to add this to your Azure Automation RunAs account


A  was If i try it after putting the very into a personal store it doesn’t return an error burn it still opens the sign-in dialog IE requiring interaction, Which is useless given the point is automation. Anyone else run into this?


UPDATE: it’s working now I had a blank thumbprint

Frequent Contributor

I've something to split hairs over.  It is the "-AppId" and "-Organization" parameters.  I feel like it would be better to make them "-ApplicationId", and "-TenantId".  Between Connect-AzureAD, Connect-AzAccount, Connect-PartnerCenter, the list goes on, they all have "-ApplicationId" and they have "-TenantId" (at least as an alias to "-Tenant" if not the primary parameter name).


So this module is suddenly different from those other modules with "-AppId" and "-Organization".  This difference is unnecessary and bad practice (in my opinion).  It should all be nice and consistent, considering it's all for authentication to the same place, via the same method.


@Jeremy Bradshaw  In the Organization parameter, we expect the tenant name to be passed and not the GUID as we have mentioned in the documentation. To avoid confusion, we explicitly chose Organization parameter instead of TenantId. Tenant ID generally is used for the GUID of the tenant which doesn't work in this case as request routing doesn't happen consistently to the right backend machine.


@MicahRowland - Support for AccessToken is in our backlog. We don't have a committed timeline to support it yet.

Senior Member

@navgupta is there any way to resolve the tenant name from the organization GUID *without* being authenticated?

I am using this in Azure Automation and I can get the tenant ID of the principal from the get-automationconnection, and I can use that with every app except Exchange, however to authenticate to azure just to resolve the tenant ID to then log into exchange adds a lot of time to the login. Is there any public API to do the resolution unauthenticated?

Frequent Visitor

@navgupta I looked at the module code in Reflector and it looks like you're connecting the same way we are (connectionUri 

"" and U/P with the authorization header contents as the password), there's just no way to bypass the token acquisition process. I originally tried passing the same formatted creds, but it looks like you're using the OAuth Resource Owner Password Credentials grant, so when it tried to pass the auth header as the password, it returned a 'your password is too long' error


We're an MSP and the "control panel" product I work on is not intended to have standing access to a single exchange environment. While we could use the -Credentials route, it would require us to collect customer user/passwords, which is a no-no, so having the ability to authenticate with access tokens is critical because we can't surface an authorization code flow popup to the end user during scheduled automations. Passing a refresh token would be fine as well, which you could use to retrieve the access token you need to connect and maintain access in the tokencache


Hey everyone! I created the PSServicePrincipal PowerShell module that is a helper module that will reduce the onboarding steps for Certificate Based Authentication to just to a few steps and you can do that from PowerShell command line itself. To fully automate this you must have sufficient permissions to register an application with your Azure AD tenant. The module will handle the creation of the service principal / application, certificate (local and Azure) and set the correct Exchange unattended application permissions. All you need to do is verify a RBAC role for security and accept the settings. You can download the module from the PowerShell Gallery here. The full help file can be found here.

Senior Member

@Dave Goldman  nice work! I'll add it to my article, I was going to write something similar but you saved me the trouble :)

Occasional Visitor

what about Skype for business online module / Teams Module? we do need similar solution for them as well? any reference ETA or workround?

Senior Member



Is there any difference using this vs using:


Connect-ExchangeOnline -Credential $Credentials



What I mean is that in C# we are already opening a PowerShell, loading Exchange Online add-in, and using Connect-ExchangeOnline and just passing Credentials built during the process (login + decrypted SecureString in file).


Yes it might looks cleaner to have a certificate and an AppID, but in terms of usage is it necessary / will the current approach with classic credentials break when Basic Auth will be stopped?




@JrouziesM Connect-ExchangeOnline uses Modern Authentication is all scenarios, whether you pass credentials via authentication prompt or pass it silently via Credential parameter. So, after Basic Auth Deprecation date, passing credential using -Credential parameter won't break and it will continue to work.


Please note that if you are using new-pssession with credential parameter, it will break after Basic Authentication is disabled.


However, we recommend using Certificate as it is more secure and standard way of authenticating background scripts.

Occasional Visitor

In following the instructions currently listed under "Connect using an existing service principal and client-secret", I hit a roadblock.


$authName = <tenant>
$clientId = <clientId>
$resourceId = ""
$username = <username>
$password = <password>
$token = Get-ADALAccessToken -AuthorityName $authName -ClientId $clientId -ResourceId $resourceId -UserName $username -Password $password $token = ConvertTo-SecureString -String $token -AsPlainText -Force $AppCredential = New-Object System.Management.Automation.PSCredential($username, $token) Connect-ExchangeOnline -Credential $AppCredential


Error response:


New-ExoPSSession : AADSTS50052: The password entered exceeds the maximum length of '256'.


Am I doing something wrong or does the 2.0.3-Preview prerelease not currently support this method as the documentation purports?


FYI - Many are having the same issue. Either the module should be fixed or the doc amended to remove this currently supported functionality:

Frequent Visitor

@SolomonIsaacson, I ran into the same issue. See my comments above. The issue is that -Credential expects a username/password (not user/token), and it then retrieves the access token using the ROPC grant and connects using the 

"" connection uri passing it the username and a password in the format of "Bearer [accesstoken]".

Unfortunately, there's no support currently for passing an access token, but I hope they add this sooner rather than later.


Senior Member

@navgupta thanks the clear explanation :)


By the way, do you know if Connect-ExchangeOnline will implement a way to limit the imported Cmdlets?


With New-PSSession -Commands I optimized some scripts that do a lot of switching between OnPrem / Online, and I limit the Cmdlets imported to what is needed in the script. It is a bit annoying to lose 30s waiting for all of them to be imported, when limiting takes like 2s.

Senior Member

@JrouziesM This is easily fixable, I made a list of cherry pickable patches that the Exchange team can merge and this is one of them. So far gotten the "brick wall" response.

Regular Visitor

Total mailboxes in org are 1.5 million. I tried this module to test by executing Get-Mailbox cmdlet.  After 1 hour, access denied occurred. I know Get-EXOMailbox will solve this. But, there are other cmdlets which will run for more than 1 hour.  


Will this work for cmdlets that run for more than 1 hour without any issues or am I missing something? 


@o355developer for the best performance you will want to batch and fan those out if you are going to use the classic cmdlets because of the throttling, session time outs, etc. The ideal solution would be to move to the V2 cmdlets for as much as you can as they fix the fanout need.

Regular Visitor

@Dave Goldman I understand that moving to v2 cmdlets is best fit. But, there are other classic cmdlets like Get-user, Get-group etc., which will face the same issue. They may run more than an hour or will break in the middle of a long script. I found the implicit session handler only solve the invoke of new cmdlets but not the already running cmdlets when access token expires in the middle of the cmdlet running time. How do we solve this?


Hi @o355developer, as I mentioned in my last reply for the best performance with the classic commands you will want to batch up your jobs and fan them out. This will solve your problem. More smaller jobs will complete faster than one large job.

Regular Visitor

@Dave Goldman  I understand your point and it will work for the cmdlets where iteration is possible. In my org, the below cmdlet for example runs for 70 to 75 minutes. It has around 2.5 million records. When I use modern authentication, Access denied error is thrown after 60 minutes. Using like filter to 

Get-MailContact -RecipientTypeDetails Mailcontact -ResultSize Unlimited

I am not sure how to batch out this job into multiple smaller jobs.  Can you help me resolve this case?

@o355developer  You will need to get the mailboxes that you need to do work against and break them in to a number of files [batch001.ext, batch002.txt, etc.). Each file will contain a certain amount of users. From there you can pass them in to background jobs to run your commands and then loop through all of the items you need to process. These batches will complete much faster allowing you to run through your entire collection without hitting any limits.


Senior Member

@o355developer A common though uneven way of doing this is searching by the first character of each name.

#Get-Mailbox A*, Get-Mailbox B*, etc.

(1..9),(65..90) | foreach {Get-Mailbox "$([char]$_)*" -ResultSize Unlimited}

There's also any of the million properties you can filter on as well.

Also is there  a specific reason you can't use Get-ExoRecipient? Or at least use it to get all users, break it into batches, and then use get-mailbox on those results? I can't think of many reasons your script would need to pull all 1.5 million users every single time, and maybe you can use the -Property parameter to narrow it down to just the properties you want and speed up the query?

Senior Member

Is there any advice on what needs to be done for unattended c# applications which use remote PS to perform mailbox related functions in o365 to migrate from basic to modern auth?

Regular Visitor

@JGrote Thanks for your suggestion. I am doing it this way since adapting modern authentication. I overwrote Get-PSImplicitRemotingSession to check token expiry before every command invoke. Just wanted to make sure if there is any provision like state listeners or setting up cookies when token expires similar to overriding Get-PSImplicitRemotingSession to check token life before command invoke. 


@Dave Goldman thank you for your kind replies.

Senior Member

In c# we use:

creds = new PSCredential(ActiveDirectory.O365AdminName, GetSecurePassword(ActiveDirectory.O365AdminPass));

String strRemoteComputerUri = ActiveDirectory.O365RemotePSUri;
WSManConnectionInfo conn = new WSManConnectionInfo(new Uri(strRemoteComputerUri),", creds);

conn.AuthenticationMechanism = AuthenticationMechanism.Basic;

I expect we will need an upgrade of some modules so that we can pass the cert info in new PSCredential command and then have a "Modern" option with AuthenticationMechanism in place of "Basic".


Is anyone working on this requirement?


Occasional Visitor

Will this authentication also work with Security & Compliance PowerShell?

Senior Member

Set this up, works great. But how can I track it?

In the AAD logs, under Sign-Ins, I can't see any of my PowerShell connections popping up when I switch to the Modern Authentication clients. 

Am I missing something? Can you share an example of how this would look in the logs?

Occasional Contributor

I am getting the following...

Could not load type 'System.Security.Cryptography.SHA256Cng' when calling Connect-ExchangeOnline from netcore 3.1

when using certificate auth.

Documented the issue here:

Senior Member

@ChrisJ not supported in Powershell 7. Windows Powershell 5.1 only. I know, in this day and age. Also doesn't support CNG certs.

Occasional Contributor

@JGrote how "modern" :) 

Occasional Contributor

Is there an official github repo or similar where we should report issues?  Apparently is only for the docs about this module. 

@Chris Johnson If you are having technical issues related to the Exchange Online PowerShell V2 modules, please ensure you have reviewed the module documentation here first.

For technical support:
- Issues related to Released (GA) PowerShell modules - please contact Microsoft Support
- Issues related to Preview PowerShell modules (The Module discussed on this page) please email the feedback address listed on the connection screen and we will review your submission. Thanks!

@Jimmy Brothers Currently this module is only supports Exchange Online at this time.

Senior Member
Any way to track these authentication sessions in the Azure AD Sign-In reports?
New Contributor

@paddy100 Could you elaborate on how you resolved the "No certificate found for the given CertificateThumbprint" issue?  I have set up Azure appropriately.  I placed the certificate in LocalMachine/Personal.  I can use a PowerShell script to search by the thumbprint and find the certificate.  However, I am getting this error, and I am unsure how to resolve it.


@DMilner put the cert in your local user cert store if interactive usage. Localmachine is for unattended automation.

New Contributor

@Dave Goldman That worked for me running the script locally to debug.  However, I am now getting the following error when trying to connect.  I added the app to the Global Reader role, but when connecting I get:

Processing data from remote server failed with the following error message: [AuthZRequestId=c4575098-8084-47bb-bc9e-6cefb1fd6711][FailureCategory=AuthZ-CmdletAccessDeniedException] The user
"[snip]" isn't assigned to any management roles.


Version history
Last update:
‎Jul 30 2020 05:41 AM
Updated by: