# Duende Software Samples This repository contains official sample applications demonstrating the implementation of Duende IdentityServer and Duende BFF (Backend for Frontend) security frameworks. Duende IdentityServer is an OpenID Connect and OAuth 2.0 framework for ASP.NET Core that enables secure authentication, single sign-on (SSO), and API access control. The samples cover various authentication scenarios including client credentials flow, authorization code flow with PKCE, token management, DPoP (Demonstrating Proof of Possession), mTLS (mutual TLS), token exchange, and personal access tokens. Duende BFF provides a security pattern for JavaScript-based frontend applications (SPAs) that keeps access tokens on the server-side, protecting them from browser-based attacks. The BFF samples demonstrate integration with various frontend frameworks including Angular, React, Vue, and Blazor WebAssembly, showing how to implement secure authentication while proxying API calls through a backend host that manages tokens automatically. ## IdentityServer Setup and Configuration The core IdentityServer configuration involves registering services, defining API scopes, identity resources, and clients. This is the foundation for all OAuth 2.0 and OpenID Connect flows. ```csharp // IdentityServer/v7/Quickstarts/1_ClientCredentials/src/IdentityServer/HostingExtensions.cs using Serilog; namespace IdentityServer; internal static class HostingExtensions { public static WebApplication ConfigureServices(this WebApplicationBuilder builder) { builder.Services.AddIdentityServer() .AddInMemoryApiScopes(Config.ApiScopes) .AddInMemoryClients(Config.Clients); return builder.Build(); } public static WebApplication ConfigurePipeline(this WebApplication app) { app.UseSerilogRequestLogging(); if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseIdentityServer(); return app; } } // IdentityServer/v7/Quickstarts/1_ClientCredentials/src/IdentityServer/Config.cs using Duende.IdentityServer.Models; namespace IdentityServer; public static class Config { public static IEnumerable ApiScopes => new ApiScope[] { new ApiScope(name: "api1", displayName: "My API") }; public static IEnumerable Clients => new Client[] { new Client { ClientId = "client", AllowedGrantTypes = GrantTypes.ClientCredentials, ClientSecrets = { new Secret("secret".Sha256()) }, AllowedScopes = { "api1" } } }; } ``` ## Client Credentials Flow Machine-to-machine authentication using the client credentials grant type. This flow is used when applications need to access protected APIs without user interaction. ```csharp // IdentityServer/v7/Quickstarts/1_ClientCredentials/src/Client/Program.cs using System.Text.Json; using Duende.IdentityModel.Client; // discover endpoints from metadata var client = new HttpClient(); var disco = await client.GetDiscoveryDocumentAsync("https://localhost:5001"); if (disco.IsError) { Console.WriteLine(disco.Error); return 1; } // request token var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest { Address = disco.TokenEndpoint, ClientId = "client", ClientSecret = "secret", Scope = "api1" }); if (tokenResponse.IsError) { Console.WriteLine(tokenResponse.Error); return 1; } Console.WriteLine(tokenResponse.AccessToken); // call api with token var apiClient = new HttpClient(); apiClient.SetBearerToken(tokenResponse.AccessToken!); var response = await apiClient.GetAsync("https://localhost:6001/identity"); if (!response.IsSuccessStatusCode) { Console.WriteLine(response.StatusCode); return 1; } var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement; Console.WriteLine(JsonSerializer.Serialize(doc, new JsonSerializerOptions { WriteIndented = true })); ``` ## Protected API with JWT Bearer Authentication Setting up an API that validates JWT access tokens issued by IdentityServer. The API uses scope-based authorization policies. ```csharp // IdentityServer/v7/Quickstarts/1_ClientCredentials/src/Api/Program.cs using System.Security.Claims; var builder = WebApplication.CreateBuilder(args); builder.Services.AddAuthentication() .AddJwtBearer(options => { options.Authority = "https://localhost:5001"; options.TokenValidationParameters.ValidateAudience = false; }); builder.Services.AddAuthorization(options => { options.AddPolicy("ApiScope", policy => { policy.RequireAuthenticatedUser(); policy.RequireClaim("scope", "api1"); }); }); var app = builder.Build(); app.UseHttpsRedirection(); app.MapGet("identity", (ClaimsPrincipal user) => user.Claims.Select(c => new { c.Type, c.Value })) .RequireAuthorization("ApiScope"); app.Run(); ``` ## Interactive Web Application with OpenID Connect Implementing user authentication in ASP.NET Core web applications using the authorization code flow with PKCE. ```csharp // IdentityServer/v7/Quickstarts/2_InteractiveAspNetCore/src/WebClient/Program.cs using Microsoft.AspNetCore.Authentication; var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorPages(); builder.Services.AddAuthentication(options => { options.DefaultScheme = "Cookies"; options.DefaultChallengeScheme = "oidc"; }) .AddCookie("Cookies") .AddOpenIdConnect("oidc", options => { options.Authority = "https://localhost:5001"; options.ClientId = "web"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("verification"); options.ClaimActions.MapJsonKey("email_verified", "email_verified"); options.GetClaimsFromUserInfoEndpoint = true; options.MapInboundClaims = false; options.SaveTokens = true; }); var app = builder.Build(); app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.MapRazorPages().RequireAuthorization(); app.Run(); ``` ## Interactive Client Configuration Configuring an interactive OpenID Connect client in IdentityServer with authorization code flow, redirect URIs, and allowed scopes. ```csharp // IdentityServer/v7/Quickstarts/2_InteractiveAspNetCore/src/IdentityServer/Config.cs using Duende.IdentityServer; using Duende.IdentityServer.Models; using Duende.IdentityModel; public static class Config { public static IEnumerable IdentityResources => new IdentityResource[] { new IdentityResources.OpenId(), new IdentityResources.Profile(), new IdentityResource() { Name = "verification", UserClaims = new List { JwtClaimTypes.Email, JwtClaimTypes.EmailVerified } } }; public static IEnumerable ApiScopes => new ApiScope[] { new ApiScope(name: "api1", displayName: "My API") }; public static IEnumerable Clients => new Client[] { // Machine-to-machine client new Client { ClientId = "client", AllowedGrantTypes = GrantTypes.ClientCredentials, ClientSecrets = { new Secret("secret".Sha256()) }, AllowedScopes = { "api1" } }, // Interactive ASP.NET Core Web App new Client { ClientId = "web", ClientSecrets = { new Secret("secret".Sha256()) }, AllowedGrantTypes = GrantTypes.Code, RedirectUris = { "https://localhost:5002/signin-oidc" }, PostLogoutRedirectUris = { "https://localhost:5002/signout-callback-oidc" }, AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, "verification", "api1" } } }; } ``` ## ASP.NET Identity Integration Integrating IdentityServer with ASP.NET Core Identity for user management, including registration, login, and password management. ```csharp // IdentityServer/v7/AspNetIdentity/IdentityServerAspNetIdentity/Program.cs using Duende.IdentityServer.Models; using IdentityServerAspNetIdentity.Data; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); builder.Services.AddDbContext(options => options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection"))); builder.Services.AddDatabaseDeveloperPageExceptionFilter(); builder.Services.AddDefaultIdentity(options => options.SignIn.RequireConfirmedAccount = true) .AddEntityFrameworkStores(); builder.Services.AddRazorPages(); builder.Services.AddIdentityServer() .AddInMemoryClients([ new Client { ClientId = "client", AllowedGrantTypes = GrantTypes.Implicit, RedirectUris = { "https://localhost:5002/signin-oidc" }, PostLogoutRedirectUris = { "https://localhost:5002/signout-callback-oidc" }, FrontChannelLogoutUri = "https://localhost:5002/signout-oidc", AllowedScopes = { "openid", "profile", "email", "phone" } } ]) .AddInMemoryIdentityResources([ new IdentityResources.OpenId(), new IdentityResources.Profile(), new IdentityResources.Email(), new IdentityResources.Phone(), ]) .AddAspNetIdentity(); var app = builder.Build(); app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseIdentityServer(); app.UseAuthorization(); app.MapRazorPages(); app.Run(); ``` ## Automatic Token Management Managing access token refresh automatically with the OpenID Connect Access Token Management library. Tokens are refreshed transparently when they expire. ```csharp // IdentityServer/v7/Basics/MvcTokenManagement/src/Program.cs using Duende.AccessTokenManagement.OpenIdConnect; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.IdentityModel.Tokens; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllersWithViews(); builder.Services.AddHttpClient(); builder.Services.AddAuthentication(options => { options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = "oidc"; }) .AddCookie(options => { options.Cookie.Name = "mvc"; options.Events.OnSigningOut = async e => { // automatically revoke refresh token at signout time await e.HttpContext.RevokeRefreshTokenAsync(); }; }) .AddOpenIdConnect("oidc", options => { options.Authority = "https://localhost:5001"; options.ClientId = "interactive.mvc.sample.short.token.lifetime"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.UsePkce = true; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("scope1"); options.Scope.Add("offline_access"); options.GetClaimsFromUserInfoEndpoint = true; options.SaveTokens = true; options.MapInboundClaims = false; options.TokenValidationParameters = new TokenValidationParameters { NameClaimType = "name", RoleClaimType = "role" }; }); // add automatic token management builder.Services.AddOpenIdConnectAccessTokenManagement(); // add HTTP client to call protected API with managed tokens builder.Services.AddUserAccessTokenHttpClient("client", configureClient: client => { client.BaseAddress = new Uri("https://localhost:5005"); }); var app = builder.Build(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.MapDefaultControllerRoute().RequireAuthorization(); app.Run(); ``` ## DPoP (Demonstrating Proof of Possession) Implementing DPoP for sender-constrained access tokens, preventing token theft and replay attacks. The client generates a proof-of-possession key pair. ```csharp // IdentityServer/v7/DPoP/ClientCredentials/Program.cs using System.Security.Cryptography; using System.Text.Json; using Duende.AccessTokenManagement; using Duende.AccessTokenManagement.DPoP; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Tokens; public class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) { var host = Host.CreateDefaultBuilder(args) .ConfigureServices((services) => { services.AddDistributedMemoryCache(); services.AddClientCredentialsTokenManagement() .AddClient("dpop", client => { client.TokenEndpoint = new Uri("https://localhost:5001/connect/token"); client.ClientId = ClientId.Parse("dpop"); client.ClientSecret = ClientSecret.Parse("905e4892-7610-44cb-a122-6209b38c882f"); client.Scope = Scope.Parse("scope1"); client.DPoPJsonWebKey = DPoPProofKey.Parse(CreateDPoPKey()); }); services.AddClientCredentialsHttpClient("client", ClientCredentialsClientName.Parse("dpop"), client => { client.BaseAddress = new Uri("https://localhost:5005/"); }); services.AddHostedService(); }); return host; } private static string CreateDPoPKey() { var key = new RsaSecurityKey(RSA.Create(2048)); var jwk = JsonWebKeyConverter.ConvertFromRSASecurityKey(key); jwk.Alg = "PS256"; return JsonSerializer.Serialize(jwk); } } // DPoP-enabled API // IdentityServer/v7/DPoP/Api/Program.cs using Duende.AspNetCore.Authentication.JwtBearer.DPoP; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddDistributedMemoryCache(); builder.Services.AddKeyedHybridCache(ServiceProviderKeys.ProofTokenReplayHybridCache); builder.Services.AddAuthentication("token") .AddJwtBearer("token", options => { options.Authority = "https://localhost:5001"; options.TokenValidationParameters.ValidateAudience = false; options.MapInboundClaims = false; options.TokenValidationParameters.ValidTypes = ["at+jwt"]; }); // layers DPoP onto the "token" scheme builder.Services.ConfigureDPoPTokensForScheme("token", opt => { opt.ProofTokenExpirationMode = DPoPProofExpirationMode.IssuedAt; }); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers().RequireAuthorization(); app.Run(); ``` ## Token Exchange Implementing RFC 8693 Token Exchange for delegation and impersonation scenarios. A downstream service can exchange tokens to act on behalf of the original caller. ```csharp // IdentityServer/v7/TokenExchange/IdentityServerHost/TokenExchangeGrantValidator.cs using System.Security.Claims; using System.Text.Json; using Duende.IdentityServer; using Duende.IdentityServer.Models; using Duende.IdentityServer.Validation; using Duende.IdentityModel; public class TokenExchangeGrantValidator : IExtensionGrantValidator { private readonly ITokenValidator _validator; public TokenExchangeGrantValidator(ITokenValidator validator) { _validator = validator; } public async Task ValidateAsync(ExtensionGrantValidationContext context) { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest); var customResponse = new Dictionary { {OidcConstants.TokenResponse.IssuedTokenType, OidcConstants.TokenTypeIdentifiers.AccessToken} }; var subjectToken = context.Request.Raw.Get(OidcConstants.TokenRequest.SubjectToken); var subjectTokenType = context.Request.Raw.Get(OidcConstants.TokenRequest.SubjectTokenType); if (string.IsNullOrWhiteSpace(subjectToken)) return; if (!string.Equals(subjectTokenType, OidcConstants.TokenTypeIdentifiers.AccessToken)) return; var validationResult = await _validator.ValidateAccessTokenAsync(subjectToken); if (validationResult.IsError) return; var sub = validationResult.Claims.First(c => c.Type == JwtClaimTypes.Subject).Value; var clientId = validationResult.Claims.First(c => c.Type == JwtClaimTypes.ClientId).Value; var style = context.Request.Raw.Get("exchange_style"); if (style == "impersonation") { context.Request.ClientId = clientId; context.Result = new GrantValidationResult( subject: sub, authenticationMethod: GrantType, customResponse: customResponse); } else if (style == "delegation") { context.Request.ClientId = clientId; var actor = new { client_id = context.Request.Client.ClientId }; var actClaim = new Claim(JwtClaimTypes.Actor, JsonSerializer.Serialize(actor), IdentityServerConstants.ClaimValueTypes.Json); context.Result = new GrantValidationResult( subject: sub, authenticationMethod: GrantType, claims: new[] { actClaim }, customResponse: customResponse); } } public string GrantType => OidcConstants.GrantTypes.TokenExchange; } // Token Exchange Client // IdentityServer/v7/TokenExchange/Client/Program.cs using Duende.IdentityModel; using Duende.IdentityModel.Client; var client = new HttpClient(); var disco = await client.GetDiscoveryDocumentAsync("https://localhost:5001"); // Get initial token var initialResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest { Address = disco.TokenEndpoint, ClientId = "front.end", ClientSecret = "secret", Scope = "scope1", }); // Exchange token with delegation var exchangeResponse = await client.RequestTokenExchangeTokenAsync(new TokenExchangeTokenRequest { Address = disco.TokenEndpoint, ClientId = "api1", ClientSecret = "secret", SubjectToken = initialResponse.AccessToken, SubjectTokenType = OidcConstants.TokenTypeIdentifiers.AccessToken, Scope = "scope2", Parameters = { { "exchange_style", "delegation" } } }); Console.WriteLine(exchangeResponse.AccessToken); ``` ## mTLS Client Authentication Configuring mutual TLS for client authentication using X.509 certificates instead of client secrets. ```csharp // IdentityServer/v7/MTLS/IdentityServerHost/Clients.cs using System.Security.Cryptography.X509Certificates; using Duende.IdentityServer; using Duende.IdentityServer.Models; public static class Clients { private static X509Certificate2 ClientCert() => X509CertificateLoader.LoadPkcs12FromFile("../localhost-client.p12", "changeit"); private static string ClientCertificateSubject() => ClientCert().Subject; public static IEnumerable List => new[] { new Client { ClientId = "mtls", ClientSecrets = { new Secret(ClientCertificateSubject()) { Type = IdentityServerConstants.SecretTypes.X509CertificateName } }, AllowedGrantTypes = GrantTypes.CodeAndClientCredentials, RedirectUris = { "https://localhost:44301/signin-oidc" }, FrontChannelLogoutUri = "https://localhost:44301/signout-oidc", PostLogoutRedirectUris = { "https://localhost:44301/signout-callback-oidc" }, AllowOfflineAccess = true, AllowedScopes = { "openid", "profile", "scope1" } }, }; } ``` ## Reference Tokens with Introspection API configuration supporting both JWT and reference tokens, using OAuth 2.0 Token Introspection for opaque tokens. ```csharp // IdentityServer/v7/PAT/Api/Program.cs var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddDistributedMemoryCache(); builder.Services.AddAuthentication("token") // JWT tokens .AddJwtBearer("token", options => { options.Authority = "https://localhost:5001"; options.Audience = "api1"; options.TokenValidationParameters.ValidTypes = new[] { "at+jwt" }; // if token does not contain a dot, it is a reference token options.ForwardDefaultSelector = Selector.ForwardReferenceToken("introspection"); }) // reference tokens via introspection .AddOAuth2Introspection("introspection", options => { options.Authority = "https://localhost:5001"; options.ClientId = "api1"; options.ClientSecret = "secret"; }); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers().RequireAuthorization(); app.Run(); ``` ## BFF (Backend for Frontend) Setup Setting up the BFF pattern for secure JavaScript SPA authentication. The BFF hosts the frontend, manages authentication, and proxies API calls. ```csharp // BFF/v4/JsBffSample/FrontendHost/Program.cs using Duende.Bff; using Duende.Bff.Yarp; using Microsoft.AspNetCore.DataProtection; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddBff() .AddRemoteApis() .ConfigureOpenIdConnect(options => { options.Authority = "https://demo.duendesoftware.com"; options.ClientId = "interactive.confidential"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.ResponseMode = "query"; options.GetClaimsFromUserInfoEndpoint = true; options.MapInboundClaims = false; options.SaveTokens = true; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("api"); options.Scope.Add("offline_access"); options.TokenValidationParameters = new() { NameClaimType = "name", RoleClaimType = "role" }; }) .ConfigureCookies(options => { options.Cookie.Name = "__Host-bff"; options.Cookie.SameSite = SameSiteMode.Strict; }); builder.Services.AddAuthentication(options => { options.DefaultScheme = BffAuthenticationSchemes.BffCookie; options.DefaultChallengeScheme = BffAuthenticationSchemes.BffOpenIdConnect; options.DefaultSignOutScheme = BffAuthenticationSchemes.BffOpenIdConnect; }); // HTTP client that uses the managed user access token builder.Services.AddUserAccessTokenHttpClient("api_client", configureClient: client => { client.BaseAddress = new Uri("https://localhost:5002/"); }); builder.Services.AddDataProtection() .SetApplicationName("BFF"); var app = builder.Build(); app.UseDefaultFiles(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseBff(); app.UseAuthorization(); // BFF management endpoints (login, logout, user) app.MapBffManagementEndpoints(); // Local API endpoints protected by BFF app.MapControllers() .RequireAuthorization() .AsBffApiEndpoint(); // Remote API proxy with token forwarding // app.MapRemoteBffApiEndpoint("/todos", "https://localhost:5020/todos") // .RequireAccessToken(Duende.Bff.TokenType.User); app.Run(); ``` ## BFF Local API Controller Implementing a local API endpoint within the BFF host that is protected by cookie authentication and CSRF protection. ```csharp // BFF/v4/JsBffSample/FrontendHost/ToDoController.cs using Microsoft.AspNetCore.Mvc; public class ToDoController : ControllerBase { private static readonly List __data = new List() { new ToDo { Id = ToDo.NewId(), Date = DateTimeOffset.UtcNow, Name = "Demo ToDo API", User = "bob" }, new ToDo { Id = ToDo.NewId(), Date = DateTimeOffset.UtcNow.AddHours(1), Name = "Stop Demo", User = "bob" }, }; [HttpGet("todos")] public IActionResult GetAll() => Ok(__data.AsEnumerable()); [HttpGet("todos/{id}")] public IActionResult Get(int id) { var item = __data.FirstOrDefault(x => x.Id == id); if (item == null) return NotFound(); return Ok(item); } [HttpPost("todos")] public IActionResult Post([FromBody] ToDo model) { model.Id = ToDo.NewId(); model.User = $"{User.FindFirst("sub").Value} ({User.FindFirst("name").Value})"; __data.Add(model); return Created(Url.Action(nameof(Get), new { id = model.Id }), model); } [HttpPut("todos/{id}")] public IActionResult Put(int id, [FromBody] ToDo model) { var item = __data.FirstOrDefault(x => x.Id == id); if (item == null) return NotFound(); item.Date = model.Date; item.Name = model.Name; return NoContent(); } [HttpDelete("todos/{id}")] public IActionResult Delete(int id) { var item = __data.FirstOrDefault(x => x.Id == id); if (item == null) return NotFound(); __data.Remove(item); return NoContent(); } } public class ToDo { static int _nextId = 1; public static int NewId() => _nextId++; public int Id { get; set; } public DateTimeOffset Date { get; set; } public string Name { get; set; } public string User { get; set; } } ``` ## BFF with Blazor WebAssembly Integrating BFF with Blazor WebAssembly for secure client-side authentication with server-side token management. ```csharp // BFF/v4/BlazorWasm/BFF/Program.cs (Server) using Duende.Bff; using Duende.Bff.Yarp; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.DataProtection; var builder = WebApplication.CreateBuilder(args); builder.Services.AddAuthorization(); builder.Services.AddCascadingAuthenticationState(); builder.Services .AddBff() .AddRemoteApis() .ConfigureOpenIdConnect(options => { options.Authority = "https://demo.duendesoftware.com"; options.ClientId = "interactive.confidential"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("api"); options.Scope.Add("offline_access"); options.MapInboundClaims = false; options.ClaimActions.MapAll(); options.GetClaimsFromUserInfoEndpoint = true; options.SaveTokens = true; options.TokenValidationParameters.NameClaimType = "name"; options.TokenValidationParameters.RoleClaimType = "role"; }) .ConfigureCookies(options => { options.Cookie.Name = "__Host-blazor"; options.Cookie.SameSite = SameSiteMode.Strict; }); builder.Services.AddDataProtection() .SetApplicationName("BFF"); var app = builder.Build(); app.UseAuthentication(); app.UseBff(); app.UseAuthorization(); app.MapBffManagementEndpoints(); app.MapStaticAssets(); // Local API endpoint app.MapGet("/api/data", async () => { var json = await File.ReadAllTextAsync("weather.json"); return Results.Content(json, "application/json"); }).RequireAuthorization().AsBffApiEndpoint(); // Remote API proxy app.MapRemoteBffApiEndpoint("/remoteapi", new Uri("https://demo.duendesoftware.com/api")) .WithAccessToken(); app.MapFallbackToFile("index.html"); app.Run(); // BFF/v4/BlazorWasm/BlazorWasm/Program.cs (Client) using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Duende.Bff.Blazor.Client; var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add("#app"); builder.RootComponents.Add("head::after"); builder.Services.AddTransient(sp => { var client = new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }; client.DefaultRequestHeaders.Add("X-CSRF", "1"); // CSRF protection header return client; }); builder.Services.AddBffBlazorClient(); await builder.Build().RunAsync(); ``` ## JavaScript BFF API Calls Example of calling BFF-protected APIs from JavaScript with CSRF protection headers. ```javascript // BFF/v4/JsBffSample/FrontendHost/wwwroot/app.js // All API calls must include the X-CSRF header for CSRF protection // Get current user session async function getUser() { const response = await fetch('/bff/user', { headers: { 'X-CSRF': '1' } }); if (response.ok) { return await response.json(); } return null; } // Call local BFF API async function getTodos() { const response = await fetch('/todos', { headers: { 'X-CSRF': '1' } }); return await response.json(); } // Create a new todo async function createTodo(todo) { const response = await fetch('/todos', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF': '1' }, body: JSON.stringify(todo) }); return await response.json(); } // Delete a todo async function deleteTodo(id) { await fetch(`/todos/${id}`, { method: 'DELETE', headers: { 'X-CSRF': '1' } }); } // Login redirect function login() { window.location.href = '/bff/login'; } // Logout redirect function logout() { window.location.href = '/bff/logout'; } ``` ## Summary The Duende Software samples repository demonstrates comprehensive OAuth 2.0 and OpenID Connect implementations for .NET applications. The IdentityServer samples cover essential authentication scenarios from basic client credentials flow to advanced patterns like DPoP, mTLS, token exchange, and reference tokens. The samples are organized progressively through quickstarts, making it easy to learn foundational concepts before exploring advanced security features. The BFF samples show how to secure modern JavaScript frontends (Angular, React, Vue, Blazor) by keeping access tokens server-side and using secure, HttpOnly cookies for session management. This pattern protects against XSS token theft and simplifies frontend code by handling token refresh automatically. The integration examples demonstrate both local API endpoints within the BFF host and proxying to remote APIs with automatic token attachment, providing a complete security solution for single-page applications communicating with protected backend services.