{"id":7939,"date":"2026-06-26T14:35:04","date_gmt":"2026-06-26T12:35:04","guid":{"rendered":"https:\/\/blog.redbaronofazure.com\/?p=7939"},"modified":"2026-06-26T14:45:37","modified_gmt":"2026-06-26T12:45:37","slug":"offline-api-authentication","status":"publish","type":"post","link":"https:\/\/blog.redbaronofazure.com\/?p=7939","title":{"rendered":"Offline API Authentication"},"content":{"rendered":"\n<p>If you have locally deployed REST APIs that authenticate with Entra ID <em>and<\/em> where the APIs need to be running 24&#215;7, you have a problem when your site looses internet connectivity. Sure, the access tokens are valid for some more minutes, but soon you need to refresh them or do token exchange in <a rel=\"noreferrer noopener\" href=\"https:\/\/learn.microsoft.com\/en-us\/entra\/identity-platform\/v2-oauth2-on-behalf-of-flow\" target=\"_blank\">OBO-flows<\/a> and that is when your system breaks down. It doesn&#8217;t happen often that Entra ID is unreachable, but that your site is disconnected from the internet is a real possibility. If your system needs to keep huming along offline, you need an architecture that isn&#8217;t 100% dependent on Entra ID.<\/p>\n\n\n\n<p>What kind of &#8220;site&#8221; are we talking about here? It can be a manufacturing site or a site supporting complex machines not connected to the public internet. Why would you involve Entra ID in such a case? Well, the management plane for you or your customers may use management APIs that are publicly available for a cloud based web frontend, but the sites operation may be local.<\/p>\n\n\n\n<p>In such a scenario you need an architecture where the publicly available management APIs use Entra ID for authentication, but the sites internal APIs uses something else.<\/p>\n\n\n\n<p>Github repo with sample code is available <a href=\"https:\/\/github.com\/cljung\/OfflineAPI\" target=\"_blank\" rel=\"noreferrer noopener\">here<\/a>.<\/p>\n\n\n\n<h2>Local site authentication architecture<\/h2>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" width=\"575\" height=\"397\" src=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/06\/Screenshot-2026-06-25-at-19.36.49-1.png\" alt=\"\" class=\"wp-image-7942\" srcset=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/06\/Screenshot-2026-06-25-at-19.36.49-1.png 575w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/06\/Screenshot-2026-06-25-at-19.36.49-1-300x207.png 300w\" sizes=\"(max-width: 575px) 100vw, 575px\" \/><\/figure>\n\n\n\n<p>The local site&#8217;s internal APIs should use some authentication in addition to Entra ID that isn&#8217;t dependant on internet connectivity. Two options are certificate based authentication or a local Identity Provider deployed on the site, only to be used within the site to authenticate. In this example Keycloak will play the role of the local IDP as it can easily be deployed in a docker container. <\/p>\n\n\n\n<p>In this architecture, you are required to come with an Entra ID access token to be authenticated by the Management API. When the Management API is talking to any of the Local Services (or Local Services are talking to the Management API &#8211; yes, that&#8217;s a possibility!) it needs to authenticate either with a certificate or with an access token from the local IDP (Keycloak). If internet connectivity is lost, no incoming calls will be received by the Management APIs, but the Local Services can still talk to each other in an authenticated way.<\/p>\n\n\n\n<h2>Wiring up the startup code in the APIs<\/h2>\n\n\n\n<p>If you have programmed authentication in aspnet REST APIs, you have had the pleasure of trying to get it right in Program.cs. Usually, its just standard code pointing to a section in appsettings.json. If you try adding Entra ID, Keycloak and certificate based authentication support, it becomes much more complicated.<\/p>\n\n\n\n<p>With a small extension, the authentication bootstrap code is a simple as below. Method AddCertificateBasedAuthentication does some initial setup and loads the trusted certificates while AddCertificateAuthenticationOptions handle the validation of a client supplied certificate in an API call. As Keycloak uses JWT tokens, adding support for it is doen via a simple AddJwtBearer call.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">builder.AddCertificateBasedAuthentication();\n\nbuilder.Services.AddAuthentication()\n    .AddJwtBearer(\"Entra\", options =&gt; {\n        options.Authority = builder.Configuration[\"Authentication:Entra:Authority\"]!.ToString();\n        options.Audience = builder.Configuration[\"Authentication:Entra:Audience\"]!.ToString();\n    })\n    .AddJwtBearer(\"Keycloak\", options =&gt; {\n        options.Authority = builder.Configuration[\"Authentication:Keycloak:Authority\"]!.ToString();\n        options.Audience = builder.Configuration[\"Authentication:Keycloak:Audience\"]!.ToString();\n        options.RequireHttpsMetadata = builder.Configuration.GetValue&lt;bool&gt;(\"Authentication:Keycloak:RequireHttpsMetadata\");\n    })\n    .AddCertificate(\"Cert\", options =&gt; {\n        options.AddCertificateAuthenticationOptions();\n    });\n<\/code><\/pre>\n\n\n\n<p>The code for <strong>AddCertificateBasedAuthentication <\/strong>is a bit long, but the idea is to remove as much as possible from Program.cs to avoid cluttering it. Also, putting it in a separate file makes it easy to reuse between local services. <\/p>\n\n\n\n<p>It first tells the Kestral engine to accept certificates that are self-signed. It then also honors the HTTP header X-Client-Cert in which reverse proxies like nginx, ngrok, etc, passes the real certificate. Then it continues loads all certificates that the service should accept. It can be multiple as you may be in a phase of swapping out old certificates. For outgoing calls that uses certificate based authentication, we need to load the PFX file as we need the passphrase. Finally we setup a HttpHandler for making outgoing certificate based authentication calls.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">    public static WebApplicationBuilder AddCertificateBasedAuthentication(this WebApplicationBuilder builder) {\r\n        \/\/ to have Kestrel accept certificates + self-signed\r\n        builder.WebHost.ConfigureKestrel(options => {\r\n            options.ConfigureHttpsDefaults(httpsOptions => {\r\n                httpsOptions.ClientCertificateMode = Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode.AllowCertificate;\r\n                httpsOptions.ClientCertificateValidation = (certificate, chain, sslPolicyErrors) => { return true; };\r\n            });\r\n        });\r\n\r\n        \/\/ to handle if you're behind a reverse proxy, like nginx, ngrok, etc\r\n        builder.Services.AddCertificateForwarding(options => {\r\n            options.CertificateHeader = \"X-Client-Cert\";\r\n            options.HeaderConverter = (headerValue) => {\r\n                return X509CertificateLoader.LoadCertificate(Convert.FromBase64String(headerValue));\r\n            };\r\n        });\r\n\r\n        using var serviceProvider = builder.Services.BuildServiceProvider();\r\n        _log = serviceProvider.GetRequiredService&lt;ILogger&lt;Program>>();\r\n\r\n        \/\/ load all trusted\/valid certificates that clients can use\r\n        try {\r\n            \/\/ certs we accept from incoming calls (we don't need to know the cert passphrase and therefor we use the .cer file(s)\r\n            string[] validCerts = builder.Configuration.GetSection(\"Authentication:Certificate:ValidCertsPaths\").Get&lt;string[]>() ?? [];\r\n            TrustedCertificateStore.store = LoadValidCertificates( validCerts, builder.Environment.ContentRootPath);\r\n            \/\/ cert we use when making outgoing API calls. Here we need to know the passprase and therefor we use a .pfx file\r\n            string certPath = builder.Configuration[\"Authentication:Certificate:CertPath\"]!.ToString();\r\n            string certPassphrase = builder.Configuration[\"Authentication:Certificate:CertPassphrase\"]!.ToString();\r\n            SecureString secureString = new SecureString();\r\n            certPassphrase.ToCharArray().ToList().ForEach(p => secureString.AppendChar(p));\r\n            TrustedCertificateStore.clientCert = new X509Certificate2(certPath, secureString);\r\n        } catch (Exception ex) {\r\n            _log.LogError($\"Failed to load certificates. Do not use the pfx file that is password protected. {ex.Message}\");\r\n        }\r\n\r\n        builder.Services.AddHttpClient(\"CertBasedAuthClient\")\r\n            .ConfigurePrimaryHttpMessageHandler(() => {\r\n                var handler = new SocketsHttpHandler();\r\n                handler.SslOptions.ClientCertificates = new X509Certificate2Collection();\r\n                handler.SslOptions.ClientCertificates.Add(TrustedCertificateStore.clientCert);\r\n                handler.SslOptions.RemoteCertificateValidationCallback = (sender, c, ch, e) => true;\r\n                return handler;\r\n            });\r\n\r\n        return builder;\r\n    }\r\n<\/code><\/pre>\n\n\n\n<p>The code for <strong>AddCertificateAuthenticationOptions <\/strong>sets up the certificate authentication options and the code for actually validating the supplied client certificate against the list of trusted certificates. The code behind ValidateClientCertificate merely matches the client subject and thumbprint against the list of trusted certificates.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">public static CertificateAuthenticationOptions AddCertificateAuthenticationOptions(this CertificateAuthenticationOptions options) {\n        options.AllowedCertificateTypes = CertificateTypes.All;                 \/\/ both chained and self-signed\n        options.ChainTrustValidationMode = X509ChainTrustMode.CustomRootTrust;  \/\/ we have our own cert store\n        options.CustomTrustStore = TrustedCertificateStore.store!;              \/\/ our cert store\n        options.RevocationMode = X509RevocationMode.NoCheck;                    \/\/ we don't check for revocation due to self-signed\n        options.ValidateCertificateUse = false;                                 \/\/ due to self-signed\n        options.Events = new CertificateAuthenticationEvents {\n            OnCertificateValidated = context =&gt; {\n                bool valid = ValidateClientCertificate(context);\n                string msg = \"Certificate is \" + (valid ? \"\" : \"not \") + $\"trusted. Subject: {context.ClientCertificate.Subject}, Thumbprint: {context.ClientCertificate.Thumbprint}\";\n                _log.LogInformation(msg);\n                return Task.CompletedTask;\n            },\n            OnAuthenticationFailed = context =&gt; {\n                string errmsg = $\"Authentication failed during certificate evaluation. {context.Exception.Message}\";\n                _log.LogInformation(errmsg);\n                context.Fail(errmsg);\n                return Task.CompletedTask;\n            }\n        };\n        return options;\n    }<\/code><\/pre>\n\n\n\n<p><\/p>\n\n\n\n<p>Finally, Program.cs sets up some policies that an aspnet MVC controller can use to do fine grain authorization depending if it is Entra, Keycloak or certificate based authentication that is being used. A policy named &#8220;RequireCertOrKeycloak&#8221; is created so that Local Services can do authorization for internal APIs, ie the ones that require that you can provide a certificate or a Keycloak access token and not an Entra ID access token.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">builder.Services.AddAuthorization(options =&gt; {\n    options.DefaultPolicy = new AuthorizationPolicyBuilder( \"Entra\", \"Keycloak\", \"Cert\" ).RequireAuthenticatedUser().Build();\n\n    options.AddPolicy(\"RequireCertOrKeycloak\", policy =&gt; {\n        policy.AddAuthenticationSchemes(\"Keycloak\", \"Cert\");\n        policy.RequireAssertion(context =&gt; {\n            bool isKeycloakUser = context.User.HasClaim(c =&gt; c.Type == System.Security.Claims.ClaimTypes.Role &amp;&amp; c.Value == \"uma_protection\");\n            bool isCertUser = context.User.HasClaim(c =&gt; c.Type == System.Security.Claims.ClaimTypes.Role &amp;&amp; c.Value == \"ApiCert\");\n            return isKeycloakUser || isCertUser;\n        });\n    });\n});\n<\/code><\/pre>\n\n\n\n<h2>Handling the API endpoint implementations<\/h2>\n\n\n\n<p>In the sample we have two API endpoints<\/p>\n\n\n\n<ul><li>\/api\/management- an endpoint that accepts any authentication (Entra\/Keycloak\/cert) that is an example of the Management API<\/li><li>\/api\/internal &#8211; an endpoint that only accepts internal authentication, ie Keycloak or cert<\/li><\/ul>\n\n\n\n<p>The purpose of endpoint <meta charset=\"utf-8\">\/api\/management is to acquire the appropriate downstream authentication and then call \/api\/internal. Since everything happens in the sample, it demonstrates that the program can handle different authentication methods. The \/api\/management honors the authentication method used, if it is Keycloak or certificate, ie if called with a cert it calls the internal API with a cert, etc.<\/p>\n\n\n\n<p>The github repo has three powershell scripts to call \/api\/management using all three authentication methods, but however you call it, it will transition from Entra ID authentication to call the \/api\/internal API using Keycloak or certificates.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">        [Authorize]\n        [HttpGet(\"\/api\/management\")]\n        public async Task&lt;IActionResult> Management([FromQuery] string auth = \"keycloak\") {            \n            \/\/ if explicitly requesting downstream internal auth should be certificate - or incoming call uses cert - then use cert\n            bool useCertificate = (auth == \"cert\" || auth == \"certificate\" || User.Identity?.AuthenticationType == \"Cert\");\n\n            HttpClient client = CreateHttpClient(useCertificate);\n            using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, $\"{this.Request.Scheme}:\/\/{this.Request.Host}\/api\/internal\");\n            if ( !useCertificate) {\n                (string? accessToken, string? errorMessage) = await _keycloak.AcquireAccessToken( _clientId, _clientSecret );\n                request.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", accessToken);\n            }\n            using HttpResponseMessage response = await client.SendAsync(request);\n            string? data = string.Empty;\n            if (response.IsSuccessStatusCode) {\n                data = await response.Content.ReadAsStringAsync();\n            } else {\n                data = $\"Failed with status code: {response.StatusCode}\";\n            }\n            return Ok(CreateResponseBody(data));\n        }\n\n<\/pre>\n\n\n\n<h2>Running the sample<\/h2>\n\n\n\n<p>How to run the sample is explained in the <a href=\"https:\/\/github.com\/cljung\/OfflineAPI\" target=\"_blank\" rel=\"noreferrer noopener\">github repo<\/a>.  Below are two test runs which do the following:<\/p>\n\n\n\n<ol><li>First run uses a powershell script that acquires an access token form Entra ID can calls \/api\/management. You can see that the isser of the token (iss) is s.s.windows.net, ie Entra ID. The \/api\/management calls \/api\/internal using an acquired Keycloak access token, and you can see the details that \/api\/internal echoes back in the &#8220;downstream&#8221; output.<\/li><li>The second run uses a powershell that loads a certificate and uses it to authenticate with \/api\/management. As \/api\/management accepts Entra\/Keycloak\/cert, it allows the call. Seeing that it is certificate based authentication, \/api\/management uses the same method when calling the downstream \/api\/internal API.<\/li><\/ol>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" width=\"1024\" height=\"528\" src=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/06\/test-script-output-1024x528.png\" alt=\"\" class=\"wp-image-7952\" srcset=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/06\/test-script-output-1024x528.png 1024w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/06\/test-script-output-300x155.png 300w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/06\/test-script-output-768x396.png 768w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/06\/test-script-output.png 1171w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<h2>Summary<\/h2>\n\n\n\n<p>If you find this post interesting, please have a look at the code in the github repo as it is hard to explain all details in written language. The goal of the sample was:<\/p>\n\n\n\n<ul><li>Keep Program.cs lightweight. I always find it intriguing when Program.cs is bloated with too much code (that you never understand 6 months later).<\/li><li>Move as much as possible of code to support Certificate Based Authentication to a separate file. <\/li><li>Make it possible to have the ApiController as clean as possible and not to burdened with code that do if\/else on what type of authentication it used.  <\/li><\/ul>\n","protected":false},"excerpt":{"rendered":"<p>If you have locally deployed REST APIs that authenticate with Entra ID and where the APIs need to be running 24&#215;7, you have a problem when your site looses internet connectivity. Sure, the access tokens are valid for some more minutes, but soon you need to refresh them or do token exchange in OBO-flows and [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":[],"categories":[453,458,459],"tags":[],"_links":{"self":[{"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=\/wp\/v2\/posts\/7939"}],"collection":[{"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=7939"}],"version-history":[{"count":16,"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=\/wp\/v2\/posts\/7939\/revisions"}],"predecessor-version":[{"id":7959,"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=\/wp\/v2\/posts\/7939\/revisions\/7959"}],"wp:attachment":[{"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=7939"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=7939"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=7939"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}