惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Engineering at Meta
Engineering at Meta
月光博客
月光博客
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
V
Visual Studio Blog
大猫的无限游戏
大猫的无限游戏
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
量子位
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
腾讯CDC
S
Securelist
Know Your Adversary
Know Your Adversary
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
博客园 - 【当耐特】
V2EX - 技术
V2EX - 技术
J
Java Code Geeks
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Hugging Face - Blog
Hugging Face - Blog
Cisco Talos Blog
Cisco Talos Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
阮一峰的网络日志
阮一峰的网络日志
美团技术团队
W
WeLiveSecurity
S
SegmentFault 最新的问题
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
宝玉的分享
宝玉的分享
博客园 - Franky
S
Security Affairs
博客园 - 司徒正美
V
V2EX
K
Kaspersky official blog
T
Threatpost
NISL@THU
NISL@THU
博客园 - 叶小钗
Help Net Security
Help Net Security
PCI Perspectives
PCI Perspectives
IT之家
IT之家
小众软件
小众软件
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Last Week in AI
Last Week in AI
Jina AI
Jina AI
爱范儿
爱范儿
罗磊的独立博客
N
News and Events Feed by Topic
博客园 - 三生石上(FineUI控件)
有赞技术团队
有赞技术团队
P
Proofpoint News Feed
L
LINUX DO - 热门话题
Google Online Security Blog
Google Online Security Blog
雷峰网
雷峰网
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Building AutoStack.Identity: A Zero-Dependency .NET 10 Library for SAML 2.0, JWT, and XML Signing
Abhishek Mishra · 2026-06-01 · via DEV Community

The Problem That Started This

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.

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

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 ComponentSpace.Saml2, Sustainsys.Saml2, 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.

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

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.

That's when I decided to write AutoStack.Identity from scratch.


