using System.Reflection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Svrnty.CQRS.Abstractions.Security; using Svrnty.CQRS.Altcha.Abstractions; namespace Svrnty.CQRS.Altcha; /// /// The single check that powers the . Plays both /// and /// roles so a request payload of either kind is gated identically. /// /// /// Self-applying: the check no-ops (returns ) /// for any request whose type isn't decorated with . /// Resolves from the request scope per call so the /// check is agnostic to the verifier's lifetime. /// public sealed class AltchaAuthorizationCheck : ICommandAuthorizationCheck, IQueryAuthorizationCheck { internal const string ReasonItemKey = "altcha_reason"; internal const string MobileAttestedItemKey = "mobile_attested"; private readonly ILogger _logger; public AltchaAuthorizationCheck(ILogger logger) { _logger = logger; } public Task CheckAsync( CommandAuthorizationCheckContext context, CancellationToken cancellationToken = default) => CheckCoreAsync(context, context.CommandType, context.Command, cancellationToken); public Task CheckAsync( QueryAuthorizationCheckContext context, CancellationToken cancellationToken = default) => CheckCoreAsync(context, context.QueryType, context.Query, cancellationToken); private async Task CheckCoreAsync( AuthorizationCheckContext context, Type subjectType, object subject, CancellationToken cancellationToken) { var altchaAttr = subjectType.GetCustomAttribute(inherit: false); if (altchaAttr is null) return AuthorizationResult.Allowed; if (altchaAttr.AllowMobileAttestationBypass && context.Items.TryGetValue(MobileAttestedItemKey, out var attested) && attested is true) { _logger.LogDebug("Altcha bypassed for {Type}: mobile attestation present.", subjectType.FullName); return AuthorizationResult.Allowed; } if (subject is not IHasAltchaSolution carrier) { // Developer error: [Altcha] on a request that doesn't carry the solution field. _logger.LogError( "[Altcha] is set on {Type} but the type does not implement IHasAltchaSolution. " + "Verification cannot proceed — treating as forbidden.", subjectType.FullName); context.Items[ReasonItemKey] = "misconfigured"; return AuthorizationResult.Forbidden; } if (string.IsNullOrWhiteSpace(carrier.AltchaSolution)) { _logger.LogWarning("Altcha required for {Type} but no solution was supplied.", subjectType.FullName); context.Items[ReasonItemKey] = "missing"; return AuthorizationResult.Unauthorized; } var verifier = context.Services.GetRequiredService(); var result = await verifier.VerifyAsync(carrier.AltchaSolution, cancellationToken); if (!result.Ok) { _logger.LogWarning( "Altcha verification rejected {Type}: reason={Reason}", subjectType.FullName, result.Reason ?? "unspecified"); context.Items[ReasonItemKey] = result.Reason ?? "invalid"; return AuthorizationResult.Unauthorized; } return AuthorizationResult.Allowed; } }