PulseAuth 1.2.2

There is a newer version of this package available.
See the version list below for details.
dotnet add package PulseAuth --version 1.2.2
                    
NuGet\Install-Package PulseAuth -Version 1.2.2
                    
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="PulseAuth" Version="1.2.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PulseAuth" Version="1.2.2" />
                    
Directory.Packages.props
<PackageReference Include="PulseAuth" />
                    
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 PulseAuth --version 1.2.2
                    
#r "nuget: PulseAuth, 1.2.2"
                    
#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 PulseAuth@1.2.2
                    
#: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=PulseAuth&version=1.2.2
                    
Install as a Cake Addin
#tool nuget:?package=PulseAuth&version=1.2.2
                    
Install as a Cake Tool

PulseAuth Banner

PulseAuth

Free, open-source OAuth2 / OpenID Connect authorization server for ASP.NET Core.

PulseAuth is a lightweight alternative to Duende IdentityServer (formerly IdentityServer4) designed to be accessible, free, and easy to set up. It implements the core OAuth2 and OIDC flows on top of ASP.NET Core minimal APIs and integrates natively with ASP.NET Core Identity.


Packages

Package Description
PulseAuth Core OAuth2/OIDC server — endpoints, token service, in-memory stores
PulseAuth.Identity Connects PulseAuth to ASP.NET Core Identity (UserManager, SignInManager)
PulseAuth.EntityFramework EF Core persistent stores (clients, codes, refresh tokens)

Supported flows

  • Authorization Code + PKCE — secure for web apps, SPAs and mobile
  • Client Credentials — service-to-service authentication
  • Refresh Token — with optional rotation
  • Resource Owner Password — direct username/password login (ideal for React/Angular SPAs that own their own login UI)
  • Google ID Token exchange — accept a Google-issued ID token from the frontend SDK and return PulseAuth tokens
  • Facebook Access Token exchange — accept a Facebook access token from the frontend SDK and return PulseAuth tokens

OIDC endpoints

Endpoint URL
Discovery /.well-known/openid-configuration
JWKS /.well-known/jwks
Authorize /connect/authorize
Token /connect/token
UserInfo /connect/userinfo
Revocation /connect/revocation
End Session /connect/endsession

Quick start

1. Install

dotnet add package PulseAuth
dotnet add package PulseAuth.Identity
dotnet add package PulseAuth.EntityFramework

# MariaDB / MySQL provider (recommended: Pomelo)
dotnet add package Pomelo.EntityFrameworkCore.MySql

2. appsettings.json

{
  "ConnectionStrings": {
    "Default": "Server=localhost;Port=3306;Database=myauth;User=root;Password=secret;"
  },
  "PulseAuth": {
    "Issuer": "https://auth.myapp.com"
  },
  "Auth": {
    "Google":   { "ClientId": "", "ClientSecret": "" },
    "Facebook": { "AppId":    "", "AppSecret":    "" }
  }
}

3. Configure Program.cs

using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using PulseAuth.Builders;
using PulseAuth.Constants;
using PulseAuth.Extensions;
using PulseAuth.Helpers;
using PulseAuth.Models;

var builder = WebApplication.CreateBuilder(args);
var conn    = builder.Configuration.GetConnectionString("Default")!;

// ── MariaDB server version (auto-detect or pin explicitly) ───────────────────
var serverVersion = ServerVersion.AutoDetect(conn);
// Or pin it:  new MariaDbServerVersion(new Version(10, 11));

// ── ASP.NET Core Identity (users & roles) ────────────────────────────────────
builder.Services
    .AddDbContext<ApplicationDbContext>(opts =>
        opts.UseMySql(conn, serverVersion))
    .AddIdentity<ApplicationUser, IdentityRole>(opts =>
    {
        opts.Password.RequiredLength  = 8;
        opts.Password.RequireDigit    = true;
        opts.SignIn.RequireConfirmedAccount = false;
    })
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

// ── PulseAuth ─────────────────────────────────────────────────────────────────
var pulse = builder.Services.AddPulseAuth(opts =>
{
    opts.Issuer              = builder.Configuration["PulseAuth:Issuer"]!;
    opts.RotateRefreshTokens = true;
});

