Ark.oAuth.Client
2.0.10
dotnet add package Ark.oAuth.Client --version 2.0.10
NuGet\Install-Package Ark.oAuth.Client -Version 2.0.10
<PackageReference Include="Ark.oAuth.Client" Version="2.0.10" />
<PackageVersion Include="Ark.oAuth.Client" Version="2.0.10" />
<PackageReference Include="Ark.oAuth.Client" />
paket add Ark.oAuth.Client --version 2.0.10
#r "nuget: Ark.oAuth.Client, 2.0.10"
#:package Ark.oAuth.Client@2.0.10
#addin nuget:?package=Ark.oAuth.Client&version=2.0.10
#tool nuget:?package=Ark.oAuth.Client&version=2.0.10
Ark.oAuth.Client
The OAuth 2.1 / OpenID Connect client for ASP.NET Core applications. One call in Program.cs,
two required settings, and [Authorize] works.
It configures ASP.NET Core's own OpenID Connect and cookie handlers rather than hand-rolling the
protocol, so PKCE, state, nonce, JWKS rollover and token refresh all come from the framework.
Built for Ark.oAuth.Oidc, but because it is the
stock handler underneath, changing Authority and ClientId is enough to point the same
application at Entra ID, Okta, Auth0 or Keycloak.
For AI coding agents — prompt-oriented, source-verified docs live beside this README:
AI-PROMPT.md(rules + capability map + injectable context block),AI-RECIPES.md(task → code recipes),API-GUIDE.md(exact public API + decision guide),TROUBLESHOOTING.md(symptom → cause → fix),MIGRATION.md(versions & upgrading). The C# in this directory is authoritative; do not invent APIs.
Install
dotnet add package Ark.oAuth.Client
Program.cs
using Ark.oAuth;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddArkOidcClient(builder.Configuration);
builder.Services.AddControllersWithViews();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting(); // MUST precede the next two
app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
appsettings.json
Authority and ClientId are the only required keys — every endpoint is read from the provider's
discovery document.
{
"ark_oauth_client": {
"Authority": "https://idp.example.com/my_idp", // the issuer
"ClientId": "my-app",
"ClientSecret": null, // null = public client + PKCE
"Scopes": [ "openid", "profile", "email", "offline_access" ],
"CallbackPath": "/signin-oidc",
"SignedOutCallbackPath": "/signout-callback-oidc",
"SignedOutRedirectUri": "/",
"AuthErrorPath": "/", // failed callbacks land here with ?auth_error=...
"RequireHttpsMetadata": true,
"CookieName": "my_app_auth",
"RoleClaimType": "role", // ark_claims from the access token map onto this
"ExpireMins": 480
}
}
Using the result
[Authorize]
public IActionResult Secure() => View();
// Ark authorization claims (ark_claims) are projected as roles, so policies work directly
[Authorize(Roles = "billing.admin")]
public IActionResult Billing() => View();
Reading the access token — always per call, never cached; the cookie handler refreshes it underneath you:
var accessToken = await HttpContext.GetArkAccessTokenAsync();
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.example.com/things");
await request.WithArkTokenAsync(HttpContext);
Protecting an API
builder.Services
.AddAuthentication()
.AddArkOidcApi(arkConfig);
The flows outside interactive sign-in
AddArkOidcClient also registers three services. All three work off the provider's discovery
document, so they need the issuer and nothing else.
| Service | For |
|---|---|
ArkSetupProbe |
Pairs local configuration against the provider's live metadata and returns ArkSetupModel — issuer mismatch, unregistered scopes, the exact redirect URI this app will send. Render it and a registration mistake reads as a sentence instead of invalid_client. |
ArkClientCredentials |
The client credentials grant. GetTokenAsync caches until shortly before expiry; RequestTokenAsync forces a live exchange. |
ArkRegistration |
Dynamic client registration (RFC 7591) and management (RFC 7592). |
var model = await setup.ProbeAsync(HttpContext);
var token = await credentials.GetTokenAsync(clientId, secret, new[] { "reports.read" });
var client = await registration.RegisterAsync(metadata, token.AccessToken);
Registering the application
Three things have to line up on the provider, not one:
- Create the client.
- Register both redirect URIs —
{host}/signin-oidcand{host}/signout-callback-oidc— matched byte for byte. No wildcards, no prefix matching. - Grant each user access to that client. Without the mapping, sign-in fails with a message that looks like a wrong password.
Scopes on a client record are a whitelist: an unregistered scope is rejected, not dropped.
offline_access is what produces a refresh token. Never add a controller action at CallbackPath —
it shadows the handler. Sign out of both ArkOidcClient.CookieScheme and ArkOidcClient.OidcScheme,
or the provider session survives and the next sign-in is silent.
Optionally, register a backchannel_logout_uri as well. Ark.oAuth.Oidc 2.0.3 and later POSTs a
signed logout_token there when a session this application took part in ends elsewhere — someone
signing out at another application, or an account being deactivated. This package does not host
that endpoint for you: write an action that validates the posted token against the provider's JWKS
(typ: logout+jwt, matching iss and aud, an events claim, never a nonce), signs out the
session it names, and answers 200 or 204. Register nothing and the application is simply never
notified, which is how it behaved before.
Shared browsers: the wrong account, and no way out
Single sign-on is a property of the browser, not of the tab. Once one person signs in to client-a, the provider's session cookie answers the authorize request of every other client in that browser. So when a second person opens client-b, the challenge is satisfied silently by the first person's session: client-b receives a valid token for an account with no mapping to it, shows "you do not have access to this application", and offers nothing that helps — the sign-in link goes back to the same session and returns the same answer. The loop ends when somebody knows to clear cookies.
Two settings break it.
"ark_oauth_client": {
"AccountSwitch": {
"RequireArkClaims": true, // refuse the callback, do not write the cookie
"AppDisplayName": "Client B" // how the page names this application
}
}
RequireArkClaims moves the entitlement check to the callback, where it belongs. An account with
no ark_claims for this client never gets a session here at all — instead of being signed in as
the wrong person and meeting 403 on every page, the user lands on a page that names the account
that is signed in and offers Sign in as a different user. That button challenges the provider
with prompt=login, the one parameter it must honour by ignoring its session and drawing the
sign-in form (OIDC Core §3.1.2.1), so the person at the keyboard can enter their own credentials.
Nothing needs adding to Program.cs: the page and its two POST endpoints register themselves.
Left at its default (false), RequireArkClaims changes no behaviour — but the page still serves
the 403s that [Authorize] produces, in place of the framework's /Account/AccessDenied, which
most applications never create.
From your own pages
The same two operations, for a "Not you?" link in your header or an action of your own:
await HttpContext.ArkSwitchUserAsync(returnUrl); // drop the local cookie, prompt=login
await HttpContext.ArkSignOutEverywhereAsync(returnUrl); // RP-initiated logout, ends the IdP session too
var refused = HttpContext.ArkDeniedAccount(); // who was refused, for your own page
Or on any challenge you issue yourself:
return Challenge(ArkChallengeProperties.SwitchUser("/", loginHint: "someone@example.com"),
ArkOidcClient.OidcScheme);
Everything else it takes
| Setting | Default | |
|---|---|---|
Enabled |
true |
Serve the endpoints. Off still leaves the extension methods usable. |
AutoRegisterEndpoints |
true |
Place them in the pipeline for you. Off to call UseArkAccountEndpoints() where you want it. |
RequiredClaims |
— | Narrow the check: the user must hold at least one of these values. |
AccessDeniedPath |
/ark/no-access |
Point at your own route, with ServeDefaultPage: false, to render it yourself. |
SwitchUserPath / SignOutPath |
/ark/switch-user, /ark/sign-out |
POST, same-origin. |
ShowSignedInAccount |
true |
Naming the account is what makes the page make sense; off if you would rather not print somebody else's address. |
EndProviderSessionOnSwitch |
false |
false re-prompts and leaves the other person's other applications alone. true signs them out everywhere — right for a kiosk. |
Prompt |
login |
What the switch sends. select_account where the provider has a picker. |
SupportUrl / SupportEmail |
— | A "request access" link on the page. |
For rules the settings do not cover — a licence lookup, a group claim, your own denial page or an access-request ticket — use the events overload:
builder.Services.AddArkOidcClient(builder.Configuration, o =>
{
o.Events.OnEvaluateAccess = ctx => Task.FromResult(ctx.ArkClaims.Count > 0 && Licensed(ctx.Principal));
o.Events.OnAccessDenied = ctx => { logger.LogWarning("no access: {Email}", ctx.Email); return Task.CompletedTask; };
});
Worth knowing: switching accounts is a client-side remedy. The provider still issued a token for an account that has no mapping to this client — only the client refuses it. Ending that at the source means the authorization endpoint declining to satisfy an SSO session with no mapping for the client it is being used against.
Legacy flow
ark_oauth_client:UseLegacyFlow keeps the original cookie/bearer middleware for deployments
migrating off the v1 endpoints. It does not validate state or nonce — treat it as a migration
aid, not a supported configuration.
Documentation
A complete, runnable client application and the full walkthrough are in the repository: https://github.com/ir-dev/ark-oauth-oidc
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net9.0
- ark.net.util (>= 1.0.4.14)
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 8.0.15)
- Microsoft.AspNetCore.Authentication.OpenIdConnect (>= 8.0.15)
- Microsoft.IdentityModel.JsonWebTokens (>= 8.8.0)
- Microsoft.IdentityModel.Protocols.OpenIdConnect (>= 8.8.0)
- System.IdentityModel.Tokens.Jwt (>= 8.8.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.0.10 | 149 | 9/12/2026 |
| 2.0.9 | 167 | 9/3/2026 |
| 2.0.8 | 133 | 9/2/2026 |
| 2.0.7 | 115 | 9/1/2026 |
| 2.0.6 | 116 | 9/1/2026 |
| 2.0.5 | 144 | 8/30/2026 |
| 2.0.4 | 129 | 8/29/2026 |
| 2.0.3 | 121 | 8/29/2026 |
| 2.0.2 | 143 | 8/19/2026 |
| 2.0.1 | 141 | 8/18/2026 |
| 2.0.0 | 124 | 8/17/2026 |
| 1.0.5.45 | 202 | 3/22/2026 |
| 1.0.5.44 | 189 | 3/20/2026 |
| 1.0.5.43 | 331 | 10/12/2025 |
| 1.0.5.42 | 345 | 7/9/2025 |
| 1.0.5.41 | 326 | 7/3/2025 |
| 1.0.5.40 | 328 | 7/3/2025 |
| 1.0.5.39 | 315 | 7/2/2025 |
| 1.0.5.38 | 313 | 7/2/2025 |
| 1.0.5.37 | 322 | 7/2/2025 |
2.1.0 — account switching on a shared browser.
Fixes the case where two people use one browser. Single sign-on is browser-wide, so once the first
person has signed in anywhere, the provider's session answers the authorize request of every other
client in that browser. The second person opens a different application, is silently signed in as
the first, is told they do not have access — and has no way back, because the sign-in link returns
to the same session and gives the same answer.
* ark_oauth_client:AccountSwitch:RequireArkClaims moves the entitlement check to the callback. An
account with no ark_claims for this client is refused before the authentication cookie is
written, so the browser never holds a session for somebody who cannot use the application.
* A built-in access-denied page at /ark/no-access names the account that is signed in and offers
"Sign in as a different user", which challenges with prompt=login so the provider draws its
sign-in form instead of answering from its session. It also replaces the framework's
/Account/AccessDenied default for ordinary [Authorize] 403s, which most applications never
create and which therefore renders as a 404.
* HttpContext.ArkSwitchUserAsync / ArkSignOutEverywhereAsync / ArkSignOutLocallyAsync, and
ArkChallengeProperties.SwitchUser for a challenge you issue yourself — prompt, login_hint and
max_age now reach the authorize request through the authentication properties.
* An AddArkOidcClient(configuration, options) overload carrying ArkClientEvents: OnEvaluateAccess
for an entitlement rule of your own, OnAccessDenied to log it or render your own page.
The page and its two POST endpoints register themselves, so Program.cs needs no edit. Every part is
off or unchanged by default: consumers on 2.0.3 who set nothing get identical behaviour except that
an [Authorize] 403 now reaches an explanation instead of a 404.
2.0.3 — version alignment.
No API change over 2.0.2. Rebuilt and republished alongside Ark.oAuth.Oidc 2.0.3 so the two halves
carry the same version. Consumers on 2.0.2 can upgrade with no code change; the notes for the
earlier versions apply unchanged and are kept below.
Worth knowing if you are upgrading the provider at the same time: 2.0.3 of Ark.oAuth.Oidc adds
back-channel logout, and this package does not host the receiving endpoint. An application that
wants its own cookie dropped when the IdP session ends registers a backchannel_logout_uri and
implements that endpoint itself — validate the posted logout_token against the provider's JWKS
(typ "logout+jwt", matching iss and aud, an events claim, no nonce), end the session it names, and
answer 200 or 204. Applications that register no URI are unaffected and keep behaving exactly as
they do on 2.0.2.
2.0.2 — version alignment.
No API change over 2.0.1. Rebuilt and republished alongside Ark.oAuth.Oidc 2.0.2 so the two halves
carry the same version. Consumers on 2.0.1 can upgrade with no code change; the 2.0.1 and 2.0.0
notes apply unchanged and are kept below.
2.0.1 — maintenance release.
No API change over 2.0.0. Rebuilt and republished alongside Ark.oAuth.Oidc 2.0.1 to keep the two
halves on the same version, and to carry the packaging fixes below. Consumers on 2.0.0 can upgrade
with no code change; the 2.0.0 notes apply unchanged and are kept below.
Packaging: the package icon (ark_logo.png) and the readme are packed at the root of the .nupkg, so
nuget.org and the Visual Studio package manager show both. The icon is referenced through
PackageIcon rather than the deprecated PackageIconUrl, and is included with an empty PackagePath so
it lands at the package root on a non-Windows build too.
2.0.0 — the standards release.
* AddArkOidcClient now configures ASP.NET Core's OpenID Connect and cookie handlers against the
provider's discovery document. Real PKCE, state and nonce validation, JWKS rollover and silent
refresh all come from the framework instead of being hand-rolled.
* Authority and ClientId are the only required settings — no pasted rsaPublic, no issuer/audience
to mirror, no per-tenant key table to keep in step with rotation.
* AddArkOidcApi for protecting an API with bearer tokens from the same provider.
* Ark authorization claims (ark_claims) are projected onto the principal as role claims, so
[Authorize(Roles = "...")] and policies work directly.
* GetArkAccessTokenAsync / WithArkTokenAsync read the current token per call, so a token refreshed
by the cookie handler is picked up automatically.
* New services for the non-interactive flows: ArkSetupProbe (configuration vs. live metadata),
ArkClientCredentials (client credentials grant with caching) and ArkRegistration (RFC 7591/7592).
Breaking / migration: the standard flow is now the default. Set ark_oauth_client:UseLegacyFlow to
true to keep the original cookie/bearer middleware while migrating — it does not validate state or
nonce and should be treated as a migration aid, not a supported configuration.
Packaging: the package no longer ships a sample appsettings file into consuming projects, and now
carries a readme, an SPDX license expression and a symbol package.