Duende IdentityServer
Authentication Library
Duende IdentityServer
Overview
Duende IdentityServer is the most flexible and standards-compliant OpenID Connect and OAuth 2.0 framework for ASP.NET Core. As of 2025, version 7.2.4 is the latest release, providing a complete standards-compliant implementation certified by the OpenID Foundation. Developed as the successor to IdentityServer4, it offers enterprise-grade authentication and authorization solutions. While a paid license is required for commercial use, a free Community Edition is available for qualifying non-profit organizations and companies.
Details
Duende IdentityServer is an authentication framework with deep flexibility to handle complex custom security scenarios. Key features include:
- Full Standards Compliance: Complete implementation of OpenID Connect 1.0 and OAuth 2.0/2.1
- ASP.NET Core Integration: Native integration with ASP.NET Core Identity
- Enterprise Features: Token Exchange, Audience Constrained Tokens, Federation support
- Flexible Deployment: Support for on-premises, cloud, Docker, Kubernetes deployments
- BFF Pattern: Backend for Frontend architecture for browser-based JavaScript applications
- Rich Integration Options: Entity Framework, external identity providers, custom stores support
Pros and Cons
Pros
- Most mature authentication solution in the .NET ecosystem
- High reliability and standards compliance with OpenID Foundation certification
- Flexible configuration and customization for complex requirements
- Enterprise-grade security features included as standard
- Comprehensive documentation and community support
- Flexible deployment options from on-premises to cloud
Cons
- Paid license required for commercial use (starting from $1,500 annually)
- Limited to .NET/ASP.NET Core ecosystem
- Complex initial setup with steep learning curve
- Excessive features for small-scale applications
- License terms require understanding and management
- Community Edition has feature limitations
Reference Pages
- Duende IdentityServer Official Site
- Duende IdentityServer Documentation
- GitHub - DuendeSoftware/IdentityServer
- NuGet - Duende.IdentityServer
Code Examples
Basic Setup and Installation
# Install NuGet packages
dotnet add package Duende.IdentityServer
dotnet add package Duende.IdentityServer.AspNetIdentity
dotnet add package Duende.IdentityServer.EntityFramework
# Create project from template
dotnet new isaspid -n MyIdentityServer
Basic Configuration (Program.cs / Startup.cs)
using Duende.IdentityServer;
using Duende.IdentityServer.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Entity Framework configuration
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// ASP.NET Core Identity configuration
builder.Services.AddDefaultIdentity<IdentityUser>(options =>
{
options.SignIn.RequireConfirmedAccount = false;
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
})
.AddEntityFrameworkStores<ApplicationDbContext>();
// IdentityServer configuration
builder.Services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
// Use proper certificates in production
options.KeyManagement.Enabled = true;
})
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
})
.AddAspNetIdentity<IdentityUser>();
var app = builder.Build();
// Pipeline configuration
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthorization();
app.MapRazorPages();
app.MapControllers();
app.Run();
Client, Resource, and Scope Configuration
public static class Config
{
public static IEnumerable<IdentityResource> IdentityResources =>
new IdentityResource[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
new IdentityResource
{
Name = "role",
UserClaims = new List<string> {"role"}
}
};
public static IEnumerable<ApiScope> ApiScopes =>
new ApiScope[]
{
new ApiScope("api1", "My API"),
new ApiScope("api2", "Another API"),
new ApiScope("read", "Read access"),
new ApiScope("write", "Write access")
};
public static IEnumerable<ApiResource> ApiResources =>
new ApiResource[]
{
new ApiResource("api1", "My API")
{
Scopes = new List<string> { "api1", "read", "write" },
UserClaims = new List<string> { "role", "email" }
}
};
public static IEnumerable<Client> Clients =>
new Client[]
{
// SPA Client (React/Angular/Vue)
new Client
{
ClientId = "spa-client",
ClientName = "SPA Client",
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
RequireClientSecret = false,
RequireConsent = false,
RedirectUris = { "https://localhost:3000/callback" },
PostLogoutRedirectUris = { "https://localhost:3000" },
AllowedCorsOrigins = { "https://localhost:3000" },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
"api1"
},
AccessTokenLifetime = 3600,
AllowAccessTokensViaBrowser = true
},
// Web Application
new Client
{
ClientId = "web-client",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.Code,
RequireConsent = false,
RedirectUris = { "https://localhost:5001/signin-oidc" },
PostLogoutRedirectUris = { "https://localhost:5001/signout-callback-oidc" },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
},
AllowOfflineAccess = true
},
// Mobile/Native App
new Client
{
ClientId = "mobile-client",
ClientName = "Mobile Client",
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
RequireClientSecret = false,
RedirectUris = { "myapp://callback" },
PostLogoutRedirectUris = { "myapp://logout" },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.OfflineAccess,
"api1"
},
AllowOfflineAccess = true,
AccessTokenLifetime = 3600,
RefreshTokenUsage = TokenUsage.ReUse
}
};
}
API Protection and Scope-based Authorization
// API Controller
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class WeatherController : ControllerBase
{
[HttpGet]
[Authorize(Policy = "ApiScope")]
public IActionResult Get()
{
var identity = HttpContext.User.Identity as ClaimsIdentity;
var claims = identity?.Claims?.Select(c => new { c.Type, c.Value }).ToArray();
return Ok(new
{
Message = "Weather data",
User = User.Identity.Name,
Claims = claims,
Scopes = User.Claims.Where(c => c.Type == "scope").Select(c => c.Value)
});
}
[HttpPost]
[Authorize(Policy = "WriteAccess")]
public IActionResult Post([FromBody] WeatherData data)
{
// Operations requiring write permission
return Ok("Weather data updated");
}
}
// API authentication setup in Program.cs
builder.Services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "https://localhost:5001";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("ApiScope", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim("scope", "api1");
});
options.AddPolicy("WriteAccess", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim("scope", "write");
});
});
Custom Profile Service
public class CustomProfileService : IProfileService
{
private readonly IUserClaimsPrincipalFactory<IdentityUser> _claimsFactory;
private readonly UserManager<IdentityUser> _userManager;
public CustomProfileService(UserManager<IdentityUser> userManager,
IUserClaimsPrincipalFactory<IdentityUser> claimsFactory)
{
_userManager = userManager;
_claimsFactory = claimsFactory;
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var sub = context.Subject.GetSubjectId();
var user = await _userManager.FindByIdAsync(sub);
var principal = await _claimsFactory.CreateAsync(user);
var claims = principal.Claims.ToList();
// Add custom claims
var roles = await _userManager.GetRolesAsync(user);
claims.AddRange(roles.Select(role => new Claim(JwtClaimTypes.Role, role)));
// Add organization info and custom properties
claims.Add(new Claim("department", "Engineering"));
claims.Add(new Claim("employee_id", user.Id));
context.IssuedClaims = claims;
}
public async Task IsActiveAsync(IsActiveContext context)
{
var sub = context.Subject.GetSubjectId();
var user = await _userManager.FindByIdAsync(sub);
context.IsActive = user != null;
}
}
// Registration in Program.cs
builder.Services.AddTransient<IProfileService, CustomProfileService>();
Token Exchange Implementation
// Token Exchange configuration
public static IEnumerable<Client> TokenExchangeClients =>
new Client[]
{
new Client
{
ClientId = "token-exchange-client",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = { "urn:ietf:params:oauth:grant-type:token-exchange" },
AllowedScopes = new List<string>
{
"api1", "api2"
},
// Token Exchange specific settings
AllowedTokenExchangeSubjectTokenTypes =
{
"urn:ietf:params:oauth:token-type:access_token"
},
AllowedTokenExchangeActorTokenTypes =
{
"urn:ietf:params:oauth:token-type:access_token"
}
}
};
// Token Exchange usage example
public class TokenExchangeService
{
private readonly HttpClient _httpClient;
public async Task<TokenResponse> ExchangeTokenAsync(string subjectToken, string audience)
{
var request = new TokenExchangeTokenRequest
{
Address = "https://localhost:5001/connect/token",
ClientId = "token-exchange-client",
ClientSecret = "secret",
SubjectToken = subjectToken,
SubjectTokenType = "urn:ietf:params:oauth:token-type:access_token",
Audience = audience,
Scope = "api2"
};
return await _httpClient.RequestTokenExchangeTokenAsync(request);
}
}
External Identity Provider Integration
// Google OAuth configuration
builder.Services.AddAuthentication()
.AddGoogle("Google", options =>
{
options.ClientId = builder.Configuration["Authentication:Google:ClientId"];
options.ClientSecret = builder.Configuration["Authentication:Google:ClientSecret"];
options.CallbackPath = "/signin-google";
options.Scope.Add("email");
options.Scope.Add("profile");
options.Events.OnCreatingTicket = context =>
{
// Custom claim processing
var email = context.Principal.FindFirst(ClaimTypes.Email)?.Value;
if (email?.EndsWith("@mycompany.com") == true)
{
context.Identity.AddClaim(new Claim("department", "internal"));
}
return Task.CompletedTask;
};
})
.AddMicrosoft("Microsoft", options =>
{
options.ClientId = builder.Configuration["Authentication:Microsoft:ClientId"];
options.ClientSecret = builder.Configuration["Authentication:Microsoft:ClientSecret"];
options.CallbackPath = "/signin-microsoft";
});