feat(auth): misc fediverse auth improvements
- remove automatic app validation - add force refresh option to GetFediverseUrlAsync - pass state to mastodon authorization URI
This commit is contained in:
		
							parent
							
								
									142ff36d3a
								
							
						
					
					
						commit
						4e9c4af4a5
					
				
					 9 changed files with 143 additions and 180 deletions
				
			
		|  | @ -22,12 +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 | ||||
|     ) | ||||
|     { | ||||
|         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); | ||||
|         var url = await fediverseAuthService.GenerateAuthUrlAsync(instance, forceRefresh); | ||||
|         return Ok(new FediverseUrlResponse(url)); | ||||
|     } | ||||
| 
 | ||||
|  | @ -36,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, | ||||
|  | @ -72,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 | ||||
|  | @ -107,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); | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										3
									
								
								Foxnouns.Backend/Controllers/SidController.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								Foxnouns.Backend/Controllers/SidController.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| namespace Foxnouns.Backend.Controllers; | ||||
| 
 | ||||
| public class SidController { } | ||||
|  | @ -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 | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -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"); | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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> | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ 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; | ||||
|  | @ -35,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) | ||||
|  | @ -52,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); | ||||
|  | @ -67,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(); | ||||
|  | @ -76,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( | ||||
|  | @ -122,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"; | ||||
|  | @ -232,9 +146,6 @@ 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, | ||||
|  |  | |||
|  | @ -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), | ||||
|         }; | ||||
|  |  | |||
|  | @ -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", | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue