How to Validate JWT Tokens in ASP.NET Core in Offline and Air-Gapped APIs

Maarten Balliauw Head of Customer Success

In a normal ASP.NET Core API, the AddJwtBearer extension method points the Authority property at your Duende IdentityServer instance. On startup, the handler fetches the discovery document at /.well-known/openid-configuration and the JSON Web Key Set (JWKS), caches the validation keys in memory, and from then on validates every incoming token against them. You never think about offline authentication because connectivity and the keys are simply there.

Then you deploy that same API within a network where the connection to your identity provider is unreliable, or not there at all: a factory floor when the WAN drops, a cargo vessel at sea, an underground mine, an aircraft in flight, an offshore platform, a remote solar or pipeline site where a technician shows up with a laptop every few weeks. The tablets and services on site still hold valid JWTs. Then the API restarts during an outage, tries to fetch the discovery document, fails, and starts returning 401 Unauthorized for every single request. The tokens are fine, but the API just has no keys to verify them with.

Because a JWT is signed and self-contained, your API can validate it locally without calling Duende IdentityServer every 5 minutes, as long as the local host already has the public signing keys. How do you retrieve and store the keys and stay up to date?

By the end of this post, you will have three ways to handle offline token validation in ASP.NET Core. From a wider JWKS cache, to pinned signing keys, to an independent edge IdentityServer. Plus a way to rotate signing keys without locking out disconnected sites.

Is Disconnected Really an Edge Case?

For a factory floor, it is tempting to say yes, it is an edge case: the WAN drops for an hour, someone restarts a router, and life goes on. But plenty of factories are not really "connected" in the way we like to assume. Many plants have a factory floor with a 1 Mbps symmetric line, and quality of service deprioritizes everything that isn't production traffic, so nothing disturbs the line. Other plants simply have a WAN connection to the head office that drops regularly. Either way, calling home to a remote identity provider is not something you want in the critical path.

In many other places, being cut off completely is the norm. An underground mine reaches the surface over a cable that a maintenance crew can cut. An offshore platform has a slow, shared satellite link, so updated signing keys may arrive with the crew change instead of over the wire. An aircraft has no issuer to reach for hours and then a fast link at the gate, so the disconnection is part of the flight plan. A cargo vessel spends days at sea. A remote pipeline or solar site gets a visit every few weeks from a technician with a laptop. A tactical field unit may not have a link at all.

In all of these cases, an unreachable identity provider isn't an edge case. It is a design assumption. You pick the token validation model around two questions: how long does the site go without a link, and do new users need to sign in while it is down?

Can an API Validate JWTs Without Contacting the Issuer?

An API can validate a JWT without contacting the issuer, provided a few conditions are met. Because a JWT is signed and self-contained, the API only needs the issuer's public key, the expected issuer value, the audience it serves, the allowed signing algorithm, and a clock it can trust. All of that can live locally on the server running the API, allowing you to validate the JWT token manually when offline.

Reference tokens are opaque handles that the API validates by calling IdentityServer's introspection endpoint, so an API that must work offline needs JWT access tokens instead.

In practice, validation means checking the signature, the exact iss, the aud, the token type, the lifetime, and the algorithm, and only then authorizing. The difference between the approaches below comes down to how your API gets those public keys, and whether the site also needs to issue new tokens while it is cut off. That gives you four designs, moving from a normal connected setup to a fully independent edge issuer. Each step along that spectrum trades operational work for more independence when the link to the authorization server is down, and changes how revocation reaches the site:

Design Keeps validating when the link is down Revocation reach

Connected

No

Central, while the link is up

Bounded JWKS cache

Short outages, if the API did not restart

Central, while the link is up

Pinned keys

As long as the pinned keys are valid

Local deny list shipped with the keys

Edge issuer

Existing tokens and new edge sign-ins

Local, for tokens the edge issued

Bounded JWKS Caching: Riding Out Short Outages

The default ASP.NET Core JWT bearer handler fetches the discovery document and the JWKS from the authority and caches them. Widening that cache on purpose lets a short WAN outage pass without breaking validation. It suits a factory floor where the link is slow, deprioritized by quality of service, or drops for a few hours at a time, and the plant should ride it out.

Look at AutomaticRefreshInterval in the next snippet: it is set to 24 hours, so the handler keeps using the keys it already has for a day before it tries to refresh them. RefreshInterval controls how soon the handler may re-fetch after it meets an unknown kid. Alongside those two settings, we configure an audience allow-list, an algorithm allow-list, MapInboundClaims = false, and a one-minute ClockSkew.

