Category Archives: Office365

Documenting with PowerShell: Increasing the Office365 Secure Score.

So previously we’ve spoken about documenting the Office 365 Secure Score. For a great resource on this I’d suggest you check out Eliot’s blog on documenting the Secure Score here. Its a fantastic resource.

This time I’m not going to focus on documenting the Secure Score directly – But increasing it. we want to make sure that the Secure Score is as high as possible with as little user impact as possible. To do this, I’ve selected some items that increase your secure score but have next to no impact on normal usage. Of course you’ll have to check if this is true for your environment too.

The Script

The script is set up to enable the following features for all tenants in your partner portal.

  • Move mail with a high confidence spam rating to the Junk Folder (Does not increase SecureScore, but was requested to add on Slack. You can remove this item if you only want the Secure Score increase)
  • Mailbox Auditing for all users
  • Mailbox Litigation hold where possible.
  • DelegateSentitemsStyle for mailboxes
  • NDR report for journaling.
  • Set the outbound spam filter reporting e-mail address
  • Set “Do not allow users to grant consent to unmanaged applications”
  • Disable password expire on user accounts
  • Enable the self-service password reset(I’d strongly recommend to first enable multi factor authentication for all your users.

I believe I could’ve added more features but I chose to only enable the ones with no to very little user impact. Using this script you can adapt it to all your wishes. It’s also very easy to disable one of the features – Just remove the the entire block of code that you do not want to enable.

#Set the recipient for outbound spam reports and Journaling NDRs.
$SpamAndEmailRecipient = "Helpdesk@limenetworks.nl"
#######################################################################
###################  CREDENTIALS     ##################################
$ApplicationId         = 'xxxx-xxxx-xxx-xxxx-xxxx'
$ApplicationSecret     = 'TheSecretTheSecrey' | Convertto-SecureString -AsPlainText -Force
$TenantID              = 'YourTenantID'
$RefreshToken          = 'RefreshToken'
$ExchangeRefreshToken  = 'ExchangeRefreshToken'
$upn                   = 'UPN-Used-To-Generate-Tokens' 
###################  END CREDENTIALS ##################################
$credential = New-Object System.Management.Automation.PSCredential($ApplicationId, $ApplicationSecret)

$aadGraphToken = New-PartnerAccessToken -ApplicationId $Applicatio nId -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 -AllowClobber -DisableNameChecking
    Write-Host "Starting process for client $($customer.name)" -ForegroundColor Green
    #Move mail with a high confidence spam to the Junk folder. 
    try {
        Get-HostedContentFilterPolicy -ErrorAction Stop | Set-HostedContentFilterPolicy -HighConfidenceSpamAction MoveToJmf -ErrorAction Stop 
    }
    catch {
        Write-Output "Failed to change the spam policy. $($_.Exception.Message)"
    }
    #Enable mailbox auditing for each user.
    try {
        Get-Mailbox -ResultSize Unlimited -ErrorAction Stop | Set-Mailbox -ErrorAction Stop -AuditEnabled $true -AuditOwner MailboxLogin, HardDelete, SoftDelete, Update, Move -AuditDelegate SendOnBehalf, MoveToDeletedItems, Move -AuditAdmin Copy, MessageBind 
    }
    catch {
        Write-Output "Failed to enable Mailbox auditing. $($_.Exception.Message)"
    }

    #Enable mailbox litigation hold
    try {
        Get-Mailbox -ResultSize Unlimited -ErrorAction Stop | Set-Mailbox -ErrorAction Stop -LitigationHoldEnabled $true -LitigationHoldDuration 2555 
    }
    catch {
        Write-Output "Failed to enable Mailbox Litigation hold. $($_.Exception.Message)"
    }
    #Enable DelegateSentItems.
    try {
        Get-Mailbox -ResultSize Unlimited -ErrorAction Stop | set-mailbox -ErrorAction Stop -MessageCopyForSentAsEnabled $true -MessageCopyForSendOnBehalfEnabled $true 
    }
    catch {
        Write-Output "Failed to enable DelegateSentItems style. $($_.Exception.Message)"
    }
    #Set Journaling NDR
    try {
        set-transportconfig -JournalingReportNdrTo "$SpamAndEmailRecipient" -ErrorAction Stop 
    }
    catch {
        Write-Output "Failed to set Transport Config Journaling NDR $($_.Exception.Message)"
    }
    #Set outbound spamfilter reporting
    try {
        Set-HostedOutboundSpamFilterPolicy "Default" -NotifyOutboundSpam $true -NotifyOutboundSpamRecipients $SpamAndEmailRecipient -ErrorAction Stop 
    }
    catch {
        Write-Output "Failed to set outbound spam settings $($_.Exception.Message)"
    }
    
    #Set "Do not allow users to grant consent to unmanaged applications"
    try {
        Set-MsolCompanySettings -tenantID $customer.TenantId -UsersPermissionToUserConsentToAppEnabled:$false -ErrorAction Stop 
    }
    catch {
        Write-Output "Failed to set Permissions to allow user to grant consent to unmanaged applications $($_.Exception.Message)"
    }
    #Disable password expire on accounts.
    try {
        Get-MsolUser -TenantId $customer.TenantId -ErrorAction Stop | Set-MsolUser –PasswordNeverExpires $true -ErrorAction Stop 
    }
    catch {
        Write-Output "Disable password expire failed. $($_.Exception.Message)"
    }

    #Enable Self Service Password Reset
    try {
        Set-MsolCompanySettings -TenantId $customer.TenantId -SelfServePasswordResetEnabled:$true -erroraction Stop
    }
    catch {
        Write-Output "Enabling Self Service Password Reset Failed. $($_.Exception.Message)"
    }


    Write-Host "Finished process for client $($customer.name)" -ForegroundColor Green
    Remove-PSSession $session
}

and that’s it! as always, Happy PowerShelling!

Documenting with PowerShell: Documenting Office365 mailbox permissions

I like being able to report to our clients exactly what the permissions on mailboxes are. The only issue with reporting via the Office 365 admin portal is that we don’t get a history. I’ve decided to make sure that I can do this by documenting the permissions daily.

To do this, I have two versions of the script; one for IT-Glue and one for general HTML file usage. Both scripts connect to the Office365 Exchange PowerShell using the Secure App Model. For more information you can check this blog post.

IT-Glue version

The IT-Glue version gets all contacts in your IT-Glue environment, compares these to the domains found in each of your partner tenants and documents the permissions for the right tenant into the correct IT-Glue client. The script creates a Flexible Asset for you if it does not yet exist.

