feat: misskey auth
This commit is contained in:
parent
51e335f090
commit
77c3047b1e
6 changed files with 214 additions and 16 deletions
|
@ -161,20 +161,13 @@ public class FediverseAuthController(
|
|||
[FromBody] FediverseCallbackRequest req
|
||||
)
|
||||
{
|
||||
await remoteAuthService.ValidateAddAccountStateAsync(
|
||||
req.State,
|
||||
CurrentUser!.Id,
|
||||
AuthType.Fediverse,
|
||||
req.Instance
|
||||
);
|
||||
|
||||
FediverseApplication app = await fediverseAuthService.GetApplicationAsync(req.Instance);
|
||||
FediverseAuthService.FediverseUser remoteUser =
|
||||
await fediverseAuthService.GetRemoteFediverseUserAsync(app, req.Code);
|
||||
try
|
||||
{
|
||||
AuthMethod authMethod = await authService.AddAuthMethodAsync(
|
||||
CurrentUser.Id,
|
||||
CurrentUser!.Id,
|
||||
AuthType.Fediverse,
|
||||
remoteUser.Id,
|
||||
remoteUser.Username,
|
||||
|
|
|
@ -59,4 +59,4 @@ public record EmailCallbackRequest(string State);
|
|||
|
||||
public record EmailChangePasswordRequest(string Current, string New);
|
||||
|
||||
public record FediverseCallbackRequest(string Instance, string Code, string State);
|
||||
public record FediverseCallbackRequest(string Instance, string Code, string? State = null);
|
||||
|
|
|
@ -91,6 +91,34 @@ public static class KeyCacheExtensions
|
|||
string state,
|
||||
CancellationToken ct = default
|
||||
) => await keyCacheService.GetKeyAsync<AddExtraAccountState>($"add_account:{state}", true, ct);
|
||||
|
||||
public static async Task<string> GenerateForgotPasswordStateAsync(
|
||||
this KeyCacheService keyCacheService,
|
||||
string email,
|
||||
Snowflake userId,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
string state = AuthUtils.RandomToken();
|
||||
await keyCacheService.SetKeyAsync(
|
||||
$"forgot_password:{state}",
|
||||
new ForgotPasswordState(email, userId),
|
||||
Duration.FromHours(1),
|
||||
ct
|
||||
);
|
||||
return state;
|
||||
}
|
||||
|
||||
public static async Task<ForgotPasswordState?> GetForgotPasswordStateAsync(
|
||||
this KeyCacheService keyCacheService,
|
||||
string state,
|
||||
CancellationToken ct = default
|
||||
) =>
|
||||
await keyCacheService.GetKeyAsync<ForgotPasswordState>(
|
||||
$"forgot_password:{state}",
|
||||
true,
|
||||
ct
|
||||
);
|
||||
}
|
||||
|
||||
public record RegisterEmailState(
|
||||
|
@ -98,4 +126,6 @@ public record RegisterEmailState(
|
|||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] Snowflake? ExistingUserId
|
||||
);
|
||||
|
||||
public record ForgotPasswordState(string Email, Snowflake UserId);
|
||||
|
||||
public record AddExtraAccountState(AuthType AuthType, Snowflake UserId, string? Instance = null);
|
||||
|
|
170
Foxnouns.Backend/Services/Auth/FediverseAuthService.Misskey.cs
Normal file
170
Foxnouns.Backend/Services/Auth/FediverseAuthService.Misskey.cs
Normal file
|
@ -0,0 +1,170 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using System.Net;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Web;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Extensions;
|
||||
|
||||
namespace Foxnouns.Backend.Services.Auth;
|
||||
|
||||
public partial class FediverseAuthService
|
||||
{
|
||||
private static string MisskeyAppUri(string instance) => $"https://{instance}/api/app/create";
|
||||
|
||||
private static string MisskeyTokenUri(string instance) =>
|
||||
$"https://{instance}/api/auth/session/userkey";
|
||||
|
||||
private static string MisskeyGenerateSessionUri(string instance) =>
|
||||
$"https://{instance}/api/auth/session/generate";
|
||||
|
||||
private async Task<FediverseApplication> CreateMisskeyApplicationAsync(
|
||||
string instance,
|
||||
Snowflake? existingAppId = null
|
||||
)
|
||||
{
|
||||
HttpResponseMessage resp = await _client.PostAsJsonAsync(
|
||||
MisskeyAppUri(instance),
|
||||
new CreateMisskeyApplicationRequest(
|
||||
$"pronouns.cc (+{_config.BaseUrl})",
|
||||
$"pronouns.cc on {_config.BaseUrl}",
|
||||
["read:account"],
|
||||
MastodonRedirectUri(instance)
|
||||
)
|
||||
);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
|
||||
PartialMisskeyApplication? misskeyApp =
|
||||
await resp.Content.ReadFromJsonAsync<PartialMisskeyApplication>();
|
||||
if (misskeyApp == null)
|
||||
{
|
||||
throw new FoxnounsError(
|
||||
$"Application created on Misskey-compatible instance {instance} was null"
|
||||
);
|
||||
}
|
||||
|
||||
FediverseApplication app;
|
||||
|
||||
if (existingAppId == null)
|
||||
{
|
||||
app = new FediverseApplication
|
||||
{
|
||||
Id = existingAppId ?? _snowflakeGenerator.GenerateSnowflake(),
|
||||
ClientId = misskeyApp.Id,
|
||||
ClientSecret = misskeyApp.Secret,
|
||||
Domain = instance,
|
||||
InstanceType = FediverseInstanceType.MisskeyApi,
|
||||
};
|
||||
|
||||
_db.Add(app);
|
||||
}
|
||||
else
|
||||
{
|
||||
app =
|
||||
await _db.FediverseApplications.FindAsync(existingAppId)
|
||||
?? throw new FoxnounsError($"Existing app with ID {existingAppId} was null");
|
||||
|
||||
app.ClientId = misskeyApp.Id;
|
||||
app.ClientSecret = misskeyApp.Secret;
|
||||
app.InstanceType = FediverseInstanceType.MisskeyApi;
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private record GetMisskeySessionUserKeyRequest(
|
||||
[property: JsonPropertyName("appSecret")] string Secret,
|
||||
[property: JsonPropertyName("token")] string Token
|
||||
);
|
||||
|
||||
private record GetMisskeySessionUserKeyResponse(
|
||||
[property: JsonPropertyName("user")] FediverseUser User
|
||||
);
|
||||
|
||||
private async Task<FediverseUser> GetMisskeyUserAsync(FediverseApplication app, string code)
|
||||
{
|
||||
HttpResponseMessage resp = await _client.PostAsJsonAsync(
|
||||
MisskeyTokenUri(app.Domain),
|
||||
new GetMisskeySessionUserKeyRequest(app.ClientSecret, code)
|
||||
);
|
||||
if (resp.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
throw new FoxnounsError($"Application for instance {app.Domain} was invalid");
|
||||
}
|
||||
|
||||
resp.EnsureSuccessStatusCode();
|
||||
GetMisskeySessionUserKeyResponse? userResp =
|
||||
await resp.Content.ReadFromJsonAsync<GetMisskeySessionUserKeyResponse>();
|
||||
if (userResp == null)
|
||||
{
|
||||
throw new FoxnounsError($"User response from instance {app.Domain} was invalid");
|
||||
}
|
||||
|
||||
return userResp.User;
|
||||
}
|
||||
|
||||
private async Task<string> GenerateMisskeyAuthUrlAsync(
|
||||
FediverseApplication app,
|
||||
bool forceRefresh,
|
||||
string? state = null
|
||||
)
|
||||
{
|
||||
if (forceRefresh)
|
||||
{
|
||||
_logger.Information(
|
||||
"An app credentials refresh was requested for {ApplicationId}, creating a new application",
|
||||
app.Id
|
||||
);
|
||||
app = await CreateMisskeyApplicationAsync(app.Domain, app.Id);
|
||||
}
|
||||
|
||||
HttpResponseMessage resp = await _client.PostAsJsonAsync(
|
||||
MisskeyGenerateSessionUri(app.Domain),
|
||||
new CreateMisskeySessionUriRequest(app.ClientSecret)
|
||||
);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
|
||||
CreateMisskeySessionUriResponse? misskeyResp =
|
||||
await resp.Content.ReadFromJsonAsync<CreateMisskeySessionUriResponse>();
|
||||
if (misskeyResp == null)
|
||||
throw new FoxnounsError($"Session create response for app {app.Id} was null");
|
||||
|
||||
return misskeyResp.Url;
|
||||
}
|
||||
|
||||
private record CreateMisskeySessionUriRequest(
|
||||
[property: JsonPropertyName("appSecret")] string Secret
|
||||
);
|
||||
|
||||
private record CreateMisskeySessionUriResponse(
|
||||
[property: JsonPropertyName("token")] string Token,
|
||||
[property: JsonPropertyName("url")] string Url
|
||||
);
|
||||
|
||||
private record CreateMisskeyApplicationRequest(
|
||||
[property: JsonPropertyName("name")] string Name,
|
||||
[property: JsonPropertyName("description")] string Description,
|
||||
[property: JsonPropertyName("permission")] string[] Permissions,
|
||||
[property: JsonPropertyName("callbackUrl")] string CallbackUrl
|
||||
);
|
||||
|
||||
private record PartialMisskeyApplication(
|
||||
[property: JsonPropertyName("id")] string Id,
|
||||
[property: JsonPropertyName("secret")] string Secret
|
||||
);
|
||||
}
|
|
@ -81,11 +81,11 @@ public partial class FediverseAuthService
|
|||
string softwareName = await GetSoftwareNameAsync(instance);
|
||||
|
||||
if (IsMastodonCompatible(softwareName))
|
||||
{
|
||||
return await CreateMastodonApplicationAsync(instance);
|
||||
}
|
||||
if (IsMisskeyCompatible(softwareName))
|
||||
return await CreateMisskeyApplicationAsync(instance);
|
||||
|
||||
throw new NotImplementedException();
|
||||
throw new ApiError.BadRequest($"{softwareName} is not a supported instance type, sorry.");
|
||||
}
|
||||
|
||||
private async Task<string> GetSoftwareNameAsync(string instance)
|
||||
|
@ -129,7 +129,11 @@ public partial class FediverseAuthService
|
|||
forceRefresh,
|
||||
state
|
||||
),
|
||||
FediverseInstanceType.MisskeyApi => throw new NotImplementedException(),
|
||||
FediverseInstanceType.MisskeyApi => await GenerateMisskeyAuthUrlAsync(
|
||||
app,
|
||||
forceRefresh,
|
||||
state
|
||||
),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(app), app.InstanceType, null),
|
||||
};
|
||||
|
||||
|
@ -141,7 +145,7 @@ public partial class FediverseAuthService
|
|||
app.InstanceType switch
|
||||
{
|
||||
FediverseInstanceType.MastodonApi => await GetMastodonUserAsync(app, code, state),
|
||||
FediverseInstanceType.MisskeyApi => throw new NotImplementedException(),
|
||||
FediverseInstanceType.MisskeyApi => await GetMisskeyUserAsync(app, code),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(app), app.InstanceType, null),
|
||||
};
|
||||
|
||||
|
|
|
@ -5,9 +5,10 @@ import createRegisterAction from "$lib/actions/register";
|
|||
export const load = createCallbackLoader("fediverse", async ({ params, url }) => {
|
||||
const code = url.searchParams.get("code") as string | null;
|
||||
const state = url.searchParams.get("state") as string | null;
|
||||
if (!code || !state) throw new ApiError(undefined, ErrorCode.BadRequest).obj;
|
||||
const token = url.searchParams.get("token") as string | null;
|
||||
if ((!code || !state) && !token) throw new ApiError(undefined, ErrorCode.BadRequest).obj;
|
||||
|
||||
return { code, state, instance: params.instance! };
|
||||
return { code: code || token, state, instance: params.instance! };
|
||||
});
|
||||
|
||||
export const actions = {
|
||||
|
|
Loading…
Reference in a new issue