Csharp

// ASP.NET Core token validation with bounded JWKS caching
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
    {
        options.Authority = "https://identity.corp.example.com";
        options.MapInboundClaims = false;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateAudience = true,
            ValidAudiences = new[] { "mes-api", "scada-api" },
            ValidAlgorithms = new[] { SecurityAlgorithms.RsaSha256 },
            ClockSkew = TimeSpan.FromMinutes(1)
        };

        // Cache JWKS for a bounded interval: tolerates WAN outages
        // without treating the deployment as offline-capable.
        options.ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
            $"{options.Authority}/.well-known/openid-configuration",
            new OpenIdConnectConfigurationRetriever(),
            new HttpDocumentRetriever())
        {
            AutomaticRefreshInterval = TimeSpan.FromHours(24),
            RefreshInterval = TimeSpan.FromMinutes(30)
        };
    });

The cache isn't persisted, so if the API restarts while the link is down, it starts empty and you will see issues when accessing the API, even with a valid token.

When you cache JWKS, always pick a lifetime that is at least as long as the outages you want to ride out, although we recommend refreshing the cache at least every 24 hours.

Pinning Signing Keys Locally: A Validation-Only Edge

If the site can be disconnected for long stretches, stop asking the issuer for keys at request time. Instead, ship the issuer's public signing keys to the site as part of your signed configuration or deployment package, and configure the API to validate only against those local files. This validation-only edge has the fewest moving parts and no second identity provider to run, so it is a good place to start. A vessel gets the current keys before leaving port. An aircraft picks up updated keys between gates. A remote pipeline station gets them on the next maintenance visit.

In the next snippet, the active key and the retiring key come from files on disk, so tokens signed just before a rotation still validate. The validation parameters name the exact issuer, audience, key IDs, and algorithm.

Csharp

var activeRsa = RSA.Create();
activeRsa.ImportFromPem(File.ReadAllText("/etc/edge-identity/central-2026-09.pem"));
var activeKey = new RsaSecurityKey(activeRsa) { KeyId = "central-2026-09" };

var retiringRsa = RSA.Create();
retiringRsa.ImportFromPem(File.ReadAllText("/etc/edge-identity/central-2026-08.pem"));
var retiringKey = new RsaSecurityKey(retiringRsa) { KeyId = "central-2026-08" };

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.MapInboundClaims = false;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = "https://identity.example-transport.com",
            ValidateAudience = true,
            ValidAudience = "onboard-maintenance-api",
            ValidateIssuerSigningKey = true,
            IssuerSigningKeys = new[] { activeKey, retiringKey },
            ValidateLifetime = true,
            ClockSkew = TimeSpan.FromMinutes(1),
            ValidAlgorithms = new[] { SecurityAlgorithms.RsaSha256 }
        };
    });

During key rotation, you hold more than one key at a time, which is why the snippet passes IssuerSigningKeys. If you manage a larger set of keys, an IssuerSigningKeyResolver scoped to this one issuer works too. Either way, reject any kid you do not know. Never take a key from the token itself, whether from a URL it supplies (jku or x5u) or from an embedded key (jwk or x5c), because a token that picks its own verification key will happily validate itself.

Running an Independent IdentityServer at the Edge

A cached JWKS and locally pinned keys only handle tokens that were issued before the link dropped. That is fine for a vessel whose crew signed in before departure. It is not fine for an underground mine or an offshore rig, where new crew or contractors arrive and need to sign in while the site is disconnected for days. Somebody has to issue tokens on site, so the site needs an issuer of its own.

Large energy utilities often organize this in tiers, with central, regional, and site issuers that each trust only the keys they were configured to trust. When connectivity returns, reconciling edge activity with the central system is a service you build for your domain, not a built-in Duende feature.

Should I Run an IdentityServer at the Edge?

You only need a server at the edge when new sign-ins have to happen while the original network link is down. A second IdentityServer is a second system to patch and operate. When you run one, keep it separate: its own issuer URI, its own keys and stores, and narrow client scopes and audiences. Do not share signing keys or the grant store with the central server, and do not replicate IdentityServer tables between central and edge. Pointing the same issuer URL at two databases that can both write while disconnected will cause conflicting grants and key state.

Let's assume you have a central IdentityServer and one that runs at your edge location. An API that trusts both issuers needs one pinned scheme per issuer, each with its own keys. In ASP.NET Core, you register a named AddJwtBearer scheme per issuer and use AddPolicyScheme with ForwardDefaultSelector to route each incoming request to the matching scheme based on the unvalidated iss claim, while rejecting unknown issuers.