$key                   = "YOUR IT Glue Key here"
$ApplicationId         = 'Application ID'
$ApplicationSecret     = 'Application Secret' | Convertto-SecureString -AsPlainText -Force
$TenantID              = 'YourTenantID'
$RefreshToken          = 'YourRefreshTokens'
$ExchangeRefreshToken  = 'YourExchangeRefresherToken'
$upn                   = 'UPN-That-Generated-Tokens'
$APIEndpoint           = "https://api.eu.itglue.com"
$FlexAssetName         = "Office365 Permissions - Automatic Documentation"
#######################################################################
write-output "Getting Module"
If (Get-Module -ListAvailable -Name "ITGlueAPI") { Import-module ITGlueAPI } Else { install-module ITGlueAPI -Force; import-module ITGlueAPI }
If (Get-Module -ListAvailable -Name "MsOnline") { Import-module "Msonline" } Else { install-module "MsOnline" -Force; import-module "Msonline" }
If (Get-Module -ListAvailable -Name "PartnerCenter") { Import-module "PartnerCenter" } Else { install-module "PartnerCenter" -Force; import-module "PartnerCenter" }
Write-Output "Generating tokens to login"
$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 
Add-ITGlueBaseURI -base_uri $APIEndpoint
Add-ITGlueAPIKey $key
write-output "Getting IT-Glue contact list"
$i = 0
do {
    $AllITGlueContacts += (Get-ITGlueContacts -page_size 1000 -page_number $i).data.attributes
    $i++
    Write-Host "Retrieved $($AllITGlueContacts.count) Contacts" -ForegroundColor Yellow
}while ($AllITGlueContacts.count % 1000 -eq 0 -and $AllITGlueContacts.count -ne 0) 
Write-Output "Checking if flexible asset exists."
$FilterID = (Get-ITGlueFlexibleAssetTypes -filter_name $FlexAssetName).data
if (!$FilterID) { 
Write-Output "No Flexible Asset found. Creating new one"
    $NewFlexAssetData = 
@{
    type = 'flexible-asset-types'
    attributes = @{
            name = $FlexAssetName
            icon = 'sitemap'
            description = $description
    }
    relationships = @{
        "flexible-asset-fields" = @{
            data = @(
                @{
                    type       = "flexible_asset_fields"
                    attributes = @{
                        order           = 1
                        name            = "tenantid"
                        kind            = "Text"
                        required        = $true
                        "show-in-list"  = $true
                        "use-for-title" = $true
                    }
                },
                @{
                    type       = "flexible_asset_fields"
                    attributes = @{
                        order          = 2
                        name           = "permissions"
                        kind           = "Textbox"
                        required       = $false
                        "show-in-list" = $true
                    }
                },
                @{
                    type       = "flexible_asset_fields"
                    attributes = @{
                        order          = 3
                        name           = "tenant-name"
                        kind           = "Text"
                        required       = $false
                        "show-in-list" = $false
                    }
                }
            )
            }
        }
          
   }
    New-ITGlueFlexibleAssetTypes -Data $NewFlexAssetData 
    $FilterID = (Get-ITGlueFlexibleAssetTypes -filter_name $FlexAssetName).data
}


write-output "Logging in."
Connect-MsolService -AdGraphAccessToken $aadGraphToken.AccessToken -MsGraphAccessToken $graphToken.AccessToken
$customers = Get-MsolPartnerContract -All 
   
foreach ($customer in $customers) {
    $MSOLPrimaryDomain = (get-msoldomain -TenantId $customer.tenantid | Where-Object { $_.IsInitial -eq $false }).name
    $customerDomains = Get-MsolDomain -TenantId $customer.TenantId | Where-Object { $_.status -contains "Verified" }
    $MSOLtentantID = $customer.tenantid
    #Connecting to the O365 tenant
    $InitialDomain = Get-MsolDomain -TenantId $customer.TenantId | Where-Object { $_.IsInitial -eq $true }
    Write-host "Documenting $($Customer.Name)"
    $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 -CommandName "Get-MailboxPermission", "Get-Mailbox" -AllowClobber
    $Mailboxes = Get-Mailbox -ResultSize:Unlimited | Sort-Object displayname

    foreach ($mailbox in $mailboxes) {
        $AccesPermissions = Get-MailboxPermission -Identity $mailbox.identity | Where-Object { $_.user.tostring() -ne "NT AUTHORITY\SELF" -and $_.IsInherited -eq $false } -erroraction silentlycontinue | Select-Object User, accessrights
        if ($AccesPermissions) { $HTMLPermissions += $AccesPermissions | convertto-html -frag -PreContent "
Permissions on $($mailbox.PrimarySmtpAddress)
" | Out-String }
    }
    Remove-PSSession $session

    $FlexAssetBody =
    @{
        type       = 'flexible-assets'
        attributes = @{
            traits = @{
                'permissions' = $HTMLPermissions
                'tenantid'    = $MSOLtentantID
                'tenant-name' = $initialdomain.name
            }
        }
    }


    write-output "             Finding $($customer.name) in IT-Glue"
    $orgID = @()
    foreach ($customerDomain in $customerdomains) {
        $orgID += ($AllITGlueContacts | Where-Object { $_.'contact-emails'.value -match $customerDomain.name }).'organization-id' | Select-Object -Unique

    }



    write-output "             Uploading Office Permission $($customer.name) into IT-Glue"
    foreach ($org in $orgID) {
        $ExistingFlexAsset = (Get-ITGlueFlexibleAssets -filter_flexible_asset_type_id $($filterID.ID) -filter_organization_id $org).data | Where-Object { $_.attributes.traits.'tenant-name' -eq $initialdomain.name }
        #If the Asset does not exist, we edit the body to be in the form of a new asset, if not, we just upload.
        if (!$ExistingFlexAsset) {
            $FlexAssetBody.attributes.add('organization-id', $org)
            $FlexAssetBody.attributes.add('flexible-asset-type-id', $($filterID.ID))
            write-output "                      Creating new Office Permission $($customer.name) into IT-Glue organisation $org"
            New-ITGlueFlexibleAssets -data $FlexAssetBody
        }
        else {
            write-output "                      Updating Office Permission $($customer.name) into IT-Glue organisation $org"
            $ExistingFlexAsset = $ExistingFlexAsset[-1]
            Set-ITGlueFlexibleAssets -id $ExistingFlexAsset.id  -data $FlexAssetBody
        }

    }
    $MSOLPrimaryDomain = $null
    $MSOLtentantID = $null
    $AccesPermissions = $null
    $HTMLPermissions = $null 
}

General HTML version

The generic HTML version makes a HTML file per client in C:\Temp. It uses the name of the client as the filename.

$ApplicationId         = 'ApplicationID'
$ApplicationSecret     = 'ApplicationSecret' | Convertto-SecureString -AsPlainText -Force
$TenantID              = 'YourTenantID'
$RefreshToken          = 'RefreshToken'
$ExchangeRefreshToken  = 'ExchangeRefreshToken'
$upn                   = 'YourUPN'
#######################################################################
write-output "Getting Modules"
If (Get-Module -ListAvailable -Name "MsOnline") { Import-module "Msonline" } Else { install-module "MsOnline" -Force; import-module "Msonline" }
If (Get-Module -ListAvailable -Name "PartnerCenter") { Import-module "PartnerCenter" } Else { install-module "PartnerCenter" -Force; import-module "PartnerCenter" }
Write-Output "Generating tokens to login"
$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 
write-output "Logging in."
Connect-MsolService -AdGraphAccessToken $aadGraphToken.AccessToken -MsGraphAccessToken $graphToken.AccessToken
$customers = Get-MsolPartnerContract -All 
$head = @"
<script>
function myFunction() {
    const filter = document.querySelector('#myInput').value.toUpperCase();
    const trs = document.querySelectorAll('table tr:not(.header)');
    trs.forEach(tr => tr.style.display = [...tr.children].find(td => td.innerHTML.toUpperCase().includes(filter)) ? '' : 'none');
  }</script>
<title>Audit Log Report</title>
<style>
body { background-color:#E5E4E2;
      font-family:Monospace;
      font-size:10pt; }
td, th { border:0px solid black; 
        border-collapse:collapse;
        white-space:pre; }
th { color:white;
    background-color:black; }
table, tr, td, th {
     padding: 2px; 
     margin: 0px;
     white-space:pre; }
tr:nth-child(odd) {background-color: lightgray}
table { width:95%;margin-left:5px; margin-bottom:20px; }
h2 {
font-family:Tahoma;
color:#6D7B8D;
}
.footer 
{ color:green; 
 margin-left:10px; 
 font-family:Tahoma;
 font-size:8pt;
 font-style:italic;
}
#myInput {
  background-image: url('https://www.w3schools.com/css/searchicon.png'); /* Add a search icon to input */
  background-position: 10px 12px; /* Position the search icon */
  background-repeat: no-repeat; /* Do not repeat the icon image */
  width: 50%; /* Full-width */
  font-size: 16px; /* Increase font-size */
  padding: 12px 20px 12px 40px; /* Add some padding */
  border: 1px solid #ddd; /* Add a grey border */
  margin-bottom: 12px; /* Add some space below the input */
}
</style>
"@
foreach ($customer in $customers) {
    $MSOLPrimaryDomain = (get-msoldomain -TenantId $customer.tenantid | Where-Object { $_.IsInitial -eq $false }).name
    $customerDomains = Get-MsolDomain -TenantId $customer.TenantId | Where-Object { $_.status -contains "Verified" }
    $MSOLtentantID = $customer.tenantid
    #Connecting to the O365 tenant
    $InitialDomain = Get-MsolDomain -TenantId $customer.TenantId | Where-Object { $_.IsInitial -eq $true }
    Write-host "Documenting $($Customer.Name)"
    $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 -CommandName "Get-MailboxPermission", "Get-Mailbox" -AllowClobber
    $Mailboxes = Get-Mailbox -ResultSize:Unlimited | Sort-Object displayname
    $MSOLPrimaryDomain = $null
    $MSOLtentantID = $null
    $AccesPermissions = $null
    $HTMLPermissions = $null
    foreach ($mailbox in $mailboxes) {
        $AccesPermissions = Get-MailboxPermission -Identity $mailbox.identity | Where-Object { $_.user.tostring() -ne "NT AUTHORITY\SELF" -and $_.IsInherited -eq $false } -erroraction silentlycontinue | Select-Object User, accessrights
        if ($AccesPermissions) { $HTMLPermissions += $AccesPermissions | convertto-html -frag -PreContent "<h4>Permissions on $($mailbox.PrimarySmtpAddress)</h4>" | Out-String }
    }
    Remove-PSSession $session
    $CompleteHTML = $head,$HTMLPermissions | Out-String | out-file "C:\Temp\$($customer.name).html"
} 

