OAuth 2.1 · OpenID Connect · Multi-tenant · MIT

Your own identity provider, in two lines.

ARK is a self-hosted OAuth 2.1 / OpenID Connect provider for ASP.NET Core. Add two lines, open a browser and answer the setup wizard: the same process is now your IdP. Apps sign in with two settings in .NET, Python or Node, and one console governs every tenant.

PKCE S256 enforced Keys rotate, nothing redeploys SQLite · MySQL · PostgreSQL Written for AI agents
ARK RAPID emblem: a shield with an ark and a keyhole, circled by an orbit, over a circuit pattern
Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.AddArkIdp();     // that's the configuration
var app = builder.Build();
app.UseArkIdp();         // wizard → IdP, no restart
app.Run();
Zero to signed-in

Three steps, and each one is the whole job.

Most identity setups are a week of config. With Ark, each of these steps is complete as written: nothing is left to wire afterwards.

1Run the provider
2 lines of C#

Add AddArkIdp() and UseArkIdp(), then run. A browser wizard asks for the rest, and the same process becomes the IdP.

dotnet add package Ark.oAuth.Oidc
The setup wizard →
2Onboard the app
1 call

One request registers the client and both redirect URIs, creates the user and maps them to the app. Forgetting that mapping is what usually breaks sign-in.

POST /api/oauth/v1/provision/client
Onboarding →
3Sign users in
2 settings

authority and client_id. Endpoints, keys and capabilities come from discovery, in .NET, Python or Node.

pip install "ark-oauth-client[flask]"
Quickstart →
Set up an IdP on main · ships in the next release

Install the package. Run it. Answer eight questions.

Started with no tenant and no database, the server sends every page to a setup wizard at /ark-idp/setup. When you press Install it writes the settings, creates and seeds the database and reloads its configuration. The same running process then serves the sign-in page and the admin console, with no restart.

  1. 1Any database. SQLite, PostgreSQL or MySQL, with Test connection. Point it at an existing Ark database and it attaches instead of re-seeding.
  2. 2Your keys. Generate RSA 2048, 3072 or 4096, or import a PEM. The private half goes into the database, never into the settings file.
  3. 3Safe on a network. From anywhere but localhost the wizard asks for the setup code printed in the start-up log. After install, /ark-idp/setup answers 404.
  4. 4Overridable. The wizard's settings.json sits below user secrets and environment variables, so a key vault can still override any value.
try it today, from a clone
git clone https://github.com/ir-dev/ark-oauth-oidc
cd ark-oauth-oidc
dotnet run --project Ark.oAuth.Oidc/Ark.oAuth.Oidc.QuickStart
# open https://localhost:7300 → the wizard

Pipeline-built? A host that already has TenantId and a connection string never sees the wizard. The configured route, AddArkOidcServer, is released on NuGet today. See the S8 scenario.

