Practical engineering guidance
Microsoft Entra hybrid join: reading dsregcmd instead of guessing
Hybrid join failures are diagnosed by a tool that prints eighty lines of state nobody reads. A script that parses it, names the failing phase and checks the service connection point.
Series: Microsoft Entra ID
A device that will not hybrid join produces one of the least helpful symptoms in Windows: a Conditional Access policy that requires a compliant or joined device blocks the user, and the device itself reports nothing at all. The user says “it worked yesterday”. The device says nothing, because nobody asked it properly.
dsregcmd /status asks it properly. The problem is that it answers with around eighty lines
across six sections, most of which are irrelevant to the current failure, and the two lines
that matter are in the middle.
The problem: three booleans decide what the device thinks it is
Every conversation about a join state should start with the same three fields from the Device State section:
| AzureAdJoined | DomainJoined | EnterpriseJoined | Meaning |
|---|---|---|---|
| YES | NO | NO | Microsoft Entra joined (cloud only) |
| YES | YES | NO | Microsoft Entra hybrid joined — the target state |
| NO | YES | NO | Domain joined, not yet registered with Entra ID |
| NO | YES | YES | Registered with the on-premises device registration service |
EnterpriseJoined being YES on a device you expected to be hybrid joined means it registered
against an on-premises federation service instead of Entra ID, which is a different problem
with a different fix.
Where the failure is recorded
When automatic registration fails on a domain-joined device, dsregcmd /status grows a
Diagnostic Data section containing a pre-join diagnostic. Two fields there end most
investigations:
- Error Phase —
pre-check,discover,authorjoin. This alone narrows the cause to a quarter of the problem space. - Client ErrorCode — the specific failure.
0x801c001dmeans the device could not read the registration service information from Active Directory: the service connection point.
The phases fail for characteristic reasons. pre-check failures usually mean no line of sight
to a domain controller. discover failures mean the service connection point is missing,
misconfigured or unreadable, or the device cannot reach
https://enterpriseregistration.windows.net or https://login.microsoftonline.com. auth and
join failures mean the device got far enough to be refused, which is a different
investigation — usually the user, the token, or the device object in the cloud.
Two event logs corroborate this:
- User Device Registration (
Microsoft-Windows-User Device Registration/Admin): event 304 records a failure at the join phase with its error code; event 307 records failure to look up the registration service in Active Directory. - AAD (
Microsoft-Windows-AAD/Operational): events 1006 and 1007 bracket a primary refresh token acquisition, with 1007 carrying the final error code. This is the log for “joined fine, but single sign-on does not work”.
The script
Read-only. It runs dsregcmd /status, parses it, applies the state matrix above, reads the
relevant event logs and optionally checks the service connection point in Active Directory.
<#
.SYNOPSIS
Reports Microsoft Entra device registration state and the reason a join failed.
.DESCRIPTION
Read-only. Parses dsregcmd /status into objects, determines the join state, extracts the
failing phase and error code from the diagnostic section, and collects the recent
registration and token events. Makes no changes to the device or the directory.
Run as the signed-in user for user-context fields (PRT, NgcSet). Run elevated for the
full diagnostic section. Neither dsregcmd /status nor this script alters registration
state; dsregcmd /leave does, and this script never calls it.
.PARAMETER EventHours
How far back to read the registration and token event logs. Default 24.
.PARAMETER CheckServiceConnectionPoint
Also read the service connection point from Active Directory. Requires the
ActiveDirectory module and line of sight to a domain controller.
.PARAMETER ReportPath
Optional JSON output path for attaching to a ticket.
.EXAMPLE
.\Get-HybridJoinState.ps1 -Verbose
.EXAMPLE
.\Get-HybridJoinState.ps1 -CheckServiceConnectionPoint -ReportPath C:\Temp\join.json
.NOTES
Read-only: this script makes no changes.
Output contains the device identity, the signed-in user and tenant identifiers.
Treat it as internal information and redact before sharing outside the organisation.
DeviceAuthStatus is present only on Windows 10 21H1 and later.
#>
#requires -Version 5.1
[CmdletBinding()]
param(
[ValidateRange(1, 720)]
[int]$EventHours = 24,
[switch]$CheckServiceConnectionPoint,
[ValidateScript({ Test-Path -Path (Split-Path -Path $_ -Parent) -PathType Container })]
[string]$ReportPath
)
$ErrorActionPreference = 'Stop'
function ConvertFrom-DsregOutput {
<#
.SYNOPSIS
Turns dsregcmd /status text into a hashtable of sections, each a hashtable of fields.
.NOTES
Section headers are delimited by lines of '+' characters; fields are 'Name : Value'.
Values containing colons (URLs, times) survive because only the first colon splits.
#>
[CmdletBinding()]
[OutputType([hashtable])]
param([Parameter(Mandatory)][AllowEmptyCollection()][AllowEmptyString()][string[]]$Text)
$sections = @{}
$current = 'Unknown'
for ($i = 0; $i -lt $Text.Count; $i++) {
$line = $Text[$i]
if ($line -match '^\s*\+-+\+\s*$') {
# The section name sits between two rules of '+' characters.
if ($i + 1 -lt $Text.Count -and $Text[$i + 1] -match '^\s*\|\s*(.+?)\s*\|\s*$') {
$current = $Matches[1].Trim()
if (-not $sections.ContainsKey($current)) { $sections[$current] = @{} }
}
continue
}
if ($line -match '^\s*([A-Za-z][A-Za-z0-9 _\-\.\(\)/]*?)\s*:\s*(.*)$') {
if (-not $sections.ContainsKey($current)) { $sections[$current] = @{} }
$sections[$current][$Matches[1].Trim()] = $Matches[2].Trim()
}
}
return $sections
}
function Get-JoinState {
<#
.SYNOPSIS
Applies the documented device state matrix to the three join booleans.
#>
[CmdletBinding()]
[OutputType([string])]
param([Parameter(Mandatory)][AllowNull()][hashtable]$DeviceState)
if ($null -eq $DeviceState) { return 'Unknown - no Device State section in dsregcmd output' }
$yes = { param($name) ($DeviceState[$name] -eq 'YES') }
$entra = & $yes 'AzureAdJoined'
$domain = & $yes 'DomainJoined'
$enterprise = & $yes 'EnterpriseJoined'
if ($entra -and $domain) { return 'Microsoft Entra hybrid joined' }
if ($entra -and -not $domain) { return 'Microsoft Entra joined' }
if ($domain -and $enterprise) { return 'On-premises DRS joined' }
if ($domain) { return 'Domain joined only - not registered with Entra ID' }
return 'Not joined'
}
Write-Verbose 'Running dsregcmd /status.'
$raw = & dsregcmd.exe /status 2>&1
if ($LASTEXITCODE -ne 0) { throw "dsregcmd exited with code $LASTEXITCODE." }
$sections = ConvertFrom-DsregOutput -Text @($raw)
# dsregcmd omits whole sections depending on state and Windows version. Treat an absent
# section as empty so a missing field reads as blank instead of ending the run.
function Get-Section {
param([Parameter(Mandatory)][hashtable]$Sections, [Parameter(Mandatory)][string]$Name)
if ($Sections.ContainsKey($Name)) { return $Sections[$Name] }
Write-Verbose "Section not present in dsregcmd output: $Name"
return @{}
}
$deviceState = Get-Section -Sections $sections -Name 'Device State'
$deviceDetails = Get-Section -Sections $sections -Name 'Device Details'
$diagnostics = Get-Section -Sections $sections -Name 'Diagnostic Data'
$sso = Get-Section -Sections $sections -Name 'SSO State'
$state = Get-JoinState -DeviceState $deviceState
Write-Verbose "Join state: $state"
# The diagnostic section only appears when automatic registration has failed.
$errorPhase = $diagnostics['Error Phase']
$clientErrorCode = $diagnostics['Client ErrorCode']
$phaseMeaning = switch ($errorPhase) {
'pre-check' { 'No line of sight to a domain controller, or the device is not eligible.' }
'discover' { 'Service connection point unreadable, or enterpriseregistration/login endpoints unreachable.' }
'auth' { 'The device reached Entra ID and authentication was refused.' }
'join' { 'Authentication succeeded and the join itself was refused.' }
default { if ($errorPhase) { 'Unrecognised phase; read the Server ErrorCode and Server Message fields.' } else { 'No failed registration recorded.' } }
}
$events = [System.Collections.Generic.List[object]]::new()
$since = (Get-Date).AddHours(-$EventHours)
$logs = @(
@{ Log = 'Microsoft-Windows-User Device Registration/Admin'; Ids = 201, 304, 305, 307 }
@{ Log = 'Microsoft-Windows-AAD/Operational'; Ids = 1006, 1007 }
)
foreach ($log in $logs) {
try {
Get-WinEvent -FilterHashtable @{ LogName = $log.Log; Id = $log.Ids; StartTime = $since } -ErrorAction Stop |
ForEach-Object {
$events.Add([pscustomobject]@{
TimeCreated = $_.TimeCreated
Log = $log.Log
Id = $_.Id
Level = $_.LevelDisplayName
Message = ($_.Message -split "`r?`n" | Select-Object -First 3) -join ' '
})
}
}
catch [System.Diagnostics.Eventing.Reader.EventLogNotFoundException] {
Write-Verbose "Log not present on this device: $($log.Log)"
}
catch {
# "No events were found" is the normal, healthy case and is not an error.
Write-Verbose "No matching events in $($log.Log): $($_.Exception.Message)"
}
}
$scp = $null
if ($CheckServiceConnectionPoint) {
try {
Import-Module ActiveDirectory -ErrorAction Stop
$configurationNamingContext = (Get-ADRootDSE -ErrorAction Stop).configurationNamingContext
$scpPath = "CN=62a0ff2e-97b9-4513-943f-0d221bd30080,CN=Device Registration Configuration,CN=Services,$configurationNamingContext"
$keywords = (Get-ADObject -Identity $scpPath -Properties keywords -ErrorAction Stop).keywords
$scp = [pscustomobject]@{
Found = $true
TenantId = ($keywords | Where-Object { $_ -like 'azureADId:*' }) -replace '^azureADId:', ''
TenantName = ($keywords | Where-Object { $_ -like 'azureADName:*' }) -replace '^azureADName:', ''
}
Write-Verbose "Service connection point found for tenant $($scp.TenantName)."
}
catch {
$scp = [pscustomobject]@{ Found = $false; TenantId = $null; TenantName = $null }
Write-Warning "Service connection point not readable: $($_.Exception.Message)"
}
}
$result = [pscustomobject]@{
Device = $env:COMPUTERNAME
CollectedUtc = (Get-Date).ToUniversalTime().ToString('yyyy-MM-dd HH:mm')
JoinState = $state
AzureAdJoined = $deviceState['AzureAdJoined']
DomainJoined = $deviceState['DomainJoined']
EnterpriseJoined = $deviceState['EnterpriseJoined']
DeviceId = $deviceState['DeviceId']
DeviceAuthStatus = $deviceDetails['DeviceAuthStatus']
AzureAdPrt = $sso['AzureAdPrt']
AzureAdPrtExpiryTime = $sso['AzureAdPrtExpiryTime']
ErrorPhase = $errorPhase
ClientErrorCode = $clientErrorCode
ServerErrorCode = $diagnostics['Server ErrorCode']
Interpretation = $phaseMeaning
ServiceConnectionPoint = $scp
Events = $events | Sort-Object TimeCreated -Descending
}
if ($ReportPath) {
$result | ConvertTo-Json -Depth 5 | Set-Content -Path $ReportPath -Encoding UTF8
Write-Verbose "Wrote $ReportPath"
}
if ($state -ne 'Microsoft Entra hybrid joined' -and $deviceState['DomainJoined'] -eq 'YES') {
Write-Warning "Device is domain joined but not hybrid joined. Phase: $(if ($errorPhase) { $errorPhase } else { 'not recorded' }). $phaseMeaning"
}
if ($sso['AzureAdPrt'] -eq 'NO') {
Write-Warning 'No primary refresh token. Single sign-on to cloud resources will fail even if the device is joined.'
}
$result
Reading the result
JoinState is hybrid joined and AzureAdPrt is NO. The join is fine; token acquisition is
not. Look at the AAD log events 1006 and 1007, and at the Previous Prt Attempt fields in the
SSO section. This is frequently a network or proxy problem affecting the user’s session rather
than the device.
ErrorPhase is discover and Client ErrorCode is 0x801c001d. The device cannot read
the service connection point. Run again with -CheckServiceConnectionPoint: either it is
missing, or the tenant it names is not the tenant you expect. In a multi-forest environment
each forest containing domain-joined computers needs its own.
DeviceAuthStatus reports the device is disabled or deleted. The cloud object was removed
or disabled while the device kept its certificate. The device must re-register; the cloud
object should not simply be re-enabled without understanding why it was disabled.
Nothing in Diagnostic Data at all. Automatic registration has not been attempted recently. Confirm the device has line of sight to a domain controller and that the scheduled task for automatic device join has run.
Why the script does not fix anything
The remediation for a stuck registration — dsregcmd /leave, then re-register — signs the
device out of cloud resources, discards the device certificate and the primary refresh token,
and is disruptive to the person using it. It is also frequently unnecessary: a missing service
connection point or a blocked endpoint is fixed centrally, and every affected device then
recovers on its own.
That is why this script reads and explains rather than acts. Fix the cause the report names,
then let automatic registration run. Reach for /leave only when the report shows the device
state itself is broken rather than the environment around it.
Verification and limits
The Device State matrix, the field names (AzureAdJoined, DomainJoined, EnterpriseJoined,
DeviceAuthStatus, AzureAdPrt, Error Phase, Client ErrorCode), the meaning of
0x801c001d, the User Device Registration and AAD event log channel names, events 304, 307,
1006 and 1007, and the service connection point distinguished name and its azureADId and
azureADName keywords were checked against current Microsoft documentation on 20 September
2026.
I verified the parser against captured dsregcmd /status text covering hybrid joined, domain
joined only and failed registration cases, confirming it returns the documented state for each.
It was not run on a production device, and no output from a real device appears in this
article. dsregcmd output differs between Windows versions: DeviceAuthStatus appears only on
Windows 10 21H1 and later, and a field the parser does not find is returned as empty rather
than causing a failure.
I could not confirm the exact message text of event 305 in the User Device Registration log, so the script collects it without interpreting it. Microsoft also publishes a device registration troubleshooter tool; where it is available, run it alongside this.
References
Reader feedback
Was this article useful?
No ratings yet. Be the first to rate this article.
