Registering Claims X-Ray in Entra ID Using PowerShell
Introduction
Most ADFS admins would probably know the Claims X-Ray web application from Microsoft, which can be used to troubleshoot SAML token issuance:
Although not officially supported, it is also possible to use Claims X-Ray with Azure Active Directory:
As Microsoft is pushing Azure AD customers to migrate applications from ADFS to AAD, this utility might become more useful than ever.
Claims X-Ray app registration through the Azure AD Portal is pretty straightforward. But what is more challenging, is doing the entire configuration with Microsoft Graph PowerShell SDK. As it took me an entire day to figure out some details, while struggling with several bugs in the PowerShell module, I have decided to publish my solution to this task. With only minor modifications, this guide can be used to register almost any SAML-based application to Azure AD using PowerShell.
App Registration
We will first need to install the Microsoft.Graph.Applications and Microsoft.Graph.Identity.SignIns PowerShell modules, including their dependencies:
Install-Module -Name Microsoft.Graph.Applications,Microsoft.Graph.Identity.SignIns -Scope AllUsers -Force
We can then connect to Azure Active Directory while specifying all permissions required by the registration process:
Connect-MgGraph -Scopes @(
'Application.ReadWrite.All',
'AppRoleAssignment.ReadWrite.All',
'DelegatedPermissionGrant.ReadWrite.All',
'Policy.Read.All',
'Policy.ReadWrite.ApplicationConfiguration'
)
We are now ready to register the Claims X-Ray application in Azure AD:
[string] $appName = 'Claims X-Ray'
[string] $appDescription = 'Use the Claims X-ray service to debug and troubleshoot problems with claims issuance.'
[string] $redirectUrl = 'https://adfshelp.microsoft.com/ClaimsXray/TokenResponse'
[hashtable] $infoUrls = @{
MarketingUrl = 'https://adfshelp.microsoft.com/Tools/ShowTools'
PrivacyStatementUrl = 'https://privacy.microsoft.com/en-us/privacystatement'
TermsOfServiceUrl = 'https://learn.microsoft.com/en-us/legal/mdsa'
SupportUrl = 'https://adfshelp.microsoft.com/Feedback/ProvideFeedback'
}
[Microsoft.Graph.PowerShell.Models.IMicrosoftGraphApplication] $registeredApp =
New-MgApplication -DisplayName $appName `
-Description $appDescription `
-Web @{ RedirectUris = $redirectUrl } `
-DefaultRedirectUri $redirectUrl `
-GroupMembershipClaims All `
-Info $infoUrls
The previous command would always fail when used with the -SignInAudience
and -IdentifierUris
parameters. These application properties thus need to be configured separately, making the application single-tenant:
Update-MgApplication -ApplicationId $registeredApp.Id `
-SignInAudience 'AzureADMyOrg' `
-IdentifierUris 'urn:microsoft:adfs:claimsxray'
Application Logo
It is time to configure the application logo. As the Claims X-Ray website only contains a logo in the SVG format, which is not supported by AAD, I had to first convert it to PNG:
The logo must be downloaded locally before it can be uploaded to AAD:
[string] $logoUrl = 'https://www.dsinternals.com/assets/images/claims-xray-logo.png'
[string] $tempLogoPath = New-TemporaryFile
Invoke-WebRequest -Uri $logoUrl -OutFile $tempLogoPath -UseBasicParsing
Due to a bug in Microsoft Graph PowerShell, the following command would fail:
Set-MgApplicationLogo -ApplicationId $registeredApp.Id -InFile $tempLogoPath
We thus need to upload the image to Azure AD by calling the raw Graph API:
Invoke-GraphRequest -Method PUT -Uri "https://graph.microsoft.com/v1.0/applications/$($registeredApp.Id)/logo" `
-InputFilePath $tempLogoPath `
-ContentType 'image/*'
Upon success, the temporary local copy of the logo can be deleted:
Remove-Item -Path $tempLogoPath
Service Principal
Now that the application itself is registered, we can now register the corresponding service principal, which will appear in the Enterprise Applications section of the Azure AD Portal:
[Microsoft.Graph.PowerShell.Models.IMicrosoftGraphServicePrincipal] $servicePrincipal =
New-MgServicePrincipal -DisplayName $appName `
-AppId $registeredApp.AppId `
-AccountEnabled `
-ServicePrincipalType Application `
-PreferredSingleSignOnMode saml `
-ReplyUrls $redirectUrl `
-Notes $appDescription `
-Tags 'WindowsAzureActiveDirectoryIntegratedApp','WindowsAzureActiveDirectoryCustomSingleSignOnApplication'
Token-Signing Certificate
One of the requirements for a functional relying party trust is a token-signing certificate. For the sake of simplicity, we can generate a self-signed one, that will be valid for 3 years:
[Microsoft.Graph.PowerShell.Models.IMicrosoftGraphSelfSignedCertificate] $tokenSigningCertificate =
Add-MgServicePrincipalTokenSigningCertificate -ServicePrincipalId $servicePrincipal.Id `
-DisplayName "CN=$appName AAD Token Signing" `
-EndDateTime (Get-Date).AddYears(3)
The result will look like this in Azure AD Portal:
Application Permissions
As we want the Claims X-Ray app to receive information about signed-in users, we need to delegate the User.Read permission:
[Microsoft.Graph.PowerShell.Models.IMicrosoftGraphServicePrincipal] $microsoftGraph =
Get-MgServicePrincipal -Filter "DisplayName eq 'Microsoft Graph'"
[Microsoft.Graph.PowerShell.Models.IMicrosoftGraphPermissionScope] $userReadScope =
$microsoftGraph.Oauth2PermissionScopes | Where-Object Value -eq 'User.Read'
Update-MgApplication -ApplicationId $registeredApp.Id -RequiredResourceAccess @{
ResourceAppId = $microsoftGraph.AppId
ResourceAccess = @(@{
id = $userReadScope.Id
type = 'Scope'
})
}
It would make sense to hide the corresponding consent prompt from end-users accessing the app:
We can therefore give the required consent on behalf of the entire AAD Tenant in advance:
[Microsoft.Graph.PowerShell.Models.IMicrosoftGraphOAuth2PermissionGrant] $adminConsent =
New-MgOauth2PermissionGrant -ClientId $servicePrincipal.Id `
-ConsentType AllPrincipals `
-ResourceId $microsoftGraph.Id `
-Scope $userReadScope.Value
This is how the results should look in the AAD Portal:
User Assignment
For users to see the application in the My Apps portal, they need to be assigned to the application. This is how we can assign ourselves to the app:
[Microsoft.Graph.PowerShell.Models.IMicrosoftGraphDirectoryObject] $currentUser =
Invoke-GraphRequest -Method GET -Uri 'https://graph.microsoft.com/v1.0/me'
[string] $defaultAppAccessRole = [Guid]::Empty
[Microsoft.Graph.PowerShell.Models.IMicrosoftGraphAppRoleAssignment] $appAssignment =
New-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId $servicePrincipal.Id `
-ResourceId $servicePrincipal.Id `
-AppRoleId $defaultAppAccessRole `
-PrincipalType User `
-PrincipalId $currentUser.Id
Note that we have not declared any custom roles for the application, so we had to reference the default app role ID of 00000000-0000-0000-0000-000000000000
.
The result can again be verified through the AAD Portal:
SAML Token Configuration
The built-in acct
and groups
claims are optional, so we need to explicitly enable them:
[Microsoft.Graph.PowerShell.Models.IMicrosoftGraphOptionalClaims] $optionalClaims = [Microsoft.Graph.PowerShell.Models.MicrosoftGraphOptionalClaims]::DeserializeFromDictionary(@{
Saml2Token = @(
@{ Name = 'acct' },
@{ Name = 'groups' }
)
})
Update-MgApplication -ApplicationId $registeredApp.Id -OptionalClaims $optionalClaims
Here is how the change will show up in the UI:
Contrary to what the documentation says, the email
and upn
do not need to be configured here to appear in SAML tokens. Even the groups
claim does not need to be specified if the default group identifier settings are sufficient.
It is also possible to define custom SAML claims for an application:
I have decided to map the AAD attributes to SAML claims as follows:
Claim Type | Value |
---|---|
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier | user.userprincipalname |
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name | user.userprincipalname |
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn | user.userprincipalname |
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress | user.mail |
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname | user.givenname |
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname | user.surname |
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/streetaddress | user.streetaddress |
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/locality | user.city |
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/postalcode | user.postalcode |
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/stateorprovince | user.state |
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/country | user.country |
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/mobilephone | user.mobilephone |
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/homephone | user.telephonenumber |
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/otherphone | user.facsimiletelephonenumber |
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/employeeid | user.employeeid |
http://schemas.microsoft.com/LiveID/Federation/2008/05/ImmutableID | user.onpremisesimmutableid |
http://schemas.microsoft.com/2012/01/requestcontext/claims/relyingpartytrustid | application.objectid |
Unfortunately, I have not found a way to configure these rules through the Graph API. Please let me know in case you were more successful than me.
As a workaround, we can override the application-specific claim issuance configuration by creating a Claims Mapping Policy and assigning it to the Claims X-Ray application:
[string] $allClaimsMapping = @'
{
"ClaimsMappingPolicy": {
"Version": 1,
"IncludeBasicClaimSet": "true",
"ClaimsSchema": [
{
"Source": "user",
"ID": "userprincipalname",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"
},
{
"Source": "user",
"ID": "userprincipalname",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"
},
{
"Source": "user",
"ID": "userprincipalname",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"
},
{
"Source": "user",
"ID": "mail",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
},
{
"Source": "user",
"ID": "givenname",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"
},
{
"Source": "user",
"ID": "surname",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"
},
{
"Source": "user",
"ID": "streetaddress",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/streetaddress"
},
{
"Source": "user",
"ID": "city",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/locality"
},
{
"Source": "user",
"ID": "postalcode",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/postalcode"
},
{
"Source": "user",
"ID": "state",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/stateorprovince"
},
{
"Source": "user",
"ID": "country",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/country"
},
{
"Source": "user",
"ID": "mobilephone",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/mobilephone"
},
{
"Source": "user",
"ID": "telephonenumber",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/homephone"
},
{
"Source": "user",
"ID": "facsimiletelephonenumber",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/otherphone"
},
{
"Source": "user",
"ID": "employeeid",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/employeeid"
},
{
"Source": "user",
"ID": "onpremisesimmutableid",
"SamlClaimType": "http://schemas.microsoft.com/LiveID/Federation/2008/05/ImmutableID"
},
{
"Source": "application",
"ID": "objectid",
"SamlClaimType": "http://schemas.microsoft.com/2012/01/requestcontext/claims/relyingpartytrustid"
}
],
"ClaimsTransformation": []
}
}
'@
[Microsoft.Graph.PowerShell.Models.IMicrosoftGraphClaimsMappingPolicy] $allClaimsPolicy =
New-MgPolicyClaimMappingPolicy -DisplayName 'Issue All Claims' -Definition $allClaimsMapping
New-MgServicePrincipalClaimMappingPolicyByRef -ServicePrincipalId $servicePrincipal.Id -OdataId "https://graph.microsoft.com/v1.0/policies/claimsMappingPolicies/$($allClaimsPolicy.Id)"
Unfortunately, there is currently no user interface for viewing/editing the policies:
Testing the Sign-In
We are finally ready to log into the Claims X-Ray application and test the SAML claim issuance. This can be done by visiting the My Apps portal:
Or we can simply run this PowerShell command, which will automatically open the Claims X-Ray application in the default browser:
Start-Process ('https://myapps.microsoft.com/signin/{0}?tenantId={1}' -f $servicePrincipal.AppId,$servicePrincipal.AppOwnerOrganizationId)
Limitations
- Because the Claims X-Ray app uses the
urn:microsoft:adfs:claimsxray
identifier, it can only be registered as a single-tenant app. - As the Claims X-Ray app is hardcoded with ADFS-specific token request relative URL, only the Identity Provider-Initiated Single Sign-On can be used.
- Unlike production applications, the Claims X-Ray does not validate the token-signing certificates.
- This article does not cover the assignment of a Conditional Access Policy, which could enforce MFA.
Fetching the New Objects
This is how we can list all Azure AD objects created by the PowerShell commands above:
Get-MgApplication -Filter "DisplayName eq 'Claims X-Ray'" | Format-List
Get-MgServicePrincipal -Filter "DisplayName eq 'Claims X-Ray'" | Format-List
Get-MgPolicyClaimMappingPolicy -Filter "DisplayName eq 'Issue All Claims'" | Format-List
End-to-End Script
To wrap things up, here is the full PowerShell script, concatenated from the code snippets above:
#Requires -Version 5
#Requires -Modules Microsoft.Graph.Applications,Microsoft.Graph.Identity.SignIns
# Note: The required modules can be installed using the following command:
# Install-Module -Name Microsoft.Graph.Applications,Microsoft.Graph.Identity.SignIns -Scope AllUsers -Force
# Connect to AzureAD
# Note: The -TenantId parameter is also required when using a Microsoft Account.
Connect-MgGraph -Scopes @(
'Application.ReadWrite.All',
'AppRoleAssignment.ReadWrite.All',
'DelegatedPermissionGrant.ReadWrite.All',
'Policy.Read.All',
'Policy.ReadWrite.ApplicationConfiguration'
)
# Register the application
[string] $appName = 'Claims X-Ray'
[string] $appDescription = 'Use the Claims X-ray service to debug and troubleshoot problems with claims issuance.'
[string] $redirectUrl = 'https://adfshelp.microsoft.com/ClaimsXray/TokenResponse'
[hashtable] $infoUrls = @{
MarketingUrl = 'https://adfshelp.microsoft.com/Tools/ShowTools'
PrivacyStatementUrl = 'https://privacy.microsoft.com/en-us/privacystatement'
TermsOfServiceUrl = 'https://learn.microsoft.com/en-us/legal/mdsa'
SupportUrl = 'https://adfshelp.microsoft.com/Feedback/ProvideFeedback'
}
[Microsoft.Graph.PowerShell.Models.IMicrosoftGraphApplication] $registeredApp =
New-MgApplication -DisplayName $appName `
-Description $appDescription `
-Web @{ RedirectUris = $redirectUrl } `
-DefaultRedirectUri $redirectUrl `
-GroupMembershipClaims All `
-Info $infoUrls
Update-MgApplication -ApplicationId $registeredApp.Id `
-SignInAudience 'AzureADMyOrg' `
-IdentifierUris 'urn:microsoft:adfs:claimsxray'
# Configure application logo
[string] $logoUrl = 'https://www.dsinternals.com/assets/images/claims-xray-logo.png'
[string] $tempLogoPath = New-TemporaryFile
Invoke-WebRequest -Uri $logoUrl -OutFile $tempLogoPath -UseBasicParsing
Invoke-GraphRequest -Method PUT -Uri "https://graph.microsoft.com/v1.0/applications/$($registeredApp.Id)/logo" `
-InputFilePath $tempLogoPath `
-ContentType 'image/*'
Remove-Item -Path $tempLogoPath
# Create the service principal
[Microsoft.Graph.PowerShell.Models.IMicrosoftGraphServicePrincipal] $servicePrincipal =
New-MgServicePrincipal -DisplayName $appName `
-AppId $registeredApp.AppId `
-AccountEnabled `
-ServicePrincipalType Application `
-PreferredSingleSignOnMode saml `
-ReplyUrls $redirectUrl `
-Notes $appDescription `
-Tags 'WindowsAzureActiveDirectoryIntegratedApp','WindowsAzureActiveDirectoryCustomSingleSignOnApplication'
# Generate a new token-signing certificate
[Microsoft.Graph.PowerShell.Models.IMicrosoftGraphSelfSignedCertificate] $tokenSigningCertificate =
Add-MgServicePrincipalTokenSigningCertificate -ServicePrincipalId $servicePrincipal.Id `
-DisplayName "CN=$appName AAD Token Signing" `
-EndDateTime (Get-Date).AddYears(3)
# Delegate the User.Read permission
[Microsoft.Graph.PowerShell.Models.IMicrosoftGraphServicePrincipal] $microsoftGraph =
Get-MgServicePrincipal -Filter "DisplayName eq 'Microsoft Graph'"
[Microsoft.Graph.PowerShell.Models.IMicrosoftGraphPermissionScope] $userReadScope =
$microsoftGraph.Oauth2PermissionScopes | Where-Object Value -eq 'User.Read'
Update-MgApplication -ApplicationId $registeredApp.Id -RequiredResourceAccess @{
ResourceAppId = $microsoftGraph.AppId
ResourceAccess = @(@{
id = $userReadScope.Id
type = 'Scope'
})
}
# Approve the User.Read permission on behalf of all tenant users
[Microsoft.Graph.PowerShell.Models.IMicrosoftGraphOAuth2PermissionGrant] $adminConsent =
New-MgOauth2PermissionGrant -ClientId $servicePrincipal.Id `
-ConsentType AllPrincipals `
-ResourceId $microsoftGraph.Id `
-Scope $userReadScope.Value
# Assign the application to the current user
[Microsoft.Graph.PowerShell.Models.IMicrosoftGraphDirectoryObject] $currentUser =
Invoke-GraphRequest -Method GET -Uri 'https://graph.microsoft.com/v1.0/me'
[string] $defaultAppAccessRole = [Guid]::Empty
[Microsoft.Graph.PowerShell.Models.IMicrosoftGraphAppRoleAssignment] $appAssignment =
New-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId $servicePrincipal.Id `
-ResourceId $servicePrincipal.Id `
-AppRoleId $defaultAppAccessRole `
-PrincipalType User `
-PrincipalId $currentUser.Id
# Configure optional claims
[Microsoft.Graph.PowerShell.Models.IMicrosoftGraphOptionalClaims] $optionalClaims = [Microsoft.Graph.PowerShell.Models.MicrosoftGraphOptionalClaims]::DeserializeFromDictionary(@{
Saml2Token = @(
@{ Name = 'acct' },
@{ Name = 'groups' }
)
})
Update-MgApplication -ApplicationId $registeredApp.Id -OptionalClaims $optionalClaims
# Create a new claims mapping policy
[string] $allClaimsMapping = @'
{
"ClaimsMappingPolicy": {
"Version": 1,
"IncludeBasicClaimSet": "true",
"ClaimsSchema": [
{
"Source": "user",
"ID": "userprincipalname",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"
},
{
"Source": "user",
"ID": "userprincipalname",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"
},
{
"Source": "user",
"ID": "userprincipalname",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"
},
{
"Source": "user",
"ID": "mail",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
},
{
"Source": "user",
"ID": "givenname",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"
},
{
"Source": "user",
"ID": "surname",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"
},
{
"Source": "user",
"ID": "streetaddress",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/streetaddress"
},
{
"Source": "user",
"ID": "city",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/locality"
},
{
"Source": "user",
"ID": "postalcode",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/postalcode"
},
{
"Source": "user",
"ID": "state",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/stateorprovince"
},
{
"Source": "user",
"ID": "country",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/country"
},
{
"Source": "user",
"ID": "mobilephone",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/mobilephone"
},
{
"Source": "user",
"ID": "telephonenumber",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/homephone"
},
{
"Source": "user",
"ID": "facsimiletelephonenumber",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/otherphone"
},
{
"Source": "user",
"ID": "employeeid",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/employeeid"
},
{
"Source": "user",
"ID": "onpremisesimmutableid",
"SamlClaimType": "http://schemas.microsoft.com/LiveID/Federation/2008/05/ImmutableID"
},
{
"Source": "application",
"ID": "objectid",
"SamlClaimType": "http://schemas.microsoft.com/2012/01/requestcontext/claims/relyingpartytrustid"
}
],
"ClaimsTransformation": []
}
}
'@
[Microsoft.Graph.PowerShell.Models.IMicrosoftGraphClaimsMappingPolicy] $allClaimsPolicy =
New-MgPolicyClaimMappingPolicy -DisplayName 'Issue All Claims' -Definition $allClaimsMapping
# Assign the claims mapping policy to the application
New-MgServicePrincipalClaimMappingPolicyByRef -ServicePrincipalId $servicePrincipal.Id -OdataId "https://graph.microsoft.com/v1.0/policies/claimsMappingPolicies/$($allClaimsPolicy.Id)"
# Open the Claims X-Ray app in a browser
# Note that it might take a minute for the application to become accessible.
Start-Process ('https://myapps.microsoft.com/signin/{0}?tenantId={1}' -f $servicePrincipal.AppId,$servicePrincipal.AppOwnerOrganizationId)