divan/GanjooRazor/Utils/GanjoorSessionChecker.cs
Hamid Reza Mohammadi af75cc18d9 xss vulnerability fix
2026-09-25 17:46:22 +03:30

197 lines
8.7 KiB
C#

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Newtonsoft.Json;
using RMuseum.Models.Auth.Memory;
using RMuseum.Models.Auth.ViewModel;
using RSecurityBackend.Models.Auth.Memory;
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
namespace GanjooRazor.Utils
{
/// <summary>
/// Ganjoor Session Checker
/// </summary>
public class GanjoorSessionChecker
{
/// <summary>
/// if user is logged in adds user token to <paramref name="secureClient"/> and then checks user session and if needs renewal, renews it
/// </summary>
/// <param name="secureClient"></param>
/// <param name="request"></param>
/// <param name="response"></param>
/// <returns></returns>
public static async Task<bool> PrepareClient(HttpClient secureClient, HttpRequest request, HttpResponse response)
{
if (string.IsNullOrEmpty(request.Cookies["Token"]) || string.IsNullOrEmpty(request.Cookies["SessionId"]))
return false;
secureClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", request.Cookies["Token"]);
var r = await secureClient.GetAsync($"{APIRoot.Url}/api/users/checkmysession/?sessionId={request.Cookies["SessionId"]}");
if (r.StatusCode == HttpStatusCode.OK)
{
return true;
}
else
if(r.StatusCode == HttpStatusCode.Unauthorized)
{
var newToken = await TryReloginAsync(request, response);
if (string.IsNullOrEmpty(newToken))
{
return false;
}
secureClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", newToken);
return true;
}
return false;
}
/// <summary>
/// Calls /api/users/relogin/{sessionId} to get a fresh token for an expired/invalidated session,
/// and - if successful - writes the refreshed session cookies (Token, SessionId, UserId, ...) to
/// <paramref name="response"/>, exactly the way a fresh login would.
///
/// This is the single place that knows how to recover from a 401 caused by token expiration; both
/// <see cref="PrepareClient(HttpClient, HttpRequest, HttpResponse)"/> (for the up-front session
/// check) and <see cref="GanjoorReloginHandler"/> (for any individual authorized API call that
/// unexpectedly gets a 401) call this same method so the relogin/cookie-refresh logic exists and
/// is fixed in exactly one place.
/// </summary>
/// <param name="request"></param>
/// <param name="response"></param>
/// <returns>the new bearer token on success, or null if the relogin attempt itself failed (e.g. the session is truly gone)</returns>
public static async Task<string> TryReloginAsync(HttpRequest request, HttpResponse response)
{
if (string.IsNullOrEmpty(request.Cookies["SessionId"]))
return null;
using (HttpClient reloginClient = new HttpClient())
{
if (!string.IsNullOrEmpty(request.Cookies["Token"]))
{
reloginClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", request.Cookies["Token"]);
}
var reLoginUrl = $"{APIRoot.Url}/api/users/relogin/{request.Cookies["SessionId"]}";
var reLoginResponse = await reloginClient.PutAsync(reLoginUrl, null);
if (reLoginResponse.StatusCode != HttpStatusCode.OK)
{
return null;
}
LoggedOnUserModelEx loggedOnUser = JsonConvert.DeserializeObject<LoggedOnUserModelEx>(await reLoginResponse.Content.ReadAsStringAsync());
// Authentication-related cookies are never read by client-side JavaScript
// (only server-side C# reads Request.Cookies[...]), so they can safely be
// marked HttpOnly to stop them being exfiltrated via document.cookie in the
// event of an XSS bug. Secure/SameSite=Lax provide additional defense-in-depth.
var cookieOption = new CookieOptions()
{
Expires = DateTime.Now.AddDays(365),
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Lax,
};
response.Cookies.Append("UserId", loggedOnUser.User.Id.ToString(), cookieOption);
response.Cookies.Append("SessionId", loggedOnUser.SessionId.ToString(), cookieOption);
response.Cookies.Append("Token", loggedOnUser.Token, cookieOption);
response.Cookies.Append("Username", loggedOnUser.User.Username, cookieOption);
response.Cookies.Append("Name", $"{loggedOnUser.User.FirstName} {loggedOnUser.User.SurName}", cookieOption);
response.Cookies.Append("NickName", $"{loggedOnUser.User.NickName}", cookieOption);
bool canEditContent = false;
var ganjoorEntity = loggedOnUser.SecurableItem.Where(s => s.ShortName == RMuseumSecurableItem.GanjoorEntityShortName).SingleOrDefault();
if (ganjoorEntity != null)
{
var op = ganjoorEntity.Operations.Where(o => o.ShortName == SecurableItem.ModifyOperationShortName).SingleOrDefault();
if (op != null)
{
canEditContent = op.Status;
}
}
response.Cookies.Append("CanEdit", canEditContent.ToString(), cookieOption);
return loggedOnUser.Token;
}
}
/// <summary>
/// has permission
/// </summary>
/// <param name="request"></param>
/// <param name="secuableShortName"></param>
/// <param name="operationShortName"></param>
/// <returns></returns>
public static async Task<bool> IsPermitted(HttpRequest request, HttpResponse response, string secuableShortName, string operationShortName)
{
using (HttpClient secureClient = new HttpClient(new GanjoorReloginHandler(request, response)))
{
if (await PrepareClient(secureClient, request, response))
{
var res = await secureClient.GetAsync($"{APIRoot.Url}/api/users/securableitems");
if (!res.IsSuccessStatusCode)
{
return false;
}
SecurableItem[] secuarbleItems = JsonConvert.DeserializeObject<SecurableItem[]>(await res.Content.ReadAsStringAsync());
var secuarbleItem = secuarbleItems.Where(s => s.ShortName == secuableShortName).SingleOrDefault();
if (secuarbleItem == null)
return false;
var operation = secuarbleItem.Operations.Where(o => o.ShortName == operationShortName).SingleOrDefault();
if (operation == null)
return false;
return operation.Status;
}
else
{
return false;
}
}
}
/// <summary>
/// apply permissions to view data
/// </summary>
/// <param name="request"></param>
/// <param name="viewData"></param>
public static async Task ApplyPermissionsToViewData(HttpRequest request, HttpResponse response, ViewDataDictionary viewData)
{
using (HttpClient secureClient = new HttpClient(new GanjoorReloginHandler(request, response)))
{
if (await PrepareClient(secureClient, request, response))
{
var res = await secureClient.GetAsync($"{APIRoot.Url}/api/users/securableitems");
if (!res.IsSuccessStatusCode)
{
return;
}
SecurableItem[] secuarbleItems = JsonConvert.DeserializeObject<SecurableItem[]>(await res.Content.ReadAsStringAsync());
foreach(SecurableItem securableItem in secuarbleItems)
foreach(SecurableItemOperation operation in securableItem.Operations)
if(operation.Status)
viewData[$"{securableItem.ShortName}-{operation.ShortName}"] = true;
}
}
}
}
}