From eb975bf8fc0baab59e7c2fbe89c0de0b08f091cb Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Mon, 30 Apr 2018 22:28:00 +0900 Subject: [PATCH] Isolate Bitpay's code outside of middleware inside BitpayClaimsFilter --- BTCPayServer.Tests/BTCPayServerTester.cs | 3 +- BTCPayServer.Tests/TestAccount.cs | 14 +- BTCPayServer.Tests/UnitTest1.cs | 8 +- BTCPayServer/Extensions.cs | 10 + BTCPayServer/Hosting/BTCPayServerServices.cs | 1 + BTCPayServer/Hosting/BTCpayMiddleware.cs | 158 +-------------- BTCPayServer/Security/BitpayClaimsFilter.cs | 196 +++++++++++++++++++ 7 files changed, 221 insertions(+), 169 deletions(-) create mode 100644 BTCPayServer/Security/BitpayClaimsFilter.cs diff --git a/BTCPayServer.Tests/BTCPayServerTester.cs b/BTCPayServer.Tests/BTCPayServerTester.cs index faec08309..77c5a9be1 100644 --- a/BTCPayServer.Tests/BTCPayServerTester.cs +++ b/BTCPayServer.Tests/BTCPayServerTester.cs @@ -2,6 +2,7 @@ using BTCPayServer.Hosting; using BTCPayServer.Payments; using BTCPayServer.Payments.Lightning; +using BTCPayServer.Security; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Rates; using BTCPayServer.Services.Stores; @@ -151,7 +152,7 @@ namespace BTCPayServer.Tests context.Request.Protocol = "http"; if (userId != null) { - context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) })); + context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }, Policies.CookieAuthentication)); } if(storeId != null) { diff --git a/BTCPayServer.Tests/TestAccount.cs b/BTCPayServer.Tests/TestAccount.cs index 7acf3006f..51d6840b3 100644 --- a/BTCPayServer.Tests/TestAccount.cs +++ b/BTCPayServer.Tests/TestAccount.cs @@ -44,14 +44,15 @@ namespace BTCPayServer.Tests public async Task GrantAccessAsync() { await RegisterAsync(); - var store = await CreateStoreAsync(); + await CreateStoreAsync(); + var store = this.GetController(); var pairingCode = BitPay.RequestClientAuthorization("test", Facade.Merchant); Assert.IsType(await store.RequestPairing(pairingCode.ToString())); await store.Pair(pairingCode.ToString(), StoreId); } - public StoresController CreateStore() + public void CreateStore() { - return CreateStoreAsync().GetAwaiter().GetResult(); + CreateStoreAsync().GetAwaiter().GetResult(); } public T GetController(bool setImplicitStore = true) where T : Controller @@ -59,14 +60,11 @@ namespace BTCPayServer.Tests return parent.PayTester.GetController(UserId, setImplicitStore ? StoreId : null); } - public async Task CreateStoreAsync() + public async Task CreateStoreAsync() { - var store = parent.PayTester.GetController(UserId); + var store = this.GetController(); await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" }); StoreId = store.CreatedStoreId; - var store2 = parent.PayTester.GetController(UserId); - store2.CreatedStoreId = store.CreatedStoreId; - return store2; } public BTCPayNetwork SupportedNetwork { get; set; } diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 5d7705048..b068f0cb9 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -527,13 +527,15 @@ namespace BTCPayServer.Tests tester.Start(); var acc = tester.NewAccount(); acc.Register(); - var store = acc.CreateStore(); + acc.CreateStore(); + var store = acc.GetController(); var pairingCode = acc.BitPay.RequestClientAuthorization("test", Facade.Merchant); Assert.IsType(store.Pair(pairingCode.ToString(), acc.StoreId).GetAwaiter().GetResult()); pairingCode = acc.BitPay.RequestClientAuthorization("test1", Facade.Merchant); - var store2 = acc.CreateStore(); - store2.Pair(pairingCode.ToString(), store2.CreatedStoreId).GetAwaiter().GetResult(); + acc.CreateStore(); + var store2 = acc.GetController(); + store2.Pair(pairingCode.ToString(), store2.StoreData.Id).GetAwaiter().GetResult(); Assert.Contains(nameof(PairingResult.ReusedKey), store2.StatusMessage, StringComparison.CurrentCultureIgnoreCase); } } diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs index bd98aed3c..b29cf762f 100644 --- a/BTCPayServer/Extensions.cs +++ b/BTCPayServer/Extensions.cs @@ -173,6 +173,16 @@ namespace BTCPayServer obj is bool b && b; } + public static void SetBitpayAuth(this HttpContext ctx, (string Signature, String Id, String Authorization) value) + { + NBitcoin.Extensions.TryAdd(ctx.Items, "BitpayAuth", value); + } + + public static (string Signature, String Id, String Authorization) GetBitpayAuth(this HttpContext ctx) + { + ctx.Items.TryGetValue("BitpayAuth", out object obj); + return ((string Signature, String Id, String Authorization))obj; + } public static StoreData GetStoreData(this HttpContext ctx) { diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 9f2052fbc..2c295ab28 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -119,6 +119,7 @@ namespace BTCPayServer.Hosting services.AddSingleton(); services.AddSingleton(); services.AddTransient, BTCPayClaimsFilter>(); + services.AddTransient, BitpayClaimsFilter>(); services.TryAddSingleton(); services.TryAddSingleton(o => diff --git a/BTCPayServer/Hosting/BTCpayMiddleware.cs b/BTCPayServer/Hosting/BTCpayMiddleware.cs index 091c4f90b..2f229d12d 100644 --- a/BTCPayServer/Hosting/BTCpayMiddleware.cs +++ b/BTCPayServer/Hosting/BTCpayMiddleware.cs @@ -6,45 +6,25 @@ using System.Collections.Generic; using System.Text; using System.Linq; using System.Threading.Tasks; -using NBitcoin; -using NBitcoin.Crypto; -using NBitcoin.DataEncoders; -using Microsoft.AspNetCore.Http.Internal; using System.IO; using BTCPayServer.Authentication; -using System.Security.Principal; -using NBitpayClient.Extensions; using BTCPayServer.Logging; using Newtonsoft.Json; using BTCPayServer.Models; using BTCPayServer.Configuration; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Routing; -using Microsoft.AspNetCore.Http.Extensions; -using BTCPayServer.Controllers; using System.Net.WebSockets; -using System.Security.Claims; -using BTCPayServer.Services; -using NBitpayClient; -using Newtonsoft.Json.Linq; using BTCPayServer.Services.Stores; namespace BTCPayServer.Hosting { public class BTCPayMiddleware { - TokenRepository _TokenRepository; - StoreRepository _StoreRepository; RequestDelegate _Next; BTCPayServerOptions _Options; public BTCPayMiddleware(RequestDelegate next, - TokenRepository tokenRepo, - StoreRepository storeRepo, BTCPayServerOptions options) { - _TokenRepository = tokenRepo ?? throw new ArgumentNullException(nameof(tokenRepo)); - _StoreRepository = storeRepo; _Next = next ?? throw new ArgumentNullException(nameof(next)); _Options = options ?? throw new ArgumentNullException(nameof(options)); } @@ -61,39 +41,7 @@ namespace BTCPayServer.Hosting httpContext.SetIsBitpayAPI(isBitpayAPI); if (isBitpayAPI) { - - string storeId = null; - var failedAuth = false; - if (!string.IsNullOrEmpty(bitpayAuth.Signature) && !string.IsNullOrEmpty(bitpayAuth.Id)) - { - storeId = await CheckBitId(httpContext, bitpayAuth.Signature, bitpayAuth.Id); - if (!httpContext.User.Claims.Any(c => c.Type == Claims.SIN)) - { - Logs.PayServer.LogDebug("BitId signature check failed"); - failedAuth = true; - } - } - else if (!string.IsNullOrEmpty(bitpayAuth.Authorization)) - { - storeId = await CheckLegacyAPIKey(httpContext, bitpayAuth.Authorization); - if (storeId == null) - { - Logs.PayServer.LogDebug("API key check failed"); - failedAuth = true; - } - } - - if (storeId != null) - { - var identity = ((ClaimsIdentity)httpContext.User.Identity); - identity.AddClaim(new Claim(Claims.OwnStore, storeId)); - var store = await _StoreRepository.FindStore(storeId); - httpContext.SetStoreData(store); - } - else if (failedAuth) - { - throw new BitpayHttpException(401, "Can't access to store"); - } + httpContext.SetBitpayAuth(bitpayAuth); } await _Next(httpContext); } @@ -255,109 +203,5 @@ namespace BTCPayServer.Hosting await writer.FlushAsync(); } } - - - private async Task CheckBitId(HttpContext httpContext, string sig, string id) - { - httpContext.Request.EnableRewind(); - - string storeId = null; - string body = string.Empty; - if (httpContext.Request.ContentLength != 0 && httpContext.Request.Body != null) - { - using (StreamReader reader = new StreamReader(httpContext.Request.Body, Encoding.UTF8, true, 1024, true)) - { - body = reader.ReadToEnd(); - } - httpContext.Request.Body.Position = 0; - } - - var url = httpContext.Request.GetEncodedUrl(); - try - { - var key = new PubKey(id); - if (BitIdExtensions.CheckBitIDSignature(key, sig, url, body)) - { - var sin = key.GetBitIDSIN(); - var identity = ((ClaimsIdentity)httpContext.User.Identity); - identity.AddClaim(new Claim(Claims.SIN, sin)); - - string token = null; - if (httpContext.Request.Query.TryGetValue("token", out var tokenValues)) - { - token = tokenValues[0]; - } - - if (token == null && !String.IsNullOrEmpty(body) && httpContext.Request.Method == "POST") - { - try - { - token = JObject.Parse(body)?.Property("token")?.Value?.Value(); - } - catch { } - } - - if (token != null) - { - var bitToken = await GetTokenPermissionAsync(sin, token); - if (bitToken == null) - { - return null; - } - storeId = bitToken.StoreId; - } - } - } - catch (FormatException) { } - return storeId; - } - - private async Task CheckLegacyAPIKey(HttpContext httpContext, string auth) - { - var splitted = auth.Split(' ', StringSplitOptions.RemoveEmptyEntries); - if (splitted.Length != 2 || !splitted[0].Equals("Basic", StringComparison.OrdinalIgnoreCase)) - { - return null; - } - - string apiKey = null; - try - { - apiKey = Encoders.ASCII.EncodeData(Encoders.Base64.DecodeData(splitted[1])); - } - catch - { - return null; - } - return await _TokenRepository.GetStoreIdFromAPIKey(apiKey); - } - - private async Task GetTokenPermissionAsync(string sin, string expectedToken) - { - var actualTokens = (await _TokenRepository.GetTokens(sin)).ToArray(); - actualTokens = actualTokens.SelectMany(t => GetCompatibleTokens(t)).ToArray(); - - var actualToken = actualTokens.FirstOrDefault(a => a.Value.Equals(expectedToken, StringComparison.Ordinal)); - if (expectedToken == null || actualToken == null) - { - Logs.PayServer.LogDebug($"No token found for facade {Facade.Merchant} for SIN {sin}"); - return null; - } - return actualToken; - } - - private IEnumerable GetCompatibleTokens(BitTokenEntity token) - { - if (token.Facade == Facade.Merchant.ToString()) - { - yield return token.Clone(Facade.User); - yield return token.Clone(Facade.PointOfSale); - } - if (token.Facade == Facade.PointOfSale.ToString()) - { - yield return token.Clone(Facade.User); - } - yield return token; - } } } diff --git a/BTCPayServer/Security/BitpayClaimsFilter.cs b/BTCPayServer/Security/BitpayClaimsFilter.cs new file mode 100644 index 000000000..c9c4ea489 --- /dev/null +++ b/BTCPayServer/Security/BitpayClaimsFilter.cs @@ -0,0 +1,196 @@ +using System; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Http.Extensions; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using BTCPayServer.Authentication; +using BTCPayServer.Models; +using BTCPayServer.Services; +using BTCPayServer.Services.Stores; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.Options; +using NBitcoin; +using NBitcoin.DataEncoders; +using NBitpayClient; +using NBitpayClient.Extensions; +using Newtonsoft.Json.Linq; +using BTCPayServer.Logging; +using Microsoft.AspNetCore.Http.Internal; + +namespace BTCPayServer.Security +{ + public class BitpayClaimsFilter : IAsyncAuthorizationFilter, IConfigureOptions + { + UserManager _UserManager; + StoreRepository _StoreRepository; + TokenRepository _TokenRepository; + + public BitpayClaimsFilter( + UserManager userManager, + TokenRepository tokenRepository, + StoreRepository storeRepository) + { + _UserManager = userManager; + _StoreRepository = storeRepository; + _TokenRepository = tokenRepository; + } + + void IConfigureOptions.Configure(MvcOptions options) + { + options.Filters.Add(typeof(BitpayClaimsFilter)); + } + + public async Task OnAuthorizationAsync(AuthorizationFilterContext context) + { + var principal = context.HttpContext.User; + if (context.HttpContext.GetIsBitpayAPI()) + { + var bitpayAuth = context.HttpContext.GetBitpayAuth(); + string storeId = null; + var failedAuth = false; + if (!string.IsNullOrEmpty(bitpayAuth.Signature) && !string.IsNullOrEmpty(bitpayAuth.Id)) + { + storeId = await CheckBitId(context.HttpContext, bitpayAuth.Signature, bitpayAuth.Id); + if (!context.HttpContext.User.Claims.Any(c => c.Type == Claims.SIN)) + { + Logs.PayServer.LogDebug("BitId signature check failed"); + failedAuth = true; + } + } + else if (!string.IsNullOrEmpty(bitpayAuth.Authorization)) + { + storeId = await CheckLegacyAPIKey(context.HttpContext, bitpayAuth.Authorization); + if (storeId == null) + { + Logs.PayServer.LogDebug("API key check failed"); + failedAuth = true; + } + } + + if (storeId != null) + { + var identity = ((ClaimsIdentity)context.HttpContext.User.Identity); + identity.AddClaim(new Claim(Claims.OwnStore, storeId)); + var store = await _StoreRepository.FindStore(storeId); + context.HttpContext.SetStoreData(store); + } + else if (failedAuth) + { + throw new BitpayHttpException(401, "Can't access to store"); + } + } + } + + private async Task CheckBitId(HttpContext httpContext, string sig, string id) + { + httpContext.Request.EnableRewind(); + + string storeId = null; + string body = string.Empty; + if (httpContext.Request.ContentLength != 0 && httpContext.Request.Body != null) + { + using (StreamReader reader = new StreamReader(httpContext.Request.Body, Encoding.UTF8, true, 1024, true)) + { + body = reader.ReadToEnd(); + } + httpContext.Request.Body.Position = 0; + } + + var url = httpContext.Request.GetEncodedUrl(); + try + { + var key = new PubKey(id); + if (BitIdExtensions.CheckBitIDSignature(key, sig, url, body)) + { + var sin = key.GetBitIDSIN(); + var identity = ((ClaimsIdentity)httpContext.User.Identity); + identity.AddClaim(new Claim(Claims.SIN, sin)); + + string token = null; + if (httpContext.Request.Query.TryGetValue("token", out var tokenValues)) + { + token = tokenValues[0]; + } + + if (token == null && !String.IsNullOrEmpty(body) && httpContext.Request.Method == "POST") + { + try + { + token = JObject.Parse(body)?.Property("token")?.Value?.Value(); + } + catch { } + } + + if (token != null) + { + var bitToken = await GetTokenPermissionAsync(sin, token); + if (bitToken == null) + { + return null; + } + storeId = bitToken.StoreId; + } + } + } + catch (FormatException) { } + return storeId; + } + + private async Task CheckLegacyAPIKey(HttpContext httpContext, string auth) + { + var splitted = auth.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (splitted.Length != 2 || !splitted[0].Equals("Basic", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + string apiKey = null; + try + { + apiKey = Encoders.ASCII.EncodeData(Encoders.Base64.DecodeData(splitted[1])); + } + catch + { + return null; + } + return await _TokenRepository.GetStoreIdFromAPIKey(apiKey); + } + + private async Task GetTokenPermissionAsync(string sin, string expectedToken) + { + var actualTokens = (await _TokenRepository.GetTokens(sin)).ToArray(); + actualTokens = actualTokens.SelectMany(t => GetCompatibleTokens(t)).ToArray(); + + var actualToken = actualTokens.FirstOrDefault(a => a.Value.Equals(expectedToken, StringComparison.Ordinal)); + if (expectedToken == null || actualToken == null) + { + Logs.PayServer.LogDebug($"No token found for facade {Facade.Merchant} for SIN {sin}"); + return null; + } + return actualToken; + } + + private IEnumerable GetCompatibleTokens(BitTokenEntity token) + { + if (token.Facade == Facade.Merchant.ToString()) + { + yield return token.Clone(Facade.User); + yield return token.Clone(Facade.PointOfSale); + } + if (token.Facade == Facade.PointOfSale.ToString()) + { + yield return token.Clone(Facade.User); + } + yield return token; + } + } +}