From 9d99c32305ece006d22c2b2857a224d2c9bff0cd Mon Sep 17 00:00:00 2001 From: Kukks Date: Fri, 20 Mar 2020 14:05:23 +0100 Subject: [PATCH] add basic auth for greenfield --- .../Controllers/RestApi/ApiKeysController.cs | 5 +- .../RestApi/TestApiKeyController.cs | 15 ++--- .../Controllers/RestApi/UsersController.cs | 17 ++--- BTCPayServer/Hosting/BTCPayServerServices.cs | 3 +- .../Security/APIKeys/APIKeyExtensions.cs | 5 -- .../Security/AuthenticationSchemes.cs | 2 + .../Basic/BasicAuthenticationHandler.cs | 65 +++++++++++++++++++ .../Basic/BasicAuthenticationOptions.cs | 8 +++ .../Security/Basic/BasicExtensions.cs | 17 +++++ .../wwwroot/swagger/v1/swagger.template.json | 27 +++++--- 10 files changed, 129 insertions(+), 35 deletions(-) create mode 100644 BTCPayServer/Security/Basic/BasicAuthenticationHandler.cs create mode 100644 BTCPayServer/Security/Basic/BasicAuthenticationOptions.cs create mode 100644 BTCPayServer/Security/Basic/BasicExtensions.cs diff --git a/BTCPayServer/Controllers/RestApi/ApiKeysController.cs b/BTCPayServer/Controllers/RestApi/ApiKeysController.cs index 6a3299418..fff308122 100644 --- a/BTCPayServer/Controllers/RestApi/ApiKeysController.cs +++ b/BTCPayServer/Controllers/RestApi/ApiKeysController.cs @@ -6,14 +6,13 @@ using BTCPayServer.Data; using BTCPayServer.Security; using BTCPayServer.Security.APIKeys; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; namespace BTCPayServer.Controllers.RestApi { [ApiController] - [Authorize(AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + [Authorize(AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] public class ApiKeysController : ControllerBase { private readonly APIKeyRepository _apiKeyRepository; @@ -34,7 +33,7 @@ namespace BTCPayServer.Controllers.RestApi } [HttpDelete("~/api/v1/api-keys/current")] - [Authorize(Policy = Policies.Unrestricted, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + [Authorize(Policy = Policies.Unrestricted, AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] public async Task> RevokeKey() { ControllerContext.HttpContext.GetAPIKey(out var apiKey); diff --git a/BTCPayServer/Controllers/RestApi/TestApiKeyController.cs b/BTCPayServer/Controllers/RestApi/TestApiKeyController.cs index 971fd63b1..3673b4fb9 100644 --- a/BTCPayServer/Controllers/RestApi/TestApiKeyController.cs +++ b/BTCPayServer/Controllers/RestApi/TestApiKeyController.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.Security; -using BTCPayServer.Security.APIKeys; using BTCPayServer.Services.Stores; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; @@ -15,7 +14,7 @@ namespace BTCPayServer.Controllers.RestApi /// [Route("api/test/apikey")] [ApiController] - [Authorize(AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + [Authorize(AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] public class TestApiKeyController : ControllerBase { private readonly UserManager _userManager; @@ -28,28 +27,28 @@ namespace BTCPayServer.Controllers.RestApi } [HttpGet("me/id")] - [Authorize(Policy = Policies.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + [Authorize(Policy = Policies.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] public string GetCurrentUserId() { return _userManager.GetUserId(User); } [HttpGet("me")] - [Authorize(Policy = Policies.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + [Authorize(Policy = Policies.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] public async Task GetCurrentUser() { return await _userManager.GetUserAsync(User); } [HttpGet("me/is-admin")] - [Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + [Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] public bool AmIAnAdmin() { return true; } [HttpGet("me/stores")] - [Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + [Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] public StoreData[] GetCurrentUserStores() { return this.HttpContext.GetStoresData(); @@ -57,7 +56,7 @@ namespace BTCPayServer.Controllers.RestApi [HttpGet("me/stores/{storeId}/can-view")] [Authorize(Policy = Policies.CanViewStoreSettings, - AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] public bool CanViewStore(string storeId) { return true; @@ -65,7 +64,7 @@ namespace BTCPayServer.Controllers.RestApi [HttpGet("me/stores/{storeId}/can-edit")] [Authorize(Policy = Policies.CanModifyStoreSettings, - AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] public bool CanEditStore(string storeId) { return true; diff --git a/BTCPayServer/Controllers/RestApi/UsersController.cs b/BTCPayServer/Controllers/RestApi/UsersController.cs index 30e57d725..301d9df46 100644 --- a/BTCPayServer/Controllers/RestApi/UsersController.cs +++ b/BTCPayServer/Controllers/RestApi/UsersController.cs @@ -1,6 +1,4 @@ -using System; using Microsoft.Extensions.Logging; -using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -13,7 +11,6 @@ using BTCPayServer.Security; using BTCPayServer.Security.APIKeys; using BTCPayServer.Services; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -23,7 +20,7 @@ using BTCPayServer.Client; namespace BTCPayServer.Controllers.RestApi { [ApiController] - [Authorize(AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + [Authorize(AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] public class UsersController : ControllerBase { private readonly UserManager _userManager; @@ -40,8 +37,8 @@ namespace BTCPayServer.Controllers.RestApi RoleManager roleManager, SettingsRepository settingsRepository, EventAggregator eventAggregator, IPasswordValidator passwordValidator, - NicolasDorier.RateLimits.RateLimitService throttleService, - Configuration.BTCPayServerOptions options, + RateLimitService throttleService, + BTCPayServerOptions options, IAuthorizationService authorizationService) { _userManager = userManager; @@ -55,7 +52,7 @@ namespace BTCPayServer.Controllers.RestApi _authorizationService = authorizationService; } - [Authorize(Policy = Policies.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + [Authorize(Policy = Policies.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] [HttpGet("~/api/v1/users/me")] public async Task> GetCurrentUser() { @@ -85,7 +82,7 @@ namespace BTCPayServer.Controllers.RestApi // Even if subscription are unlocked, it is forbidden to create admin unauthenticated if (anyAdmin && request.IsAdministrator is true && !isAuth) - return Forbid(AuthenticationSchemes.ApiKey); + return Forbid(AuthenticationSchemes.ApiKeyOrBasic); // You are de-facto admin if there is no other admin, else you need to be auth and pass policy requirements bool isAdmin = anyAdmin ? (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanModifyServerSettings))).Succeeded && (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.Unrestricted))).Succeeded @@ -93,14 +90,14 @@ namespace BTCPayServer.Controllers.RestApi : true; // You need to be admin to create an admin if (request.IsAdministrator is true && !isAdmin) - return Forbid(AuthenticationSchemes.ApiKey); + return Forbid(AuthenticationSchemes.ApiKeyOrBasic); if (!isAdmin && policies.LockSubscription) { // If we are not admin and subscriptions are locked, we need to check the Policies.CanCreateUser.Key permission var canCreateUser = (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanCreateUser))).Succeeded; if (!isAuth || !canCreateUser) - return Forbid(AuthenticationSchemes.ApiKey); + return Forbid(AuthenticationSchemes.ApiKeyOrBasic); } var user = new ApplicationUser diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 07d1282e7..c32ea9907 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -286,7 +286,8 @@ namespace BTCPayServer.Hosting services.AddAuthentication() .AddCookie() .AddBitpayAuthentication() - .AddAPIKeyAuthentication(); + .AddAPIKeyAuthentication() + .AddBasicAuthentication(); } public static IApplicationBuilder UsePayServer(this IApplicationBuilder app) diff --git a/BTCPayServer/Security/APIKeys/APIKeyExtensions.cs b/BTCPayServer/Security/APIKeys/APIKeyExtensions.cs index 5cf617329..d1b6a391a 100644 --- a/BTCPayServer/Security/APIKeys/APIKeyExtensions.cs +++ b/BTCPayServer/Security/APIKeys/APIKeyExtensions.cs @@ -1,15 +1,10 @@ using System; using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; using BTCPayServer.Client; -using BTCPayServer.Data; using BTCPayServer.Security.Bitpay; -using BTCPayServer.Services.Stores; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Primitives; diff --git a/BTCPayServer/Security/AuthenticationSchemes.cs b/BTCPayServer/Security/AuthenticationSchemes.cs index 93bcfedc5..e5d8683f9 100644 --- a/BTCPayServer/Security/AuthenticationSchemes.cs +++ b/BTCPayServer/Security/AuthenticationSchemes.cs @@ -5,5 +5,7 @@ public const string Cookie = "Identity.Application"; public const string Bitpay = "Bitpay"; public const string ApiKey = "GreenfieldApiKey"; + public const string Basic = "Basic"; + public const string ApiKeyOrBasic = "Basic,GreenfieldApiKey"; } } diff --git a/BTCPayServer/Security/Basic/BasicAuthenticationHandler.cs b/BTCPayServer/Security/Basic/BasicAuthenticationHandler.cs new file mode 100644 index 000000000..34be18070 --- /dev/null +++ b/BTCPayServer/Security/Basic/BasicAuthenticationHandler.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using BTCPayServer.Client; +using BTCPayServer.Data; +using BTCPayServer.Security.APIKeys; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace BTCPayServer.Security.Basic +{ + public class BasicAuthenticationHandler : AuthenticationHandler + { + private readonly IOptionsMonitor _identityOptions; + private readonly SignInManager _signInManager; + private readonly UserManager _userManager; + + public BasicAuthenticationHandler( + + IOptionsMonitor identityOptions, + SignInManager signInManager, + UserManager userManager, + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock) : base(options, logger, encoder, clock) + { + _identityOptions = identityOptions; + _signInManager = signInManager; + _userManager = userManager; + } + + protected override async Task HandleAuthenticateAsync() + { + + string authHeader = Context.Request.Headers["Authorization"]; + + if (authHeader == null || !authHeader.StartsWith("Basic ")) return AuthenticateResult.NoResult(); + var encodedUsernamePassword = authHeader.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries)[1]?.Trim(); + var decodedUsernamePassword = Encoding.UTF8.GetString(Convert.FromBase64String(encodedUsernamePassword)).Split(':'); + var username = decodedUsernamePassword[0]; + var password = decodedUsernamePassword[1]; + + var result = await _signInManager.PasswordSignInAsync(username, password, true, true); + if (!result.Succeeded) return AuthenticateResult.Fail(result.ToString()); + + var user = await _userManager.FindByNameAsync(username); + var claims = new List() + { + new Claim(_identityOptions.CurrentValue.ClaimsIdentity.UserIdClaimType, user.Id), + new Claim(APIKeyConstants.ClaimTypes.Permission, Permission.Create(Policies.Unrestricted).ToString()) + }; + + return AuthenticateResult.Success(new AuthenticationTicket( + new ClaimsPrincipal(new ClaimsIdentity(claims, APIKeyConstants.AuthenticationType)), APIKeyConstants.AuthenticationType)); + + } + } +} diff --git a/BTCPayServer/Security/Basic/BasicAuthenticationOptions.cs b/BTCPayServer/Security/Basic/BasicAuthenticationOptions.cs new file mode 100644 index 000000000..f326d66e8 --- /dev/null +++ b/BTCPayServer/Security/Basic/BasicAuthenticationOptions.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Authentication; + +namespace BTCPayServer.Security.Basic +{ + public class BasicAuthenticationOptions : AuthenticationSchemeOptions + { + } +} diff --git a/BTCPayServer/Security/Basic/BasicExtensions.cs b/BTCPayServer/Security/Basic/BasicExtensions.cs new file mode 100644 index 000000000..fd51395b8 --- /dev/null +++ b/BTCPayServer/Security/Basic/BasicExtensions.cs @@ -0,0 +1,17 @@ +using BTCPayServer.Security.Basic; +using Microsoft.AspNetCore.Authentication; + +namespace BTCPayServer.Security.APIKeys +{ + public static class BasicExtensions + { + + public static AuthenticationBuilder AddBasicAuthentication(this AuthenticationBuilder builder) + { + builder.AddScheme(AuthenticationSchemes.Basic, + o => { }); + return builder; + } + + } +} diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json index 52a48c965..8d7dfb820 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json @@ -5,8 +5,7 @@ "description": "A full API to use your BTCPay Server", "contact": { "name": "BTCPay Server", - "url": "https://btcpayserver.org", - "email": "nicolas.dorier@gmail.com" + "url": "https://btcpayserver.org" }, "version": "v1" }, @@ -105,7 +104,8 @@ { "GreenField Authentication": [ "btcpay.user.canviewprofile" - ] + ], + "Basic": [] } ] } @@ -182,7 +182,8 @@ { "GreenField Authentication": [ "btcpay.server.cancreateuser" - ] + ], + "Basic": [] } ] } @@ -208,7 +209,8 @@ }, "security": [ { - "GreenField Authentication": [] + "GreenField Authentication": [], + "Basic": [] } ] }, @@ -232,7 +234,8 @@ }, "security": [ { - "GreenField Authentication": [ "unrestricted" ] + "GreenField Authentication": [ "unrestricted" ], + "Basic": [] } ] } @@ -347,9 +350,17 @@ "securitySchemes": { "GreenField Authentication": { "type": "apiKey", - "description": "BTCPay Server supports authenticating and authorizing users through an API Key that is generated by them. Send the API Key as a header value to Authorization with the format: token {token}. For a smoother experience, you can generate a url that redirects users to an API key creation screen.\n\n The following permissions applies to the context of the user creating the API Key:\n * `unrestricted`: Allow unrestricted access to your account.\n * `btcpay.server.canmodifyserversettings`: Allow total control on the server settings. (only if user is administrator)\n * `btcpay.server.cancreateuser`: Allow the creation new users on this server. (only if user is administrator)\n * `btcpay.user.canviewprofile`: Allow view access to your user profile.\n * `btcpay.user.canmodifyprofile`: Allow view and modification access to your user profile.\n\nThe following permissions applies to all stores of the user, you can limit to a specific store with the following format: `btcpay.store.cancreateinvoice:6HSHAEU4iYWtjxtyRs9KyPjM9GAQp8kw2T9VWbGG1FnZ`:\n * `btcpay.store.canviewstoresettings`: Allow view access to the stores settings. \n * `btcpay.store.canmodifystoresettings`: Allow view and modification access to the stores settings.\n * `btcpay.store.cancreateinvoice`: Allow invoice creation of the store.\n\nNote that API Keys only limits permission of a user and can never expand it. If an API Key has the permission `btcpay.server.canmodifyserversettings` but that the user account creating this API Key is not administrator, the API Key will not be able to modify the server settings.\n", + "description": "BTCPay Server supports authenticating and authorizing users through an API Key that is generated by them. Send the API Key as a header value to Authorization with the format: `token {token}`. For a smoother experience, you can generate a url that redirects users to an API key creation screen.\n\n The following permissions applies to the context of the user creating the API Key:\n * `unrestricted`: Allow unrestricted access to your account.\n * `btcpay.server.canmodifyserversettings`: Allow total control on the server settings. (only if user is administrator)\n * `btcpay.server.cancreateuser`: Allow the creation of new users on this server. (only if user is an administrator)\n * `btcpay.user.canviewprofile`: Allow view access to your user profile.\n * `btcpay.user.canmodifyprofile`: Allow view and modification access to your user profile.\n\nThe following permissions applies to all stores of the user, you can limit to a specific store with the following format: `btcpay.store.cancreateinvoice:6HSHAEU4iYWtjxtyRs9KyPjM9GAQp8kw2T9VWbGG1FnZ`:\n * `btcpay.store.canviewstoresettings`: Allow view access to the stores settings. \n * `btcpay.store.canmodifystoresettings`: Allow view and modification access to the stores settings.\n * `btcpay.store.cancreateinvoice`: Allow invoice creation of the store.\n\nNote that API Keys only limits permission of a user and can never expand it. If an API Key has the permission `btcpay.server.canmodifyserversettings` but that the user account creating this API Key is not administrator, the API Key will not be able to modify the server settings.\n", "name": "Authorization", - "in": "header" + "in": "header", + "scheme": "token" + }, + "Basic": { + "type": "http", + "description": "BTCPay Server supports authenticating and authorizing users through the Basic HTTP authentication scheme. Send the user and password encoded in base64 with the format `Basic {base64(username:password)}`. Using this authentication method implicitly provides you with the `unrestricted` permission", + "name": "Authorization", + "in": "header", + "scheme": "Basic" } } },