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 namespace BTCPayServer.Controllers
{ {
[BitpayAPIConstraint]
public class AccessTokenController : Controller public class AccessTokenController : Controller
{ {
TokenRepository _TokenRepository; TokenRepository _TokenRepository;

View file

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

View file

@ -30,6 +30,7 @@ using BTCPayServer.Models;
using System.Security.Claims; using System.Security.Claims;
using System.Globalization; using System.Globalization;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Data;
namespace BTCPayServer namespace BTCPayServer
{ {
@ -153,6 +154,26 @@ namespace BTCPayServer
return principal.Claims.Where(c => c.Type == Claims.OwnStore).Select(c => c.Value).FirstOrDefault(); 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() }; private static JsonSerializerSettings jsonSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() };
public static string ToJson(this object o) public static string ToJson(this object o)
{ {

View file

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

View file

@ -27,20 +27,24 @@ using System.Security.Claims;
using BTCPayServer.Services; using BTCPayServer.Services;
using NBitpayClient; using NBitpayClient;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using BTCPayServer.Services.Stores;
namespace BTCPayServer.Hosting namespace BTCPayServer.Hosting
{ {
public class BTCPayMiddleware public class BTCPayMiddleware
{ {
TokenRepository _TokenRepository; TokenRepository _TokenRepository;
StoreRepository _StoreRepository;
RequestDelegate _Next; RequestDelegate _Next;
BTCPayServerOptions _Options; BTCPayServerOptions _Options;
public BTCPayMiddleware(RequestDelegate next, public BTCPayMiddleware(RequestDelegate next,
TokenRepository tokenRepo, TokenRepository tokenRepo,
StoreRepository storeRepo,
BTCPayServerOptions options) BTCPayServerOptions options)
{ {
_TokenRepository = tokenRepo ?? throw new ArgumentNullException(nameof(tokenRepo)); _TokenRepository = tokenRepo ?? throw new ArgumentNullException(nameof(tokenRepo));
_StoreRepository = storeRepo;
_Next = next ?? throw new ArgumentNullException(nameof(next)); _Next = next ?? throw new ArgumentNullException(nameof(next));
_Options = options ?? throw new ArgumentNullException(nameof(options)); _Options = options ?? throw new ArgumentNullException(nameof(options));
} }
@ -49,27 +53,48 @@ namespace BTCPayServer.Hosting
public async Task Invoke(HttpContext httpContext) public async Task Invoke(HttpContext httpContext)
{ {
RewriteHostIfNeeded(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 try
{ {
bool isBitId = false; var bitpayAuth = GetBitpayAuth(httpContext, out bool isBitpayAuth);
if (!string.IsNullOrEmpty(sig) && !string.IsNullOrEmpty(id)) 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); await _Next(httpContext);
} }
catch (WebSocketException) 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) private void RewriteHostIfNeeded(HttpContext httpContext)
{ {
string reverseProxyScheme = null; 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(); httpContext.Request.EnableRewind();
string storeId = null;
string body = string.Empty; string body = string.Empty;
if (httpContext.Request.ContentLength != 0 && httpContext.Request.Body != null) if (httpContext.Request.ContentLength != 0 && httpContext.Request.Body != null)
{ {
@ -227,23 +302,22 @@ namespace BTCPayServer.Hosting
var bitToken = await GetTokenPermissionAsync(sin, token); var bitToken = await GetTokenPermissionAsync(sin, token);
if (bitToken == null) 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) { } 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); var splitted = auth.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (splitted.Length != 2 || !splitted[0].Equals("Basic", StringComparison.OrdinalIgnoreCase)) if (splitted.Length != 2 || !splitted[0].Equals("Basic", StringComparison.OrdinalIgnoreCase))
{ {
throw new BitpayHttpException(401, $"Invalid Authorization header"); return null;
} }
string apiKey = null; string apiKey = null;
@ -253,16 +327,9 @@ namespace BTCPayServer.Hosting
} }
catch catch
{ {
throw new BitpayHttpException(401, $"Invalid Authorization header"); return null;
} }
var storeId = await _TokenRepository.GetStoreIdFromAPIKey(apiKey); return 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);
} }
private async Task<BitTokenEntity> GetTokenPermissionAsync(string sin, string expectedToken) private async Task<BitTokenEntity> GetTokenPermissionAsync(string sin, string expectedToken)