https://localhost:7300/ark-idp/setup
    Ark Identity Server · setup

    Organization & branding

    Onboard an app

    Four steps, one call, nothing forgotten.

    Standing up an application means registering the client, registering its redirect URIs, creating the account and mapping that account to the client. The mapping is the step that gets forgotten, and without it sign-in fails exactly like a wrong password. Provisioning does all four steps, or none of them.

    POST /api/oauth/v1/provision/client
    {
      "client_name": "Nexus Grid Portal",
      "user_name":   "jane@example.com",
      "redirect_uris": ["https://portal.example.com/signin-oidc"],
      "post_logout_redirect_uris": ["https://portal.example.com/signout-callback-oidc"],
      "client_logo": "data:image/png;base64,…"
    }
    • ✓Safe on collision. A taken name answers 409 client_exists and writes nothing, so a script can never rewrite a live app's redirect URIs.
    • ✓Second apps are one call too. provision/user maps an existing account to another app without touching the account itself.
    • ✓A form if you prefer. /{tenant}/admin/provisioning does the same, and shows you the curl for what you typed.
    jane@example.comnew account
    Nexus Grid logo (example)
    • client_createdtrue
    • redirect URIs (sign-in + sign-out)registered
    • user_credentialdefault_password
    • mapping_createdtrue
    • setup_url/oauth2/integrate/nexus_grid_portal

    The sign-in page shows two logos: the host's, so users can see who is asking for their password, and the app's own client_logo, so they can see what they are signing in to. Theme and text size are the viewer's choice on every page.

    Govern

    One operator. Every tenant. Every instance.

    Each tenant is its own issuer, with its own signing key, discovery document and branding. Administration is deliberately single-operator: the tenant you named in the wizard governs all of them, from one console and one API that a pipeline can drive too.

    Operator tenant · ark_idp

    Admin console at /ark_idp/admin, with Provisioning and Data pages. Management API at /api/oauth/v1/…, open to the operator's console session or its machine client's token.

    tenant/upsertprovision/clientactivation/useruser/client/claims/upsertdata/exportdata/import/apply
    Nexus Grid logo (example tenant)
    • issuer/nexus_grid
    • signing keyits own, in JWKS
    • apps & usersmapped per app
    Sentinel Forge logo (example tenant)
    • issuer/sentinel_forge
    • signing keyits own, in JWKS
    • apps & usersmapped per app
    Bluehaven Systems logo (example tenant)
    • issuer/bluehaven
    • signing keyits own, in JWKS
    • apps & usersmapped per app

    Example tenants. Every tenant's endpoints sit under {BaseUrl}/{tenant_id}/; any standard OIDC library pointed there configures itself.

    Provision

    App, redirect URIs, user and access in one call. A taken name is 409 and nothing is written.

    Switch off, really

    Deactivating a user or an app revokes its sessions and refresh tokens at once, without deleting anything.

    Ark claims per app

    What a user may do is granted per user and per app. Apps check billing.admin, not scopes.

    Pipelines, not passwords

    CI calls the API with a {tenant}_machine token whose secret comes from a vault. No other client is admitted.

    Encrypted backup

    Every table, keys included, as one file with a checksum, optionally sealed with AES-256-GCM.

    Import that shows its plan

    replace, merge_skip or merge_overwrite, shown row by row before anything is written. Unsafe rows are excluded by named rules.

    Factory reset

    Empties every table and re-runs the first-run seed. It refuses if no administrator password is configured.

    Upgrades apply themselves

    Pending scripts run once and are recorded. Missing tables and columns are added from the model, on every provider.

    Many instances, one shape.

    Every instance runs the same self-applying bootstrap, so a rolling upgrade has no migration step. Configuration comes from the environment or a key vault, so instances are identical. Use PostgreSQL or MySQL once there is more than one writer. To promote, clone or recover, take an encrypted export from one instance and an analysed import into the other. Every export, import and reset is written to the audit log.

    governance from a pipeline
    TOKEN=$(curl -s -X POST https://idp.example.com/ark_idp/oauth2/token \
      -d grant_type=client_credentials -d client_id=ark_idp_machine \
      --data-urlencode client_secret="$ARK_MACHINE_SECRET" \
      -d scope=client.register | jq -r .access_token)
    
    # a leaver: sessions and refresh tokens go now
    curl -s -X POST https://idp.example.com/api/oauth/v1/activation/user \
      -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
      -d '{ "user_name": "leaver@example.com", "is_active": false }'
    How sign-in works

    Two settings. Everything else is discovered.

    An app is configured with the issuer and its client id, and nothing else. Endpoints, signing keys and capabilities come from the provider's discovery document at runtime, so rotating a key or moving an endpoint never means redeploying a client.

    Your app

    Configured with

    • authority{BaseUrl}/{TenantId}
    • client_idmy-app
    • client_secretconfidential only
    ARK identity provider

    Discovers at runtime

    • Endpoints/oauth2/*
    • Signing keysjwks.json
    • Capabilities*_supported
    • Who may use the appuser ↔ client map
    Back in your app

    A verified user

    • state & noncevalidated
    • Tokensrefreshed for you
    • Ark claimsgate routes

    Authorization is expressed as Ark claims granted per user and per client, not as OAuth scopes. A route asks for billing.admin; the provider decides who holds it.

    Quickstart

    Sign an app in, in any of three languages.

    The three clients are ports of each other and share one version number, so an integration in one translates almost line for line into the others.

    Register the OIDC handlers from one configuration section, then keep the middleware in order.

    • Ark claims arrive as role claims, so [Authorize(Roles = …)] just works.
    • Read the access token per request with GetArkAccessTokenAsync(). Never cache it.
    • Shared PCs: AccountSwitch:RequireArkClaims plus RequiredClaims. Configuration only.
    • Keep Microsoft.IdentityModel.* on one version. A mixed set only fails at runtime.
    terminal
    dotnet add package Ark.oAuth.Client
    Program.cs
    using Ark.oAuth;
    
    builder.Services.AddArkOidcClient(builder.Configuration);  // binds "ark_oauth_client"
    var app = builder.Build();
    app.UseRouting();                                          // MUST precede the next two
    app.UseAuthentication();
    app.UseAuthorization();
    appsettings.json
    "ark_oauth_client": {
      "Authority": "https://idp.example.com/my_idp",
      "ClientId": "my-app",
      "Scopes": ["openid", "profile", "email", "offline_access"]
    }
    Packages

    One version number, four packages.

    Every release ships all four together from one workflow, using Trusted Publishing on every registry. No tokens are involved.

    NuGet ·

    Ark.oAuth.Oidc

    The identity provider: setup wizard, sign-in, consent, admin console, provisioning and data tools.

    dotnet add package Ark.oAuth.Oidc
    NuGet ·

    Ark.oAuth.Client

    The ASP.NET Core client: OIDC handlers, token refresh, the account switch and claim projection.

    dotnet add package Ark.oAuth.Client
    PyPI ·

    ark-oauth-client

    A Python client for Flask, with a core that doesn't depend on any framework.

    pip install "ark-oauth-client[flask]"
    npm ·

    ark-oauth-client

    A Node client: Express/Connect middleware, bearer protection and sessions that interoperate with the Python client.

    npm install ark-oauth-client
    AI first

    Written for the coding agent that will do the work.

    Point Claude Code, Copilot, Cursor or Codex at this repository and it finds rules, prompts and checks it can follow, so it has nothing to guess and no config to invent.

    txt

    llms.txt

    A digest of the whole project at a fixed URL: packages, entry points, required settings, governance API and the rules that matter.

    S15

    A scenario router

    Fifteen tasks, from adding sign-in to restoring a backup. Each one has a paste-ready prompt, exact code, a Done when check and a Do not list.

    .md

    AI-PROMPT & AI-RECIPES in the packages

    The .NET and Python packages ship rules, recipes, the API surface, troubleshooting and migration notes beside their READMEs and inside the package.

    §4

    One system-prompt block

    The whole contract in about forty lines, for an agent that works with Ark every day.

    paste into your agent · S1
    Add Ark sign-in to this ASP.NET Core app using the Ark.oAuth.Client NuGet package. Do not hand-roll OAuth. Authority is https://idp.example.com/my_idp and ClientId is my-app. Put both in the "ark_oauth_client" config section; the client secret, if any, goes in user-secrets, not appsettings. Call AddArkOidcClient(builder.Configuration), and make sure UseRouting() precedes UseAuthentication() and UseAuthorization(). Protect the dashboard with [Authorize], and admin pages with [Authorize(Roles = "billing.admin")]. Add a sign-out that signs out of BOTH ArkOidcClient.CookieScheme and ArkOidcClient.OidcScheme. Do not add any route at /signin-oidc. Afterwards, list the exact redirect URIs I must register on the IdP and remind me to map my user to the client.
    Secure by default

    The safe choice is the only choice.

    OAuth 2.1 removed the risky grants, and Ark never implemented them. The rest is enforced as well, not left as advice.

    • PKCE S256 enforced for public clients; plain is refused.
    • Codes and refresh tokens stored hashed. Read access to the database yields nothing redeemable.
    • Replay is compromise. A reused refresh token revokes its whole family.
    • Exact redirect matching, and no open redirect before the URI is validated.
    • No account enumeration. One message for every sign-in failure, constant-time client authentication, and lockout.
    Standards

    Built on OAuth 2.1, with the specs you'd expect.

    Any standard OIDC library pointed at the issuer configures itself. You don't need the ARK clients to use the provider; they just remove the boilerplate.

    RFC 6749 · 7636

    Authorization code + PKCE

    PKCE is required for public clients, with S256 only.

    OIDC Core · Discovery

    OpenID Connect

    ID tokens, UserInfo, nonce, at_hash, sid and discovery.

    RFC 9068 · 7517

    JWT access tokens & JWKS

    Access tokens are at+jwt, with a published key set and two-phase rotation.

    RFC 8628

    Device authorization

    For CLIs and TVs, including verification_uri_complete.

    RFC 9126

    Pushed authorization requests

    Optional, and can be made mandatory per server.

    RFC 7662 · 7009

    Introspection & revocation

    Revoking a token revokes its whole refresh-token family.

    OIDC Logout

    RP-initiated & back-channel

    A signed logout_token goes to every client the user signed into.

    RFC 7591 · 7592

    Dynamic client registration

    Available, but off by default.

    RFC 9207 · 8252

    Issuer ID & native apps

    Returns iss in the authorization response and accepts loopback redirects on any port.

    Deliberately not supported: the implicit grant, the hybrid flow and the password grant (all removed in OAuth 2.1), plain PKCE, and front-channel logout.

    Before you ship

    Four things that look like a wrong password.

    Most failed sign-ins come down to one of these, in every language. Provisioning avoids the first two.

    Symptom: “invalid credentials”

    Map the user to the client

    ARK maps users to clients explicitly. Without a mapping, sign-in fails with an error that is indistinguishable from a wrong password.

    Symptom: invalid redirect_uri

    Register both callbacks, exactly

    Redirect URIs are matched byte for byte. Register both /signin-oidc and /signout-callback-oidc.

    Symptom: invalid_scope

    Scopes are a whitelist

    A scope that isn't registered on the client is rejected, not dropped. offline_access is what produces a refresh token.

    Symptom: stuck on “no access”

    Shared browsers need an account switch

    SSO signs the next person in as the previous one. All three clients ship the fix: turn on RequireArkClaims with RequiredClaims, and the app offers prompt=login instead of a dead end.

    Try it

    Build a PKCE sign-in request for your issuer.

    Enter your authority and client id to get the authorization URL and the matching token exchange. The PKCE verifier, state and nonce are generated with Web Crypto in this tab.

    Nothing you type leaves this page. It makes no network calls, and nothing is stored.

    1 · send the browser to
    Generate a request to see it here.
    2 · exchange the code (public client)
    …
    keep for the callback
    …
    The marks

    From a simple oAuth2 sample to ARK RAPID.

    Ark started as a small ASP.NET Core OAuth 2 implementation and grew into a standards-complete, multi-tenant identity provider. These are its marks, then and now.

    ARK RAPID oAuth2 (OIDC) IdP: secure identity, trusted access, faithful innovation ARK RAPID oAuth2 (OIDC) IdP: secure identity, trusted access, faithful innovation