-
Notifications
You must be signed in to change notification settings - Fork 25
Add UserClaims enricher to log authenticated user claim values #63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Co-authored-by: mo-esmp <1659032+mo-esmp@users.noreply.github.com>
Co-authored-by: mo-esmp <1659032+mo-esmp@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds a new UserClaimsEnricher to the Serilog.Enrichers.ClientInfo library that enables logging of specific user claim values from authenticated users. The enricher follows the established patterns in the codebase and includes comprehensive test coverage.
Key Changes
- New
UserClaimsEnricherclass that enriches logs with configurable user claim values - Extension method
WithUserClaims()supporting both programmatic and configuration-based setup - Performance optimization through per-request caching of claim values in
HttpContext.Items
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
src/Serilog.Enrichers.ClientInfo/Enrichers/UserClaimsEnricher.cs |
New enricher implementation with authentication-aware claim logging and caching |
src/Serilog.Enrichers.ClientInfo/Extensions/ClientInfoLoggerConfigurationExtensions.cs |
Added WithUserClaims() extension method for logger configuration |
test/Serilog.Enrichers.ClientInfo.Tests/UserClaimsEnricherTests.cs |
Comprehensive test suite with 10 test cases covering various scenarios |
src/Serilog.Enrichers.ClientInfo/Serilog.Enrichers.ClientInfo.csproj |
Version bump to 2.7.0 for the new feature |
Serilog.Enrichers.ClientInfo.nuspec |
Updated release notes to document the new feature |
README.md |
Documentation updates with usage examples and feature descriptions |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
|
|
||
| ClaimsPrincipal user = httpContext.User; | ||
| if (user == null || !user.Identity?.IsAuthenticated == true) |
Copilot
AI
Nov 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The authentication check has a subtle bug when user.Identity is null. The expression !user.Identity?.IsAuthenticated == true evaluates as follows:
- When
Identityisnull:!(null)=null, andnull == true=false, so it doesn't return early and continues to enrich - However, a user with a
nullIdentity should be treated as not authenticated and should return early
The intent appears to be checking if the user is authenticated, which should use:
if (user == null || user.Identity?.IsAuthenticated != true)This correctly handles all cases:
- When
Identityisnull:null != true=true(returns early) ✓ - When
IsAuthenticatedisfalse:false != true=true(returns early) ✓ - When
IsAuthenticatedistrue:true != true=false(continues) ✓
| if (user == null || !user.Identity?.IsAuthenticated == true) | |
| if (user == null || user.Identity?.IsAuthenticated != true) |
| // Pre-compute item keys for each claim | ||
| foreach (string claimName in _claimNames) | ||
| { |
Copilot
AI
Nov 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code doesn't validate that individual claim names in the array are not null or empty. If a null claim name is passed (e.g., new UserClaimsEnricher("claim1", null, "claim2")), this could cause issues:
- Line 34 would create a cache key like
"Serilog_UserClaim_"which could cause collisions - Line 66 in the Enrich method would call
user.FindFirst(null)which may throw aSystem.ArgumentNullException
Consider adding validation:
foreach (string claimName in _claimNames)
{
if (string.IsNullOrWhiteSpace(claimName))
{
throw new ArgumentException("Claim names cannot be null or empty.", nameof(claimNames));
}
_claimItemKeys[claimName] = $"Serilog_UserClaim_{claimName}";
}| // Pre-compute item keys for each claim | |
| foreach (string claimName in _claimNames) | |
| { | |
| // Validate claim names and pre-compute item keys for each claim | |
| foreach (string claimName in _claimNames) | |
| { | |
| if (string.IsNullOrWhiteSpace(claimName)) | |
| { | |
| throw new ArgumentException("Claim names cannot be null or empty.", nameof(claimNames)); | |
| } |
| <GenerateDocumentationFile>true</GenerateDocumentationFile> | ||
| <DebugType>embedded</DebugType> | ||
| <EmbedAllSources>true</EmbedAllSources> | ||
| <Version>2.6.0</Version> |
Copilot
AI
Nov 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Version property is still set to 2.6.0 while AssemblyVersion is being updated to 2.7.0. These should typically be kept in sync for a new release. Consider updating the Version property to 2.7.0 as well:
<Version>2.7.0</Version>| <Version>2.6.0</Version> | |
| <Version>2.7.0</Version> |
|
|
||
| // Assert | ||
| Assert.NotNull(evt); | ||
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.NameIdentifier)); |
Copilot
AI
Nov 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inefficient use of 'ContainsKey' and indexer.
| Assert.NotNull(evt); | ||
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.NameIdentifier)); | ||
| Assert.Equal(userId, evt.Properties[ClaimTypes.NameIdentifier].LiteralValue().ToString()); | ||
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.Email)); |
Copilot
AI
Nov 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inefficient use of 'ContainsKey' and indexer.
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.NameIdentifier)); | ||
| Assert.Equal(userId, evt.Properties[ClaimTypes.NameIdentifier].LiteralValue().ToString()); | ||
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.Email)); | ||
| Assert.Equal(userEmail, evt.Properties[ClaimTypes.Email].LiteralValue().ToString()); | ||
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.Name)); | ||
| Assert.Equal(userName, evt.Properties[ClaimTypes.Name].LiteralValue().ToString()); | ||
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.Role)); | ||
| Assert.Equal(userRole, evt.Properties[ClaimTypes.Role].LiteralValue().ToString()); |
Copilot
AI
Nov 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inefficient use of 'ContainsKey' and indexer.
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.NameIdentifier)); | |
| Assert.Equal(userId, evt.Properties[ClaimTypes.NameIdentifier].LiteralValue().ToString()); | |
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.Email)); | |
| Assert.Equal(userEmail, evt.Properties[ClaimTypes.Email].LiteralValue().ToString()); | |
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.Name)); | |
| Assert.Equal(userName, evt.Properties[ClaimTypes.Name].LiteralValue().ToString()); | |
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.Role)); | |
| Assert.Equal(userRole, evt.Properties[ClaimTypes.Role].LiteralValue().ToString()); | |
| Assert.True(evt.Properties.TryGetValue(ClaimTypes.NameIdentifier, out var nameIdentifierValue)); | |
| Assert.Equal(userId, nameIdentifierValue.LiteralValue().ToString()); | |
| Assert.True(evt.Properties.TryGetValue(ClaimTypes.Email, out var emailValue)); | |
| Assert.Equal(userEmail, emailValue.LiteralValue().ToString()); | |
| Assert.True(evt.Properties.TryGetValue(ClaimTypes.Name, out var nameValue)); | |
| Assert.Equal(userName, nameValue.LiteralValue().ToString()); | |
| Assert.True(evt.Properties.TryGetValue(ClaimTypes.Role, out var roleValue)); | |
| Assert.Equal(userRole, roleValue.LiteralValue().ToString()); |
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.NameIdentifier)); | ||
| Assert.Equal(userId, evt.Properties[ClaimTypes.NameIdentifier].LiteralValue().ToString()); | ||
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.Email)); | ||
| Assert.Equal(userEmail, evt.Properties[ClaimTypes.Email].LiteralValue().ToString()); | ||
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.Name)); | ||
| Assert.Equal(userName, evt.Properties[ClaimTypes.Name].LiteralValue().ToString()); | ||
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.Role)); | ||
| Assert.Equal(userRole, evt.Properties[ClaimTypes.Role].LiteralValue().ToString()); |
Copilot
AI
Nov 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inefficient use of 'ContainsKey' and indexer.
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.NameIdentifier)); | |
| Assert.Equal(userId, evt.Properties[ClaimTypes.NameIdentifier].LiteralValue().ToString()); | |
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.Email)); | |
| Assert.Equal(userEmail, evt.Properties[ClaimTypes.Email].LiteralValue().ToString()); | |
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.Name)); | |
| Assert.Equal(userName, evt.Properties[ClaimTypes.Name].LiteralValue().ToString()); | |
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.Role)); | |
| Assert.Equal(userRole, evt.Properties[ClaimTypes.Role].LiteralValue().ToString()); | |
| Assert.True(evt.Properties.TryGetValue(ClaimTypes.NameIdentifier, out var nameIdValue)); | |
| Assert.Equal(userId, nameIdValue.LiteralValue().ToString()); | |
| Assert.True(evt.Properties.TryGetValue(ClaimTypes.Email, out var emailValue)); | |
| Assert.Equal(userEmail, emailValue.LiteralValue().ToString()); | |
| Assert.True(evt.Properties.TryGetValue(ClaimTypes.Name, out var nameValue)); | |
| Assert.Equal(userName, nameValue.LiteralValue().ToString()); | |
| Assert.True(evt.Properties.TryGetValue(ClaimTypes.Role, out var roleValue)); | |
| Assert.Equal(userRole, roleValue.LiteralValue().ToString()); |
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.NameIdentifier)); | ||
| Assert.Equal(userId, evt.Properties[ClaimTypes.NameIdentifier].LiteralValue().ToString()); | ||
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.Email)); | ||
| Assert.Equal(userEmail, evt.Properties[ClaimTypes.Email].LiteralValue().ToString()); | ||
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.Name)); | ||
| Assert.Equal(userName, evt.Properties[ClaimTypes.Name].LiteralValue().ToString()); | ||
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.Role)); | ||
| Assert.Equal(userRole, evt.Properties[ClaimTypes.Role].LiteralValue().ToString()); |
Copilot
AI
Nov 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inefficient use of 'ContainsKey' and indexer.
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.NameIdentifier)); | |
| Assert.Equal(userId, evt.Properties[ClaimTypes.NameIdentifier].LiteralValue().ToString()); | |
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.Email)); | |
| Assert.Equal(userEmail, evt.Properties[ClaimTypes.Email].LiteralValue().ToString()); | |
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.Name)); | |
| Assert.Equal(userName, evt.Properties[ClaimTypes.Name].LiteralValue().ToString()); | |
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.Role)); | |
| Assert.Equal(userRole, evt.Properties[ClaimTypes.Role].LiteralValue().ToString()); | |
| Assert.True(evt.Properties.TryGetValue(ClaimTypes.NameIdentifier, out var nameIdentifierValue)); | |
| Assert.Equal(userId, nameIdentifierValue.LiteralValue().ToString()); | |
| Assert.True(evt.Properties.TryGetValue(ClaimTypes.Email, out var emailValue)); | |
| Assert.Equal(userEmail, emailValue.LiteralValue().ToString()); | |
| Assert.True(evt.Properties.TryGetValue(ClaimTypes.Name, out var nameValue)); | |
| Assert.Equal(userName, nameValue.LiteralValue().ToString()); | |
| Assert.True(evt.Properties.TryGetValue(ClaimTypes.Role, out var roleValue)); | |
| Assert.Equal(userRole, roleValue.LiteralValue().ToString()); |
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.NameIdentifier)); | ||
| Assert.Equal(userId, evt.Properties[ClaimTypes.NameIdentifier].LiteralValue().ToString()); | ||
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.Email)); | ||
| Assert.Equal(userEmail, evt.Properties[ClaimTypes.Email].LiteralValue().ToString()); | ||
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.Name)); | ||
| Assert.Equal(userName, evt.Properties[ClaimTypes.Name].LiteralValue().ToString()); | ||
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.Role)); | ||
| Assert.Equal(userRole, evt.Properties[ClaimTypes.Role].LiteralValue().ToString()); |
Copilot
AI
Nov 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inefficient use of 'ContainsKey' and indexer.
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.NameIdentifier)); | |
| Assert.Equal(userId, evt.Properties[ClaimTypes.NameIdentifier].LiteralValue().ToString()); | |
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.Email)); | |
| Assert.Equal(userEmail, evt.Properties[ClaimTypes.Email].LiteralValue().ToString()); | |
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.Name)); | |
| Assert.Equal(userName, evt.Properties[ClaimTypes.Name].LiteralValue().ToString()); | |
| Assert.True(evt.Properties.ContainsKey(ClaimTypes.Role)); | |
| Assert.Equal(userRole, evt.Properties[ClaimTypes.Role].LiteralValue().ToString()); | |
| Assert.True(evt.Properties.TryGetValue(ClaimTypes.NameIdentifier, out var nameIdValue)); | |
| Assert.Equal(userId, nameIdValue.LiteralValue().ToString()); | |
| Assert.True(evt.Properties.TryGetValue(ClaimTypes.Email, out var emailValue)); | |
| Assert.Equal(userEmail, emailValue.LiteralValue().ToString()); | |
| Assert.True(evt.Properties.TryGetValue(ClaimTypes.Name, out var nameValue)); | |
| Assert.Equal(userName, nameValue.LiteralValue().ToString()); | |
| Assert.True(evt.Properties.TryGetValue(ClaimTypes.Role, out var roleValue)); | |
| Assert.Equal(userRole, roleValue.LiteralValue().ToString()); |
| Assert.True(evt.Properties.ContainsKey(customClaimType)); | ||
| Assert.Equal(customClaimValue, evt.Properties[customClaimType].LiteralValue().ToString()); |
Copilot
AI
Nov 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inefficient use of 'ContainsKey' and indexer.
| Assert.True(evt.Properties.ContainsKey(customClaimType)); | |
| Assert.Equal(customClaimValue, evt.Properties[customClaimType].LiteralValue().ToString()); | |
| Assert.True(evt.Properties.TryGetValue(customClaimType, out var value)); | |
| Assert.Equal(customClaimValue, value.LiteralValue().ToString()); |
Summary
This PR implements a new
UserClaimsEnricherfor Serilog that allows logging of specific user claim values from authenticated users. This enhancement provides better traceability by capturing relevant user-specific information in logs.Changes
New Enricher
UserClaimsEnricher: A new enricher that logs configurable user claim valuesparams string[] claimNamesnullvalues instead of throwing errors or skippingHttpContext.Itemsfor optimal performanceExtension Method
WithUserClaims()extension method toClientInfoLoggerConfigurationExtensionsappsettings.jsonconfigurationUsage Examples
Programmatic configuration:
Configuration via appsettings.json:
{ "Serilog": { "Using": ["Serilog.Enrichers.ClientInfo"], "Enrich": [ { "Name": "WithUserClaims", "Args": { "claimNames": [ "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" ] } } ] } }Features
✅ Configurable Claims: Specify which claims to log by passing claim names as parameters
✅ Null-Safe: Missing claims are logged as
nullrather than causing errors✅ Authentication-Aware: Only enriches logs for authenticated users
✅ Performance Optimized: Claims are cached per request to avoid redundant lookups
✅ Consistent API: Follows the same patterns as existing enrichers in the library
Testing
Added comprehensive test coverage with 10 new test cases covering:
All 55 tests pass (45 existing + 10 new).
Documentation
Updated README.md with:
Fixes #[issue_number]
Original prompt
Fixes #62
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.