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.

Resolution:

  1. Make sure you have the latest and greatest of the Exchange Online PowerShell Module (you can get it at https://aka.ms/exopspreview)
  2. Make sure you have the latest version of the AzureAD Module (I'm using at least version 2.0.2.26 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):

    [CmdletBinding()]
    param(
        [string]$MFAPath,
        [string]$O365TenantId = "YOUR-TENANT-ID-GOES-HERE",
        [string]$O365ClientId = "1b730954-1685-4b74-9bfd-dac224a7b894",
        [string]$O365ResourceUrl = "https://graph.windows.net",
        [uri]$O365URI = "urn:ietf:wg:oauth:2.0:oob",
        [string]$DefaultDomain = "contoso.com"
    )
    Import-Module AzureAD
    #Load the MFA module.
    try{
        $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 https://aka.ms/exopspreview. Press any key to exit"
        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 "https://login.windows.net/$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"
        Exit
    }
    

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