Wednesday, September 18, 2019

PowerShell Scripts - Connect to Azure AD with MFA - reusing your existing connection

Situation: You are a very good user and have Multi-factor Authentication enabled for your account and you do things in Azure often. You want to connect to Azure without having to be prompted AGAIN for your MFA once you have done so for something else (e.g. the Exchange Online PowerShell module). You could also be creating a script that many people have to run many times a day and you don't want them re-authenticating over MFA every...single...time.


  1. Make sure you have the latest and greatest of the Exchange Online PowerShell Module (you can get it at
  2. Make sure you have the latest version of the AzureAD Module (I'm using at least version at the time of this writing)
  3. Make a script similar to this (or add to your script...this assumes the current user is the one that installed the EXO module):

        [string]$O365TenantId = "YOUR-TENANT-ID-GOES-HERE",
        [string]$O365ClientId = "1b730954-1685-4b74-9bfd-dac224a7b894",
        [string]$O365ResourceUrl = "",
        [uri]$O365URI = "urn:ietf:wg:oauth:2.0:oob",
        [string]$DefaultDomain = ""
    Import-Module AzureAD
    #Load the MFA module.
        $getChildItemSplat = @{
            Path = "$Env:LOCALAPPDATA\Apps\2.0\*\CreateExoPSSession.ps1"
            Recurse = $true
            ErrorAction = 'Stop'
            Verbose = $false
        $MFAPath = ((Get-ChildItem @getChildItemSplat | Sort-Object LastWriteTime -Descending | where-object {(Test-Path "$($_.PSParentPath)\Microsoft.Exchange.Management.ExoPowershellModule.dll") -eq $true} | Select-Object -First 1 | Select-Object -ExpandProperty fullname).Replace("\CreateExoPSSession.ps1", ""))
        . "$MFAPath\CreateExoPSSession.ps1" 3>$null
        Write-Verbose -Message "MFA module found in this folder - $MFAPath"
    } catch {
        Read-Host "MFA Module not found inside the local appdata folder for $ENV:USERNAME. If it is installed for another user already, run powershell as that user. To install the latest module, go to Press any key to exit"
    try {
        #Since the MFA module has a file that holds all of the authentication processes, let's use that to authenticate silently
        $IdentityPath = Get-ChildItem -Path "$MFAPath\Microsoft.IdentityModel.Clients.ActiveDirectory.dll" -Recurse -Verbose:$false | Sort-Object LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName
        Add-Type -Path $IdentityPath
        #Using the documentation for the AuthenticationContext, you need a resource url, client ID, URI (special), PromptBehavior (special), and UserIdentifier (special)
        #This means that we create a basic context pointing to a common login url for O365 but the AzureAD graph url as the resource url
        $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList "$O365TenantId"
        #Specifying Auto for this allows MFA to check for an existing token and use it if possible, otherwise prompt for MFA
        $MFAPromptBehavior = [Microsoft.IdentityModel.Clients.ActiveDirectory.PromptBehavior]::Auto
        #Using an identifier type of 2 is required where the displayId is required for token to pass (ie the email is the username)
        $AADcredential = [Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier]::new(("$ENV:USERNAME@$DefaultDomain"),2)
        #Get the authentication...let's hope the result is successful :)
        $authResult = $authContext.AcquireToken($O365ResourceUrl,$O365ClientId,$O365URI,$MFAPromptBehavior,$AADcredential)
        #Now that all the weird auth stuff has completed, connect to azuread using the resulting data
        Connect-azuread -TenantId $authResult.TenantId -AadAccessToken $authResult.AccessToken -AccountId $authResult.UserInfo.DisplayableId
    } catch {
        write-output "Failure to connect to O365/Azure MSOLService for user account management. Here is the error from MS: $_"
        Read-Host "Press any key to exit script"

I learned this from a smattering of blog posts that you may also find helpful (one I found after I figured all this out on my own sadly):

Thursday, April 11, 2019

Getting around the 50k limit for Azure / O365 Groups in Azure AD Sync

Problem: A giant group in your on-premises Active Directory does not sync through Azure Active Directory Sync

Source: Azure / O365 has a limit in the Azure AD Sync (AAD Sync) such that it ignores groups that are over 50k - I even found groups really close to this limit acted weird

Resolution: Script a solution that will take your on-prem group and create mirror groups with a maximum number of users in each so that all the miniature groups will sync and auto-add/remove users from the mirror groups, and you can use these groups in Azure.

Update 2019-06-17 - I have moved this script to a github repository to make updates easier. DO NOT WORRY - it is not some crazy-looking developer page...just a list of scripts from this site. Here is the url to my repository:

Azure CloudShell - Store Scripts Centrally for your Team

Problem: Cannot find or run scripts from Azure Cloud Shell that are centrally managed

Source: Azure Cloud Shell uses a secure linux vm with Powershell Core running on it and that shell has no access to any local resources (aka your file server) and you really can't connect to a git repo made by one of those fancy developers in your area - though you can clone one every time you open your cloud shell if that helps and may be good for their source control...I'm a sysadmin, I don't really get the git mantra except for backups. Also, many of your IT workers may attempt to access secure resources on insecure devices (aka their random personal tablet since you called them on a saturday and they don't want to VPN in using a laptop).


Assuming you have an AD group or Azure/O365 group that has your team members in it like we did:

  1. Log into with permissions to create stuff in your subscription
  2. Create a Resource Group for your team in the same location as your subscription
  3. Grant Reader permissions for the team to the Resource Group (use the IAM menu option to grant this)
  4. Create a storage account with the cheapest settings possible (see below)
    1. Set the Resource Group to the one you created for your team in step 2
    2. Give it a nice, short, simple name for the Storage Account Name
    3. Set the location to the same location as your resource group (NOTE: I had problems with East US 2; if your subscription is in East US 2, choose US EAST for this)
    4. Set Performance to Standard
    5. Leave Account kind for StorageV2 (general purpose v2)
    6. Set Replication to Locally Redundant Storage (LRS) if possible, it's the cheapest
    7. Set Access Tier to Cool
    8. Use the Review and Create button and create the resource...this will take a minute
  5. Once the storage account is created, navigate to the Storage Account, and grant "Reader and Data Access" for your team
    • If you receive errors about an unauthorized header, close your browser and re-login to Azure portal
  6. Inside the storage account, click Files and create a file share, call it something like "share", don't set a quota - every new user that accesses CloudShell and creates a profile will commit 5gb to this share so there's really no point in trying to limit unless you are scared of the hackers
  7. Click the "share" share so that it opens and click "Upload" and upload scripts and other resources here for your team
To access these resources, you need to have your each member on your team open a new Cloud Shell and, hopefully, they've never used this before. If they have, you will need to have them use a command to mount this new storage instead of their original storage (clouddrive mount -s SUBSCRIPTIONGUID -g "YOURRESOURCEGROUPNAME" -n "YOURSTORAGEACCOUNTNAME" -f "YOURSHARENAME")...beware, they may want to backup their stuff before they switch storage like that:

  1. Click the Cloud Shell button immediately to the right of the search bar in Azure Portal
  2. Click PowerShell as the language (you can change this later if you like)
  3. Click Advanced Settings on the right
  4. Make sure the settings all match what you have made (you may have to manually type the Share name)
  5. Click Connect/Yes/whatever allows it to go
Now that it is connected (you can verify if you use the Get-Clouddrive command and it gives you useful information), you can access those shared resources by typing this:
cd $HOME/clouddrive
Now your PowerShell window is located in the main directory of the can use dir to list the contents and even use tab completion to load your script name and it'll work. One downside: you cannot access local resources (e.g. local Active Directory or modules) when the script is accessed within cloud shell (you can actually map it as a network drive). The best way to handle this situation is to create a Hybrid Runbook Worker so that you can create Runbooks (aka scripts) and use the scripts stored in your shared location to call the runbooks and specify that they run on the Hybrid Runbook Worker.