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.Abstractions.Extensions; using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.Services; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NicolasDorier.RateLimits; namespace BTCPayServer.Security.Greenfield { public class BasicAuthenticationHandler( IOptionsMonitor identityOptions, IOptionsMonitor options, IOptions mvcOptions, ILoggerFactory logger, UrlEncoder encoder, SignInManager signInManager, UserService userService, IRateLimitService rateLimitService, UserManager userManager) : GreenfieldAuthenticationHandler(options, logger, encoder, mvcOptions) { protected override async Task HandleAuthenticateAsync() { string authHeader = Context.Request.Headers["Authorization"]; if (authHeader == null || !authHeader.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase)) return AuthenticateResult.NoResult(); string password; string username; try { var encodedUsernamePassword = authHeader.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries)[1].Trim(); var decodedUsernamePassword = Encoding.UTF8.GetString(Convert.FromBase64String(encodedUsernamePassword)).Split(':'); username = decodedUsernamePassword[0]; password = decodedUsernamePassword[1]; } catch (Exception) { return Fail( "Basic authentication header was not in a correct format. (username:password encoded in base64)"); } var user = await userManager.Users .Include(applicationUser => applicationUser.Fido2Credentials) .FirstOrDefaultAsync(applicationUser => applicationUser.NormalizedUserName == userManager.NormalizeName(username)); // Give new accounts time to create API keys via the Greenfield API. TimeSpan? accountAge = user?.Created is { } created ? DateTimeOffset.UtcNow - created : null; var isNewAccount = accountAge >= TimeSpan.Zero && accountAge <= TimeSpan.FromMinutes(5); if (!isNewAccount) { if (Context.Connection.RemoteIpAddress?.ToString() is string ip) if (!await rateLimitService.Throttle(ZoneLimits.Login, ip)) return Fail($"Basic authentication failed: Rate limited. Please use authentication with API Keys to avoid throttling."); } var loggingContext = new UserService.CanLoginContext(user, baseUrl: Request.GetRequestBaseUrl()); if (!await userService.CanLogin(loggingContext)) { return Fail($"Basic authentication failed: {loggingContext.Failures[0].Text.Value}"); } if (user is null) return Fail($"Basic authentication failed"); if (!isNewAccount && user.GetBlob()?.AllowGreenfieldBasicAuth is not true) return Fail("Basic authentication for the Greenfield API is not enabled for this user."); if (await signInManager.IsTwoFactorEnabledAsync(user)) { return Fail("Cannot use Basic authentication when multi-factor is enabled."); } var result = await signInManager.CheckPasswordSignInAsync(user, password, true); if (!result.Succeeded) return Fail(result.ToString()); var claims = new List() { new Claim(identityOptions.CurrentValue.ClaimsIdentity.UserIdClaimType, user.Id), new Claim(GreenfieldConstants.ClaimTypes.Permission, Permission.Create(Policies.Unrestricted).ToString()) }; claims.AddRange((await userManager.GetRolesAsync(user)).Select(s => new Claim(identityOptions.CurrentValue.ClaimsIdentity.RoleClaimType, s))); return AuthenticateResult.Success(new AuthenticationTicket( new ClaimsPrincipal(new ClaimsIdentity(claims, GreenfieldConstants.AuthenticationType)), GreenfieldConstants.AuthenticationType)); } AuthenticateResult Fail(string reason) { Context.Items.TryAdd(GreenfieldAuthenticationHandler.GreenfieldAuthFailureReason, reason); return AuthenticateResult.Fail(reason); } } }