Converting group policy registry preferences to PowerShell scripts

Most of the clients at my firm are moving to cloud only solutions in which we have less management options available. We can use Intune for Administrative Templates, or as we do use our RMM system as the management platform. To make sure we can use our RMM system we have several scripts that deploy registry keys in the same way as the GPO does. If you want to find what keys a GPO sets you can use this website.

Now the issue with this is that you do not directly have the option to deploy Group Policy Preferences instead of Group Policy Administrative Templates. A lot of our clients have these for applications that do not support ADMX files. To convert these I’ve created the following script.

The conversion script

 function convert-gpo($GPOFilePath) {
    $XMLFile = get-content $GPOFilePath
    $RegKeys = ($xmlfile | Select-Xml -Xpath "//Registry").Node.properties
    foreach ($regkey in $RegKeys) {
        #Converting Hkey to actual path
        switch ($regkey.hive) {
            "HKEY_CURRENT_USER" { $regkey.hive = "HKCU:" ; break }
            "HKEY_LOCAL_MACHINE" { $regkey.hive = "HKLM:" ; break }
        }
        $RegPath = Join-Path $regkey.hive $regkey.key
        write-host "####### $($RegPath) with $($regkey.name) #######"
        write-host "New-ItemProperty -Path $($RegPath) -name $($regkey.name) -Value $($regkey.value) -Force"
        write-host "####### NEXT KEY #######"
    }
    
}

Using this script you can run the Convert-GPO function with the file path of the export of your GPO preference file as the only parameter.

The script will then write a new script using write-host. You can copy and paste this to your RMM system, or save it as a file by running "Convert-GPO $Filename | out-file NewScript.ps1" and that’s it! as always, Happy PowerShelling.

Monitoring with PowerShell: Monitoring Office365 admin password changes

So when I was at Dattocon I was approached by an MSP that was using his RMM system to alert on changes of the local admin password, as he wanted to be updated every time a local admin got a new password. He did this by using an older script of mine below.

Monitoring Local Admin Password changes

$LastDay = (Get-Date).addhours(-24)
$AdminGroup = Get-LocalGroupMember -SID "S-1-5-32-544"
foreach($Admin in $AdminGroup){
$ChangedAdmins = get-localuser -sid $admin.sid | Where-Object {$_.PasswordLastSet -gt $LastDay}
}

But he came to me telling me that recently he had a need to start using this to alert on that a password needed to be updated in his documentation system to complete a process, but he was missing this for Office365 environments. I figured I would give him a hand and made the following script

Monitoring Office365 Global Admin Password changes – All tenants

$LastDay = (Get-Date).addhours(-24)
$credential = Get-Credential
Connect-MsolService -Credential $credential
$customers = Get-msolpartnercontract -All
$ChangedUsers = @()
foreach($customer in $customers){
write-host "getting users for $($Customer.Name)" -ForegroundColorGreen
$adminemails = Get-MsolRoleMember -TenantId $customer.tenantid -RoleObjectId(Get-MsolRole-RoleName"CompanyAdministrator").ObjectId
$Users = $adminemails | get-msoluser-TenantId$customer.TenantId
foreach($User in $Users){
if($User.LastPasswordChangeTimestamp -gt $LastDay){$ChangedUsers += "$($User.UserPrincipalName)has changed his password in the last 24 hours.Please update documentation to reflect.`n"}
}
}

 

Monitoring Office365 Global Admin Password Changes – Single tenant

$TenantName = "YourTenantName.onmicrosoft.com"
$LastDay = (Get-Date).addhours(-24)
$credential = Get-Credential
Connect-MsolService -Credential $credential
$Customer=Get-msolpartnercontract -All | Where-Object{$_.DefaultDomainName -eq $TenantName}
$ChangedUsers=@()
write-host"getting users for $($Customer.Name)" -ForegroundColorGreen
$adminemails = Get-MsolRoleMember -TenantId$customer.tenantid -RoleObjectId (Get-MsolRole -RoleName "CompanyAdministrator").ObjectId
$Users= $adminemails | get-msoluser-TenantId $customer.TenantId
foreach($User in $Users){
if($User.LastPasswordChangeTimestamp -gt $LastDay){$ChangedUsers +="$($User.UserPrincipalName) has changed his password in the last 24 hours.Please update documentation to reflect.`n"}
}

 

This script checks if a password has been changed in the last day, and if so alerts on it, notifying you that a global admin password has been updated and needs to be changed in the documentation. You can also use this as a warning system if you do not have anyone that should be changing these passwords.

Anyway, hope it helps, and as always. Happy PowerShelling!

Monitoring with PowerShell: Monitoring failed logins for Office365

So this was another request by a reader; he has MFA configured for all his users, but still wants to know when the failed logon count increases. Mostly so he can warn his users that a possible spear-phising attempt might also be imminent. We know that when brute force does not work, focussed bad actors will often try the next avenue of attack.

At his request i’ve made the following scripts, one will monitor all possible locations using your partner credentials. The other will monitor only one tenant. I personally like the latter better as I’ve integrated this into my RMM so it can run and alert per client, Also it’s a little faster.

The Script

The script is designed to run at least every 4 hours, but can be run even on a 5-10 minute basis. It will get all info for the previous 4 hours, If you want to decrease on increase this you can edit line 13. Getting the logs is based on Elliot’s script to get the unified logs here. To connect with MFA, use my other blog here to generate your Secure App Model credentials.

Get Failed Logon information for all tenants

$credential = Get-Credential
Connect-MsolService -Credential $credential
$customers = Get-msolpartnercontract -All
$FilteredLogs = @()
$FailedLogonCount = 0
foreach ($customer in $customers) {
    $InitDomain = $customer.DefaultDomainName
    $DelegatedOrgURL = "https://outlook.office365.com/powershell-liveid?DelegatedOrg=" + $InitDomain
    write-host "Connecting to $($customer.Name) Security Center"
    $s = New-PSSession -ConnectionUri $DelegatedOrgURL -Credential $credential -Authentication Basic -ConfigurationName Microsoft.Exchange -AllowRedirection
    Import-PSSession $s -CommandName Search-UnifiedAuditLog -AllowClobber
    write-host "Getting last 30 minutes of logs for $($customer.Name)"
    $startDate = (Get-Date).addhours(-4)
    $endDate = (Get-Date)
    $Logs = @()
    Write-Host "Retrieving logs for $($customer.name)" -ForegroundColor Blue
    do {
        $logs += Search-unifiedAuditLog -SessionCommand ReturnLargeSet -SessionId $customer.name -ResultSize 5000 -StartDate $startDate -EndDate $endDate -Operations userloginfailed #-SessionId "$($customer.name)"
        Write-Host "Retrieved $($logs.count) logs" -ForegroundColor Yellow
    }while ($Logs.count % 5000 -eq 0 -and $logs.count -ne 0)
   $FilteredLogs +=  $logs.auditdata | convertfrom-json -ErrorAction SilentlyContinue | Select-Object UserID, ClientIP | Group-Object -Property UserID
   Foreach($item in $FilteredLogs){
       if($item.name -ne $null){ $FailedLogonCount += $item.count; $FailedLogon += "$($Item.name) has $($item.count) failed logons from the following IPs: $($Item.group.ClientIP) `n" }
   }
}

