Fix PermissionTagHelper to behave in the right scope when in nav bar
What changed, and why it matters
This commit fixes how BTCPay Server's navigation bar decides which store's permissions to use when showing or hiding menu items. Previously, the permission checks inside the navigation bar could accidentally use the store from the main page (for example, the server settings page) instead of the store selected in the navigation bar. This could cause menu items to be shown or hidden incorrectly. The change makes the navigation bar temporarily switch to the correct store context while it renders, and removes a caching shortcut that could reuse the wrong permission result. There is no direct evidence in the commit message or diff that this was exploited or reported as a security vulnerability.
Treat this as a defense-in-depth UI authorization fix. Review whether any nav-bar links were previously visible to users who lacked the corresponding store permission under the old logic, and consider whether a security advisory is warranted if any sensitive action could be reached through those links. No immediate emergency patching is indicated by the supplied materials alone.
Security signals we found
Authorization scope confusion between page context and navigation component context
Removal of cached authorization results in PermissionTagHelper, which could have returned stale/wrong decisions
Addition of explicit store context switching (SwitchStoreData) for nav bar rendering
Refactoring of implicit store ID resolution into a single scope provider
No explicit security advisory, CVE, or exploit description in commit or supplied references
Evidence from the diff
The patch refactors store-scoping for ASP.NET Core authorization in the MainNav view component and PermissionTagHelper. Key changes: (1) PermissionTagHelper no longer caches AuthorizationResult per (permission, resource) in HttpContext.Items; it now calls IAuthorizationService.AuthorizeAsync every time. (2) MainNav/Default.cshtml replaces manual Get/SetStoreData with a new SwitchStoreData(Model.Store) disposable that temporarily sets BTCPAY.STOREDATA to the nav store for the duration of the nav render and restores it afterwards. (3) BuiltInPermissionScopeProvider inlines and extends the old GetImplicitStoreId logic, now checking GetCurrentStoreId first, then route/query/form/walletId sources. (4) BuiltInPermissionHandler and SetContextFilter add a per-request BTCPAY.CACHEDSTOREDATA dictionary to avoid redundant DB lookups and to keep store data consistent. (5) PermissionAuthorizationHandler now obtains the user id directly from ClaimsPrincipal instead of via UserManager. The combined effect is that permission checks evaluated inside the nav bar use the nav-selected store scope rather than the page’s store scope, preventing UI authorization inconsistencies.
Changed components
BTCPayServer.Abstractions/TagHelpers/PermissionTagHelper.csBTCPayServer/Components/MainNav/Default.cshtmlBTCPayServer/Components/MainNav/MainNav.csBTCPayServer/Security/BuiltInPermissionHandler.csBTCPayServer/Security/BuiltInPermissionScopeProvider.csBTCPayServer/Security/PermissionAuthorizationHandler.csBTCPayServer/Security/SetContextFilter.csBTCPayServer/Extensions.csBTCPayServer/Controllers/UIServerController.csBTCPayServer/Plugins/Bitpay/Security/BitpayAuthorizationHandler.csInspect captured patch +157 / −134
diff --git a/BTCPayServer.Abstractions/TagHelpers/PermissionTagHelper.cs b/BTCPayServer.Abstractions/TagHelpers/PermissionTagHelper.cs
index 18ae24c..e094844 100644
--- a/BTCPayServer.Abstractions/TagHelpers/PermissionTagHelper.cs
+++ b/BTCPayServer.Abstractions/TagHelpers/PermissionTagHelper.cs
@@ -9,17 +9,9 @@ namespace BTCPayServer.Abstractions.TagHelpers;
[HtmlTargetElement(Attributes = "[permission]")]
[HtmlTargetElement(Attributes = "[not-permission]")]
-public class PermissionTagHelper : TagHelper
+public class PermissionTagHelper(IAuthorizationService authorizationService, IHttpContextAccessor httpContextAccessor)
+ : TagHelper
{
- private readonly IAuthorizationService _authorizationService;
- private readonly IHttpContextAccessor _httpContextAccessor;
-
- public PermissionTagHelper(IAuthorizationService authorizationService, IHttpContextAccessor httpContextAccessor)
- {
- _authorizationService = authorizationService;
- _httpContextAccessor = httpContextAccessor;
- }
-
public string Permission { get; set; }
public string NotPermission { get; set; }
public string PermissionResource { get; set; }
@@ -32,10 +24,10 @@ public class PermissionTagHelper : TagHelper
if (!permissions.Any() && !notPermissions.Any())
return;
- if (_httpContextAccessor.HttpContext is null)
+ if (httpContextAccessor.HttpContext is null)
return;
- bool shouldRender = true; // Assume tag should be rendered unless a check fails
+ var shouldRender = true; // Assume tag should be rendered unless a check fails
// Process 'Permission' - User must have these permissions
if (permissions.Any())
@@ -43,8 +35,7 @@ public class PermissionTagHelper : TagHelper
bool finalResult = AndMode;
foreach (var perm in permissions)
{
- var key = $"{perm}_{PermissionResource}";
- AuthorizationResult res = await GetOrAddAuthorizationResult(key, perm);
+ var res = await Check(perm);
if (AndMode)
finalResult &= res.Succeeded;
@@ -62,8 +53,7 @@ public class PermissionTagHelper : TagHelper
{
foreach (var notPerm in notPermissions)
{
- var key = $"{notPerm}_{PermissionResource}";
- AuthorizationResult res = await GetOrAddAuthorizationResult(key, notPerm);
+ var res = await Check(notPerm);
if (res.Succeeded) // If the user has a 'NotPermission', they should not see the tag
{
@@ -79,16 +69,10 @@ public class PermissionTagHelper : TagHelper
}
}
- private async Task<AuthorizationResult> GetOrAddAuthorizationResult(string key, string permission)
+ private async Task<AuthorizationResult> Check(string permission)
{
- if (!_httpContextAccessor.HttpContext.Items.TryGetValue(key, out var cachedResult))
- {
- var res = await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User,
- PermissionResource, permission);
- _httpContextAccessor.HttpContext.Items[key] = res;
- return res;
- }
-
- return cachedResult as AuthorizationResult;
+ var res = await authorizationService.AuthorizeAsync(httpContextAccessor.HttpContext!.User,
+ PermissionResource, permission);
+ return res;
}
}
diff --git a/BTCPayServer.Tests/UtilitiesTests.cs b/BTCPayServer.Tests/UtilitiesTests.cs
index d7adf63..88e6f3c 100644
--- a/BTCPayServer.Tests/UtilitiesTests.cs
+++ b/BTCPayServer.Tests/UtilitiesTests.cs
@@ -352,7 +352,7 @@ namespace BTCPayServer.Tests
// [Trait("PreReleaseCheck", "PreReleaseCheck")]
// [Fact]
// ReSharper disable once UnusedMember.Global
- public async Task CheckDefaultTranslationsUpToDate()
+ private async Task CheckDefaultTranslationsUpToDate()
{
var soldir = TestUtils.TryGetSolutionDirectoryInfo();
var path = Path.Combine(soldir.FullName, "BTCPayServer/Services/Translations.Default.cs");
diff --git a/BTCPayServer/Components/MainNav/Default.cshtml b/BTCPayServer/Components/MainNav/Default.cshtml
index 2516fd5..476e94a 100644
--- a/BTCPayServer/Components/MainNav/Default.cshtml
+++ b/BTCPayServer/Components/MainNav/Default.cshtml
@@ -9,7 +9,6 @@
@using BTCPayServer.Configuration
@using BTCPayServer.Plugins.Emails
@using BTCPayServer.Plugins.Translations
-@inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContext;
@inject BTCPayServerOptions BtcPayServerOptions
@inject BTCPayServerEnvironment Env
@inject SignInManager<ApplicationUser> SignInManager
@@ -25,10 +24,20 @@
// from the rest of the page.
// For example, if on the Server settings page, GetStoreData should be
// null, but the navigation menu should show the last selected store.
- var oldStore = Context.GetStoreData();
- Context.SetStoreData(Context.GetNavStoreData());
+ using var _ = this.Context.SwitchStoreData(Model.Store);
}
+
+<div id="mainMenuHead">
+ <button id="mainMenuToggle" class="mainMenuButton" type="button" data-bs-toggle="offcanvas" data-bs-target="#mainNav" aria-controls="mainNav" aria-expanded="false" aria-label="Toggle navigation">
+ <span>Menu</span>
+ </button>
+ <vc:store-selector />
+ @if (SignInManager.IsSignedIn(User))
+ {
+ <component type="typeof(BTCPayServer.Blazor.NotificationsDropDown)" render-mode="ServerPrerendered" />
+ }
+</div>
<nav id="mainNav" class="d-flex flex-column justify-content-between">
<div class="accordion px-3 px-lg-4">
@if (SignInManager.IsSignedIn(User))
@@ -290,7 +299,7 @@
})()
</script>
}
- else if (Env.IsSecure(HttpContext.HttpContext))
+ else if (Env.IsSecure(Context))
{
<ul class="navbar-nav">
@if (!PoliciesSettings.LockSubscription)
@@ -441,7 +450,3 @@
if (activeEl) activeEl.scrollIntoView({ block: 'center', inline: 'center' })
})()
</script>
-
-@{
- Context.SetStoreData(oldStore);
-}
diff --git a/BTCPayServer/Components/MainNav/MainNav.cs b/BTCPayServer/Components/MainNav/MainNav.cs
index fc2e1c5..1888757 100644
--- a/BTCPayServer/Components/MainNav/MainNav.cs
+++ b/BTCPayServer/Components/MainNav/MainNav.cs
@@ -10,6 +10,7 @@ using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
+using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
@@ -25,26 +26,28 @@ namespace BTCPayServer.Components.MainNav
SettingsRepository settingsRepository,
IMemoryCache cache,
UriResolver uriResolver,
- PoliciesSettings policiesSettings)
+ PoliciesSettings policiesSettings,
+ StoreRepository storeRepository)
: ViewComponent
{
public PoliciesSettings PoliciesSettings { get; } = policiesSettings;
public async Task<IViewComponentResult> InvokeAsync()
{
- var store = ViewContext.HttpContext.GetNavStoreData();
+ var navStore = HttpContext.GetNavStoreData();
+
var serverSettings = await settingsRepository.GetSettingAsync<ServerSettings>() ?? new ServerSettings();
var vm = new MainNavViewModel
{
- Store = store,
+ Store = navStore,
ContactUrl = serverSettings.ContactUrl
};
- if (store != null)
+ if (navStore != null)
{
- var storeBlob = store.GetStoreBlob();
+ var storeBlob = navStore.GetStoreBlob();
// Wallets
- storesController.AddPaymentMethods(store, storeBlob,
+ storesController.AddPaymentMethods(navStore, storeBlob,
out var derivationSchemes, out var lightningNodes);
foreach (var lnNode in lightningNodes)
@@ -63,7 +66,7 @@ namespace BTCPayServer.Components.MainNav
entry.SetAbsoluteExpiration(TimeSpan.FromMinutes(5));
try
{
- var paymentMethodDetails = store.GetPaymentMethodConfig<LightningPaymentMethodConfig>(pmi, paymentMethodHandlerDictionary);
+ var paymentMethodDetails = navStore.GetPaymentMethodConfig<LightningPaymentMethodConfig>(pmi, paymentMethodHandlerDictionary);
await handler.GetNodeInfo(paymentMethodDetails!, null, throws: true);
// if we came here without exception, this means the node is available
return true;
@@ -82,7 +85,7 @@ namespace BTCPayServer.Components.MainNav
vm.LightningNodes = lightningNodes;
// Apps
- var apps = await appService.GetAllApps(UserId, false, store.Id, true);
+ var apps = await appService.GetAllApps(UserId, false, navStore.Id, true);
vm.Apps = apps
.Where(a => !a.Archived)
.Select(a => new StoreApp
diff --git a/BTCPayServer/Controllers/UIServerController.cs b/BTCPayServer/Controllers/UIServerController.cs
index e0a28d4..1de4ac2 100644
--- a/BTCPayServer/Controllers/UIServerController.cs
+++ b/BTCPayServer/Controllers/UIServerController.cs
@@ -368,8 +368,8 @@ namespace BTCPayServer.Controllers
if (command == "SetTemplate")
{
ModelState.Clear();
- var storeId = this.HttpContext.GetNavStoreData()?.Id;
- if (storeId is null)
+ var navStore = this.HttpContext.GetNavStoreData();
+ if (navStore is null)
{
this.TempData.SetStatusMessageModel(new()
{
@@ -379,8 +379,8 @@ namespace BTCPayServer.Controllers
}
else
{
- await _StoreRepository.SetDefaultStoreTemplate(storeId, GetUserId());
- this.TempData.SetStatusSuccess(StringLocalizer["Store template created from store '{0}'. New stores will inherit these settings.", HttpContext.GetNavStoreData().StoreName]);
+ await _StoreRepository.SetDefaultStoreTemplate(navStore.Id, GetUserId());
+ this.TempData.SetStatusSuccess(StringLocalizer["Store template created from store '{0}'. New stores will inherit these settings.", navStore.StoreName]);
}
return RedirectToAction(nameof(Policies));
}
diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs
index ce044bb..2a44807 100644
--- a/BTCPayServer/Extensions.cs
+++ b/BTCPayServer/Extensions.cs
@@ -10,6 +10,7 @@ using System.Net;
using System.Net.WebSockets;
using System.Reflection;
using System.Security.Claims;
+using System.Security.Principal;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.RegularExpressions;
@@ -741,11 +742,46 @@ namespace BTCPayServer
}
}
- public static StoreData GetNavStoreData(this HttpContext ctx)
+#nullable enable
+ public static string GetUserId(this IPrincipal? principal)
+ {
+ var claimsPrincipal = principal as ClaimsPrincipal;
+ if (claimsPrincipal is null)
+ return "";
+ return claimsPrincipal.FindFirstValue(ClaimTypes.NameIdentifier) ?? "";
+ }
+
+ public static StoreData AddCachedStoreData(this HttpContext ctx, StoreData storeData)
+ {
+ if (!ctx.Items.TryGetValue("BTCPAY.CACHEDSTOREDATA", out var item) ||
+ item is not Dictionary<string, StoreData> dictionary)
+ {
+ dictionary = new Dictionary<string, StoreData>();
+ ctx.Items["BTCPAY.CACHEDSTOREDATA"] = dictionary;
+ }
+ dictionary[storeData.Id] = storeData;
+ return storeData;
+ }
+ public static StoreData? GetCachedStoreData(this HttpContext ctx, string storeId)
+ {
+ if (!ctx.Items.TryGetValue("BTCPAY.CACHEDSTOREDATA", out var item) ||
+ item is not Dictionary<string, StoreData> dictionary)
+ return null;
+ dictionary.TryGetValue(storeId, out var storeData);
+ return storeData;
+ }
+ public static StoreData? GetNavStoreData(this HttpContext ctx)
=> ctx.Items.TryGet("BTCPAY.NAVSTOREDATA") as StoreData;
- public static void SetNavStoreData(this HttpContext ctx, StoreData storeData)
+ public static void SetNavStoreData(this HttpContext ctx, StoreData? storeData)
=> ctx.Items["BTCPAY.NAVSTOREDATA"] = storeData;
+ public static IDisposable SwitchStoreData(this HttpContext ctx, StoreData? storeData)
+ {
+ var old = ctx.GetStoreData();
+ ctx.SetStoreData(storeData);
+ return new ActionDisposable(() => { ctx.SetStoreData(old); });
+ }
+#nullable restore
public static StoreData GetStoreData(this HttpContext ctx)
=> ctx.Items.TryGet("BTCPAY.STOREDATA") as StoreData;
diff --git a/BTCPayServer/Plugins/Bitpay/Security/BitpayAuthorizationHandler.cs b/BTCPayServer/Plugins/Bitpay/Security/BitpayAuthorizationHandler.cs
index 70d1fbf..c893c2f 100644
--- a/BTCPayServer/Plugins/Bitpay/Security/BitpayAuthorizationHandler.cs
+++ b/BTCPayServer/Plugins/Bitpay/Security/BitpayAuthorizationHandler.cs
@@ -6,6 +6,7 @@ using BTCPayServer.Security;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
namespace BTCPayServer.Plugins.Bitpay.Security;
@@ -17,6 +18,9 @@ public class BitpayAuthorizationHandler(
{
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PolicyRequirement requirement)
{
+ if (httpContextAccessor.HttpContext is null)
+ return;
+ var httpContext = httpContextAccessor.HttpContext;
string storeId = null;
if (context.User.Identity is { AuthenticationType: BitpayAuthenticationTypes.ApiKeyAuthentication })
{
@@ -30,7 +34,16 @@ public class BitpayAuthorizationHandler(
}
else if (context.User.Identity is { AuthenticationType: BitpayAuthenticationTypes.Anonymous })
{
- storeId = httpContextAccessor.HttpContext.GetImplicitStoreId();
+ if (httpContext.GetRouteData().Values.TryGetValue("storeId", out var v))
+ storeId = v as string;
+
+ if (storeId == null)
+ {
+ if (httpContext.Request.Query.TryGetValue("storeId", out var sv))
+ {
+ storeId = sv.FirstOrDefault();
+ }
+ }
}
if (storeId == null)
diff --git a/BTCPayServer/Security/BuiltInPermissionHandler.cs b/BTCPayServer/Security/BuiltInPermissionHandler.cs
index 5802422..d474204 100644
--- a/BTCPayServer/Security/BuiltInPermissionHandler.cs
+++ b/BTCPayServer/Security/BuiltInPermissionHandler.cs
@@ -18,7 +18,7 @@ public class BuiltInPermissionHandler(
public const string StoreKey = "BuiltInPermissionHandler-Store";
public const string StoresKey = "BuiltInPermissionHandler-Stores";
- //TODO: In the future, we will add these store permissions to actual aspnet roles, and remove this class.
+ //TODO: In the future, we will add these store permissions to actual aspnet roles and remove this class.
private static readonly PermissionSet ServerAdminRolePermissions =
new PermissionSet(new[] { Permission.Create(Policies.CanViewStoreSettings) });
@@ -54,7 +54,10 @@ public class BuiltInPermissionHandler(
{
if (authContext.HasPermission(permContext.Permission.WithScope(store.Id), permissionService) &&
store.HasPolicy(permContext.UserId, permContext.Permission.Policy, permissionService))
+ {
permissionedStores.Add(store);
+ permContext.HttpContext.AddCachedStoreData(store);
+ }
}
success = true;
}
@@ -82,17 +85,18 @@ public class BuiltInPermissionHandler(
private async Task<StoreData?> GetStoreData(PermissionAuthorizationContext permContext, string storeId, bool isAdmin)
{
- var store = permContext.HttpContext.GetStoreData();
+ var store = permContext.HttpContext.GetCachedStoreData(storeId);
if (store is not null)
{
if (isAdmin ||
store.UserStores.Any(u => u.ApplicationUserId == permContext.UserId))
return store;
- store = null;
}
store = await storeRepository.FindStore(storeId, permContext.UserId);
if (store is null && isAdmin)
store = await storeRepository.FindStore(storeId);
+ if (store is not null)
+ permContext.HttpContext.AddCachedStoreData(store);
return store;
}
}
diff --git a/BTCPayServer/Security/BuiltInPermissionScopeProvider.cs b/BTCPayServer/Security/BuiltInPermissionScopeProvider.cs
index 9d0c6d6..e6d808e 100644
--- a/BTCPayServer/Security/BuiltInPermissionScopeProvider.cs
+++ b/BTCPayServer/Security/BuiltInPermissionScopeProvider.cs
@@ -1,6 +1,7 @@
#nullable enable
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Client;
using Microsoft.AspNetCore.Authorization;
@@ -37,8 +38,45 @@ public class BuiltInPermissionScopeProvider(
{
public async Task<string?> GetStoreId(AuthorizationHandlerContext authContext, ScopeProviderAuthorizationContext providerContext, RouteData routeData)
{
+
await using var ctx = dbContextFactory.CreateContext();
- var storeId = providerContext.HttpContext.GetImplicitStoreId();
+ var httpContext = providerContext.HttpContext;
+ var storeId = providerContext.HttpContext.GetCurrentStoreId();
+ if (storeId is null)
+ {
+ // 1. Check in the routeData
+ if (routeData.Values.TryGetValue("storeId", out var v))
+ storeId = v as string;
+
+ if (storeId == null)
+ {
+ if (httpContext.Request.Query.TryGetValue("storeId", out var sv))
+ {
+ storeId = sv.FirstOrDefault();
+ }
+ }
+
+ // 2. Check in forms
+ if (storeId == null)
+ {
+ if (httpContext.Request.HasFormContentType &&
+ httpContext.Request.Form.TryGetValue("storeId", out var sv))
+ {
+ storeId = sv.FirstOrDefault();
+ }
+ }
+
+ // 3. Checks in walletId
+ if (storeId == null)
+ {
+ if (routeData.Values.TryGetValue("walletId", out var walletId) &&
+ WalletId.TryParse(walletId as string ?? "", out var w))
+ {
+ storeId = w.StoreId;
+ }
+ }
+ }
+
List<AdditionalScope> additionalScopes = new();
foreach (var i in routeDataToStoreIds)
{
diff --git a/BTCPayServer/Security/PermissionAuthorizationHandler.cs b/BTCPayServer/Security/PermissionAuthorizationHandler.cs
index 53c764a..42f2756 100644
--- a/BTCPayServer/Security/PermissionAuthorizationHandler.cs
+++ b/BTCPayServer/Security/PermissionAuthorizationHandler.cs
@@ -2,12 +2,10 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
-using BTCPayServer.Data;
using BTCPayServer.Security.Greenfield;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Identity;
namespace BTCPayServer.Security;
@@ -15,8 +13,7 @@ public class PermissionAuthorizationHandler(
PermissionService permissionService,
IHttpContextAccessor httpContext,
IEnumerable<IPermissionHandler> permissionHandlers,
- IEnumerable<IPermissionScopeProvider> implicitScopeProviders,
- UserManager<ApplicationUser> userManager)
+ IEnumerable<IPermissionScopeProvider> implicitScopeProviders)
: AuthorizationHandler<PolicyRequirement>
{
public const string PolicyRequirementKey = nameof(PolicyRequirementKey);
@@ -24,8 +21,8 @@ public class PermissionAuthorizationHandler(
{
if (context.User.Identity is not ({ AuthenticationType: AuthenticationSchemes.Cookie } or { AuthenticationType: GreenfieldConstants.AuthenticationType }))
return;
- var userId = userManager.GetUserId(context.User);
- if (userId is null || httpContext.HttpContext is null)
+ var userId = context.User.GetUserId();
+ if (httpContext.HttpContext is null)
return;
httpContext.HttpContext.Items[PolicyRequirementKey] = requirement;
diff --git a/BTCPayServer/Security/SecurityExtensions.cs b/BTCPayServer/Security/SecurityExtensions.cs
deleted file mode 100644
index a213e8c..0000000
--- a/BTCPayServer/Security/SecurityExtensions.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-using System;
-using System.Linq;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Routing;
-
-namespace BTCPayServer.Security
-{
- public static class SecurityExtensions
- {
-
- public static string GetImplicitStoreId(this HttpContext httpContext)
- {
- // 1. Check in the routeData
- var routeData = httpContext.GetRouteData();
- string storeId = null;
- if (routeData != null)
- {
- if (routeData.Values.TryGetValue("storeId", out var v))
- storeId = v as string;
- }
-
- if (storeId == null)
- {
- if (httpContext.Request.Query.TryGetValue("storeId", out var sv))
- {
- storeId = sv.FirstOrDefault();
- }
- }
-
- // 2. Check in forms
- if (storeId == null)
- {
- if (httpContext.Request.HasFormContentType &&
- httpContext.Request.Form != null &&
- httpContext.Request.Form.TryGetValue("storeId", out var sv))
- {
- storeId = sv.FirstOrDefault();
- }
- }
-
- // 3. Checks in walletId
- if (storeId == null && routeData != null)
- {
- if (routeData.Values.TryGetValue("walletId", out var walletId) &&
- WalletId.TryParse((string)walletId, out var w))
- {
- storeId = w.StoreId;
- }
- }
-
- return storeId;
- }
- }
-}
diff --git a/BTCPayServer/Security/SetContextFilter.cs b/BTCPayServer/Security/SetContextFilter.cs
index c2c77a4..94f1912 100644
--- a/BTCPayServer/Security/SetContextFilter.cs
+++ b/BTCPayServer/Security/SetContextFilter.cs
@@ -14,31 +14,37 @@ namespace BTCPayServer.Security;
public class SetContextFilter(
PaymentRequestRepository paymentRequestRepository,
- StoreRepository storeRepository,
InvoiceRepository invoiceRepository,
AppService appService,
- UserManager<ApplicationUser> userManager) : IAsyncActionFilter
+ StoreRepository storeRepository) : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var httpContext = context.HttpContext;
- var userId = userManager.GetUserId(context.HttpContext.User) ?? "??";
+ var userId = context.HttpContext.User.GetUserId();
var isCookie = context.HttpContext.User.Identity is { AuthenticationType: AuthenticationSchemes.Cookie };
+
if (httpContext.Items.TryGetValue(BuiltInPermissionHandler.StoreKey, out var oo) && oo is StoreData store)
{
httpContext.SetStoreData(store);
if (isCookie)
httpContext.SetNavStoreData(store);
}
- else if (isCookie && httpContext.GetUserPrefsCookie()?.CurrentStoreId is { } preferredStoreId)
+ else if (isCookie && httpContext.GetUserPrefsCookie()?.CurrentStoreId is string preferredStoreId)
{
- httpContext.SetNavStoreData(await storeRepository.FindStore(preferredStoreId, userId));
+ var nav = httpContext.GetCachedStoreData(preferredStoreId);
+ if (nav is null)
+ {
+ nav = await storeRepository.FindStore(preferredStoreId, httpContext.User.GetUserId());
+ if (nav is not null)
+ httpContext.AddCachedStoreData(nav);
+ }
+ httpContext.SetNavStoreData(nav);
}
if (httpContext.Items.TryGetValue(BuiltInPermissionHandler.StoresKey, out var ooo) && ooo is StoreData[] stores)
- {
httpContext.SetStoresData(stores);
- }
+
if (httpContext.Items.TryGetValue(BuiltInPermissionScopeProvider.AdditionalScopeKey, out var o) && o is IEnumerable<BuiltInPermissionScopeProvider.AdditionalScope> additionalScopes)
{
foreach (var additionalScope in additionalScopes)
diff --git a/BTCPayServer/Services/Stores/StoreRepository.cs b/BTCPayServer/Services/Stores/StoreRepository.cs
index d057fed..80e039d 100644
--- a/BTCPayServer/Services/Stores/StoreRepository.cs
+++ b/BTCPayServer/Services/Stores/StoreRepository.cs
@@ -53,6 +53,9 @@ namespace BTCPayServer.Services.Stores
public async Task<StoreData?> FindStore(string storeId, string userId)
{
ArgumentNullException.ThrowIfNull(userId);
+ if (string.IsNullOrEmpty(storeId) ||
+ string.IsNullOrEmpty(userId) || userId == "???")
+ return null;
await using var ctx = _ContextFactory.CreateContext();
return await ctx
.UserStore
diff --git a/BTCPayServer/Views/Shared/_Layout.cshtml b/BTCPayServer/Views/Shared/_Layout.cshtml
index c667f1b..1cd11c0 100644
--- a/BTCPayServer/Views/Shared/_Layout.cshtml
+++ b/BTCPayServer/Views/Shared/_Layout.cshtml
@@ -2,7 +2,6 @@
@using BTCPayServer.Components.MainNav
@inject Microsoft.AspNetCore.Http.IHttpContextAccessor _context;
@inject BTCPayServer.Services.BTCPayServerEnvironment _env
-@inject SignInManager<ApplicationUser> _signInManager
@inject UserManager<ApplicationUser> _userManager
@{
@@ -20,16 +19,6 @@
</head>
<body class="d-flex flex-column flex-lg-row min-vh-100">
<header id="mainMenu" class="btcpay-header d-flex flex-column d-print-none" v-pre>
- <div id="mainMenuHead">
- <button id="mainMenuToggle" class="mainMenuButton" type="button" data-bs-toggle="offcanvas" data-bs-target="#mainNav" aria-controls="mainNav" aria-expanded="false" aria-label="Toggle navigation">
- <span>Menu</span>
- </button>
- <vc:store-selector />
- @if (_signInManager.IsSignedIn(User))
- {
- <component type="typeof(BTCPayServer.Blazor.NotificationsDropDown)" render-mode="ServerPrerendered" />
- }
- </div>
<vc:main-nav />
</header>
<template id="badUrl">
@@ -54,7 +43,7 @@
<section>
@RenderBody()
</section>
-
+
<partial name="_Footer"/>
<partial name="LayoutFoot"/>
Why this scored 49/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.