AspNet.Security.OpenIdConnect.Server更新后令牌失效及刷新令牌机制咨询
Hi Eduardo, sorry to hear you're hitting this token invalidation issue after updating your .NET Core project or restarting IIS—let's break down why this is happening and how to fix it for AspNet.Security.OpenIdConnect.Server 2.0.0-rc2-final.
Why This Happens
The core problem here is the default behavior of ASOS 2.0.0-rc2-final:
- It uses an in-memory store for refresh tokens, which gets wiped clean every time your app restarts (whether from updating DLLs, recycling the IIS app pool, or a full restart).
- It also generates a temporary signing key on startup—when the app restarts, this key changes, so any existing access tokens can't be validated anymore.
Together, these two issues mean all existing tokens (access and refresh) become invalid after a restart, forcing users to log in again.
Fixes to Keep Users Logged In
You'll need to address both the token storage and signing key issues. Here's how to implement each step:
1. Use a Persistent Refresh Token Store
Replace the in-memory store with a persistent one (like a database) so refresh tokens survive app restarts.
First, create a refresh token entity and DbContext:
public class RefreshToken { public int Id { get; set; } public string Token { get; set; } public string UserId { get; set; } public string ClientId { get; set; } public DateTime Expires { get; set; } } public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } public DbSet<RefreshToken> RefreshTokens { get; set; } }
Next, configure ASOS to use this persistent store by hooking into its provider events:
public void ConfigureServices(IServiceCollection services) { // Add your database context services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); // Configure ASOS services.AddOpenIdConnectServer(options => { options.TokenEndpointPath = "/connect/token"; options.AllowRefreshTokenFlow = true; // 👇 Hook into refresh token creation to save it to the database options.Provider.OnCreateRefreshToken = context => { // Generate a unique refresh token (you can use Guid or a more secure method) context.RefreshToken = Guid.NewGuid().ToString("n"); // Save the token to your database var token = new RefreshToken { Token = context.RefreshToken, UserId = context.Ticket.Principal.FindFirst(ClaimTypes.NameIdentifier).Value, ClientId = context.ClientId, Expires = context.Ticket.Properties.ExpiresUtc?.UtcDateTime ?? DateTime.UtcNow.AddDays(7) }; using (var db = context.HttpContext.RequestServices.GetRequiredService<ApplicationDbContext>()) { db.RefreshTokens.Add(token); db.SaveChanges(); } return Task.CompletedTask; }; // 👇 Validate refresh tokens against the database options.Provider.OnValidateRefreshToken = context => { using (var db = context.HttpContext.RequestServices.GetRequiredService<ApplicationDbContext>()) { var token = db.RefreshTokens.FirstOrDefault(t => t.Token == context.RefreshToken); // Reject if token doesn't exist or is expired if (token == null || token.Expires < DateTime.UtcNow) { context.Reject( error: OpenIdConnectConstants.Errors.InvalidGrant, description: "The specified refresh token is invalid or has expired."); return Task.CompletedTask; } // Validate the user and client match var user = db.Users.Find(token.UserId); if (user == null || token.ClientId != context.ClientId) { context.Reject( error: OpenIdConnectConstants.Errors.InvalidGrant, description: "The specified refresh token is invalid."); return Task.CompletedTask; } // Create a new claims principal for the user var identity = new ClaimsIdentity(context.Scheme.Name); identity.AddClaim(ClaimTypes.NameIdentifier, user.Id.ToString()); // Add any other user claims you need here context.Validate(new ClaimsPrincipal(identity)); } return Task.CompletedTask; }; // 👇 Optional: Clean up revoked refresh tokens options.Provider.OnRevokeRefreshToken = context => { using (var db = context.HttpContext.RequestServices.GetRequiredService<ApplicationDbContext>()) { var token = db.RefreshTokens.FirstOrDefault(t => t.Token == context.RefreshToken); if (token != null) { db.RefreshTokens.Remove(token); db.SaveChanges(); } } return Task.CompletedTask; }; }); }
2. Use a Fixed Signing Key
To ensure access tokens remain valid after restarts, configure a fixed signing key instead of the temporary one ASOS generates by default. You can use either a symmetric key or an X.509 certificate:
Option A: Symmetric Key (Simpler for Development/Testing)
options.SigningCredentials.AddKey(new SymmetricSecurityKey( Encoding.UTF8.GetBytes("your-32-character-or-longer-secure-secret-key-here")));
Make sure the key is long enough (at least 32 bytes for HS256) and stored securely (use environment variables or a secrets manager in production).
Option B: X.509 Certificate (Recommended for Production)
options.SigningCredentials.AddCertificate(new X509Certificate2( "path/to/your-certificate.pfx", "your-certificate-password"));
Use a valid SSL certificate (self-signed for development, trusted CA for production).
Bonus: Clean Up Expired Tokens
Add a background task or scheduled job to delete expired refresh tokens from your database regularly—this prevents your store from getting cluttered with unused tokens.
Final Notes
With these changes, when you update your DLLs or restart IIS:
- Access tokens will still be valid (since the signing key is fixed) until their expiration time.
- Refresh tokens will be retrieved from the database, so users can get new access tokens without logging in again.
内容的提问来源于stack exchange,提问作者Eduardo Tolino

