Skip to main content
Dejan Demonjić brand logoDejan Demonjić
Decision log
Work
About
Contact

Decision log / Decision record

One identity server for the cloud and for every site

When the same product runs in the cloud and at customer sites, identity is part of the product, so one identity host on OpenIddict and ASP.NET Core Identity serves both. The decision record: the model, the values, the options rejected, and what it costs.

Filed
2026-10-16
Topic
Identity and security
Reading
8 min
Tags
#identity#openiddict#oauth#passkeys

Part 1 of 5 Identity and access

  1. One identity server for the cloud and for every site
  2. Decide from the endpoint, not from the path
  3. When the token store is down, the honest answer is 503
  4. A Content Security Policy for an identity server
  5. Mutual TLS and certificate-bound tokens for the links that matter
Next Decide from the endpoint, not from the path

The claim

If a product ships both to the cloud and to customer sites, identity belongs to the product. Deciding it once, before the first release, is cheaper than shipping several copies of it and replacing them in the field. For Hexavera the choice was one identity host, built on OpenIddict and ASP.NET Core Identity, running as the same binary in the cloud and as a service at each site.

Context

Hexavera is a workforce and access-control platform with a cloud and standalone sites. Before this decision it had three hosts, each with its own ASP.NET Core Identity store and its own token issuing, all signed with one shared symmetric secret. There were no account pages beyond a sign-in form, no external provider worked, and the products signed in to each other through hand-built token controllers.

Nothing had shipped. That is the fact the whole decision rests on: with no installed base to stay compatible with, the cheapest moment to get identity right was right then, and the most expensive moment would have been a year after the first customer went live.

Options

Option Why not
Keep the three stores and add a directory bind at the site Three stores, three role sets and one shared secret would ship to customers and be replaced afterwards
A hosted or licensed identity server A product that installs an issuer at every customer site makes per-deployment licence terms and a heavy runtime at each site a poor fit, and the hosted services have no on-premises form
One issuer per tenant A person who works in two tenants would need two sessions; products would stop sharing one sign-in; every resource server would trust a list of issuers learned at runtime
Roles and permissions inside the identity server Couples every product's permission catalogue to the identity server's release, puts stale permissions in tokens, and leaves a site unable to authorise while the server is unreachable
One account per tenant (the old model) Cannot express self-registration, a person in two tenants, or signing in without first asking which tenant
The site trusts the cloud and replicates credentials Offline sign-in from replicated password hashes is custom security code, and a standalone site does not need it
Old and new issuers side by side during a migration No release had shipped, so there was nothing to keep working

The decision

  1. One identity host, on OpenIddict and ASP.NET Core Identity. The same binary runs in the cloud and as a service at each site. Products hold no credential, session or provider table of their own.
  2. One global account per person, with membership in any number of tenants. An email address is unique within a deployment.
  3. Authorization stays in each product. A token says who is calling and in which tenant they act; it carries no role and no permission. The product's own role store decides the rest, so a site keeps authorising while the identity server is down, and a permission change never waits for a token to expire.
  4. One issuer per deployment, with the tenant as a claim. The cloud is one issuer. Each site is one issuer.
  5. A site's identity service is the authority for that site. Nothing is replicated from the cloud for sign-in. A site reaches cloud services by exchanging a site-issued token at the cloud's token endpoint, outbound, with the site's own certificate, and the cloud records each site issuer and its keys.
  6. No SAML in the first release. Every provider named so far speaks OpenID Connect.
  7. Identity first, and alone. The access-control delivery waits for it and resumes on top of it.
  8. No backwards compatibility. No existing token, route, table or account is kept alive. Each host is cut over in one step, its migrations rebaselined, its development databases reseeded.

The values, confirmed the same day:

Value Chosen
Access token lifetime 15 minutes, in the cloud and at a site
Signing keys ES256; rotated every 90 days; the next key published 7 days before it signs; a retired key published for 30 days
Revocation staleness at most 60 seconds on a connected product, never more than the access token lifetime
Refresh tokens rotated on every use; a spent one is accepted again for 10 seconds, so a retried request does not sign the person out, and reuse after that revokes the whole session; idle 14 days, absolute 30 days
Break-glass a one-time setup code on the identity host, valid 24 hours, reopened only when no active administrator can sign in, every use audited

