1
0
mirror of https://github.com/bitwarden/server.git synced 2025-05-25 05:21:03 -05:00

add ChangePasswordUri controller and service to Icons

This commit is contained in:
Nick Krantz 2025-05-20 09:31:55 -05:00
parent 725a793863
commit 07390ef9fb
No known key found for this signature in database
GPG Key ID: FF670021ABCAB82E
6 changed files with 210 additions and 0 deletions

View File

@ -0,0 +1,77 @@
using Bit.Icons.Models;
using Bit.Icons.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
namespace Bit.Icons.Controllers;
[Route("change-password-uri")]
public class ChangePasswordUriController : Controller
{
private readonly IMemoryCache _memoryCache;
private readonly IDomainMappingService _domainMappingService;
private readonly IChangePasswordUriService _changePasswordService;
private readonly ChangePasswordUriSettings _changePasswordSettings;
public ChangePasswordUriController(
IMemoryCache memoryCache,
IDomainMappingService domainMappingService,
IChangePasswordUriService changePasswordService)
{
_memoryCache = memoryCache;
_domainMappingService = domainMappingService;
_changePasswordService = changePasswordService;
}
[HttpGet]
public async Task<IActionResult> Get([FromQuery] string uri)
{
if (string.IsNullOrWhiteSpace(uri))
{
return new BadRequestResult();
}
var uriHasProtocol = uri.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
uri.StartsWith("https://", StringComparison.OrdinalIgnoreCase);
var url = uriHasProtocol ? uri : $"https://{uri}";
if (!Uri.TryCreate(url, UriKind.Absolute, out var validUri))
{
return new BadRequestResult();
}
var domain = validUri.Host;
var mappedDomain = _domainMappingService.MapDomain(domain);
if (!_changePasswordSettings.CacheEnabled || !_memoryCache.TryGetValue(mappedDomain, out string changePasswordUri))
{
var result = await _changePasswordService.GetChangePasswordUri(domain);
if (result == null)
{
changePasswordUri = null;
}
else
{
changePasswordUri = result;
}
if (_changePasswordSettings.CacheEnabled)
{
_memoryCache.Set(mappedDomain, changePasswordUri, new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = new TimeSpan(_changePasswordSettings.CacheHours, 0, 0),
Size = changePasswordUri?.Length ?? 0,
Priority = changePasswordUri == null ? CacheItemPriority.High : CacheItemPriority.Normal
});
}
}
if (changePasswordUri == null)
{
return Ok(new ChangePasswordUriResponse(changePasswordUri));
}
return Ok(new ChangePasswordUriResponse(changePasswordUri));
}
}

View File

@ -0,0 +1,13 @@
#nullable enable
namespace Bit.Icons.Models;
public class ChangePasswordUriResponse
{
public string? uri { get; set; }
public ChangePasswordUriResponse(string? uri)
{
this.uri = uri;
}
}

View File

@ -0,0 +1,8 @@
namespace Bit.Icons.Models;
public class ChangePasswordUriSettings
{
public virtual bool CacheEnabled { get; set; }
public virtual int CacheHours { get; set; }
public virtual long? CacheSizeLimit { get; set; }
}

View File

@ -0,0 +1,103 @@
#nullable enable
namespace Bit.Icons.Services;
public class ChangePasswordUriService : IChangePasswordUriService
{
private readonly HttpClient _httpClient;
public ChangePasswordUriService(IHttpClientFactory httpClientFactory)
{
_httpClient = httpClientFactory.CreateClient("ChangePasswordUri");
}
/// <summary>
/// Fetches the well-known change password URL for the given domain.
/// </summary>
/// <param name="domain"></param>
/// <returns></returns>
public async Task<string?> GetChangePasswordUri(string domain)
{
if (string.IsNullOrWhiteSpace(domain))
{
return null;
}
var hasReliableStatusCode = await HasReliableHttpStatusCode(domain);
var wellKnownChangePasswordUrl = await GetWellKnownChangePasswordUrl(domain);
if (hasReliableStatusCode && wellKnownChangePasswordUrl != null)
{
return wellKnownChangePasswordUrl;
}
// Reliable well-known URL criteria not met, return null
return null;
}
/// <summary>
/// Checks if the server returns a non-200 status code for a resource that should not exist.
// See https://w3c.github.io/webappsec-change-password-url/response-code-reliability.html#semantics
/// </summary>
/// <param name="urlDomain">The domain of the URL to check</param>
/// <returns>True when the domain responds with a non-ok response</returns>
private async Task<bool> HasReliableHttpStatusCode(string urlDomain)
{
try
{
var url = new UriBuilder(urlDomain)
{
Path = "/.well-known/resource-that-should-not-exist-whose-status-code-should-not-be-200"
};
var request = new HttpRequestMessage(HttpMethod.Get, url.ToString())
{
Headers =
{
{ "Cache-Control", "no-store" },
}
};
var response = await _httpClient.SendAsync(request);
return !response.IsSuccessStatusCode;
}
catch
{
return false;
}
}
/// <summary>
/// Builds a well-known change password URL for the given origin. Attempts to fetch the URL to ensure a valid response
/// is returned. Returns null if the request throws or the response is not 200 OK.
/// See https://w3c.github.io/webappsec-change-password-url/
/// </summary>
/// <param name="urlDomain">The domain of the URL to check</param>
/// <returns>The well-known change password URL if valid, otherwise null</returns>
private async Task<string?> GetWellKnownChangePasswordUrl(string urlDomain)
{
try
{
var url = new UriBuilder(urlDomain)
{
Path = "/.well-known/change-password"
};
var request = new HttpRequestMessage(HttpMethod.Get, url.ToString())
{
Headers =
{
{ "Cache-Control", "no-store" },
}
};
var response = await _httpClient.SendAsync(request);
return response.IsSuccessStatusCode ? url.ToString() : null;
}
catch
{
return null;
}
}
}

View File

@ -0,0 +1,8 @@
#nullable enable
namespace Bit.Icons.Services;
public interface IChangePasswordUriService
{
Task<string?> GetChangePasswordUri(string domain);
}

View File

@ -40,5 +40,6 @@ public static class ServiceCollectionExtension
services.AddSingleton<IUriService, UriService>(); services.AddSingleton<IUriService, UriService>();
services.AddSingleton<IDomainMappingService, DomainMappingService>(); services.AddSingleton<IDomainMappingService, DomainMappingService>();
services.AddSingleton<IIconFetchingService, IconFetchingService>(); services.AddSingleton<IIconFetchingService, IconFetchingService>();
services.AddSingleton<IChangePasswordUriService, ChangePasswordUriService>();
} }
} }