SharePoint 2013: Troubleshooting Check Permissions – Windows auth
Update 11/24/19: Although the “Background” section below applies to all SharePoint versions and authentication types, this post is specific to Windows Authentication (NLTM or Kerberos) within SharePoint 2013. For SharePoint 2016, see this post: https://joshroark.com/sharepoint-2016-check-permissions-windows-auth/
For Forms-based authentication see this: https://joshroark.com/sharepoint-check-permissions-and-external-tokens-fba/
And for Trusted Provider (SAML) auth, see this: https://joshroark.com/sharepoint-check-permissions-and-external-tokens-adfs-saml-auth/
Why should you care?
Having “Check Permissions” fail to give you an accurate representation of user permissions can be annoying, but it doesn’t actually effect the users ability to log in and utilize those permissions, so that part is relatively minor. However, “Check Permissions” is not the only function that utilizes the External Token to determine user permissions. Pretty much any non-interactive function in SharePoint that must determine a users permission will use the External Token. Some examples include Alerts, Workflow, and SQL Server Reporting Services (SSRS) report subscriptions. Those functions will fail if permissions cannot be determined. For example, certain users may intermittently fail to receive Alert emails because the permission check failed for them.
Background:
The SharePoint “Check Permissions” function can be difficult to troubleshoot because it is dependent on the External Token, which can be refreshed two different ways and is cached within the content database. Add to that the fact that we usually only log to ULS when the token is actually refreshed (and not when a cached token is simply reused), and that custom code can be in play, and it’s a real joy to figure out.
The understanding of External Tokens and interactive vs non-interactive token refresh can be helpful here.
Interactive Refresh: The user logs in interactively, like they browse to a site.
Non-interactive Refresh: Some other process (“check permissions”, alerts, etc) must determine permissions non-interactively.
TokenTimeout (how long the external token is cached): 24 hours.
This can lead to intermittent “Check Permissions” problems, or may even make it look like you have some users that “work” and some that do not. I’ll explain:
- Usually, the Interactive Refresh works. The user logs in, and their interactive logon token contains their role claims. That is used to update their External Token with those claims, which is then cached for 24 hours.
- Within those 24 hours, if you run “Check Permissions”, since no refresh is needed, it simply checks the existing External Token and outputs the users permission based on that. If the previous refresh worked because it was Interactive, that can make it look like Check Permissions is “working” for that user when it’s really not. In this case, since no refresh was needed, pretty much nothing is logged in the ULS logs.
- After those 24 hours, if you run “Check Permissions” again, the External Token is expired, and a refresh is required. At this point, the Application Pool account must work through the claims providers, including any custom claims provider you may have (LDAPCP, PING, Okta, etc) and try to resolve the user account and pull back their claims, including role claims (groups).
This is usually where things go wrong. In the Windows authentication case, there can be AD permission and .NET problems, as covered in the rest of this blog post.
In the SAML authentication case, it’s usually a problem with the custom claims provider. Either there is a configuration problem with the custom claim provider, or possibly the custom claim provider does not even have the ability (like the code does not exist) to augment claims in a non-interactive scenario.
In any case, this results in a “bad” External Token (“bad” in that it contains no role claims), which will make “Check Permissions” fail to show the proper permission.
Keep in mind that the “bad” External Token is also cached. If the user logs in interactively, the External Token may not be updated at that time. It could remain that same “bad” External Token that the “Check Permissions” function came up with due to a previous failed non-interactive refresh.
The moral of this interactive vs non-interactive / token caching story is that unless you’re able to decode the External Token and understand when and how it was last refreshed, you might be chasing your tail.
Troubleshooting:
When troubleshooting authorization issues with these functions, the behavior can be intermittent. That’s due to the 24 hour caching of the External Token, which goes back to what I covered above, and is further covered in my colleague Tehnoons Post. Usually the user can refresh their own token when they log in interactively. Then it continues to work for non-interactive processes (like Check Permissions) for up to 24 hours. After the token expires, the non-interactive process is forced to refresh it. With Check Permissions, this is done by the App Pool account. With timer jobs like Alerts and Workflow, it will be the account running the SharePoint Timer service.
First, go take a look at the SharePoint ULS logs that cover a reproduction of the problem. You may see errors like this:
w3wp.exe (0x30C0) 0x1FB4 SharePoint Foundation Claims Authentication g7m8 Medium SPClaimsAuthRoleProvider.GetRolesForUserBestEffort() failed to load groups for ‘0#.w|contoso\User1’: System.DirectoryServices.AccountManagement.PrincipalOperationException: There is no such object on the server. —> System.DirectoryServices.DirectoryServicesCOMException: There is no such object on the server.
at System.DirectoryServices.DirectoryEntry.Bind(Boolean throwIfFail)
at System.DirectoryServices.DirectoryEntry.Bind()
at System.DirectoryServices.DirectoryEntry.RefreshCache()
at System.DirectoryServices.AccountManagement.ADStoreCtx.LoadDirectoryEntryAttributes(DirectoryEntry de)
Or, an error like this:
w3wp.exe (0x2F40) 0x2C7C SharePoint Foundation Claims Authentication g7m8 Medium SPClaimsAuthRoleProvider.GetRolesForUserBestEffort() failed to load groups for ‘0#.w|contoso\User1’: System.DirectoryServices.AccountManagement.PrincipalOperationException: An error (1301) occurred while enumerating the groups. The group’s SID could not be resolved.
at System.DirectoryServices.AccountManagement.SidList.TranslateSids(String target, IntPtr[] pSids)
at System.DirectoryServices.AccountManagement.SidList..ctor(SID_AND_ATTR[] sidAndAttr)
at System.DirectoryServices.AccountManagement.AuthZSet..ctor(Byte[] userSid, NetCred credentials, ContextOptions contextOptions, String flatUserAuthority, StoreCtx userStoreCtx, Object userCtxBase)
at System.DirectoryServices.AccountManagement.ADStoreCtx.GetGroupsMemberOfAZ(Principal p)
at System.DirectoryServices.AccountManagement.UserPrincipal.GetAuthorizationGroupsHelper()
at Microsoft.SharePoint.Administration.Claims.SPClaimsAuthRoleProvider.<>c__DisplayClass4.<GetRolesForUserBestEffort>b__0()
at Microsoft.SharePoint.Utilities.SecurityContext.RunAsProcess(CodeToRunElevated secureCode)
at Microsoft.SharePoint.Administration.Claims.SPClaimsAuthRoleProvider.GetRolesForUserBestEffort(String username).
Take this outside of SharePoint:
Another problem with troubleshooting “Check Permissions” is that it calls some .NET methods to resolve the user (FindByIdentity) and enumerate AD group membership (GetAuthorizationGroups). If those methods fail, the error is not displayed in the SharePoint UI, and in some cases, not even logged to ULS.
However, we can use PowerShell to call those exact same methods and reproduce the error outside of SharePoint. See the “#EnumerateGroupMembership” script in the “Scripts” section below. It replicates the .NET methods that “Check Permissions” uses. The script does not call anything SharePoint-related, so it can be run and tested from just about any machine. However, for the results to be most applicable to your “Check Permissions” / External Token problem, it should be run from the SharePoint WFEs.
Possible Causes:
1. Poor error handling for the GetAuthorizationGroups method in .NET Framework version 4.6.1 and lower.
2. Regression introduced in 15.0.4659.1000 (October 2014 CU for SharePoint 2013) that causes group membership lookup to fail in cross-domain trust scenarios.
3. If your SharePoint servers are Windows Server 2008 R2, you could have the issue defined here: http://support.microsoft.com/kb/2830145
4. Improper permissions on the default Users or Computers containers in Active Directory.
By default NT Authority\Authenticated Users has Read permission to these containers, so every account should have it. To check those permissions, you can use the DSACLS command like so:
dsacls “CN=Computers,DC=contoso,DC=com”
dsacls “CN=Users,DC=contoso,DC=com”
5. The app pool and / or timer service account does not have proper AD permissions to some Organizational Units (OUs) that contain some of the groups.
To identify problem OUs, you can run the #UserInfoFromAD PowerShell script from the “Scripts” section below. You may notice that groups from certain OUs fail to resolve.
6. The web app was converted from Classic to Claims authentication. The Account Names for the AD groups still show in Classic notation, which is not valid for a Claims site. It shows “Contoso\SecGroup” instead of “c:0+.w|<The group SID>”.
7. The group in question was deleted in Active Directory and then recreated with the same name and membership.
SharePoint uses the groups SID, not the name to determine group identity. Since the new group would have a new SID, SharePoint does not see it as the same group.
8. The user is a member of a group that was migrated with SID History from another domain.
The old domain is no longer available so the SID within SID History for that group cannot be resolved.
Please keep in mind that the “problem group” or groups are not necessarily the groups being used to give the users permission to the SharePoint site. They can be any groups the user is a member of. The .NET method that the SharePoint “Check Permissions” function calls to enumerate group memberships (GetAuthorizationGroups) tries to resolve all the SIDs within SID History for every group membership. If a single SID fails, the entire method fails, which results in SharePoint thinking the user is not a member of any groups. –> Update: This last sentence is only true on .NET version lower than 4.6. See Cause and Resolution #1.
9. The user exists within a complex Active Directory environment with multiple trusts and group memberships spanning those trusts.
Resolutions that correspond with the Causes above:
1. Install .NET Framework update 4.6.2.
Notes:
- .NET Framework 4.6 is supported for SharePoint 2013 as long as you have SP1 installed.
- You can check which version of .NET Framework you have installed using steps here.
- It’s important that you upgrade .NET on every SharePoint server in the farm.
2. Install January 2016 Cumulative Update (CU) for SharePoint 2013 (15.0.4787.1000) or a higher build.
3. Install the hotfix for KB 2830145 on your Win 2008 R2 SharePoint servers.
4. Give your application pool and Farm service accounts at least Read permission to the default Users and Computers containers in AD.
5. Give your application pool and Farm service accounts at least Read permission to the problem OUs. Also, see Resolution #1. Better error handling within the GetAuthorizationGroups method allows us to get enough information about the groups (we really only need the SID) within the restricted OUs for this to work.
6. Remove the group from site permissions and then add it back. You should see the account name in Claims notation when it’s added back. If you have a large number of groups in this state or across a number of site collections, you may choose to employ a PowerShell solution to do this.
7. You can either delete the group from the site and add it back (the new group with new SID would then be added), or you can use something like the SPFarm.MigrateGroup method to migrate the old group SharePoint permissions to the new one.
8. See Resolution #1 and #2. You better be at or above .NET 4.6.2, and SharePoint 2013 build 15.0.4787.1000. Those fixes have SID History implications as well. And try setting ClaimsAuthRoleProviderSidHistorySafeMode to True. That People Picker property implements different logic to resolve group SIDs when SID History is in play.
$w = Get-SPWebApplication <Your Web App Here>
$w.PeoplePickerSettings.ClaimsAuthRoleProviderSidHistorySafeMode = $true
$w.Update()
If you already have the above or later .NET Framework update and SharePoint CU installed, and have set ClaimsAuthRoleProviderSidHistorySafeMode to True, and it still fails, then we will have to identify the problem groups and then remove the problem SIDs within SID History for them.
Log on to a domain controller within the domain that SharePoint exists in and run the #ResolveSIDHistoryPerUser PowerShell script found below in the “Scripts” section. Just supply your identified problem user as the value for $user and run it. The log file it creates should indicate which SIDs could not be resolved along with the corresponding groups.
If you would like to try to identify all of the groups in the domain that may have problematic SID History, you can try the #ResolveSIDHistoryForAllGroups script also located below.
9. Again, see Resolution #1. That fixes a lot of different scenarios. You can also make the switch to some custom People Picker logic that should work better across domain boundaries by setting the ClaimsAuthRoleProviderUsesGlobalCatalog property to True.
$w = Get-SPWebApplication <Your Web App Here>
$w.PeoplePickerSettings.ClaimsAuthRoleProviderUsesGlobalCatalog = $true
$w.Update()
Scripts:
This PowerShell script will do the user principal and group membership lookup just like SharePoint 2013 Check Permissions does. You should note that this only calls methods from System.DirectoryServices. It does not call any SharePoint APIs. If this script fails for the user you’re trying to do “Check Permissions” on, then you have effectively reproduced the problem outside of SharePoint.
Please note that this script is designed to throw the exception that “Check Permissions” swallows. If you run it and it throws an error, that doesn’t mean that there’s something wrong with the script or that you’re doing it wrong. It usually means that you just reproduced the error that you should be focused on.
#################### SCRIPT EnumerateGroupMembership ################################################
#EnumerateGroupMembership
#Authors: JFrick; Joroar
#Date 8/11/14
#Just set the following three variables first. You will be prompted to enter the password for $SearchAsUser
$AccountUPN = "user1@fabrikam.com" #this is the user you're trying to enumerate group membership for in UPN format
$AccountUserDomain = "fabrikam.com" #this is the domain name for the user you are looking up ($AccountUPN) in FQDN format
$SearchAsUser = "contoso\AppPoolAccount" #this is the user doing the lookup -- typically this will be the app pool account
#You shouldn't need the change anything else below here
$password = Read-Host "Enter the password for the user doing the lookup" -AsSecureString
$password = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password))
[void][System.Reflection.Assembly]::LoadWithPartialName("System.DirectoryServices.AccountManagement")
$context = new-object System.DirectoryServices.AccountManagement.PrincipalContext([System.DirectoryServices.AccountManagement.ContextType]::Domain,$AccountUserDomain,$SearchAsUser,$password)
$userPrincipal = "" #Set this to blank so that subsequent runs don't produce misleading results
$userPrincipal = [System.DirectoryServices.AccountManagement.UserPrincipal]::FindByIdentity($context,$AccountUPN)
If(!$userPrincipal)
{Write-host "User was not found. Check your value for the AccountUPN variable. Current value is " $AccountUPN -ForegroundColor red}
else{
Write-host "User Info" -ForegroundColor red
$userPrincipal | select samaccountname, givenname, surname, displayname, distinguishedname, sid
Write-host "==============================================" -ForegroundColor red
$groups = "" #Set this to blank so that subsequent runs don't produce misleading results
$groups = $userPrincipal.GetAuthorizationGroups()
Write-host "Group Info" -ForegroundColor red
$groups | select samaccountname, DistinguishedName, sid, issecuritygroup, groupscope}
#################### SCRIPT ################################################
There are a few errors I’ve seen running this script.
If you see “Exception calling “FindByIdentity” with “2” argument(S): “The specified directory service attribute or value does not exist.” Then you are likely running into Cause #4 above.
If you see “Exception calling “GetAuthorizationGroups” with “0” argument(s): “An error (1301) occurred while enumerating the groups. The group’s SID could not be resolved”, then it could be either Cause #3 or Cause #8 above.
To check for cause #3, you can test by running the following PowerShell to manually resolve the SIDs:
$sid = New-Object System.Security.Principal.SecurityIdentifier("S-1-18-2")
$SID.Translate([System.Security.Principal.NTAccount])
$sid = New-Object System.Security.Principal.SecurityIdentifier("S-1-18-1")
$SID.Translate([System.Security.Principal.NTAccount])
— They should resolve to “Service asserted identity” and “Authentication authority asserted identity”
If you see “An error occurred while enumerating through a collection: An error occurred while enumerating the groups. The group could not be found..” Then it could be Cause #5 or Cause #8.
#################### SCRIPT UserInfoFromAD ################################################
#UserInfoFromAD
#Author: Joroar
#Date: I forget
#Outputs AD values for a user. Then tries to resolve all groups the user is a member of.
#The idea is to identify "bad" groups, ie: ones the app pool account doesn't have permission to.
#Just supply $userName for the problem user.
$userName = "user1"
$logfile = "c:\temp\User1-ADGroups.txt"
$account = ([adsisearcher]"samAccountName=$($userName)").FindOne()
"SamAccountName: " + $account.Properties.samaccountname | out-file $logfile -append
"DisplayName: " + $account.Properties.displayname | out-file $logfile -append
"UPN: " + $account.Properties.userprincipalname | out-file $logfile -append
"DN: " + $account.Properties.distinguishedname | out-file $logfile -append
"Created: " + $account.properties.whencreated | out-file $logfile -append
"Last Change: " + $account.properties.whenchanged | out-file $logfile -append
"" | out-file $logfile -append
$groupDNs = $account.Properties.memberof
foreach($groupDN in $groupDNs)
{
"Group DN: " + $groupDN | out-file $logfile -append
$groupName = ([adsisearcher]"distinguishedName=$($groupDN)").FindOne().Properties.samaccountname
if($groupName)
{"Group Resolved to: " + $groupName | out-file $logfile -append}
else {"ERROR!! Group lookup failed for " + $groupDN | out-file $logfile -append}
"" | out-file $logfile -append
}
#################### SCRIPT ################################################
#################### SCRIPT ResolveSIDHistoryPerUser ################################################
# ResolveSIDHistoryPerUser
# Author: Joroar
# Date: 8/11/14
# resolve sids in sid history for all group user is a member of
# the only input you need to supply is the value for $user. It should be the sAMAccountName ex: "User1"
# The output goes to a log file called "<TheUserName>-GroupSidHistory.txt", located in the logged-on users documents folder
import-module activedirectory
$user = "User1"
$logfile = "$env:userprofile\Documents\$user-GroupSidHistory.txt"
[array]$memberGroups = @()
$memberGroups += (get-aduser -id $user -Properties Memberof | Select-Object MemberOf).MemberOf
"Finding and Resolving Sid History for all groups user $user is a member of" | out-file $logfile -append
"=================================================" | out-file $logfile -append
foreach($memberGroup in $memberGroups)
{
$mg = $memberGroup | out-string
$g = Get-ADgroup -filter {DistinguishedName -eq $mg} -property sidHistory | select-object * -ExpandProperty sidHistory | Select-Object DistinguishedName, @{name="SIDHistory";expression={$_.Value}}
if ($g)
{ "Found SID History for: " + $mg.trim() | out-file $logfile -append
$sh = $g.sidhistory
foreach($s in $sh)
{
"Translating:" + $s | out-file $logfile -append
$objSID = New-Object System.Security.Principal.SecurityIdentifier($s)
$objUser = $objSID.Translate([System.Security.Principal.NTAccount])
"Translated to: " + $objUser.Value | out-file $logfile -append
" " | out-file $logfile -append }
}
else
{"No SID History for: " + $mg.trim() | out-file $logfile -append
"" | out-file $logfile -append }
}
Write-host "Done. Go check the log file at $logfile"
#################### SCRIPT ################################################
#################### SCRIPT ResolveSIDHistoryForAllGroups ################################################
# ResolveSIDHistoryForAllGroups
# Author: Joroar
# Date: 8/11/14
# Get every group in the domain that has SID History and then try to resolve that SID
# The output goes to a log file called GroupsWSidHistory.txt, located in the logged-on users documents folder
import-module activedirectory
$logfile = "$env:userprofile\Documents\GroupsWSidHistory.txt"
[array]$groups = @()
$groups+=Get-ADgroup -filter 'sidhistory -like "*"' -property sidHistory | select-object * -ExpandProperty sidHistory | Select-Object DistinguishedName, @{name="SIDHistory";expression={$_.Value}}
foreach($group in $groups)
{
$sh = $group.sidhistory
foreach($s in $sh)
{
"Found SID History for: " + $group.DistinguishedName | out-file $logfile -append
"Translating:" + $sh | out-file $logfile -append
$objSID = New-Object System.Security.Principal.SecurityIdentifier($sh)
$objUser = $objSID.Translate([System.Security.Principal.NTAccount])
"Translated to: " + $objUser.Value | out-file $logfile -append
" " | out-file $logfile -append
}
}
Write-host "Done. Go check the log file at $logfile"
#################### SCRIPT ################################################
Add a Comment
Cancel reply
You must be logged in to post a comment.