Whether the edge IdentityServer uses automatic key rotation or static keys depends on who validates its tokens. If only local APIs on the same site validate edge-issued tokens, and they can reach the edge IdentityServer's JWKS endpoint over the local network, automatic key management works fine. If central or shore-side APIs also need to validate tokens minted at the edge, or if keys are provisioned from a central PKI or hardware token during maintenance visits, turn off automatic rotation (KeyManagement.Enabled = false) and load static keys with AddSigningCredential and AddValidationKey. Otherwise, the edge server will rotate keys on a wall-clock timer while cut off and start signing tokens with a key that central has never seen. The next snippet takes the static route.

Csharp

// Illustrative: LoadEdgeSigningCertificate and LoadPreviousEdgeSigningCertificate are
// application code, not Duende APIs. They read the certificates and their passwords from a
// secret-store-backed configuration provider, not from plain environment variables.
X509Certificate2 edgeSigningCertificate = LoadEdgeSigningCertificate();
X509Certificate2 previousEdgeSigningCertificate = LoadPreviousEdgeSigningCertificate();

builder.Services.AddIdentityServer(options =>
    {
        options.IssuerUri = "https://identity.edge.port-17.example-transport.com";
        // Static signing credential below; disable automatic key management.
        options.KeyManagement.Enabled = false;
    })
    .AddEntityFrameworkConfigurationStore(options =>
    {
        options.ConfigureDbContext = db =>
            db.UseSqlServer(builder.Configuration.GetConnectionString("EdgeConfiguration"));
    })
    .AddEntityFrameworkOperationalStore(options =>
    {
        options.ConfigureDbContext = db =>
            db.UseSqlServer(builder.Configuration.GetConnectionString("EdgeOperational"));
        options.EnableTokenCleanup = true;
    })
    .AddSigningCredential(edgeSigningCertificate)
    .AddValidationKey(previousEdgeSigningCertificate);

Keep certificate passwords out of plain environment variables, which anything running as the same user can read.

Issuer-Side Key Management for Disconnected Consumers

Everything so far has been on the API side. The other half sits on the IdentityServer side: your server must publish, rotate, and retain signing keys on a schedule that gives disconnected APIs time to receive a new public key before any token signed with it shows up.

Automatic key management rotates keys on a timer: PropagationTime announces a new key in discovery before it signs anything, and RetentionDuration keeps retired public keys published afterward. That timer does not know whether a vessel at sea or a pipeline station has actually downloaded the new key yet! You have two ways to handle that: either stretch those intervals so the announcement and retention windows are wider than your longest outage or maintenance cycle, or use static key management (AddSigningCredential and AddValidationKey) so your deployment pipeline controls exactly when a key is announced, activated, and retired.

How Do I Rotate Signing Keys in a Disconnected Environment?

When you rotate signing keys for disconnected sites, you have to announce and distribute the new public key (B) to every edge site before IdentityServer starts signing tokens with it, and keep the old public key (A) around after the switch for at least the maximum token lifetime plus clock skew plus your distribution delay:

sequenceDiagram
    participant Issuer
    participant Distribution
    participant Edge as Disconnected Edge
    Issuer->>Distribution: Announce key B
    Distribution->>Edge: Deliver keys A and B
    Note over Edge: Link drops
    Issuer->>Issuer: Activate key B
    Note over Edge: Validates tokens signed with A or B
    Note over Issuer,Edge: Retain A for max token lifetime plus skew plus distribution delay
    Issuer->>Distribution: Remove key A
    Distribution->>Edge: Deliver key B only

Now consider what goes wrong when you skip the confirmation step. If the central server activates key B on a schedule before an offline site has synced since last week, that site's pinned keys only contain key A. Every newly minted token arrives with a kid the edge API does not recognize and gets rejected, while older tokens signed with A keep working, which makes the first hour of debugging confusing:

sequenceDiagram
    participant Client
    participant Issuer
    participant Distribution
    participant Edge as Disconnected Edge
    Issuer->>Distribution: Announce key B
    Note over Edge: Key B never arrives at edge
    Issuer->>Issuer: Activate key B too early
    Client->>Edge: Present token signed with B
    Edge->>Edge: Unknown kid, reject

The docs describe the working sequence as phased rotation.

If a site is only reachable during scheduled maintenance windows, like a remote pipeline station, you can configure automatic key management to match that cadence. In the next snippet, keys live for a year and are retained for two. Longer lifetimes mean a compromised key stays useful for longer, so weigh that against the maintenance schedule.

