Compare commits

...

7 commits

18 changed files with 237 additions and 201 deletions

View file

@ -6,7 +6,6 @@ using Foxnouns.Backend.Utils;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using FediverseAuthService = Foxnouns.Backend.Services.Auth.FediverseAuthService;
namespace Foxnouns.Backend.Controllers.Authentication;
@ -23,9 +22,15 @@ public class FediverseAuthController(
[HttpGet]
[ProducesResponseType<FediverseUrlResponse>(statusCode: StatusCodes.Status200OK)]
public async Task<IActionResult> GetFediverseUrlAsync([FromQuery] string instance)
public async Task<IActionResult> GetFediverseUrlAsync(
[FromQuery] string instance,
[FromQuery] bool forceRefresh = false
)
{
var url = await fediverseAuthService.GenerateAuthUrlAsync(instance);
if (instance.Any(c => c is '@' or ':' or '/') || !instance.Contains('.'))
throw new ApiError.BadRequest("Not a valid domain.", "instance", instance);
var url = await fediverseAuthService.GenerateAuthUrlAsync(instance, forceRefresh);
return Ok(new FediverseUrlResponse(url));
}
@ -34,7 +39,11 @@ public class FediverseAuthController(
public async Task<IActionResult> FediverseCallbackAsync([FromBody] CallbackRequest req)
{
var app = await fediverseAuthService.GetApplicationAsync(req.Instance);
var remoteUser = await fediverseAuthService.GetRemoteFediverseUserAsync(app, req.Code);
var remoteUser = await fediverseAuthService.GetRemoteFediverseUserAsync(
app,
req.Code,
req.State
);
var user = await authService.AuthenticateUserAsync(
AuthType.Fediverse,
@ -70,12 +79,16 @@ public class FediverseAuthController(
)
{
var ticketData = await keyCacheService.GetKeyAsync<FediverseTicketData>(
$"fediverse:{req.Ticket}"
$"fediverse:{req.Ticket}",
delete: true
);
if (ticketData == null)
throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket);
var app = await db.FediverseApplications.FindAsync(ticketData.ApplicationId);
if (app == null)
throw new FoxnounsError("Null application found for ticket");
if (
await db.AuthMethods.AnyAsync(a =>
a.AuthType == AuthType.Fediverse
@ -105,7 +118,7 @@ public class FediverseAuthController(
return Ok(await authService.GenerateUserTokenAsync(user));
}
public record CallbackRequest(string Instance, string Code);
public record CallbackRequest(string Instance, string Code, string State);
private record FediverseUrlResponse(string Url);

View file

@ -94,6 +94,7 @@ public class MembersController(
Names = req.Names ?? [],
Pronouns = req.Pronouns ?? [],
Unlisted = req.Unlisted ?? false,
Sid = null!,
};
db.Add(member);

View file

@ -0,0 +1,52 @@
using System.Diagnostics.CodeAnalysis;
using Foxnouns.Backend.Database;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Foxnouns.Backend.Controllers;
[Route("/sid")]
[SuppressMessage(
"Performance",
"CA1862:Use the \'StringComparison\' method overloads to perform case-insensitive string comparisons",
Justification = "Not usable with EFCore"
)]
public class SidController(Config config, DatabaseContext db) : ApiControllerBase
{
[HttpGet("{**id}")]
public async Task<IActionResult> ResolveSidAsync(string id, CancellationToken ct = default) =>
id.Length switch
{
5 => await ResolveUserSidAsync(id, ct),
6 => await ResolveMemberSidAsync(id, ct),
_ => Redirect(config.BaseUrl),
};
private async Task<IActionResult> ResolveUserSidAsync(string id, CancellationToken ct = default)
{
var username = await db
.Users.Where(u => u.Sid == id.ToLowerInvariant() && !u.Deleted)
.Select(u => u.Username)
.FirstOrDefaultAsync(ct);
if (username == null)
return Redirect(config.BaseUrl);
return Redirect($"{config.BaseUrl}/@{username}");
}
private async Task<IActionResult> ResolveMemberSidAsync(
string id,
CancellationToken ct = default
)
{
var member = await db
.Members.Include(m => m.User)
.Where(m => m.Sid == id.ToLowerInvariant() && !m.User.Deleted)
.Select(m => new { m.Name, m.User.Username })
.FirstOrDefaultAsync(ct);
if (member == null)
return Redirect(config.BaseUrl);
return Redirect($"{config.BaseUrl}/@{member.Username}/{member.Name}");
}
}

View file

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
#nullable disable
namespace Foxnouns.Backend.Database.Migrations
{
/// <inheritdoc />
[DbContext(typeof(DatabaseContext))]
[Migration("20241123210306_RemoveFediverseApplicationTokens")]
public partial class RemoveFediverseApplicationTokens : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(name: "access_token", table: "fediverse_applications");
migrationBuilder.DropColumn(name: "token_valid_until", table: "fediverse_applications");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "access_token",
table: "fediverse_applications",
type: "text",
nullable: true
);
migrationBuilder.AddColumn<Instant>(
name: "token_valid_until",
table: "fediverse_applications",
type: "timestamp with time zone",
nullable: true
);
}
}
}

View file

@ -107,10 +107,6 @@ namespace Foxnouns.Backend.Database.Migrations
.HasColumnType("bigint")
.HasColumnName("id");
b.Property<string>("AccessToken")
.HasColumnType("text")
.HasColumnName("access_token");
b.Property<string>("ClientId")
.IsRequired()
.HasColumnType("text")
@ -130,10 +126,6 @@ namespace Foxnouns.Backend.Database.Migrations
.HasColumnType("integer")
.HasColumnName("instance_type");
b.Property<Instant?>("TokenValidUntil")
.HasColumnType("timestamp with time zone")
.HasColumnName("token_valid_until");
b.HasKey("Id")
.HasName("pk_fediverse_applications");

View file

@ -8,10 +8,6 @@ public class FediverseApplication : BaseModel
public required string ClientId { get; set; }
public required string ClientSecret { get; set; }
public required FediverseInstanceType InstanceType { get; set; }
// These are for ensuring the application is still valid.
public string? AccessToken { get; set; }
public Instant? TokenValidUntil { get; set; }
}
public enum FediverseInstanceType

View file

@ -10,7 +10,7 @@ using SixLabors.ImageSharp.Processing.Processors.Transforms;
namespace Foxnouns.Backend.Extensions;
public static class AvatarObjectExtensions
public static class ImageObjectExtensions
{
private static readonly string[] ValidContentTypes = ["image/png", "image/webp", "image/jpeg"];
@ -39,7 +39,7 @@ public static class AvatarObjectExtensions
) => await objectStorageService.RemoveObjectAsync(CreateFlagInvocable.Path(hash), ct);
public static async Task<(string Hash, Stream Image)> ConvertBase64UriToImage(
this string uri,
string uri,
int size,
bool crop
)

View file

@ -20,6 +20,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0"/>
<PackageReference Include="Minio" Version="6.0.3"/>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
<PackageReference Include="NodaTime" Version="3.1.11"/>
@ -35,6 +36,7 @@
<PackageReference Include="Serilog.Sinks.Seq" Version="8.0.0"/>
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5"/>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2"/>
<PackageReference Include="System.Text.Json" Version="9.0.0"/>
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1"/>
</ItemGroup>

View file

@ -26,7 +26,8 @@ public class CreateFlagInvocable(
try
{
var (hash, image) = await Payload.ImageData.ConvertBase64UriToImage(
var (hash, image) = await ImageObjectExtensions.ConvertBase64UriToImage(
Payload.ImageData,
size: 256,
crop: false
);

View file

@ -39,7 +39,11 @@ public class MemberAvatarUpdateInvocable(
try
{
var (hash, image) = await newAvatar.ConvertBase64UriToImage(size: 512, crop: true);
var (hash, image) = await ImageObjectExtensions.ConvertBase64UriToImage(
newAvatar,
size: 512,
crop: true
);
var prevHash = member.Avatar;
await objectStorageService.PutObjectAsync(Path(id, hash), image, "image/webp");

View file

@ -39,7 +39,11 @@ public class UserAvatarUpdateInvocable(
try
{
var (hash, image) = await newAvatar.ConvertBase64UriToImage(size: 512, crop: true);
var (hash, image) = await ImageObjectExtensions.ConvertBase64UriToImage(
newAvatar,
size: 512,
crop: true
);
image.Seek(0, SeekOrigin.Begin);
var prevHash = user.Avatar;

View file

@ -18,7 +18,7 @@ builder.AddSerilog();
builder
.WebHost.UseSentry(opts =>
{
opts.Dsn = config.Logging.SentryUrl;
opts.Dsn = config.Logging.SentryUrl ?? "";
opts.TracesSampleRate = config.Logging.SentryTracesSampleRate;
opts.MaxRequestBodySize = RequestSize.Small;
})

View file

@ -45,6 +45,7 @@ public class AuthService(
},
},
LastActive = clock.GetCurrentInstant(),
Sid = null!,
};
db.Add(user);
@ -88,6 +89,7 @@ public class AuthService(
},
},
LastActive = clock.GetCurrentInstant(),
Sid = null!,
};
db.Add(user);

View file

@ -1,8 +1,9 @@
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Web;
using Foxnouns.Backend.Database;
using Foxnouns.Backend.Database.Models;
using Duration = NodaTime.Duration;
using Foxnouns.Backend.Extensions;
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
namespace Foxnouns.Backend.Services.Auth;
@ -17,16 +18,13 @@ public partial class FediverseAuthService
Snowflake? existingAppId = null
)
{
var resp = await _client.PostAsync(
var resp = await _client.PostAsJsonAsync(
$"https://{instance}/api/v1/apps",
new FormUrlEncodedContent(
new Dictionary<string, string>
{
{ "client_name", $"pronouns.cc (+{_config.BaseUrl})" },
{ "redirect_uris", MastodonRedirectUri(instance) },
{ "scope", "read:accounts" },
{ "website", _config.BaseUrl },
}
new CreateMastodonApplicationRequest(
ClientName: $"pronouns.cc (+{_config.BaseUrl})",
RedirectUris: MastodonRedirectUri(instance),
Scopes: "read read:accounts",
Website: _config.BaseUrl
)
);
resp.EnsureSuccessStatusCode();
@ -37,12 +35,6 @@ public partial class FediverseAuthService
$"Application created on Mastodon-compatible instance {instance} was null"
);
var token = await GetMastodonAppTokenAsync(
instance,
mastodonApp.ClientId,
mastodonApp.ClientSecret
);
FediverseApplication app;
if (existingAppId == null)
@ -54,8 +46,6 @@ public partial class FediverseAuthService
ClientSecret = mastodonApp.ClientSecret,
Domain = instance,
InstanceType = FediverseInstanceType.MastodonApi,
AccessToken = token,
TokenValidUntil = _clock.GetCurrentInstant() + Duration.FromDays(60),
};
_db.Add(app);
@ -69,8 +59,6 @@ public partial class FediverseAuthService
app.ClientId = mastodonApp.ClientId;
app.ClientSecret = mastodonApp.ClientSecret;
app.InstanceType = FediverseInstanceType.MastodonApi;
app.AccessToken = null;
app.TokenValidUntil = null;
}
await _db.SaveChangesAsync();
@ -78,8 +66,14 @@ public partial class FediverseAuthService
return app;
}
private async Task<FediverseUser> GetMastodonUserAsync(FediverseApplication app, string code)
private async Task<FediverseUser> GetMastodonUserAsync(
FediverseApplication app,
string code,
string state
)
{
await _keyCacheService.ValidateAuthStateAsync(state);
var tokenResp = await _client.PostAsync(
MastodonTokenUri(app.Domain),
new FormUrlEncodedContent(
@ -124,109 +118,27 @@ public partial class FediverseAuthService
private record MastodonTokenResponse([property: J("access_token")] string AccessToken);
// TODO: Mastodon's OAuth documentation doesn't specify a "state" parameter. that feels... wrong
// https://docs.joinmastodon.org/methods/oauth/
private async Task<string> GenerateMastodonAuthUrlAsync(FediverseApplication app)
private async Task<string> GenerateMastodonAuthUrlAsync(
FediverseApplication app,
bool forceRefresh
)
{
try
if (forceRefresh)
{
await ValidateMastodonAppAsync(app);
}
catch (FoxnounsError.RemoteAuthError e)
{
_logger.Error(
e,
"Error validating app token for {AppId} on {Instance}",
app.Id,
app.Domain
_logger.Information(
"An app credentials refresh was requested for {ApplicationId}, creating a new application",
app.Id
);
app = await CreateMastodonApplicationAsync(app.Domain, existingAppId: app.Id);
}
var state = HttpUtility.UrlEncode(await _keyCacheService.GenerateAuthStateAsync());
return $"https://{app.Domain}/oauth/authorize?response_type=code"
+ $"&client_id={app.ClientId}"
+ $"&scope={HttpUtility.UrlEncode("read:accounts")}"
+ $"&redirect_uri={HttpUtility.UrlEncode(MastodonRedirectUri(app.Domain))}";
}
private async Task ValidateMastodonAppAsync(FediverseApplication app)
{
// If we don't have an access token stored, or it's too old, get one
// When doing this we don't need to fetch the application info
if (app.AccessToken == null || app.TokenValidUntil < _clock.GetCurrentInstant())
{
_logger.Debug(
"Application {AppId} on instance {Instance} has no valid token, fetching it",
app.Id,
app.Domain
);
app.AccessToken = await GetMastodonAppTokenAsync(
app.Domain,
app.ClientId,
app.ClientSecret
);
app.TokenValidUntil = _clock.GetCurrentInstant() + Duration.FromDays(60);
_db.Update(app);
await _db.SaveChangesAsync();
return;
}
_logger.Debug(
"Checking whether application {AppId} on instance {Instance} is still valid",
app.Id,
app.Domain
);
var req = new HttpRequestMessage(HttpMethod.Get, MastodonCurrentAppUri(app.Domain));
req.Headers.Add("Authorization", $"Bearer {app.AccessToken}");
var resp = await _client.SendAsync(req);
if (!resp.IsSuccessStatusCode)
{
var error = await resp.Content.ReadAsStringAsync();
throw new FoxnounsError.RemoteAuthError(
"Verifying app credentials returned an error",
error
);
}
}
private async Task<string> GetMastodonAppTokenAsync(
string instance,
string clientId,
string clientSecret
)
{
var resp = await _client.PostAsync(
MastodonTokenUri(instance),
new FormUrlEncodedContent(
new Dictionary<string, string>
{
{ "grant_type", "client_credentials" },
{ "client_id", clientId },
{ "client_secret", clientSecret },
}
)
);
if (!resp.IsSuccessStatusCode)
{
var error = await resp.Content.ReadAsStringAsync();
throw new FoxnounsError.RemoteAuthError(
"Requesting app token returned an error",
error
);
}
var token = (await resp.Content.ReadFromJsonAsync<MastodonTokenResponse>())?.AccessToken;
if (token == null)
{
throw new FoxnounsError($"Token response from instance {instance} was invalid");
}
return token;
+ $"&redirect_uri={HttpUtility.UrlEncode(MastodonRedirectUri(app.Domain))}"
+ $"&state={state}";
}
private static string MastodonTokenUri(string instance) => $"https://{instance}/oauth/token";
@ -234,12 +146,17 @@ public partial class FediverseAuthService
private static string MastodonCurrentUserUri(string instance) =>
$"https://{instance}/api/v1/accounts/verify_credentials";
private static string MastodonCurrentAppUri(string instance) =>
$"https://{instance}/api/v1/apps/verify_credentials";
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")]
private record PartialMastodonApplication(
[property: J("name")] string Name,
[property: J("client_id")] string ClientId,
[property: J("client_secret")] string ClientSecret
);
private record CreateMastodonApplicationRequest(
[property: J("client_name")] string ClientName,
[property: J("redirect_uris")] string RedirectUris,
[property: J("scopes")] string Scopes,
[property: J("website")] string Website
);
}

View file

@ -1,7 +1,6 @@
using Foxnouns.Backend.Database;
using Foxnouns.Backend.Database.Models;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
namespace Foxnouns.Backend.Services.Auth;
@ -10,26 +9,27 @@ public partial class FediverseAuthService
{
private const string NodeInfoRel = "http://nodeinfo.diaspora.software/ns/schema/2.0";
private readonly ILogger _logger;
private readonly HttpClient _client;
private readonly DatabaseContext _db;
private readonly ILogger _logger;
private readonly Config _config;
private readonly DatabaseContext _db;
private readonly KeyCacheService _keyCacheService;
private readonly ISnowflakeGenerator _snowflakeGenerator;
private readonly IClock _clock;
public FediverseAuthService(
ILogger logger,
Config config,
DatabaseContext db,
ISnowflakeGenerator snowflakeGenerator,
IClock clock
KeyCacheService keyCacheService,
ISnowflakeGenerator snowflakeGenerator
)
{
_logger = logger.ForContext<FediverseAuthService>();
_config = config;
_db = db;
_keyCacheService = keyCacheService;
_snowflakeGenerator = snowflakeGenerator;
_clock = clock;
_logger = logger.ForContext<FediverseAuthService>();
_client = new HttpClient();
_client.DefaultRequestHeaders.Remove("User-Agent");
_client.DefaultRequestHeaders.Remove("Accept");
@ -37,10 +37,10 @@ public partial class FediverseAuthService
_client.DefaultRequestHeaders.Add("Accept", "application/json");
}
public async Task<string> GenerateAuthUrlAsync(string instance)
public async Task<string> GenerateAuthUrlAsync(string instance, bool forceRefresh)
{
var app = await GetApplicationAsync(instance);
return await GenerateAuthUrlAsync(app);
return await GenerateAuthUrlAsync(app, forceRefresh);
}
// thank you, gargron and syuilo, for agreeing on a name for *once* in your lives,
@ -96,21 +96,25 @@ public partial class FediverseAuthService
);
}
private async Task<string> GenerateAuthUrlAsync(FediverseApplication app) =>
private async Task<string> GenerateAuthUrlAsync(FediverseApplication app, bool forceRefresh) =>
app.InstanceType switch
{
FediverseInstanceType.MastodonApi => await GenerateMastodonAuthUrlAsync(app),
FediverseInstanceType.MastodonApi => await GenerateMastodonAuthUrlAsync(
app,
forceRefresh
),
FediverseInstanceType.MisskeyApi => throw new NotImplementedException(),
_ => throw new ArgumentOutOfRangeException(nameof(app), app.InstanceType, null),
};
public async Task<FediverseUser> GetRemoteFediverseUserAsync(
FediverseApplication app,
string code
string code,
string state
) =>
app.InstanceType switch
{
FediverseInstanceType.MastodonApi => await GetMastodonUserAsync(app, code),
FediverseInstanceType.MastodonApi => await GetMastodonUserAsync(app, code, state),
FediverseInstanceType.MisskeyApi => throw new NotImplementedException(),
_ => throw new ArgumentOutOfRangeException(nameof(app), app.InstanceType, null),
};

View file

@ -32,7 +32,7 @@ public class MemberRendererService(DatabaseContext db, Config config)
member.Id,
member.Sid,
member.Name,
member.DisplayName,
member.DisplayName ?? member.Name,
member.Bio,
AvatarUrlFor(member),
member.Links,
@ -60,7 +60,7 @@ public class MemberRendererService(DatabaseContext db, Config config)
member.Id,
member.Sid,
member.Name,
member.DisplayName,
member.DisplayName ?? member.Name,
member.Bio,
AvatarUrlFor(member),
member.Names,
@ -87,7 +87,7 @@ public class MemberRendererService(DatabaseContext db, Config config)
Snowflake Id,
string Sid,
string Name,
string? DisplayName,
string DisplayName,
string? Bio,
string? AvatarUrl,
IEnumerable<FieldEntry> Names,
@ -99,7 +99,7 @@ public class MemberRendererService(DatabaseContext db, Config config)
Snowflake Id,
string Sid,
string Name,
string? DisplayName,
string DisplayName,
string? Bio,
string? AvatarUrl,
string[] Links,

View file

@ -162,6 +162,7 @@ public static partial class ValidationUtils
}
public const int MaxBioLength = 1024;
public const int MaxAvatarLength = 1_500_000;
public static ValidationError? ValidateBio(string? bio)
{
@ -183,7 +184,10 @@ public static partial class ValidationUtils
return avatar?.Length switch
{
0 => ValidationError.GenericValidationError("Avatar cannot be empty", null),
> 1_500_000 => ValidationError.GenericValidationError("Avatar is too large", null),
> MaxAvatarLength => ValidationError.GenericValidationError(
"Avatar is too large",
null
),
_ => null,
};
}

View file

@ -96,6 +96,19 @@
"Mono.TextTemplating": "2.2.1"
}
},
"Microsoft.Extensions.Caching.Memory": {
"type": "Direct",
"requested": "[9.0.0, )",
"resolved": "9.0.0",
"contentHash": "zbnPX/JQ0pETRSUG9fNPBvpIq42Aufvs15gGYyNIMhCun9yhmWihz0WgsI7bSDPjxWTKBf8oX/zv6v2uZ3W9OQ==",
"dependencies": {
"Microsoft.Extensions.Caching.Abstractions": "9.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
"Microsoft.Extensions.Logging.Abstractions": "9.0.0",
"Microsoft.Extensions.Options": "9.0.0",
"Microsoft.Extensions.Primitives": "9.0.0"
}
},
"Minio": {
"type": "Direct",
"requested": "[6.0.3, )",
@ -246,6 +259,16 @@
"Swashbuckle.AspNetCore.SwaggerUI": "6.6.2"
}
},
"System.Text.Json": {
"type": "Direct",
"requested": "[9.0.0, )",
"resolved": "9.0.0",
"contentHash": "js7+qAu/9mQvnhA4EfGMZNEzXtJCDxgkgj8ohuxq/Qxv+R56G+ljefhiJHOxTNiw54q8vmABCWUwkMulNdlZ4A==",
"dependencies": {
"System.IO.Pipelines": "9.0.0",
"System.Text.Encodings.Web": "9.0.0"
}
},
"System.Text.RegularExpressions": {
"type": "Direct",
"requested": "[4.3.1, )",
@ -412,22 +435,10 @@
},
"Microsoft.Extensions.Caching.Abstractions": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "3KuSxeHoNYdxVYfg2IRZCThcrlJ1XJqIXkAWikCsbm5C/bCjv7G0WoKDyuR98Q+T607QT2Zl5GsbGRkENcV2yQ==",
"resolved": "9.0.0",
"contentHash": "FPWZAa9c0H4dvOj351iR1jkUIs4u9ykL4Bm592yhjDyO5lCoWd+TMAHx2EMbarzUvCvgjWjJIoC6//Q9kH6YhA==",
"dependencies": {
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.Caching.Memory": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "7pqivmrZDzo1ADPkRwjy+8jtRKWRCPag9qPI+p7sgu7Q4QreWhcvbiWXsbhP+yY8XSiDvZpu2/LWdBv7PnmOpQ==",
"dependencies": {
"Microsoft.Extensions.Caching.Abstractions": "8.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Logging.Abstractions": "8.0.0",
"Microsoft.Extensions.Options": "8.0.0",
"Microsoft.Extensions.Primitives": "8.0.0"
"Microsoft.Extensions.Primitives": "9.0.0"
}
},
"Microsoft.Extensions.Configuration": {
@ -465,8 +476,8 @@
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
"resolved": "8.0.1",
"contentHash": "fGLiCRLMYd00JYpClraLjJTNKLmMJPnqxMaiRzEBIIvevlzxz33mXy39Lkd48hu1G+N21S7QpaO5ZzKsI6FRuA=="
"resolved": "9.0.0",
"contentHash": "+6f2qv2a3dLwd5w6JanPIPs47CxRbnk+ZocMJUhv9NxP88VlOcJYZs9jY+MYSjxvady08bUZn6qgiNh7DadGgg=="
},
"Microsoft.Extensions.DependencyModel": {
"type": "Transitive",
@ -542,10 +553,11 @@
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==",
"resolved": "9.0.0",
"contentHash": "g0UfujELzlLbHoVG8kPKVBaW470Ewi+jnptGS9KUi6jcb+k2StujtK3m26DFSGGwQ/+bVgZfsWqNzlP6YOejvw==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
"System.Diagnostics.DiagnosticSource": "9.0.0"
}
},
"Microsoft.Extensions.Logging.Configuration": {
@ -570,11 +582,11 @@
},
"Microsoft.Extensions.Options": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==",
"resolved": "9.0.0",
"contentHash": "y2146b3jrPI3Q0lokKXdKLpmXqakYbDIPDV6r3M8SqvSf45WwOTzkyfDpxnZXJsJQEpAsAqjUq5Pu8RCJMjubg==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Primitives": "8.0.0"
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
"Microsoft.Extensions.Primitives": "9.0.0"
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions": {
@ -591,8 +603,8 @@
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g=="
"resolved": "9.0.0",
"contentHash": "N3qEBzmLMYiASUlKxxFIISP4AiwuPTHF5uCh+2CWSwwzAJiIYx0kBJsS30cp1nvhSySFAVi30jecD307jV+8Kg=="
},
"Microsoft.NETCore.Platforms": {
"type": "Transitive",
@ -983,8 +995,8 @@
},
"System.Diagnostics.DiagnosticSource": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "c9xLpVz6PL9lp/djOWtk5KPDZq3cSYpmXoJQY524EOtuFl5z9ZtsotpsyrDW40U1DRnQSYvcPKEUV0X//u6gkQ=="
"resolved": "9.0.0",
"contentHash": "ddppcFpnbohLWdYKr/ZeLZHmmI+DXFgZ3Snq+/E7SwcdW4UnvxmaugkwGywvGVWkHPGCSZjCP+MLzu23AL5SDw=="
},
"System.Diagnostics.Tracing": {
"type": "Transitive",
@ -1072,8 +1084,8 @@
},
"System.IO.Pipelines": {
"type": "Transitive",
"resolved": "6.0.3",
"contentHash": "ryTgF+iFkpGZY1vRQhfCzX0xTdlV3pyaTTqRu2ETbEv+HlV7O6y7hyQURnghNIXvctl5DuZ//Dpks6HdL/Txgw=="
"resolved": "9.0.0",
"contentHash": "eA3cinogwaNB4jdjQHOP3Z3EuyiDII7MT35jgtnsA4vkn0LUrrSHsU0nzHTzFzmaFYeKV7MYyMxOocFzsBHpTw=="
},
"System.Linq": {
"type": "Transitive",
@ -1484,16 +1496,8 @@
},
"System.Text.Encodings.Web": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ=="
},
"System.Text.Json": {
"type": "Transitive",
"resolved": "8.0.4",
"contentHash": "bAkhgDJ88XTsqczoxEMliSrpijKZHhbJQldhAmObj/RbrN3sU5dcokuXmWJWsdQAhiMJ9bTayWsL1C9fbbCRhw==",
"dependencies": {
"System.Text.Encodings.Web": "8.0.0"
}
"resolved": "9.0.0",
"contentHash": "e2hMgAErLbKyUUwt18qSBf9T5Y+SFAL3ZedM8fLupkVj8Rj2PZ9oxQ37XX2LF8fTO1wNIxvKpihD7Of7D/NxZw=="
},
"System.Threading": {
"type": "Transitive",