PulseAuth 1.1.0
See the version list below for details.
dotnet add package PulseAuth --version 1.1.0
NuGet\Install-Package PulseAuth -Version 1.1.0
<PackageReference Include="PulseAuth" Version="1.1.0" />
<PackageVersion Include="PulseAuth" Version="1.1.0" />
<PackageReference Include="PulseAuth" />
paket add PulseAuth --version 1.1.0
#r "nuget: PulseAuth, 1.1.0"
#:package PulseAuth@1.1.0
#addin nuget:?package=PulseAuth&version=1.1.0
#tool nuget:?package=PulseAuth&version=1.1.0

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 # optional, for production
2. Configure Program.cs
using PulseAuth.Constants;
using PulseAuth.Extensions;
using PulseAuth.Builders;
using PulseAuth.Helpers;
using PulseAuth.Models;
var builder = WebApplication.CreateBuilder(args);
// ── ASP.NET Core Identity ────────────────────────────────────────────────────
builder.Services
.AddDbContext<ApplicationDbContext>(opts =>
opts.UseSqlServer(builder.Configuration.GetConnectionString("Default")))
.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
// ── PulseAuth ────────────────────────────────────────────────────────────────
builder.Services
.AddPulseAuth(opts =>
{
opts.Issuer = "https://auth.myapp.com";
opts.LoginPath = "/Account/Login";
})
// Signing key — use AddDeveloperSigningCredential() for dev,
// replace with a persistent key service for production
.AddDeveloperSigningCredential()
// In-memory clients — swap for .AddEntityFrameworkStores() in production
.AddInMemoryClients(new[]
{
new Client
{
ClientId = "my-spa",
ClientName = "My SPA",
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
AllowOfflineAccess = true,
RedirectUris = ["https://myapp.com/callback"],
PostLogoutRedirectUris = ["https://myapp.com/"],
AllowedScopes = ["openid", "profile", "email", "offline_access"],
},
new Client
{
ClientId = "api-service",
ClientName = "Backend API",
ClientSecretHash = ClientSecretHelper.HashSecret("super-secret"),
AllowedGrantTypes = GrantTypes.ClientCredentialsOnly,
AllowedScopes = ["api"],
},
})
// Connect to ASP.NET Core Identity for user management
.AddIdentityUsers<ApplicationUser>()
// Cookie auth for the interactive session
.AddCookieAuthentication();
// ── External providers (optional) ────────────────────────────────────────────
builder.Services
.AddPulseAuth() // returns the existing builder
.AddGoogle(
clientId: builder.Configuration["Auth:Google:ClientId"]!,
clientSecret: builder.Configuration["Auth:Google:ClientSecret"]!)
.AddFacebook(
appId: builder.Configuration["Auth:Facebook:AppId"]!,
appSecret: builder.Configuration["Auth:Facebook:AppSecret"]!)
.AddGitHub(
clientId: builder.Configuration["Auth:GitHub:ClientId"]!,
clientSecret: builder.Configuration["Auth:GitHub:ClientSecret"]!)
.AddMicrosoft(
clientId: builder.Configuration["Auth:Microsoft:ClientId"]!,
clientSecret: builder.Configuration["Auth:Microsoft:ClientSecret"]!);
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
// Map all PulseAuth endpoints
app.MapPulseAuth();
app.Run();
3. (Production) Switch to persistent stores
builder.Services
.AddPulseAuth(opts => opts.Issuer = "https://auth.myapp.com")
.AddEntityFrameworkStores(opts =>
opts.UseSqlServer(builder.Configuration.GetConnectionString("Default")))
.AddIdentityUsers<ApplicationUser>();
Apply migrations:
dotnet ef migrations add InitPulseAuth --context PulseAuthDbContext
dotnet ef database update
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)
- Add the exchange on the auth server:
builder.Services
.AddPulseAuth(...)
.AddGoogleTokenExchange(googleClientId: "123-xxx.apps.googleusercontent.com");
- Enable the grant type on the client:
AllowedGrantTypes = [GrantTypes.Password, GrantTypes.GoogleIdToken, GrantTypes.RefreshToken],
- From React (using
@react-oauth/googleor 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)
- Add the exchange on the auth server:
builder.Services
.AddPulseAuth(...)
.AddFacebookTokenExchange(appId: "123456789", appSecret: "your-secret");
- Enable the grant type on the client:
AllowedGrantTypes = [GrantTypes.Password, GrantTypes.FacebookAccessToken, GrantTypes.RefreshToken],
- From React (using
react-facebook-loginor 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:
📝 License
This project is licensed under the MIT License. See the LICENSE file for details.
Made with ❤️ for the .NET community
| Product | Versions 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. |
-
net10.0
- Microsoft.AspNetCore.Authentication.Facebook (>= 10.0.9)
- Microsoft.AspNetCore.Authentication.Google (>= 10.0.9)
- Microsoft.AspNetCore.Authentication.MicrosoftAccount (>= 10.0.9)
- Microsoft.IdentityModel.Protocols.OpenIdConnect (>= 8.19.1)
- Microsoft.IdentityModel.Tokens (>= 8.19.1)
- System.IdentityModel.Tokens.Jwt (>= 8.19.1)
-
net8.0
- Microsoft.AspNetCore.Authentication.Facebook (>= 8.0.28)
- Microsoft.AspNetCore.Authentication.Google (>= 8.0.28)
- Microsoft.AspNetCore.Authentication.MicrosoftAccount (>= 8.0.28)
- Microsoft.IdentityModel.Protocols.OpenIdConnect (>= 8.19.1)
- Microsoft.IdentityModel.Tokens (>= 8.19.1)
- System.IdentityModel.Tokens.Jwt (>= 8.19.1)
-
net9.0
- Microsoft.AspNetCore.Authentication.Facebook (>= 9.0.17)
- Microsoft.AspNetCore.Authentication.Google (>= 9.0.17)
- Microsoft.AspNetCore.Authentication.MicrosoftAccount (>= 9.0.17)
- Microsoft.IdentityModel.Protocols.OpenIdConnect (>= 8.19.1)
- Microsoft.IdentityModel.Tokens (>= 8.19.1)
- System.IdentityModel.Tokens.Jwt (>= 8.19.1)
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.