Let me describe the auth experience in a typical C# project and see if this sounds familiar.

Day 1: You need login. You add AddOpenIdConnect, configure an Authority, register a client with your IdP, wire up cookie auth, handle token validation, and 200 lines later you can sign in with one provider.

Day 3: Product wants "Sign in with Google" and "Sign in with Microsoft." Now you're registering apps in two separate cloud portals, writing two separate AddOpenIdConnect blocks with different scopes, different claim mappings, and different user info endpoints. Each provider returns user data in a slightly different shape — Google gives you sub, Microsoft gives you oid, GitHub gives you id. You write normalization code.

Day 7: You realize you can't test any of this offline. Your dev environment depends on cloud IdPs that require internet, registered apps, and callback URLs that point to localhost — which half the providers reject. So you hardcode a test user. Or you skip auth in dev entirely and hope it works in staging.

Day 14: Someone suggests IdentityServer for local development. You spend an afternoon configuring it. It works, but now you're maintaining an entire identity server project alongside your actual app.

Day 30: You have 400 lines of auth plumbing, three cloud portal configurations, a local IdentityServer project you barely understand, and a nagging feeling that this should be simpler.

It should be. And now it is.

Two Packages. The Whole Problem. Solved.

dotnet add package Noundry.Authnz
dotnet tool install -g Noundry.AuthnzNet.CLI

Noundry.Authnz is a production OAuth client library for ASP.NET Core. Multi-provider, built-in UI components, normalized user data, PKCE security, three lines to configure.

AuthnzNet is a local OAuth server distributed as a CLI tool. Three commands and you have a fully-functional OIDC server running on localhost — the same protocol your production providers speak.

Together, they replace every piece of auth plumbing you've ever written.

What Noundry.Authnz Actually Looks Like

Here's your entire auth setup:

Program.cs:

builder.Services.AddNoundryOAuth(builder.Configuration);

// ... later in pipeline:

app.UseNoundryOAuth();

That's it. Two lines of C#.

appsettings.json:

{
  "OAuth": {
    "Providers": {
      "google": {
        "ClientId": "YOUR_GOOGLE_CLIENT_ID",
        "ClientSecret": "YOUR_GOOGLE_CLIENT_SECRET"
      },
      "microsoft": {
        "ClientId": "YOUR_MICROSOFT_CLIENT_ID",
        "ClientSecret": "YOUR_MICROSOFT_CLIENT_SECRET"
      },
      "github": {
        "ClientId": "YOUR_GITHUB_CLIENT_ID",
        "ClientSecret": "YOUR_GITHUB_CLIENT_SECRET"
      }
    }
  }
}

Your login page:

<noundry-oauth-login show-all="true" />

One tag helper. It renders a login button for every configured provider. Google, Microsoft, GitHub, Apple, Facebook — whatever you've configured, it shows up. Add a provider to config, the button appears. Remove it, the button disappears.

No AddOpenIdConnect. No AddGoogle. No AddMicrosoftAccount. No per-provider configuration blocks. No manual scope management. No claim mapping. No normalization code.

Five Providers, Pre-Configured

Noundry.Authnz ships with built-in defaults for the providers you actually use:

  • Google — openid, profile, email
  • Microsoft — openid, profile, email
  • GitHub — user:email
  • Apple — name, email
  • Facebook — email, public_profile

Each provider returns user data in a different format. Google gives you sub as the user ID. Microsoft gives you oid. GitHub gives you id as an integer. Facebook gives you a scoped app ID.

Noundry.Authnz normalizes all of this into one clean object:

