Introduction
I am Eunice Chinchilla, and I am a Premier Field Engineer at Microsoft focusing in the Identity space. In this guide you will find a description of the task at hand and the journey I went on to deliver a solution for my customer. Each section includes a high-level summary of what will be discussed and any pre-requisites to be able to try these in your own test environment. Once you are ready to start testing there are steps accompanied by descriptions and screenshots to walk you through the whole process I went through, including any hiccups I ran into. Please read the Problem Statement/Customer Story up next to provide you with background information and help you get started.
Problem Statement/Customer Story
I was working with a customer and they needed to identify why user lockouts were occurring. They had a hybrid environment with ADFS in place meaning the domains are federated. In this scenario, it is necessary to know if the lockout happened on-prem or on a O365 application. The plan of action was to look at the logs in both the Domain Controller and the ADFS server and compare them to the logs in Azure to see what we could find. Unfortunately, we were limited to only the ADFS server logs because there was no access to the Domain controller due to permissions. All the investigation was done solely on the ADFS server and Azure logs. The following solutions are a summary of the journey I went on with the purpose to find the most information possible related to the lockouts. I spent time testing in my own environment which is set up like theirs. In the end, it was narrowed down to the application used when the lockout occurred.
Approaches
First Approach
The first attempt in searching for the information needed was to look through the security logs and see what we could find.
Second Approach
Since looking through the logs resulted in very little information that was not enough for the customer to figure out where the lockouts where coming from, we resorted to Log Analytics and Azure Security Center to help find more specific and detailed information.
Third Approach
The customer did not have access to Azure Log Analytics and Azure Security Center which left us to explore the Azure Sign in Logs.
First Approach – PowerShell Script run on ADFS Server
Summary
Initially, I looked at the event logs in the ADFS server in my test environment. There were a few things I did before which are listed below in a checklist.
☐ Enable the appropriate logs: ‘Source AD FS Auditing Logs’
☐ Create test lockout events
Note: You can create these for testing purposes or just use the lockouts that already exist in your environment.
☐ Make sure you have PowerShell installed in your ADFS Server
☐ If you have multiple servers, make sure to enable remote access
Steps
1. First, make sure the ‘Source AD FS Auditing Logs’ are enabled in the ADFS server. This allows you to see the events with ID 411. Event 411 occurs when there is a failed token validation attempt (authentication attempts). In the event viewer, the IP address of the device used is provided. This can be useful for tracking the lockout.
Enabling the Source AD FS Auditing Logs
Open the Local Security Policy window from the Start menu on your server.
Once opened, you should see a view like the window below. Click on ‘Advanced Audit Policy Configuration.’
Click on ‘System Audit Policies – Local Group Policy Object.’
Navigate to the ‘Logon/Logoff’ category. You should now see all the subcategories available.
Select the ‘Audit Account Lockout’ subcategory.
A separate window will appear, and you can check the boxes for Success and Failure audit events. After having done this, the ‘Source AD FS Auditing Logs’ have successfully been enabled.
Under the ‘Explain’ tab you can find a detailed explanation of what happens when you enable these audit logs.
2. Skip this step if you already have lockouts in your environment. If not, you can create some account lockouts, as I did in my test environment.
Create test account lockout events
Open the ‘Local Security Policy’ window and click on ‘Account Policies.’
Click on ‘Account Lockout Policy.’
On the right-hand side are the security settings you can customize for the account lockouts. I set lower amounts of time so I could create multiple account lockout in shorter amounts of time.
Note: It is necessary to set the account lockout policy in the Domain Controller as well. Make sure the account lockout policy in the ADFS Server does not surpass the Domain Controller account lockout policy amounts of time and attempts. This will cause an error.
After setting the lockout policy on the appropriate server, start one of your Windows 10 joined Virtual Machines. From this VM, on any browser, you will attempt to log on to your account using any of your test user accounts.
Enter the wrong password on purpose as a simulation of an account lockout.
The ADFS splash page will not notify you when you’ve been locked out and will continue to display the view below. If your “invalid attempt logon” number was 2, repeat this process 3 times to ensure the lockout of the account occurred.
View the lockout event(s)
To verify the lockout happened open the Event Viewer.
Navigate to the ‘Security Logs’ under ‘Windows Logs.’
Here you can view the event(s) generated when the lockout(s) occurred.
You can also filter by error code (once you know which error code to look for). In this case, we can filter by error code 4625.
On the right-hand side, in the ‘Security’ window under ‘Actions’ select the ‘Filter Current Log…’ option.
Check every box for the ‘Event Level’ and enter the event ID. Click ‘Ok.’
Below are the results of the filtered events.
3. In your ADFS Server, open PowerShell ISE to run script that will be pulling the events related the lockout events. In this script we are querying for all the 411 events from the Source AD FS Auditing logs. The reason you want to filter for Event ID 411 is because this event gets created when there is a failed authentication attempt. The expected output is the Username, Activity ID, IP address, Date Created.
If you don’t have PowerShell ISE installed visit this site and resume at this step after installing successfully.
Running the Script
In your ADFS Server, Open PowerShell ISE and ‘Run as an Administrator.’
Copy the script below (Ctrl + C). It will be pasted onto the PowerShell ISE Script Window
#Get all the events with Event ID 411
$eventLogs = Get-EventLog -LogName Security -Source "AD FS Auditing" | Where-Object {$_.EventID -eq 411}
$eventList = @() #list that holds event objects
foreach ($event in $eventLogs)
{
$message = $event.Message
$emailRegEx = "\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\b"
$message -match $emailRegEx | Out-Null
$eventObject = New-Object -TypeName psobject #start off with an new object
#add the User Name
$eventObject | Add-Member -MemberType NoteProperty -Name Username -Value $Matches.Values
$activityIDRegEx = "[A-Za-z0-9]+-[A-Za-z0-9]+-[A-Za-z0-9]+-[A-Za-z0-9]+-[A-Za-z0-9]*"
$message -match $activityIDRegEx | Out-Null
#add the activity ID
$eventObject | Add-Member -MemberType NoteProperty -Name "Activity ID" -Value $Matches.Values
$IPAddressRegEx = "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]*"
$message -match $IPAddressRegEx | Out-Null
#add the IP address
$eventObject | Add-Member -MemberType NoteProperty -Name "IP Address" -Value $Matches.Values
#add the date the event happened
$eventObject | Add-Member -MemberType NoteProperty -Name "Date Created" -Value $event.TimeGenerated
$eventList += $eventObject #add each event object to a list
#Now, there is an event object that contains all the relevant information about the event.
}
#Group by user name and send the list of account info to a CSV file
$eventList | Sort-Object -Property "Username" | Out-File -FilePath "C:Enter your path here\lockouts.csv" -Encoding utf8
Paste the code into the Script window (Ctrl + V).
Note: If you have multiple ADFS Servers you can either run the script above on each server individually and have multiple csv output files or run the code below in PowerShell ISE from any ADFS server, but you must first enable remote access to the other ADFS servers. See link below on how to enable remote access to your other machines.
Multiple Server Script
foreach($s in $adfsServers){
#Get all the events with Event ID 411
$_xmlLockoutAdfs = "*[System[EventID=411]]"
$eventLogs = Get-WinEvent -ComputerName $s -LogName Security -FilterXPath $_xmlLockoutAdfs
$eventList = @() #list that holds event objects
foreach ($event in $eventLogs)
{
$eventObject = New-Object -TypeName psobject #start off empty object
$message = $event.Message
$emailRegEx = "\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\b"
$message -match $emailRegEx | Out-Null
#add the User Name
$eventObject | Add-Member -MemberType NoteProperty -Name Username -Value $Matches.Values
$activityIDRegEx = "[A-Za-z0-9]+-[A-Za-z0-9]+-[A-Za-z0-9]+-[A-Za-z0-9]+-[A-Za-z0-9]*"
$message -match $activityIDRegEx | Out-Null
#add the activity ID
$eventObject | Add-Member -MemberType NoteProperty -Name "Activity ID" -Value $Matches.Values
$IPAddressRegEx = "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]*"
$message -match $IPAddressRegEx | Out-Null
#add the IP address
$eventObject | Add-Member -MemberType NoteProperty -Name "IP Address" -Value $Matches.Values
#add the date the event happened
$eventObject | Add-Member -MemberType NoteProperty -Name "Date Created" -Value $event.TimeCreated
$eventList += $eventObject #add each event object to a list
#Now, there is an event object that contains all the relevant information about the event.
}
$eventList | Sort-Object -Property "Username" | Out-File -FilePath "C:Enter your path here\lockouts.csv" -Encoding utf8
}
4. Open the Event Viewer and filter for Events 411. This is useful to confirm the results from the script.
Second Approach – Log Analytics and Kusto Query Language on ADFS Server
Summary
Given the limited results of the event logs we decided to take another approach in the search of more detailed information. The event logs provided the Username, Activity ID, IP address, Date Created which was not enough information to conclude where exactly the lockouts where occurring. In hopes of finding more specific information we resorted to Azure Log Analytics. Through Log Analytics we were able to successfully collect the information needed such as the application being used when the lockout occurred. Below is a list of requirements to attempt this approach.
☐ Log Analytics must be set up with current environment and connected to the correct Virtual Machines.
☐ Have Azure Security Center set up in order to retrieve security events from the ADFS Server.
☐ Admin Credentials to Azure Active Directory to be able to access the logs
Steps
If you don’t have Log Analytics, below are some helpful links to get all set up.
Creating a workspace:
https://docs.microsoft.com/en-us/azure/azure-monitor/learn/quick-create-workspace
Collecting data from Azure Virtual Machines:
https://docs.microsoft.com/en-us/azure/azure-monitor/learn/quick-collect-azurevm
If you do not have Azure Security Center set up, below are some helpful links to get all set up.
Onboard Windows computers to Azure Security Center:
https://docs.microsoft.com/en-us/azure/security-center/quick-onboard-windows-computer
1. Navigate to your Log Analytics workspace -> Logs where you will be querying the security logs.
2. Copy (Ctrl + C) and Paste (Ctrl + V) the Kusto Query shown below and then run it by clicking on the ‘Run’ button.
SecurityEvent
| where EventID == 411
| project EventData, Computer, TimeGenerated, EventSourceName
| extend p = parse_xml(EventData)
| extend CorrelationId = tostring(parse_json(tostring(parse_json(tostring(p.EventData)).Data))[0])
| join (SigninLogs
| where Status.errorCode == "50140"
| project Status, UserDisplayName, UserPrincipalName, UserId, AppDisplayName, ClientAppUsed, AppId, IPAddress, CorrelationId
) on CorrelationId
| project EventSourceName, Status, UserDisplayName, UserPrincipalName, UserId, AppDisplayName, ClientAppUsed, AppId, IPAddress, TimeGenerated, CorrelationId
3. Once you see the results in the result window you can export the result to a CSV file where it will display the columns specified in the query. You can find the button in the top right-hand corner of the window. Make sure you set the time range to your desired time range before running the query.
Third Approach – Run PS Command on ADFS Server to retrieve Sign in Logs from Azure Active Directory
Summary
The customer did not have a Log Analytics workspace or Azure Security Center set up. After having a couple conversations, we concluded we had to move forward without the help of these services. This meant we were now limited to only viewing the Sign-in Logs from the portal. Since the portal does not allow to filter by error code, we can use a command in PowerShell to pull the right records.
The error code being used to filter by is error code 50140 which is occurs when there is an interruption that has occurred when attempting to authenticate. Since we know the authentication is happening in ADFS and not in Azure that is why we used this error code to help us pinpoint the accounts that were being locked out. An interruption in the Azure Sign-in Logs actually meant a lockout for us.
In order to attempt this approach, you must have all the below items checked off.
☐ Make sure you have PowerShell installed in your ADFS Server
☐ Verify the Azure AD Preview is installed
☐ Connect to Azure AD from PowerShell
Steps
If you don’t have PowerShell ISE installed visit this site and resume at this step after installing successfully.
1. In the ADFS Server, open PowerShell and run command with error code. This command is pulling from the Azure AD Sign-in logs. You can export your results to CSV file from PowerShell.
Verify the AzureADPreview Module is installed
In your ADFS Server, Open PowerShell ISE and ‘Run as an Administrator.’
In the Script window, type in the command below into the Script window and hit the ‘Enter’ key
Get-Module azureadpreview -ListAvailable
If you need to install the AzureADPreview module type the command below into the Script window and hit the ‘Enter’ key.
Install-module AzureADPreview -Verbose
Running the Command
In your ADFS Server, Open PowerShell ISE and ‘Run as an Administrator.’
Type in the command ‘Connect-AzureAD’ into the Script window and hit the ‘Enter’ key.
After hitting enter, the ‘Sign in to your account’ window appears. Enter your Azure credentials.
Now, your Azure account is connected to PowerShell, you’ve verified that you have the AzureADPreview module installed and the command can be run.
Copy the command below (Ctrl + C) and paste (Ctrl + V) onto the PowerShell ISE Script Window and hit the ‘Enter’ key.
Get-AzureADAuditSignInLogs -Filter "status/errorCode eq 50140"
Output to CSV file
To export the results onto a CSV file, run the command below.
Get-AzureADAuditSignInLogs -Filter "status/errorCode eq 50140" | Out-File -FilePath "\logs.csv"
How to handle the bug
During testing in my environment, the command ran smoothly, and I acquired the desired results with my test data. Unfortunately, in my customer’s environment we were getting the error below when running the command.
After researching, we discovered this was a bug in the AzureADPreview module, to which this command belongs to. Check out the GitHub blog below if you’d like to read more about the bug and check for updates.
https://github.com/Azure/azure-docs-powershell-azuread/issues/337
We reached out to the product group and it is an ongoing process meaning we would not get a resolution in a timely manner. Then we reached out to Microsoft support and worked with a Support Engineer on finding alternatives. Below is a script provided by him to help us filter and retrieve the sign in logs from Azure and send them to a CSV file for record keeping.
Get Sign-in Logs using the script below in PowerShell
# ------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# ------------------------------------------------------------
Import-Module Azure
# Replace with your tenantId or tenantDomain
$tenantId = ""
# Replace with your desired time ranges
$toDate = "{0:s}" -f (get-date).ToUniversalTime() + "Z"
$fromDate = "{0:s}" -f (get-date).AddDays(-7).ToUniversalTime() + "Z"
# You can add more filters here
$url = "https://graph.microsoft.com/beta/auditLogs/signIns?`$filter=createdDateTime ge $fromDate and createdDateTime le $toDate and status/errorcode eq 50140"
# By default, it saves the result to DownloadedReport_currentTime.csv. Change it to different file name as needed.
$now = "{0:yyyyMMdd_hhmmss}" -f (get-date)
$outputFile = "AAD_SignInReport_$now.csv"
###################################
#### DO NOT MODIFY BELOW LINES ####
###################################
Function Expand-Collections {
[cmdletbinding()]
Param (
[parameter(ValueFromPipeline)]
[psobject]$MSGraphObject
)
Begin {
$IsSchemaObtained = $False
}
Process {
If (!$IsSchemaObtained) {
$OutputOrder = $MSGraphObject.psobject.properties.name
$IsSchemaObtained = $True
}
$MSGraphObject | ForEach-Object {
$singleGraphObject = $_
$ExpandedObject = New-Object -TypeName PSObject
$OutputOrder | ForEach-Object {
Add-Member -InputObject $ExpandedObject -MemberType NoteProperty -Name $_ -Value $(($singleGraphObject.$($_) | Out-String).Trim())
}
$ExpandedObject
}
}
End {}
}
Function Get-Headers {
param( $token )
Return @{
"Authorization" = ("Bearer {0}" -f $token);
"Content-Type" = "application/json";
}
}
$clientId = "1b730954-1685-4b74-9bfd-dac224a7b894" # Azure Active Directory PowerShell clientId
$redirectUri = "urn:ietf:wg:oauth:2.0:oob"
$MSGraphURI = "https://graph.microsoft.com"
$authority = "https://login.microsoftonline.com/$tenantId"
$authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority
$authResult = $authContext.AcquireToken($MSGraphURI, $clientId, $redirectUri, "Always")
$token = $authResult.AccessToken
if ($token -eq $null) {
Write-Output "ERROR: Failed to get an Access Token"
exit
}
Write-Output "--------------------------------------------------------------"
Write-Output "Downloading report from $url"
Write-Output "Output file: $outputFile"
Write-Output "--------------------------------------------------------------"
# Call Microsoft Graph
$headers = Get-Headers($token)
$count=0
$retryCount = 0
$oneSuccessfulFetch = $False
Do {
Write-Output "Fetching data using Url: $url"
Try {
$myReport = (Invoke-WebRequest -UseBasicParsing -Headers $headers -Uri $url)
$convertedReport = ($myReport.Content | ConvertFrom-Json).value
$convertedReport | Expand-Collections | ConvertTo-Csv -NoTypeInformation | Add-Content $outputFile
$url = ($myReport.Content | ConvertFrom-Json).'@odata.nextLink'
$count = $count+$convertedReport.Count
Write-Output "Total Fetched: $count"
$oneSuccessfulFetch = $True
$retryCount = 0
}
Catch [System.Net.WebException] {
$statusCode = [int]$_.Exception.Response.StatusCode
Write-Output $statusCode
Write-Output $_.Exception.Message
if($statusCode -eq 401 -and $oneSuccessfulFetch)
{
# Token might have expired! Renew token and try again
$authResult = $authContext.AcquireToken($MSGraphURI, $clientId, $redirectUri, "Auto")
$token = $authResult.AccessToken
$headers = Get-Headers($token)
$oneSuccessfulFetch = $False
}
elseif($statusCode -eq 429)
{
# throttled request, wait for a few seconds and retry
Start-Sleep -5
}
elseif($statusCode -eq 403 -or $statusCode -eq 400 -or $statusCode -eq 401)
{
Write-Output "Please check the permissions of the user"
break;
}
else {
if ($retryCount -lt 5) {
Write-Output "Retrying..."
$retryCount++
}
else {
Write-Output "Download request failed. Please try again in the future."
break
}
}
}
Catch {
$exType = $_.Exception.GetType().FullName
$exMsg = $_.Exception.Message
Write-Output "Exception: $_.Exception"
Write-Output "Error Message: $exType"
Write-Output "Error Message: $exMsg"
if ($retryCount -lt 5) {
Write-Output "Retrying..."
$retryCount++
}
else {
Write-Output "Download request failed. Please try again in the future."
break
}
}
Write-Output "--------------------------------------------------------------"
} while($url -ne $null)
GitHub Reference to Scripts
I have posted all the scripts used in this guide onto GitHub. If any updates are made in the future you will find them here. Please access through the link below.
https://github.com/Eunice-Chinchilla/ADFSLockoutTracking/tree/master
Conclusion
After exploring different methods and going down different paths to achieve our goal of tracking where the ADFS Account Lockouts were coming from I was able to deliver multiple solutions to our customer. It was a learning experience which allowed me to readjust when presented with blockers or obstacles. Through my journey I did not have too many sources to guide me and therefore I documented the steps I went through. Hopefully this guide provided you with some helpful information.
Thanks for reading!
Disclaimer
The sample scripts are not supported under any Microsoft standard support program or service. The sample scripts are provided AS IS without warranty of any kind. Microsoft further disclaims all implied warranties including, without limitation, any implied warranties of merchantability or of fitness for a particular purpose. The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. In no event shall Microsoft, its authors, or anyone else involved in the creation, production, or delivery of the scripts be liable for any damages whatsoever (including, without limitation, damages for loss of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use of or inability to use the sample scripts or documentation, even if Microsoft has been advised of the possibility of such damages.