Harden auth, headers, and container per OWASP review
- Add per-account lockout + IP rate limiter on local sign-in (A07) - Emit CSP and security headers on every response (A05) - Run container as non-root `app`, /data 0700 (A05/A02) - Stop reflecting raw token-endpoint body into redirect URL (A09) - Handle missing refresh_token in connect callback without a 500 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -72,12 +72,18 @@ public static class OAuthEndpoints
|
||||
string? error_description,
|
||||
IOAuthFlowCache flowCache,
|
||||
IOptions<ClientConnectOptions> opts,
|
||||
IHttpClientFactory httpClientFactory) =>
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ILoggerFactory loggerFactory) =>
|
||||
{
|
||||
var log = loggerFactory.CreateLogger("SharepointToolbox.Web.OAuth.Connect");
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
var errMsg = Uri.EscapeDataString(error_description ?? error);
|
||||
return Results.Redirect($"/?connect_error={errMsg}");
|
||||
// The provider's verbose error_description can carry correlation/trace ids and
|
||||
// lands in the URL bar + proxy access logs. Log it server-side; surface only the
|
||||
// short, safe OAuth error code (e.g. "access_denied") to the browser.
|
||||
log.LogWarning("Connect callback returned error {Error}: {Description}", error, error_description);
|
||||
return Results.Redirect($"/?connect_error={Uri.EscapeDataString(error)}");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(code) || string.IsNullOrEmpty(state))
|
||||
@@ -107,14 +113,24 @@ public static class OAuthEndpoints
|
||||
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
var msg = Uri.EscapeDataString($"Token exchange failed: {json}");
|
||||
return Results.Redirect($"/?connect_error={msg}");
|
||||
// The raw token-endpoint body can contain trace ids / claim hints — keep it out of
|
||||
// the URL and the proxy logs. Record it server-side, redirect with a generic notice.
|
||||
log.LogWarning("Token exchange failed ({Status}): {Body}", resp.StatusCode, json);
|
||||
return Results.Redirect($"/?connect_error={Uri.EscapeDataString("Token exchange failed. Please try connecting again.")}");
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
var upn = ExtractUpnFromIdToken(root);
|
||||
var refreshToken = root.GetProperty("refresh_token").GetString()!;
|
||||
|
||||
// offline_access should yield a refresh_token; if the tenant/app withheld it the
|
||||
// session can't be persisted. Fail cleanly instead of throwing a 500 + stack trace.
|
||||
if (!root.TryGetProperty("refresh_token", out var refreshTokenEl) ||
|
||||
refreshTokenEl.GetString() is not { Length: > 0 } refreshToken)
|
||||
{
|
||||
log.LogWarning("Token response had no refresh_token for tenant {Tenant}.", flowState.TenantId);
|
||||
return Results.Redirect($"/?connect_error={Uri.EscapeDataString("Sign-in did not return a refresh token. Please try again.")}");
|
||||
}
|
||||
|
||||
var tokens = new SessionTokens
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user