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 (166 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>

  Script Name: Get-TokenSizeReport.ps1
  Release 2.1
  Modified by 01/12/2014
  Written by 13/12/2013

  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:

  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 sIDHistory.

  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 a pure Windows Server

  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
    - However, tokenGroups 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
    - 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 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:
  - 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.
  - Work out how to report on cross-forest/domain group memberships as neither tokenGroups or
    GetAuthorizationGroups() can achieve this.


Set-StrictMode -Version 2.0


# 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 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 value to true to use the tokenGroups attribute
$UseTokenGroups = $True

# Set this value to true to use the GetAuthorizationGroups() method
$UseGetAuthorizationGroups = $False

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

# Set this to remove the double quotes from each value within the
# CSV.
$RemoveQuotesFromCSV = $False

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


Function Get-UserPrincipal($cName, $cContainer, $userName)
  $dsam = "System.DirectoryServices.AccountManagement" 
  $rtn = [reflection.assembly]::LoadWithPartialName($dsam)
  $cType = "domain" #context type
  $iType = "SamAccountName"
  $dsamUserPrincipal = "$dsam.userPrincipal" -as [type]
  $principalContext = new-object "$dsam.PrincipalContext"($cType,$cName,$cContainer)
} # end Get-UserPrincipal

Function Test-DotNetFrameWork35
 Test-path -path 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5'
} #end Test-DotNetFrameWork35
Function Test-DotNetFrameWork4
 Test-path -path 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full'
} #end Test-DotNetFrameWork4

If(-not(Test-DotNetFrameWork35) -AND -not(Test-DotNetFrameWork4)) { "Requires at least .NET Framework 3.5" ; exit }

$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] {
    write-host -ForegroundColor red $_.Exception.Message

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

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

$arrayoftopusers = @()
$TotalUsersProcessed = 0
$UserCount = 0
$GroupCount = 0
$LargestTokenSize = 0
$TotalGoodTokens = 0
$TotalTokensBetween8and12K = 0
$TotalLargeTokens = 0
$TotalVeryLargeTokens = 0

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 all enabled users
  $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 = "SUBTREE"
$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) {
$Users = $ADSearcher.Findall()
$UserCount = $users.Count

if ($UserCount -ne 0) {
  $Users | ForEach-Object {
    $lastLogonTimeStamp = ""
    $lastLogon = ""
    $UserDN = $_.Properties.distinguishedname[0]
    $samAccountName = $_.Properties.samaccountname[0]
    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 = ""
    $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 SID history
    $SIDCounter = 0
    if ($(Try{$_.Properties.sidhistory -ne $null}Catch{$False})) {
      foreach ($sidhistory in $_.Properties.sidhistory) {
        $SIDHistObj = New-Object System.Security.Principal.SecurityIdentifier($sidhistory, 0)
        #Write-Host -ForegroundColor green $SIDHistObj.Value "is in the SIDHistory."
      $SIDHistObj = $null

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

    # Use TokenGroups Attribute
    If ($UseTokenGroups) {
      $UserAccount = [ADSI]"$($_.Path)"
      $UserAccount.GetInfoEx(@("tokenGroups"),0) | Out-Null
      $ErrorActionPreference = "continue"
      $groups = $UserAccount.GetEx("tokengroups")
      if ($Error) {
        Write-Warning "  Tokengroups not readable"
        $Groups=@()   #empty enumeration
      $GroupCount = 0
      # Note that the tokengroups includes all principals, which includes siDHistory, so we need
      # to subtract the sIDHistory count to correctly report on the number of groups in the token.
      $GroupCount = $groups.count - $SIDCounter

      $SecurityDomainLocalScope = 0
      $SecurityGlobalInternalScope = 0
      $SecurityGlobalExternalScope = 0
      $SecurityUniversalInternalScope = 0
      $SecurityUniversalExternalScope = 0

      foreach($token in $groups) {
        $GroupSIDBytes = 0
        $GroupSIDHistoryCount = 0
        $principal = New-Object System.Security.Principal.SecurityIdentifier($token,0)
        $GroupSid = $principal.value
        #$group = $principal.Translate([System.Security.Principal.NTAccount])
        $grp = [ADSI]"LDAP://<SID=$GroupSid>"
        if ($grp.Path -ne $null) {
          $grpdn = $grp.distinguishedName.tostring().ToLower()
          $grouptype = $grp.groupType.psbase.value
          $ValidGroup = $False
          switch -exact ($GroupType) {
            "-2147483646"   {
                            # Global security scope 
                            if ($GroupSid -match $AccountDomainSid) 
                            } else { 
                              # Global groups from others.
                            $ValidGroup = $True
            "-2147483644"   { 
                            # Domain Local scope 
                            $ValidGroup = $True
            "-2147483643"   { 
                            # Domain Local BuiltIn scope
                            $ValidGroup = $True
            "-2147483640"   { 
                            # Universal security scope 
                            if ($GroupSid -match $AccountDomainSid) 
                            } else { 
                              # Universal groups from others.
                            $ValidGroup = $True
          If ($ValidGroup) {
            #write-host "$($grpdn)"
            $GroupSIDBytes = $GroupSIDBytes + $principal.BinaryLength + 16
            #write-host " - Number of bytes in SID: $($principal.BinaryLength + 16) (incliding overhead)"
            If (($grp.sidhistory.psbase.value | Measure-Object).Count -gt 0) {
              $grp.sidhistory.psbase.value | ForEach-Object {
                $GroupSIDHistoryCount = $GroupSIDHistoryCount + 1
            #write-host " - Number of SIDs in SIDHistory: $GroupSIDHistoryCount"

    # Use GetAuthorizationGroups() Method
    If ($UseGetAuthorizationGroups) {

      $userPrincipal = Get-UserPrincipal -userName $SamAccountName -cName $domain -cContainer "$OU"

      $GroupCount = 0
      $SecurityDomainLocalScope = 0
      $SecurityGlobalInternalScope = 0
      $SecurityGlobalExternalScope = 0
      $SecurityUniversalInternalScope = 0
      $SecurityUniversalExternalScope = 0

      # Use GetAuthorizationGroups() for Indirect Group MemberShip, which includes all Nested groups and the Primary group
      Try {
        $groups = $userPrincipal.GetAuthorizationGroups() | select SamAccountName, GroupScope, SID
        $GroupCount = $groups.count

        foreach ($group in $groups) {
          $GroupSid = $group.SID.value

          switch ($group.GroupScope)
              "Local" {
                # Domain Local & Domain Local BuildIn scope
              "Global" {
                # Global security scope 
                if ($GroupSid -match $AccountDomainSid) {
                } else { 
                  # Global groups from others.
                "Universal" {
                # Universal security scope 
                if ($GroupSid -match $AccountDomainSid) {
                } else {
                  # Universal groups from others.
      Catch {
        write-host "Error with the GetAuthorizationGroups() method: $($_.Exception.Message)" -ForegroundColor Red

    If ($ConsoleOutput) {
      Write-Host -ForegroundColor green "Checking the token of user $SamAccountName in domain $domain"
      Write-Host -ForegroundColor green "There are $GroupCount groups in the token."
      Write-Host -ForegroundColor green "- $SecurityDomainLocalScope are domain local security groups."
      Write-Host -ForegroundColor green "- $SecurityGlobalInternalScope are domain global scope security groups inside the users domain."
      Write-Host -ForegroundColor green "- $SecurityGlobalExternalScope are domain global scope security groups outside the users domain."
      Write-Host -ForegroundColor green "- $SecurityUniversalInternalScope are universal security groups inside the users domain."
      Write-Host -ForegroundColor green "- $SecurityUniversalExternalScope are universal security groups outside the users domain."
      Write-host -ForegroundColor green "The primary group is $primarygroupname."
      Write-host -ForegroundColor green "There are $SIDCounter SIDs in the users SIDHistory."
      Write-Host -ForegroundColor green "The current userAccountControl value is $UACValue."

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

    # 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 + $SIDCounter)) + (8 * ($SecurityGlobalInternalScope  + $SecurityUniversalInternalScope))
    if ($TrustedforDelegation -eq $false) {
      If ($ConsoleOutput) {
        Write-Host -ForegroundColor green "Token size is $Tokensize and the user is not trusted for delegation."
    } else {
      $TokenSize = 2 * $TokenSize
      If ($ConsoleOutput) {
        Write-Host -ForegroundColor green "Token size is $Tokensize and the user is trusted for delegation."

    If ($TokenSize -le 12000) {
      $TotalGoodTokens ++
      If ($TokenSize -gt 8192) {
        $TotalTokensBetween8and12K ++
    } elseIf ($TokenSize -le 48000) {
      $TotalLargeTokens ++
    } else {
      $TotalVeryLargeTokens ++

    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 "SIDHistory" -value $SIDCounter
      $obj | Add-Member -MemberType NoteProperty -Name "UACValue" -value $UACValue
      $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


    If ($ConsoleOutput -AND $ProcessSingleAccount -eq $False) {
      $percent = "{0:P}" -f ($TotalUsersProcessed/$UserCount)
      write-host -ForegroundColor green "Processed $TotalUsersProcessed of $UserCount user accounts = $percent complete."
      Write-host " "

  If ($OutputSummary) {
    Write-Host -ForegroundColor green "Summary:"
    Write-Host -ForegroundColor green "- Processed $UserCount user accounts."
    Write-Host -ForegroundColor green "- $TotalGoodTokens have a calculated token size of less than or equal to 12000 bytes."
    If ($TotalGoodTokens -gt 0) {
      Write-Host -ForegroundColor green "  - These users are good."
    If ($TotalTokensBetween8and12K -gt 0) {
      Write-Host -ForegroundColor green "  - Although $TotalTokensBetween8and12K of these user accounts have tokens above 8K and should therefore be reviewed."
    Write-Host -ForegroundColor green "- $TotalLargeTokens have a calculated token size larger than 12000 bytes."
    If ($TotalLargeTokens -gt 0) {
      Write-Host -ForegroundColor green "  - These users will be okay if you have increased the MaxTokenSize to 48000 bytes.`n  - Consider reducing direct and transitive (nested) group memberships."
    Write-Host -ForegroundColor red "- $TotalVeryLargeTokens have a calculated token size larger than 48000 bytes."
    If ($TotalVeryLargeTokens -gt 0) {
      Write-Host -ForegroundColor red "  - These users will have problems. Do NOT increase the MaxTokenSize beyond 48000 bytes.`n  - Reduce the direct and transitive (nested) group memberships."
    Write-Host -ForegroundColor green "- $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

  # Remove the quotes from the output file.
  If ($RemoveQuotesFromCSV) {
    (get-content "$ReferenceFile") |% {$_ -replace '"',""} | out-file "$ReferenceFile" -Fo -En ascii
    If ($ProcessSingleAccount -eq $False) {
      (get-content "$ReferenceFileTopUsers") |% {$_ -replace '"',""} | out-file "$ReferenceFileTopUsers" -Fo -En ascii


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?


    • Jeremy

      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.


    • Jeremy

      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.



    • Jeremy Saunders

      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!

        • Jeremy Saunders

          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.

        • Jeremy Saunders

          Awesome. Thanks for testing that.

  • 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?

    • Jeremy Saunders

      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.

        • Jeremy Saunders

          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.

      • Jeremy Saunders

        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.

    • Jeremy Saunders

      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?

    • Jeremy Saunders

      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.

    • Jeremy Saunders

      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 :(

        • Jeremy Saunders

          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.

          • Jeremy Saunders

            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

    • Jeremy Saunders

      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.

    • Jeremy Saunders

      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'”

        • Jeremy Saunders

          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.

          • Jeremy Saunders

            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?

          • Jeremy Saunders

            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.

    • Jeremy Saunders

      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?

    • Jeremy Saunders

      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!

        • Jeremy Saunders

          Thanks for the feedback Matt :-)

  • 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.

    • Jeremy Saunders

      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.


  • Jeremy Saunders

    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.


  • Jeremy Saunders

    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?

    • Jeremy Saunders

      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.

    • Jeremy Saunders

      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.

Previous post:

Next post: