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

Param 
( 
    [Parameter(Mandatory = $false)]
    [switch]$ConfigurePreconsent,
    [Parameter(Mandatory = $true)]
    [string]$DisplayName,
    [Parameter(Mandatory = $false)]
    [string]$TenantId
)

$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..."
    Exit
} 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." `
        "$($Error[0].Exception)"
}

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

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

$partnerCenterAppAccess = [Microsoft.Open.AzureAD.Model.RequiredResourceAccess]@{
    ResourceAppId = "fa3d9a0c-3fb0-42cc-9193-47c7ecd2edbd";
    ResourceAccess =
        [Microsoft.Open.AzureAD.Model.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 'https://api.partnercenter.microsoft.com/user_impersonation' -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 'https://outlook.office365.com/.default' -Tenant $($spn.AppOwnerTenantID) -UseDeviceAuthentication
write-host "Last initation required: Please browse to https://login.microsoftonline.com/$($spn.AppOwnerTenantID)/adminConsent?client_id=$($app.AppId)"
write-host "Press any key after auth. An error report about incorrect URIs is expected!"
[void][System.Console]::ReadKey($true)
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 ================"
Write-Host "    SAVE THESE IN A SECURE LOCATION     " 

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:

MSOL-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 'https://graph.windows.net/.default' -ServicePrincipal -Tenant $tenantID 
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -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 'https://graph.windows.net/.default' -ServicePrincipal -Tenant $tenantID 
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -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 'https://graph.windows.net/.default' -ServicePrincipal -Tenant $tenantID 
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -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 'https://outlook.office365.com/.default' -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 "https://ps.outlook.com/powershell-liveid?DelegatedOrg=$($customerId)&BasicAuthToOAuthConversion=true" -Credential $credential -Authentication Basic -AllowRedirection
    Import-PSSession $session
#From here you can enter your own commands
get-mailbox 
#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 'https://api.partnercenter.microsoft.com/user_impersonation' -ServicePrincipal -Credential $credential -Tenant $TenantID -UseAuthorizationCode
$Exchangetoken = New-PartnerAccessToken -ApplicationId 'a0c73c16-a7e3-4564-9a95-2bdf47383716' -Scopes 'https://outlook.office365.com/.default' -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 ================"
Write-Host "    SAVE THESE IN A SECURE LOCATION     " 

And that’s it! I hope it helps MSPs, CSPs, etc. 🙂 as always, Happy PowerShelling!

117 thoughts on “Connect to Exchange Online automated when MFA is enabled (Using the SecureApp Model)

  1. Ban Hang

    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

    Reply
  2. Heiner

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

    Reply
    1. Kelvin Tegelaar Post author

      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… 🙂

      Reply
  3. Geert W

    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?

    Reply
    1. Kelvin Tegelaar Post author

      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.

      Reply
    2. Kelvin Tegelaar Post author

      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!

      Reply
  4. Geert W

    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!

    Reply
    1. Kelvin Tegelaar Post author

      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.

      Reply
  5. Pingback: Documenting with PowerShell: Documenting Office365 mailbox permissions - CyberDrain

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

  7. Saul Ansbacher

    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 ‘https://graph.windows.net/.default’ -ServicePrincipal -Tenant $customer.CustomerContextId
    $CustGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes ‘https://graph.microsoft.com/.default’ -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!

    Reply
    1. John Perez

      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!

      Reply
  8. Vitus Quinny

    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

    Reply
    1. Kelvin Tegelaar Post author

      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.

      Reply
      1. Austin

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

        Reply
  9. Ariel

    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:
    hxxps://o365reports.com/2019/09/25/basic-authentication-exchange-online/
    hxxps://o365reports.com/2019/12/11/connect-exchange-online-powershell-without-basic-authentication/#Install-Exchange-Online-PowerShell-V2-Module

    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!

    Reply
    1. Kelvin Tegelaar Post author

      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!

      Reply
  10. Paul

    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 “https://ps.compliance.protection.outlook.com/powershell-liveid?DelegatedOrg=$($customerId)&BasicAuthToOAuthConversion=true” -Credential $credential -Authentication Basic -AllowRedirection

    Import-PSSession $SccSession -Prefix cc

    Reply
    1. Kelvin Tegelaar Post author

      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.

      Reply
      1. Paul

        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

        Reply
        1. Kelvin Tegelaar Post author

          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 ‘https://outlook.office365.com/.default’ -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.

          Reply
          1. Paul

            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 ‘https://ps.compliance.protection.outlook.com/.default’ -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.

  11. James

    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!

    Reply
        1. James

          Hi Kelvin,

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

          Thanks,

          James

          Reply
          1. Kelvin Tegelaar Post author

            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:
            Connect-AzureAD
            $(Get-AzureADTenantDetail).ObjectId

            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!

  12. James

    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

    Reply
        1. Kelvin Tegelaar Post author

          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 https://partner.microsoft.com/commerce/customers/list and checking if your clients are listed there.

          Regards,

          Reply
          1. James

            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?

            Thanks,

            James

          2. Kelvin Tegelaar Post author

            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.

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

  14. Nick Tonkin

    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…)

    Thanks

    -nick

    Reply
    1. Kelvin Tegelaar Post author

      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.

      Reply
      1. Nick Tonkin

        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.

        Reply
  15. Michael Joseph

    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 : [ps.outlook.com] Connecting to remote server ps.outlook.com 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

    Reply
      1. Michael Joseph

        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!

        Reply
  16. Michael Joseph

    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 ‘https://graph.windows.net/.default’ -ServicePrincipal -Tenant $tenantID
    $graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes ‘https://graph.microsoft.com/.default’ -ServicePrincipal -Tenant $tenantID

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

    $CustomerID = (Get-MsolPartnerContract -domain mycustomer.com).tenantId.guid

    $token = New-PartnerAccessToken -ApplicationId ‘a0c73c16-a7e3-4564-9a95-2bdf47383716’-RefreshToken $ExchangeRefreshToken -Scopes ‘https://outlook.office365.com/.default’ -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 “https://ps.outlook.com/powershell-liveid?DelegatedOrg=$($CustomerId)&BasicAuthToOAuthConversion=true” -Credential $credential -Authentication Basic -AllowRedirection -Debug
    Import-PSSession $session

    Reply
  17. Joerg

    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?

    Reply
      1. Jeremy Bradshaw

        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.

        Reply
  18. Jeremy Bradshaw

    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.

    Reply
    1. Kelvin Tegelaar Post author

      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.

      Reply
      1. Jeremy Bradshaw

        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.

        Reply
        1. Nick Gauthier

          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.

          Reply
  19. KB

    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.

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

  21. Gagan

    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
    Console.Read.”
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : InvalidOperationException

    Reply
    1. Kelvin Tegelaar Post author

      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 🙂

      Reply
  22. Ryan

    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?

    Reply
    1. Kelvin Tegelaar Post author

      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.

      Reply
      1. Ryan

        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.

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

  24. David

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

    Reply
  25. BG

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

    New-PartnerAccessToken : AADSTS500014: Resource ‘https://outlook.office365.com’ 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.

    Reply
      1. BG

        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.

        Reply
      2. BG

        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?

        Reply
  26. Peter

    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.

    Reply
  27. Luigi Teti

    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
    Luigi

    Reply
  28. Jon

    ‘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?

    Reply
    1. Kelvin Tegelaar Post author

      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 🙂

      Reply
  29. Pingback: Automating with PowerShell: Automating intune Autopilot configuration - CyberDrain

  30. Andreas

    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.

    Cheers
    Andreas

    Reply
    1. Kelvin Tegelaar Post author

      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.

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

  32. phaedrusschmaedrus

    Kevin,

    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.

    Reply
    1. Kelvin Tegelaar Post author

      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: https://www.thelazyadministrator.com/2019/07/22/connect-and-navigate-the-microsoft-graph-api-with-powershell/, if you want more details on the permissions, you can check out https://docs.microsoft.com/en-us/powershell/module/azuread/new-azureadapplication?view=azureadps-2.0 and https://docs.microsoft.com/en-us/previous-versions/azure/ad/graph/api/entity-and-complex-type-reference#requiredresourceaccess-type.

      Hope that helps! 🙂

      Reply
      1. phaedrusschmaedrus

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

        Reply
  33. Pingback: Documenting with PowerShell: Documenting intune applications - CyberDrain

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

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

  36. Muhammad Usman

    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?

    Regards,

    Muhammad Usman

    Reply
    1. Kelvin Tegelaar Post author

      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.

      Reply
      1. Muhammad Usman

        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?

        Thanks,

        Warm Regards,

        Muhammad Usman.

        Reply
  37. BG

    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 ‘https://outlook.office365.com’ 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.

    Reply
      1. BG

        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?

        Reply
  38. Pingback: Documenting and monitoring blogs updates - CyberDrain

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

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

  41. Paul Obrien

    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?

    Reply
  42. Alex Pawlak

    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,
    AlexPawlak

    Reply
  43. Rob Kooiman

    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
    get-mailbox
    #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

    Reply
  44. Alex

    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: https://i.imgur.com/1fGm4pV.png

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

    Reply
  45. Samarah

    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.

    Source: https://o365reports.com/2020/07/04/modern-auth-and-unattended-scripts-in-exchange-online-powershell-v2/

    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.

    Reply
  46. Stewart McAdoo

    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.

    Reply
  47. Pingback: Automating with PowerShell: Teams Automapping - CyberDrain

  48. Rob Kooiman

    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

    Reply
  49. Andrew Cullen

    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 ‘https://management.azure.com/user_impersonation’ -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 ‘https://management.azure.com/user_impersonation’ -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!

    Cheers,
    Andrew

    Reply
  50. Pingback: Documenting with PowerShell: Documenting the O365 portal - CyberDrain

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

  52. Michael McCool

    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
    ——- ———– ——– ———— ———–
    VALIDEMAILADDRESS AzureCloud ‘TENANTID’ ‘TENANTDOMAIN’ AccessToken
    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
    ‘00000002-0000-0ff1-ce00-000000000000’.

    Reply
    1. Kelvin Tegelaar Post author

      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!

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

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.