public class OAuthUserInfo
{
    public string Id { get; set; }
    public string Email { get; set; }
    public string Name { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string AvatarUrl { get; set; }
    public string Provider { get; set; }
}

Same shape. Every provider. No more if (provider == "google") { ... } else if (provider == "microsoft") { ... }.

The Dev Problem — And How AuthnzNet Solves It

Here's the thing about Google, Microsoft, and GitHub OAuth: they're cloud services. You need internet. You need registered apps. You need callback URLs. You need to manage client secrets in cloud portals.

For production, that's fine. For local development at 7am with spotty wifi, that's friction.

AuthnzNet eliminates that friction:

ndaz quickstart
ndaz serve

You now have a real OIDC server on https://localhost:5000 with RS256 signing, PKCE, login UI, and all four OAuth grant types. It speaks the same protocol as Google, Microsoft, and every other OIDC provider.

And Noundry.Authnz has AuthnzNet as a built-in provider — first-class, just like Google:

builder.Services.AddNoundryOAuth(builder.Configuration, options =>
{
    options.ConfigureOAuthProvider(OAuthProvider.AuthnzNet, clientId, clientSecret);
});

One line. Your login page shows "Sign in with AuthnzNet" in dev. In production, it shows Google, Microsoft, and GitHub.

The Full Picture: Dev to Production

Here's what this looks like in a real project:

Development (appsettings.Development.json)

{
  "OAuth": {
    "Providers": {
      "authnznet": {
        "ClientId": "my-dev-client",
        "ClientSecret": "my-dev-secret"
      }
    }
  }
}

Production (appsettings.Production.json)

{
  "OAuth": {
    "Providers": {
      "google": {
        "ClientId": "your-google-client-id",
        "ClientSecret": "your-google-secret"
      },
      "microsoft": {
        "ClientId": "your-microsoft-client-id",
        "ClientSecret": "your-microsoft-secret"
      }
    }
  }
}

Your login page renders one button in dev: "Sign in with AuthnzNet." You click it, get redirected to the local AuthnzNet login page, enter dev@localhost / Password123!, and you're in. Full OAuth flow. PKCE. ID tokens. All of it — running on your machine, no internet required.

In production, it renders two buttons: "Sign in with Google" and "Sign in with Microsoft." Same code. Same tag helper. Same Program.cs. Zero conditional logic. The config drives everything.

The code you wrote? It never changed. Not one line.

What About the User Data?

This is where the success handler shines. When a user authenticates — from any provider — you get a normalized callback:

public class MyOAuthHandler : IOAuthSuccessHandler
{
    public async Task<OAuthSuccessResult?> HandleAsync(
        OAuthSuccessContext context)
    {
        var userService = context.GetRequiredService<IUserService>();

        var user = await userService.FindOrCreateFromOAuthAsync(
            providerId: context.UserInfo.Id,
            provider: context.UserInfo.Provider,
            email: context.UserInfo.Email,
            name: context.UserInfo.Name
        );

        return new OAuthSuccessResult
        {
            AdditionalClaims = new List<Claim>
            {
                new("app_user_id", user.Id.ToString()),
                new(ClaimTypes.Role, user.Role)
            }
        };
    }
}

Whether the user came from Google, Microsoft, GitHub, or your local AuthnzNet server — this handler runs the same way, receives the same normalized data, and your app doesn't care which provider authenticated them.

Register it once:

builder.Services.AddScoped<IOAuthSuccessHandler, MyOAuthHandler>();

Done. Every OAuth login flows through your handler. Create users, assign roles, log events, sync to your database — whatever your app needs.

The Old Way vs. The New Way

The old way — adding Google + Microsoft auth to an ASP.NET Core app:

// 30+ lines of cookie configuration
builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = "google";
})
.AddCookie()
.AddGoogle("google", options =>
{
    options.ClientId = "...";
    options.ClientSecret = "...";
    options.Scope.Add("profile");
    options.Scope.Add("email");
    options.ClaimActions.MapJsonKey("picture", "picture");
    options.Events.OnCreatingTicket = async context =>
    {
        // manual user info normalization
        // manual user creation
        // manual claim mapping
    };
})
.AddMicrosoftAccount("microsoft", options =>
{
    options.ClientId = "...";
    options.ClientSecret = "...";
    options.Scope.Add("User.Read");
    options.Events.OnCreatingTicket = async context =>
    {
        // different normalization for Microsoft
        // different claim names
        // different user info format
    };
});

// Then build your own login page with provider buttons
// Then handle the different claim shapes in your app
// Then figure out local dev without these cloud providers

The new way:

builder.Services.AddNoundryOAuth(builder.Configuration);
app.UseNoundryOAuth();
<noundry-oauth-login show-all="true" />

That's 3 lines vs. 50+. And the 3-line version handles more providers, normalizes user data, includes UI components, and works with a local dev server out of the box.

Need a Custom Provider? One Call.

Noundry.Authnz isn't limited to the five built-in providers. Any OAuth 2.0 or OpenID Connect provider works:

builder.Services.AddNoundryOAuth(builder.Configuration, options =>
{
    options.ConfigureCustomOAuthProvider(
        providerName: "okta",
        clientId: "your-client-id",
        clientSecret: "your-client-secret",
        authorizationEndpoint: "https://your-domain.okta.com/oauth2/v1/authorize",
        tokenEndpoint: "https://your-domain.okta.com/oauth2/v1/token",
        userInfoEndpoint: "https://your-domain.okta.com/oauth2/v1/userinfo",
        scopes: new List<string> { "openid", "profile", "email" },
        usePkce: true
    );
});

Okta, Auth0, Keycloak, your company's internal IdP — if it speaks OAuth, Noundry.Authnz can connect to it.

The Tag Helpers

Three components. That's the entire auth UI:

Login button — renders provider buttons, handles the OAuth redirect:

<noundry-oauth-login show-all="true" />
<noundry-oauth-login provider="google" />

Status display — shows the authenticated user's avatar, name, and email:

<noundry-oauth-status show-avatar="true" show-name="true" />

Logout button — signs out and redirects:

<noundry-oauth-logout button-text="Sign out" redirect-uri="/" />

Add them to any Razor page. They handle the OAuth flow, cookie management, and session lifecycle. No JavaScript. No custom forms. No token management.

Why This Matters

Auth in C# has been a solved protocol problem for years — OAuth 2.0 and OIDC work great. But it's been an unsolved developer experience problem.

Adding authentication to an ASP.NET Core app has always meant:

  • Dozens of lines of middleware configuration
  • Per-provider plumbing with different SDKs and scopes
  • Manual claim normalization across providers
  • No local dev story without cloud dependencies or IdentityServer
  • No built-in UI components — you build your own login page

Noundry.Authnz and AuthnzNet fix all of that:

  • 2 lines to configure auth (not 50+)
  • 1 tag helper for login UI (not a custom Razor page)
  • 1 normalized object for user data (not per-provider claim mapping)
  • 1 CLI command for local dev (not a cloud portal or IdentityServer project)
  • 0 code changes to go from dev to production (just config)

Get Started

For production auth (the client library):

dotnet add package Noundry.Authnz

Docs: noundry.com/docs/authnz

For local development (the OAuth server):

dotnet tool install -g Noundry.AuthnzNet.CLI
ndaz quickstart
ndaz serve

Docs: authnz.net

For both together (the full story):

dotnet add package Noundry.Authnz
dotnet tool install -g Noundry.AuthnzNet.CLI
ndaz quickstart && ndaz serve

Add Noundry.Authnz to your app. Point it at AuthnzNet in development. Point it at Google, Microsoft, or GitHub in production. Ship it.

Auth in C# is no longer a mess.

Both packages are free and open source. No telemetry, no accounts, no vendor lock-in.

The auth story for C# just got a lot simpler.

Need help wiring up OAuth in your .NET project? I help teams get authentication right without the ceremony. Let's talk.