Script to create a Kerberos Token Size Report

by Jeremy Saunders on December 20, 2013

This PowerShell script will enumerate all user accounts in a Domain, calculate their estimated Token Size and create a report of the top x users in CSV format.

However, before I talk about the script it’s important to provide some background information on Kerberos token size; how to calculate it; and how to manage it.

The Kerberos token size grows depending on the following facts:

  • Amount of direct and indirect (nested) group memberships.
    • Distribution groups are not included in the token, but all security groups are included.
    • All group scopes are included in the token evaluation.
  • Whether or not the user has a SID history, and if so, the number of entries.
  • Authentication method (username/password or multi-factor like Smart Cards).
  • The user is enabled for Kerberos delegation.
  • Local user rights assigned to the user.

If it grows beyond the default maximum allowed size…

  • Access and single sign-on (SSO) to Kerberos enabled services will fail, as well as causing unknown side-effects to other services.
  • You have the option of reducing the Kerberos Token Size, or modifying the MaxTokenSize setting on the computers. Now days increasing the  MaxTokenSize setting is a given, however reducing the Kerberos Token Size is a necessary management task and often focussed around the security group design and implementation, and perhaps the clean-up of SID history.

As per KB327825, use the following formula to determine whether it is necessary to modify the MaxTokenSize value or not:

TokenSize = 1200 + 40d + 8s

This formula uses the following values:

  • d: The number of domain local groups a user is a member of plus the number of universal groups outside the user’s account domain that the user is a member of plus the number of groups represented in security ID (SID) history.
    • Although it’s not documented in KB327825 or any other Microsoft references, I also add the number of global groups outside the user’s account domain that the user is a member of to the “d” calculation of the TokenSize. Whilst the Microsoft methodology is to add universal groups from other domains, it is possible to add global groups too. Therefore it’s important to capture this and correctly include it in the calculation.
  • s: The number of security global groups that a user is a member of plus the number of universal groups in a user’s account domain that the user is a member of.
  • 1200: The estimated value for ticket overhead. This value can vary, depending on factors such as DNS domain name length, client name, and other factors.
    • User rights include rights such as “Log on locally” or “Access this Computer from the network”. The only user rights that are added to an access token are those user rights that are configured on the server that hosts a secured resource. Most of the users are likely to have only two or three user rights on the Exchange server. Administrators may have dozens of user rights. Each user right requires 12 bytes to store it in the token.
    • Token overhead includes multiple fields such as the token source, expiration time, and impersonation information. For example, a typical domain user has no special access or restrictions; token overhead is likely to be between 400 and 500 bytes.
    • Estimated value for ticket overhead can vary depending on factors such as DNS domain name length, client name and other factors.
  • Each group membership adds the group SID to the token together with an additional 16 bytes for associated attributes and information. The maximum possible size for SID is 68 bytes. Therefore, each security group to which a user belongs typically adds 44 bytes to the user’s token size.
    • Domain Local group SIDs are 40 bytes each.
    • Domain Global and Universal groups are 8 bytes each.

You can see here that there is a higher tax to pay for using Domain Local groups. Hence the reason why you often find a greater use of Global and/or Universal groups in larger environments. The AGDLP group design and methodology is good in principle, but needs to be implemented sensibly.

You may also see the formula represented as:

TokenSize = [12 x number of user rights] + [token overhead] + 40d + 8s

…where [12 x number of user rights] + [token overhead] is typically estimated to be 1200 bytes.

The default value of the Kerberos MaxTokenSize will be different based on the Windows Operating System version:

  • 12000 bytes for Windows XP/2003/Vista/2008/7/2008R2
  • 48000 bytes for Windows 8/2012 and above

This can be increased by setting the Kerberos MaxTokenSize (the maximum Kerberos SSPI context token buffer size) registry parameter to a supported maximum value of up to 65335 bytes. Microsoft does NOT recommend increasing this beyond to 48000 bytes. However, blatantly increasing the MaxTokenSize can have further impacts on applications, such as Microsoft Internet Information Services (IIS). Best practice is to review methods to reduce the token size, such as reducing and consolidating group membership, ensuring there is no looping (circular nesting) in groups, and cleaning up SID History, before increasing the MaxTokenSize.

If the MaxTokenSize is to be increased, it’s still extremely important to manage the number of groups each user is a member of. Although 1,024 is the maximum number of security groups that a user can be a member of, it is a best practice to restrict the number to 1,015. This number makes sure that token generation will always succeed because it provides space for up to 9 SIDs of well-known groups that are inserted by the Local Security Authority (LSA) when a user logs onto a computer.

The MaxTokenSize registry value can be deployed via a Group Policy Object (GPO) as per KB938118.

Existing Tools:

  • There is a little known UI application called TokenMaster.exe, which was released back in 2000 by Jeffrey Richter and Jason Clark. It’s very hard to find, so I’ve zipped it up and included it here:
  • Microsoft also has a tool called Tokensz.exe that could also be used in a login script.
  • A 3rd party Active Directory Audit Tool called Gold Finger lets you view the complete access token of any users Active Directory domain user account.


Now we get to the fun part…the script 🙂

The script is fully documented and shows that I’ve been extremely thorough in all aspects of testing to produce the most accurate and error free results. I’ve had some great feedback so far that’s helped me tweak this for different environments. But like anything, my knowledge is based on experience and research, so I welcome your feedback if there is something you feel I could improve on. Refer to the end of this article for information on how to tune your PowerShell environment to avoid the “OutOfMemory Exception” error.

The script has some variables that can be set:

  • In a large environment you don’t need to export the whole user list and their token size, etc, but rather focus on the top x users with potential issues. So set the $TopUsers variable to the number of users with large tokens that you want to report on. For example: $TopUsers = 200
  • In a large environment with tens of thousands of users the $array of objects will grow very large and may cause memory issues, so we use the $TokensSizeThreshold variable to set a threshold size in bytes that you want to start to capture the user information into the $array for the report.
  • In large environments the script can take hours to complete, so I’ve implemented a progress bar and a count with percent complete calculation. This is so that you can monitor it and know where it’s up to. These are controlled by the $ProgressBar and $ConsoleOutput variables. Once you’re comfortable with the script, you could turn these off and run the script as a scheduled task on a regular basis as part of your administrative reporting tasks.
  • When the script completes it will write a summary to the console. This is controlled by the $OutputSummary variable.
  • Leave the $UseTokenGroups and $UseGetAuthorizationGroups as they are. Make sure you read the documentation in the script before you make changes to these variables.

IMPORTANT: The current release of this script does not report on cross-forest/domain group memberships as neither the tokenGroups attribute or GetAuthorizationGroups() method can achieve this. Therefore reporting on Global and Universal security groups outside the users Domain will always report as 0.

The screen shots shown in this post are from a recent health check I completed in a large environment with 8228 enabled user accounts. Disabled user accounts are excluded. Review the LDAP filter in the script.

The screen shot below shows the progress bar and the information calculated per user. As mentioned, this output can be turned on and off by the $ProgressBar and $ConsoleOutput variables.

Kerberos Token Size - Screen Output

The screen show below shows the summary report produced when the script completes. As mentioned, this output can also be turned on and off by the $OutputSummary variable.

Kerberos Token Size - Summary

The screen shot below shows the output of the csv file that has been imported into Excel, with an overlay from the report I completed for the customer in a Word table format. As you can see here their issue was group memberships of Domain Local groups, so I didn’t need to include all columns when presenting this to the customer, just enough to demonstrate the root cause of their Kerberos Token issues.

Kerberos Token Size - csv output

