We Built Our Own OIDC Library. Why?
Fair enough question. It definitely deserves a good answer.
I read the news in late 2020: IdentityServer4 was going commercial. The new product was called Duende IdentityServer, a paid, licensed product. The last free version of IS4 kept receiving security patches for some time and then stopped.
I had spent years as a lead developer at a large IT-security company, building the authentication service on top of IS4. I'd been in this ecosystem long enough to remember when IdentityServer still shipped with a tightly integrated UI, before IS4 separated it into a standalone scaffolded project. And I had a list. A list of things the team quietly accepted because the library was free. Things I'd been telling colleagues about for years. Things I'd filed away under "we'd do this differently if we could".
Now the library became commercial. And the list was still on the table.
Some teams paid. Some migrated. I had my own vision. Eventually, I left to build what I'd always wanted.
I'm Kirill Kovalev, co-founder and CTO of Abblix. This is the story of Abblix OIDC Server: how it was started, what drove it, and where things stand today.
TL;DR
After years working on enterprise-scale identity built on IdentityServer4, I had a long list of things I wished were different.
When IS4 went commercial, Duende IdentityServer became the direct successor: the same codebase and architecture continued commercially. Later I started Abblix OIDC Server to go the other way: the approach rethought, the architecture redesigned, and the code written from scratch. Same origin, different answers to the same list.
The result is a .NET OAuth 2.0 / OIDC library: hexagonal architecture with the protocol core independent of ASP.NET MVC, small composable interfaces via Composite/Decorator patterns, Result-pattern error handling, and a JWT layer built on System.Text.Json.Nodes with no string-valued Claim.Value. Everything is assembled through standard .NET dependency injection (keyed services included), and wherever the platform already provides a mechanism, the library uses it rather than inventing its own. Verified by 600+ OpenID Foundation conformance tests with zero failures across all profiles.
I believe the .NET community wins when the ecosystem has multiple mature products competing on bright ideas. Abblix OIDC Server is my contribution: a different take on the same problem.
What Bothered Me
Every developer who works with a library long enough builds a mental list. Things that are wrong but tolerable. Things you work around. Things you explain to every new team member with "yes, I know, that's just how it works".
With IS4, my list had several things that never went away.
The Black Box Middleware
When you call app.UseIdentityServer(), IS4 puts its middleware early in the pipeline, before MVC. Inside that middleware lives a custom router that does simple path matching against a table of registered OIDC endpoints: /connect/authorize, /connect/token, .well-known/openid-configuration. If a request path matches, the middleware resolves a handler from DI, runs it, writes the response, and returns. The rest of the pipeline never fires. ASP.NET Core's routing system never sees the request. MVC never sees it. And you cannot easily change those paths either: they are baked into the middleware's internal table, not exposed as MVC route templates you can customize.
This was the first wall we hit. Our architect came to me with a simple suggestion: let's align those OIDC paths with the rest of our API schema. We started investigating whether we could. Not easily, it turned out. We had to dig into IS4's internal routing, find a way to intervene in its configuration, and re-register the endpoints under different paths.
The architectural choice underneath: the OIDC protocol core is tied to raw middleware primitives, HttpContext, and never plugs into MVC's controller or any other layer. It runs in a hidden pipeline, before MVC and before standard routing. The result is two parallel worlds of request handling inside the same process, sharing the same DI container, but with completely different handling models. To understand what happens during an authorization request, you read IS4's source. To change it, you figure out IS4's DI-based extension points, a separate vocabulary entirely.
At the authentication service, customizing endpoint behavior meant working with IS4's service collection directly. Before writing a single line of custom code, we had to dig through IS4's source to understand how its internal routing system worked and where there was even a seam to inject into. Then we wrote ~50 lines of infrastructure: iterating over the DI service collection at runtime, finding Endpoint metadata objects by name, instantiating generic decorator types via reflection, removing original service descriptors, registering new ones. The actual decorator registrations were three lines. The machinery to make those three lines work was the other 47.
The middleware-only approach has a real strength: it runs alongside any HTTP framework the rest of your app uses, whether MVC, Minimal APIs, or anything else. The trade-off is customization. Reaching into that pipeline to adjust its behavior means descending into the middleware's internal abstractions, not using the tools each framework already gives you.
Abblix OIDC Server makes the opposite architectural choice. It is designed to integrate into your service as an organic part (not a separate component plugged in from the side), and to let you swap, extend, or remove any part with minimal code, ideally through standard DI registration.
The core Abblix.Oidc.Server contains the OIDC state machine: what requests are valid, what responses to produce, what invariants to enforce. Abblix.Oidc.Server.Mvc is a thin adapter that plugs that core into standard MVC controllers, model binding, and action results. This follows the hexagonal (ports-and-adapters) architecture pattern. Protocol logic sits in the center; transport bindings, storage bindings, and cryptographic bindings plug in as adapters around it. The architecture is deliberate and documented in the Understanding the Architecture guide.
Through the MVC adapter, every OIDC endpoint becomes a standard MVC controller. Everything lives in the same routing table, the same filter pipeline, the same call stacks. Even CORS and CSP stay on their standard ASP.NET Core wiring. No parallel routing world. No reflection over service descriptors. When you put a breakpoint in an OIDC endpoint, you are looking at a normal MVC controller action. All of this is familiar to any .NET developer.
That familiarity is a design principle, not a side effect of choosing MVC: wherever .NET or ASP.NET Core already provides a mechanism (routing, model binding, DI, options, logging, distributed caching), the library builds on that mechanism instead of shipping a private replacement.
Since the endpoints are standard MVC controllers, their paths are configurable at runtime via tokenized route templates - no recompilation needed when deploying across dev, staging, and production environments. See Dynamic Routes for details.
Abblix ships an adapter for ASP.NET MVC out of the box. But the hexagonal architecture makes it straightforward to create others. That is not a theoretical promise: the upcoming release adds a second adapter, for Minimal API, built without touching the core itself.
The Override Tax
IS4 had a pattern of large, monolithic service classes, hundreds of lines, that handled multiple responsibilities and made configuration-level decisions deep inside method bodies.
This matters when you need to change something small.
Say you want to modify how the token endpoint validates a specific parameter, or how the authorization endpoint handles a particular edge case. In an ideal library, you'd implement a small, focused interface and register it. In IS4 the relevant logic often lives inside a large default implementation. To change it, you copy the entire class, hundreds of lines, into your codebase, adjust the ten lines you actually care about - and maintain your fork of that class through every library update.
I call this the override tax. Every customization required inheriting (or copying) more code than you were actually changing. A new team member asks "why do we have a class that runs into hundreds of lines in our project that looks like it belongs in the library?" Answer: "we needed to change one condition in the middle of it".
This also makes future upgrades painful. The library releases a new version. Does your override still work with it? Does the library provide an updated version of the class you forked? If yes, how do you merge it with your customization? You go line by line through the diff, figure out which parts of the upstream class changed, and manually reconcile.
Here's an example from the service I was developing while working with IS4. We needed to change the HTML generated by the front-channel logout page: the iframe-based page that notifies all registered clients to sign out. Actually, we needed custom JavaScript barrier synchronization to coordinate the iframe callbacks, and different URL construction for legacy WS-Federation clients alongside OpenID Connect ones (weird, I know, but enterprise makes strange bedfellows sometimes).
The class responsible for that logout page was marked internal, inaccessible outside the library. No subclassing, no partial override. The only path was a full reimplementation: a decorator of over two hundred lines, replicating the endpoint's entire request-processing pipeline just to reach the few dozen lines of HTML generation at the bottom. That's the most insidious consequence of internal on infrastructure code: the change itself was trivial. The path to making it was not.
Another real-world example: we needed to handle post_logout_redirect_uri validation when the request doesn't include an id_token_hint. The validator for that endpoint had no virtual hook for "client not found". The decorator we ended up writing required around sixty lines of code, of which roughly fifteen were new logic; the rest was infrastructure required to intercept the validator at all.
In IS4, the largest files are exactly where the business logic lives: validators, managers, response generators. IS4 earned those validators the hard way, accumulating the OIDC complexity in a small number of critical files. The question is whether that complexity is extractable into smaller pieces. The token-request validator handling all grant types was already 800+ lines. Most files are modest on both sides; the difference shows up in the classes that matter most.
Abblix OIDC Server takes the opposite approach from the very beginning. The validators are decomposed into small, focused classes, each responsible for a single concern. The surface area you need to implement to change a specific behavior is sized to that behavior, not to whatever class happened to contain it. Adding a new grant type or changing one validation rule means implementing one focused interface to plug the implementation into the pipeline - not copying a thousand-line class to change ten lines inside it.
The glue holding those small classes together is standard .NET dependency injection, used at full depth. Composite implementations gather independently registered handlers behind one interface: client authenticators, grant handlers, logout notifiers. Decorators wrap existing services to layer on cross-cutting behavior such as consent prompts, token status checks, or a guard against authorization-code reuse. And where the protocol itself names the variants, keyed services turn the protocol identifier into the DI key: JWT signers are registered under their JOSE algorithm names, CIBA delivery handlers under ping, poll, and push. Extending any of these means adding one registration; the dispatch lives in the container, not in a switch statement you have to fork.
Only two Abblix files exceed 500 lines. Both are DI registrations, not business logic.
Claim Values as Strings
This one is more subtle, but it often produced bugs in practice. To be fair, it isn't strictly an IS4 problem; it's an inherited one. The JWT layer underneath, Microsoft's own Microsoft.IdentityModel.Tokens, is built around the SecurityToken abstraction and relies on the BCL Claim type, whose Value is always a string - name-value pairs with no native JSON types. IS4 integrated deeply with that stack and carried the limitation forward into every token it issued.
That string-based model was reasonable years ago, when XML was dominant (SAML tokens, for example). The problem is that modern OAuth 2.0 and OIDC often require structured claims in tokens.
Consider the aud (audience) claim. In a simple case it's a single string. In multi-resource scenarios it's an array. When you get the audience from a ClaimsIdentity, you iterate claims named aud. If there's one, you have a string. If there are multiple, you have multiple claims. The JWT representation of an array and multiple claims with the same name are not the same thing in all contexts. The amr (Authentication Methods References) claim has the same shape. It records how a user authenticated, ["pwd", "otp"] for password plus OTP, ["mfa", "hwk"] for a hardware key. A JSON array of strings, with standard values from RFC 8176. Every OIDC provider that supports MFA returns it.
But the problem goes even further. Consider the address claim from OIDC Core: a JSON object with street_address, locality, postal_code, country. The cnf (confirmation) claim from RFC 7800 carries a public key for proof-of-possession, the mechanism behind mTLS (RFC 8705) and DPoP (RFC 9449). The authorization_details claim from RFC 9396 is an array of JSON objects encoding fine-grained permissions. And there are plenty of other structured claims, including custom ones: at the authentication service we had several in exactly the same shape, and each one was a recurring pain point. Each of these, in the string-based model, has to be serialized to a string, embedded in the claim, and deserialized on the other side.
This works until it doesn't: whitespace differences, key ordering, numeric type ambiguity, arrays with a single element being serialized differently by different JSON serializers. Microsoft.IdentityModel.Tokens has switched its internal JSON serializer more than once across versions, and each switch quietly changed how claim values were serialized and deserialized. Each switch added another layer of fragility on top of the string model.
I tracked down bugs of this kind many times. The root cause was always the same: something that was a proper JSON type somewhere in the pipeline got squeezed through the string bottleneck and came out different.
Abblix.JWT is built directly on System.Text.Json.Nodes. A claim value is a JsonNode and can carry any JSON type: string, number, boolean, array, or object. No serialization through strings and back. The JWT payload is composed from JSON types end to end. When you put a JSON object into a token payload, it comes out as a JSON object on the other side. This sounds like a baseline expectation. With the string-based model, it isn't.
Success and Failure, One Type
IS4 handles validation failures through result types, but those types are weak. A single ValidationResult carries both success and failure semantics at once, distinguished only by an IsError boolean plus Error / ErrorDescription string properties. The distinction between success and failure lives in the IsError flag checked at runtime, not in the type signature. Naming convention alone tells which properties belong to which outcome: success data and error data live side by side on the same object. Remember to check IsError before reading the success fields, and things work. Forget, and you read garbage. Nothing in the type system forces the compiler to check that.
This works in practice. It also means that the "happy path" in your code is the path where nothing has been validated yet: failures are invisible in the type system, and the compiler does nothing to stop you from forgetting to handle one. The point here is not that IS4's pattern was broken. It shipped and worked. The problem is that in a protocol library, where OAuth error responses are a first-class part of the specification, a result type that doesn't enforce anything pushes correctness work onto every caller.
Abblix embraces the Result pattern end to end. Every operation that can fail returns a Result<TSuccess, TFailure>: success with a value, or failure with a structured error. Two type parameters, not one: success and failure are different shapes in the signature, not two faces of the same object. The compiler enforces that callers handle both cases. Validation failures are typed values, not boolean flags plus loose strings. The control flow is explicit: bind, map, match. And the Result pattern makes that enforcement the compiler's job, not yours.
Why We Didn't Choose OpenIddict
OpenIddict is the obvious free alternative. Apache 2.0, solid community, impressive adoption numbers. I genuinely appreciate the work Kevin Chalet has put into it over the years. I looked at it seriously alongside IS4, weighing one against the other.
The tiebreaker was certification. Back when my team had to choose a library for the authentication service at the company where I worked at the time, we picked IS4 over OpenIddict for exactly this reason: IS4 was certified by the OpenID Foundation, OpenIddict was not. That wasn't my personal call. For a large company specializing in IT security, compliance officers made the final decision. "It works in our integration tests" is not the same as "it has passed the OpenID Foundation conformance suite".
That lesson stayed with me. Certification became one of the most important factors when choosing an identity library, especially when targeting large enterprise or IT-security companies. When I started building Abblix OIDC Server, passing the conformance suite was a day-one commitment.
What Was Built
The protocol surface covers what production identity providers need. Authorization Code, Implicit, Hybrid, Client Credentials, Device Authorization (RFC 8628). PKCE (RFC 7636), Pushed Authorization Requests (RFC 9126), JWT-Secured Authorization Requests (RFC 9101), CIBA, Mutual-TLS (RFC 8705), Resource Indicators (RFC 8707), Dynamic Client Registration (RFC 7591/7592), Token Introspection (RFC 7662), Token Revocation (RFC 7009), all logout profiles including Front-Channel and Back-Channel.
The JWT layer, Abblix.JWT (NuGet), is a complete implementation of RFC 7515 (JWS), RFC 7516 (JWE), and RFC 7519 (JWT) based on System.Text.Json.Nodes using native .NET cryptography. There is no Microsoft.IdentityModel.Tokens under the hood. This also unlocked the broader JWE algorithms: RSA-OAEP-256, AES-GCM key wrapping (A128GCMKW through A256GCMKW), direct key agreement.
OpenID Certification
The OpenID Foundation conformance test suite runs hundreds of semi-automated tests against every behavioral requirement in the OIDC specifications. Libraries and servers can apply for official certification across multiple profiles.
Abblix passed the full suite: Basic, Implicit, Hybrid, Config, Dynamic, Form Post, Third Party-Initiated Login, and all four logout variants: RP-Initiated Logout, Session Management, Front-Channel Logout, and Back-Channel Logout. The motivation wasn't marketing; it was proving it to myself first. When you implement a complex protocol from first principles, the conformance suite is the authoritative answer to "is this correct?" The internal test suite tells me it works. The official OpenID Foundation conformance suite proves it works according to the specification.
600+ tests. Zero failures. Zero warnings. All profiles.
Where Things Stand
The library is in production. The authentication service I built for certification purposes is now public as Abblix Account, and it is powered by the latest Abblix OIDC Server. The codebase is at github.com/Abblix/Oidc.Server, and I'm always happy to answer questions in GitHub Discussions.
Are you trying to compete with Duende Software?
I believe competition is the normal state of software. An ecosystem with a single mature vendor stagnates; one more independent player forces everyone to keep improving. Postgres didn't fade when MySQL shipped. Nginx didn't displace Apache. Vue didn't close the door on React. Every healthy ecosystem has several serious players with different visions - and that's exactly the condition that keeps each of them sharp. That's the competition worth having: not market share, but bright ideas.
So, to the question itself: is Abblix trying to compete with Duende? In the sense that we offer the .NET identity ecosystem one more option, and the ecosystem benefits from it: yes. In the sense of trying to displace a working product people rely on, no. Both libraries share the same IS4 origin, and that's the honest framing.
Duende IdentityServer is the direct successor of IS4: the codebase continued commercially, the same architecture preserved for compatibility with existing integrations. For teams that want that continuity and the maintainer's support model, it's the natural fit.
Abblix took the other path: rethink the approach and rewrite the code from scratch. Same origin, different answers. That's a philosophy difference. If the pain points I describe above don't bother you in IS4, that's fine. If they do, Abblix is worth a serious look.
That's our mission as we see it: to develop an alternative worth having, contribute to a .NET ecosystem that becomes richer with every new player, and let teams pick what fits their engineering values. If some pick Abblix, great. If some choose another, that's also a win for the ecosystem we're all trying to improve.
Why I'd Reach for Abblix
The ideas behind Abblix reflect my vision of software design. Hexagonal separation of the protocol core from the ASP.NET MVC binding, making the core transport-agnostic. Small focused interfaces with Composite/Decorator extensibility, so customization size is matched to the thing being customized. An extension model that is plain .NET dependency injection, keyed services and all: no plugin system of its own to learn, and every tool and technique your team has accumulated around .NET DI applies as-is. Result-pattern error handling, so validation failures are typed values the compiler enforces. A JWT layer that treats JSON as a strongly typed model end to end. Runtime-configurable endpoint paths via route templates, so the same binary deploys across dev, staging, and production with environment-specific paths. Certification across all conformance profiles with zero warnings as a standing answer to "is this correct to spec?"
None of those individually is a reason to switch away from a working integration. But each one is a good reason to give Abblix OIDC Server a look. All together, on a greenfield project, they make Abblix my own choice.
Abblix doesn't plan to stop at what's already built. The upcoming release adds the Minimal API adapter, with new RFCs and an expanding protocol surface to follow. Worth keeping an eye on.
The list I kept is now a product. The choice is yours.
Abblix OIDC Server on NuGet · GitHub · Certification: Regular Profiles · Logout Profiles