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:
parent
725a793863
commit
07390ef9fb
77
src/Icons/Controllers/ChangePasswordUriController.cs
Normal file
77
src/Icons/Controllers/ChangePasswordUriController.cs
Normal 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));
|
||||
}
|
||||
}
|
13
src/Icons/Models/ChangePasswordUriResponse.cs
Normal file
13
src/Icons/Models/ChangePasswordUriResponse.cs
Normal 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;
|
||||
}
|
||||
}
|
8
src/Icons/Models/ChangePasswordUriSettings.cs
Normal file
8
src/Icons/Models/ChangePasswordUriSettings.cs
Normal 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; }
|
||||
}
|
103
src/Icons/Services/ChangePasswordUriService.cs
Normal file
103
src/Icons/Services/ChangePasswordUriService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
8
src/Icons/Services/IChangePasswordUriService.cs
Normal file
8
src/Icons/Services/IChangePasswordUriService.cs
Normal file
@ -0,0 +1,8 @@
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Icons.Services;
|
||||
|
||||
public interface IChangePasswordUriService
|
||||
{
|
||||
Task<string?> GetChangePasswordUri(string domain);
|
||||
}
|
@ -40,5 +40,6 @@ public static class ServiceCollectionExtension
|
||||
services.AddSingleton<IUriService, UriService>();
|
||||
services.AddSingleton<IDomainMappingService, DomainMappingService>();
|
||||
services.AddSingleton<IIconFetchingService, IconFetchingService>();
|
||||
services.AddSingleton<IChangePasswordUriService, ChangePasswordUriService>();
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user