Here is the Get-TokenSizeReport.ps1 (966 downloads)  script:

  This script will enumerate all enabled user accounts in a Domain, calculate their estimated Token
  Size and create two reports in CSV format:
  1) A report of all users with an estimated token size greater than or equal to the number defined
     by the $TokensSizeThreshold variable.
  2) A report of the top x users as defined by the $TopUsers variable.


  - To run the script against all enabled user accounts in the current domain:

  - To run the script against all enabled user accounts of a trusted domain:

  - To run the script against 1 user account:
      Get-TokenSizeReport.ps1 -AccountName:<samaccountname>

  - To run the script against 1 user account of a trusted domain:
      Get-TokenSizeReport.ps1 -AccountName:<samaccountname>

  Script Name: Get-TokenSizeReport.ps1
  Release 2.7
  Written by Jeremy Saunders ( 13/12/2013
  Modified by Jeremy Saunders ( 19/12/2016

  Original script was derived from the CheckMaxTokenSize.ps1 written by Tim Springston [MS] on 7/19/2013

  Re-wrote the script to be more efficient and provide a report for all users in the

  - Microsoft KB327825: Problems with Kerberos authentication when a user belongs to many groups
  - Microsoft KB243330: Well-known security identifiers in Windows operating systems
  - Microsoft KB328889: Users who are members of more than 1,015 groups may fail logon authentication
  - Microsoft KB938118: How to use Group Policy to add the MaxTokenSize registry entry to multiple computers
  - Microsoft Blog: Managing Token Bloat:

  The calculation in this script is an estimation based on the formula documented under KB327825. I prefer
  to review any account with a token size over 6K, hence why I default the $TokensSizeThreshold variable
  to 6000.

  A user's access token is increased in 4K blocks. The default size of a user's access token is 4K. Once
  a user goes over this amount Windows does not increment the size of the token by the amount needed for
  each additional SID added. Instead Windows allocates another 4K of memory, thus doubling the size of
  the access token to 8K. And again once the size of the access token breaches 8K it will again increase
  by a further 4K to 12K. And so on.

  So given that the token size calculation is an estimation, and the access token is increased in blocks
  of 4K, it is quite conceivable that an estimated calculated token size of between 6000 and 12000 can
  actually breach the 12K token size limit and cause problems in some environments. There is a real lack
  of understanding here.

  Although it's not documented in KB327825 or any other Microsoft references, I also add the number of
  global groups outside the user's account domain that the user is a member of to the "d" calculation of
  the TokenSize. Whilst the Microsoft methodology is to add universal groups from other domains, it is
  possible to add global groups too. Therefore it's important to capture this and correctly include it in
  the calculation.

  My calculations consider the BuiltIn groups as Domain Local groups, which means I'm allowing 40 bytes
  per BuiltIn group that the user is a member of. Others seem to only allow 8 bytes in their calculations.
  However depending on the length of the SID, BuiltIn groups are actually either 16 or 28 bytes in reality.
  Therefore, whilst I may be overcompensating for some of the groups, others are always underestimating.

  If we wanted to be as accurate as possible we can calculate the byte length of each SID and then add a
  further 16 bytes for associated attributes and information. Most user and group SIDs are 28 bytes in

  Refer to the following thread to get a full understanding how this needs to be calculated for complete
  acuracy. You also need to understand how the token is managed in 4KB blocks. It starts to make sense
  when you tie together the comments from Paul Bergson, Richard Mueller and Marcin Polich:

  There is some further good information on SIDs published by Philipp Fockeler:

  For users with large tokens consider reducing direct and transitive (nested) group memberships.
  Larger environments that have evolved over time also have a tendancy to suffer from Circular Group
  Nesting and both user and group SIDHistory.

  The SIDHistory of the user and the group accounts are included in the calculation of the token size,
  which is why it's important to clean up SIDHistory after a domain migration. From experience I find
  that this rarely happens.

  It's important to note that with Windows 2012 Active Directory, compression enhancements were added to
  the KDC functionality. Therefore the formula used in this script does not apply to when the domain
  functionality level is Windows 2012 and above.

  On the odd ocasion I was receiving the following error:
  - Exception calling "FindByIdentity" with "2" argument(s): "Multiple principals contain
    a matching Identity."
  - There seemed to be a known bug in .NET 4.x when passing two arguments to the FindByIdentity() method.
  - The solution was to either use a machine with .NET 3.5 or re-write the script to pass
    three arguments as per the Get-UserPrincipal function provided in the following Scripting Guy article:
    This function passes the Context Type, FQDN Domain Name and Parent OU/Container.
  - Other references:

  I have also seen the following error:
  - Exception calling "GetAuthorizationGroups" with "0" argument(s): "An error (1301) occurred while
    enumerating the groups. The group's SID could not be resolved."
  - Other references:

  Added the tokenGroups attribute to get all nested groups as I could not achieve 100% reliability using
  the GetAuthorizationGroups() method. Could not afford for it to start failing after running for hours
  in large environments.
  - References:

  There are important differences between using the GetAuthorizationGroups() method versus the tokenGroups
  attribute that need to be understood. Aside from the unreliability of GetAuthorizationGroups(), when push
  comes to shove you get different results depending on which method you use, and what you want to achieve.
    - The tokenGroups attribute only contains the actual "Active Directory" principals, which are groups and
      siDHistory (from both the user and groups that th user is a member of).
    - However, whilst tokenGroups contains transitive groups (groups within the same forest), it does not
      reveal cross-forest/domain group memberships. The tokenGroups attribute is constructed by Active
      Directory on request, and this depends on the availability of a Global Catalog server:
    - The GetAuthorizationGroups() method also returns the well-known security identifiers of the local
      system (LSALogonUser) for the user running the script, which will include groups such as:
      - Everyone (S-1-1-0)
      - Authenticated Users (S-1-5-11)
      - This Organization (S-1-5-15)
      - Low Mandatory Level (S-1-16-4096)
      This will vary depending on where you're running the script from and in what user context. The result
      is still consistent, as it adds the same overhead to each user. But this is misleading.
    - GetAuthorizationGroups() will return cross-forest/domain group memberships, but cannot resolve them
      because they contain a ForeignSecurityPrincipal. It therefore fails as documented above.
    - GetAuthorizationGroups() does not contain siDHistory.

  In my view you would always use the tokenGroups attribute to collate a consistent and accurate user report
  across the environment, whereas the GetAuthorizationGroups() method could be used in a logon script to
  calucate the token of the user together with the system they are logging on to. The actual calculation of
  the token size adds the estimated value for ticket overhead anyway, hence the reason why using the tokenGroups
  attribute provides a consistent result for all users.
  If you wanted an accurate token size per user per system and GetAuthorizationGroups() method continues to
  prove to be unreliable, you could use the tokenGroups attribute together with the addition of the output
  from the "whoami /groups" command to get all the well-known groups and label needed to calculate the
  complete local token.

  Microsoft also has a tool called Tokensz.exe that could also be used in a logon script. It can be downloded
  from here:

  To be completed:
  - Implement a garbage collection process to reduce memory usage:
  - Some further research and testing needs to be completed with the code that retrieves the tokenGroups
    attribute to validate performance between the GetInfoEx method or RefreshCache method.
  - Investigate if using the WindowsIdentity.Groups Property to get the users token is a better approach.



# Set Powershell Compatibility Mode
Set-StrictMode -Version 2.0

# Enable verbose, warning and error mode
$VerbosePreference = 'Continue'
$WarningPreference = 'Continue'
$ErrorPreference = 'Continue'


# Set this to the OU structure where the you want to search to
# start from. Do not add the Domain DN. If you leave it blank,
# the script will start from the root of the domain.
$OUStructureToProcess = ""

# Set the search scope:
# - SUBTREE is the defined OU and all child OUs (including their
#   children, etc)
# - ONELEVEL is the defined container
$SearchScope = "SUBTREE"

# Set the name of the OU(s) you want to exclude. Use the full OU
# structure minus the Domain DN.
$ExcludeOUs = @("OU=ExcludeMe","OU=ExcludeMeToo,OU=People")

# Set this value to the number of users with large tokens that
# you want to report on.
$TopUsers = 200

# Set this to the size in bytes that you want to capture the user
# information for the report.
$TokensSizeThreshold = 6000

# Set this value to true if you want to output to the console
$ConsoleOutput = $True

# Set this value to true if you want a summary output to the
# console when the script has completed.
$OutputSummary = $True

# Set this to the delimiter for the CSV output
$Delimiter = ","

# Set this value to true if you want to see the progress bar.
$ProgressBar = $True


write-verbose "This script is running under PowerShell version $($PSVersionTable.PSVersion.Major).$($PSVersionTable.PSVersion.Minor)"

$invalidChars = [io.path]::GetInvalidFileNamechars() 
$datestampforfilename = ((Get-Date -format s).ToString() -replace "[$invalidChars]","-")

# Get the script path
$ScriptPath = {Split-Path $MyInvocation.ScriptName}
$ReferenceFile = $(&$ScriptPath) + "\KerberosTokenSizeReport-$($datestampforfilename).csv"
$ReferenceFileTopUsers = $(&$ScriptPath) + "\KerberosTokenSizeReport-TopUsers-$($datestampforfilename).csv"

if (Test-Path -path $ReferenceFile) {
  remove-item $ReferenceFile -force -confirm:$false

if (Test-Path -path $ReferenceFileTopUsers) {
  remove-item $ReferenceFileTopUsers -force -confirm:$false

if ([String]::IsNullOrEmpty($TrustedDomain)) {
  # Get the Current Domain Information
  $domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
} else {
  $context = new-object System.DirectoryServices.ActiveDirectory.DirectoryContext("domain",$TrustedDomain)
  Try {
    $domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($context)
  Catch [exception] {
    $Host.UI.WriteErrorLine("ERROR: $($_.Exception.Message)")

# Get AD Distinguished Name
$DomainDistinguishedName = $Domain.GetDirectoryEntry() | select -ExpandProperty DistinguishedName  

If ($OUStructureToProcess -eq "") {
  $ADSearchBase = $DomainDistinguishedName
} else {
  $ADSearchBase = $OUStructureToProcess + "," + $DomainDistinguishedName

$garbageCounter = 0
$arrayoftopusers = @()
$TotalUsersProcessed = 0
$UserCount = 0
$GroupCount = 0
$LargestTokenSize = 0
$TotalTokensLessThan8K = 0
$TotalTokensBetween8and12K = 0
$TotalTokensGreaterThan12K = 0
$TotalTokensGreaterThan48K = 0
$TotalExcludedUsers = 0

$UseInputFile = $False
If (-not [String]::IsNullOrEmpty($InputFile)) {
  $InputFile = $(&$ScriptPath) + "\$InputFile"
  If (Test-Path $InputFile) {
    $UseInputFile = $True

If ([String]::IsNullOrEmpty($AccountName)) {
  # Create an LDAP search for all enabled users
  $ADFilter = "(&(objectClass=user)(objectcategory=person)(!userAccountControl:1.2.840.113556.1.4.803:=2))"
  $ProcessSingleAccount = $False
} Else {
  # Create an LDAP search for a simple user
  $ADFilter = "(&(objectClass=user)(objectcategory=person)(samaccountname=$AccountName))"
  $ProcessSingleAccount = $True
  $TokensSizeThreshold = 65335
  $OutputSummary = $False

# There is a known bug in PowerShell requiring the DirectorySearcher
# properties to be in lower case for reliability.
$ADPropertyList = @("distinguishedname","samaccountname","useraccountcontrol","objectsid","sidhistory","primarygroupid","lastlogontimestamp","memberof")
$ADScope = $SearchScope
$ADPageSize = 1000
$ADSearchRoot = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$($ADSearchBase)") 
$ADSearcher = New-Object System.DirectoryServices.DirectorySearcher 
$ADSearcher.SearchRoot = $ADSearchRoot
$ADSearcher.PageSize = $ADPageSize 
$ADSearcher.Filter = $ADFilter 
$ADSearcher.SearchScope = $ADScope
if ($ADPropertyList) {
  foreach ($ADProperty in $ADPropertyList) {
Try {
  write-host " "
  If ([String]::IsNullOrEmpty($AccountName)) {
    write-verbose "Please be patient whilst the script retrieves all enabled user objects and specified attributes..."
  } Else {
    write-verbose "Retrieving the user object and specified attributes..."
  write-host " "
  $UserCount = $ADSearcher.FindAll().Count
Catch {
  $UserCount = 0
  $Host.UI.WriteErrorLine("ERROR: The $ADSearchBase structure cannot be found!")
Finally {
  # Dispose of the search and results properly to avoid a memory leak
  $ADSearcher.Dispose() | Out-Null
  [System.GC]::Collect() | Out-Null

if ($UserCount -ne 0) {
  # The ForEach-Object cmdlet processes each item in turn as it is passed through the pipeline
  # whereas foreach generates the whole collection first. So this should alleviate memory issues.
  $ADSearcher.Findall() | ForEach-Object {
    $lastLogonTimeStamp = ""
    $lastLogon = ""
    $UserDN = $_.Properties.distinguishedname[0]
    $samAccountName = $_.Properties.samaccountname[0]
    $ParentOU = $UserDN -split '(?<![\\]),'
    $ParentOU = $ParentOU[1..$($ParentOU.Count-1)] -join ','

    $TotalUsersProcessed ++
    If ($ProgressBar) {
      Write-Progress -Activity "Processing $($UserCount) Users" -Status ("Count: $($TotalUsersProcessed) - Username: {0}" -f $samAccountName) -PercentComplete (($TotalUsersProcessed/$UserCount)*100)

    $ExcludeOUMatch = $False
    If ($ExcludeOUs -eq "" -OR ($ExcludeOUs | Measure-Object).Count -eq 0) {
      $ExcludeOUMatch = $False
    } Else {
      ForEach ($ExcludeOU in $ExcludeOUs) {
        If ($ParentOU -Like "*$ExcludeOU*") {
          $ExcludeOUMatch = $True

    If ($ExcludeOUMatch -eq $False) {

      If ($(Try{($_.Properties.lastlogontimestamp | Measure-Object).Count -gt 0}Catch{$False})) {
        $lastLogonTimeStamp = $_.Properties.lastlogontimestamp[0]
        $lastLogon = [System.DateTime]::FromFileTime($lastLogonTimeStamp)
        if ($lastLogon -match "1/01/1601") {$lastLogon = "Never logged on before"}
      } else {
        $lastLogon = "Never logged on before"
      $OU = $_.GetDirectoryEntry().Parent
      $OU = $OU -replace ("LDAP:\/\/","")

      # Get user SID
      $arruserSID = New-Object System.Security.Principal.SecurityIdentifier($_.Properties.objectsid[0], 0)
      $userSID = $arruserSID.Value

      # Get the SID of the Domain the account is in
      $AccountDomainSid = $arruserSID.AccountDomainSid.Value

      # Get User Account Control & Primary Group by binding to the user account
      # ADSI Requires that / Characters be Escaped with the \ Escape Character
      $UserDN = $UserDN.Replace("/", "\/")
      $objUser = [ADSI]("LDAP://" + $UserDN)
      If ($(Try{($objUser.useraccountcontrol | Measure-Object).Count -gt 0}Catch{$False})) {
        $UACValue = $objUser.useraccountcontrol[0]
      } else {
        $UACValue = ""

      $Enabled = $True
      if (($UACValue -bor 0x0002) -eq $UACValue) {
        $Enabled = $False

      $TrustedforDelegation = $false
      if ((($UACValue -bor 0x80000) -eq $UACValue) -OR (($UACValue -bor 0x1000000) -eq $UACValue)) {
        $TrustedforDelegation = $true

      $primarygroupID = $objUser.primarygroupid
      If ($(Try{$primarygroupID -ne $NULL}Catch{$False})) {
        # Primary group can be calculated by merging the account domain SID and primary group ID
        $primarygroupSID = $AccountDomainSid + "-" + $primarygroupID.ToString()
        $primarygroup = [adsi]("LDAP://<SID=$primarygroupSID>")
        $primarygroupname = $[0]
      } else {
        $primarygroupname = "NULL"
      $objUser = $null

      # Get User SID history
      $UserSIDHistoryCount = 0
      if ($(Try{$_.Properties.sidhistory -ne $null}Catch{$False})) {
        foreach ($sidhistory in $_.Properties.sidhistory) {
          $SIDHistObj = New-Object System.Security.Principal.SecurityIdentifier($sidhistory, 0)
          #write-verbose "$($SIDHistObj.Value) is in the SIDHistory."
        $SIDHistObj = $null

      $UserAccount = [ADSI]"$($_.Path)"
      $tokenGroupsMethod1 = $true
      $tokenGroupsMethod2 = $false
      If ($tokenGroupsMethod1) {
        $UserAccount.GetInfoEx(@("tokenGroups"),0) | Out-Null
        $ErrorActionPreference = "continue"
        $tokengroups = $UserAccount.GetEx("tokengroups")
        if ($Error) {
          Write-Warning "  Tokengroups not readable"
          $tokengroups=@()   #empty enumeration
      If ($tokenGroupsMethod2) {
        # Rebuild the tokenGroups attribute for the user, which is a dynamic attribute not part
        # of the schema.
        $tokengroups = $UserAccount.psbase.Properties.Item("tokenGroups")
      $UserPrincipalOutput = @()
      $PrincipalCount = 0
      $GroupCount = 0
      $SecurityDomainLocalScope = 0
      $SecurityGlobalInternalScope = 0
      $SecurityGlobalExternalScope = 0
      $SecurityUniversalInternalScope = 0
      $SecurityUniversalExternalScope = 0
      $ExternalGroupsFound = $false
      $TotalGroupSIDHistoryCount = 0

      foreach($sidByte in $tokengroups) {
        $GroupSIDBytes = 0
        $principal = New-Object System.Security.Principal.SecurityIdentifier($sidByte,0)
        $objUserToken = New-Object -TypeName PSObject
          $PrincipalAccountName = $principal.Translate([System.Security.Principal.NTAccount])
          $objUserToken | Add-Member -MemberType NoteProperty -Name "Account" -value $PrincipalAccountName.value
        Catch {
          $objUserToken | Add-Member -MemberType NoteProperty -Name "Account" -value "Cannot translate SID to an account"
        $objUserToken | Add-Member -MemberType NoteProperty -Name "SID" -value $principal.Value
        $objUserToken | Add-Member -MemberType NoteProperty -Name "BinaryLength" -value $principal.BinaryLength
        $objUserToken | Add-Member -MemberType NoteProperty -Name "AccountDomainSid" -value $principal.AccountDomainSid

        $grp = [ADSI]("LDAP://<SID=$($principal.Value)>")

        if ($grp.Path -ne $null) {
          $grpdn = $grp.distinguishedName.tostring().ToLower()
          $grouptype = $grp.groupType.value
          $objUserToken | Add-Member -MemberType NoteProperty -Name "GroupType" -value $grouptype

          switch -exact ($grouptype) {
            "-2147483646"   {
                              # Global security scope
                              $groupscope = "Global"
                              if ($principal.AccountDomainSid -eq $AccountDomainSid) 
                              } else { 
                                # Global groups from others.
                                $ExternalGroupsFound = $true
            "-2147483644"   { 
                              # Domain Local scope 
                              $groupscope = "DomainLocal"
            "-2147483643"   { 
                              # Domain Local BuiltIn scope
                              $groupscope = "Builtin"
            "-2147483640"   { 
                              # Universal security scope 
                              $groupscope = "Universal"
                              if ($principal.AccountDomainSid -eq $AccountDomainSid)
                              } else { 
                                # Universal groups from others.
                                $ExternalGroupsFound = $true
            Default         {
                                $groupscope = "Unknown"
                                write-warning "No valid group type found!"
          $objUserToken | Add-Member -MemberType NoteProperty -Name "GroupScope" -value $groupscope
          $GroupSIDHistoryCount = 0
          If ($(Try{($grp.sidhistory | Measure-Object).Count -gt 0}Catch{$False})) {
            foreach ($groupsidhistory in $grp.sidhistory) {
              (new-object System.Security.Principal.SecurityIdentifier $groupsidhistory,0).Value | out-null
          $objUserToken | Add-Member -MemberType NoteProperty -Name "SIDHistoryCount" -value $GroupSIDHistoryCount

        } Else {
          # The SID Histoty of a group does not have a group type. Therefore, if the group path equals $null,
          # the principal is a user or group SID History
          $GroupSIDHistoryCount = 0
          $objUserToken | Add-Member -MemberType NoteProperty -Name "GroupType" -value "SIDHistory"
          $objUserToken | Add-Member -MemberType NoteProperty -Name "GroupScope" -value "SIDHistory"
          $objUserToken | Add-Member -MemberType NoteProperty -Name "SIDHistoryCount" -value "N/A"
        $UserPrincipalOutput += $objUserToken
        $objUserToken = $null
        $TotalGroupSIDHistoryCount = $TotalGroupSIDHistoryCount + $GroupSIDHistoryCount
      If ($ProcessSingleAccount) {
        $SingleAccountOutputFile = "$SamAccountName-$domain.csv"
        if (Test-Path -path $SingleAccountOutputFile) {
          remove-item $SingleAccountOutputFile -force -confirm:$false
        if ($PSVersionTable.PSVersion.Major -gt 2) {
          $UserPrincipalOutput | Export-Csv -Path "$SingleAccountOutputFile" -Append -Delimiter $Delimiter -NoTypeInformation -Encoding ASCII
        } Else {
          if (!(Test-Path -path $ReferenceFileTopUsers)) {
            $UserPrincipalOutput | ConvertTo-Csv -NoTypeInformation -Delimiter $Delimiter | Select-Object -First 1 | Out-File -Encoding ascii -filepath "$SingleAccountOutputFile"
          $UserPrincipalOutput | ConvertTo-Csv -NoTypeInformation -Delimiter $Delimiter | Select-Object -Skip 1 | Out-File -Encoding ascii -filepath "$SingleAccountOutputFile" -append -noclobber

      If ($ConsoleOutput) {
        write-verbose "Checking the token of user $SamAccountName in domain $domain"
        # The principals in TokenGroups includes the user SID History and user group SID History
        write-verbose "- There are $PrincipalCount principals in the token. This is made up of:"
        write-verbose "  - $GroupCount groups"
        write-verbose "    - $SecurityDomainLocalScope are domain local security groups"
        write-verbose "    - $SecurityGlobalInternalScope are domain global scope security groups inside the users domain"
        write-verbose "    - $SecurityGlobalExternalScope are domain global scope security groups outside the users domain"
        write-verbose "    - $SecurityUniversalInternalScope are universal security groups inside the users domain"
        write-verbose "    - $SecurityUniversalExternalScope are universal security groups outside the users domain"
        write-verbose "  - $($UserSIDHistoryCount + $TotalGroupSIDHistoryCount) SIDs in the SIDHistory"
        write-verbose "    - $UserSIDHistoryCount SIDs are in the users SIDHistory"
        write-verbose "    - $TotalGroupSIDHistoryCount SIDs are in the users group SIDHistory"
        write-verbose "- The current userAccountControl value is $UACValue"
        If ($Enabled) {
          write-verbose "  - The account is enabled"
        } Else {
          write-verbose "  - The account is disabled"
        If ($TrustedforDelegation -eq $false) {
          write-verbose "  - The account is not trusted for delegation"
        } Else {
          write-verbose "  - The account is trusted for delegation"
        write-verbose "- The primary group is $primarygroupname"

      # Calculate the current token size, taking into account whether or not the account is trusted for delegation or not.
      $TokenSize = 1200 + (40 * ($SecurityDomainLocalScope + $SecurityGlobalExternalScope + $SecurityUniversalExternalScope + $UserSIDHistoryCount + $TotalGroupSIDHistoryCount)) + (8 * ($SecurityGlobalInternalScope  + $SecurityUniversalInternalScope))
      if ($TrustedforDelegation) {
        $TokenSize = 2 * $TokenSize
      write-verbose "- Therefore the estimated Token size is $Tokensize"

      If ($ProcessSingleAccount) {
        write-verbose "- Refer to $SingleAccountOutputFile for a detailed output"

      If ($TokenSize -le 12000) {
        $TotalTokensLessThan8K ++
        If ($TokenSize -gt 8192) {
          $TotalTokensBetween8and12K ++
      } elseIf ($TokenSize -le 48000) {
        $TotalTokensGreaterThan12K ++
      } else {
        $TotalTokensGreaterThan48K ++

      If ($TokenSize -gt $LargestTokenSize) {
        $LargestTokenSize = $TokenSize
        $LargestTokenUser = $SamAccountName

      If ($TokenSize -ge $TokensSizeThreshold) {
        $obj = New-Object -TypeName PSObject
        $obj | Add-Member -MemberType NoteProperty -Name "Domain" -value $domain
        $obj | Add-Member -MemberType NoteProperty -Name "SamAccountName" -value $SamAccountName
        $obj | Add-Member -MemberType NoteProperty -Name "TokenSize" -value $TokenSize
        $obj | Add-Member -MemberType NoteProperty -Name "Memberships" -value $GroupCount
        $obj | Add-Member -MemberType NoteProperty -Name "DomainLocal" -value $SecurityDomainLocalScope
        $obj | Add-Member -MemberType NoteProperty -Name "GlobalInternal" -value $SecurityGlobalInternalScope
        $obj | Add-Member -MemberType NoteProperty -Name "GlobalExternal" -value $SecurityGlobalExternalScope
        $obj | Add-Member -MemberType NoteProperty -Name "UniversalInternal" -value $SecurityUniversalInternalScope
        $obj | Add-Member -MemberType NoteProperty -Name "UniversalExternal" -value $SecurityUniversalExternalScope
        $obj | Add-Member -MemberType NoteProperty -Name "UserSIDHistory" -value $UserSIDHistoryCount
        $obj | Add-Member -MemberType NoteProperty -Name "GroupSIDHistory" -value $TotalGroupSIDHistoryCount
        $obj | Add-Member -MemberType NoteProperty -Name "UACValue" -value $UACValue
        $obj | Add-Member -MemberType NoteProperty -Name "Enabled" -value $Enabled
        $obj | Add-Member -MemberType NoteProperty -Name "TrustedforDelegation" -value $TrustedforDelegation
        $obj | Add-Member -MemberType NoteProperty -Name "LastLogon" -value $lastLogon
        $arrayoftopusers += $obj

        # PowerShell V2 doesn't have an Append parameter for the Export-Csv cmdlet. Out-File does, but it's
        # very difficult to get the formatting right, especially if you want to use quotes around each item
        # and add a delimeter. However, we can easily do this by piping the object using the ConvertTo-Csv,
        # Select-Object and Out-File cmdlets instead.
        if ($PSVersionTable.PSVersion.Major -gt 2) {
          $obj | Export-Csv -Path "$ReferenceFile" -Append -Delimiter $Delimiter -NoTypeInformation -Encoding ASCII
        } Else {
          if (!(Test-Path -path $ReferenceFile)) {
            $obj | ConvertTo-Csv -NoTypeInformation -Delimiter $Delimiter | Select-Object -First 1 | Out-File -Encoding ascii -filepath "$ReferenceFile"
          $obj | ConvertTo-Csv -NoTypeInformation -Delimiter $Delimiter | Select-Object -Skip 1 | Out-File -Encoding ascii -filepath "$ReferenceFile" -append -noclobber

        If ($ProcessSingleAccount -eq $False) {
          # Manage an array of the top X users as per the $TopUsers variable.
          $arrayoftopusers | Sort-Object TokenSize -descending | select-object -first $TopUsers | out-null


    } Else {
      write-verbose "Excluding user $SamAccountName in domain $domain"
      $TotalExcludedUsers ++

    If ($ConsoleOutput -AND $ProcessSingleAccount -eq $False) {
      $percent = "{0:P}" -f ($TotalUsersProcessed/$UserCount)
      write-verbose "- Processed $TotalUsersProcessed of $UserCount user accounts = $percent complete."
      # Add a blank line
      Write-Host " "

    if ($garbageCounter -eq 500) {
      $garbageCounter = 0

  # Dispose of the search and results properly to avoid a memory leak
  $ADSearcher.Dispose() | Out-Null
  [System.GC]::Collect() | Out-Null

  If ($OutputSummary) {
    write-verbose " Summary:"
    write-verbose " - Processed $UserCount user accounts."
    If ($TotalExcludedUsers -gt 0) {
      write-verbose " - Excluded $TotalExcludedUsers user accounts."
    write-verbose " - $TotalTokensLessThan8K have a calculated token size of less than or equal to 12000 bytes."
    If ($TotalTokensLessThan8K -gt 0) {
      write-verbose "  - These users are good."
    If ($TotalTokensBetween8and12K -gt 0) {
      write-verbose "  - Although $TotalTokensBetween8and12K of these user accounts have tokens above 8K and should therefore be reviewed."
    write-verbose " - $TotalTokensGreaterThan12K have a calculated token size larger than 12000 bytes."
    If ($TotalTokensGreaterThan12K -gt 0) {
      write-verbose "   - These users will be okay if you have increased the MaxTokenSize to 48000 bytes across the domain/forest."
      write-verbose "   - Consider reducing direct and transitive (nested) group memberships."
    If ($TotalTokensGreaterThan48K -gt 0) {
      write-warning " - $TotalTokensGreaterThan48K have a calculated token size larger than 48000 bytes."
      write-warning "   - These users will have problems. Do NOT increase the MaxTokenSize beyond 48000 bytes."
      write-warning "   - Reduce the direct and transitive (nested) group memberships."
    write-verbose " - $LargestTokenUser has the largest calculated token size of $LargestTokenSize bytes in the $domain domain."

  If ($ProcessSingleAccount -eq $False) {
    # Write the $arrayoftopusers to a CSV.
    # $arrayoftopusers | export-csv -notype -path "$ReferenceFileTopUsers" -Delimiter $Delimiter
    if ($PSVersionTable.PSVersion.Major -gt 2) {
      $arrayoftopusers | Export-Csv -Path "$ReferenceFileTopUsers" -Append -Delimiter $Delimiter -NoTypeInformation -Encoding ASCII
    } Else {
      if (!(Test-Path -path $ReferenceFileTopUsers)) {
        $arrayoftopusers | ConvertTo-Csv -NoTypeInformation -Delimiter $Delimiter | Select-Object -First 1 | Out-File -Encoding ascii -filepath "$ReferenceFileTopUsers"
      $arrayoftopusers | ConvertTo-Csv -NoTypeInformation -Delimiter $Delimiter | Select-Object -Skip 1 | Out-File -Encoding ascii -filepath "$ReferenceFileTopUsers" -append -noclobber


Dealing with the “OutOfMemory Exception” error:

If you’re running the script on a Virtual Machine with only 2GB of RAM, you should first increase this to at least 4GB.

When running this script in very large environments, or when a large proportion of your users have a calculated token greater than the $TokensSizeThreshold, the $array of objects grows and consumes all available memory for the shell. When this happens you will receive the “OutOfMemory Exception” error and the script will fail. This is because the PowerShell MaxMemoryPerShellMB quota defaults to only 150MB in versions 1.0 and 2.0, and 1024MB in version 3.0. But even under PowerShell 3.0 large objects can consume all this memory.

The MaxMemoryPerShellMB quota is the maximum amount of memory allocated per shell, including the shell’s child processes.

If you’re still running PowerShell version 2.0, increase it to 1024MB by running the following command from PowerShell as an Admin:

set-item wsman:localhost\Shell\MaxMemoryPerShellMB 1024

If the script continues to fail, increase it to 2048MB.

If you’re running PowerShell version 3.0, increase it to 2048MB by running the following command from PowerShell as an Admin:

set-item wsman:localhost\Shell\MaxMemoryPerShellMB 2048

Then just restart the WinRM service for the change to take effect.

You must also be aware of Microsoft hotfixes such as KB2842230.

This is a typical script you would run from a management or administrative server, so you really should be looking at tuning the PowerShell quota settings that are best for your environment and the needs of the IT Pros you work with.


Jeremy Saunders

Jeremy Saunders

Independent Consultant | Contractor | Microsoft & Citrix Specialist | Desktop Virtualization Specialist at J House Consulting
Jeremy is a highly respected, IT Professional, with over 30 years’ experience in the industry. He is an independent IT consultant providing expertise to enterprise, corporate, higher education and government clients. His skill set, high ethical standards, integrity, morals and attention to detail, coupled with his friendly nature and exceptional design and problem solving skills, makes him one of the most highly respected and sought after Microsoft and Citrix technical resources in Australia. His alignment with industry and vendor best practices puts him amongst the leaders of his field.
Jeremy Saunders
Jeremy Saunders
Jeremy Saunders
  • Doug Neely

    This script looks great. The only issue I ran into was when it found a user with SID history. When that happened, it returned the following error:

    New-Object : Specified cast is not valid.
    At C:\Proxy\Test\Get-TokenSize.ps1:211 char:21
    + $SIDHistObj = New-Object PSObject -Property $ADObject.Properties
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [New-Object], InvalidCastException
    + FullyQualifiedErrorId : System.InvalidCastException,Microsoft.PowerShell.Commands.NewObjectCommand
    It would then fail to update the $SIDHistObj and report it as zero. Do you know what might be causing this?


    • Hi Doug,

      Thanks for the feedback.

      I have only tested the sIDHistory in my lab environment where I have several accounts with multiple SIDs in their sIDHistory, and it works as expected. From the error you posted it looks like there is an issue with $ADObject. Can you write-host the $AccountDomainSid variable for each user? Is it okay for those users with sIDHistory?

      My lab environment is all 2008 R2 Domain Controllers to match my current customers environment. Did you copy the code or download the text file via the hyperlink? I’ve uploaded an updated version of the script. Could you download that and trying again? If it fails again, I’d like to know more about your environment so that I can track down the issue.


  • Al Mulnick

    Thanks for doing putting this together and publishing it. Just a quick note to let you know that while using it, it overran the memory on the system I ran it on. So I made a few changes to the script to be more memory friendly in a large-ish environment. About 40K active accounts and many groups. Changing to have it append the spreadsheet for every pass (about line 394) made it more memory friendly.
    I also really appreciate the detailed notes on your thinking and choices. That has been incredibly helpful.


    • Hi Al,

      Thanks for your great feedback and idea about appending to the CSV file. I have only just updated this article with the latest version which seems to be more memory friendly, but would love to have a look at integrating your suggestions too. Would you mind sending me a copy?


  • CarlWebster

    Feature request: In an extremely large AD this script can take a very long time to run. Would be nice to specify an individual user or security group or OU to process instead of every user in AD. When there are in excess of 250,000 users, this script can take a long time.



    • Hi Webster,

      Sure, that’s pretty easy to do.

      To run it for a particular user you would change the filter…
      $Username = “jeremy”
      $ADFilter = “(&(samAccountName=$username))”

      To change it to search for all users based on an OU you would set the LDAP path
      $ADSearchRoot = New-Object System.DirectoryServices.DirectoryEntry(“LDAP://OU=Finance,$($DefaultNamingContext)”)

      You may also want to change the search scope from subtree to onelevel
      $ADScope = “OneLevel”

      I’ll play around with some ideas to add optional parameters and let you know.



      • CarlWebster

        Thanks. It looks like it is going to take just over nine hours to process 266,069 accounts!

        • That’s actually not too bad for that number of users. If Microsoft were doing an ADRAP, they would have the same challenge. It’s typically something you leave running overnight.


      • CarlWebster

        The change for one user worked like a champ.

  • Falko Dohse

    The script is consuming all available memory. I’m running the script on a 16GB Win2k8R2 server and it consumes the complete memory until it fails. The AD consists of only 22.000 users. Any idea what the cause could be?

    • Hi Falko,

      The $array object must either be large, or your PowerShell environment may need to be sized correctly.

      To manage the $array object try increasing the value of the $TokensSizeThreshold variable in the script to start with and then work back from there.

      To manage the PowerShell environment you need to understand…
      – What version of PowerShell are you using?
      – What’s the MaxMemoryPerShellMB value set to?
      – Does KB2842230 apply to your environment?

      Hope that helps.


      • Falko Dohse

        I’m using PowerShell Version 3.0. The MaxMemoryPerShellMB is set to 4096. The KB2842230 is not installed. I’ve already adjusted the $TokenSizeThreashold to 10000 with no effect at all. The PowerShell process consumes ALL (16GB) available memory until it fails.

        • Hi Falko,

          Either KB2842230 applies to your environment or you have another issue that’s causing it to run out of memory. This script has been run on much larger environments without issue. Do you have the same problem If you run it from a different host as the PowerShell process should never consume all that memory?

          Or maybe most of your users have a very large token size? I don’t know anything about your environment, but perhaps most of them have a token size of greater than 10000. Set the $TokenSizeThreashold higher.


    • David Jewer

      I had to update the script to use the foreach-object cmdlet in order to avoid memory issues. foreach will collect all objects first before the loop body begins to execute. With the foreach-object, the statement body is executed as soon as the object is produced.

      • Thanks David. I have done just that with the new version. But more importantly, I’ve now restructured the script so that it’s constantly writing out to file and therefore no longer creating a large object.


  • Pingback: Active Directory Health Check, Audit and Remediation Scripts()

  • Pingback: Kerberos Token Size | AD Gaurinn()

  • James Kendig

    Jeremy, One issue I see with your script is that the $groups = $UserAccount.GetEx(“tokengroups”) command will only return a max of 1000 groups so the results are not actually accurate.

    • Hi James,

      This is not a limitation with my script, but a system limitation with Group Membership as per 1,015 or thereabouts is the maximum number of groups a user should be a member of. This is documented in my article. I can’t code around that!

      I have never seen an environment with users in so many groups. That is crazy group design and asking for trouble as Microsoft points out. The screen shot in my article is of an environment at its worst. I would suggest you have bigger issues to address with the group design in your environment.

      You should also look to run a script to assess if you have an issue with circular nested groups. This will add to your problem. I’ve posted a link to a script by Richard Mueller on my blog here:

      What I can do though is create another column in the csv to flag that it’s reached its maximum and wrap more of an explanation around it.

      Typically I only see large group membership issues for Sys Admins and Power Users, so I wouldn’t imagine an environment hitting this limitation for standard users.

      Interested to know more about your environment.

      Thanks for the feedback.


  • Mike Crampton

    Hey, thanks for this script, it ran perfectly on the first try. We had several users who were getting a kerberos-security token size error over 12000 earlier in the day. We got them logged on and working by adding the token size registry key as Microsoft documents, but wanted to see if there were any other users likely to have the same issue. Hence your script. However, your script reports that no users in the domain have a token size over 12000, and the largest is 5952 (one of the users who had the issue earlier). Any guesses on why the local computer might report a token size issue if the AD does not think there is?

    • Hi Mike,

      Thanks for the feedback.

      Is this a single domain flat forest, or do users belong to groups from other domains/forests? My script only calculates the groups the user belongs to from the current domain. In cases where there is cross-domain/forest group membership, Microsoft suggests doubling my calculated token size, which would pretty much make sense.

      My script doesn’t calculate the token at the local computer level. The Microsoft formula used makes an assumption about that. It’s not accurate, but I can’t see this being an issue.

      So of the users that your computers are saying have a token size of over 12000, what is the break down of their group membership? ie. How many Domain Local groups are they a member of?

      What OS are your DC’s and what the Domain Level they are running at?

      What’s your client OS that’s reporting the token size of over 12000?


  • Jaxelos .

    Thank you for the very nice script and this article.
    Tested this script with a PS4.0 but it will only output 25 rows ?

    Tested 3 times and every time the max number of rows is 25.

    At the end I get :

    – Processed 1706 user accounts.
    – 1706 have a calculated token size of less than or equal to 12000 bytes.
    – These users are good.
    – 0 have a calculated token size larger than 12000 bytes.
    – 0 have a calculated token size larger than 48000 bytes.
    – USERX has the largest calculated token size of 6976 bytes in the xxx.NET domain.

    Is this by design ?
    I would still like to get the list made complete even if I have no accounts with token size bigger than the rest.

    • Hi Jaxelos,

      Yes, this is by design. It only writes out to file users that have a token size greater than the $TokensSizeThreshold. So just set this to 100 and you’ll pretty much get everything. I uploaded a new version yesterday, so make sure you’re running the latest.


      • Jaxelos .

        Thanks for the fast reply.
        I will try the new one today.

        The problem I see on my side is VERY strange.
        I run the script and ALL my users tokens are actually under 7000 as you can see, but I still get the errors in event log and can not access \domainnamenetlogon or other shares on DC’s and I still get the message :

        The Kerberos SSPI package generated an output token of size 13964 bytes, which was too large to fit in the token buffer of size 12000 bytes, provided by process id 4.

        If I look at the token size calculated by your script , his token size is only 6976

        I am pretty confused at this point 🙁

        • Do you have a multi-domain/forest, root domain, and/or trust relationships in place where these users are potentially in nested or direct members of cross domain/forest groups? My script and any others you find, cannot calculate that. Microsoft says that under these situations you should double your token size, which would more or less make sense for your top users. So if you have 25 users with tokens between 6,000 and 7,000, are these the ones causing issues?

          Of the user(s) your seeing issues with, what’s their group membership breakdown? Do they have a sidHistory?



          • Jaxelos .

            Yes, we have a Forest transitive trust with our “Test” domain but no SID History.
            And the issue is right now with this top 20-30 users yes, as far as I know, did not hear from other users until now.

            As far as I can see we do not use cross domain group membership. Trust was set up long before my time so I am just starting to dig into this problem as I overtook the setup very recently.

            Is there any article from MS regarding the token size when talking forest groups ? Could not find anything related to this until now but your statement is very close to what I am seeing with calculated token size of 6-7000 but seeing almost double values in the event logs.

          • A transitive trust means that they are in the same forest. That’s typically not the case with Dev, Test and Pre-prod environments. So I would say it’s a Forest trust, and it really should only be one-way. ie. Test should trust Prod users, but Prod should not trust Test users. What you may find is that a group from Prod is nested in a group in Test, giving those Prod users the ability to test systems in Test.

            KB327825 eludes to groups outside the user’s account domain, but is not thorough simply because there is no accurate calculation. I have had a conversation with the person at Microsoft that created their tools for the ADRAP. When Microsoft report it during the ADRAP, if they find a Trust, they simply double the estimated token size.

            I could have done that too, but then it becomes misleading. My script provides as accurate as possible, the token size for the account domain only. I do not take into consideration trusts. I probably need to state this more clearly in the article.

            There is no real way to calculate cross domain group memberships using PowerShell, unless you write a new function in C#. The only way I believe is to invoke dsquery. I have not gone to these lengths as yet.


          • Jaxelos .

            And I have not accounts trusted for delegation.

  • Claus Olsen

    I did some test with the other script also mentioned here: checkmaxtokensize.ps1, here I got a number:

    Total estimated token size is 6408.
    For access to DCs and delegatable resources the total estimated token
    delegation size is 12816.
    Effective MaxTokenSize value is: 65535
    WARNING: The token was large enough that it may have problems when being used for Kerberos delegation or for access to A
    ctive Directory domain controller services. Alter the maximum size per KB and con
    sider reducing direct and transitive group memberships.

    My question to this is: Why is 6408/12816 a problem. Also if I do a manually calculation of tokensize after the formula:

    TokenSize = 1200 + 40 d + 8s, if not using ths S variable, i get 9648 for a user here (which have my concern). Still not sure if I calculate it correct but maybe not that important. So now to your script which I like (looks more transparent) , but I want to test it with only 1 user.
    Is that possible ?
    Also I am woundering why that same user which have my concern here having problems with acces to a site because of tokensize (the site use basic auth) can log in to another similar site with same basic auth enabled .

    I am pretty sure its the tokensize here. Also I did raise the maxtokensize on the server, still same result not working for 1 user (not able to connect and only that user)
    Only answer if you can find the time.
    Cheers Claus

    • Hi Claus,

      I’ve uploaded a new version of the script so that you can run it against a single user account. Instructions are in the script header.

      KB327825 suggests that in scenarios in which delegation is used Microsoft recommends that you double the token size. Unlike my script, the checkmaxtokensize.ps1 script doesn’t check for delegation, therefore he outputs a line to state that it “may” be double. I specifically do a check to see if the account has been trusted for delegation.

      My script simply calculates the token of the account domain. But if you have trusts and across domain/forest group memberships, then this will also increase your token size. This is something that cannot be calculated. So Microsoft say in these situations you should double the estimated token size.

      I assume you’re taking about an IIS web site here. There is a bit more too it than just setting the MaxTokenSize on an IIS web server. See here for example:

      Hope that helps.


  • leednc

    First, thank you for this very well constructed script. I am late to the game learning powershell and this is awesome.

    In our environment we have some very old accounts that were created through some mainframes apps and start with the “$” character. When I try to run the report against a single user like this: “.get-tokensizereport.ps1 -Accountname:$abcd ” (without the quotes) it just starts scanning my entire 12000+user environment. I am assuming the dollar sign is throwing off the logic, how do I get the script to not read $ as a variable?
    Thank you again.

    • Hi Lee,

      Thanks for the feedback.

      You should just be able to escape it like this…



      • leednc

        I tried with a space after the colon and no space after it and get the same response, a new command prompt. Tried running it in the ISE and it goes to a new prompt and say Completed down below.

        .Get-TokenSizeReport.ps1 -accountname:”‘$abcd'”

        • Hi Lee,

          Did you try using the escape character ` as the example I posted?


          • leednc

            Yes sir. Typed ” first then ‘ and closed with ‘ first and ” second.

          • Then you haven’t followed what I wrote. I never suggested single quotes. Just an Escape character in front of the $ character.

          • leednc

            I thought I did. is that the tilde character or a single quote?

          • No, the PowerShell escape character is the grave-accent (`), which is typically on the same key as the tilde (~) character.

  • Pingback: Kerberos Token and Max Token Size – Group membership limits | Jacques DALBERA's IT world()

  • Andreas

    Hi, thanks for the script!
    I get this error multiple Times for each account, any idea why this happens?

    At C:TempGet-TokenSizeReport.ps1:347 char:50
    + if ($GroupSid -match $DomainSID)
    + ~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (DomainSID:String) [], Runtime
    + FullyQualifiedErrorId : VariableIsUndefined
    The variable ‘$DomainSID’ cannot be retrieved because it has not been set.

    • Hi Andreas,

      Are you running it from an Administrative Command Prompt or PowerShell session?

      Have you downloaded the script, or have you just copied the code?

      Anyway, just in case there is something wrong with the format of the code on my site, I uploaded a new version. Download the script and have another go.


      • Andreas

        It works now, thanks!!

  • Matt Johnsen

    Jeremy, I’m having issues running out of memory on my PowerShell session with your excellent script. I’ve updated the PS Memory to 2048 and increased the memory on the terminal server I’m running it with. I get to about 2500 users (some OU’s have over 5k) and it errors out.
    Is there a way to streamline this to handle tens of thousands of users?

    • Hi Matt, I’ve tweaked the script as much as possible and have exhausted all avenues I can think of. It comes down to the available memory in the machine you’re running it from. For tens of thousands of user accounts I reckon you’ll need 16+GB of free memory. It’s difficult to tell, but you can watch it grow in Task Manager to get an idea of what it needs. Alternatively you can set the $OUStructureToProcess variable to limit the enumerated users to a specific OU structure. Cheers, Jeremy

      • Matt Johnsen

        You were 100% correct, upgrading the server to PowerShell V4 resolved the memory issue. Thank you very much!

  • Surry

    Hi, The script is brillian!!! I like to much.

    I dont know why I am getting this issue on Windows 2012 R2
    On Windows 20008 R2 everything is perfect.

    I think that the report is fine, I think only is to write on screen the percentage. Jeremy, Is it true?

    Checking the token of user User1 in domain xxxxxxxxx

    There are 58 groups in the token.

    – 2 are domain local security groups.
    – 48 are domain global scope security groups inside the users domain.
    – 0 are domain global scope security groups outside the users domain.
    – 4 are universal security groups inside the users domain.
    – 4 are universal security groups outside the users domain.
    The primary group is Domain Users.
    There are 0 SIDs in the users SIDHistory.
    The current userAccountControl value is 512.
    Token size is 1856 and the user is not trusted for delegation.

    Attempted to divide by zero.
    At C:admScriptsGet-TokenSizeReport.ps1:559 char:7
    + $percent = “{0:P}” -f ($TotalUsersProcessed/$UserCount)

    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [], RuntimeException
    + FullyQualifiedErrorId : RuntimeException

    Processed 1907 of user accounts = complete.

    Attempted to divide by zero.
    At C:admScriptsGet-TokenSizeReport.ps1:334 char:7
    + Write-Progress -Activity ‘Processing Users’ -Status (“Username: {0}” -f $s …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [], RuntimeException
    + FullyQualifiedErrorId : RuntimeException

    Checking the token of user user2 in domain xxxxxxxxxx
    There are 40 groups in the token.
    – 2 are domain local security groups.

    • Hi Surry,

      Thanks for the great feedback. I had uploaded an older version that had a bug. I’ve now fixed this.

      Please download again and try it out.


  • Hi Tommy,

    This is one of my older PowerShell scripts before I started using “Set-StrictMode -Version 2.0”. It’s strage because I’ve never had that issue, nor has it been reported to me, so it might be part of your PowerShell profile. Either way I’ve updated the script to allow for this.


  • Hi MagnusMagnus,

    Just use “OU=111111,OU=Kunder”. Drop the Domain DN.


  • Alex

    Hi Jeremy.
    I am running your script i domain with 54000 enabled users.
    Windows Server 2012 R2 on ESX with 96 GB RAM.
    After script completes checking about 10% i see it consumed ~90 GB of RAM.
    If i allow it to run “till the end”, i see this powershell process killed by system.
    I’ve changed:
    “$arrayoftopusers = @()” to “$arrayoftopusers = New-Object System.Collections.ArrayList”
    “$arrayoftopusers += $obj” to “$arrayoftopusers.Add($obj)”
    But it does not helped me much, so looks like the DirectorySeracher causing such behavior.

    What is the issue could be there?

    • Hi Alex,

      The larger the $arrayoftopusers object, the more memory PowerShell will consume. It needs to build the array to calculate and present you with a list of large tokens based on the $TopUsers variable, which is 200 by default.

      However, it only adds users to the $arrayoftopusers object if their token size has been calculated to be above the $TokenSizeThreshold value, which is 6000 by default.

      Therefore change the $TokenSizeThreshold value from 6000 to so 12000 as an attempt to limit the array.

      See how that goes.


  • Nissan

    Hi Jeremy,
    I found your script very useful for checking all our AD users (4500) but came with a strange results
    with the estimated token size value.
    so I run your script on one user in our domain, and also run MS CheckMaxTokenSize for the
    same user and there a big difference:

    your Get-TokenSizeReport reports – 4280
    MS CheckMaxTokenSize reports – 10120
    also run Tokensz.exe for this user and it reports – 9875
    pleas, your help is strongly needed.
    see my runs on the attached image.

    • Hi Nissan,
      Sorry for the delay in responding. I’ve been very busy on multiple consulting engagements. I’ll look into this. I would suggest it will either be due to non-transitive trust relationship or SID History of groups. I’m working for a customer that has both, so can double check. Will update you during the week.

      • Nissan

        Hi Jeremy,
        Thanks for your reply, waiting curiously for your check.

        • Hi Nissan,
          I have uploaded an updated version of the script. I’ve made a heap of changes and addressed the SID history calculation issue. Let me know how it goes.

          • Nissan

            Hi Jeremy,
            That is brilliant !!!
            SID history calculation really made the sense and the numbers are now looks accurate.
            Thank you very much for a persistence help.


          • No problems Nissan. Thanks for the feedback.

  • Hi Maff,

    I’ve uploaded a new version which definitely outputs the CSV for a user. It will only output per use when you specify the -AccountName parameter.

    You need to use the syntax:
    Get-TokenSizeReport.ps1 -AccountName:

    For Example:

    Get-TokenSizeReport.ps1 -AccountName:mmace

    I hope that helps.


    • Maff Mace

      Brilliant, thank you very much.
      I have noticed one issue, all calculations with your script are around 5,600 out from what we get with the MS script, seems that they include “claims” and theirs seems to be accurate as our users that are over 12,000 with theirs but around 6,500 with yours are having issues until we remove them from loads of legacy groups.
      For now, I’ve added 5,600 to the formula but this will be a different number for each user.

      • Hi Maff,

        I’d like to see the output from both scripts for at least 1 user. Unsure which Microsoft script you’re referencing, but I’ve done side-by-side testing with most scripts and processes published, and they all calculate a very similar total. Can you e-mail the screen output and CSV after processing a user to jeremy at


  • Pingback: Token Bloat – Cannot generate SSPI context – Marcelo's Spaces()

  • Ryan Smyth

    Hi Jeremy,
    Quick question please.. 🙂
    I have just used your updated script and appears that the memberships column does not add the groupSIDHistory column anymore?
    See below your csv will output group membership as 543 where before it would output 1037?
    Just trying to understand the change..For a user to hit the hard limit of 1015 should we use 543 or 1037? Thinking that 1037 is the nested membership so that is actually not an issue and we only need to be concerned when the group membership of 534 hits 1015?

    VERBOSE: Checking the token of user **** in domain ******
    VERBOSE: – There are 1037 principals in the token. This is made up of:
    VERBOSE: – 543 groups
    VERBOSE: – 37 are domain local security groups
    VERBOSE: – 14 are domain global scope security groups inside the users domain
    VERBOSE: – 0 are domain global scope security groups outside the users domain
    VERBOSE: – 492 are universal security groups inside the users domain
    VERBOSE: – 0 are universal security groups outside the users domain
    VERBOSE: – 494 SIDs in the SIDHistory
    VERBOSE: – 1 SIDs are in the users SIDHistory
    VERBOSE: – 493 SIDs are in the users group SIDHistory
    VERBOSE: – The current userAccountControl value is 512
    VERBOSE: – The account is enabled
    VERBOSE: – The account is not trusted for delegation
    VERBOSE: – The primary group is Domain Users
    VERBOSE: – Therefore the estimated Token size is 26488
    Many Thanks

    • Hi Ryan,

      The output looks good to me. I probably need to update the article to explain it further the way it’s now broken down in the output.

      It’s all to do with the number of SIDs/security principals in the users token, which is still 1037. The break up of these are 543 actual groups plus 494 entries in the SIDHistory. 1 SIDHistory entry is on the user account itself, and the other 493 SIDHistories are in the 543 groups that the user is a member of. The 543 groups include direct and nested groups.

      SIDHistory is there for migration purposes and to be cleaned up afterwards. Not to be left in-place 🙂

      If you run the script using the -AccountName specifying the username in the example you’ve provided, it’ll output a CSV that you can open in Excel that will make more sense.

      Let me know if I haven’t explained this correctly.


      • Ryan Smyth

        Perfect thanks Jeremy that is what I thought. I should not have said nested membership but rather SID history. Again great job on the script thank you! Currently using to run on around 25k users, takes a few hours to run but does a good job generating a monthly report for the helpdesk! 🙂

        • You’re welcome Ryan.Thanks for the feedback and support 🙂

Previous post:

Next post: