Workflow 401 Access Denied Troubleshooter Script

I’ve compiled a script that can be used to validate some of the common issues with workflows getting 401 access denied beyond the user running the workflow doesn’t have proper permissions on the list or library.

Script Location:
https://github.com/BrentPerson/workflowAuthTS

What the Script will do:

  1. WFM Trust
    1. Check and validate the token signing certificate for the WFM trusted security token issuer against the WFM metadata endpoint listed in the trust
      1. It will also check the entire certificate chain if one exists
    2. It will also check each cert in the chain if it exists in the trusted root authority
    3. It will also ask you if you want to attempt to fix the issue by running the timer job “Refresh Security Token Service Metadata Feed”
      1. There is a 20 second sleep after starting the timer job before we check the trust again
      2. This will loop until the trust is valid or we tell the script we do not want to attempt to fix the issue
  2. User Profiles
    1. This will check for the existence of a user profile(s) based on the provided, SPS-Userprincipalname, WorkMail, and/or SID properties.
    2. It will display the profile count and ask you if you want to display the profiles
      1. If you say yes it will list each profile object to console otherwise it’ll break out of script
  3. Workflow App Only Policy
    1. This will prompt for a web url so we can check if the workflow app only policy feature is activated
      1. If it is activated it will list all the workflow app ID’s listed for the web
      2. If it’s not activated it will ask you if you want to try and activate the feature
        1. If you do it will attempt to enable the feature for the web otherwise it’ll break out of script

ULS Log Examples when this script would be useful

  • #3 Workflow App Only Policy
    • We can see that the Oauth request is an app only request but our app is not allowed to use the apponly policy

      OAuth app principal Name=i:0i.t|ms.sp.ext|6966b269-8a6c-4fb3-8b16-234f8deadf6c@43d4a423-c0c5-49d1-943b-c808c34350d6, IsAppOnlyRequest=True, UserIdentityName=0i.t|00000003-0000-0ff1-ce00-000000000000|app@sharepoint, ClaimsCount=11

      TenantScopedPerm=0, AllowAppOnlyPolicy=False, AppId=i:0i.t|ms.sp.ext|6966b269-8a6c-4fb3-8b16-234f8deadf6c@43d4a423-c0c5-49d1-943b-c808c34350d6.

      It’s app only request, but the app i:0i.t|ms.sp.ext|6966b269-8a6c-4fb3-8b16-234f8deadf6c@43d4a423-c0c5-49d1-943b-c808c34350d6 is not allowed to use app only policy.
  • #2 User Profiles Duplicate UPN’s
    • We can see below that we match on more than one profile based on the SPS-UserPrincipalName property when we should only match on one profile.

      Error trying to search in the UPA. The exception message is ‘Microsoft.Office.Server.UserProfiles.DuplicateEntryException: GetUserProfileByPropertyValue: Multiple User Profiles found with propertyName ‘SPS-UserPrincipalName’ of specified value

      The set of claims could not be mapped to a single user identity. Exception GetUserProfileByPropertyValue: Multiple User Profiles found with propertyName ‘SPS-UserPrincipalName’ of specified value has occured.
  • #2 User Profile doesn’t exist by UPN
    • We can see below that we did not find any profiles for the identity claim specified
    • When using OAuth with user context we must lookup the users profile to augment the claims in the OAuth token and we must only get one profile matching the UPN in the OAuth token.

      Couldn’t find a user using property ‘{0}’ of value ‘{1}’.

      Identity claims mapped to ‘0’ user profiles.

      UserProfileException caught.. Exception Microsoft.Office.Server.Security.UserProfileNoUserFoundException: 3001002;reason=The incoming identity is not mapped to any user profile account in SharePoint. Possible cause is that no user profiles are created in user profile database. Contact your administrator.
  • #1  Can’t resolve the issuer of the OAuth token
    • We can see below that we are unable to resolve the issuer of the token.
    • When dealing with Workflows and OAuth the issuer is the WFM outbound certificate.
    • We will try to match the thumbprint to a trusted security token issuer.

      SPApplicationAuthenticationModule: Invalid token or signature. Exception: System.IdentityModel.Tokens.SecurityTokenException: Invalid JWT token. Could not resolve issuer token.

SharePoint and OAuth Troubleshooting

Troubleshooting access denied issues when publishing/consuming service applications.  The two Service application I deal with the most in this type of scenario is the User Profile Service, and The Search Service Application.

For the User Profile Service Application its usually when following sites where the S2S (Server to Server) OAuth authentication will throw a 401 access denied.  Or the lovely and most helpful error “Sorry, something went wrong.”

For the Search service application it would be issuing search queries, where you won’t get the results expected due to security trimming, or you’ll get access denied/Sorry, something went wrong when issuing the search query.

I won’t dive into either of those particular issues in this article, but watch for future OAuth blogs in regards :).  This particular blog is about making our troubleshooting easier when working any OAuth access issues.  Especially when there are multiple farms involved.

I have put together a series of PS Scripts that we can use every time we are troubleshooting an OAuth access issue.

This is a two part process as the second part will require Microsoft excel to be on the Box we run the create workbook script from.

Since we will be getting the outputs directly from the SharePoint server there’s a good chance they do not have office client bits installed.

 Part 1 (create the CSV files per farm)

<#
--This script is provided AS-IS and with no warranty. 
--Please review the file locations before running this script
--Run this script for both publishing and consuming farms
--update the physical location for the CSV export 
--so we know which set of outputs is for which farm
--Or change the name of the output csv files to signify which farm they are a part of.
#>
#Create our CSV array's
$rootauthcsvList = @()
$STSConfigcsvList = @()
$SecTokencsvList = @()
$ServiceTokencsvList = @()

#Create Variables
$STSConfig = Get-SPSecurityTokenServiceConfig
$TrustedSecurityTokenServices = $stsconfig.TrustedSecurityTokenServices | select-object Name -ExpandProperty Name
$TrustedAccessProviders = $stsconfig.TrustedAccessProviders | select-object Name -ExpandProperty Name
$RootAuth = Get-SPTrustedRootAuthority
$SecurityTokenIssuers = Get-SPTrustedSecurityTokenIssuer
$ServiceTokenIssuers = Get-SPTrustedServiceTokenIssuer
$additionalSigningCerts = $null
$fileloc = "C:\Temp\FarmA\FarmA.xlsx"
$CSVExportLocation = Read-Host -Prompt "Enter the file location to export the CSV files to"

#Begin the data gathering
#============STS Config=============#
$STSConfigcsvList += [PSCustomObject][Ordered]@{
RegisteredIssuerName = $stsconfig.NameIdentifier
AllowOAuthOverHttp = $stsconfig.AllowOAuthOverHttp
AllowMetaDataOverHttp = $stsconfig.AllowMetadataOverHttp
TrustedSecurityTokenServices = "$($TrustedSecurityTokenServices)"
TrustedAccessProvider = "$($TrustedAccessProviders)"} | Export-CSV "$CSVExportLocation\STSConfig.csv" -Append -NoTypeInformation;

#============Root Authority (Trusted Certificates)=============#
Foreach($root in $rootauth)
{
   $rootauthcsvList += [PSCustomObject][Ordered]@{
   Root_Authority_Name = $root.name
   Root_Cert_Thumbprint = $root.certificate.Thumbprint
   Root_Cert_Subject = $root.Certificate.Subject
   Root_Cert_Issuer = $root.Certificate.Issuer
   Root_Cert_EXPDate = $root.Certificate.NotAfter} | Export-CSV "$CSVExportLocation\trustedrootauthorities.csv" -Append -NoTypeInformation;
}

#============Security Token Issuers (S2S Principal Trusts)=============#
Foreach($secIssuer in $securitytokenissuers)
{
   $additionalSigningCerts = $secIssuer.AdditionalSigningCertificates
   If($additionalSigningCerts -ne $null)
   {
       $additionalCertThumbPrints = $secIssuer.AdditionalSigningCertificates | Select-Object thumbprint -ExpandProperty thumbprint
       $additionalCertSubjects = $secIssuer.AdditionalSigningCertificates | Select-Object subject -ExpandProperty subject
   }
   $SecTokencsvList += [PSCustomObject][Ordered]@{
   TokenIssuerName = $secIssuer.Name
   TokenIssuerNameID = $secIssuer.RegisteredIssuerName
   TokenIssuerCertificateThumbprint = $secIssuer.SigningCertificate.Thumbprint
   TokenIssuerCertificateIssuer = $secIssuer.SigningCertificate.Issuer
   TokenIssuerCertificateSubject = $secIssuer.SigningCertificate.Subject
   TokenIssuerCertificateEXPDate = $secIssuer.SigningCertificate.NotAfter
   "TokenIssuerAdditionalCertSubject(s)" = "$additionalCertSubjects"
   "TokenIssuerAdditionalCertThumbprint(s)" = "$additionalCertThumbPrints"
   TokenIssuerMetaDataEndPoint = $secissuer.MetadataEndPoint} | Export-CSV "$CSVExportLocation\trustedSecTokenIssuers.csv" -Append -NoTypeInformation;
   #clear out the the objects
   $additionalCertSubjects = $null
   $additionalCertThumbPrints = $null
   $additionalSigningCerts = $null
}

#============Service Token Issuers (farm trusts)=============#
Foreach($serviceIssuer in $ServiceTokenIssuers)
{
   $ServiceTokencsvList += [PSCustomObject][Ordered]@{
   ServiceTokenIssuerName = $serviceIssuer.Name
   TokenIssuerCertificateThumbprint = $serviceIssuer.SigningCertificate.Thumbprint
   TokenIssuerCertificateIssuer = $serviceIssuer.SigningCertificate.Issuer
   TokenIssuerCertificateSubject = $serviceIssuer.SigningCertificate.Subject
   TokenIssuerCertificateEXPDate = $secIssuer.SigningCertificate.NotAfter} | Export-CSV "$CSVExportLocation\trustedServiceTokenIssuers.csv" -Append -NoTypeInformation;
}

 

Part 2 (Merge CSV’s into a single Excel Workbook)

<#
--This script is provided AS-IS and with no warranty. 
--This script requires Microsoft Excel to be installed on the machine this script is executed on
--Run this script for each farms CSV output files
--Make sure to separate your farms CSV files into separate folders as this script grabs all CSV files in the location specified
#>

$WorkBookLoc = Read-Host -Prompt "Enter the file location, including filename.filetype where you want to create the Excel Workbook: Example C:\temp\PublishingFarm.xlsx"
$csvfilelocation = Read-Host -Prompt "Enter the folder location where the CSV files reside:   Exmaple C:\temp\PubFarmCSV   **Do Not Include a trailing backslash or quotes"
$CSVFiles = Get-ChildItem $csvfilelocation | ?{$_.Name -match ".csv"}
$Excel = New-Object -ComObject excel.application
$Excel.visible = $false
$workbook = $Excel.workbooks.add(1)
foreach($CSVfile in $CSVFiles)
{
   $filepath = $csvfile.FullName
   $WorkSheetName = $csvfile.name.Replace(".csv", " ")
   $processes = Import-Csv -Path $filepath -
   $worksheet = $workbook.WorkSheets.add()
   $processes | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation | Clip.exe
   $worksheet.select()
   $worksheet.Name = $WorkSheetName
   [void]$Excel.ActiveSheet.Range("A1:A1").Select()
   [void]$Excel.ActiveCell.PasteSpecial()
   [void]$worksheet.UsedRange.EntireColumn.AutoFit()
}

#Remove the default sheet (Sheet1)
$Excel.DisplayAlerts = $false
$workbook.Worksheets|?{$_.name -match "Sheet1"}|%{$_.Delete()}
$Excel.DisplayAlerts = $true
[void]$Excel.ActiveSheet.Range("A1:A1").Select()
$workbook.saveas($WorkBookLoc)
$Excel.Quit()
Remove-Variable -Name excel
[gc]::collect()
[gc]::WaitForPendingFinalizers()