Background: What Existing Solutions Provided (and Didn't)

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

  • System.IdentityModel.Tokens.Jwt handles JWT nicely, but it's tied to ClaimsPrincipal and SecurityToken abstractions that add weight when all you need is issue-and-verify.
  • Microsoft.Identity.Web is excellent for Azure AD and OIDC but SAML support is absent by design.
  • Sustainsys.Saml2 and ComponentSpace 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.
  • System.Security.Cryptography.Xml 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.

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


What AutoStack.Identity Does

At its core the library is three things:

JWT issuance and validation with pluggable signing algorithms (RS256/384/512, ES256/384/512, HS256/384/512).

SAML 2.0 SP flows — building AuthnRequest XML for SP-initiated SSO, and parsing SAMLResponse payloads back into typed models.

SAML 2.0 IdP flows — building signed assertions and Response documents, useful for testing IdP behavior or running a custom IdP.

XML digital signing and verification — the underlying layer powering SAML signing, also exposed directly if you need to sign arbitrary XML documents.

The only external NuGet dependency is System.Security.Cryptography.Xml. Everything else comes from the BCL.


Architecture and Design Decisions

Crypto Behind Interfaces

The most deliberate structural decision was keeping cryptographic implementations completely separate from the protocol logic.

public interface IJwtSigner
{
    string Algorithm { get; }
    byte[] Sign(byte[] data);
}

public interface IJwtVerifier
{
    bool Verify(byte[] data, byte[] signature);
}

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

This matters for testing. You can implement IJwtSigner 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.

The same pattern applies to XML signing via IXmlSigner, and to SAML response parsing via ISamlSpProvider.

TimeProvider Injection Everywhere

Every class that touches time takes TimeProvider? timeProvider = null in its constructor. When null, it defaults to TimeProvider.System.

public JwtIssuerHelper(JwtIssuerOptions options, IJwtSigner signer, TimeProvider? timeProvider = null)
{
    _timeProvider = timeProvider ?? TimeProvider.System;
}

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

XML Verification as a Composable Pipeline

Rather than writing a monolithic verification method, XML signature verification is structured as a pipeline of independent rules:

var pipeline = XmlVerificationPipelineBuilder.Create()
    .AddRule(new SignatureVerificationRule())
    .AddRule(new CertificateExpiryRule())
    .AddRule(new CertificateTrustRule())
    .WithFailFast(true)
    .Build();

var result = await pipeline.VerifyAsync(context);

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

// In SignatureVerificationRule:
context.Bag[BagKey] = cert;

// In CertificateExpiryRule:
var cert = context.GetBagValue<X509Certificate2>(SignatureVerificationRule.BagKey);

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

The XmlBuilder

Working with System.Xml.XmlDocument 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.

I wrote a thin fluent builder that separates the description of a document from its serialization:

var doc = XmlBuilder
    .Create("AuthnRequest", SamlConstants.Ns.Protocol)
    .WithPrefix(SamlConstants.Prefix.Protocol)
    .WithAttribute("ID", requestId)
    .WithAttribute("Version", "2.0")
    .WithAttribute("IssueInstant", issuedAt.UtcDateTime)
    .WithChild("Issuer", SamlConstants.Ns.Assertion, b => b
        .WithPrefix(SamlConstants.Prefix.Assertion)
        .WithText(_options.EntityId))
    .BuildDocument();

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


Implementation Details

JWT Issuance — Staying Close to the Spec

The JWT implementation in JwtIssuerHelper.Issue() deliberately avoids abstractions layered on top of the spec:

var headerEncoded = Base64UrlEncode(JsonSerializer.SerializeToUtf8Bytes(BuildHeader()));
var payloadEncoded = Base64UrlEncode(JsonSerializer.SerializeToUtf8Bytes(BuildPayload(descriptor, now, expires, jti)));

var signingInput = $"{headerEncoded}.{payloadEncoded}";
var signature = _signer.Sign(Encoding.ASCII.GetBytes(signingInput));

return $"{signingInput}.{Base64UrlEncode(signature)}";

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.

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

Timing-Safe HMAC Comparison

One detail worth calling out in HmacJwtSigner:

public bool Verify(byte[] data, byte[] signature)
{
    var expected = _hmac.ComputeHash(data);
    return CryptographicOperations.FixedTimeEquals(expected, signature);
}

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

ECDSA Signatures Use IEEE P1363 Format

For ECDSA JWT signing, the implementation uses DSASignatureFormat.IeeeP1363FixedFieldConcatenation:

return _ecdsa.SignData(data, _hashAlgorithm, DSASignatureFormat.IeeeP1363FixedFieldConcatenation);

This matters because .NET's default ECDSA output is DER-encoded, but the JWT spec (RFC 7518) requires the raw concatenation of r and s 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.

Signature Insertion Mode

SAML is particular about where the <ds:Signature> element appears within the response XML. For SAML assertions specifically, the signature must follow the <saml:Issuer> element (i.e., the second child position). The SignatureInsertionMode enum encodes the three common positions:

public enum SignatureInsertionMode
{
    AppendToRoot,
    AfterFirstChild,  // used for SAML assertions
    PrependToRoot
}

The SAML helpers pass InsertionMode = SignatureInsertionMode.AfterFirstChild 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.


Challenges and Trade-offs

"No External Dependencies" Has a Cost

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:

internal static string Base64UrlEncode(byte[] data) =>
    Convert.ToBase64String(data).TrimEnd('=').Replace('+', '-').Replace('/', '_');

internal static byte[] Base64UrlDecode(string input)
{
    var padded = input.Replace('-', '+').Replace('_', '/');
    padded = (padded.Length % 4) switch
    {
        2 => padded + "==",
        3 => padded + "=",
        _ => padded
    };
    return Convert.FromBase64String(padded);
}

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

XML Canonicalization Is Not Forgiving

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 PreserveWhitespace = true and false on XmlDocument can break a signature.

The X509XmlSigner.SignAsync method clones the document before signing to avoid mutating the caller's document:

var workingDoc = CloneDocument(document);

And the SAML parser sets PreserveWhitespace = true explicitly:

var doc = new XmlDocument { PreserveWhitespace = true };

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.

The Bag Pattern Has a Weak Contract

The Dictionary<string, object> Bag on XmlVerificationContext is the inter-rule communication mechanism. It works, but it's stringly-typed on the key side. The mitigation is using const string bag keys defined on the rule that produces the value:

public const string BagKey = "SignatureVerificationRule.SignerCertificate";

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.


What I Learned

Own your security-critical abstractions. 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.

TimeProvider is worth using everywhere time touches logic. Once you've written a validation test that didn't require a Thread.Sleep, you won't go back.

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

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

Sealed records for descriptors and options. Using sealed record with required init-only properties for things like JwtDescriptor, SamlSpOptions, and JwtIssuerOptions 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.


Future Improvements

A few things I'd like to add that weren't blocking the production use case:

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

SAML metadata parsing. Currently you configure IdP endpoints manually in SamlSpOptions. Parsing the standard EntityDescriptor XML from an IdP's metadata URL would reduce the manual configuration surface.

JWK (JSON Web Key) support. 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 /.well-known/jwks.json endpoint.

NuGet package. 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.


Conclusion

AutoStack.Identity isn't trying to replace Microsoft.Identity.Web 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.

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.

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.

Source: github.com/mishrababhishek/AutoStack