Last active
July 19, 2019 00:06
Star
You must be signed in to star a gist
Azure AD MFA Microsoft Identity Manager Management Agent. Associated blogpost https://blog.darrenjrobinson.com/an-azure-mfa-management-agent-for-user-mfa-reporting-using-microsoft-identity-manager/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
param ( | |
$Username, | |
$Password, | |
$OperationType, | |
[bool] $usepagedimport, | |
$pagesize, | |
$Credentials | |
) | |
$DebugFilePath = "C:\Program Files\Microsoft Forefront Identity Manager\2010\Synchronization Service\Extensions\AzureMFA\Debug\mfaUsersImport.txt" | |
# Debug Logging File | |
if (!(Test-Path $DebugFilePath)) { | |
$DebugFile = New-Item -Path $DebugFilePath -ItemType File | |
} | |
else { | |
$DebugFile = Get-Item -Path $DebugFilePath | |
} | |
"Starting Import as : " + $OperationType + " - " + (Get-Date) | Out-File $DebugFile -Append | |
"Paged Import : " + $usepagedimport | Out-File $DebugFile -Append | |
"PageSize : " + $pagesize | Out-File $DebugFile -Append | |
#Sample oAuth 2.0 Microsoft API Powershell AuthN/AuthZ Script | |
#The resource URI | |
$resource = "https://graph.microsoft.com" | |
#Your Client ID and Client Secret obainted when registering your WebApp | |
$clientid = "AzureADAppClientID" | |
$clientSecret = "AzureADAppSecret" | |
#Your Reply URL configured when registering your WebApp | |
$redirectUri = "https://localhost" | |
#Scope | |
$scope = "AuditLog.Read.All;Directory.Read.All" | |
Add-Type -AssemblyName System.Web | |
#UrlEncode the ClientID and ClientSecret and URL's for special characters | |
$clientIDEncoded = [System.Web.HttpUtility]::UrlEncode($clientid) | |
$clientSecretEncoded = [System.Web.HttpUtility]::UrlEncode($clientSecret) | |
$resourceEncoded = [System.Web.HttpUtility]::UrlEncode($resource) | |
$scopeEncoded = [System.Web.HttpUtility]::UrlEncode($scope) | |
#Refresh Token Path | |
$refreshtokenpath = "C:\Program Files\Microsoft Forefront Identity Manager\2010\Synchronization Service\Extensions\AzureMFA\refresh.token" | |
# Output Path | |
$outPath = "C:\Program Files\Microsoft Forefront Identity Manager\2010\Synchronization Service\Extensions\AzureMFA" | |
#Functions | |
# Function to popup Auth Dialog Windows Form for getting an AuthCode | |
Function Get-AuthCode { | |
Add-Type -AssemblyName System.Windows.Forms | |
$form = New-Object -TypeName System.Windows.Forms.Form -Property @{Width = 440; Height = 640 } | |
$web = New-Object -TypeName System.Windows.Forms.WebBrowser -Property @{Width = 420; Height = 600; Url = ($url -f ($Scope -join "%20")) } | |
$DocComp = { | |
$Global:uri = $web.Url.AbsoluteUri | |
if ($Global:uri -match "error=[^&]*|code=[^&]*") { $form.Close() } | |
} | |
$web.ScriptErrorsSuppressed = $true | |
$web.Add_DocumentCompleted($DocComp) | |
$form.Controls.Add($web) | |
$form.Add_Shown( { $form.Activate() }) | |
$form.ShowDialog() | Out-Null | |
$queryOutput = [System.Web.HttpUtility]::ParseQueryString($web.Url.Query) | |
$Global:output = @{ } | |
foreach ($key in $queryOutput.Keys) { | |
$output["$key"] = $queryOutput[$key] | |
} | |
$output | |
} | |
function Get-AzureAuthN ($resource) { | |
# Get Permissions (if the first time, get an AuthCode and Get a Bearer and Refresh Token | |
# Get AuthCode | |
$url = "https://login.microsoftonline.com/common/oauth2/authorize?response_type=code&redirect_uri=$redirectUri&client_id=$clientID&resource=$resourceEncoded&scope=$scopeEncoded" | |
Get-AuthCode | |
# Extract Access token from the returned URI | |
$regex = '(?<=code=)(.*)(?=&)' | |
$authCode = ($uri | Select-String -pattern $regex).Matches[0].Value | |
Write-Output "Received an authCode, $authCode" | |
#get Access Token | |
$body = "grant_type=authorization_code&redirect_uri=$redirectUri&client_id=$clientId&client_secret=$clientSecretEncoded&code=$authCode&resource=$resource" | |
$Authorization = Invoke-RestMethod https://login.microsoftonline.com/common/oauth2/token ` | |
-Method Post -ContentType "application/x-www-form-urlencoded" ` | |
-Body $body ` | |
-ErrorAction STOP | |
Write-Output $Authorization.access_token | |
$Global:accesstoken = $Authorization.access_token | |
$Global:refreshtoken = $Authorization.refresh_token | |
if ($refreshtoken) { $refreshtoken | Out-File "$($refreshtokenpath)" } | |
if ($Authorization.token_type -eq "Bearer" ) { | |
Write-Host "You've successfully authenticated to $($resource) with authorization for $($Authorization.scope)" | |
} | |
else { | |
Write-Host "Check the console for errors. Chances are you provided the incorrect clientID and clientSecret combination for the API Endpoint selected" | |
} | |
} | |
function Get-NewTokens { | |
# We have a previous refresh token. | |
# use it to get a new token | |
$refreshtoken = Get-Content "$($refreshtokenpath)" | |
# Refresh the token | |
#get Access Token | |
$body = "grant_type=refresh_token&refresh_token=$refreshtoken&redirect_uri=$redirectUri&client_id=$clientId&client_secret=$clientSecretEncoded" | |
$Global:Authorization = Invoke-RestMethod https://login.microsoftonline.com/common/oauth2/token ` | |
-Method Post -ContentType "application/x-www-form-urlencoded" ` | |
-Body $body ` | |
-ErrorAction STOP | |
$Global:accesstoken = $Authorization.access_token | |
$Global:refreshtoken = $Authorization.refresh_token | |
if ($refreshtoken) { | |
$refreshtoken | Out-File "$($refreshtokenpath)" | |
Write-Host "Updated tokens" | |
$Authorization | |
$Global:headerParams = @{'Authorization' = "$($Authorization.token_type) $($Authorization.access_token)" } | |
} | |
} | |
#AuthN | |
#Get-AzureAuthN ($resource) | |
# Refresh our tokens | |
Get-NewTokens | |
if (!$global:groups) { | |
# No Deltas. It's a Full Sync each time. But its GRAPH API and pretty snappy. | |
Try { | |
# Get All Enabled User's MFA Registration | |
$mfaRegistrationReportData = @() | |
$mfaRegistrations = Invoke-RestMethod -Method Get -Uri "https://graph.microsoft.com/beta/reports/credentialUserRegistrationDetails?`$filter=isMfaRegistered eq true" -Headers @{Authorization = "Bearer $($Global:accesstoken)" } | |
Write-Host -ForegroundColor Cyan "Retreived $($mfaRegistrations.value.count) records" | |
# Are there more MFA Registrations to get ? | |
$skipToken = $mfaRegistrations.'@odata.nextLink' | |
$mfaRegistrationReportData += $mfaRegistrations.value | |
# if more than 1000 events get the reset | |
if ($skipToken) { | |
[int]$i = 0 | |
do { | |
$results = Invoke-RestMethod -Method Get -Uri $skipToken -Headers @{Authorization = "Bearer $($Global:accesstoken)" } | |
$mfaRegistrationReportData += $results.value | |
Write-Host -ForegroundColor Cyan "Retreived another $($results.value.count) records" | |
$skipToken = $null | |
$skipToken = $results.'@odata.nextLink' | |
$i++ | |
} while ($skipToken) | |
Write-Host -ForegroundColor Blue "Total Records $($mfaRegistrationReportData.count)" | |
} | |
if ($mfaRegistrationReportData.count -gt 0) { | |
# chunk up the results for processing based on Page Size | |
$counter = [pscustomobject] @{ Value = 0 } | |
# $pageSize = 1000 | |
$global:groups = $mfaRegistrationReportData | Group-Object -Property { [math]::Floor($counter.Value++ / $pageSize) } | |
[int]$Global:groupsCount = $Global:groups.count | |
[int]$global:groupsProcessed = -1 | |
"$($Global:groupsCount) Groups to process" | Out-File $DebugFile -Append | |
$results = $null | |
$mfaRegistrationReportData = $null | |
} | |
# Process the first batch | |
# Process Users in Batches based off Page Size | |
# Get the next batch | |
$global:mfaBatch = $Global:groups[$global:groupsProcessed + 1] | |
# if we are at the end then set MoreToImport to False and quit | |
if (!$global:mfaBatch -or ($global:groupsProcessed + 1 -eq $Global:groupsCount)) { | |
"End of Import as : " + $OperationType + " - " + (Get-Date) | Out-File $DebugFile -Append | |
$global:MoreToImport = $false | |
break | |
} | |
# Process the batch | |
$usersProcessed = 0 | |
foreach ($user in $global:mfaBatch.Group) { | |
$obj = @{ } | |
$obj.add("ID", $user.id) | |
$obj.add("objectClass", "mfaUser") | |
$obj.add("userPrincipalName", $user.userPrincipalName) | |
$obj.add("isMfaRegistered", $user.isMfaRegistered) | |
$obj.add("isRegistered", $user.isRegistered) | |
$obj.add("isEnabled", $user.isEnabled) | |
$obj.add("isCapable", $user.isCapable) | |
$obj.add("authMethods", @($user.authMethods)) | |
if ($user.authMethods) {$obj.add("authMethodsCount", $user.authMethods.count)} else {$obj.add("authMethodsCount",0)} | |
if ($user.authMethods.Contains("appNotification")) {$obj.add("hasAuthApp",$true)} else {$obj.add("hasAuthApp",$false)} | |
# Pass to the MA | |
$obj | |
$usersProcessed++ | |
} | |
"Users Processed: $($usersProcessed)" | Out-File $DebugFile -Append | |
# More Groups to Process? Let the Sync Engine know | |
$global:groupsProcessed++ | |
if ($global:groupsProcessed -lt $Global:groupsCount) { | |
"Groups Processed: $($groupsProcessed + 1)" | Out-File $DebugFile -Append | |
$global:groupsProcessed + "Groups out of " + $Global:groupsCount + " Groups processed." | Out-File $DebugFile -Append | |
$global:MoreToImport = $true | |
break | |
} | |
} | |
catch { | |
"Error obtaining MFA Info from Azure AD" | Out-File $DebugFile -Append | |
} | |
} | |
# Global:groups Variable exists | |
# Process Users in Batches based off Page Size | |
# Get the next batch | |
$global:mfaBatch = $Global:groups[$global:groupsProcessed + 1] | |
# if we are at the end then set MoreToImport to False and quit | |
if (!$global:mfaBatch -or ($global:groupsProcessed + 1 -eq $Global:groupsCount)) { | |
"End of Import as : " + $OperationType + " - " + (Get-Date) | Out-File $DebugFile -Append | |
$global:MoreToImport = $false | |
break | |
} | |
# Process the batch | |
$usersProcessed = 0 | |
foreach ($user in $global:mfaBatch.Group) { | |
$obj = @{ } | |
$obj.add("ID", $user.id) | |
$obj.add("objectClass", "mfaUser") | |
$obj.add("userPrincipalName", $user.userPrincipalName) | |
$obj.add("isMfaRegistered", $user.isMfaRegistered) | |
$obj.add("isRegistered", $user.isRegistered) | |
$obj.add("isEnabled", $user.isEnabled) | |
$obj.add("isCapable", $user.isCapable) | |
$obj.add("authMethods", @($user.authMethods)) | |
if ($user.authMethods) {$obj.add("authMethodsCount", $user.authMethods.count)} else {$obj.add("authMethodsCount",0)} | |
if ($user.authMethods.Contains("appNotification")) {$obj.add("hasAuthApp",$true)} else {$obj.add("hasAuthApp",$false)} | |
# Pass to the MA | |
$obj | |
$usersProcessed++ | |
} | |
"Users Processed: $($usersProcessed)" | Out-File $DebugFile -Append | |
# More Groups to Process? Let the Sync Engine know | |
$global:groupsProcessed++ | |
if ($global:groupsProcessed -lt $Global:groupsCount) { | |
"Groups Processed: $($groupsProcessed + 1)" | Out-File $DebugFile -Append | |
$global:groupsProcessed + "Groups out of " + $Global:groupsCount + " Groups processed." | Out-File $DebugFile -Append | |
$global:MoreToImport = $true | |
break | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment