Skip to content

Instantly share code, notes, and snippets.

@darrenjrobinson
Last active July 19, 2019 00:06
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save darrenjrobinson/84d358a5a36ab53a04d79dd6e2c8c528 to your computer and use it in GitHub Desktop.
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/
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