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 DivanRazor.Utils { /// /// Divan Session Checker /// public class DivanSessionChecker { /// /// if user is logged in adds user token to and then checks user session and if needs renewal, renews it /// /// /// /// /// public static async Task 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; } /// /// 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 /// , 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 /// (for the up-front session /// check) and (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. /// /// /// /// the new bearer token on success, or null if the relogin attempt itself failed (e.g. the session is truly gone) public static async Task 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(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 divanEntity = loggedOnUser.SecurableItem.Where(s => s.ShortName == RMuseumSecurableItem.DivanEntityShortName).SingleOrDefault(); if (divanEntity != null) { var op = divanEntity.Operations.Where(o => o.ShortName == SecurableItem.ModifyOperationShortName).SingleOrDefault(); if (op != null) { canEditContent = op.Status; } } response.Cookies.Append("CanEdit", canEditContent.ToString(), cookieOption); return loggedOnUser.Token; } } /// /// has permission /// /// /// /// /// public static async Task IsPermitted(HttpRequest request, HttpResponse response, string secuableShortName, string operationShortName) { using (HttpClient secureClient = new HttpClient(new DivanReloginHandler(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(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; } } } /// /// apply permissions to view data /// /// /// public static async Task ApplyPermissionsToViewData(HttpRequest request, HttpResponse response, ViewDataDictionary viewData) { using (HttpClient secureClient = new HttpClient(new DivanReloginHandler(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(await res.Content.ReadAsStringAsync()); foreach(SecurableItem securableItem in secuarbleItems) foreach(SecurableItemOperation operation in securableItem.Operations) if(operation.Status) viewData[$"{securableItem.ShortName}-{operation.ShortName}"] = true; } } } } }