pulse
    .AddDeveloperSigningCredential()          // swap for persistent key in prod
    .AddEntityFrameworkStores(opts =>         // clients, codes and tokens in MariaDB
        opts.UseMySql(conn, serverVersion))
    .AddIdentityUsers<ApplicationUser>()
    // ── Social token exchange (pure API — no redirect pages required) ────────
    .AddGoogleTokenExchange(
        builder.Configuration["Auth:Google:ClientId"]!)
    .AddFacebookTokenExchange(
        builder.Configuration["Auth:Facebook:AppId"]!,
        builder.Configuration["Auth:Facebook:AppSecret"]!)
    // ── Clients ──────────────────────────────────────────────────────────────
    .AddInMemoryClients(
    [
        // React / Angular SPA — password grant + social exchange + refresh
        new Client
        {
            ClientId           = "my-spa",
            ClientName         = "My SPA",
            AllowedGrantTypes  =
            [
                GrantTypes.Password,
                GrantTypes.GoogleIdToken,
                GrantTypes.FacebookAccessToken,
                GrantTypes.RefreshToken,
            ],
            AllowOfflineAccess = true,
            AllowedScopes      = [StandardScopes.OpenId, StandardScopes.Profile,
                                   StandardScopes.Email, StandardScopes.OfflineAccess, "api"],
        },
        // Backend microservice — client credentials
        new Client
        {
            ClientId          = "my-api",
            ClientName        = "Backend API",
            ClientSecretHash  = ClientSecretHelper.HashSecret("change-me-in-prod"),
            AllowedGrantTypes = GrantTypes.ClientCredentialsOnly,
            AllowedScopes     = ["api"],
        },
    ]);

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();
app.MapPulseAuth();

app.Run();

4. Apply migrations

Two contexts: one for Identity, one for PulseAuth stores.

# Identity tables (AspNetUsers, AspNetRoles, etc.)
dotnet ef migrations add InitIdentity  --context ApplicationDbContext
dotnet ef database update              --context ApplicationDbContext

# PulseAuth tables (PulseAuth_Clients, PulseAuth_AuthCodes, etc.)
dotnet ef migrations add InitPulseAuth --context PulseAuthDbContext
dotnet ef database update              --context PulseAuthDbContext

Tip: you can share the same MariaDB database for both contexts; the table prefixes (AspNet_* vs PulseAuth_*) prevent collisions.


Hashing client secrets

Never store plaintext secrets. Use the helper:

var (plain, hash) = ClientSecretHelper.GenerateAndHash();
Console.WriteLine($"Secret: {plain}");   // share this with the client
Console.WriteLine($"Hash:   {hash}");    // store this in the DB

// Or hash an existing secret:
string hash = ClientSecretHelper.HashSecret("my-secret");

Custom user store

Implement IUserAuthenticationService to use any user database:

public class MyUserService : IUserAuthenticationService
{
    public Task<UserInfo?> ValidateCredentialsAsync(string user, string pass, CancellationToken ct) { ... }
    public Task<UserInfo?> GetUserByIdAsync(string subjectId, CancellationToken ct) { ... }
    public Task<UserInfo?> FindByExternalProviderAsync(string provider, string externalId, CancellationToken ct) { ... }
    public Task<UserInfo>  AutoProvisionUserAsync(string provider, string externalId, IEnumerable<Claim> claims, CancellationToken ct) { ... }
}

// Register:
builder.Services
    .AddPulseAuth(...)
    .AddUserAuthentication<MyUserService>();

React / Angular SPA — pure API mode

If your frontend is a React or Angular SPA and you want to keep all UI in the frontend (no server-rendered login pages), configure PulseAuth in pure API mode:

Email/password login

Enable the password grant on your client and call /connect/token directly from the SPA:

// Auth server Program.cs
var client = new Client
{
    ClientId          = "mundoecoa-spa",
    AllowedGrantTypes = [GrantTypes.Password, GrantTypes.RefreshToken],
    AllowOfflineAccess = true,
    AllowedScopes     = [StandardScopes.OpenId, StandardScopes.Profile,
                          StandardScopes.Email, StandardScopes.OfflineAccess, "api"],
};
// React / TypeScript
const tokens = await fetch('https://auth.myapp.com/connect/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'password',
    client_id:  'mundoecoa-spa',
    username:   email,
    password:   password,
    scope:      'openid profile email offline_access api',
  }),
}).then(r => r.json());
// tokens.access_token, tokens.refresh_token, tokens.id_token

Google Sign-In (SDK → token exchange)

  1. Add the exchange on the auth server:
