Make sure that we don't authenticate call with bitpay auth methods on non bitpay calls

This commit is contained in:
nicolas.dorier 2018-04-29 20:32:43 +09:00
parent 2848caff2e
commit f0145142a4
5 changed files with 127 additions and 55 deletions

View file

@ -12,6 +12,7 @@ using System.Threading.Tasks;
namespace BTCPayServer.Controllers
{
[BitpayAPIConstraint]
public class AccessTokenController : Controller
{
TokenRepository _TokenRepository;

View file

@ -22,17 +22,14 @@ namespace BTCPayServer.Controllers
{
private InvoiceController _InvoiceController;
private InvoiceRepository _InvoiceRepository;
private StoreRepository _StoreRepository;
private BTCPayNetworkProvider _NetworkProvider;
public InvoiceControllerAPI(InvoiceController invoiceController,
InvoiceRepository invoceRepository,
StoreRepository storeRepository,
BTCPayNetworkProvider networkProvider)
{
this._InvoiceController = invoiceController;
this._InvoiceRepository = invoceRepository;
this._StoreRepository = storeRepository;
this._NetworkProvider = networkProvider;
}
@ -41,20 +38,14 @@ namespace BTCPayServer.Controllers
[MediaTypeConstraint("application/json")]
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] Invoice invoice)
{
var store = await _StoreRepository.FindStore(this.User.GetStoreId());
if (store == null)
throw new BitpayHttpException(401, "Can't access to store");
return await _InvoiceController.CreateInvoiceCore(invoice, store, HttpContext.Request.GetAbsoluteRoot());
return await _InvoiceController.CreateInvoiceCore(invoice, HttpContext.GetStoreData(), HttpContext.Request.GetAbsoluteRoot());
}
[HttpGet]
[Route("invoices/{id}")]
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id, string token)
{
var store = await _StoreRepository.FindStore(this.User.GetStoreId());
if (store == null)
throw new BitpayHttpException(401, "Can't access to store");
var invoice = await _InvoiceRepository.GetInvoice(store.Id, id);
var invoice = await _InvoiceRepository.GetInvoice(HttpContext.GetStoreData().Id, id);
if (invoice == null)
throw new BitpayHttpException(404, "Object not found");
var resp = invoice.EntityToDTO(_NetworkProvider);
@ -75,10 +66,7 @@ namespace BTCPayServer.Controllers
{
if (dateEnd != null)
dateEnd = dateEnd.Value + TimeSpan.FromDays(1); //Should include the end day
var store = await _StoreRepository.FindStore(this.User.GetStoreId());
if (store == null)
throw new BitpayHttpException(401, "Can't access to store");
var query = new InvoiceQuery()
{
Count = limit,
@ -88,10 +76,9 @@ namespace BTCPayServer.Controllers
OrderId = orderId,
ItemCode = itemCode,
Status = status == null ? null : new[] { status },
StoreId = new[] { store.Id }
StoreId = new[] { this.HttpContext.GetStoreData().Id }
};
var entities = (await _InvoiceRepository.GetInvoices(query))
.Select((o) => o.EntityToDTO(_NetworkProvider)).ToArray();

View file

@ -30,6 +30,7 @@ using BTCPayServer.Models;
using System.Security.Claims;
using System.Globalization;
using BTCPayServer.Services;
using BTCPayServer.Data;
namespace BTCPayServer
{
@ -153,6 +154,26 @@ namespace BTCPayServer
return principal.Claims.Where(c => c.Type == Claims.OwnStore).Select(c => c.Value).FirstOrDefault();
}
public static void SetIsBitpayAPI(this HttpContext ctx, bool value)
{
NBitcoin.Extensions.TryAdd(ctx.Items, "IsBitpayAPI", value);
}
public static bool GetIsBitpayAPI(this HttpContext ctx)
{
return ctx.Items.TryGetValue("IsBitpayAPI", out object obj) &&
obj is bool b && b;
}
public static StoreData GetStoreData(this HttpContext ctx)
{
return ctx.Items.TryGet("BTCPAY.STOREDATA") as StoreData;
}
public static void SetStoreData(this HttpContext ctx, StoreData storeData)
{
ctx.Items["BTCPAY.STOREDATA"] = storeData;
}
private static JsonSerializerSettings jsonSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() };
public static string ToJson(this object o)
{

View file

@ -43,11 +43,7 @@ namespace BTCPayServer.Filters
public bool Accept(ActionConstraintContext context)
{
var hasVersion = context.RouteContext.HttpContext.Request.Headers["x-accept-version"].Where(h => h == "2.0.0").Any();
var isBitpayAPI =
context.RouteContext.HttpContext.Items.TryGetValue("IsBitpayAPI", out object obj) &&
obj is bool b && b;
return (hasVersion || isBitpayAPI) == IsBitpayAPI;
return context.RouteContext.HttpContext.GetIsBitpayAPI() == IsBitpayAPI;
}
}

View file

@ -27,20 +27,24 @@ 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));
}
@ -49,27 +53,48 @@ namespace BTCPayServer.Hosting
public async Task Invoke(HttpContext httpContext)
{
RewriteHostIfNeeded(httpContext);
httpContext.Request.Headers.TryGetValue("x-signature", out StringValues values);
var sig = values.FirstOrDefault();
httpContext.Request.Headers.TryGetValue("x-identity", out values);
var id = values.FirstOrDefault();
httpContext.Request.Headers.TryGetValue("Authorization", out values);
var auth = values.FirstOrDefault();
try
{
bool isBitId = false;
if (!string.IsNullOrEmpty(sig) && !string.IsNullOrEmpty(id))
var bitpayAuth = GetBitpayAuth(httpContext, out bool isBitpayAuth);
var isBitpayAPI = IsBitpayAPI(httpContext, isBitpayAuth);
httpContext.SetIsBitpayAPI(isBitpayAPI);
if (isBitpayAPI)
{
await HandleBitId(httpContext, sig, id);
isBitId = httpContext.User.HasClaim(c => c.Type == Claims.SIN);
if (!isBitId)
Logs.PayServer.LogDebug("BitId signature check failed");
}
if (!isBitId && !string.IsNullOrEmpty(auth))
{
await HandleLegacyAPIKey(httpContext, auth);
}
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");
}
}
await _Next(httpContext);
}
catch (WebSocketException)
@ -89,6 +114,55 @@ namespace BTCPayServer.Hosting
}
}
private static (string Signature, String Id, String Authorization) GetBitpayAuth(HttpContext httpContext, out bool hasBitpayAuth)
{
httpContext.Request.Headers.TryGetValue("x-signature", out StringValues values);
var sig = values.FirstOrDefault();
httpContext.Request.Headers.TryGetValue("x-identity", out values);
var id = values.FirstOrDefault();
httpContext.Request.Headers.TryGetValue("Authorization", out values);
var auth = values.FirstOrDefault();
hasBitpayAuth = auth != null || (sig != null && id != null);
return (sig, id, auth);
}
private bool IsBitpayAPI(HttpContext httpContext, bool bitpayAuth)
{
if (!httpContext.Request.Path.HasValue)
return false;
var path = httpContext.Request.Path.Value;
if (
bitpayAuth &&
path == "/invoices" &&
httpContext.Request.Method == "POST" &&
(httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
return true;
if (
bitpayAuth &&
path == "/invoices" &&
httpContext.Request.Method == "GET")
return true;
if (
bitpayAuth &&
path.StartsWith("/invoices/", StringComparison.OrdinalIgnoreCase) &&
httpContext.Request.Method == "GET")
return true;
if (path.Equals("/rates", StringComparison.OrdinalIgnoreCase) &&
httpContext.Request.Method == "GET")
return true;
if (
path.Equals("/tokens", StringComparison.Ordinal) &&
( httpContext.Request.Method == "GET" || httpContext.Request.Method == "POST"))
return true;
return false;
}
private void RewriteHostIfNeeded(HttpContext httpContext)
{
string reverseProxyScheme = null;
@ -183,10 +257,11 @@ namespace BTCPayServer.Hosting
}
private async Task HandleBitId(HttpContext httpContext, string sig, string id)
private async Task<string> 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)
{
@ -227,23 +302,22 @@ namespace BTCPayServer.Hosting
var bitToken = await GetTokenPermissionAsync(sin, token);
if (bitToken == null)
{
throw new BitpayHttpException(401, $"This endpoint does not support this facade");
return null;
}
identity.AddClaim(new Claim(Claims.OwnStore, bitToken.StoreId));
storeId = bitToken.StoreId;
}
Logs.PayServer.LogDebug($"BitId signature check success for SIN {sin}");
NBitcoin.Extensions.TryAdd(httpContext.Items, "IsBitpayAPI", true);
}
}
catch (FormatException) { }
return storeId;
}
private async Task HandleLegacyAPIKey(HttpContext httpContext, string auth)
private async Task<string> CheckLegacyAPIKey(HttpContext httpContext, string auth)
{
var splitted = auth.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (splitted.Length != 2 || !splitted[0].Equals("Basic", StringComparison.OrdinalIgnoreCase))
{
throw new BitpayHttpException(401, $"Invalid Authorization header");
return null;
}
string apiKey = null;
@ -253,16 +327,9 @@ namespace BTCPayServer.Hosting
}
catch
{
throw new BitpayHttpException(401, $"Invalid Authorization header");
return null;
}
var storeId = await _TokenRepository.GetStoreIdFromAPIKey(apiKey);
if (storeId == null)
{
throw new BitpayHttpException(401, $"Invalid Authorization header");
}
var identity = ((ClaimsIdentity)httpContext.User.Identity);
identity.AddClaim(new Claim(Claims.OwnStore, storeId));
NBitcoin.Extensions.TryAdd(httpContext.Items, "IsBitpayAPI", true);
return await _TokenRepository.GetStoreIdFromAPIKey(apiKey);
}
private async Task<BitTokenEntity> GetTokenPermissionAsync(string sin, string expectedToken)