From 36fdba9597b59539049835c7b0f0d95d0c470cdd Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Sat, 17 Oct 2026 23:56:41 +0200 Subject: [PATCH] Decode JWT: net10.0, JsonWebTokenHandler, Base64Url decode, Live test - App and Tests move from net8.0 to net10.0. - System.IdentityModel.Tokens.Jwt 7.0.3 is replaced by Microsoft.IdentityModel.JsonWebTokens 8.23.0, and the deprecated IdentityModel 6.2.0 by Duende.IdentityModel 8.1.0. - JwtUtils reads the token with JsonWebTokenHandler.ReadJsonWebToken() and DecodeJwt reads Kid, Alg and EncodedToken from JsonWebToken. - New DecodeJwtWithoutLibrary() decodes the header and payload with System.Buffers.Text.Base64Url. - The test that calls the Duende demo server is renamed with a _Live suffix so CI skips it; new theory covers the no-package decode. - Test packages: xunit 2.9.3, xunit.runner.visualstudio 4.0.1, Microsoft.NET.Test.Sdk 18.11.0, coverlet.collector 10.1.0. --- authorization-dotnet/DecodeJwt/App/App.csproj | 6 ++-- .../DecodeJwt/App/JwtUtils.cs | 35 ++++++++++++------- authorization-dotnet/DecodeJwt/App/Program.cs | 8 +++-- .../DecodeJwt/Tests/DecodeJwtTest.cs | 16 +++++++-- .../DecodeJwt/Tests/Tests.csproj | 10 +++--- 5 files changed, 50 insertions(+), 25 deletions(-) diff --git a/authorization-dotnet/DecodeJwt/App/App.csproj b/authorization-dotnet/DecodeJwt/App/App.csproj index bb496081c7..ac8d17ed1d 100644 --- a/authorization-dotnet/DecodeJwt/App/App.csproj +++ b/authorization-dotnet/DecodeJwt/App/App.csproj @@ -2,13 +2,13 @@ Exe - net8.0 + net10.0 enable enable - - + + diff --git a/authorization-dotnet/DecodeJwt/App/JwtUtils.cs b/authorization-dotnet/DecodeJwt/App/JwtUtils.cs index c7174aa835..d0b70f0540 100644 --- a/authorization-dotnet/DecodeJwt/App/JwtUtils.cs +++ b/authorization-dotnet/DecodeJwt/App/JwtUtils.cs @@ -1,5 +1,7 @@ -using System.IdentityModel.Tokens.Jwt; -using IdentityModel.Client; +using System.Buffers.Text; +using System.Text; +using Duende.IdentityModel.Client; +using Microsoft.IdentityModel.JsonWebTokens; namespace App; @@ -20,25 +22,25 @@ public static class JwtUtils { Address = discoveryDocument.TokenEndpoint, ClientId = "m2m", ClientSecret = "secret", Scope = "api" }); } - if (tokenResponse.IsError) + if (tokenResponse.IsError) { throw new Exception(tokenResponse.Error); } - + return tokenResponse.AccessToken; } - public static JwtSecurityToken ConvertJwtStringToJwtSecurityToken(string? jwt) + public static JsonWebToken ConvertJwtStringToJsonWebToken(string jwt) { - var handler = new JwtSecurityTokenHandler(); - var token = handler.ReadJwtToken(jwt); - + var handler = new JsonWebTokenHandler(); + var token = handler.ReadJsonWebToken(jwt); + return token; } - public static DecodedToken DecodeJwt(JwtSecurityToken token) + public static DecodedToken DecodeJwt(JsonWebToken token) { - var keyId = token.Header.Kid; + var keyId = token.Kid; var audience = token.Audiences.ToList(); var claims = token.Claims.Select(claim => (claim.Type, claim.Value)).ToList(); @@ -48,12 +50,21 @@ public static DecodedToken DecodeJwt(JwtSecurityToken token) audience, claims, token.ValidTo, - token.SignatureAlgorithm, - token.RawData, + token.Alg, + token.EncodedToken, token.Subject, token.ValidFrom, token.EncodedHeader, token.EncodedPayload ); } + + public static (string Header, string Payload) DecodeJwtWithoutLibrary(string jwt) + { + var parts = jwt.Split('.'); + var header = Encoding.UTF8.GetString(Base64Url.DecodeFromChars(parts[0])); + var payload = Encoding.UTF8.GetString(Base64Url.DecodeFromChars(parts[1])); + + return (header, payload); + } } diff --git a/authorization-dotnet/DecodeJwt/App/Program.cs b/authorization-dotnet/DecodeJwt/App/Program.cs index 350180ffd1..f9a75b3bfa 100644 --- a/authorization-dotnet/DecodeJwt/App/Program.cs +++ b/authorization-dotnet/DecodeJwt/App/Program.cs @@ -1,8 +1,12 @@ using App; -var accessToken = await JwtUtils.GetAccessTokenFromDuendeIdentityServerAsync(); +var accessToken = await JwtUtils.GetAccessTokenFromDuendeIdentityServerAsync() + ?? throw new InvalidOperationException("The token endpoint returned no access token."); Console.WriteLine(accessToken); -var jwt = JwtUtils.ConvertJwtStringToJwtSecurityToken(accessToken); +var jwt = JwtUtils.ConvertJwtStringToJsonWebToken(accessToken); Console.WriteLine(jwt); var jwtData = JwtUtils.DecodeJwt(jwt); Console.WriteLine(jwtData); +var (header, payload) = JwtUtils.DecodeJwtWithoutLibrary(accessToken); +Console.WriteLine(header); +Console.WriteLine(payload); diff --git a/authorization-dotnet/DecodeJwt/Tests/DecodeJwtTest.cs b/authorization-dotnet/DecodeJwt/Tests/DecodeJwtTest.cs index be826fd12f..5845f24081 100644 --- a/authorization-dotnet/DecodeJwt/Tests/DecodeJwtTest.cs +++ b/authorization-dotnet/DecodeJwt/Tests/DecodeJwtTest.cs @@ -3,7 +3,7 @@ namespace Tests; public class DecodeJwtTest { [Fact] - public async Task WhenJwtIsRetrievedFromServer_ThenItShouldNotBeNull() + public async Task WhenJwtIsRetrievedFromServer_ThenItShouldNotBeNull_Live() { var jwt = await JwtUtils.GetAccessTokenFromDuendeIdentityServerAsync(); @@ -14,11 +14,21 @@ public async Task WhenJwtIsRetrievedFromServer_ThenItShouldNotBeNull() [ClassData(typeof(JwtTestData))] public void WhenJwtIsValid_ThenItShouldDecode(string token) { - var jwt = JwtUtils.ConvertJwtStringToJwtSecurityToken(token); + var jwt = JwtUtils.ConvertJwtStringToJsonWebToken(token); var jwtData = JwtUtils.DecodeJwt(jwt); Assert.NotNull(jwtData.Issuer); Assert.NotEmpty(jwtData.Audience); Assert.NotEmpty(jwtData.Claims); } -} \ No newline at end of file + + [Theory] + [ClassData(typeof(JwtTestData))] + public void WhenJwtIsDecodedWithoutLibrary_ThenHeaderAndPayloadAreJson(string token) + { + var (header, payload) = JwtUtils.DecodeJwtWithoutLibrary(token); + + Assert.Contains("\"alg\":\"RS256\"", header); + Assert.Contains("\"iss\":\"https://demo.duendesoftware.com\"", payload); + } +} diff --git a/authorization-dotnet/DecodeJwt/Tests/Tests.csproj b/authorization-dotnet/DecodeJwt/Tests/Tests.csproj index 5884ed7ca2..c878fa5b95 100644 --- a/authorization-dotnet/DecodeJwt/Tests/Tests.csproj +++ b/authorization-dotnet/DecodeJwt/Tests/Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable @@ -10,13 +10,13 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all