builder.Services
    .AddPulseAuth(...)
    .AddGoogleTokenExchange(googleClientId: "123-xxx.apps.googleusercontent.com");
  1. Enable the grant type on the client:
AllowedGrantTypes = [GrantTypes.Password, GrantTypes.GoogleIdToken, GrantTypes.RefreshToken],
  1. From React (using @react-oauth/google or similar):
// After Google Sign-In returns credential (ID token)
const tokens = await fetch('https://auth.myapp.com/connect/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'urn:ietf:params:oauth:grant-type:google_id_token',
    client_id:  'mundoecoa-spa',
    token:      googleCredential,   // from useGoogleLogin / CredentialResponse
    scope:      'openid profile email offline_access api',
  }),
}).then(r => r.json());

Facebook Login (SDK → token exchange)

  1. Add the exchange on the auth server:
builder.Services
    .AddPulseAuth(...)
    .AddFacebookTokenExchange(appId: "123456789", appSecret: "your-secret");
  1. Enable the grant type on the client:
AllowedGrantTypes = [GrantTypes.Password, GrantTypes.FacebookAccessToken, GrantTypes.RefreshToken],
  1. From React (using react-facebook-login or the JS SDK):
// After FB.login() returns authResponse.accessToken
const tokens = await fetch('https://auth.myapp.com/connect/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'urn:ietf:params:oauth:grant-type:facebook_access_token',
    client_id:  'mundoecoa-spa',
    token:      fbAccessToken,
    scope:      'openid profile email offline_access api',
  }),
}).then(r => r.json());

Validating tokens in other microservices

Any microservice in your ecosystem can validate PulseAuth tokens using standard JWT Bearer authentication — no package dependency required:

// In any ASP.NET Core microservice
builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(opts =>
    {
        // Auth server URL — tokens are validated against its JWKS automatically
        opts.Authority = "https://auth.myapp.com";
        opts.Audience  = "mundoecoa-spa";   // or your client_id
        opts.RequireHttpsMetadata = false;  // only in dev
    });

The microservice downloads the public keys from https://auth.myapp.com/.well-known/jwks and caches them. No shared secrets, no extra packages beyond Microsoft.AspNetCore.Authentication.JwtBearer.


Login page

PulseAuth redirects to LoginPath (default /Account/Login) when the user is not authenticated. Your login page must sign the user in using ASP.NET Core Identity and then redirect back to the returnUrl parameter.

// Example Razor Page
public class LoginModel : PageModel
{
    public async Task<IActionResult> OnPostAsync(string returnUrl = "/")
    {
        var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, false, false);
        if (result.Succeeded)
            return LocalRedirect(returnUrl);

        ModelState.AddModelError(string.Empty, "Invalid login attempt.");
        return Page();
    }
}

External provider callback

After a social login, redirect the user back to the authorize endpoint:

// /Account/ExternalLoginCallback
public async Task<IActionResult> Callback(string returnUrl = "/")
{
    var info = await _signInManager.GetExternalLoginInfoAsync();
    
    // Find or auto-provision the user
    var user = await _pulseAuthUsers.FindByExternalProviderAsync(info.LoginProvider, info.ProviderKey)
            ?? await _pulseAuthUsers.AutoProvisionUserAsync(
                info.LoginProvider, info.ProviderKey, info.Principal.Claims);

    await _signInManager.SignInAsync(identityUser, isPersistent: false);
    return LocalRedirect(returnUrl);
}

💖 Support

This project is developed and maintained by Andrés Mariño. If you find this library useful, consider supporting its continued development:

  • Bitcoin (BTC): bc1p9zqgxghkjhauruhsza9n382e6kp5tpj4xtzu2csv4mypsdtdc4tqvdyg86
  • Ko-fi: Support Me

📝 License

This project is licensed under the MIT License. See the LICENSE file for details.


Made with ❤️ for the .NET community

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  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 is compatible.  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 (2)

Showing the top 2 NuGet packages that depend on PulseAuth:

Package Downloads
PulseAuth.Identity

ASP.NET Core Identity integration for PulseAuth — connects PulseAuth's user authentication to Identity's UserManager and SignInManager.

PulseAuth.EntityFramework

Entity Framework Core persistent stores for PulseAuth — clients, authorization codes, refresh tokens and grants backed by any EF Core provider.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.2.4 91 6/24/2026
1.2.3 130 6/21/2026
1.2.2 127 6/20/2026
1.1.0 134 6/18/2026
1.0.0 131 6/18/2026