Connect to Exchange Online automated when MFA is enabled (Using the SecureApp Model)

So in the past months Microsoft has been forcing CSPs and MSPs to use MFA, something I strongly encourage and am glad with. The only issue with this was that Microsoft made this move without accounting for automation and automated jobs that need to run, especially jobs that run unattended and over multiple delegates.

To make sure that we could be able to use this I’ve been advocating and discussing this with Microsoft a lot, including a public part here, but after several months and great help from
Janosch Ulmer at Microsoft I’ve been able to compose a script for everyone to use, to connect to all Microsoft resources using the secure application model, including Exchange Online.

Compiling all of these scripts took me quite some time. If you have questions or issues just let me know!

The Script

    [Parameter(Mandatory = $false)]
    [Parameter(Mandatory = $true)]
    [Parameter(Mandatory = $false)]

$ErrorActionPreference = "Stop"

# Check if the Azure AD PowerShell module has already been loaded.
if ( ! ( Get-Module AzureAD ) ) {
    # Check if the Azure AD PowerShell module is installed.
    if ( Get-Module -ListAvailable -Name AzureAD ) {
        # The Azure AD PowerShell module is not load and it is installed. This module
        # must be loaded for other operations performed by this script.
        Write-Host -ForegroundColor Green "Loading the Azure AD PowerShell module..."
        Import-Module AzureAD
    } else {
        Install-Module AzureAD

try {
    Write-Host -ForegroundColor Green "When prompted please enter the appropriate credentials... Warning: Window might have pop-under in VSCode"

    if([string]::IsNullOrEmpty($TenantId)) {
        Connect-AzureAD | Out-Null

        $TenantId = $(Get-AzureADTenantDetail).ObjectId
    } else {
        Connect-AzureAD -TenantId $TenantId | Out-Null
} catch [Microsoft.Azure.Common.Authentication.AadAuthenticationCanceledException] {
    # The authentication attempt was canceled by the end-user. Execution of the script should be halted.
    Write-Host -ForegroundColor Yellow "The authentication attempt was canceled. Execution of the script will be halted..."
} catch {
    # An unexpected error has occurred. The end-user should be notified so that the appropriate action can be taken.
    Write-Error "An unexpected error has occurred. Please review the following error message and try again." `

$adAppAccess = [Microsoft.Open.AzureAD.Model.RequiredResourceAccess]@{
    ResourceAppId = "00000002-0000-0000-c000-000000000000";
    ResourceAccess =
        Id = "5778995a-e1bf-45b8-affa-663a9f3f4d04";
        Type = "Role"},
        Id = "a42657d6-7f20-40e3-b6f0-cee03008a62a";
        Type = "Scope"},
        Id = "311a71cc-e848-46a1-bdf8-97ff7156d8e6";
        Type = "Scope"}

$graphAppAccess = [Microsoft.Open.AzureAD.Model.RequiredResourceAccess]@{
    ResourceAppId = "00000003-0000-0000-c000-000000000000";
    ResourceAccess =
            Id = "bf394140-e372-4bf9-a898-299cfc7564e5";
            Type = "Role"},
            Id = "7ab1d382-f21e-4acd-a863-ba3e13f7da61";
            Type = "Role"}

$partnerCenterAppAccess = [Microsoft.Open.AzureAD.Model.RequiredResourceAccess]@{
    ResourceAppId = "fa3d9a0c-3fb0-42cc-9193-47c7ecd2edbd";
    ResourceAccess =
            Id = "1cebfa2a-fb4d-419e-b5f9-839b4383e05a";
            Type = "Scope"}

$SessionInfo = Get-AzureADCurrentSessionInfo

Write-Host -ForegroundColor Green "Creating the Azure AD application and related resources..."

$app = New-AzureADApplication -AvailableToOtherTenants $true -DisplayName $DisplayName -IdentifierUris "https://$($SessionInfo.TenantDomain)/$((New-Guid).ToString())" -RequiredResourceAccess $adAppAccess, $graphAppAccess, $partnerCenterAppAccess -ReplyUrls @("urn:ietf:wg:oauth:2.0:oob","","https://localhost","http://localhost","http://localhost:8400")
$password = New-AzureADApplicationPasswordCredential -ObjectId $app.ObjectId
$spn = New-AzureADServicePrincipal -AppId $app.AppId -DisplayName $DisplayName

    $adminAgentsGroup = Get-AzureADGroup -Filter "DisplayName eq 'AdminAgents'"
    Add-AzureADGroupMember -ObjectId $adminAgentsGroup.ObjectId -RefObjectId $spn.ObjectId

write-host "Installing PartnerCenter Module." -ForegroundColor Green
install-module PartnerCenter -Force
write-host "Sleeping for 30 seconds to allow app creation on O365" -foregroundcolor green
start-sleep 30
write-host "Please approve General consent form." -ForegroundColor Green
$PasswordToSecureString = $password.value | ConvertTo-SecureString -asPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential($($app.AppId),$PasswordToSecureString)
$token = New-PartnerAccessToken -ApplicationId "$($app.AppId)" -Scopes '' -ServicePrincipal -Credential $credential -Tenant $($spn.AppOwnerTenantID) -UseAuthorizationCode
write-host "Please approve Exchange consent form." -ForegroundColor Green
$Exchangetoken = New-PartnerAccessToken -ApplicationId 'a0c73c16-a7e3-4564-9a95-2bdf47383716' -Scopes '' -Tenant $($spn.AppOwnerTenantID) -UseDeviceAuthentication
write-host "Last initation required: Please browse to$($spn.AppOwnerTenantID)/adminConsent?client_id=$($app.AppId)"
write-host "Press any key after auth. An error report about incorrect URIs is expected!"
Write-Host "================ Secrets ================"
Write-Host "`$ApplicationId         = $($app.AppId)"
Write-Host "`$ApplicationSecret     = $($password.Value)"
Write-Host "`$TenantID              = $($spn.AppOwnerTenantID)"
write-host "`$RefreshToken          = $($token.refreshtoken)" -ForegroundColor Blue
write-host "`$Exchange RefreshToken = $($ExchangeToken.Refreshtoken)" -ForegroundColor Green
Write-Host "================ Secrets ================"

Update: Please note that you should NOT run this script in the PowerShell ISE as it will not work. Also note that when running the script with an MFA whitelist via the portal, the script fails. You must remove this whitelisting beforehand.

The following script creates a new application, and connects to all resources. At the end you will receive several private keys. Store these in a secure location for future usage such as a Azure Keyvault or IT-Glue. With these keys you can connect to all your delegated resources without MFA. There currently are some slight issues on the Azure side with performing consent, and as thus you’ll have to click a couple of times. Sorry about that πŸ™‚

So, now that you have these keys you can use the following scripts to connect to the correct resources:

$ApplicationId         = 'xxxx-xxxx-xxxx-xxxx-xxx'
$ApplicationSecret     = 'YOURSECRET' | Convertto-SecureString -AsPlainText -Force
$TenantID              = 'xxxxxx-xxxx-xxx-xxxx--xxx' 
$RefreshToken          = 'LongResourcetoken'
$ExchangeRefreshToken  = 'LongExchangeToken'
$credential = New-Object System.Management.Automation.PSCredential($ApplicationId, $ApplicationSecret)

$aadGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes '' -ServicePrincipal -Tenant $tenantID 
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes '' -ServicePrincipal -Tenant $tenantID 

Connect-MsolService -AdGraphAccessToken $aadGraphToken.AccessToken -MsGraphAccessToken $graphToken.AccessToken
AzureAD Module
$ApplicationId         = 'xxxx-xxxx-xxxx-xxxx-xxx' 
$ApplicationSecret     = 'YOURSECRET' | Convertto-SecureString -AsPlainText -Force
$TenantID              = 'xxxxxx-xxxx-xxx-xxxx--xxx'
$RefreshToken          = 'LongResourcetoken'
$ExchangeRefreshToken  = 'LongExchangeToken'
$credential = New-Object System.Management.Automation.PSCredential($ApplicationId, $ApplicationSecret)

$aadGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes '' -ServicePrincipal -Tenant $tenantID 
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes '' -ServicePrincipal -Tenant $tenantID 

Connect-AzureAD -AadAccessToken $aadGraphToken.AccessToken -AccountId 'VALIDEMAILADDRESS' -MsAccessToken $graphToken.AccessToken -TenantId $tenantID

Exchange Online

For the Exchange Online module we’ll need to do a little more effort – You will need the tenantid of the client you are connecting too. I’ll show you how to perform a specific action for each client you have delegated access too.

$ApplicationId         = 'xxxx-xxxx-xxx-xxxx-xxxx'
$ApplicationSecret     = 'TheSecretTheSecrey' | Convertto-SecureString -AsPlainText -Force
$TenantID              = 'YourTenantID'
$RefreshToken          = 'RefreshToken'
$ExchangeRefreshToken  = 'ExchangeRefreshToken'
$upn                   = 'UPN-Used-To-Generate-Tokens'
$credential = New-Object System.Management.Automation.PSCredential($ApplicationId, $ApplicationSecret)

$aadGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes '' -ServicePrincipal -Tenant $tenantID 
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes '' -ServicePrincipal -Tenant $tenantID 

Connect-MsolService -AdGraphAccessToken $aadGraphToken.AccessToken -MsGraphAccessToken $graphToken.AccessToken
$customers = Get-MsolPartnerContract -All
foreach($customer in $customers){
    $token = New-PartnerAccessToken -ApplicationId 'a0c73c16-a7e3-4564-9a95-2bdf47383716'-RefreshToken $ExchangeRefreshToken -Scopes '' -Tenant $customer.TenantId
    $tokenValue = ConvertTo-SecureString "Bearer $($token.AccessToken)" -AsPlainText -Force
    $credential = New-Object System.Management.Automation.PSCredential($upn, $tokenValue)
    $customerId = $customer.DefaultDomainName
    $session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "$($customerId)&BasicAuthToOAuthConversion=true" -Credential $credential -Authentication Basic -AllowRedirection
    Import-PSSession $session
#From here you can enter your own commands
#end of commands
Remove-PSSession $session

Refreshing the tokens

Use the following script to refresh the tokens

$ApplicationId = 'ApplicationID'
$ApplicationSecret = 'Secret' | Convertto-SecureString -AsPlainText -Force
$TenantID = 'YourTenantID'
$credential = New-Object System.Management.Automation.PSCredential($ApplicationId, $ApplicationSecret)
$token = New-PartnerAccessToken -ApplicationId $ApplicationID -Scopes '' -ServicePrincipal -Credential $credential -Tenant $TenantID -UseAuthorizationCode
$Exchangetoken = New-PartnerAccessToken -ApplicationId 'a0c73c16-a7e3-4564-9a95-2bdf47383716' -Scopes '' -Tenant $TenantID -UseDeviceAuthentication
Write-Host "================ Secrets ================"
Write-Host "`$ApplicationId         = $($applicationID)"
Write-Host "`$ApplicationSecret     = $($ApplicationSecret)"
Write-Host "`$TenantID              = $($tenantid)"
write-host "`$RefreshToken          = $($token.refreshtoken)" -ForegroundColor Blue
write-host "`$ExchangeRefreshToken  = $($ExchangeToken.Refreshtoken)" -ForegroundColor Green
Write-Host "================ Secrets ================"

And that’s it! I hope it helps MSPs, CSPs, etc. πŸ™‚ as always, Happy PowerShelling!


  1. Casper Lammertink November 15, 2019 at 12:05 am

    Kelvin, je bent weer top bezig! Powershell op hoog niveau!

  2. Ban Hang November 15, 2019 at 4:15 am

    Hey just wanted to give you a quick heads up.
    the first <# seems to be lost in encoding. I'm not sure if this is a formatting issue or something to do with browser compatibility but I thought I'd post to let you know. The script is fanstatic and look great though! thanks

  3. Heiner November 15, 2019 at 8:43 am

    I have been working on this since the forced MFA change and could not get it working. Thank you so much for posting this.

  4. ryan November 18, 2019 at 6:48 am

    Awesome job! Do you have a script for Teams/SFB?

    1. Kelvin Tegelaar November 18, 2019 at 8:05 am

      The Microsoft Teams module currently does not support delegated access yet. I’ve requested Microsoft to look into it and got the confirmation that it should in a future version, but we’ll have to wait for this… πŸ™‚

  5. Geert W November 19, 2019 at 3:18 pm

    Hi Kelvin! Sadly, nothing is happening for me after ‘Please approve Exchange consent form’ – there just isn’t anything to approve, no pop-ups. Any idea what this can be?

    1. Kelvin Tegelaar November 19, 2019 at 3:24 pm

      You should be getting the output

      “To sign in, use a web browser to open the page xxxxx and enter the code xxxxx”

      Are you running the script from ISE or VSCode? I’ve had someone else report the same issue today, So I am suspecting this has something to do with the IDE used. I’ve just tested the script in my VSCode instance and it seems to run fine.

    2. Kelvin Tegelaar November 19, 2019 at 3:34 pm

      Just checked this for you; When running in ISE the script indeed does not finish running. Please save the script somewhere and run it from a non-ise PowerShell window by entering “.\scriptname.ps1”. Hope it helps!

  6. Geert W November 19, 2019 at 3:37 pm

    Hi Kelvin,

    Thank you – you are right. I was using the ISE and this was causing the problem.
    However, when using the normal PowerShell windows it won’t output a $RefreshToken or $Exchange RefreshToken for me (just empty).

    I will troubleshoot and let you know :).

    Thank you!

    1. Kelvin Tegelaar November 19, 2019 at 3:39 pm

      The only tip I can give in this is rerunning the script; the Partnercenter sometimes just does not return any token. I believe this is due to the API getting a time-out but the PartnerCenter module does not report this.

      Good luck, and if you need any more help let me know.

  7. Pingback: Documenting with PowerShell: Documenting Office365 mailbox permissions - CyberDrain

  8. Pingback: Monitoring with PowerShell: Alerting on large Office 365 mailboxes - CyberDrain

  9. Saul Ansbacher December 12, 2019 at 11:25 pm

    Hey, awesome post, very helpful! I was able to connect to the Exchange Online, which I couldn’t before – this worked. But I was having trouble with the AzureAD: connecting to my CUSTOMER’s AzureAD. I managed to work it out. For the benefit for anyone else:

    Connecting to our CUSTOMERS with Msol is fairly straight forward, use the above Connect-MsolService command to connect to YOUR (eg. CSP) Msol service, then Get-MsolPartnerContract will list all YOUR Customers you have DELEGATED permissions to. You can then use a foreach ($customer in $Customers) {} loop.
    Inside the foreach you can then use any Msol command AND specify their TenantID, eg: Get-MsolUser -TenantId $Customer.TenantId will show the CUSTOMER’S users, not yours. You need to add -TenantId to all Msol commands since you are connected to your Msol service endpoint (but you have delegated permissions to your Customers)

    For AzureAD it was slightly more complicated: I could get MY internal (eg. CSP) Users, but not my CUSTOMER’s Users that I had Delegated permissions to, I would get an error like:
    Get-AzureADUser : Error occurred while executing GetUsers
    Code: Request_BadRequest
    Message: Invalid domain name in the request url.

    The solution for Azure AD is:
    Connect to AzureAD as listed above, this will connect to YOUR (eg. CSP) Azure AD
    Get all your Customers that you have Delegated Admin rights to with: Get-AzureADContract
    Then Disconnect-AzureAd (will disconnect from your AzureAD)
    You will need (just like the Exch Online code): $upn = ‘UPN-Used-To-Generate-Tokens’
    Then in your foreach ($customer in $Customers) {} loop you must obtain NEW AAD and Graph Tokens for the Customer’s TenantId, such as:
    $CustAadGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes ‘’ -ServicePrincipal -Tenant $customer.CustomerContextId
    $CustGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes ‘’ -ServicePrincipal -Tenant $customer.CustomerContextId
    Finally connect to the CUSTOMER’S AzureAD (to which you have Delegated rights):
    Connect-AzureAD -AadAccessToken $CustAadGraphToken.AccessToken -AccountId $upn -MsAccessToken $CustGraphToken.AccessToken -TenantId $customer.CustomerContextId
    [Note that $customer.CustomerContextId is the CUSTOMER’s TenantID, which is obtained from Get-AzureAdContract (when connected to YOUR Azure AD, the first time)]
    You can now run commands like Get-AzureAdUser or Get-AzureAdDomain and it will show the CUSTOMER’s users/info.
    Disconnect-AzureAd at the end of the loop (from the Customer’s AzureAD)

    Thanks again!

    1. John Perez July 7, 2020 at 9:17 pm

      Really late comment but you have just helped me more than you know with this comment. I’ve been trying to figure this out for SO long now. Thank you!

  10. Vitus Quinny December 18, 2019 at 10:43 pm

    Hi Kelvin,

    I’ve copied & saved everything exactly how it is mentioned in “The Script” to “SecureApp.ps1”
    However, when i execute it through Powershell and enter the Credentials after the prompt, it throws me the following:

    When prompted please enter the appropriate credentials… Warning: Window might have pop-under in VSCode
    Creating the Azure AD application and related resources…
    C:\Temp\SecureApp.ps1 : Cannot bind argument to parameter ‘ObjectId’ because it is null.
    At line:1 char:1
    + .\SecureApp.ps1
    + ~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidData: (:) [SecureApp.ps1], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,SecureApp.ps1

    1. Kelvin Tegelaar December 19, 2019 at 9:50 am

      Try running the script form VSCode, I’ve seen it do weird things from ISE. If you are already running it from VSCode or a normal PowerShell window let me know, so I can research it.

      1. Austin May 14, 2020 at 4:10 pm

        I’m also getting this issue when running from an elevated powershell window. Null argument ObjectId

        1. Kelvin Tegelaar May 14, 2020 at 4:26 pm

          Are you a Microsoft Partner and do you have access to the partner center? This script is only for partners currently.

  11. Ariel December 27, 2019 at 7:56 pm

    Hi Kelvin,
    Nice script! But I had a question about the PS Session for exchange. I believe that Basic authentication will be deprecated as of October 31st 2020. So can the SPN/App Account be adapted to work with the ExchangeOnlineManagement PS Module? How would one apply the credentials created by your script to the CMDLET Connect-ExchangeOnline?
    read more here:

    These articles started me down this path that eventually led me to your site.
    Thanks for your work on this script. This was very useful for automation scripts and such!

    1. Kelvin Tegelaar December 27, 2019 at 8:07 pm

      Hi Ariel,

      Microsoft has announced the new Exchange Online module will be made compatible with the Secure App Model, they just didn’t say when which is a shame.

      I’ve tested this method by disabling all forms of basic authentication too, and it still works due to it using the oauth conversion, so with a bit of luck you won’t need any changes after october 2020.

      Hope that helps!

  12. Paul December 31, 2019 at 12:45 am

    Awesome work, thanks so much for your hard work putting this together. Successfully connecting to Exchange Online which is awesome. Also trying to connect to the Compliance Centre and not having the same luck. Used the same format as connecting to Exch session but get an access denied (where the exchange one connects fine). Any ideas how to connect to this endpoint (or maybe doesnt support same method?)?

    $SccSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri “$($customerId)&BasicAuthToOAuthConversion=true” -Credential $credential -Authentication Basic -AllowRedirection

    Import-PSSession $SccSession -Prefix cc

    1. Kelvin Tegelaar December 31, 2019 at 12:53 am

      Glad to help! I haven’t checked the security center yet – I expect we’ll need to give an extra consent for the Security Center application. I’ll try to blog about this next year.

      1. Paul January 2, 2020 at 9:17 pm

        Awesome, cheers Kelvin. I’ve spent this morning trying to get my head around these ‘ResourceAccess’ GUIDs and what they are all for and mean. Out of interest, which one in your registration of the app is allowing access to Exchange Online? Using Get-AzureADServicePrincipal -All $true, i can see a list of the apps and figured which ones you are granting access to.. I see there’s one called Office 365 Exchange Online with AppId 00000002-0000-0ff1-ce00-000000000000 but see your script doesnt reference that one. the other one we connect to a lot is SharePoint API

        1. Kelvin Tegelaar January 2, 2020 at 9:21 pm

          No problem! You don’t give access to the Exchange Online portion in your own tenant, but do this on the centrally managed application by Microsoft. That’s what the line “$Exchangetoken = New-PartnerAccessToken -ApplicationId ‘a0c73c16-a7e3-4564-9a95-2bdf47383716’ -Scopes ‘’ -Tenant $($spn.AppOwnerTenantID) -UseDeviceAuthentication” is for. This generates consent for the Exchange Online application hosted by microsoft to access you tenant(s).

          Hope that helps.

          1. Paul January 2, 2020 at 11:10 pm

            Ahh OK. in the Registration script, i added a new line against the compliance scope and it seemed to return a token (different from the exchange token) $ComplianceToken = New-PartnerAccessToken -ApplicationId ‘a0c73c16-a7e3-4564-9a95-2bdf47383716’ -Scopes ‘’ -Tenant $($spn.AppOwnerTenantID) -UseDeviceAuthentication

            When i then go to auth with it (like your Exchange example) i still get Access Denied.. is that where i need to possibly grant additional consent like your first reply? Appreciate your help.. there doesn’t seem to be much clear end to end documentation on this.

  13. James January 2, 2020 at 1:55 pm

    Hi Kelvin,

    I get the same error mentioned above using PowerShell, ISE and VSCode. I’ve named the script SecureApp.ps1.

    C:\users\XXXX\SecureApp.ps1 : Cannot bind argument to parameter
    ‘ObjectId’ because it is null.
    At line:1 char:1
    + .\SecureApp.ps1
    + ~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidData: (:) [SecureApp.ps1], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,SecureApp.ps1

    Any ideas?

    Thanks in advance!

    1. Kelvin Tegelaar January 2, 2020 at 2:01 pm

      I can’t seem to simulate this error in my environment, are you running the script as both a global admin, and partner admin?

      1. James January 2, 2020 at 2:05 pm

        I’m running the script as a global admin.

        1. James January 6, 2020 at 10:10 am

          Hi Kelvin,

          I appreciate you’re probably busy but just wondering if you have any other ideas on this?



          1. Adam Brown January 6, 2020 at 12:37 pm

            IHi Kevin,

            Im getting the same issue as James.

            Any thoughts?


          2. Kelvin Tegelaar January 6, 2020 at 12:45 pm

            Hi Adam, Its Kelvin with an L, not Kevin πŸ™‚

            I’ve been trying to simulate the error but can’t seem to do it on my environment. I’m thinking it has to do either with the Azure AD setup, or a permissions blocker somewhere.

            Can you & James try the following:

            This should return an objectID, if it does not, then I’m thinking it cannot retrieve your tenantID and you should try running the script as “.\SecureAppModel.ps1 -TenantID “. You can find your tenant ID in the Azure AD portal.

            Let me know if this works!

  14. James January 6, 2020 at 2:33 pm

    Hi Kelvin,

    I can’t seem to reply to your last comment but I get the same error I’m afraid. That’s when running β€œ.\SecureAppModel.ps1 -TenantID XXXXβ€œ

    C:\XXXX\SecureApp.ps1 : Cannot bind argument to parameter
    ‘ObjectId’ because it is null.
    At line:1 char:1
    + .\SecureApp.ps1 -TenantId XXXX
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidData: (:) [SecureApp.ps1], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,SecureApp.ps1

    I’m able to find the tenant ID by running $(Get-AzureADTenantDetail).ObjectId

    1. Kelvin Tegelaar January 7, 2020 at 6:51 pm

      Hi James,

      Still researching this, I’ll get back to you on this soon.

      1. James January 7, 2020 at 9:39 pm


        1. Kelvin Tegelaar January 8, 2020 at 7:31 am

          Hi James,

          After a lot of testing I think I’ve finally figured it out – The script tries to grab the Microsoft Partnership role and grant access to a user group based on this. The only reason for the error to appear that I’ve seen is that you do not have a Microsoft Partnership(CSP partner/Cloud advisory model/others) or that you do not have complete partnership access.

          To use these scripts you must have some form of Partner Administration enabled. you can check this by going to the URL and checking if your clients are listed there.


          1. James January 8, 2020 at 10:10 am

            Hi Kelvin,

            Thanks for this.

            I do have MS Partnership but was trying to run this on a few of the tenancies individually. Is there a way I can do this?



          2. Kelvin Tegelaar January 10, 2020 at 8:43 am

            Hi James! I’ve checked; If you uncomment the part about the AdminRole in the script, it might work. if not then I’m afraid I can’t support that configuration. I understand only wanting a subset of clients first for testing, but limiting that is not something I’ve scripted for.

  15. Pingback: Documenting with PowerShell: Downloading and storing the Office 365 Audit logs (With search!) - CyberDrain

  16. Nick Tonkin February 4, 2020 at 9:37 pm

    Hi Kelvin, I am wondering whether you know whether this solution will work in Powershell Core running on *nix.

    We have no Windows machines in our environment, and if Windows is required for this, wouldn’t it also be possible to run the EXOv2 module which supposedly handles MFA? (I can’t run that in PWSH on Linux…)



    1. Kelvin Tegelaar February 4, 2020 at 9:46 pm

      Wow that is a brilliant question really. I’m pretty sure PartnerCenter module will not work for Core, but I don’t see why the rest should not. This means you’ll only need a windows machine to generate your tokens.

      The Exov2 module does not support Secure App Model. Microsoft has stated you’ll need to use the “old method” as described above for that.

      1. Nick Tonkin March 4, 2020 at 11:29 pm

        Hi again, I just wanted to follow up.

        I was able to get a solution working for this, and it does involve using the Partner Center module on PowerShell core. The latest versions support this.

        A couple of gotchas we encountered for anyone else who is trying to connect to Exchange Online for a customer with MFA enabled in a fully automated flow, on Unix:

        1) The initial refresh token must be obtained using Powershell, because the -Module param is not known to the Oauth flow and thus a REST API call cannot produce a valid token.

        2) The application_id param passed to the New-PartnerAccessToken command *must* be ‘a0c73c16-a7e3-4564-9a95-2bdf47383716’ — as shown above. (We thought this was a stray uuid left in the example script.) This is the app ID not for your own app but for Exchange Online itself within MSFT.

  17. Michael Joseph February 6, 2020 at 2:00 pm

    Thank you for these scripts. As an MSP I plan to incorporate them into some Azure runbooks to automate reporting for our customers and keep all the scripting in one place. I am able to run the Exchange online script against all my customers and get data back except for one customer. I get New-PSSession : [] Connecting to remote server failed with the following error message : Access is denied. What is crazy is that they show in my partner portal and I can administer all services from the GUI, the only thing I cannot do is powershell with delegated access. I contacted Microsoft support but all they could offer was that my access to our customer looked fine and correct and the issue must be a PowerShell problem? Since no other customer of mine has this issue I’m stumped and reaching out to different forums for opinions.

    Thanks again –

    M. Joseph

      1. Michael Joseph February 9, 2020 at 9:54 pm

        Thanks for the reply. I ended up figuring out that the issue was I had an old guest account in my customer’s Azure AD that had the same UPN as the account I used for the script. As soon as I deleted that guest account everything worked!

  18. Michael Joseph February 9, 2020 at 10:07 pm

    For anyone wishing to connect to Exchange Online for a particular customer and not query against all your customers, you can use the code below and just change the customer domain name in the CustomerID variable:

    $ApplicationId = ‘xxxx-xxxx-xxx-xxxx-xxxx’
    $ApplicationSecret = ‘TheSecretTheSecrey’ | Convertto-SecureString -AsPlainText -Force
    $TenantID = ‘YourTenantID’
    $RefreshToken = ‘RefreshToken’
    $ExchangeRefreshToken = ‘ExchangeRefreshToken’
    $upn = ‘UPN-Used-To-Generate-Tokens’
    $credential = New-Object System.Management.Automation.PSCredential($ApplicationId, $ApplicationSecret)

    $aadGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes ‘’ -ServicePrincipal -Tenant $tenantID
    $graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes ‘’ -ServicePrincipal -Tenant $tenantID

    Connect-MsolService -AdGraphAccessToken $aadGraphToken.AccessToken -MsGraphAccessToken $graphToken.AccessToken

    $CustomerID = (Get-MsolPartnerContract -domain

    $token = New-PartnerAccessToken -ApplicationId ‘a0c73c16-a7e3-4564-9a95-2bdf47383716’-RefreshToken $ExchangeRefreshToken -Scopes ‘’ -Tenant $customerID
    $tokenValue = ConvertTo-SecureString “Bearer $($token.AccessToken)” -AsPlainText -Force
    $credential = New-Object System.Management.Automation.PSCredential($upn, $tokenValue)
    $session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri “$($CustomerId)&BasicAuthToOAuthConversion=true” -Credential $credential -Authentication Basic -AllowRedirection -Debug
    Import-PSSession $session

    1. Kelvin Tegelaar February 10, 2020 at 8:34 am

      Currently this module is not supported it seems. I don’t see any options for it at least.

  19. Joerg February 20, 2020 at 10:26 am

    Our Company is not a CSP and I don’t have access to the Partner Center.
    I want to connect our own Exchange Online Instance and run automated scripts.
    Is there a way to do that?

    1. Kelvin Tegelaar February 20, 2020 at 11:28 am

      Good question! I guess you could try to create an AzureAD application with access, I’ll see if I can figure this out for none-CSPs and blog about it. πŸ™‚

      1. Jeremy Bradshaw March 2, 2020 at 2:59 pm

        Will be trying to remember to check back for this! It seems like the only attention on the internet around unattended PowerShell for Microsoft’s cloud services is for Partners / CSP. Nothing yet for the actual paying customers themselves, who have until October 13th 2020 to keep using Basic Authentication.

  20. Jeremy Bradshaw March 2, 2020 at 1:49 pm

    Hopefully this is an easy question and I feel a little exposed even asking, but oh well…

    How does this stuff apply to a regular customer, as in, not a Partner / not delegated admin, etc? If a regular EXO customer wants to do unattended PowerShell with modern authentication, do they have to get and use the Partner Center module for this functionality?

    Thanks in advance.

    1. Kelvin Tegelaar March 2, 2020 at 2:05 pm

      Actually this is a pretty good question. I honestly don’t know. You could use Conditional Access and exclude accounts, but that feels a little unsafe still. You cannot use the secure application model as a client because you don’t have partner API access. I haven’t spend time yet to research this.

      1. Jeremy Bradshaw March 2, 2020 at 3:01 pm

        Well that was fast! Thanks for the [speedy] reply, and it makes me feel better to know that it’s not just me, there’s a real void in this area for non-partners. I have also asked MS Support so will try to remember to share the response here.

        1. Nick Gauthier March 4, 2020 at 4:30 pm

          I am also trying to find a way to do this. While we are a CSP and have some clients that this works for, we have 1 particular client that is not linked to us as a CSP (long story), has MFA enabled on all accounts, and requires some automation from us. I am struggling to find a way to do this securely, without just using whitelisting. This seems to be a major shortfall in Microsoft’s current model.

  21. KB March 2, 2020 at 9:28 pm

    H Kelvin, excellent blog.

    Do you know if this can be used to automate SfB related tasks using the SkypeOnlineConnector module? We have some business requirements to do so and currently hitting a brick wall with MFA being enforced on customer tenants.

  22. Pingback: Documenting with PowerShell: Documenting Office 365 usage reports - CyberDrain

  23. Gagan March 17, 2020 at 3:09 pm

    I am getting this error while running the script. any idea.

    Exception calling “ReadKey” with “1” argument(s): “Cannot read keys when either application
    does not have a console or when console input has been redirected from a file. Try
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : InvalidOperationException

    1. Kelvin Tegelaar March 17, 2020 at 3:36 pm

      That’s a line to just prevent you from entering too fast. You can ignore that error/remove the line with “readkey”. That’ll solve the issue. Just pay attention to approve all consent popups πŸ™‚

    2. Gagan March 17, 2020 at 3:45 pm

      Please ignore i am able run the script

  24. Ryan March 20, 2020 at 5:26 am

    Hi Kelvin,

    Thanks for all you hard work on this, will make our lives so much easier.

    Have you or anyone else been able to get the Compliance Centre to work yet?
    Any way to adapt the Connect-IPPssession command?

    1. Kelvin Tegelaar March 20, 2020 at 7:48 am

      Actually having been trying to hack that together yesterday. The problem is that I’m receiving an access denied both when running Connect-IPPSsession and using the secure app model, so I’ll have to investigate that first.

      1. Ryan March 22, 2020 at 1:07 am

        I feel like Pauls comment above has the right idea, to get the refresh token from the compliance centre, but I am also still getting ‘access denied’ every way I try it.
        Hopefully they’ll put some actual doco out on this soon.

        1. KB March 3, 2021 at 5:58 pm

          Hi the script is not working of us. Is there a way to get the refresh token and exchange refresh token by logging on the Microsoft Azure online as we only have office 365 through Microsoft.

          1. Kelvin Tegelaar March 4, 2021 at 11:42 am

            No, but the script might not be working because you’re not a Microsoft partner. Like it says in the blog you need a CSP partner account.

  25. Pingback: Monitoring with PowerShell: Monitoring Onedrive and Sharepoint file limits - CyberDrain

  26. David March 24, 2020 at 11:10 pm

    Now that the Exchange Online PowerShell V2 module has been released, are there any plans to update this for the new module?

    1. Kelvin Tegelaar March 24, 2020 at 11:11 pm

      Microsoft has announced that the V2 Module is not going to get SecureApp Model support, and that partners will have to use the method described above.

  27. BG March 27, 2020 at 5:24 am

    This works awesome, using EXOL module, except one client I get the error:

    New-PartnerAccessToken : AADSTS500014: Resource ‘’ is disabled.

    I saw your comment about making sure Remote PowerShell is enabled, but I am not sure how that should be done when using delegated access, since the setting is specific to user. I made sure there was no guest account in the tenant matching my admin UPN.

    1. Kelvin Tegelaar March 27, 2020 at 7:58 am

      Sometimes you also get this for any MSOL client that does not have any form of exchange licenses, does this tenant have them? πŸ™‚

      1. BG March 27, 2020 at 3:03 pm

        They do, yes. The message I get for client w/o Exchange Licencing is:

        Get-Mailbox : The term ‘Get-Mailbox’ is not recognized as the name of a cmdlet,

        Which is to be expected. Not sure what is up with this one client. I am going to compare some org configurations and see if there is something globally set.

      2. BG April 2, 2020 at 2:43 am

        This is definitely an issue with my UPN. There was no guest account, no deleted guest account wiht the same UPN, no contact either. I can connect with MSOL but not EOL. Just won’t work with my UPN. I swapped it out for a different UPN in my tenant, didn’t regenerate any tokens or anything, and it worked. Does that UPN provided just need to be present in my tenant to work?

        1. Kelvin Tegelaar April 2, 2020 at 8:53 am

          Yes, the UPN needs to be present AFAIK, and have partner permissions.

  28. Peter March 30, 2020 at 5:34 am

    What do I do when I want to do this.
    We look after our own tenancy, are not a partner, but have a partner for sales only.

    We just enabled MFA and I have to modify all my scripts that I was running.

    1. Kelvin Tegelaar March 30, 2020 at 9:01 am

      I’m still not sure about that, as this requires the partner APIs. My best guess would be creating a CA policy using an Azure P1 or P2 subscription.

  29. Luigi Teti March 31, 2020 at 9:22 pm

    Hello Kelvin, i’m a bit newbbie in the use of Powershell, it seems your script doing much more than i need,i spent the last 3 days at the phone with MS Support cause i cannot login via Powershell with MFA.
    Before MFA i was able to connect, now i get “Access Denied” error (user is global admin, as before MFA).
    Do you have some tips for help me?

    Thanks in advance

  30. Jon April 1, 2020 at 9:15 pm

    ‘K Maybe I’m trying to overthink it… In the Exchange example above… the line
    $upn = ‘UPN-Used-To-Generate-Tokens’

    DO you use that word for word, or where example am i supposed to *get* the UPN used to generate tokens?

    1. Kelvin Tegelaar April 1, 2020 at 9:31 pm

      When you’ve generated the tokens using the first script, you’ve logged on with a ‘normal’ account. The UPN you need to fill in is the account you used there πŸ™‚

  31. Pingback: Automating with PowerShell: Automating intune Autopilot configuration - CyberDrain

  32. Andreas April 16, 2020 at 1:10 pm

    Hi Kelvin,

    Great script that really helps. Big thanks for putting in the time and effort, but most of all that you follow Arnold Schwarzneggers “Give back” :).

    I gotta be honest i’m still learning alot in Powershell and I have read this script line by line but I still have some worries and questions.
    The script creates a new Azure Ad application in the Azure tenant. Now i’m guessing that this application is created just for the reason of being able to create the private keys and to connect to all the Microsoft cloud services.
    Will this application that is created through this script remain in the Azure tenant afterwards? If yes, how can it affect the security of the environment?
    I worry that i would create new apps or objects in our environment that can be a security risk or that can cause other conflicts in the environment.


    1. Kelvin Tegelaar April 16, 2020 at 5:12 pm

      Hi Andreas,

      The application created actually is the “credentials object” if you will. The application has permissions on your clients locations. Deleting the application means the access is revoked. This is why its so important to store the keys in a secure location such as an Azure Key Vault or IT-Glue documentation system.

  33. Pingback: Automating with PowerShell: Automatically uploading applications to intune tenants - CyberDrain

  34. phaedrusschmaedrus April 20, 2020 at 6:07 pm


    Let me start by saying that I really appreciate what you’re doing with this blog; as someone just getting started using Powershell and with my IT career in general your posts have been a huge help in discovering what’s possible with Powershell and starting me on the path to figuring out things on my own.

    That being said, I have a couple questions about this implementation:

    1) I have this currently working with my Partner-enabled account, but I’d like to set up a separate account to use for running scripts unattended against our tenants. To do that, do I need to run the script again using that account’s credentials. or do I just need to add that account to the already created application in Azure and assign it the Default Access role?

    2) We’ve recently implemented Duo MFA for all of our Office 365 accounts, and while the command-line scripts still /work/ it does force me to authenticate via Duo before continuing. Do you have any guidance on how to integrate the existing application with a third-party MFA provider, or would you just recommend that we create a separate, Microsoft MFA enabled service account for use with this access method?

    3) Do you have any general advice on what resources I should look at to get a better understanding of the principals at work behind this script? I’d like to better understand what’s going on here but I have only a vague idea of where to start.

    1. Kelvin Tegelaar April 20, 2020 at 7:05 pm

      Hi phaedrusschmaedrus,

      Thanks! comments like yours are one of the reasons I do this, so seriously thank you, just one note: My name is Kelvin with an L πŸ˜‰

      To get all your questions:

      1.) You do not have to rerun the script, now that the application is registered, you can use any legal UPN inside your partner tenant as long as it has partner permissions. For prettieness sake I do tend to create new applications.

      2.) Your only option here is using Conditional Access I think. I don’t work with duo normally speaking so it would be best to test. πŸ™‚

      3.) So there’s a couple of good blogs that go in-depth on how the creation of applications work. The problem with most of them is that they go on a very deep, almost developer level. The best one for PowerShell novices is by Bradley Wyatt here:, if you want more details on the permissions, you can check out and

      Hope that helps! πŸ™‚

      1. phaedrusschmaedrus April 20, 2020 at 7:38 pm

        Ah! Sorry about that Kelvin, I misread. I appreciate your answering, especially so promptly, and I’ll definitely look at those links.

  35. Pingback: Documenting with PowerShell: Documenting intune applications - CyberDrain

  36. Pingback: Documenting with PowerShell: Using PowerShell to create faster partner portal - CyberDrain

  37. Pingback: Monitoring with PowerShell: Monitoring the used MFA type for O365/Azure. - CyberDrain

  38. Muhammad Usman May 14, 2020 at 7:13 am

    Hi Kelvin,

    Thanks for your efforts and making it public.

    My company is automating O365 user creations and management, for which we use different PS modules and commands (MSOnline, SharePoint, SkypeForBusiness and Exchange). We rely on Tenant Admin UserName and Password to connect all these modules.

    We have big challenge to get this automation running for MFA case, as the processing is performed automatically without user presence or input.

    I have tried your script, but $adminAgentsGroup = Get-AzureADGroup -Filter “DisplayName eq ‘AdminAgents'”, does not return any group.

    Again do note that it is a normal tenant, not a CSP reseller tenant.

    What is wrong or their needs to be some more steps?


    Muhammad Usman

    1. Kelvin Tegelaar May 14, 2020 at 8:50 am

      Hi Muhammed,

      The method of just using a username and password is a considered bad practice. If you’re not the delegated administrator/partner for the client,your company should look into using an Azure Application to access these resources.

      1. Muhammad Usman May 17, 2020 at 7:15 am

        Hi Kelvin,

        Thanks for answering. We have not seen any documentation where we can use Azure Application to perform operations we perform using PowerShell, only few operations are possible using Graph API.

        Does a delegated administrator/password has full admin access on all PowerShells of all modules like MSOnline, Azure, SharePoint, SkypeForBusiness etc?

        Can you share an example how to connect SkypeForBusiness module using above method as well?


        Warm Regards,

        Muhammad Usman.

  39. BG May 21, 2020 at 9:41 am

    Does anything need to be done for new tenants added into the Partner Portal after the initial Secure App is created? I put this project aside a few weeks ago and getting back to it now, I get this on a tenant that was added since I set everything up:

    New-PartnerAccessToken : AADSTS500014: The service principal for resource ‘’ is disabled. This indicate that a subscription within the tenant has lapsed, or that the administrator for this tenant has disabled the
    application, preventing tokens from being issued for it.

    There is a single Microsoft 365 Business Standard license applied to one user.

    1. Kelvin Tegelaar May 21, 2020 at 10:05 am

      If you haven’t given all 3 consents, you’ll need to recreate the application.

      1. BG May 22, 2020 at 7:11 pm

        Yes I had done that, and it was working for all of the tenants in my partner portal. Are you saying the application needs to be recreated each time a new tenant is brought into the partner portal?

  40. Pingback: Documenting and monitoring blogs updates - CyberDrain

  41. Pingback: Automating with PowerShell: Using the Secure Application model updates. - CyberDrain

  42. Pingback: Automating with PowerShell: Storing Office 365 audit logs longer than 90 days - CyberDrain

  43. Paul Obrien June 12, 2020 at 4:02 am

    Is there any example of not using partner center? trying to just create an ‘auth’ app without partnercenter and use this to connet-msolservice.. i see you can connect to exchange online with a well known app-id and scope.. can this be done to just get the required to do an msol connection?

    1. Kelvin Tegelaar June 12, 2020 at 9:00 am

      You could create the application in each tenant and try to retrieve the keys using the oauth method, but that’s not something I can help with.

  44. Alex Pawlak June 13, 2020 at 12:08 am

    Hello Kelvin!

    Thank you for this great shortcut and work Microsoft should have done in the first place! I was able to follow through the script and understood what and why it’s doing this thing! Your post is awesome and I’ll sure read through your website.

    With kind regards,

  45. Rob Kooiman June 16, 2020 at 1:54 pm

    Hi Kelvin,

    Nice work!

    Exchange commands to customer tenants does work great for commands like get-mailbox, but it does not work for Get-MailboxRegionalConfiguration, then I got the error “The specified mailbox … doesn’t exist”.

    This one does work:

    Import-PSSession $session
    #From here you can enter your own commands
    #end of commands
    Remove-PSSession $session

    This one does not work:

    Import-PSSession $session
    #From here you can enter your own commands
    get-mailbox | Get-MailboxRegionalConfiguration
    #end of commands
    Remove-PSSession $session

    Have you used this command Get-MailboxRegionalConfiguration for customer tenants from Partner account?

    Regards, Rob

    1. Kelvin Tegelaar June 16, 2020 at 2:41 pm

      Hi Rob,

      I haven’t tried that to be honest, but I believe there is also an MSOL regional settings cmdlet. Maybe that helps?!

  46. Alex June 17, 2020 at 1:47 am

    Hey Kelvin!

    I could get MSOL commands to run just fine, however I think I misconfigured something for Exchange Online and I’m out of ideas. Every time I request Exchange token for any of my customers, I get rejected a PS session with error message “Access denied”

    I’m not sure what I could have missed. I’ve extracted the Bearer token and parsed it – after small redaction:

    Which step could be the blocker here?
    Thanks in advance!

    1. Kelvin Tegelaar June 18, 2020 at 3:31 pm

      Have you applied all three consents? It’s important to follow the instructions to the letter. πŸ™‚

  47. Samarah July 4, 2020 at 6:47 pm

    Hi Kelvin,
    Thank you for sharing this.

    I read Microsoft has released preview module for Exchange Online PowerShell V2 to connect Exchange Online unattended when MFA enabled. This feature is available in all environments.


    If you could provide the automated script for connecting exchange online using the new module then it would be helpful for people with non-partner tenant like me.

    1. Kelvin Tegelaar July 5, 2020 at 2:35 pm

      Hi Samarah,

      I won’t be using the CBA method myself, so won’t blog about it. I do know that the team is hard at work to allow SPN authentication.

  48. Stewart McAdoo July 6, 2020 at 7:25 pm

    Hello, Kelvin

    I am having an issue at the last validation. It redirects to localhost:8400 which I can see in the script but this clearly times out. I can still print the required keys however. Is this expected or am I missing something.

    1. Kelvin Tegelaar July 6, 2020 at 7:26 pm

      That’s expected behaviour, no worries. If you have all keys and gave all consents, then you are done πŸ™‚

  49. Pingback: Automating with PowerShell: Teams Automapping - CyberDrain

  50. Rob Kooiman July 9, 2020 at 11:55 am

    Hi all,

    I can confirm that “Get-MailboxRegionalConfiguration” via Secure App model does work now with newer module “ExchangeOnlineManagement” (version 2.0.3-Preview).

    Regards, Rob

  51. Andrew Cullen July 15, 2020 at 1:17 am

    Awesome work Kelvin, this saved me so much time – i’ve been looking for something like this for a while. I managed to adapt your script to work with connect-azaccount and the Az module and manage customer Azure server environments.
    Just added these two lines into the Setup script:

    write-host “Please approve Azure consent form.” -ForegroundColor Green
    $Azuretoken = New-PartnerAccessToken -ApplicationId “$($app.AppId)” -Scopes ‘’ -ServicePrincipal -Credential $credential -Tenant $($spn.AppOwnerTenantID) -UseAuthorizationCode

    and these two lines into the connect /main script:
    $azureToken = New-PartnerAccessToken -ApplicationId $ApplicationID -Credential $credential -RefreshToken $refreshToken -Scopes ‘’ -ServicePrincipal -Tenant $TenantId
    Connect-Azaccount -AccessToken $azureToken.AccessToken -GraphAccessToken $graphToken.AccessToken -AccountId $upn -TenantId $tenantID

    This allows me to then connect to customer azure subscriptions by specifing the tenant ID – something i couldn’t do at all even when authenticating manually!!

    I used it to make a script that loops through customer Azure subscriptions and sets up alerts for CPU credits for B series VMs – works brilliantly!
    Thanks so much for sharing!


  52. Pingback: Documenting with PowerShell: Documenting the O365 portal - CyberDrain

  53. Pingback: Monitoring with PowerShell: O365 location alerts - CyberDrain

  54. Michael McCool July 28, 2020 at 1:04 am

    I’m receiving the following with the Exchange Online script. I’m guessing that this is due to Microsoft’s recent changes with MFA access. Any thoughts on how to resolve the issue?

    Account Environment TenantId TenantDomain AccountType
    ——- ———– ——– ———— ———–
    New-PartnerAccessToken : AADSTS50076: Due to a configuration change made by your administrator, or because you moved to a new location, you must use multi-factor authentication to access

    1. Kelvin Tegelaar July 28, 2020 at 2:07 pm

      Hey Michael! πŸ™‚

      I’ve noticed this myself during deployment of conditional access a while ago. We changed the CA policies and this suddenly invalidated the tokens.

      Microsoft told us to turn of the setting “Allow users to remember this device” in the MFA service settings temporarily, created new tokens, and re enabled it and this caused it to work again.

      Hope that helps!

      1. Daniel Parker April 5, 2021 at 4:19 pm

        Just to confirm would you have to create new tokens from scratch? Or just update the Exchange refresh token?

        I have been getting the experience where I turn off “Allow users to remember this device” generating a new refresh token(which succeeds), re-enabling the MFA requirement , then using the new refresh token to generate an access token (fails with the same MFA error).

      2. JC Reyes November 16, 2021 at 10:50 pm

        Hey Kelvin,

        I just wanted to check in if this is still accurate? We don’t have Conditional Access enabled but I tried the fix you recommended. I’m getting the same error message when I run the Tenant Access Check in the CIPP app.
        Failed to connect to Response status code does not indicate success: 400 (Bad Request).
        TENANT DOMAIN: Failed to connect to Exchange: AADSTS50076: Due to a configuration change made by your administrator, or because you moved to a new location, you must use multi-factor authentication to access

  55. Pingback: Monitoring with PowerShell: Monitoring B-Series VM credits - CyberDrain

  56. Pingback: Documenting with PowerShell: Documenting Office 365 guest access - CyberDrain

  57. Pingback: Documenting with PowerShell: Office 365 Secure Score PowerShell module - CyberDrain

  58. David Hodge September 9, 2020 at 1:20 am

    Hi Kevin,

    Firstly thanks so much for doing the huge amount of research to get this working!

    I am a CSP in New Zealand and when I try to connect to each tenant exchange instance I get an access denied return code. I am pretty sure that I have completed the triple consent process (though it ends with an odd error, as you warn). Just wondering if you might have any guidance where I could look to see how I can resolve the error. I am wondering if being in a different geographical location might mean some important detail like application id’s might be different down here?


  59. Pingback: Documenting with PowerShell: O365 Groups (And Warranty updates) - CyberDrain

  60. Pingback: Monitoring with PowerShell: Monitoring O365 unused products - CyberDrain

  61. Bas Maree October 8, 2020 at 5:39 pm

    Hi Kelvin,

    Great work on all scripts, we are already using a lot of them with our RMM solution.

    Regarding this script, we are testing with this and I have run it with our credentials and partner rights so that works like a charm. The thing is that we want that your monitors are creating tickets connected to a customer site. So I’m trying to create the an application registration with the above script for a tenant without partner rights.

    I have read all the comments and in one comment (January 10, 2020) you mention that it might be possible if we uncomment the part of the admin role. Could you be more specific to which part you are referring to? Like with script line numbers or even more detailed.

    I thank you in advance and keep up the good work.

    Regards, Bas

  62. Pingback: Automating with PowerShell: Enabling Secure Defaults (And SD explained) - CyberDrain

  63. Rhett Brodeur November 10, 2020 at 7:47 pm

    Hi Kelvin,

    I’m getting the following error which apparently says I don’t have the right permissions. This is being run on the partner account, I am not a global administrator, but I do have access to the partner/csp side of things. Any idea what role or permissions I might need?

    Add-AzureADGroupMember : Error occurred while executing AddGroupMember
    Code: Authorization_RequestDenied
    Message: Insufficient privileges to complete the operation.
    RequestId: 93e8c272-d87a-41a5-a42b-c08322b4b1b1
    DateTimeStamp: Tue, 10 Nov 2020 18:39:06 GMT
    HttpStatusCode: Forbidden
    HttpStatusDescription: Forbidden
    HttpResponseStatus: Completed
    At C:\scripts\Secure_Model.ps1:89 char:5
    + Add-AzureADGroupMember -ObjectId $adminAgentsGroup.ObjectId -RefO …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [Add-AzureADGroupMember], ApiException
    + FullyQualifiedErrorId : Microsoft.Open.AzureAD16.Client.ApiException,Microsoft.Open.AzureAD16.PowerShell.AddGroupMember

    1. Kelvin Tegelaar November 10, 2020 at 8:03 pm

      You have to be allowed to write in your AAD, so you’ll need temporary user administration permissions, or global admin to run it one time.

  64. Pingback: Automating with PowerShell: Backup Teams Chats - CyberDrain

  65. Pingback: Monitoring with PowerShell: Monitoring potential phishing campaigns - CyberDrain

  66. Pingback: Automating with PowerShell: Automatically following all Sharepoint Sites or TEas for all users - CyberDrain

  67. Travis Phipps January 23, 2021 at 6:42 pm

    Kelvin, as others have said, thanks so much for the incredible scripts and help implementing them!
    Now that we have this working, we’re trying to figure out how to automate the token refresh process. The snippet you provide seems to still require the same interactive approval processes which eliminates the possibility of a fully silent refresh process.
    I had assumed once we authorized this all once, using our new Secure App we’d be able to have the app creds used for a silent refresh of the tokens.

    Am I missing something? Or do we literally have to manually refresh the tokens every 60-90 days?

    1. DWM April 30, 2021 at 11:03 pm

      I have the same question… what does it take to make it an automated silent refresh?

  68. Eleander Maes February 3, 2021 at 5:07 pm

    I’ve stumbled upon this post via the Teams Automating because that is something I need.
    But I’m very new to scripting etc in general.

    Anything I should read up or have activated within Azure to execute this because I’m realy into the blue… Educational licenses atm

  69. Eleander Maes February 8, 2021 at 3:39 pm

    Okay so I’ve made progress with these things but running into this issue:

    This happens atr row 32 of the script..

    Cannot bind argument to parameter ‘ObjectId’ because it is null.
    + CategoryInfo : InvalidData: (:) [], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed

  70. David Deboeck February 15, 2021 at 3:01 pm


    As a csp, the connection to exchange powershell works for commands like get-mailbox with the appid ‘a0c73c16-a7e3-4564-9a95-2bdf47383716’. But once you try to use commands make AD changes, there is an permission error. Example command: ‘Get-MailboxPlan | Set-MailboxPlan -MaxSendSize 150MB -MaxReceiveSize 150MB’

    This gives error:
    Active Directory operation failed on VI1xxxA004xx3.EURP18xx004.PROD.OUTLOOK.COM. This error is not retriable.
    Additional information: Insufficient access rights to perform the operation.
    Active directory response: 00002098: SecErr: DSID-03150F94, problem 4003 (INSUFF_ACCESS_RIGHTS), data 0
    + CategoryInfo : NotSpecified: (:) [Set-MailboxPlan], ADOperationException
    + FullyQualifiedErrorId : [Server=AMxxx701MB2818,RequestId=60f09c1d-d450-445b-xxxx-965e0a41f1d6,TimeStamp=2/15/20 21 1:52:28 PM] [FailureCategory=Cmdlet-ADOperationException] 1113F40E,Microsoft.Exchange.Management.RecipientTasks
    + PSComputerName :

    I have been struggling for weeks trying to set it for our customers without creating an admin in each tenant but for unfortunately that is the only way I can get this done.

    1. Chance Ellis February 25, 2021 at 7:33 pm

      Having the exact same issue. The funny part is, some commands still work even though you get the access rights error.

      For example – issue a set mailbox -identity -CustomAttribute1 “foo” and you will get the error. However, after about 5 minutes, the attribute will be updated to “foo”. Other commands using set mailbox will fail completely however.

      It is very frustrating.

    2. Tekwhat May 21, 2021 at 5:21 pm

      I have been using this for running reports on single tenants as part of a larger script with report function blocks, I’m sure you can use the same to run your changes for single customers. I tried making it where I could back out and select another customer to work with, but it would never actually go into the second customer, maybe you are smarter than I to fix that? I have another one I use for making bulk changes to all of my customers I can provide if you are interested.

      (login with your partner account)
      $customer = Get-MsolPartnerContract -All | Select Name, TenantID | Sort Name | Out-GridView -PassThru -Title “Select Customer to Work With”

      $InitialDomain = Get-MsolDomain -TenantId $customer.TenantId | Where-Object {$_.IsInitial}
      $DelegatedOrgURL = “” + $InitialDomain.Name
      $EXODS = New-PSSession -ConnectionUri $DelegatedOrgURL -Credential $credential -Authentication Basic -ConfigurationName Microsoft.Exchange -AllowRedirection
      Import-PSSession $EXODS | Out-Null

  71. Adam Casgar March 2, 2021 at 8:46 pm

    Does the script have an issue if your partner account is federated and using Duo MFA? For the life of me I can not get this to run.

    1. Kelvin Tegelaar March 4, 2021 at 11:42 am

      I’m fairly sure it does not, because it never processes the StrongAuthclaim token. You’ll need to have Azure MFA configured for this afaik.

  72. Pingback: Documenting with PowerShell: Documenting admin actions - CyberDrain

    1. Kelvin Tegelaar March 17, 2021 at 3:45 pm

      Yes, but only for the first creation of the tokens. I’ll be editing the script soon to make sure that is updated too πŸ™‚

  73. Pingback: Monitoring with PowerShell: Monitoring Azure File Shares - CyberDrain

  74. Andrew HUSSEY May 14, 2021 at 1:53 pm

    What’s the best practise for using this in scripting? Obviously, as you stated, store the tokens somewhere safe and secure. Is it a matter of saving scripts without the tokens included and just inserting them at runtime?



  75. Eysteihn May 14, 2021 at 9:28 pm

    Just wanted to say that this worked amazing, and on the first try. This is just amazing. Prod ready tool, Kelvin! Thanks a million!

  76. Pingback: Automating with PowerShell: Sending MFA push messages to users - CyberDrain

  77. Pingback: Automating with PowerShell: Quickly offboarding a M365 user - CyberDrain

  78. Pingback: Automating with PowerShell: Deploying Microsoft Teams Templates - CyberDrain

  79. Iggy Matula June 24, 2021 at 4:41 am


    Everything works, except the Exchange Online module for me – which I believe is the reason for Datto RMM 365 Mailbox Size Monitor to not function.

    When I run the EO Module, I get the following error:

    S C:\Users\LocalAdmin\Desktop\CyberDrain> & ‘.\3-Exchange Module (not working).ps1’
    At C:\Users\LocalAdmin\Desktop\CyberDrain\3-Exchange Module (not working).ps1:14 char:33
    + foreach($customer in $customers){
    + ~
    Missing closing ‘}’ in statement block or type definition.
    + CategoryInfo : ParserError: (:) [], ParseException
    + FullyQualifiedErrorId : MissingEndCurlyBrace

  80. SolutionsA July 3, 2021 at 10:14 pm

    Hi Kelvin,

    Very nice work, as always! Reading through some of these comments — it seems an enterprise cannot use this script? We are using Hudu internally, but without the ability to leverage this SecureApp Model, not sure how we can implement a number of your scripts.

    Let me know if you have any thoughts, and happy 4th of July!

  81. Jim-Roy Grippeling August 20, 2021 at 11:25 am

    Hi Kelvin,
    I am trying out the script. I am able to connect to the partner powershell and I can see the tennants. But I am unbale to connect to Exchange part. I get the following error:
    New-PSSession : [] Connecting to remote server failed with the following error message : Access Denied. For more information, see the about_Remote_Troubleshooting Help topic.

    I have run it in normal Powershell (Not the iSE)

    1. Emil Arntsen October 25, 2021 at 10:49 pm

      I get the same problem, any idea?

      1. Kelvin Tegelaar October 26, 2021 at 7:05 pm

        That’s exactly what you need to do, loop through your tenants and logon under the client. Please read the instructions and example scripts carefully.

  82. Glenn August 21, 2021 at 2:57 pm

    Hello Kelvin,

    I have been using your brilliant script for a while to perform some delegated admin poweshell scripting tasks across our partners.
    I recently received a message from Microsoft informing me that:

    On 30 June 2022, we’ll retire Azure AD Graph. Before that date, you’ll need to update your apps that use it to instead use Microsoft Graph

    I have been experimenting with your script and removed the $adappaccess section instead moving the permissions into $graphappaccess.

    $graphAppAccess = [Microsoft.Open.AzureAD.Model.RequiredResourceAccess]@{
    ResourceAppId = “00000003-0000-0000-c000-000000000000”;
    ResourceAccess =
    Id = “bf394140-e372-4bf9-a898-299cfc7564e5”;
    Type = “Role”},
    Id = “7ab1d382-f21e-4acd-a863-ba3e13f7da61”;
    Type = “Role”},
    Id = “0e263e50-5827-48a4-b97c-d940288653c7”;
    Type = “Scope”},
    Id = “e1fe6dd8-ba31-4d61-89e7-88639da4683d”;
    Type = “Scope”}

    This has resulted in an app that has no Azure AD component showing in the app registrations in the Azure portal. It also now passes the microsoft check for dependency.
    Strangely though it still seems to respond to the old url:

    $aadGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes ‘’ -ServicePrincipal -Tenant $tenantID
    $graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes ‘’ -ServicePrincipal -Tenant $tenantID
    Connect-AzureAD -AadAccessToken $aadGraphToken.AccessToken -AccountId $UPN -MsAccessToken $graphToken.AccessToken -TenantId $tenantID

    This still works even though there is no Azure AD Graph dependencies in the azure app registrations any more.
    Are you aware of another url that can be used instead of ‘’? If I use the url instead then no connections to Azure AD ot MSOnline etc will work.

    Any ideas.

    1. Kelvin Tegelaar August 25, 2021 at 8:10 pm

      You don’t have to worry, new applications and adding permissions is deprecated, but we’re only using these permissions in very rare occurrences, and not in any of my scripts.

      1. Glenn August 28, 2021 at 11:46 am

        Thanks for the response Kelvin.

        All I really want to know is can I continue to use Partner centre PowerShell to connect to modules like AzureAD and MSOnline, using the Secure Application model, across all partner tenants after June 30th 2022 when Azure AD Graph is due to be deprecated?

        Will β€˜’ still respond after June 30th 2022 or will there be an alternative endpoint that will work with Connect-AzureAd and Connect-MsolService ?

        I am trying to get this information direct from Microsoft too. I will share anything I find.

  83. Pingback: Automating with PowerShell: Setting up application consent - CyberDrain

  84. Glenn September 1, 2021 at 9:17 pm

    Hello again Kelvin,

    I am assured by Microsoft that the Microsoft Graph SDK module is the go forward replacement for both the AzureAD and MSOnline modules, and there are plans to provide 1-to-1 replacement for all cmdlets. Many cmdlets already have a replacement and you can see the mapping between those under

    After some experimentation and back and forth to Microsoft I have modified your excellent script to add permissions for Microsoft Graph that are currently in Azure AD Graph.
    The modified bit is the $graphAppAccess definition :

    $graphAppAccess = [Microsoft.Open.AzureAD.Model.RequiredResourceAccess]@{
    ResourceAppId = “00000003-0000-0000-c000-000000000000”;
    ResourceAccess =
    Id = “bf394140-e372-4bf9-a898-299cfc7564e5”;
    Type = “Role”},
    Id = “7ab1d382-f21e-4acd-a863-ba3e13f7da61”;
    Type = “Role”},
    Id = “0e263e50-5827-48a4-b97c-d940288653c7”; #directory.accessasuser.all
    Type = “Scope”},
    Id = “e1fe6dd8-ba31-4d61-89e7-88639da4683d”;
    Type = “Scope”}

    After deprecation of Azure AD Graph, on 30th June 2022, $adAppAccess references could be removed from the script as they wont be useable any more.

    To connect to MgGraph obviously install the Microsoft Graph PowerShell module first. Install-Module Microsoft.Graph
    Then the below can be used in PowerShell scripts to connect to MgGraph using the secure application model.

    $ApplicationID = ‘ApplicationID’
    $ApplicationSecret = ‘ApplicationSecret’
    $TenantID = ‘YourTenantID’
    $refreshtoken = ‘LongRefreshToken’
    $ApplicationSecretSecure = ConvertTo-SecureString -String $ApplicationSecret -AsPlainText -Force
    $credential = New-Object System.Management.Automation.PSCredential($ApplicationId, $ApplicationSecretSecure)
    $graphToken = New-PartnerAccessToken -ApplicationId $applicationId -Scopes ‘’ -RefreshToken $refreshtoken -Credential $credential -Tenant $tenantID
    Connect-MgGraph -AccessToken $graphToken.AccessToken

    Seems to work fine πŸ™‚ Still testing.
    Just need to convert all my scripts to use MgGraph instead of MSOnline and AzureAD PowerShell. πŸ™
    Hope this is useful.

  85. Pingback: Automating with PowerShell: Disabling anonymous reports for Office365 - CyberDrain

  86. Pingback: Automating with PowerShell: setting OneDrive ownership - CyberDrain

  87. Pingback: Secure Application Model – For the Layman and Step by Step – – Everything ConnectWise Automate, LabTech, MSP and Reports

  88. Thomas Balder November 11, 2021 at 3:40 pm

    Hey Kelvin, thanks for the script, but could you tell us how to use this or maybe another script to connect to AzureAD, Exchange and Teams without the use of the partner center module? We use dedicated customer-admin accounts, not CSP-admin accounts.

Leave a Reply to Emil Arntsen Cancel 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.