fix: replace cookie-based Discord OAuth CSRF with server-side state store
- Replace __DiscordOAuthState cookie (blocked by third-party cookie policies) with in-memory DiscordOAuthStateStore singleton - State is created server-side and validated on callback, eliminating cross-site cookie transmission issues entirely - Removed CryptographicOperations dependency from Program.cs Bump version → 2.8.1 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,7 @@ builder.AddNpgsqlDataSource("gmrelaydb");
|
||||
builder.Services.AddSingleton<TelegramAuthService>();
|
||||
builder.Services.Configure<DiscordOAuthOptions>(builder.Configuration.GetSection("Discord"));
|
||||
builder.Services.AddSingleton<DiscordAuthService>();
|
||||
builder.Services.AddSingleton<DiscordOAuthStateStore>();
|
||||
builder.Services.AddSingleton<ISessionStore, SessionService>();
|
||||
builder.Services.AddScoped<AuthorizedSessionService>();
|
||||
builder.Services.AddScoped<CalendarSubscriptionService>();
|
||||
@@ -185,16 +186,9 @@ app.MapPost("/auth/logout", async (HttpContext context) =>
|
||||
});
|
||||
|
||||
// Discord OAuth endpoints
|
||||
app.MapGet("/auth/discord", (HttpContext context, DiscordAuthService discordAuth) =>
|
||||
app.MapGet("/auth/discord", (DiscordAuthService discordAuth, DiscordOAuthStateStore stateStore) =>
|
||||
{
|
||||
var state = Guid.NewGuid().ToString("N");
|
||||
context.Response.Cookies.Append("__DiscordOAuthState", state, new CookieOptions
|
||||
{
|
||||
HttpOnly = true,
|
||||
Secure = true,
|
||||
SameSite = SameSiteMode.None,
|
||||
MaxAge = TimeSpan.FromMinutes(5)
|
||||
});
|
||||
var state = stateStore.CreateState();
|
||||
var url = discordAuth.BuildAuthorizeUrl(state);
|
||||
return Results.Redirect(url);
|
||||
});
|
||||
@@ -202,23 +196,16 @@ app.MapGet("/auth/discord", (HttpContext context, DiscordAuthService discordAuth
|
||||
app.MapGet("/auth/discord/callback", async (
|
||||
HttpContext context,
|
||||
DiscordAuthService discordAuth,
|
||||
ISessionStore sessionStore,
|
||||
ILogger<Program> logger) =>
|
||||
DiscordOAuthStateStore stateStore,
|
||||
ISessionStore sessionStore) =>
|
||||
{
|
||||
var code = context.Request.Query["code"].ToString();
|
||||
var state = context.Request.Query["state"].ToString();
|
||||
var storedState = context.Request.Cookies["__DiscordOAuthState"];
|
||||
|
||||
context.Response.Cookies.Delete("__DiscordOAuthState");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(code) ||
|
||||
string.IsNullOrWhiteSpace(state) ||
|
||||
!CryptographicOperations.FixedTimeEquals(
|
||||
System.Text.Encoding.UTF8.GetBytes(state),
|
||||
System.Text.Encoding.UTF8.GetBytes(storedState ?? string.Empty)))
|
||||
!stateStore.ValidateAndRemove(state))
|
||||
{
|
||||
logger.LogWarning("Discord OAuth CSRF validation failed. code_present={CodePresent}, state_present={StatePresent}, stored_state_present={StoredStatePresent}",
|
||||
!string.IsNullOrWhiteSpace(code), !string.IsNullOrWhiteSpace(state), !string.IsNullOrWhiteSpace(storedState));
|
||||
return Results.Redirect("/login?error=auth_failed");
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace GmRelay.Web.Services;
|
||||
|
||||
public sealed class DiscordOAuthStateStore(ILogger<DiscordOAuthStateStore> logger)
|
||||
{
|
||||
private readonly System.Collections.Concurrent.ConcurrentDictionary<string, DateTime> _states = new();
|
||||
|
||||
public string CreateState()
|
||||
{
|
||||
var state = Guid.NewGuid().ToString("N");
|
||||
_states[state] = DateTime.UtcNow.AddMinutes(5);
|
||||
logger.LogDebug("Discord OAuth state created: {State}", state);
|
||||
return state;
|
||||
}
|
||||
|
||||
public bool ValidateAndRemove(string state)
|
||||
{
|
||||
if (!_states.TryRemove(state, out var expiresAt))
|
||||
{
|
||||
logger.LogWarning("Discord OAuth state not found or already used: {State}", state);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (DateTime.UtcNow > expiresAt)
|
||||
{
|
||||
logger.LogWarning("Discord OAuth state expired: {State}", state);
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.LogDebug("Discord OAuth state validated: {State}", state);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user