From 9c6caabd4511fc771fc3097d31dfd1dc2a0f18c3 Mon Sep 17 00:00:00 2001 From: Brett Hazen <2651260+bhazen@users.noreply.github.com> Date: Fri, 23 May 2025 15:30:22 -0500 Subject: [PATCH 1/6] Added more data to token issued event to allow tracking counts of which type of tokens are issued --- .../Endpoints/AuthorizeEndpointBase.cs | 7 ++++++- .../IdentityServer/Endpoints/TokenEndpoint.cs | 5 ++++- identity-server/src/Telemetry/Telemetry.cs | 21 +++++++++++++++++-- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/identity-server/src/IdentityServer/Endpoints/AuthorizeEndpointBase.cs b/identity-server/src/IdentityServer/Endpoints/AuthorizeEndpointBase.cs index c479580a0..faf3c16b4 100644 --- a/identity-server/src/IdentityServer/Endpoints/AuthorizeEndpointBase.cs +++ b/identity-server/src/IdentityServer/Endpoints/AuthorizeEndpointBase.cs @@ -239,7 +239,12 @@ private Task RaiseResponseEventAsync(AuthorizeResponse response) Telemetry.Metrics.TokenIssued( response.Request.ClientId, response.Request.GrantType, - response.Request.AuthorizeRequestType); + response.Request.AuthorizeRequestType, + response.AccessToken.IsPresent(), + response.Request.AccessTokenType, + false, + ProofType.None, + response.IdentityToken.IsPresent()); return _events.RaiseAsync(new TokenIssuedSuccessEvent(response)); } diff --git a/identity-server/src/IdentityServer/Endpoints/TokenEndpoint.cs b/identity-server/src/IdentityServer/Endpoints/TokenEndpoint.cs index 23a1cedad..9a87ee13a 100644 --- a/identity-server/src/IdentityServer/Endpoints/TokenEndpoint.cs +++ b/identity-server/src/IdentityServer/Endpoints/TokenEndpoint.cs @@ -139,7 +139,10 @@ private async Task ProcessTokenRequestAsync(HttpContext context var response = await _responseGenerator.ProcessAsync(requestResult); await _events.RaiseAsync(new TokenIssuedSuccessEvent(response, requestResult)); - Telemetry.Metrics.TokenIssued(clientResult.Client.ClientId, requestResult.ValidatedRequest.GrantType, null); + + Telemetry.Metrics.TokenIssued(clientResult.Client.ClientId, requestResult.ValidatedRequest.GrantType, null, + response.AccessToken.IsPresent(), requestResult.ValidatedRequest.AccessTokenType, response.RefreshToken.IsPresent(), + requestResult.ValidatedRequest.ProofType, response.IdentityToken.IsPresent()); LogTokens(response, requestResult); // return result diff --git a/identity-server/src/Telemetry/Telemetry.cs b/identity-server/src/Telemetry/Telemetry.cs index 70ff513e3..5ae123531 100644 --- a/identity-server/src/Telemetry/Telemetry.cs +++ b/identity-server/src/Telemetry/Telemetry.cs @@ -3,6 +3,7 @@ using System.Diagnostics.Metrics; +using Duende.IdentityServer.Models; using Duende.IdentityServer.Validation; namespace Duende.IdentityServer; @@ -65,6 +66,11 @@ public static class Tags public const string Scheme = "scheme"; public const string Result = "result"; public const string Type = "type"; + public const string AccessTokenIssued = "access_token_issued"; + public const string AccessTokenType = "access_token_type"; + public const string RefreshTokenIssued = "refresh_token_issued"; + public const string ProofType = "proof_type"; + public const string IdTokenIssued = "id_token_issued"; } /// @@ -442,13 +448,24 @@ public static void RevocationFailure(string clientId, string error) /// Client Id /// Grant Type /// Type of authorization request - public static void TokenIssued(string clientId, string grantType, AuthorizeRequestType? requestType) + /// Whether an access token was issued + /// The type of access token issued + /// Whether a refresh token was issued + /// The proof type used + /// Whether an id token was issued + public static void TokenIssued(string clientId, string grantType, AuthorizeRequestType? requestType, + bool accessTokenIssued, AccessTokenType accessTokenType, bool refreshTokenIssued, ProofType proofType, bool idTokenIssued) { Success(clientId); TokenIssuedCounter.Add(1, new(Tags.Client, clientId), new(Tags.GrantType, grantType), - new(Tags.AuthorizeRequestType, requestType)); + new(Tags.AuthorizeRequestType, requestType), + new(Tags.AccessTokenIssued, accessTokenIssued), + new(Tags.AccessTokenType, accessTokenType), + new(Tags.RefreshTokenIssued, refreshTokenIssued), + new(Tags.ProofType, proofType), + new(Tags.IdTokenIssued, idTokenIssued)); } /// From d9e66dde183650ef6bbc6017fad8fcc1239e6169 Mon Sep 17 00:00:00 2001 From: Brett Hazen <2651260+bhazen@users.noreply.github.com> Date: Fri, 23 May 2025 16:16:45 -0500 Subject: [PATCH 2/6] Added diagnostic entry for token issue counts --- .../BuilderExtensions/Core.cs | 1 + .../Licensing/V2/AtomicCounter.cs | 11 ++ .../TokenIssueCountDiagnosticEntry.cs | 135 ++++++++++++++++++ .../TokenIssueCountDiagnosticEntryTests.cs | 127 ++++++++++++++++ 4 files changed, 274 insertions(+) create mode 100644 identity-server/src/IdentityServer/Licensing/V2/AtomicCounter.cs create mode 100644 identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/TokenIssueCountDiagnosticEntry.cs create mode 100644 identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticEntries/TokenIssueCountDiagnosticEntryTests.cs diff --git a/identity-server/src/IdentityServer/Configuration/DependencyInjection/BuilderExtensions/Core.cs b/identity-server/src/IdentityServer/Configuration/DependencyInjection/BuilderExtensions/Core.cs index 7511256d4..e13e2d129 100644 --- a/identity-server/src/IdentityServer/Configuration/DependencyInjection/BuilderExtensions/Core.cs +++ b/identity-server/src/IdentityServer/Configuration/DependencyInjection/BuilderExtensions/Core.cs @@ -218,6 +218,7 @@ public static IIdentityServerBuilder AddCoreServices(this IIdentityServerBuilder builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddHostedService(); diff --git a/identity-server/src/IdentityServer/Licensing/V2/AtomicCounter.cs b/identity-server/src/IdentityServer/Licensing/V2/AtomicCounter.cs new file mode 100644 index 000000000..0a08bcd07 --- /dev/null +++ b/identity-server/src/IdentityServer/Licensing/V2/AtomicCounter.cs @@ -0,0 +1,11 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + +namespace Duende.IdentityServer.Licensing.V2; + +internal class AtomicCounter +{ + private long _count; + public void Increment() => Interlocked.Increment(ref _count); + public long Count => _count; +} diff --git a/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/TokenIssueCountDiagnosticEntry.cs b/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/TokenIssueCountDiagnosticEntry.cs new file mode 100644 index 000000000..41d57596c --- /dev/null +++ b/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/TokenIssueCountDiagnosticEntry.cs @@ -0,0 +1,135 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + +#nullable enable +using System.Collections.Concurrent; +using System.Diagnostics.Metrics; +using System.Text.Json; +using Duende.IdentityServer.Models; + +namespace Duende.IdentityServer.Licensing.V2.Diagnostics.DiagnosticEntries; + +internal class TokenIssueCountDiagnosticEntry : IDiagnosticEntry +{ + private readonly ConcurrentDictionary _tokenCounts; + private readonly MeterListener _meterListener; + + public TokenIssueCountDiagnosticEntry() + { + _tokenCounts = new ConcurrentDictionary([ + new("Jwt", new AtomicCounter()), + new ("Reference", new AtomicCounter()), + new ("Refresh", new AtomicCounter()), + new("JwtPoPDPoP", new AtomicCounter()), + new("ReferencePoPDPoP", new AtomicCounter()), + new("JwtPoPmTLS", new AtomicCounter()), + new("ReferencePoPmTLS", new AtomicCounter()), + new("Id", new AtomicCounter()) + ]); + _meterListener = new MeterListener(); + + _meterListener.InstrumentPublished += (instrument, listener) => + { + if (instrument.Name == Telemetry.Metrics.Counters.TokenIssued) + { + listener.EnableMeasurementEvents(instrument); + } + }; + + _meterListener.SetMeasurementEventCallback(HandleLongMeasurementRecorded); + + _meterListener.Start(); + } + + public Task WriteAsync(Utf8JsonWriter writer) + { + writer.WritePropertyName("TokenIssueCounts"); + writer.WriteStartObject(); + + foreach (var (tokenType, counter) in _tokenCounts) + { + writer.WriteNumber(tokenType, counter.Count); + } + + writer.WriteEndObject(); + + return Task.CompletedTask; + } + + private void HandleLongMeasurementRecorded(Instrument instrument, long value, ReadOnlySpan> tags, object? state) + { + if (instrument.Name != Telemetry.Metrics.Counters.TokenIssued) + { + return; + } + + var accessTokenIssued = false; + var accessTokenType = AccessTokenType.Jwt; + var refreshTokenIssued = false; + var proofType = ProofType.None; + var identityTokenIssued = false; + + foreach (var tag in tags) + { + switch (tag.Key) + { + case Telemetry.Metrics.Tags.AccessTokenType: + if (!Enum.TryParse(tag.Value?.ToString(), out accessTokenType)) + { + accessTokenType = AccessTokenType.Jwt; + } + break; + case Telemetry.Metrics.Tags.RefreshTokenIssued: + bool.TryParse(tag.Value?.ToString(), out refreshTokenIssued); + break; + case Telemetry.Metrics.Tags.ProofType: + if (!Enum.TryParse(tag.Value?.ToString(), out proofType)) + { + proofType = ProofType.None; + } + break; + case Telemetry.Metrics.Tags.AccessTokenIssued: + bool.TryParse(tag.Value?.ToString(), out accessTokenIssued); + break; + case Telemetry.Metrics.Tags.IdTokenIssued: + bool.TryParse(tag.Value?.ToString(), out identityTokenIssued); + break; + } + } + + if (accessTokenIssued) + { + switch (proofType) + { + case ProofType.None when accessTokenType == AccessTokenType.Jwt: + _tokenCounts["Jwt"].Increment(); + break; + case ProofType.None when accessTokenType == AccessTokenType.Reference: + _tokenCounts["Reference"].Increment(); + break; + case ProofType.DPoP when accessTokenType == AccessTokenType.Jwt: + _tokenCounts["JwtPoPDPoP"].Increment(); + break; + case ProofType.DPoP when accessTokenType == AccessTokenType.Reference: + _tokenCounts["ReferencePoPDPoP"].Increment(); + break; + case ProofType.ClientCertificate when accessTokenType == AccessTokenType.Jwt: + _tokenCounts["JwtPoPmTLS"].Increment(); + break; + case ProofType.ClientCertificate when accessTokenType == AccessTokenType.Reference: + _tokenCounts["ReferencePoPmTLS"].Increment(); + break; + } + } + + if (refreshTokenIssued) + { + _tokenCounts["Refresh"].Increment(); + } + + if (identityTokenIssued) + { + _tokenCounts["Id"].Increment(); + } + } +} diff --git a/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticEntries/TokenIssueCountDiagnosticEntryTests.cs b/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticEntries/TokenIssueCountDiagnosticEntryTests.cs new file mode 100644 index 000000000..4f3d36117 --- /dev/null +++ b/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticEntries/TokenIssueCountDiagnosticEntryTests.cs @@ -0,0 +1,127 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + +using Duende.IdentityServer.Licensing.V2.Diagnostics.DiagnosticEntries; +using Duende.IdentityServer.Models; + +namespace IdentityServer.UnitTests.Licensing.V2.DiagnosticEntries; + +public class TokenIssueCountDiagnosticEntryTests +{ + private readonly TokenIssueCountDiagnosticEntry _subject = new(); + + [Fact] + public async Task Should_Count_JwtAccessToken() + { + IssueToken(true, AccessTokenType.Jwt, false, ProofType.None, false); + + var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); + + result.RootElement.GetProperty("TokenIssueCounts").GetProperty("Jwt").GetInt64().ShouldBe(1); + } + + [Fact] + public async Task Should_Count_JwtReferenceToken() + { + IssueToken(true, AccessTokenType.Reference, false, ProofType.None, false); + + var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); + + result.RootElement.GetProperty("TokenIssueCounts").GetProperty("Reference").GetInt64().ShouldBe(1); + } + + [Fact] + public async Task Should_Count_JwtDPoPToken() + { + IssueToken(true, AccessTokenType.Jwt, false, ProofType.DPoP, false); + + var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); + + result.RootElement.GetProperty("TokenIssueCounts").GetProperty("JwtPoPDPoP").GetInt64().ShouldBe(1); + } + + [Fact] + public async Task Should_Count_ReferenceDPoPToken() + { + IssueToken(true, AccessTokenType.Reference, false, ProofType.DPoP, false); + + var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); + + result.RootElement.GetProperty("TokenIssueCounts").GetProperty("ReferencePoPDPoP").GetInt64().ShouldBe(1); + } + + [Fact] + public async Task Should_Count_JwtMTlsToken() + { + IssueToken(true, AccessTokenType.Jwt, false, ProofType.ClientCertificate, false); + + var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); + + result.RootElement.GetProperty("TokenIssueCounts").GetProperty("JwtPoPmTLS").GetInt64().ShouldBe(1); + } + + [Fact] + public async Task Should_Count_ReferenceMTlsToken() + { + IssueToken(true, AccessTokenType.Reference, false, ProofType.ClientCertificate, false); + + var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); + + result.RootElement.GetProperty("TokenIssueCounts").GetProperty("ReferencePoPmTLS").GetInt64().ShouldBe(1); + } + + [Fact] + public async Task Should_Count_RefreshToken() + { + IssueToken(false, AccessTokenType.Jwt, true, ProofType.None, false); + + var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); + + result.RootElement.GetProperty("TokenIssueCounts").GetProperty("Refresh").GetInt64().ShouldBe(1); + } + + [Fact] + public async Task Should_Count_IdToken() + { + IssueToken(false, AccessTokenType.Jwt, false, ProofType.None, true); + + var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); + + result.RootElement.GetProperty("TokenIssueCounts").GetProperty("Id").GetInt64().ShouldBe(1); + } + + [Fact] + public async Task Should_Handle_Multiple_Token_Types() + { + IssueToken(true, AccessTokenType.Jwt, true, ProofType.None, false); + IssueToken(true, AccessTokenType.Jwt, false, ProofType.DPoP, false); + + var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); + + var tokenIssueCounts = result.RootElement.GetProperty("TokenIssueCounts"); + tokenIssueCounts.GetProperty("Jwt").GetInt64().ShouldBe(1); + tokenIssueCounts.GetProperty("JwtPoPDPoP").GetInt64().ShouldBe(1); + tokenIssueCounts.GetProperty("Refresh").GetInt64().ShouldBe(1); + } + + [Fact] + public async Task Should_Ignore_Non_TokenIssued_Instruments() + { + Duende.IdentityServer.Telemetry.Metrics.TokenIssuedFailure("ClientId", "GrantType", null, "error"); + + var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); + + var tokenIssueCounts = result.RootElement.GetProperty("TokenIssueCounts"); + tokenIssueCounts.GetProperty("Jwt").GetInt64().ShouldBe(0); + tokenIssueCounts.GetProperty("Reference").GetInt64().ShouldBe(0); + tokenIssueCounts.GetProperty("JwtPoPDPoP").GetInt64().ShouldBe(0); + tokenIssueCounts.GetProperty("JwtPoPmTLS").GetInt64().ShouldBe(0); + tokenIssueCounts.GetProperty("ReferencePoPDPoP").GetInt64().ShouldBe(0); + tokenIssueCounts.GetProperty("ReferencePoPmTLS").GetInt64().ShouldBe(0); + tokenIssueCounts.GetProperty("Refresh").GetInt64().ShouldBe(0); + } + + private void IssueToken(bool accessTokenIssued, AccessTokenType accessTokenType, bool refreshTokenIssued, + ProofType proofType, bool idTokenIssued) => + Duende.IdentityServer.Telemetry.Metrics.TokenIssued("ClientId", "GrantType", null, accessTokenIssued, accessTokenType, refreshTokenIssued, proofType, idTokenIssued); +} From 6008f3cc114ae5fbb81aba4fd3c4f9663f3b6f57 Mon Sep 17 00:00:00 2001 From: Brett Hazen <2651260+bhazen@users.noreply.github.com> Date: Tue, 27 May 2025 13:07:31 -0500 Subject: [PATCH 3/6] Include count of each grant type in token issue count diagnostic entry --- .../Licensing/V2/AtomicCounter.cs | 6 +- .../TokenIssueCountDiagnosticEntry.cs | 14 +++++ .../TokenIssueCountDiagnosticEntryTests.cs | 60 +++++++++++++++---- 3 files changed, 66 insertions(+), 14 deletions(-) diff --git a/identity-server/src/IdentityServer/Licensing/V2/AtomicCounter.cs b/identity-server/src/IdentityServer/Licensing/V2/AtomicCounter.cs index 0a08bcd07..dd32453f6 100644 --- a/identity-server/src/IdentityServer/Licensing/V2/AtomicCounter.cs +++ b/identity-server/src/IdentityServer/Licensing/V2/AtomicCounter.cs @@ -3,9 +3,11 @@ namespace Duende.IdentityServer.Licensing.V2; -internal class AtomicCounter +internal class AtomicCounter(int initialCount = 0) { - private long _count; + private long _count = initialCount; + public void Increment() => Interlocked.Increment(ref _count); + public long Count => _count; } diff --git a/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/TokenIssueCountDiagnosticEntry.cs b/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/TokenIssueCountDiagnosticEntry.cs index 41d57596c..4731b0cae 100644 --- a/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/TokenIssueCountDiagnosticEntry.cs +++ b/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/TokenIssueCountDiagnosticEntry.cs @@ -68,6 +68,7 @@ private void HandleLongMeasurementRecorded(Instrument instrument, long value, Re var refreshTokenIssued = false; var proofType = ProofType.None; var identityTokenIssued = false; + var grantType = string.Empty; foreach (var tag in tags) { @@ -94,6 +95,9 @@ private void HandleLongMeasurementRecorded(Instrument instrument, long value, Re case Telemetry.Metrics.Tags.IdTokenIssued: bool.TryParse(tag.Value?.ToString(), out identityTokenIssued); break; + case Telemetry.Metrics.Tags.GrantType: + grantType = tag.Value?.ToString(); + break; } } @@ -131,5 +135,15 @@ private void HandleLongMeasurementRecorded(Instrument instrument, long value, Re { _tokenCounts["Id"].Increment(); } + + var tokenWasIssued = accessTokenIssued || refreshTokenIssued || identityTokenIssued; + if (tokenWasIssued && !string.IsNullOrEmpty(grantType)) + { + _tokenCounts.AddOrUpdate(grantType, new AtomicCounter(1), (_, counter) => + { + counter.Increment(); + return counter; + }); + } } } diff --git a/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticEntries/TokenIssueCountDiagnosticEntryTests.cs b/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticEntries/TokenIssueCountDiagnosticEntryTests.cs index 4f3d36117..b02bda63a 100644 --- a/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticEntries/TokenIssueCountDiagnosticEntryTests.cs +++ b/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticEntries/TokenIssueCountDiagnosticEntryTests.cs @@ -13,7 +13,7 @@ public class TokenIssueCountDiagnosticEntryTests [Fact] public async Task Should_Count_JwtAccessToken() { - IssueToken(true, AccessTokenType.Jwt, false, ProofType.None, false); + IssueToken("authorization_code", true, AccessTokenType.Jwt, false, ProofType.None, false); var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); @@ -23,7 +23,7 @@ public async Task Should_Count_JwtAccessToken() [Fact] public async Task Should_Count_JwtReferenceToken() { - IssueToken(true, AccessTokenType.Reference, false, ProofType.None, false); + IssueToken("authorization_code", true, AccessTokenType.Reference, false, ProofType.None, false); var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); @@ -33,7 +33,7 @@ public async Task Should_Count_JwtReferenceToken() [Fact] public async Task Should_Count_JwtDPoPToken() { - IssueToken(true, AccessTokenType.Jwt, false, ProofType.DPoP, false); + IssueToken("authorization_code", true, AccessTokenType.Jwt, false, ProofType.DPoP, false); var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); @@ -43,7 +43,7 @@ public async Task Should_Count_JwtDPoPToken() [Fact] public async Task Should_Count_ReferenceDPoPToken() { - IssueToken(true, AccessTokenType.Reference, false, ProofType.DPoP, false); + IssueToken("authorization_code", true, AccessTokenType.Reference, false, ProofType.DPoP, false); var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); @@ -53,7 +53,7 @@ public async Task Should_Count_ReferenceDPoPToken() [Fact] public async Task Should_Count_JwtMTlsToken() { - IssueToken(true, AccessTokenType.Jwt, false, ProofType.ClientCertificate, false); + IssueToken("authorization_code", true, AccessTokenType.Jwt, false, ProofType.ClientCertificate, false); var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); @@ -63,7 +63,7 @@ public async Task Should_Count_JwtMTlsToken() [Fact] public async Task Should_Count_ReferenceMTlsToken() { - IssueToken(true, AccessTokenType.Reference, false, ProofType.ClientCertificate, false); + IssueToken("authorization_code", true, AccessTokenType.Reference, false, ProofType.ClientCertificate, false); var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); @@ -73,7 +73,7 @@ public async Task Should_Count_ReferenceMTlsToken() [Fact] public async Task Should_Count_RefreshToken() { - IssueToken(false, AccessTokenType.Jwt, true, ProofType.None, false); + IssueToken("refresh_token", false, AccessTokenType.Jwt, true, ProofType.None, false); var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); @@ -83,7 +83,7 @@ public async Task Should_Count_RefreshToken() [Fact] public async Task Should_Count_IdToken() { - IssueToken(false, AccessTokenType.Jwt, false, ProofType.None, true); + IssueToken("authorization_code", false, AccessTokenType.Jwt, false, ProofType.None, true); var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); @@ -93,8 +93,8 @@ public async Task Should_Count_IdToken() [Fact] public async Task Should_Handle_Multiple_Token_Types() { - IssueToken(true, AccessTokenType.Jwt, true, ProofType.None, false); - IssueToken(true, AccessTokenType.Jwt, false, ProofType.DPoP, false); + IssueToken("authorization_code", true, AccessTokenType.Jwt, true, ProofType.None, false); + IssueToken("authorization_code", true, AccessTokenType.Jwt, false, ProofType.DPoP, false); var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); @@ -104,6 +104,42 @@ public async Task Should_Handle_Multiple_Token_Types() tokenIssueCounts.GetProperty("Refresh").GetInt64().ShouldBe(1); } + [Fact] + public async Task Should_Handle_Initial_Grant_Type_Count() + { + IssueToken("authorization_code", true, AccessTokenType.Jwt, false, ProofType.None, false); + + var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); + + var tokenIssueCounts = result.RootElement.GetProperty("TokenIssueCounts"); + tokenIssueCounts.GetProperty("authorization_code").GetInt64().ShouldBe(1); + } + + [Fact] + public async Task Should_Handle_Multiple_Grant_Type_Counts() + { + IssueToken("authorization_code", true, AccessTokenType.Jwt, false, ProofType.None, false); + IssueToken("client_credentials", true, AccessTokenType.Jwt, false, ProofType.None, false); + + var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); + + var tokenIssueCounts = result.RootElement.GetProperty("TokenIssueCounts"); + tokenIssueCounts.GetProperty("authorization_code").GetInt64().ShouldBe(1); + tokenIssueCounts.GetProperty("client_credentials").GetInt64().ShouldBe(1); + } + + [Fact] + public async Task Should_Handle_Multiple_Grant_Type_Counts_With_Grant_Type() + { + IssueToken("authorization_code", true, AccessTokenType.Jwt, false, ProofType.None, false); + IssueToken("authorization_code", true, AccessTokenType.Jwt, false, ProofType.None, false); + + var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); + + var tokenIssueCounts = result.RootElement.GetProperty("TokenIssueCounts"); + tokenIssueCounts.GetProperty("authorization_code").GetInt64().ShouldBe(2); + } + [Fact] public async Task Should_Ignore_Non_TokenIssued_Instruments() { @@ -121,7 +157,7 @@ public async Task Should_Ignore_Non_TokenIssued_Instruments() tokenIssueCounts.GetProperty("Refresh").GetInt64().ShouldBe(0); } - private void IssueToken(bool accessTokenIssued, AccessTokenType accessTokenType, bool refreshTokenIssued, + private void IssueToken(string grantType, bool accessTokenIssued, AccessTokenType accessTokenType, bool refreshTokenIssued, ProofType proofType, bool idTokenIssued) => - Duende.IdentityServer.Telemetry.Metrics.TokenIssued("ClientId", "GrantType", null, accessTokenIssued, accessTokenType, refreshTokenIssued, proofType, idTokenIssued); + Duende.IdentityServer.Telemetry.Metrics.TokenIssued("ClientId", grantType, null, accessTokenIssued, accessTokenType, refreshTokenIssued, proofType, idTokenIssued); } From 183bd83ac62f5ddab735193b08c6387e13d9fb20 Mon Sep 17 00:00:00 2001 From: Brett Hazen <2651260+bhazen@users.noreply.github.com> Date: Tue, 27 May 2025 14:51:58 -0500 Subject: [PATCH 4/6] Mark previous token issued metric convenience method obsolete in favor of new more detailed method --- .../Extensions/IClientStoreExtensions.cs | 1 + identity-server/src/Telemetry/Telemetry.cs | 21 +++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/identity-server/src/IdentityServer/Extensions/IClientStoreExtensions.cs b/identity-server/src/IdentityServer/Extensions/IClientStoreExtensions.cs index eb161bc00..a4e54f9e1 100644 --- a/identity-server/src/IdentityServer/Extensions/IClientStoreExtensions.cs +++ b/identity-server/src/IdentityServer/Extensions/IClientStoreExtensions.cs @@ -22,6 +22,7 @@ public static async Task FindEnabledClientByIdAsync(this IClientStore st var client = await store.FindClientByIdAsync(clientId); if (client != null && client.Enabled) { + //Telemetry.Metrics.ClientLoaded(client); return client; } diff --git a/identity-server/src/Telemetry/Telemetry.cs b/identity-server/src/Telemetry/Telemetry.cs index 5ae123531..89adffaee 100644 --- a/identity-server/src/Telemetry/Telemetry.cs +++ b/identity-server/src/Telemetry/Telemetry.cs @@ -442,6 +442,23 @@ public static void RevocationFailure(string clientId, string error) public static readonly Counter TokenIssuedCounter = Meter.CreateCounter(Counters.TokenIssued); + + /// + /// Helper method to increase + /// + /// Client Id + /// Grant Type + /// Type of authorization request + [Obsolete("This overload will be removed in a future version. Use the overload with accessTokenIssued, accessTokenType, refreshTokenIssued, proofType, and idTokenIssued parameters instead.")] + public static void TokenIssued(string clientId, string grantType, AuthorizeRequestType? requestType) + { + Success(clientId); + TokenIssuedCounter.Add(1, + new(Tags.Client, clientId), + new(Tags.GrantType, grantType), + new(Tags.AuthorizeRequestType, requestType)); + } + /// /// Helper method to increase /// @@ -449,9 +466,9 @@ public static void RevocationFailure(string clientId, string error) /// Grant Type /// Type of authorization request /// Whether an access token was issued - /// The type of access token issued + /// The type of access token issued (JWT or Reference) /// Whether a refresh token was issued - /// The proof type used + /// The proof type used (None, ClientCertificate, or DPoP) /// Whether an id token was issued public static void TokenIssued(string clientId, string grantType, AuthorizeRequestType? requestType, bool accessTokenIssued, AccessTokenType accessTokenType, bool refreshTokenIssued, ProofType proofType, bool idTokenIssued) From 99a456365f4b8e38958df1bb8e9853bfeae4e9f8 Mon Sep 17 00:00:00 2001 From: Brett Hazen <2651260+bhazen@users.noreply.github.com> Date: Tue, 27 May 2025 15:17:03 -0500 Subject: [PATCH 5/6] Make access token type nullable on token issued event for when an access token is not issued --- .../Endpoints/AuthorizeEndpointBase.cs | 2 +- .../IdentityServer/Endpoints/TokenEndpoint.cs | 2 +- .../Extensions/IClientStoreExtensions.cs | 1 - identity-server/src/Telemetry/Telemetry.cs | 4 ++-- .../TokenIssueCountDiagnosticEntryTests.cs | 20 ++++++++++++++++++- 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/identity-server/src/IdentityServer/Endpoints/AuthorizeEndpointBase.cs b/identity-server/src/IdentityServer/Endpoints/AuthorizeEndpointBase.cs index faf3c16b4..8a608980e 100644 --- a/identity-server/src/IdentityServer/Endpoints/AuthorizeEndpointBase.cs +++ b/identity-server/src/IdentityServer/Endpoints/AuthorizeEndpointBase.cs @@ -241,7 +241,7 @@ private Task RaiseResponseEventAsync(AuthorizeResponse response) response.Request.GrantType, response.Request.AuthorizeRequestType, response.AccessToken.IsPresent(), - response.Request.AccessTokenType, + response.AccessToken.IsPresent() ? response.Request.AccessTokenType : null, false, ProofType.None, response.IdentityToken.IsPresent()); diff --git a/identity-server/src/IdentityServer/Endpoints/TokenEndpoint.cs b/identity-server/src/IdentityServer/Endpoints/TokenEndpoint.cs index 9a87ee13a..875ed27e5 100644 --- a/identity-server/src/IdentityServer/Endpoints/TokenEndpoint.cs +++ b/identity-server/src/IdentityServer/Endpoints/TokenEndpoint.cs @@ -141,7 +141,7 @@ private async Task ProcessTokenRequestAsync(HttpContext context await _events.RaiseAsync(new TokenIssuedSuccessEvent(response, requestResult)); Telemetry.Metrics.TokenIssued(clientResult.Client.ClientId, requestResult.ValidatedRequest.GrantType, null, - response.AccessToken.IsPresent(), requestResult.ValidatedRequest.AccessTokenType, response.RefreshToken.IsPresent(), + response.AccessToken.IsPresent(), response.AccessTokenType.IsPresent() ? requestResult.ValidatedRequest.AccessTokenType : null, response.RefreshToken.IsPresent(), requestResult.ValidatedRequest.ProofType, response.IdentityToken.IsPresent()); LogTokens(response, requestResult); diff --git a/identity-server/src/IdentityServer/Extensions/IClientStoreExtensions.cs b/identity-server/src/IdentityServer/Extensions/IClientStoreExtensions.cs index a4e54f9e1..eb161bc00 100644 --- a/identity-server/src/IdentityServer/Extensions/IClientStoreExtensions.cs +++ b/identity-server/src/IdentityServer/Extensions/IClientStoreExtensions.cs @@ -22,7 +22,6 @@ public static async Task FindEnabledClientByIdAsync(this IClientStore st var client = await store.FindClientByIdAsync(clientId); if (client != null && client.Enabled) { - //Telemetry.Metrics.ClientLoaded(client); return client; } diff --git a/identity-server/src/Telemetry/Telemetry.cs b/identity-server/src/Telemetry/Telemetry.cs index 89adffaee..b1380a4a6 100644 --- a/identity-server/src/Telemetry/Telemetry.cs +++ b/identity-server/src/Telemetry/Telemetry.cs @@ -466,12 +466,12 @@ public static void TokenIssued(string clientId, string grantType, AuthorizeReque /// Grant Type /// Type of authorization request /// Whether an access token was issued - /// The type of access token issued (JWT or Reference) + /// The type of access token issued (Null if no access token was issued, otherwise JWT or Reference) /// Whether a refresh token was issued /// The proof type used (None, ClientCertificate, or DPoP) /// Whether an id token was issued public static void TokenIssued(string clientId, string grantType, AuthorizeRequestType? requestType, - bool accessTokenIssued, AccessTokenType accessTokenType, bool refreshTokenIssued, ProofType proofType, bool idTokenIssued) + bool accessTokenIssued, AccessTokenType? accessTokenType, bool refreshTokenIssued, ProofType proofType, bool idTokenIssued) { Success(clientId); TokenIssuedCounter.Add(1, diff --git a/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticEntries/TokenIssueCountDiagnosticEntryTests.cs b/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticEntries/TokenIssueCountDiagnosticEntryTests.cs index b02bda63a..38c09e80f 100644 --- a/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticEntries/TokenIssueCountDiagnosticEntryTests.cs +++ b/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticEntries/TokenIssueCountDiagnosticEntryTests.cs @@ -104,6 +104,24 @@ public async Task Should_Handle_Multiple_Token_Types() tokenIssueCounts.GetProperty("Refresh").GetInt64().ShouldBe(1); } + [Fact] + public async Task Should_Handle_No_Token_Issued() + { + IssueToken("authorization_code", false, null, false, ProofType.None, false); + + var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); + + var tokenIssueCounts = result.RootElement.GetProperty("TokenIssueCounts"); + tokenIssueCounts.GetProperty("Jwt").GetInt64().ShouldBe(0); + tokenIssueCounts.GetProperty("Reference").GetInt64().ShouldBe(0); + tokenIssueCounts.GetProperty("JwtPoPDPoP").GetInt64().ShouldBe(0); + tokenIssueCounts.GetProperty("JwtPoPmTLS").GetInt64().ShouldBe(0); + tokenIssueCounts.GetProperty("ReferencePoPDPoP").GetInt64().ShouldBe(0); + tokenIssueCounts.GetProperty("ReferencePoPmTLS").GetInt64().ShouldBe(0); + tokenIssueCounts.GetProperty("Refresh").GetInt64().ShouldBe(0); + tokenIssueCounts.GetProperty("Id").GetInt64().ShouldBe(0); + } + [Fact] public async Task Should_Handle_Initial_Grant_Type_Count() { @@ -157,7 +175,7 @@ public async Task Should_Ignore_Non_TokenIssued_Instruments() tokenIssueCounts.GetProperty("Refresh").GetInt64().ShouldBe(0); } - private void IssueToken(string grantType, bool accessTokenIssued, AccessTokenType accessTokenType, bool refreshTokenIssued, + private void IssueToken(string grantType, bool accessTokenIssued, AccessTokenType? accessTokenType, bool refreshTokenIssued, ProofType proofType, bool idTokenIssued) => Duende.IdentityServer.Telemetry.Metrics.TokenIssued("ClientId", grantType, null, accessTokenIssued, accessTokenType, refreshTokenIssued, proofType, idTokenIssued); } From 2443eff841cfa6cc5bd48eea7ff50ca820faade2 Mon Sep 17 00:00:00 2001 From: Brett Hazen <2651260+bhazen@users.noreply.github.com> Date: Tue, 3 Jun 2025 08:28:47 -0500 Subject: [PATCH 6/6] Move to individual incrementer variables over concurrent dictionary --- .../Licensing/V2/AtomicCounter.cs | 13 --- .../TokenIssueCountDiagnosticEntry.cs | 97 +++++++++++++------ .../TokenIssueCountDiagnosticEntryTests.cs | 60 ++++++++---- 3 files changed, 108 insertions(+), 62 deletions(-) delete mode 100644 identity-server/src/IdentityServer/Licensing/V2/AtomicCounter.cs diff --git a/identity-server/src/IdentityServer/Licensing/V2/AtomicCounter.cs b/identity-server/src/IdentityServer/Licensing/V2/AtomicCounter.cs deleted file mode 100644 index dd32453f6..000000000 --- a/identity-server/src/IdentityServer/Licensing/V2/AtomicCounter.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) Duende Software. All rights reserved. -// See LICENSE in the project root for license information. - -namespace Duende.IdentityServer.Licensing.V2; - -internal class AtomicCounter(int initialCount = 0) -{ - private long _count = initialCount; - - public void Increment() => Interlocked.Increment(ref _count); - - public long Count => _count; -} diff --git a/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/TokenIssueCountDiagnosticEntry.cs b/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/TokenIssueCountDiagnosticEntry.cs index 4731b0cae..1f977712a 100644 --- a/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/TokenIssueCountDiagnosticEntry.cs +++ b/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/TokenIssueCountDiagnosticEntry.cs @@ -2,7 +2,6 @@ // See LICENSE in the project root for license information. #nullable enable -using System.Collections.Concurrent; using System.Diagnostics.Metrics; using System.Text.Json; using Duende.IdentityServer.Models; @@ -11,21 +10,27 @@ namespace Duende.IdentityServer.Licensing.V2.Diagnostics.DiagnosticEntries; internal class TokenIssueCountDiagnosticEntry : IDiagnosticEntry { - private readonly ConcurrentDictionary _tokenCounts; + private long _jwtTokenIssued; + private long _referenceTokenIssued; + private long _refreshTokenIssued; + private long _jwtPoPDPoPTokenIssued; + private long _referencePoPDPoPTokenIssued; + private long _jwtPoPmTLSTokenIssued; + private long _referencePoPmTLSTokenIssued; + private long _idTokenIssued; + + private long _implicitGrantTypeFlows; + private long _hybridGrantTypeFlows; + private long _authorizationCodeGrantTypeFlows; + private long _clientCredentialsGrantTypeFlows; + private long _resourceOwnerPasswordGrantTypeFlows; + private long _deviceFlowGrantTypeFlows; + private long _otherGrantTypeFlows; + private readonly MeterListener _meterListener; public TokenIssueCountDiagnosticEntry() { - _tokenCounts = new ConcurrentDictionary([ - new("Jwt", new AtomicCounter()), - new ("Reference", new AtomicCounter()), - new ("Refresh", new AtomicCounter()), - new("JwtPoPDPoP", new AtomicCounter()), - new("ReferencePoPDPoP", new AtomicCounter()), - new("JwtPoPmTLS", new AtomicCounter()), - new("ReferencePoPmTLS", new AtomicCounter()), - new("Id", new AtomicCounter()) - ]); _meterListener = new MeterListener(); _meterListener.InstrumentPublished += (instrument, listener) => @@ -46,10 +51,21 @@ public Task WriteAsync(Utf8JsonWriter writer) writer.WritePropertyName("TokenIssueCounts"); writer.WriteStartObject(); - foreach (var (tokenType, counter) in _tokenCounts) - { - writer.WriteNumber(tokenType, counter.Count); - } + writer.WriteNumber("Jwt", _jwtTokenIssued); + writer.WriteNumber("Reference", _referenceTokenIssued); + writer.WriteNumber("JwtPoPDPoP", _jwtPoPDPoPTokenIssued); + writer.WriteNumber("ReferencePoPDPoP", _referencePoPDPoPTokenIssued); + writer.WriteNumber("JwtPoPmTLS", _jwtPoPmTLSTokenIssued); + writer.WriteNumber("ReferencePoPmTLS", _referencePoPmTLSTokenIssued); + writer.WriteNumber("Refresh", _refreshTokenIssued); + writer.WriteNumber("Id", _idTokenIssued); + writer.WriteNumber(GrantType.Implicit, _implicitGrantTypeFlows); + writer.WriteNumber(GrantType.Hybrid, _hybridGrantTypeFlows); + writer.WriteNumber(GrantType.AuthorizationCode, _authorizationCodeGrantTypeFlows); + writer.WriteNumber(GrantType.ClientCredentials, _clientCredentialsGrantTypeFlows); + writer.WriteNumber(GrantType.ResourceOwnerPassword, _resourceOwnerPasswordGrantTypeFlows); + writer.WriteNumber(GrantType.DeviceFlow, _deviceFlowGrantTypeFlows); + writer.WriteNumber("Other", _otherGrantTypeFlows); writer.WriteEndObject(); @@ -106,44 +122,65 @@ private void HandleLongMeasurementRecorded(Instrument instrument, long value, Re switch (proofType) { case ProofType.None when accessTokenType == AccessTokenType.Jwt: - _tokenCounts["Jwt"].Increment(); + Interlocked.Increment(ref _jwtTokenIssued); break; case ProofType.None when accessTokenType == AccessTokenType.Reference: - _tokenCounts["Reference"].Increment(); + Interlocked.Increment(ref _referenceTokenIssued); break; case ProofType.DPoP when accessTokenType == AccessTokenType.Jwt: - _tokenCounts["JwtPoPDPoP"].Increment(); + Interlocked.Increment(ref _jwtPoPDPoPTokenIssued); break; case ProofType.DPoP when accessTokenType == AccessTokenType.Reference: - _tokenCounts["ReferencePoPDPoP"].Increment(); + Interlocked.Increment(ref _referencePoPDPoPTokenIssued); break; case ProofType.ClientCertificate when accessTokenType == AccessTokenType.Jwt: - _tokenCounts["JwtPoPmTLS"].Increment(); + Interlocked.Increment(ref _jwtPoPmTLSTokenIssued); break; case ProofType.ClientCertificate when accessTokenType == AccessTokenType.Reference: - _tokenCounts["ReferencePoPmTLS"].Increment(); + Interlocked.Increment(ref _referencePoPmTLSTokenIssued); break; } } if (refreshTokenIssued) { - _tokenCounts["Refresh"].Increment(); + Interlocked.Increment(ref _refreshTokenIssued); } if (identityTokenIssued) { - _tokenCounts["Id"].Increment(); + Interlocked.Increment(ref _idTokenIssued); } var tokenWasIssued = accessTokenIssued || refreshTokenIssued || identityTokenIssued; - if (tokenWasIssued && !string.IsNullOrEmpty(grantType)) + if (!tokenWasIssued || string.IsNullOrEmpty(grantType)) { - _tokenCounts.AddOrUpdate(grantType, new AtomicCounter(1), (_, counter) => - { - counter.Increment(); - return counter; - }); + return; + } + + switch (grantType) + { + case GrantType.Implicit: + Interlocked.Increment(ref _implicitGrantTypeFlows); + break; + case GrantType.Hybrid: + Interlocked.Increment(ref _hybridGrantTypeFlows); + break; + case GrantType.AuthorizationCode: + Interlocked.Increment(ref _authorizationCodeGrantTypeFlows); + break; + case GrantType.ClientCredentials: + Interlocked.Increment(ref _clientCredentialsGrantTypeFlows); + break; + case GrantType.ResourceOwnerPassword: + Interlocked.Increment(ref _resourceOwnerPasswordGrantTypeFlows); + break; + case GrantType.DeviceFlow: + Interlocked.Increment(ref _deviceFlowGrantTypeFlows); + break; + default: + Interlocked.Increment(ref _otherGrantTypeFlows); + break; } } } diff --git a/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticEntries/TokenIssueCountDiagnosticEntryTests.cs b/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticEntries/TokenIssueCountDiagnosticEntryTests.cs index 38c09e80f..59763f74e 100644 --- a/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticEntries/TokenIssueCountDiagnosticEntryTests.cs +++ b/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticEntries/TokenIssueCountDiagnosticEntryTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Duende Software. All rights reserved. // See LICENSE in the project root for license information. +using System.Reflection; using Duende.IdentityServer.Licensing.V2.Diagnostics.DiagnosticEntries; using Duende.IdentityServer.Models; @@ -13,7 +14,7 @@ public class TokenIssueCountDiagnosticEntryTests [Fact] public async Task Should_Count_JwtAccessToken() { - IssueToken("authorization_code", true, AccessTokenType.Jwt, false, ProofType.None, false); + IssueToken(GrantType.AuthorizationCode, true, AccessTokenType.Jwt, false, ProofType.None, false); var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); @@ -23,7 +24,7 @@ public async Task Should_Count_JwtAccessToken() [Fact] public async Task Should_Count_JwtReferenceToken() { - IssueToken("authorization_code", true, AccessTokenType.Reference, false, ProofType.None, false); + IssueToken(GrantType.AuthorizationCode, true, AccessTokenType.Reference, false, ProofType.None, false); var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); @@ -33,7 +34,7 @@ public async Task Should_Count_JwtReferenceToken() [Fact] public async Task Should_Count_JwtDPoPToken() { - IssueToken("authorization_code", true, AccessTokenType.Jwt, false, ProofType.DPoP, false); + IssueToken(GrantType.AuthorizationCode, true, AccessTokenType.Jwt, false, ProofType.DPoP, false); var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); @@ -43,7 +44,7 @@ public async Task Should_Count_JwtDPoPToken() [Fact] public async Task Should_Count_ReferenceDPoPToken() { - IssueToken("authorization_code", true, AccessTokenType.Reference, false, ProofType.DPoP, false); + IssueToken(GrantType.AuthorizationCode, true, AccessTokenType.Reference, false, ProofType.DPoP, false); var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); @@ -53,7 +54,7 @@ public async Task Should_Count_ReferenceDPoPToken() [Fact] public async Task Should_Count_JwtMTlsToken() { - IssueToken("authorization_code", true, AccessTokenType.Jwt, false, ProofType.ClientCertificate, false); + IssueToken(GrantType.AuthorizationCode, true, AccessTokenType.Jwt, false, ProofType.ClientCertificate, false); var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); @@ -63,7 +64,7 @@ public async Task Should_Count_JwtMTlsToken() [Fact] public async Task Should_Count_ReferenceMTlsToken() { - IssueToken("authorization_code", true, AccessTokenType.Reference, false, ProofType.ClientCertificate, false); + IssueToken(GrantType.AuthorizationCode, true, AccessTokenType.Reference, false, ProofType.ClientCertificate, false); var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); @@ -83,7 +84,7 @@ public async Task Should_Count_RefreshToken() [Fact] public async Task Should_Count_IdToken() { - IssueToken("authorization_code", false, AccessTokenType.Jwt, false, ProofType.None, true); + IssueToken(GrantType.AuthorizationCode, false, AccessTokenType.Jwt, false, ProofType.None, true); var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); @@ -93,8 +94,8 @@ public async Task Should_Count_IdToken() [Fact] public async Task Should_Handle_Multiple_Token_Types() { - IssueToken("authorization_code", true, AccessTokenType.Jwt, true, ProofType.None, false); - IssueToken("authorization_code", true, AccessTokenType.Jwt, false, ProofType.DPoP, false); + IssueToken(GrantType.AuthorizationCode, true, AccessTokenType.Jwt, true, ProofType.None, false); + IssueToken(GrantType.AuthorizationCode, true, AccessTokenType.Jwt, false, ProofType.DPoP, false); var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); @@ -107,7 +108,7 @@ public async Task Should_Handle_Multiple_Token_Types() [Fact] public async Task Should_Handle_No_Token_Issued() { - IssueToken("authorization_code", false, null, false, ProofType.None, false); + IssueToken(GrantType.AuthorizationCode, false, null, false, ProofType.None, false); var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); @@ -125,37 +126,58 @@ public async Task Should_Handle_No_Token_Issued() [Fact] public async Task Should_Handle_Initial_Grant_Type_Count() { - IssueToken("authorization_code", true, AccessTokenType.Jwt, false, ProofType.None, false); + IssueToken(GrantType.AuthorizationCode, true, AccessTokenType.Jwt, false, ProofType.None, false); var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); var tokenIssueCounts = result.RootElement.GetProperty("TokenIssueCounts"); - tokenIssueCounts.GetProperty("authorization_code").GetInt64().ShouldBe(1); + tokenIssueCounts.GetProperty(GrantType.AuthorizationCode).GetInt64().ShouldBe(1); } [Fact] public async Task Should_Handle_Multiple_Grant_Type_Counts() { - IssueToken("authorization_code", true, AccessTokenType.Jwt, false, ProofType.None, false); - IssueToken("client_credentials", true, AccessTokenType.Jwt, false, ProofType.None, false); + IssueToken(GrantType.AuthorizationCode, true, AccessTokenType.Jwt, false, ProofType.None, false); + IssueToken(GrantType.ClientCredentials, true, AccessTokenType.Jwt, false, ProofType.None, false); var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); var tokenIssueCounts = result.RootElement.GetProperty("TokenIssueCounts"); - tokenIssueCounts.GetProperty("authorization_code").GetInt64().ShouldBe(1); - tokenIssueCounts.GetProperty("client_credentials").GetInt64().ShouldBe(1); + tokenIssueCounts.GetProperty(GrantType.AuthorizationCode).GetInt64().ShouldBe(1); + tokenIssueCounts.GetProperty(GrantType.ClientCredentials).GetInt64().ShouldBe(1); } [Fact] public async Task Should_Handle_Multiple_Grant_Type_Counts_With_Grant_Type() { - IssueToken("authorization_code", true, AccessTokenType.Jwt, false, ProofType.None, false); - IssueToken("authorization_code", true, AccessTokenType.Jwt, false, ProofType.None, false); + IssueToken(GrantType.AuthorizationCode, true, AccessTokenType.Jwt, false, ProofType.None, false); + IssueToken(GrantType.AuthorizationCode, true, AccessTokenType.Jwt, false, ProofType.None, false); var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); var tokenIssueCounts = result.RootElement.GetProperty("TokenIssueCounts"); - tokenIssueCounts.GetProperty("authorization_code").GetInt64().ShouldBe(2); + tokenIssueCounts.GetProperty(GrantType.AuthorizationCode).GetInt64().ShouldBe(2); + } + + [Fact] + public async Task Should_Handle_Grant_Type_Counts_For_All_Grant_Types() + { + var grantTypes = typeof(GrantType).GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy) + .Where(field => field.IsLiteral && !field.IsInitOnly) + .Select(field => field.GetValue(null)?.ToString()) + .Where(value => value != null); + foreach (var grantType in grantTypes) + { + IssueToken(grantType, true, AccessTokenType.Jwt, false, ProofType.None, false); + } + + var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); + + var tokenIssueCounts = result.RootElement.GetProperty("TokenIssueCounts"); + foreach (var grantType in grantTypes) + { + tokenIssueCounts.GetProperty(grantType).GetInt64().ShouldBe(1); + } } [Fact]