Csharp

// Air-gapped IdentityServer, key management via options.KeyManagement
builder.Services.AddIdentityServer(options =>
{
    // Nodes share keys through the common operational store; Data Protection
    // keys must also be shared so every node can read the protected keys.
    options.KeyManagement.Enabled = true;

    // Extended lifetimes for air-gapped environments: keys must last
    // until the next scheduled maintenance window.
    options.KeyManagement.RotationInterval = TimeSpan.FromDays(365);
    options.KeyManagement.RetentionDuration = TimeSpan.FromDays(730);
    options.KeyManagement.PropagationTime = TimeSpan.FromDays(1);
});

Don't Forget ASP.NET Core Data Protection Keys

The comment about shared Data Protection keys in the snippet above is easy to skim past, but it matters. When automatic key management writes signing keys to the database (or ~/keys), it encrypts the private key material at rest using ASP.NET Core Data Protection. Teams often back up or provision the IdentityServer database for an air-gapped site and forget the Data Protection key ring. If you restore the database (or spin up a second node) without the matching Data Protection keys, IdentityServer cannot decrypt its own signing keys on startup. Persist and back up both together.

To ensure keys are safely backed up before being encrypted, you can implement IKeyEscrowSink to export Data Protection keys upon creation. Keep in mind that Data Protection keys themselves can also be encrypted at rest using an X.509 certificate. If you use one, ensure the certificate and its private key are backed up along with the key ring.

What You Can't Do Offline: Revoke a Token

Can you revoke a token for an offline API? No design gives you immediate central revocation over a failed link. An already-issued JWT access token stays valid until it expires, and the revocation endpoint is unreachable anyway. You can shrink the damage with short access tokens and with deny entries, either shipped to the site or added locally at the edge. Refresh-token revocation only takes effect once the issuer is reachable, so require fresh authentication after reconnecting for privileged work.

Avoid setting the access token lifetime to match a multi-day outage. A token that stays valid for days gives anyone who intercepts it days of access that you cannot easily revoke.

FAQ

How Do I Validate a JWT Token in ASP.NET Core Web API?

Configure JWT bearer authentication using AddJwtBearer in your service configuration. Provide the authority or explicitly set TokenValidationParameters (including issuer, audience, and signing keys) to ensure incoming requests contain a valid, trusted token.

What Is a JWKS Endpoint?

A JSON Web Key Set (JWKS) is a JSON object containing a set of public keys used to verify signatures on JSON Web Tokens (JWTs) issued by an authorization server. The JWKS endpoint is an HTTP endpoint published by the server (typically linked from /.well-known/openid-configuration) that allows APIs to dynamically fetch and cache those public keys for offline signature validation.

Can an API Validate JWTs Without Contacting the Issuer?

An API can validate JWTs without contacting the issuer if it holds the issuer's public key and the other validation inputs locally. Use JWT access tokens, because reference tokens need introspection.

How Do I Rotate Signing Keys in a Disconnected Environment?

To rotate signing keys in a disconnected environment, announce the new key, confirm every edge has it, then activate it. Keep the old public key for the maximum token lifetime plus clock skew plus distribution delay.

How Long Should I Cache a JWKS?

Set a JWKS cache lifetime on purpose, bounded but at least as long as the outages you want to survive. A day is a common starting point. A cache is resilience, not an air-gapped design.

Can I Revoke a Token for an Offline API?

A token for an offline API cannot be revoked immediately, so mitigate with short access tokens and deny entries, signed or local. Refresh-token revocation takes effect once the issuer is reachable again.

Should I Run an Identity Server at the Edge?

You should run an identity server at the edge only if new sign-ins must happen during a long disconnection. A validation-only edge with pinned keys is the lower-risk default.

Wrapping Up

Validating JWTs when the network goes away comes down to two questions: how long is the site cut off, and do new users need to sign in while it is down? Most sites may only need a bounded JWKS cache or locally pinned signing keys. An independent edge IdentityServer is there for when on-site sign-ins during multi-day outages are a hard requirement.

If you are designing an offline or air-gapped architecture and want an experienced set of eyes on your key rotation, edge topology, or rollout plan, consider working with one of our Duende partners who specialize in architectural guidance and complex IdentityServer implementations. You can also reach out in the Duende community discussions.

About the author

Maarten Balliauw Head of Customer Success

Loves building web and cloud apps. He focuses on .NET web technologies, C#, Microsoft Azure, and application performance. Creator of SpeakerTravel and formerly founded MyGet.

Related Articles