The library is OpenIddict 7.7.1 (published 2026-09-17), on .NET 10 and ASP.NET Core Identity, which in .NET 10 gained passkey (WebAuthn) support of its own.

Two questions the standards settled

Two questions came up while building the account pages. Both were decided from the current primary texts, with the clause written into the record, a habit worth keeping: it ends the argument and it survives the next review.

Is a passkey a second factor, or a complete sign-in? NIST SP 800-63B-4 lists multi-factor cryptographic authentication among the authenticator types permitted at AAL2, and requires that "each authentication event SHALL require input and verification of the local activation factor" (section 3.1.7.1). Its Appendix B says that syncable authenticators "may be offered as a new option for applications that seek to implement up to AAL2", and that the verifier must check the WebAuthn User Verified flag: an assertion without it is treated as single-factor. So a passkey with user verification required is a complete sign-in on its own and satisfies a tenant's second-factor policy. That also matches how ASP.NET Core Identity treats a passkey: as a primary factor, not a second one.

Is a second factor required for everyone? OWASP ASVS 5.0.0 states that "L2 applications must force the use of multi-factor authentication", and requirement 6.3.3 asks, at level 2, for a multi-factor mechanism or a combination of single factors. So: a second factor for everyone, with no tenant setting that turns it off. A passkey is complete on its own; a password is followed by an authenticator app; a tenant policy may ask for more (passkeys only) and never for less.

The account lockout rule came from the NIST document. NIST sets an upper bound of 100 consecutive failed attempts with one authenticator before it is disabled (section 3.2.2) and allows lower limits. The account itself uses the bound, until a reset and with one notice to the person, and the everyday throttling sits on the source address and account pair, so that one address cannot keep a person out of their own account.

What it cost

  • Every product stops when the identity server's key and token endpoints do, apart from resource servers that keep validating on cached keys through the overlap window. The identity host gets its own availability objective, replicas and runbooks, which three embedded stores never had.
  • OpenIddict supports only its latest stable version, by its own support page, with two exceptions: sponsors receive extended support by tier, and ABP Framework users receive patches while their ABP version is supported. Its README names one maintainer. It is confined to the identity host and its store project, so a forced upgrade touches two projects and no product; the client library the products reference uses ASP.NET Core's own handlers and carries no OpenIddict dependency.
  • A site install grows to four services and two databases, and loses the shared token secret.
  • The access-control product waits for the identity phases up to the site cut-over. That trade was made knowingly: a delay before the first release, against three identity stores in the field.

What a team can take from this

Decide where authorization lives before you pick an identity server. Once roles ride in tokens, every product's release is coupled to the issuer's, and you will not get them out again. Then decide the issuer model from the shape of your customers, not from the library's defaults: one issuer per deployment with the tenant as a claim is simple to reason about and simple to operate, and a person who works in two tenants gets one session. And when a question is really a standards question, such as what counts as a factor or when to lock an account, read the current text and quote the clause in the decision record.

DD

Dejan Demonjić

Principal software engineer, Belgrade

Designs and builds secure, multi-tenant .NET platforms. Questions about this entry, or a different view on it, are welcome by email.

  • Email
  • LinkedIn
  • GitHub
Previous SignalR between a cloud and its sitesA hub that pushes changes from a cloud to on-premises sites is a security boundary and a lifecycle problem: who may connect, what each connection hears, how long an admission holds, and how a client comes back after the server closes it. The design note: a service-client policy, a group per tenant, a fifteen-second sweep with the same check requests get, and a client that starts itself again because automatic reconnect does not follow a server-side Abort.Next Panel passwords are sealed to the host that uses themA secret that one process uses should be readable by that process alone. The decision record for the administrator passwords of access-control panels: write-only entry, a fresh AES-GCM key per secret wrapped with RSA-OAEP under the connector host's key, opened in memory only, one escrow export and then a key nobody can export, a TPM tier that excludes escrow, and why machine-scope DPAPI and the web runtime's key ring were the wrong tools.

In this entry

  1. The claim
  2. Context
  3. Options
  4. The decision
  5. Two questions the standards settled
  6. What it cost
  7. What a team can take from this
Dejan Demonjić

Designing and building secure, multi-tenant .NET platforms, and writing down the decisions behind them.

Read

  • Decision log
  • Work
  • RSS feed

Contact

  • dejan@demonjic.com
  • Write to me
  • About

Elsewhere

© Dejan Demonjić. Written in Belgrade.