ASP.NET Core utilities for JWT authentication and SPA hosting with domain-based routing. Use when building web apps needing JWT tokens, multi-SPA hosting, or development-mode controller filtering.
Astrolabe.Web.Common provides utilities for .NET web projects, focusing on JWT authentication and SPA (Single Page Application) virtual hosting with domain-based routing.
When to use: Use this library when building ASP.NET Core applications that need JWT token authentication, multi-SPA hosting, or development-mode controller filtering.
Package: Astrolabe.Web.Common
Dependencies: ASP.NET Core, Microsoft.AspNetCore.Authentication.JwtBearer
Target Framework: .NET 7-8
Simplified JWT token generation and validation using BasicJwtToken and extension methods that configure ASP.NET Core authentication.
Host multiple Single Page Applications in one ASP.NET Core app with intelligent routing based on:
Mark controllers that should only be available during development using [DevMode] attribute, automatically hidden in production.
using Astrolabe.Web.Common;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
// 1. Create JWT token parameters
var jwtSecretKey = Encoding.UTF8.GetBytes(builder.Configuration["Jwt:SecretKey"] ?? throw new Exception("JWT secret key not configured"));
var jwtIssuer = builder.Configuration["Jwt:Issuer"] ?? "MyApp";
var jwtAudience = builder.Configuration["Jwt:Audience"] ?? "MyApp";
var jwtToken = new BasicJwtToken(jwtSecretKey, jwtIssuer, jwtAudience);
// 2. Configure authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(jwtToken.ConfigureJwtBearer());
builder.Services.AddAuthorization();
var app = builder.Build();
// 3. Use authentication middleware
app.UseAuthentication();
app.UseAuthorization();
app.Run();
using Astrolabe.Web.Common;
using System.Security.Claims;
// Create token generator
var jwtToken = new BasicJwtToken(secretKey, issuer, audience);
var tokenGenerator = jwtToken.MakeTokenSigner();
// Generate token with claims
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Email),
new Claim(ClaimTypes.Role, user.Role)
};
// Token expires in 1 hour (3600 seconds)
string token = tokenGenerator(claims, 3600);
// Return to client
return Ok(new { Token = token });
using Astrolabe.Web.Common;
var app = builder.Build();
// Main app at example.com
app.UseDomainSpa(app.Environment, "main", fallback: true);
// Admin panel at admin.example.com
app.UseDomainSpa(app.Environment, "admin");
// Dashboard at dashboard.example.com
app.UseDomainSpa(app.Environment, "dashboard");
app.Run();
Directory Structure:
ClientApp/
└── sites/
├── main/ # Main SPA files
├── admin/ # Admin SPA files
└── dashboard/ # Dashboard SPA files
using Astrolabe.Web.Common;
using Microsoft.AspNetCore.Mvc;
// This controller is only available in development mode
[DevMode]
[ApiController]
[Route("api/dev")]
public class DevToolsController : ControllerBase
{
[HttpGet("clear-cache")]
public IActionResult ClearCache()
{
// Development-only endpoint
return Ok("Cache cleared");
}
[HttpGet("seed-data")]
public async Task<IActionResult> SeedTestData()
{
// Seed test data for development
return Ok("Data seeded");
}
}
using Astrolabe.Web.Common;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(options =>
{
// Hide controllers marked with [DevMode] in production
if (!builder.Environment.IsDevelopment())
{
options.Conventions.Add(new HideDevModeControllersConvention());
}
});
var app = builder.Build();
app.Run();
// ✅ DO - Store secret keys in configuration/secrets
var secretKey = Encoding.UTF8.GetBytes(builder.Configuration["Jwt:SecretKey"]!);
// ❌ DON'T - Hardcode secret keys
var secretKey = Encoding.UTF8.GetBytes("my-secret-key-123");
// ✅ DO - Set reasonable expiration times
var token = tokenGenerator(claims, 3600); // 1 hour for web
var refreshToken = tokenGenerator(claims, 604800); // 7 days for refresh
// ❌ DON'T - Use excessively long or short expirations
var token = tokenGenerator(claims, 31536000); // 1 year - too long
var token = tokenGenerator(claims, 60); // 1 minute - too short
// ✅ DO - Most specific routes first, fallback last
app.UseDomainSpa(app.Environment, "admin");
app.UseDomainSpa(app.Environment, "dashboard", pathString: "/dashboard");
app.UseDomainSpa(app.Environment, "main", fallback: true); // Last!
// ❌ DON'T - Put fallback first (it will catch everything)
app.UseDomainSpa(app.Environment, "main", fallback: true);
app.UseDomainSpa(app.Environment, "admin"); // Never reached!
Issue: 401 Unauthorized on all requests
app.UseAuthentication() comes before app.UseAuthorization() and before app.MapControllers()Issue: SPA not loading / 404 errors
ClientApp/sites/{siteDir} exists and contains built SPA filesIssue: All requests going to wrong SPA
Issue: JWT tokens invalid across restarts
Astrolabe.Web.Common/Astrolabe.Web.Common.csprojAstrolabe.Web.Common