if(!$FailedLogonCount){ $FailedLogon = "Healthy"}

Get failed logins for only one tenant

$TenantName = "TenantDomain.onmicrosoft.com"

$credential = Get-Credential
Connect-MsolService -Credential $credential
$FilteredLogs = @()
$FailedLogonCount = 0
$customer = Get-msolpartnercontract | Where-Object {$_.DefaultDomainName -eq $TenantName}

$InitDomain = $customer.DefaultDomainName
    $DelegatedOrgURL = "https://outlook.office365.com/powershell-liveid?DelegatedOrg=" + $InitDomain
    write-host "Connecting to $($customer.Name) Security Center"
    $s = New-PSSession -ConnectionUri $DelegatedOrgURL -Credential $credential -Authentication Basic -ConfigurationName Microsoft.Exchange -AllowRedirection
    Import-PSSession $s -CommandName Search-UnifiedAuditLog -AllowClobber
    write-host "Getting last 30 minutes of logs for $($customer.Name)"
    $startDate = (Get-Date).addhours(-4)
    $endDate = (Get-Date)
    $Logs = @()
    Write-Host "Retrieving logs for $($customer.name)" -ForegroundColor Blue
    do {
        $logs += Search-unifiedAuditLog -SessionCommand ReturnLargeSet -SessionId $customer.name -ResultSize 5000 -StartDate $startDate -EndDate $endDate -Operations userloginfailed #-SessionId "$($customer.name)"
        Write-Host "Retrieved $($logs.count) logs" -ForegroundColor Yellow
    }while ($Logs.count % 5000 -eq 0 -and $logs.count -ne 0)
   $FilteredLogs +=  $logs.auditdata | convertfrom-json -ErrorAction SilentlyContinue | Select-Object UserID, ClientIP | Group-Object -Property UserID
   Foreach($item in $FilteredLogs){
       if($item.name -ne $null){ $FailedLogonCount += $item.count; $FailedLogon += "$($Item.name) has $($item.count) failed logons from the following IPs: $($Item.group.ClientIP) `n" }
   }


if(!$FailedLogonCount){ $FailedLogon = "Healthy"}

You can choose to alert just on the FailedLogon variable, or alert based on the actual count via FailedLogonCount. As always, Happy Powershelling.

Monitoring with PowerShell: Monitoring Office365 Azure AD Sync

We deploy Azure AD Sync for all of our clients that have hybrid environments. Sometimes the Office365 Azure AD Sync might break down, due to the Accidental Deletion Threshold or no longer perform passwords syncs due to other problems. The Azure AD sync client does tend to break from time to time.

To make sure you are alerted when this happens and can jump in on it early, there are a couple of solutions. In the Office365 portal you can easily set up Office365 to send you an email when this happens. I just don’t like receiving emails for critical infrastructure, and our RMM system has the ability to monitor cloud systems.

The scripts below can be used to monitor the Office365 Azure Active Directory Sync for one tenant, or all tenants in one go.

Single tenant script

$TenantName = "ClientDomain.onmicrosoft.com"
$AlertingTime = (Get-Date).AddHours(-24)
$credential = Get-Credential
Connect-MsolService -Credential $credential
$customer = Get-msolpartnercontract | Where-Object {$_.DefaultDomainName -eq $TenantName}

    $DirectorySynchronizationEnabled = (Get-MsolCompanyInformation -TenantId $customer.TenantId).DirectorySynchronizationEnabled
    if ($DirectorySynchronizationEnabled -eq $true) {
        $LastDirSyncTime = (Get-MsolCompanyInformation -TenantId $customer.TenantId).LastDirSyncTime
        $LastPasswordSyncTime = (Get-MsolCompanyInformation -TenantId $customer.TenantId).LastPasswordSyncTime
        If ($LastDirSyncTime -lt $AlertingTime) { $LastDirSync += "Dirsync Failed for $($Customer.DefaultDomainName) - Last Sync time was at $LastDirSyncTime `n" }
        If ($LastPasswordSyncTime -lt $AlertingTime) { $LastPasswordSync += "Password Sync Failed for $($Customer.DefaultDomainName) - Last Sync time was at $LastPasswordSyncTime `n" }
    }

if(!$LastDirSync){ $LastDirSync = "Healthy"}
if(!$LastPasswordSync){ $LastPasswordSync = "Healthy"}

Multiple tenants script

$AlertingTime = (Get-Date).AddHours(-24)
$credential = Get-Credential
Connect-MsolService -Credential $credential
$customers = Get-msolpartnercontract -All
foreach ($customer in $customers) {
    $DirectorySynchronizationEnabled = (Get-MsolCompanyInformation -TenantId $customer.TenantId).DirectorySynchronizationEnabled
    if ($DirectorySynchronizationEnabled -eq $true) {
        $LastDirSyncTime = (Get-MsolCompanyInformation -TenantId $customer.TenantId).LastDirSyncTime
        $LastPasswordSyncTime = (Get-MsolCompanyInformation -TenantId $customer.TenantId).LastPasswordSyncTime
        If ($LastDirSyncTime -lt $AlertingTime) { $LastDirSync += "Dirsync Failed for $($Customer.DefaultDomainName) - Last Sync time was at $LastDirSyncTime `n" }
        If ($LastPasswordSyncTime -lt $AlertingTime) { $LastPasswordSync += "Password Sync Failed for $($Customer.DefaultDomainName) - Last Sync time was at $LastPasswordSyncTime `n" }
    }
}

if(!$LastDirSync){ $LastDirSync = "Healthy"}
if(!$LastPasswordSync){ $LastPasswordSync = "Healthy"} 

And that’s it! With this monitoring set you’ve created a cloud-sided monitoring set that can show you exactly where your Office365 Azure AD Sync fails. As always, Happy PowerShelling.

Using the Secure Application Model with PartnerCenter 2.0 for Office365.

I was recently informed that my scripts for the secure application model no longer worked. This is due to Microsoft updating the PartnerCenter module with some breaking changes. To make sure you can use the Secure App Model script I’ve made a new version below.

The changes in this script are in the way the access token is generated, Normally you’d get a Windows Authentication pop-up to allow consent. This is no longer possible with the PartnerCenter 2.0 module. This also requires us to add an extra return-URI to the Azure Application. To fix these issues, use the script below.

The script

<#
    .SYNOPSIS
        This script will create the require Azure AD application.
    .EXAMPLE
        .\Create-AzureADApplication.ps1 -ConfigurePreconsent -DisplayName "Partner Center Web App"

        .\Create-AzureADApplication.ps1 -ConfigurePreconsent -DisplayName "Partner Center Web App" -TenantId eb210c1e-b697-4c06-b4e3-8b104c226b9a

        .\Create-AzureADApplication.ps1 -ConfigurePreconsent -DisplayName "Partner Center Web App" -TenantId tenant01.onmicrosoft.com
    .PARAMETER ConfigurePreconsent
        Flag indicating whether or not the Azure AD application should be configured for preconsent.
    .PARAMETER DisplayName
        Display name for the Azure AD application that will be created.
    .PARAMETER TenantId
        [OPTIONAL] The domain or tenant identifier for the Azure AD tenant that should be utilized to create the various resources.
