Compare commits
7 commits
9160281ea2
...
c8cd483d20
Author | SHA1 | Date | |
---|---|---|---|
c8cd483d20 | |||
7cb17409cd | |||
4e9c4af4a5 | |||
142ff36d3a | |||
d87856bf2c | |||
6abf505c40 | |||
d0bf638a21 |
18 changed files with 237 additions and 201 deletions
|
@ -6,7 +6,6 @@ using Foxnouns.Backend.Utils;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using FediverseAuthService = Foxnouns.Backend.Services.Auth.FediverseAuthService;
|
|
||||||
|
|
||||||
namespace Foxnouns.Backend.Controllers.Authentication;
|
namespace Foxnouns.Backend.Controllers.Authentication;
|
||||||
|
|
||||||
|
@ -23,9 +22,15 @@ public class FediverseAuthController(
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResponseType<FediverseUrlResponse>(statusCode: StatusCodes.Status200OK)]
|
[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));
|
return Ok(new FediverseUrlResponse(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +39,11 @@ public class FediverseAuthController(
|
||||||
public async Task<IActionResult> FediverseCallbackAsync([FromBody] CallbackRequest req)
|
public async Task<IActionResult> FediverseCallbackAsync([FromBody] CallbackRequest req)
|
||||||
{
|
{
|
||||||
var app = await fediverseAuthService.GetApplicationAsync(req.Instance);
|
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(
|
var user = await authService.AuthenticateUserAsync(
|
||||||
AuthType.Fediverse,
|
AuthType.Fediverse,
|
||||||
|
@ -70,12 +79,16 @@ public class FediverseAuthController(
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var ticketData = await keyCacheService.GetKeyAsync<FediverseTicketData>(
|
var ticketData = await keyCacheService.GetKeyAsync<FediverseTicketData>(
|
||||||
$"fediverse:{req.Ticket}"
|
$"fediverse:{req.Ticket}",
|
||||||
|
delete: true
|
||||||
);
|
);
|
||||||
if (ticketData == null)
|
if (ticketData == null)
|
||||||
throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket);
|
throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket);
|
||||||
|
|
||||||
var app = await db.FediverseApplications.FindAsync(ticketData.ApplicationId);
|
var app = await db.FediverseApplications.FindAsync(ticketData.ApplicationId);
|
||||||
|
if (app == null)
|
||||||
|
throw new FoxnounsError("Null application found for ticket");
|
||||||
|
|
||||||
if (
|
if (
|
||||||
await db.AuthMethods.AnyAsync(a =>
|
await db.AuthMethods.AnyAsync(a =>
|
||||||
a.AuthType == AuthType.Fediverse
|
a.AuthType == AuthType.Fediverse
|
||||||
|
@ -105,7 +118,7 @@ public class FediverseAuthController(
|
||||||
return Ok(await authService.GenerateUserTokenAsync(user));
|
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);
|
private record FediverseUrlResponse(string Url);
|
||||||
|
|
||||||
|
|
|
@ -94,6 +94,7 @@ public class MembersController(
|
||||||
Names = req.Names ?? [],
|
Names = req.Names ?? [],
|
||||||
Pronouns = req.Pronouns ?? [],
|
Pronouns = req.Pronouns ?? [],
|
||||||
Unlisted = req.Unlisted ?? false,
|
Unlisted = req.Unlisted ?? false,
|
||||||
|
Sid = null!,
|
||||||
};
|
};
|
||||||
db.Add(member);
|
db.Add(member);
|
||||||
|
|
||||||
|
|
52
Foxnouns.Backend/Controllers/SidController.cs
Normal file
52
Foxnouns.Backend/Controllers/SidController.cs
Normal 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}");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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")
|
.HasColumnType("bigint")
|
||||||
.HasColumnName("id");
|
.HasColumnName("id");
|
||||||
|
|
||||||
b.Property<string>("AccessToken")
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("access_token");
|
|
||||||
|
|
||||||
b.Property<string>("ClientId")
|
b.Property<string>("ClientId")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text")
|
.HasColumnType("text")
|
||||||
|
@ -130,10 +126,6 @@ namespace Foxnouns.Backend.Database.Migrations
|
||||||
.HasColumnType("integer")
|
.HasColumnType("integer")
|
||||||
.HasColumnName("instance_type");
|
.HasColumnName("instance_type");
|
||||||
|
|
||||||
b.Property<Instant?>("TokenValidUntil")
|
|
||||||
.HasColumnType("timestamp with time zone")
|
|
||||||
.HasColumnName("token_valid_until");
|
|
||||||
|
|
||||||
b.HasKey("Id")
|
b.HasKey("Id")
|
||||||
.HasName("pk_fediverse_applications");
|
.HasName("pk_fediverse_applications");
|
||||||
|
|
||||||
|
|
|
@ -8,10 +8,6 @@ public class FediverseApplication : BaseModel
|
||||||
public required string ClientId { get; set; }
|
public required string ClientId { get; set; }
|
||||||
public required string ClientSecret { get; set; }
|
public required string ClientSecret { get; set; }
|
||||||
public required FediverseInstanceType InstanceType { 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
|
public enum FediverseInstanceType
|
||||||
|
|
|
@ -10,7 +10,7 @@ using SixLabors.ImageSharp.Processing.Processors.Transforms;
|
||||||
|
|
||||||
namespace Foxnouns.Backend.Extensions;
|
namespace Foxnouns.Backend.Extensions;
|
||||||
|
|
||||||
public static class AvatarObjectExtensions
|
public static class ImageObjectExtensions
|
||||||
{
|
{
|
||||||
private static readonly string[] ValidContentTypes = ["image/png", "image/webp", "image/jpeg"];
|
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);
|
) => await objectStorageService.RemoveObjectAsync(CreateFlagInvocable.Path(hash), ct);
|
||||||
|
|
||||||
public static async Task<(string Hash, Stream Image)> ConvertBase64UriToImage(
|
public static async Task<(string Hash, Stream Image)> ConvertBase64UriToImage(
|
||||||
this string uri,
|
string uri,
|
||||||
int size,
|
int size,
|
||||||
bool crop
|
bool crop
|
||||||
)
|
)
|
|
@ -20,6 +20,7 @@
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0"/>
|
||||||
<PackageReference Include="Minio" Version="6.0.3"/>
|
<PackageReference Include="Minio" Version="6.0.3"/>
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
|
||||||
<PackageReference Include="NodaTime" Version="3.1.11"/>
|
<PackageReference Include="NodaTime" Version="3.1.11"/>
|
||||||
|
@ -35,6 +36,7 @@
|
||||||
<PackageReference Include="Serilog.Sinks.Seq" Version="8.0.0"/>
|
<PackageReference Include="Serilog.Sinks.Seq" Version="8.0.0"/>
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5"/>
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5"/>
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2"/>
|
<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"/>
|
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,8 @@ public class CreateFlagInvocable(
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var (hash, image) = await Payload.ImageData.ConvertBase64UriToImage(
|
var (hash, image) = await ImageObjectExtensions.ConvertBase64UriToImage(
|
||||||
|
Payload.ImageData,
|
||||||
size: 256,
|
size: 256,
|
||||||
crop: false
|
crop: false
|
||||||
);
|
);
|
||||||
|
|
|
@ -39,7 +39,11 @@ public class MemberAvatarUpdateInvocable(
|
||||||
|
|
||||||
try
|
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;
|
var prevHash = member.Avatar;
|
||||||
|
|
||||||
await objectStorageService.PutObjectAsync(Path(id, hash), image, "image/webp");
|
await objectStorageService.PutObjectAsync(Path(id, hash), image, "image/webp");
|
||||||
|
|
|
@ -39,7 +39,11 @@ public class UserAvatarUpdateInvocable(
|
||||||
|
|
||||||
try
|
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);
|
image.Seek(0, SeekOrigin.Begin);
|
||||||
var prevHash = user.Avatar;
|
var prevHash = user.Avatar;
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ builder.AddSerilog();
|
||||||
builder
|
builder
|
||||||
.WebHost.UseSentry(opts =>
|
.WebHost.UseSentry(opts =>
|
||||||
{
|
{
|
||||||
opts.Dsn = config.Logging.SentryUrl;
|
opts.Dsn = config.Logging.SentryUrl ?? "";
|
||||||
opts.TracesSampleRate = config.Logging.SentryTracesSampleRate;
|
opts.TracesSampleRate = config.Logging.SentryTracesSampleRate;
|
||||||
opts.MaxRequestBodySize = RequestSize.Small;
|
opts.MaxRequestBodySize = RequestSize.Small;
|
||||||
})
|
})
|
||||||
|
|
|
@ -45,6 +45,7 @@ public class AuthService(
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
LastActive = clock.GetCurrentInstant(),
|
LastActive = clock.GetCurrentInstant(),
|
||||||
|
Sid = null!,
|
||||||
};
|
};
|
||||||
|
|
||||||
db.Add(user);
|
db.Add(user);
|
||||||
|
@ -88,6 +89,7 @@ public class AuthService(
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
LastActive = clock.GetCurrentInstant(),
|
LastActive = clock.GetCurrentInstant(),
|
||||||
|
Sid = null!,
|
||||||
};
|
};
|
||||||
|
|
||||||
db.Add(user);
|
db.Add(user);
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Web;
|
using System.Web;
|
||||||
using Foxnouns.Backend.Database;
|
using Foxnouns.Backend.Database;
|
||||||
using Foxnouns.Backend.Database.Models;
|
using Foxnouns.Backend.Database.Models;
|
||||||
using Duration = NodaTime.Duration;
|
using Foxnouns.Backend.Extensions;
|
||||||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
||||||
|
|
||||||
namespace Foxnouns.Backend.Services.Auth;
|
namespace Foxnouns.Backend.Services.Auth;
|
||||||
|
@ -17,16 +18,13 @@ public partial class FediverseAuthService
|
||||||
Snowflake? existingAppId = null
|
Snowflake? existingAppId = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var resp = await _client.PostAsync(
|
var resp = await _client.PostAsJsonAsync(
|
||||||
$"https://{instance}/api/v1/apps",
|
$"https://{instance}/api/v1/apps",
|
||||||
new FormUrlEncodedContent(
|
new CreateMastodonApplicationRequest(
|
||||||
new Dictionary<string, string>
|
ClientName: $"pronouns.cc (+{_config.BaseUrl})",
|
||||||
{
|
RedirectUris: MastodonRedirectUri(instance),
|
||||||
{ "client_name", $"pronouns.cc (+{_config.BaseUrl})" },
|
Scopes: "read read:accounts",
|
||||||
{ "redirect_uris", MastodonRedirectUri(instance) },
|
Website: _config.BaseUrl
|
||||||
{ "scope", "read:accounts" },
|
|
||||||
{ "website", _config.BaseUrl },
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
resp.EnsureSuccessStatusCode();
|
resp.EnsureSuccessStatusCode();
|
||||||
|
@ -37,12 +35,6 @@ public partial class FediverseAuthService
|
||||||
$"Application created on Mastodon-compatible instance {instance} was null"
|
$"Application created on Mastodon-compatible instance {instance} was null"
|
||||||
);
|
);
|
||||||
|
|
||||||
var token = await GetMastodonAppTokenAsync(
|
|
||||||
instance,
|
|
||||||
mastodonApp.ClientId,
|
|
||||||
mastodonApp.ClientSecret
|
|
||||||
);
|
|
||||||
|
|
||||||
FediverseApplication app;
|
FediverseApplication app;
|
||||||
|
|
||||||
if (existingAppId == null)
|
if (existingAppId == null)
|
||||||
|
@ -54,8 +46,6 @@ public partial class FediverseAuthService
|
||||||
ClientSecret = mastodonApp.ClientSecret,
|
ClientSecret = mastodonApp.ClientSecret,
|
||||||
Domain = instance,
|
Domain = instance,
|
||||||
InstanceType = FediverseInstanceType.MastodonApi,
|
InstanceType = FediverseInstanceType.MastodonApi,
|
||||||
AccessToken = token,
|
|
||||||
TokenValidUntil = _clock.GetCurrentInstant() + Duration.FromDays(60),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
_db.Add(app);
|
_db.Add(app);
|
||||||
|
@ -69,8 +59,6 @@ public partial class FediverseAuthService
|
||||||
app.ClientId = mastodonApp.ClientId;
|
app.ClientId = mastodonApp.ClientId;
|
||||||
app.ClientSecret = mastodonApp.ClientSecret;
|
app.ClientSecret = mastodonApp.ClientSecret;
|
||||||
app.InstanceType = FediverseInstanceType.MastodonApi;
|
app.InstanceType = FediverseInstanceType.MastodonApi;
|
||||||
app.AccessToken = null;
|
|
||||||
app.TokenValidUntil = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await _db.SaveChangesAsync();
|
await _db.SaveChangesAsync();
|
||||||
|
@ -78,8 +66,14 @@ public partial class FediverseAuthService
|
||||||
return app;
|
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(
|
var tokenResp = await _client.PostAsync(
|
||||||
MastodonTokenUri(app.Domain),
|
MastodonTokenUri(app.Domain),
|
||||||
new FormUrlEncodedContent(
|
new FormUrlEncodedContent(
|
||||||
|
@ -124,109 +118,27 @@ public partial class FediverseAuthService
|
||||||
|
|
||||||
private record MastodonTokenResponse([property: J("access_token")] string AccessToken);
|
private record MastodonTokenResponse([property: J("access_token")] string AccessToken);
|
||||||
|
|
||||||
// TODO: Mastodon's OAuth documentation doesn't specify a "state" parameter. that feels... wrong
|
private async Task<string> GenerateMastodonAuthUrlAsync(
|
||||||
// https://docs.joinmastodon.org/methods/oauth/
|
FediverseApplication app,
|
||||||
private async Task<string> GenerateMastodonAuthUrlAsync(FediverseApplication app)
|
bool forceRefresh
|
||||||
|
)
|
||||||
{
|
{
|
||||||
try
|
if (forceRefresh)
|
||||||
{
|
{
|
||||||
await ValidateMastodonAppAsync(app);
|
_logger.Information(
|
||||||
}
|
"An app credentials refresh was requested for {ApplicationId}, creating a new application",
|
||||||
catch (FoxnounsError.RemoteAuthError e)
|
app.Id
|
||||||
{
|
|
||||||
_logger.Error(
|
|
||||||
e,
|
|
||||||
"Error validating app token for {AppId} on {Instance}",
|
|
||||||
app.Id,
|
|
||||||
app.Domain
|
|
||||||
);
|
);
|
||||||
|
|
||||||
app = await CreateMastodonApplicationAsync(app.Domain, existingAppId: 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"
|
return $"https://{app.Domain}/oauth/authorize?response_type=code"
|
||||||
+ $"&client_id={app.ClientId}"
|
+ $"&client_id={app.ClientId}"
|
||||||
+ $"&scope={HttpUtility.UrlEncode("read:accounts")}"
|
+ $"&scope={HttpUtility.UrlEncode("read:accounts")}"
|
||||||
+ $"&redirect_uri={HttpUtility.UrlEncode(MastodonRedirectUri(app.Domain))}";
|
+ $"&redirect_uri={HttpUtility.UrlEncode(MastodonRedirectUri(app.Domain))}"
|
||||||
}
|
+ $"&state={state}";
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string MastodonTokenUri(string instance) => $"https://{instance}/oauth/token";
|
private static string MastodonTokenUri(string instance) => $"https://{instance}/oauth/token";
|
||||||
|
@ -234,12 +146,17 @@ public partial class FediverseAuthService
|
||||||
private static string MastodonCurrentUserUri(string instance) =>
|
private static string MastodonCurrentUserUri(string instance) =>
|
||||||
$"https://{instance}/api/v1/accounts/verify_credentials";
|
$"https://{instance}/api/v1/accounts/verify_credentials";
|
||||||
|
|
||||||
private static string MastodonCurrentAppUri(string instance) =>
|
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")]
|
||||||
$"https://{instance}/api/v1/apps/verify_credentials";
|
|
||||||
|
|
||||||
private record PartialMastodonApplication(
|
private record PartialMastodonApplication(
|
||||||
[property: J("name")] string Name,
|
[property: J("name")] string Name,
|
||||||
[property: J("client_id")] string ClientId,
|
[property: J("client_id")] string ClientId,
|
||||||
[property: J("client_secret")] string ClientSecret
|
[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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
using Foxnouns.Backend.Database;
|
using Foxnouns.Backend.Database;
|
||||||
using Foxnouns.Backend.Database.Models;
|
using Foxnouns.Backend.Database.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
|
||||||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
||||||
|
|
||||||
namespace Foxnouns.Backend.Services.Auth;
|
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 const string NodeInfoRel = "http://nodeinfo.diaspora.software/ns/schema/2.0";
|
||||||
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
private readonly HttpClient _client;
|
private readonly HttpClient _client;
|
||||||
private readonly DatabaseContext _db;
|
private readonly ILogger _logger;
|
||||||
private readonly Config _config;
|
private readonly Config _config;
|
||||||
|
private readonly DatabaseContext _db;
|
||||||
|
private readonly KeyCacheService _keyCacheService;
|
||||||
private readonly ISnowflakeGenerator _snowflakeGenerator;
|
private readonly ISnowflakeGenerator _snowflakeGenerator;
|
||||||
private readonly IClock _clock;
|
|
||||||
|
|
||||||
public FediverseAuthService(
|
public FediverseAuthService(
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
Config config,
|
Config config,
|
||||||
DatabaseContext db,
|
DatabaseContext db,
|
||||||
ISnowflakeGenerator snowflakeGenerator,
|
KeyCacheService keyCacheService,
|
||||||
IClock clock
|
ISnowflakeGenerator snowflakeGenerator
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
_logger = logger.ForContext<FediverseAuthService>();
|
||||||
_config = config;
|
_config = config;
|
||||||
_db = db;
|
_db = db;
|
||||||
|
_keyCacheService = keyCacheService;
|
||||||
_snowflakeGenerator = snowflakeGenerator;
|
_snowflakeGenerator = snowflakeGenerator;
|
||||||
_clock = clock;
|
|
||||||
_logger = logger.ForContext<FediverseAuthService>();
|
|
||||||
_client = new HttpClient();
|
_client = new HttpClient();
|
||||||
_client.DefaultRequestHeaders.Remove("User-Agent");
|
_client.DefaultRequestHeaders.Remove("User-Agent");
|
||||||
_client.DefaultRequestHeaders.Remove("Accept");
|
_client.DefaultRequestHeaders.Remove("Accept");
|
||||||
|
@ -37,10 +37,10 @@ public partial class FediverseAuthService
|
||||||
_client.DefaultRequestHeaders.Add("Accept", "application/json");
|
_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);
|
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,
|
// 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
|
app.InstanceType switch
|
||||||
{
|
{
|
||||||
FediverseInstanceType.MastodonApi => await GenerateMastodonAuthUrlAsync(app),
|
FediverseInstanceType.MastodonApi => await GenerateMastodonAuthUrlAsync(
|
||||||
|
app,
|
||||||
|
forceRefresh
|
||||||
|
),
|
||||||
FediverseInstanceType.MisskeyApi => throw new NotImplementedException(),
|
FediverseInstanceType.MisskeyApi => throw new NotImplementedException(),
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(app), app.InstanceType, null),
|
_ => throw new ArgumentOutOfRangeException(nameof(app), app.InstanceType, null),
|
||||||
};
|
};
|
||||||
|
|
||||||
public async Task<FediverseUser> GetRemoteFediverseUserAsync(
|
public async Task<FediverseUser> GetRemoteFediverseUserAsync(
|
||||||
FediverseApplication app,
|
FediverseApplication app,
|
||||||
string code
|
string code,
|
||||||
|
string state
|
||||||
) =>
|
) =>
|
||||||
app.InstanceType switch
|
app.InstanceType switch
|
||||||
{
|
{
|
||||||
FediverseInstanceType.MastodonApi => await GetMastodonUserAsync(app, code),
|
FediverseInstanceType.MastodonApi => await GetMastodonUserAsync(app, code, state),
|
||||||
FediverseInstanceType.MisskeyApi => throw new NotImplementedException(),
|
FediverseInstanceType.MisskeyApi => throw new NotImplementedException(),
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(app), app.InstanceType, null),
|
_ => throw new ArgumentOutOfRangeException(nameof(app), app.InstanceType, null),
|
||||||
};
|
};
|
||||||
|
|
|
@ -32,7 +32,7 @@ public class MemberRendererService(DatabaseContext db, Config config)
|
||||||
member.Id,
|
member.Id,
|
||||||
member.Sid,
|
member.Sid,
|
||||||
member.Name,
|
member.Name,
|
||||||
member.DisplayName,
|
member.DisplayName ?? member.Name,
|
||||||
member.Bio,
|
member.Bio,
|
||||||
AvatarUrlFor(member),
|
AvatarUrlFor(member),
|
||||||
member.Links,
|
member.Links,
|
||||||
|
@ -60,7 +60,7 @@ public class MemberRendererService(DatabaseContext db, Config config)
|
||||||
member.Id,
|
member.Id,
|
||||||
member.Sid,
|
member.Sid,
|
||||||
member.Name,
|
member.Name,
|
||||||
member.DisplayName,
|
member.DisplayName ?? member.Name,
|
||||||
member.Bio,
|
member.Bio,
|
||||||
AvatarUrlFor(member),
|
AvatarUrlFor(member),
|
||||||
member.Names,
|
member.Names,
|
||||||
|
@ -87,7 +87,7 @@ public class MemberRendererService(DatabaseContext db, Config config)
|
||||||
Snowflake Id,
|
Snowflake Id,
|
||||||
string Sid,
|
string Sid,
|
||||||
string Name,
|
string Name,
|
||||||
string? DisplayName,
|
string DisplayName,
|
||||||
string? Bio,
|
string? Bio,
|
||||||
string? AvatarUrl,
|
string? AvatarUrl,
|
||||||
IEnumerable<FieldEntry> Names,
|
IEnumerable<FieldEntry> Names,
|
||||||
|
@ -99,7 +99,7 @@ public class MemberRendererService(DatabaseContext db, Config config)
|
||||||
Snowflake Id,
|
Snowflake Id,
|
||||||
string Sid,
|
string Sid,
|
||||||
string Name,
|
string Name,
|
||||||
string? DisplayName,
|
string DisplayName,
|
||||||
string? Bio,
|
string? Bio,
|
||||||
string? AvatarUrl,
|
string? AvatarUrl,
|
||||||
string[] Links,
|
string[] Links,
|
||||||
|
|
|
@ -162,6 +162,7 @@ public static partial class ValidationUtils
|
||||||
}
|
}
|
||||||
|
|
||||||
public const int MaxBioLength = 1024;
|
public const int MaxBioLength = 1024;
|
||||||
|
public const int MaxAvatarLength = 1_500_000;
|
||||||
|
|
||||||
public static ValidationError? ValidateBio(string? bio)
|
public static ValidationError? ValidateBio(string? bio)
|
||||||
{
|
{
|
||||||
|
@ -183,7 +184,10 @@ public static partial class ValidationUtils
|
||||||
return avatar?.Length switch
|
return avatar?.Length switch
|
||||||
{
|
{
|
||||||
0 => ValidationError.GenericValidationError("Avatar cannot be empty", null),
|
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,
|
_ => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,6 +96,19 @@
|
||||||
"Mono.TextTemplating": "2.2.1"
|
"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": {
|
"Minio": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[6.0.3, )",
|
"requested": "[6.0.3, )",
|
||||||
|
@ -246,6 +259,16 @@
|
||||||
"Swashbuckle.AspNetCore.SwaggerUI": "6.6.2"
|
"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": {
|
"System.Text.RegularExpressions": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[4.3.1, )",
|
"requested": "[4.3.1, )",
|
||||||
|
@ -412,22 +435,10 @@
|
||||||
},
|
},
|
||||||
"Microsoft.Extensions.Caching.Abstractions": {
|
"Microsoft.Extensions.Caching.Abstractions": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "8.0.0",
|
"resolved": "9.0.0",
|
||||||
"contentHash": "3KuSxeHoNYdxVYfg2IRZCThcrlJ1XJqIXkAWikCsbm5C/bCjv7G0WoKDyuR98Q+T607QT2Zl5GsbGRkENcV2yQ==",
|
"contentHash": "FPWZAa9c0H4dvOj351iR1jkUIs4u9ykL4Bm592yhjDyO5lCoWd+TMAHx2EMbarzUvCvgjWjJIoC6//Q9kH6YhA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
"Microsoft.Extensions.Primitives": "9.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.Configuration": {
|
"Microsoft.Extensions.Configuration": {
|
||||||
|
@ -465,8 +476,8 @@
|
||||||
},
|
},
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "8.0.1",
|
"resolved": "9.0.0",
|
||||||
"contentHash": "fGLiCRLMYd00JYpClraLjJTNKLmMJPnqxMaiRzEBIIvevlzxz33mXy39Lkd48hu1G+N21S7QpaO5ZzKsI6FRuA=="
|
"contentHash": "+6f2qv2a3dLwd5w6JanPIPs47CxRbnk+ZocMJUhv9NxP88VlOcJYZs9jY+MYSjxvady08bUZn6qgiNh7DadGgg=="
|
||||||
},
|
},
|
||||||
"Microsoft.Extensions.DependencyModel": {
|
"Microsoft.Extensions.DependencyModel": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
|
@ -542,10 +553,11 @@
|
||||||
},
|
},
|
||||||
"Microsoft.Extensions.Logging.Abstractions": {
|
"Microsoft.Extensions.Logging.Abstractions": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "8.0.0",
|
"resolved": "9.0.0",
|
||||||
"contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==",
|
"contentHash": "g0UfujELzlLbHoVG8kPKVBaW470Ewi+jnptGS9KUi6jcb+k2StujtK3m26DFSGGwQ/+bVgZfsWqNzlP6YOejvw==",
|
||||||
"dependencies": {
|
"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": {
|
"Microsoft.Extensions.Logging.Configuration": {
|
||||||
|
@ -570,11 +582,11 @@
|
||||||
},
|
},
|
||||||
"Microsoft.Extensions.Options": {
|
"Microsoft.Extensions.Options": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "8.0.0",
|
"resolved": "9.0.0",
|
||||||
"contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==",
|
"contentHash": "y2146b3jrPI3Q0lokKXdKLpmXqakYbDIPDV6r3M8SqvSf45WwOTzkyfDpxnZXJsJQEpAsAqjUq5Pu8RCJMjubg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
|
||||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
"Microsoft.Extensions.Primitives": "9.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Microsoft.Extensions.Options.ConfigurationExtensions": {
|
"Microsoft.Extensions.Options.ConfigurationExtensions": {
|
||||||
|
@ -591,8 +603,8 @@
|
||||||
},
|
},
|
||||||
"Microsoft.Extensions.Primitives": {
|
"Microsoft.Extensions.Primitives": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "8.0.0",
|
"resolved": "9.0.0",
|
||||||
"contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g=="
|
"contentHash": "N3qEBzmLMYiASUlKxxFIISP4AiwuPTHF5uCh+2CWSwwzAJiIYx0kBJsS30cp1nvhSySFAVi30jecD307jV+8Kg=="
|
||||||
},
|
},
|
||||||
"Microsoft.NETCore.Platforms": {
|
"Microsoft.NETCore.Platforms": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
|
@ -983,8 +995,8 @@
|
||||||
},
|
},
|
||||||
"System.Diagnostics.DiagnosticSource": {
|
"System.Diagnostics.DiagnosticSource": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "8.0.0",
|
"resolved": "9.0.0",
|
||||||
"contentHash": "c9xLpVz6PL9lp/djOWtk5KPDZq3cSYpmXoJQY524EOtuFl5z9ZtsotpsyrDW40U1DRnQSYvcPKEUV0X//u6gkQ=="
|
"contentHash": "ddppcFpnbohLWdYKr/ZeLZHmmI+DXFgZ3Snq+/E7SwcdW4UnvxmaugkwGywvGVWkHPGCSZjCP+MLzu23AL5SDw=="
|
||||||
},
|
},
|
||||||
"System.Diagnostics.Tracing": {
|
"System.Diagnostics.Tracing": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
|
@ -1072,8 +1084,8 @@
|
||||||
},
|
},
|
||||||
"System.IO.Pipelines": {
|
"System.IO.Pipelines": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "6.0.3",
|
"resolved": "9.0.0",
|
||||||
"contentHash": "ryTgF+iFkpGZY1vRQhfCzX0xTdlV3pyaTTqRu2ETbEv+HlV7O6y7hyQURnghNIXvctl5DuZ//Dpks6HdL/Txgw=="
|
"contentHash": "eA3cinogwaNB4jdjQHOP3Z3EuyiDII7MT35jgtnsA4vkn0LUrrSHsU0nzHTzFzmaFYeKV7MYyMxOocFzsBHpTw=="
|
||||||
},
|
},
|
||||||
"System.Linq": {
|
"System.Linq": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
|
@ -1484,16 +1496,8 @@
|
||||||
},
|
},
|
||||||
"System.Text.Encodings.Web": {
|
"System.Text.Encodings.Web": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "8.0.0",
|
"resolved": "9.0.0",
|
||||||
"contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ=="
|
"contentHash": "e2hMgAErLbKyUUwt18qSBf9T5Y+SFAL3ZedM8fLupkVj8Rj2PZ9oxQ37XX2LF8fTO1wNIxvKpihD7Of7D/NxZw=="
|
||||||
},
|
|
||||||
"System.Text.Json": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.4",
|
|
||||||
"contentHash": "bAkhgDJ88XTsqczoxEMliSrpijKZHhbJQldhAmObj/RbrN3sU5dcokuXmWJWsdQAhiMJ9bTayWsL1C9fbbCRhw==",
|
|
||||||
"dependencies": {
|
|
||||||
"System.Text.Encodings.Web": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"System.Threading": {
|
"System.Threading": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
|
|
Loading…
Reference in a new issue