| | | 1 | | // SPDX-FileCopyrightText: 2026 Alper Çelik <[email protected]> |
| | | 2 | | // |
| | | 3 | | // SPDX-License-Identifier: AGPL-3.0-or-later |
| | | 4 | | |
| | | 5 | | using System.Buffers.Text; |
| | | 6 | | using System.Security.Cryptography; |
| | | 7 | | using System.Text; |
| | | 8 | | using System.Text.Unicode; |
| | | 9 | | using Api.Auth.Models; |
| | | 10 | | using Api.Auth.Utils; |
| | | 11 | | using Api.Database; |
| | | 12 | | using Api.Database.Utils; |
| | | 13 | | using Api.Utils; |
| | | 14 | | using FluentValidation; |
| | | 15 | | using HotChocolate.Authorization; |
| | | 16 | | using NodaTime; |
| | | 17 | | |
| | | 18 | | namespace Api.Auth.Mutations; |
| | | 19 | | |
| | | 20 | | /// <param name="CurrentSecond">Unix timestamp in seconds (GraphQL Long).</param> |
| | | 21 | | public record LoginInstanceServiceInput( |
| | | 22 | | string AuthProof, |
| | | 23 | | string Salt, |
| | | 24 | | long CurrentSecond |
| | | 25 | | ); |
| | | 26 | | |
| | 1 | 27 | | public record LoginInstanceServicePayload(string Token); |
| | | 28 | | |
| | | 29 | | [MutationType] |
| | | 30 | | public static partial class LoginInstanceServiceMutations |
| | | 31 | | { |
| | | 32 | | [AllowAnonymous] |
| | | 33 | | public static async Task<LoginInstanceServicePayload> LoginTheInstanceService( |
| | | 34 | | [Service] PGContext db, |
| | | 35 | | [Service] IEFTransactionDIAccessorService txAccessor, |
| | | 36 | | [Service] IConfiguration config, |
| | | 37 | | [Service] IValidator<LoginInstanceServiceInput> validator, |
| | | 38 | | LoginInstanceServiceInput input, |
| | | 39 | | CancellationToken ct |
| | | 40 | | ) |
| | | 41 | | { |
| | | 42 | | await validator.ValidateOrThrowAsync(input, ct); |
| | | 43 | | |
| | | 44 | | var proof = HKDF.DeriveKey( |
| | | 45 | | HashAlgorithmName.SHA256, |
| | | 46 | | Base64Url.DecodeFromChars(config["ProjectReadingSecretSeed"]), |
| | | 47 | | 720 / 8, |
| | | 48 | | Base64Url.DecodeFromChars(input.Salt), |
| | | 49 | | Encoding.UTF8.GetBytes("The Instance Service Auth : " + input.CurrentSecond) |
| | | 50 | | ); |
| | | 51 | | |
| | | 52 | | if (Geralt.ConstantTime.Equals(proof, Base64Url.DecodeFromChars(input.AuthProof))) |
| | | 53 | | { |
| | | 54 | | string token = await AuthUtils.CreateRemoteServiceSession( |
| | | 55 | | RemoteService.TheInstanceServiceId, |
| | | 56 | | db |
| | | 57 | | ); |
| | | 58 | | await db.SaveChangesAsync(ct); |
| | | 59 | | |
| | | 60 | | await txAccessor.CommitTX(ct); |
| | | 61 | | return new(token); |
| | | 62 | | } |
| | | 63 | | |
| | | 64 | | throw new GraphQLException( |
| | | 65 | | ErrorBuilder |
| | | 66 | | .New() |
| | | 67 | | .SetMessage("Invalid Credentials") |
| | | 68 | | .SetCode(ErrorCodes.INVALID_CREDS) |
| | | 69 | | .Build() |
| | | 70 | | ); |
| | | 71 | | } |
| | | 72 | | } |
| | | 73 | | |
| | | 74 | | public class LoginInstanceServiceInputValidator |
| | | 75 | | : AbstractValidator<LoginInstanceServiceInput> |
| | | 76 | | { |
| | | 77 | | public LoginInstanceServiceInputValidator() |
| | | 78 | | { |
| | | 79 | | RuleFor(i => i.AuthProof).Must(str => Base64Url.IsValid(str)).Length(120); |
| | | 80 | | |
| | | 81 | | RuleFor(i => i.Salt).Must(str => Base64Url.IsValid(str)).MinimumLength(22); |
| | | 82 | | |
| | | 83 | | RuleFor(i => i.CurrentSecond) |
| | | 84 | | .LessThan((_) => Now().Plus(Duration.FromMinutes(5)).ToUnixTimeSeconds()) |
| | | 85 | | .GreaterThan((_) => Now().Minus(Duration.FromMinutes(5)).ToUnixTimeSeconds()); |
| | | 86 | | } |
| | | 87 | | } |