Ark.oAuth.Client 2.0.7

There is a newer version of this package available.
See the version list below for details.
dotnet add package Ark.oAuth.Client --version 2.0.7
                    
NuGet\Install-Package Ark.oAuth.Client -Version 2.0.7
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Ark.oAuth.Client" Version="2.0.7" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Ark.oAuth.Client" Version="2.0.7" />
                    
Directory.Packages.props
<PackageReference Include="Ark.oAuth.Client" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Ark.oAuth.Client --version 2.0.7
                    
#r "nuget: Ark.oAuth.Client, 2.0.7"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Ark.oAuth.Client@2.0.7
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Ark.oAuth.Client&version=2.0.7
                    
Install as a Cake Addin
#tool nuget:?package=Ark.oAuth.Client&version=2.0.7
                    
Install as a Cake Tool

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.

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:

  1. Create the client.
  2. Register both redirect URIs — {host}/signin-oidc and {host}/signout-callback-oidc — matched byte for byte. No wildcards, no prefix matching.
  3. 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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 150 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
Loading failed

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.