<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Abhishek Mishra</title>
    <description>The latest articles on DEV Community by Abhishek Mishra (@mishrababhishek).</description>
    <link>https://dev.to/mishrababhishek</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3961394%2F59c25c71-b35a-4727-85cf-fb9e8c71ae1a.png</url>
      <title>DEV Community: Abhishek Mishra</title>
      <link>https://dev.to/mishrababhishek</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mishrababhishek"/>
    <language>en</language>
    <item>
      <title>Building AutoStack.Identity: A Zero-Dependency .NET 10 Library for SAML 2.0, JWT, and XML Signing</title>
      <dc:creator>Abhishek Mishra</dc:creator>
      <pubDate>Sun, 31 May 2026 16:19:52 +0000</pubDate>
      <link>https://dev.to/mishrababhishek/building-autostackidentity-a-zero-dependency-net-10-library-for-saml-20-jwt-and-xml-signing-5992</link>
      <guid>https://dev.to/mishrababhishek/building-autostackidentity-a-zero-dependency-net-10-library-for-saml-20-jwt-and-xml-signing-5992</guid>
      <description>&lt;h2&gt;
  
  
  The Problem That Started This
&lt;/h2&gt;

&lt;p&gt;We were building a healthcare connectivity platform — multi-tenant, Azure-hosted, integrating with enterprise IdPs via SAML 2.0. The stack was .NET 10, Angular 21, Azure SQL. Standard stuff.&lt;/p&gt;

&lt;p&gt;When the SSO requirement came in, the instinct was to reach for one of the usual suspects: &lt;code&gt;Microsoft.Identity.Web&lt;/code&gt;, a hosted OpenID Connect flow, or something sitting on top of &lt;code&gt;IdentityServer&lt;/code&gt;. We tried each. They all work fine if your scenario fits their opinionated model. Ours didn't.&lt;/p&gt;

&lt;p&gt;The first wrinkle was that SAML 2.0 is not natively supported by ASP.NET Core's auth middleware — you have to pull in third-party libraries like &lt;code&gt;ComponentSpace.Saml2&lt;/code&gt;, &lt;code&gt;Sustainsys.Saml2&lt;/code&gt;, or similar. These are not small. They carry entire middleware pipelines, NuGet trees, and assumptions about how you've wired your host. For a greenfield platform where we controlled everything end-to-end, that overhead felt like the wrong tradeoff.&lt;/p&gt;

&lt;p&gt;The second problem was testing. Most SAML libraries are tightly coupled to &lt;code&gt;HttpContext&lt;/code&gt;. Running an assertion round-trip in a unit test meant mocking the entire request pipeline, which is neither fast nor reliable.&lt;/p&gt;

&lt;p&gt;The third — and this one bit us directly — was a security gap. During an audit of our SSO implementation, I identified a SAML token replay vulnerability: the IdP responses lacked assertion ID tracking. An attacker who intercepted a valid SAMLResponse could replay it within its validity window and authenticate as that user. Fixing it in a third-party library meant either patching or wrapping in ways that felt fragile.&lt;/p&gt;

&lt;p&gt;That's when I decided to write &lt;code&gt;AutoStack.Identity&lt;/code&gt; from scratch.&lt;/p&gt;




&lt;h2&gt;
  
  
  Background: What Existing Solutions Provided (and Didn't)
&lt;/h2&gt;

&lt;p&gt;The .NET identity ecosystem has matured a lot in recent years, but it's optimized for certain shapes of problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;System.IdentityModel.Tokens.Jwt&lt;/code&gt;&lt;/strong&gt; handles JWT nicely, but it's tied to &lt;code&gt;ClaimsPrincipal&lt;/code&gt; and &lt;code&gt;SecurityToken&lt;/code&gt; abstractions that add weight when all you need is issue-and-verify.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Microsoft.Identity.Web&lt;/code&gt;&lt;/strong&gt; is excellent for Azure AD and OIDC but SAML support is absent by design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sustainsys.Saml2&lt;/strong&gt; and &lt;strong&gt;ComponentSpace&lt;/strong&gt; both work, but they install themselves into the middleware pipeline and make testing hard. They also assume you want ASP.NET Core integration, not a portable library.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;System.Security.Cryptography.Xml&lt;/code&gt;&lt;/strong&gt; is the underlying BCL primitive for XML signing — available since .NET Framework — but using it directly for SAML is painful because of namespace canonicalization issues, signature insertion placement, and the need to preserve whitespace semantics.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The gap I was targeting: a library that provides these primitives as &lt;strong&gt;pure, testable helpers&lt;/strong&gt; — no middleware, no DI framework dependency, no ASP.NET coupling. Just a class you can instantiate anywhere and call.&lt;/p&gt;




&lt;h2&gt;
  
  
  What AutoStack.Identity Does
&lt;/h2&gt;

&lt;p&gt;At its core the library is three things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JWT issuance and validation&lt;/strong&gt; with pluggable signing algorithms (RS256/384/512, ES256/384/512, HS256/384/512).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SAML 2.0 SP flows&lt;/strong&gt; — building &lt;code&gt;AuthnRequest&lt;/code&gt; XML for SP-initiated SSO, and parsing &lt;code&gt;SAMLResponse&lt;/code&gt; payloads back into typed models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SAML 2.0 IdP flows&lt;/strong&gt; — building signed assertions and &lt;code&gt;Response&lt;/code&gt; documents, useful for testing IdP behavior or running a custom IdP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;XML digital signing and verification&lt;/strong&gt; — the underlying layer powering SAML signing, also exposed directly if you need to sign arbitrary XML documents.&lt;/p&gt;

&lt;p&gt;The only external NuGet dependency is &lt;code&gt;System.Security.Cryptography.Xml&lt;/code&gt;. Everything else comes from the BCL.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture and Design Decisions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Crypto Behind Interfaces
&lt;/h3&gt;

&lt;p&gt;The most deliberate structural decision was keeping cryptographic implementations completely separate from the protocol logic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IJwtSigner&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Algorithm&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="nf"&gt;Sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IJwtVerifier&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;Verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;JwtIssuerHelper&lt;/code&gt; and &lt;code&gt;JwtValidationHelper&lt;/code&gt; accept these interfaces. They have no idea whether the backing key is RSA, ECDSA, or HMAC. The three concrete implementations (&lt;code&gt;RsaJwtSigner&lt;/code&gt;, &lt;code&gt;EcdsaJwtSigner&lt;/code&gt;, &lt;code&gt;HmacJwtSigner&lt;/code&gt;) each handle their specific crypto, and all three implement both &lt;code&gt;IJwtSigner&lt;/code&gt; and &lt;code&gt;IJwtVerifier&lt;/code&gt; where applicable.&lt;/p&gt;

&lt;p&gt;This matters for testing. You can implement &lt;code&gt;IJwtSigner&lt;/code&gt; with a trivial fake that returns a fixed signature and write tests that focus entirely on claim construction, expiry logic, or audience validation — without touching real cryptographic keys.&lt;/p&gt;

&lt;p&gt;The same pattern applies to XML signing via &lt;code&gt;IXmlSigner&lt;/code&gt;, and to SAML response parsing via &lt;code&gt;ISamlSpProvider&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  TimeProvider Injection Everywhere
&lt;/h3&gt;

&lt;p&gt;Every class that touches time takes &lt;code&gt;TimeProvider? timeProvider = null&lt;/code&gt; in its constructor. When null, it defaults to &lt;code&gt;TimeProvider.System&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;JwtIssuerHelper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JwtIssuerOptions&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IJwtSigner&lt;/span&gt; &lt;span class="n"&gt;signer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TimeProvider&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;timeProvider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_timeProvider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;timeProvider&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="n"&gt;TimeProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the .NET 8+ abstraction for abstractable time — it lets tests inject a fake time source and make deterministic assertions about expiry and &lt;code&gt;NotBefore&lt;/code&gt; enforcement without &lt;code&gt;Thread.Sleep&lt;/code&gt; or brittle timestamp comparisons. This pattern is throughout the library: &lt;code&gt;JwtIssuerHelper&lt;/code&gt;, &lt;code&gt;JwtValidationHelper&lt;/code&gt;, &lt;code&gt;SamlSpHelper&lt;/code&gt;, &lt;code&gt;SamlIdpHelper&lt;/code&gt;, &lt;code&gt;CertificateExpiryRule&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  XML Verification as a Composable Pipeline
&lt;/h3&gt;

&lt;p&gt;Rather than writing a monolithic verification method, XML signature verification is structured as a pipeline of independent rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;XmlVerificationPipelineBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SignatureVerificationRule&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CertificateExpiryRule&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CertificateTrustRule&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithFailFast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;VerifyAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each rule implements &lt;code&gt;IXmlVerificationRule&lt;/code&gt; — a single async method that takes a context and returns a &lt;code&gt;VerificationResult&lt;/code&gt;. Rules communicate through a shared &lt;code&gt;Bag&lt;/code&gt; dictionary on the context. &lt;code&gt;SignatureVerificationRule&lt;/code&gt; runs first and stores the extracted certificate in the bag under a typed key. &lt;code&gt;CertificateExpiryRule&lt;/code&gt; and &lt;code&gt;CertificateTrustRule&lt;/code&gt; read from that key rather than re-extracting the certificate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In SignatureVerificationRule:&lt;/span&gt;
&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bag&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;BagKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// In CertificateExpiryRule:&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cert&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetBagValue&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;X509Certificate2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;SignatureVerificationRule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BagKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pipeline can be configured with &lt;code&gt;FailFast&lt;/code&gt; — stop on first failure — or collect all failures and return them together. Custom rules can be added without modifying the library.&lt;/p&gt;

&lt;h3&gt;
  
  
  The XmlBuilder
&lt;/h3&gt;

&lt;p&gt;Working with &lt;code&gt;System.Xml.XmlDocument&lt;/code&gt; directly while building SAML is frustrating. Namespace declarations bleed across elements, attribute order is unpredictable, and it's easy to produce XML that is syntactically valid but doesn't survive canonicalization the way SAML processors expect.&lt;/p&gt;

&lt;p&gt;I wrote a thin fluent builder that separates the description of a document from its serialization:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;XmlBuilder&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"AuthnRequest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SamlConstants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Protocol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SamlConstants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Prefix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Protocol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"IssueInstant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;issuedAt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcDateTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Issuer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SamlConstants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Assertion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SamlConstants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Prefix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Assertion&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EntityId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BuildDocument&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each &lt;code&gt;WithChild&lt;/code&gt; call takes a lambda for configuration, so nesting stays readable without the call depth exploding. The builder first constructs an intermediate &lt;code&gt;XmlElementNode&lt;/code&gt; tree (plain records with no DOM involvement), then materializes it into a &lt;code&gt;XmlDocument&lt;/code&gt; in a single pass. This makes the structural intent easy to follow and keeps the messy &lt;code&gt;doc.CreateElement(prefix, localName, ns)&lt;/code&gt; calls isolated to one method.&lt;/p&gt;




&lt;h2&gt;
  
  
  Implementation Details
&lt;/h2&gt;

&lt;h3&gt;
  
  
  JWT Issuance — Staying Close to the Spec
&lt;/h3&gt;

&lt;p&gt;The JWT implementation in &lt;code&gt;JwtIssuerHelper.Issue()&lt;/code&gt; deliberately avoids abstractions layered on top of the spec:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;headerEncoded&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Base64UrlEncode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SerializeToUtf8Bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;BuildHeader&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;payloadEncoded&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Base64UrlEncode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SerializeToUtf8Bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;BuildPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;descriptor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jti&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;signingInput&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;headerEncoded&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;payloadEncoded&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;signature&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_signer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ASCII&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signingInput&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;signingInput&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;Base64UrlEncode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire RFC 7519 issuance path: build header, build payload, base64url-encode both, sign the concatenation with a period separator, append the base64url-encoded signature. Nothing hidden.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;BuildPayload&lt;/code&gt; constructs a &lt;code&gt;Dictionary&amp;lt;string, object&amp;gt;&lt;/code&gt; using the registered claim names as constants, then merges in any extra claims from the &lt;code&gt;JwtDescriptor&lt;/code&gt;. Because this is serialized by &lt;code&gt;System.Text.Json&lt;/code&gt; rather than a JWT-specific library, the JSON output is predictable and doesn't carry extra envelope fields.&lt;/p&gt;

&lt;h3&gt;
  
  
  Timing-Safe HMAC Comparison
&lt;/h3&gt;

&lt;p&gt;One detail worth calling out in &lt;code&gt;HmacJwtSigner&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;Verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ComputeHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;CryptographicOperations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FixedTimeEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CryptographicOperations.FixedTimeEquals&lt;/code&gt; was added in .NET Core 2.1 precisely to prevent timing attacks on MAC verification. A naive &lt;code&gt;expected.SequenceEqual(signature)&lt;/code&gt; would short-circuit on the first mismatched byte, leaking timing information. This is the correct implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  ECDSA Signatures Use IEEE P1363 Format
&lt;/h3&gt;

&lt;p&gt;For ECDSA JWT signing, the implementation uses &lt;code&gt;DSASignatureFormat.IeeeP1363FixedFieldConcatenation&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_ecdsa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SignData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_hashAlgorithm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DSASignatureFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IeeeP1363FixedFieldConcatenation&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters because .NET's default ECDSA output is DER-encoded, but the JWT spec (RFC 7518) requires the raw concatenation of &lt;code&gt;r&lt;/code&gt; and &lt;code&gt;s&lt;/code&gt; values (IEEE P1363). Using the wrong format produces tokens that will fail verification by any compliant JWT library. It's easy to get this wrong and not notice until another JWT consumer rejects your tokens.&lt;/p&gt;

&lt;h3&gt;
  
  
  Signature Insertion Mode
&lt;/h3&gt;

&lt;p&gt;SAML is particular about where the &lt;code&gt;&amp;lt;ds:Signature&amp;gt;&lt;/code&gt; element appears within the response XML. For SAML assertions specifically, the signature must follow the &lt;code&gt;&amp;lt;saml:Issuer&amp;gt;&lt;/code&gt; element (i.e., the second child position). The &lt;code&gt;SignatureInsertionMode&lt;/code&gt; enum encodes the three common positions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;SignatureInsertionMode&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;AppendToRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;AfterFirstChild&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// used for SAML assertions&lt;/span&gt;
    &lt;span class="n"&gt;PrependToRoot&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SAML helpers pass &lt;code&gt;InsertionMode = SignatureInsertionMode.AfterFirstChild&lt;/code&gt; when calling the signer. Without this, some IdP and SP implementations will reject an otherwise valid signature simply because the element order doesn't match their schema expectations.&lt;/p&gt;




&lt;h2&gt;
  
  
  Challenges and Trade-offs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  "No External Dependencies" Has a Cost
&lt;/h3&gt;

&lt;p&gt;The decision to avoid NuGet dependencies beyond the BCL means implementing things that you'd normally get for free. Base64Url encoding and decoding, for instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;Base64UrlEncode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToBase64String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;TrimEnd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'='&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'+'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sc"&gt;'-'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sc"&gt;'_'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="nf"&gt;Base64UrlDecode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;padded&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'-'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sc"&gt;'+'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'_'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sc"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;padded&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;padded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;padded&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"=="&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="m"&gt;3&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;padded&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"="&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;padded&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromBase64String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;padded&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not complex, but code that needs to exist and be tested. The payoff is zero transitive dependency issues at the call site.&lt;/p&gt;

&lt;h3&gt;
  
  
  XML Canonicalization Is Not Forgiving
&lt;/h3&gt;

&lt;p&gt;The XML signature standard (XMLDsig) requires exclusive canonicalization (Exc-C14N) before signing. This means the serialized form of the document matters — whitespace, namespace declarations, attribute order. Even the difference between &lt;code&gt;PreserveWhitespace = true&lt;/code&gt; and &lt;code&gt;false&lt;/code&gt; on &lt;code&gt;XmlDocument&lt;/code&gt; can break a signature.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;X509XmlSigner.SignAsync&lt;/code&gt; method clones the document before signing to avoid mutating the caller's document:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;workingDoc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;CloneDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the SAML parser sets &lt;code&gt;PreserveWhitespace = true&lt;/code&gt; explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;XmlDocument&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;PreserveWhitespace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Getting the whitespace handling wrong produces documents that fail signature verification even when the certificate and key are correct. It took more than a few "but the cert is fine" debugging sessions to pin this down.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Bag Pattern Has a Weak Contract
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;Dictionary&amp;lt;string, object&amp;gt; Bag&lt;/code&gt; on &lt;code&gt;XmlVerificationContext&lt;/code&gt; is the inter-rule communication mechanism. It works, but it's stringly-typed on the key side. The mitigation is using &lt;code&gt;const string&lt;/code&gt; bag keys defined on the rule that produces the value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;BagKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"SignatureVerificationRule.SignerCertificate"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Consuming rules reference that constant rather than duplicating the string. It's still more fragile than a typed property would be, but adding a typed property to the context would mean the context knows about specific rules — which breaks the pipeline's extensibility.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Own your security-critical abstractions.&lt;/strong&gt; When you need to add assertion ID tracking to prevent token replay, it's a two-line change in your own library. In someone else's library, it might be a support ticket or a fork.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;TimeProvider&lt;/code&gt; is worth using everywhere time touches logic.&lt;/strong&gt; Once you've written a validation test that didn't require a &lt;code&gt;Thread.Sleep&lt;/code&gt;, you won't go back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SAML 2.0 is actually not that scary.&lt;/strong&gt; The spec is verbose but the SP-initiated SSO flow is well-defined: generate an &lt;code&gt;AuthnRequest&lt;/code&gt;, redirect the user to the IdP, receive a &lt;code&gt;SAMLResponse&lt;/code&gt; at your ACS URL, verify the signature, extract the &lt;code&gt;NameID&lt;/code&gt; and attributes. The implementation fits in a few hundred lines when you're not also shipping middleware.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't serialize to &lt;code&gt;XmlDocument&lt;/code&gt; and then sign. Build first, sign once.&lt;/strong&gt; The signer operates on the document after construction. The &lt;code&gt;XmlBuilder&lt;/code&gt; generates a clean document without extraneous namespace declarations, and the signer transforms a clone — so the original is never touched.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sealed records for descriptors and options.&lt;/strong&gt; Using &lt;code&gt;sealed record&lt;/code&gt; with &lt;code&gt;required&lt;/code&gt; &lt;code&gt;init&lt;/code&gt;-only properties for things like &lt;code&gt;JwtDescriptor&lt;/code&gt;, &lt;code&gt;SamlSpOptions&lt;/code&gt;, and &lt;code&gt;JwtIssuerOptions&lt;/code&gt; catches misconfiguration at object construction rather than at runtime. Combined with nullable reference types enabled, it's hard to accidentally build a half-initialized configuration object.&lt;/p&gt;




&lt;h2&gt;
  
  
  Future Improvements
&lt;/h2&gt;

&lt;p&gt;A few things I'd like to add that weren't blocking the production use case:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assertion ID store for replay prevention.&lt;/strong&gt; The library currently builds SAML assertions with unique IDs (&lt;code&gt;_{Guid.NewGuid():N}&lt;/code&gt;) and enforces &lt;code&gt;NotBefore&lt;/code&gt;/&lt;code&gt;NotOnOrAfter&lt;/code&gt; boundaries, but the assertion ID tracking store needs to be implemented by the consuming application. Adding an &lt;code&gt;IAssertionIdStore&lt;/code&gt; interface with an in-memory and Redis implementation would make replay prevention easier to wire up correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SAML metadata parsing.&lt;/strong&gt; Currently you configure IdP endpoints manually in &lt;code&gt;SamlSpOptions&lt;/code&gt;. Parsing the standard &lt;code&gt;EntityDescriptor&lt;/code&gt; XML from an IdP's metadata URL would reduce the manual configuration surface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JWK (JSON Web Key) support.&lt;/strong&gt; The library currently deals with raw key material and certificates. Adding support for JWKS endpoints would make it easier to use in systems that publish public keys via a &lt;code&gt;/.well-known/jwks.json&lt;/code&gt; endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NuGet package.&lt;/strong&gt; The library is open-source and consumed directly today. Publishing it as a package on NuGet would reduce the friction for anyone who wants to use it without adding a submodule.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;AutoStack.Identity&lt;/code&gt; isn't trying to replace &lt;code&gt;Microsoft.Identity.Web&lt;/code&gt; or a full-featured SAML middleware. It's a different thing: a set of precise, testable primitives for teams that need direct control over how JWT tokens and SAML assertions are constructed, signed, and validated.&lt;/p&gt;

&lt;p&gt;The constraints were real — healthcare data, enterprise IdP integration, a hard security finding to address. Writing the library rather than adopting an existing one turned out to be the right call for this context. Not because existing libraries are bad, but because owning the implementation meant the token replay fix was a two-hour task rather than a multi-library investigation.&lt;/p&gt;

&lt;p&gt;The entire thing is a single .NET 10 class library. One external NuGet dependency. Works in any context that can reference a class library — ASP.NET Core, Azure Functions, console apps, test runners.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/mishrababhishek/AutoStack" rel="noopener noreferrer"&gt;github.com/mishrababhishek/AutoStack&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>opensource</category>
      <category>jwt</category>
      <category>authentication</category>
    </item>
  </channel>
</rss>