#>

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..."

    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")
$password = New-AzureADApplicationPasswordCredential -ObjectId $app.ObjectId
$spn = New-AzureADServicePrincipal -AppId $app.AppId -DisplayName $DisplayName

if($ConfigurePreconsent) {
    $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 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 "================ Secrets ================"
Write-Host "ApplicationId       = $($app.AppId)"
Write-Host "ApplicationSecret   = $($password.Value)"
write-host "RefreshToken        = $($token.refreshtoken)"
Write-Host "================ Secrets ================"
Write-Host "    SAVE THESE IN A SECURE LOCATION     "

This script should help you back on the Secure App Model Train. As always, Happy PowerShelling.

Monitoring with PowerShell: Monitoring Office C2R updates

This blog might be a little shorter than normally, I’ve been a bit swamped with work so if you have any questions, let me know!

This time we’re going to monitor the update status of Microsoft Office that’s been installed using C2R. C2R installers do not get updates from the Microsoft Update services and thus RMM systems often can’t update these. Seeing as C2R is now the standard for all Office Installations we’ll need to start monitoring this separately from Windows Updates. We also want all our of clients to be in the same update channel.

$ReportedVersion = Get-ItemPropertyValue -Path "HKLM:\SOFTWARE\Microsoft\Office\ClickToRun\Configuration" -Name "VersionToReport"
$Channel = (Get-ItemPropertyValue -Path "HKLM:\SOFTWARE\Microsoft\Office\ClickToRun\Configuration" -Name "CDNBaseUrl") -split "/" | Select-Object -Last 1

If (!$Channel) { 
    $Channel = "Non-C2R version or No Channel selected."
}
else {
    switch ($Channel) { 
        "492350f6-3a01-4f97-b9c0-c7c6ddf67d60"  { $Channel = 'Current ("Monthly")' }
        "64256afe-f5d9-4f86-8936-8840a6a4f5be"  { $Channel = "Current Preview (`"Monthly Targeted`"/`"Insiders`")" }
        "7ffbc6bf-bc32-4f92-8982-f9dd17fd3114"  { $Channel = "Semi-Annual Enterprise (`"Broad`")" }
        "b8f9b850-328d-4355-9145-c59439a0c4cf"  { $Channel = "Semi-Annual Enterprise Preview (`"Targeted`")" }
        "55336b82-a18d-4dd6-b5f6-9e5095c314a6"  { $Channel = "Monthly Enterprise" }
        "5440fd1f-7ecb-4221-8110-145efaa6372f"  { $Channel = "Beta" }
        "f2e724c1-748f-4b47-8fb8-8e0d210e9208"  { $Channel = "LTSC" }
        "2e148de9-61c8-4051-b103-4af54baffbb4"  { $Channel = "LTSC Preview" }
    }
}

To monitor on the versions we want to support by checking this page by Microsoft. We also monitor the Channel by alerting on anything that is not “Monthly Channel”, as soon as we see an agent that has the incorrect channel we fix it by running the following command

"C:\Program Files\Common Files\Microsoft Shared\ClickToRun\OfficeC2RClient.exe" /changesetting Channel=Monthly

When a client is not up to date, we force the latest update via the following command, this updates the client specifically to the version we want.

 "C:\Program Files\Common Files\Microsoft Shared\ClickToRun\OfficeC2RClient.exe"  /update USER displaylevel=False updatetoversion=16.0.7341.2029

If you want to update to any update that is available, for the channel the installation is in.

 "C:\Program Files\Common Files\Microsoft Shared\ClickToRun\OfficeC2RClient.exe"  /update USER displaylevel=False

And that’s it! you can now use this to update to the latest versions, and monitor the minimum required version you need installed. As always, Happy PowerShelling!

Update: Updated as Microsoft changed the channel names, one of my friends had an updated version of the version table for me so I added that. 🙂

Monitoring with PowerShell Chapter 3: Monitoring Modern Authentication

Modern Authentication is turned on by default for new tenants, but if you have legacy tenants or take over tenants from others MSP’s than sometimes you might have tenants that do not use Modern Authentication yet.

Monitoring and auto remediation is key in this when using Multi factor Authentication. We want the best user experience, so we must have it enabled to make sure users get a nice looking pop-up in outlook. also we want to avoid using App Passwords.

PowerShell Monitoring script:

This script only monitors the Modern Auth status, and does not auto-remediate.

$creds = get-credential
Connect-MsolService -Credential $creds
$clients = Get-MsolPartnerContract -All
 
foreach ($client in $clients) { 
 $ClientDomain = Get-MsolDomain -TenantId $client.TenantId | Where-Object {$_.IsInitial -eq $true}
 Write-host "Logging into portal for $($client.Name)"
 $DelegatedOrgURL = "https://ps.outlook.com/powershell-liveid?DelegatedOrg=" + $ClientDomain.Name
 $ExchangeOnlineSession = New-PSSession -ConnectionUri $DelegatedOrgURL -Credential $credential -Authentication Basic -ConfigurationName Microsoft.Exchange -AllowRedirection
 Import-PSSession -Session $ExchangeOnlineSession -AllowClobber -DisableNameChecking
 
 $Oauth = Get-OrganizationConfig 
 
 if($Oauth.OAuth2ClientProfileEnabled -eq $false){ $ModernAuthState += "$($ClientDomain.name) has modern auth disabled"}
 
 Remove-PSSession $ExchangeOnlineSession
}

if(!$ModernAuthState){ $ModernAuthState = "Healthy"}
PowerShell auto-remediation script
$creds = get-credential
Connect-MsolService -Credential $creds 
$clients = Get-MsolPartnerContract -All
 
foreach ($client in $clients) { 
 $ClientDomain = Get-MsolDomain -TenantId $client.TenantId | Where-Object {$_.IsInitial -eq $true}
 Write-host "Logging into portal for $($client.Name)"
 $DelegatedOrgURL = "https://ps.outlook.com/powershell-liveid?DelegatedOrg=" + $ClientDomain.Name
 $ExchangeOnlineSession = New-PSSession -ConnectionUri $DelegatedOrgURL -Credential $credential -Authentication Basic -ConfigurationName Microsoft.Exchange -AllowRedirection
 Import-PSSession -Session $ExchangeOnlineSession -AllowClobber -DisableNameChecking
 
 $Oauth = Get-OrganizationConfig 
 
 if($Oauth.OAuth2ClientProfileEnabled -eq $false){ Set-OrganizationConfig -OAuth2ClientProfileEnabled $true }
 
 Remove-PSSession $ExchangeOnlineSession
}

And that’s it! Hope it helps and as always, Happy PowerShelling.

Monitoring with PowerShell Chapter 3: Monitoring MFA-Server and Office365 MFA status

We use both Azure MFA Server to secure our on-site resources, and Office365 MFA for our clients. To make sure we don’t have aggressors changing the MFA settings, or simply administrators forgetting to set-up MFA for clients we make sure that we alert on both.

The issue with monitoring the MFA server is that its a product Microsoft bought later on its in life. As such does not have a PowerShell module included, and it does not conform to the current Common Engineering Criteria.

To solve this, we add a DLL that exposes the functionality that we need, when they get a list of all users(starting with A, until Z).

Add-Type -Path "C:\Program Files\Multi-Factor Authentication Server\pfsvcclientclr.DLL"
$problem = [PfSvcClientClr.ConstructResult]::miscError;
$main = [PfSvcClientClr.PfSvcClient]::construct([PfSvcClientClr.ConstructTarget]::local, [ref] $problem);
$DLLModule = $main.GetType().GetMethod("getInterface").MakeGenericMethod([PfSvcClientClr.IPfMasterComposite]).Invoke($main, $null);
$users = $DLLModule.find_users_startsWith("a")
foreach($username in $users){
if($DLLModule.get_user_disabledBehavior("$username") -eq "succeed") { $UserOutput += "$($username) is set to succeed authentication without MFA `n" }
}
if(!$UserOutput) { $UserOutput = "Healthy" }

Next we will monitor the multi-factor authentication on the Office365 side. For this we will use the MSOL module and your partner credentials you’ve generated using this blog. The script can be used both to get information for all clients, or a single client. To demonstrate I’ve added both

All Clients script:

$ApplicationId       = "xxxxxx-xxxx-xxxx-xxxx-xxxxxxxx"
$ApplicationSecret   = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxx="
$RefreshToken        = "VeryLongRefreshToken"
$Tenantname          = "YourTenant.onmicrosoft.com"

$PasswordToSecureString = $ApplicationSecret  | ConvertTo-SecureString -asPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential($ApplicationId,$PasswordToSecureString)
$aadGraphToken = New-PartnerAccessToken -RefreshToken $refreshToken -Resource https://graph.windows.net -Credential $credential -TenantId $Tenantname
$graphToken =  New-PartnerAccessToken -RefreshToken $refreshToken -Resource https://graph.microsoft.com -Credential $credential -TenantId $Tenantname

Connect-MsolService -AdGraphAccessToken $aadGraphToken.AccessToken -MsGraphAccessToken $graphToken.AccessToken
$AllUSers = Get-MsolPartnerContract |  Get-MsolUser -all | select DisplayName,UserPrincipalName,@{N="MFA Status"; E={ 
if( $_.StrongAuthenticationRequirements.State -ne $null) {$_.StrongAuthenticationRequirements.State} else { "Disabled"}}}
foreach($User in $allusers | Where-Object{ $_.'MFA Status' -eq "Disabled"}){
$DisabledUsers += "$($User.UserPrincipalName) Has MFA disabled `n"
}
if(!$DisabledUsers){ $DisabledUsers = "Healthy"}

Single client script

$ApplicationId       = "xxxx-xxxxx-xxxxx-xxxxx-xxxxxx"
$ApplicationSecret   = "xxxxxxxxxxxxxxxxxxxxxxxxxxxx="
$RefreshToken        = "VeryLongStringHere"
$Tenantname          = "YourTenant.onmicrosoft.com"
$ClientTenantName    = "TheClientsTenant.onmicrosoft.com"
$PasswordToSecureString = $ApplicationSecret  | ConvertTo-SecureString -asPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential($ApplicationId,$PasswordToSecureString)
$aadGraphToken = New-PartnerAccessToken -RefreshToken $refreshToken -Resource https://graph.windows.net -Credential $credential -TenantId $Tenantname
$graphToken =  New-PartnerAccessToken -RefreshToken $refreshToken -Resource https://graph.microsoft.com -Credential $credential -TenantId $Tenantname

Connect-MsolService -AdGraphAccessToken $aadGraphToken.AccessToken -MsGraphAccessToken $graphToken.AccessToken
$AllUSers = Get-MsolPartnerContract -DomainName $ClientTenantName |  Get-MsolUser -all | select DisplayName,UserPrincipalName,@{N="MFA Status"; E={ 
if( $_.StrongAuthenticationRequirements.State -ne $null) {$_.StrongAuthenticationRequirements.State} else { "Disabled"}}}
foreach($User in $allusers | Where-Object{ $_.'MFA Status' -eq "Disabled"}){
$DisabledUsers += "$($User.UserPrincipalName) Has MFA disabled"
}
if(!$DisabledUsers){ $DisabledUsers = "Healthy"}

Using the Secure App Model to connect to microsoft partner resources

This is a quick and dirty blog, as I am quite busy editing our PowerShell Functions to use the secure app model before the deadline of august first.

Microsoft has announced that any Microsoft partner will need to start enforcing MFA on all accounts. This means you can no longer have an exclusion for accounts that run PowerShell scripts, or even use whitelisting. To learn more about this, I’d suggest to check out the Microsoft announcement here.

Because I know a lot of MSP’s and other service providers that will see this a major issue, I’ve hacked together a couple of quick scripts that can help you start using the new authentication model as easy as you are currently using credentials.

By running the script below. You will get 3 parts of the new credential object: the clientID, the ClientSecret, and the refresh token. you can use these to log into any of the Secure App Model endpoints. Save the script below and run it as follows:

Create-AzureADApplication.ps1 -ConfigurePreconsent -DisplayName "Partner Center Web App"

This script is originally from the Microsoft support center for partners which you can find here. I just combined the scripts to make sure you only have to enter your credentials twice: once to log into Azure and create the app, and the second time to log in with a user that will be used as the API service account.

<#
    .SYNOPSIS
        This script will create the require Azure AD application.
    .EXAMPLE
        .\Create-AzureADApplication.ps1 -ConfigurePreconsent -DisplayName "Partner Center Web App"

        .\Create-AzureADApplication.ps1 -ConfigurePreconsent -DisplayName "Partner Center Web App" -TenantId eb210c1e-b697-4c06-b4e3-8b104c226b9a

        .\Create-AzureADApplication.ps1 -ConfigurePreconsent -DisplayName "Partner Center Web App" -TenantId tenant01.onmicrosoft.com
    .PARAMETER ConfigurePreconsent
        Flag indicating whether or not the Azure AD application should be configured for preconsent.
    .PARAMETER DisplayName
        Display name for the Azure AD application that will be created.
    .PARAMETER TenantId
        [OPTIONAL] The domain or tenant identifier for the Azure AD tenant that should be utilized to create the various resources.
#>

Param
(
    [Parameter(Mandatory = $true)]
    [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..."

    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")
$password = New-AzureADApplicationPasswordCredential -ObjectId $app.ObjectId
$spn = New-AzureADServicePrincipal -AppId $app.AppId -DisplayName $DisplayName

if($ConfigurePreconsent) {
    $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 "Please approve consent form." -ForegroundColor Green
$PasswordToSecureString = $password.value | ConvertTo-SecureString -asPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential($($app.AppId),$PasswordToSecureString)
$TenantNameOrID = read-host "Please enter your tenantID or tenantname(e.g. Tenant.onmicrosoft.com)"
$token = New-PartnerAccessToken -Consent -Credential $credential -Resource https://api.partnercenter.microsoft.com -TenantId $TenantNameOrID

Write-Host "================ Secrets ================"
Write-Host "ApplicationId       = $($app.AppId)"
Write-Host "ApplicationSecret   = $($password.Value)"
write-host "RefreshToken        = $($token.refreshtoken)"
Write-Host "================ Secrets ================"
Write-Host "    SAVE THESE IN A SECURE LOCATION     "

To use the tokens in your scripts, use the Application ID as the username, the Application secret as the password, and the RefreshToken as the refreshtoken value requested by the modules.

$credential = Get-Credential
$refreshToken = 'Your-Refresh-Token-Value'

$aadGraphToken = New-PartnerAccessToken -RefreshToken $refreshToken -Resource https://graph.windows.net -Credential $credential
$graphToken =  New-PartnerAccessToken -RefreshToken $refreshToken -Resource https://graph.microsoft.com -Credential $credential

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

Update

There has been a small update: Microsoft has advised the following workaround for the Microsoft Exchange Module. This workaround creates a user that is excluded from all baselines policies as long as no interactive logon with the account is performed. For more information check this url

Import-Module AzureAD
Connect-AzureAD
$PasswordProfile = New-Object -TypeName Microsoft.Open.AzureAD.Model.PasswordProfile
$PasswordProfile.Password = "Password"
$PasswordProfile.ForceChangePasswordNextLogin = $false
$user = New-AzureADUser -DisplayName "New User" -PasswordProfile $PasswordProfile -UserPrincipalName "NewUser@contoso.com" -AccountEnabled $true
$adminAgentsGroup = Get-AzureADGroup -Filter "DisplayName eq 'AdminAgents'"
Add-AzureADGroupMember -ObjectId $adminAgentsGroup.ObjectId -RefObjectId $user.ObjectId

Please note that this is a workaround that should only be used for Exchange Access.

Deploy MFA to all Administrator accounts in all (Partner) tenants

As a MSP we tend to take over a lot of Microsoft tenants which therefore do not have their state of security in order. To make sure that we always use MFA for administration purposes we have an Azure Function running that deploys MFA for our administrator accounts. We do this by using our central phone number. That way MFA is always configured and we notice when an admin is trying to log in. We tend to use our delegated access as the normal administration purpose but some tasks require a local admin account in the customer’s portal.

We currently have the following running as an Azure Function. I’ve edited the script to allow anyone to run this directly. If you need help converting this to an Azure Function please see my other blog here.

The great thing about this script is that we completely negate the need for our helpdesk to configure MFA for the admin accounts. The script makes sure that MFA is completely prepared for them all.

Script breakdown

In the first part of the script we set our phone numbers, connect to the MSOLService and get all the Microsoft Partner Contracts we have permissions on

$MobileNumber = "+31611111111"
$AltNumber = "+31010101010"
Connect-MsolService
$ClientList = get-msolpartnerContract -All

Now that $ClientList contains all our clients we loop trough these to get all of our admin accounts.

foreach($Client in $ClientList){
$adminemails = Get-MsolRoleMember -TenantId $Client.tenantid -RoleObjectId (Get-MsolRole -RoleName "Company Administrator").ObjectId
$admins = $adminemails | get-msoluser -TenantId $client.tenantid

Now that we have our admin accounts in $Admins the next step is to create two objects – One for the Strong Authentication Requirements and another for the Strong Authentication Method.

$AuthReq = New-Object -TypeName Microsoft.Online.Administration.StrongAuthenticationRequirement
$AuthReq.RelyingParty = "*"
$AuthReq.State = "Enabled"
$AuthReq.RememberDevicesNotIssuedBefore = (Get-Date)
$AuthMethod = New-Object -TypeName Microsoft.Online.Administration.StrongAuthenticationMethod
$AuthMethod.MethodType = "TwoWayVoiceMobile"
$AuthMethod.IsDefault = $true
$AuthMethodObj = @($AuthMethod)

This makes sure that the user is completely pre-provisioned and does not need to go through the entire process of activating MFA first, which saves us valuable time. To finalize we run a for-each loop which enables all found Admin accounts with Multi Factor Authentication. It also sets the phone numbers to the ones we’ve entered as variables above.

foreach ($admin in $admins | Where-Object {$_.StrongAuthenticationMethods -eq $Null}) {
        Write-Host "Enabling MFA for $($admin.userprincipalname)" -ForegroundColor Green
        Set-MsolUser -UserPrincipalName $admin.userprincipalname -StrongAuthenticationRequirements $AuthReq -StrongAuthenticationMethods $AuthMethodObj -TenantId $client.tenantid  -MobilePhone $MobileNumber -AlternateMobilePhones $AltNumber
    }
}

And thats it! as a result the clients Secure Score will increase by about 50 points. Awesome! The complete script will look like this

Final Script

$MobileNumber = "+31611111111"
$AltNumber = "+31010101010"
Connect-MsolService
$ClientList = get-msolpartnerContract -All

foreach($Client in $ClientList){
$adminemails = Get-MsolRoleMember -TenantId $Client.tenantid -RoleObjectId (Get-MsolRole -RoleName "Company Administrator").ObjectId
$admins = $adminemails | get-msoluser -TenantId $client.tenantid
$AuthReq = New-Object -TypeName Microsoft.Online.Administration.StrongAuthenticationRequirement
$AuthReq.RelyingParty = "*"
$AuthReq.State = "Enabled"
$AuthReq.RememberDevicesNotIssuedBefore = (Get-Date)
$AuthMethod = New-Object -TypeName Microsoft.Online.Administration.StrongAuthenticationMethod
$AuthMethod.MethodType = "TwoWayVoiceMobile"
$AuthMethod.IsDefault = $true
$AuthMethodObj = @($AuthMethod)
foreach ($admin in $admins | Where-Object {$_.StrongAuthenticationMethods -eq $Null}) {
        Write-Host "Enabling MFA for $($admin.userprincipalname)" -ForegroundColor Green
        Set-MsolUser -UserPrincipalName $admin.userprincipalname -StrongAuthenticationRequirements $AuthReq -StrongAuthenticationMethods $AuthMethodObj -TenantId $client.tenantid  -MobilePhone $MobileNumber -AlternateMobilePhones $AltNumber
    }
}

Tada! as always, be careful, and happy PowerShelling!

Connecting to all O365 services at the same time in PowerShell (Including Installation and Teams)

I often use scripts that uses cmdlets from different modules so I can use all sorts of data sets when handling my Office365 administration tasks. The problem is that I often found myself connecting to a specific service such as the Exchange services, only to need the security center moments later – or the MSOL Module right after. When reinstalling my laptop I decided to not bump into this anymore, mostly to just get rid of my annoyance.

After a short search online I’ve found that there already was a bit of an example here on the Microsoft docs website. Unfortunately this guide is outdated and does not included the newer Teams Module. It also does not automatically find the correct SharePoint URL. As I am quite lazy and never want to look things up I also added the functionality to find the correct URL and to download the right modules. Currently it connects to the following services:

Services

ServiceModule Name
AzureAD (Graph)AzureAD Module
AzureAD(MSOL)MSOL Module (Legacy)
TeamsMicrosoftTeams(Beta)
Exchange OnlinePSSession to Exchange Endpoint
Security & CompliancePSSession to Protection endpoint
SharepointMicrosoft.Online.SharePoint.PowerShell
Skype for Business SkypeOnlineConnector

The function has only two options; a -Disconnect in case you want to end the sessions cleanly, and a -Credentials option for passing the credentials. Both are optional. To make sure the Function is always available to me I’ve added it both to my VSCode Profile and my PowerShell profile by starting a PowerShell prompt in both tools and entering the following one-liner:

New-Item -Path $profile -ItemType File

This creates a file in your profile path. Any code you paste here will always be loaded on startup. Because I use a lot of functions I do not like posting my code directly here, instead I save my modules to a specific folder, and have it look in that folder for any functions that I add.

$MyFunctions = "C:\Posh\Functions"
write-host "Loading Functions" -ForegroundColor Yellow
Get-ChildItem "$MyFunctions\*.ps1" | %{.$_}
write-host "Done Loading functions." -ForegroundColor Green 

This small piece of code looks in C:\Posh\Functions for all .PS1 files and loads these files as if I just run them as a script. This makes sure small functions which do not need to be an entire module on their own are loaded correctly. In C:\Posh\Functions I’ve saved the following script which I’ve called “Connect-Office365.ps1”

Script

function Connect-Office365 {
    param(
        [System.Management.Automation.PSCredential]$Credential,
        [switch]$Disconnect
    )
    if($Disconnect){
    Get-PSSession | remove-pssession
    Disconnect-SPOService
    Disconnect-AzureAD 
    exit }
write-host "Checking Prerequisites" -ForegroundColor Green
try{
    Import-Module AzureAD -ErrorAction stop 
} catch { 
        write-host "Could not find AzureAD Module. Installing."  -ForegroundColor Green
        install-module -Name AzureAD -Scope CurrentUser -force
        Import-Module AzureAD -ErrorAction stop 
        }
try{
     Import-Module MSOnline -ErrorAction stop
        } catch { 
     write-host "Could not find MSOL Module. Installing."  -ForegroundColor Green
     install-module -Name MSOnline -Scope CurrentUser -force
     Import-Module MSOnline -ErrorAction stop 
       }

 try{
     Import-Module MicrosoftTeams -ErrorAction stop
    } catch { 
     write-host "Could not find MSTeams Module. Installing"  -ForegroundColor Green
     install-module -Name MicrosoftTeams -Scope CurrentUser -force
     Import-Module MicrosoftTeams
      }
try{
    Import-Module Microsoft.Online.SharePoint.PowerShell -ErrorAction stop
    } catch { 
    write-host "Could not find Sharepoint Module. Installing"  -ForegroundColor Green
    Install-Module -Name Microsoft.Online.SharePoint.PowerShell -Scope CurrentUser -force
    Import-Module Microsoft.Online.SharePoint.PowerShell
    }
try{
    Import-Module SkypeOnlineConnector -ErrorAction stop
    } catch { 
    write-host "Could not find Skype For Business Module. Installing"  -ForegroundColor Green
    Invoke-WebRequest "https://download.microsoft.com/download/2/0/5/2050B39B-4DA5-48E0-B768-583533B42C3B/SkypeOnlinePowerShell.Exe" -OutFile "$($env:TEMP)/SkypeOnlinePowerShell.exe"
    Start-Process "$($env:TEMP)/SkypeOnlinePowerShell.exe" -ArgumentList "/install /quiet" -wait
    Import-Module "C:\Program Files\Common Files\Skype for Business Online\Modules\SkypeOnlineConnector"
     }
if(!$Credential){ $credential = Get-Credential }
write-host "Connecting to AzureAD Module" -ForegroundColor Green
Connect-AzureAD -Credential $credential
write-host "Connecting to MSOL Services" -ForegroundColor Green
Connect-msolservice -Credential $credential
write-host "Connecting to Teams Services" -ForegroundColor Green
Connect-MicrosoftTeams -Credential $credential
write-host "Connecting to Exchange Online" -ForegroundColor Green
$exchangeSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "https://outlook.office365.com/powershell-liveid/" -Credential $credential -Authentication "Basic" -AllowRedirection
Import-PSSession $exchangeSession -DisableNameChecking
write-host "Connecting to Security &amp; Compliance Center" -ForegroundColor Green
$SccSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "https://ps.compliance.protection.outlook.com/powershell-liveid/" -Credential $credential -Authentication "Basic" -AllowRedirection
Import-PSSession $SccSession -Prefix cc
Connect-MicrosoftTeams -Credential $Credential
write-host "Getting initial domain for Sharepoint Online"
$InitDomain = (Get-MsolDomain | Where-Object { $_.IsInitial -eq $true }).name
$InitDomain = $InitDomain.Split(".")
$SharepointDomain = $InitDomain[0]
write-host "Connecting to $($SharePointDomain) Sharepoint"
Connect-SPOService -Url "https://$($SharepointDomain)-admin.sharepoint.com" -credential $Credential
$sfboSession = New-CsOnlineSession -Credential $credential
Import-PSSession $sfboSession -AllowClobber
}

And that’s it! the code above is a bit rough around the edges but that is because it is just a simple function I use for my own personal preference. To use it, all you have to do is type Connect-Office365 and there you go 🙂

If you encounter any issues or have questions, let me know! and as always, Be careful and happy PowerShelling!

Running Office365 Powershell scripts cross tenant

When you manage multiple tenants or have a Microsoft Partner account to manage a lot of tenants it often gets annoying having to redeploy the same scripts to each tenant over and over. You can try using the MSOL commands but most of the times the scripts you are trying to run are Exchange, Skype for business, or Teams scripts, and you’re not able to run these just by using the Azure Active Directory Module for PowerShell.

The great thing is that Microsoft actually has a perfect solution for this by using your partner credentials cross tenant. You can change the session URL to match the specific client you are trying to manage, this means you can deploy scripts across all clients with relative ease when using the following scripts.

To get started on this, I’ll demonstrate how to get all mailbox sizes across your multiple tenants.
First, we’ll have to connect to Office365 by using the MSOL Microsoft Azure Active Directory Module for Windows PowerShell 

connect-msolservice
$clients = Get-MsolPartnerContract -All

When we’re connected, our client list is filled in $clients. This means we can loop trough each client and perform actions for them. In our case we’ll simply be gathering information, but you could also change settings such as disabling clutter, or enabling other functionality.

foreach ($client in $clients) { 
$ClientDomain = Get-MsolDomain -TenantId $client.TenantId | Where-Object {$_.IsInitial -eq $true}
    Write-host "Logging into portal for $($client.Name)"
    $DelegatedOrgURL = "https://ps.outlook.com/powershell-liveid?DelegatedOrg=" + $ClientDomain.Name
    $ExchangeOnlineSession = New-PSSession -ConnectionUri $DelegatedOrgURL -Credential $credential -Authentication Basic -ConfigurationName Microsoft.Exchange -AllowRedirection
    Import-PSSession -Session $ExchangeOnlineSession -AllowClobber -DisableNameChecking

Now that we’re connected to the Exchange PowerShell session for this client we can run whaterver Exchange commands we’d like. in our case, getting the mailboxes and respective size:

Get-Mailbox | Get-MailboxStatistics | Select-Object DisplayName, IsArchiveMailbox, ItemCount, TotalItemSize | format-table

When we’ve performed our commands, we do need to destroy our session as PowerShell sessions on Office365 can be rate limited.

Remove-PSSession $ExchangeOnlineSession
}

See that final curly bracket? With that one we close our for-each loop, Simply put we’re telling PowerShell that the loop is over, and it can continue with the next client in the list. If we put all of this together our script will look like this:

$credential = Get-Credential
connect-msolservice -Credential $credential
$clients = Get-MsolPartnerContract -All
foreach ($client in $clients) { 
    $ClientDomain = Get-MsolDomain -TenantId $client.TenantId | Where-Object {$_.IsInitial -eq $true}
    Write-host "Logging into portal for $($client.Name)"
    $DelegatedOrgURL = "https://ps.outlook.com/powershell-liveid?DelegatedOrg=" + $ClientDomain.Name
    $ExchangeOnlineSession = New-PSSession -ConnectionUri $DelegatedOrgURL -Credential $credential -Authentication Basic -ConfigurationName Microsoft.Exchange -AllowRedirection
    Import-PSSession -Session $ExchangeOnlineSession -AllowClobber -DisableNameChecking
    Get-Mailbox | Get-MailboxStatistics | Select-Object DisplayName, IsArchiveMailbox, ItemCount, TotalItemSize | format-table
    Remove-PSSession $ExchangeOnlineSession
}

So that’s it. you can easily replace the command for something as disabling clutter, focused inbox, enable auditing, etc. The great thing is that by combining this with an Azure Function gives your the ability to automatically apply your preffered settings to all clients. Happy PowerShelling!

Adding branding to the Office 365 portal

Hi all,

Today we’re going to change the branding of the Office365 portal. Something that helps clients identify that they are logging into the correct portal and are entering their usernames correctly. The great thing about this is you can have your own support information directly on the office365 portal.

you should make notes of some prerequisites before you start:
Company branding is a feature that is available only if you are using the Premium or Basic edition of Azure Active Directory, or are an Office 365 user. To check if branding is available you can try the following link

If you’ve checked that you can use branding, and you have your Office 365/Azure Admin account available you first log in on the portal at https://portal.microsoftonline.com, If you’ve previously used Azure you can directly log into Azure using https://portal.azure.com

After logging in browse to the admin panel and click on the “Azure Active Directory” link in the sidebar. This will open the Azure Portal. In case you have never signed into the Azure portal you will be forced to fill in some minor information such as e-mail address and physical address.

When logged into the Azure Portal you can click on Azure Active directory in the sidebar. You will not be presented with the following screen, Click on the area i’ve marked as yellow;

Screenshot01
After clicking, a new sidebar appears and you can select the option to “Configure Company Branding Now”. You can upload your images directly and also change the login tips and support information. After uploading your images you can log out of the portal. When logging in again you will see the branding as soon as you’ve entered your e-mail address or when you log into the portal using the URL https://outlook.com/tenantname(e.g. outlook.com/constoso.onmicrosoft.com). If you want users to access the webmail or portal using their own domain-name my advise would be to create a forward from webmail.clientname.com to https://outlook.com/tenantname.

An example of the branding can be found below;

A screenshot of the branding applied

A screenshot of the branding applied


Happy branding! 🙂