SharePoint: Troubleshooting Check Permissions - Windows auth

Note: This post is specific to Windows Authentication (NLTM or Kerberos) within SharePoint.

For Forms-based authentication see this: https://blogs.technet.microsoft.com/spjr/2018/03/20/sharepoint-check-permissions-and-external-tokens-fba/

And for Trusted Provider (SAML) auth, see this: https://blogs.technet.microsoft.com/spjr/2018/10/03/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.

Usually the scenario we see is that an Active Directory group is given permission to a site or other SharePoint resource.  You then run “Check Permissions” for members of that group and it doesn’t show the permissions the user should have.  If the user only gets permissions through groups, “Check Permissions” may say they have “None”.

This can happen for a number of reasons.
Before we begin here, it’s important to understand that “Check Permissions” is executed by the Application Pool account for the web app.  It does so by checking the External Token for the user.  If the External Token is expired (by default, it only lasts 24 hours), the Application Pool account must refresh it.  That’s usually where things go wrong.

The understanding of External Tokens and interactive vs non-interactive logins can be helpful.  However, it’s already been covered pretty well here, so I won’t rehash it:
Why SharePoint Check Permissions Can Give Wrong Results for AD Groups

One note about the above article.  There’s a section about “Reducing External Token Timeout” to something like 2 minutes.  I wouldn’t do that.  Leave it at the default 24 hours, or bad and (seemingly) mysterious things can happen.

Update: I came across a blog post from my colleague Tehnoon that covers some additional details of the External Token, and lists some common problems and resolutions with updating it: https://blogs.msdn.microsoft.com/tehnoonr/2017/07/02/sharepoint-2013-check-permissions-and-doesuserhavepermissions-internals/

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 we learned in Tehnoons Post, and Sams 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

 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: https://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 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
#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
#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
# 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
# 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 ################################################