The default transport for IAltchaVerifier / IAltchaChallengeProvider —
calls a self-hosted altcha service over gRPC.
Wire contract
- Protos/altcha.proto defines svrnty.cqrs.altcha.v1.AltchaService with
CreateChallenge + VerifyChallenge RPCs. Shipped in this package as
source-of-truth; Go (and other) implementations vendor a copy.
- Challenge.challenge_hash is named (not "challenge") to avoid a C#
property/class name collision; the MinimalApi widget JSON remaps.
Runtime
- AltchaGrpcVerifier maps RpcException → AltchaVerifyResult.Fail with
a diagnostic reason ("verify-timeout", "service-unavailable", etc.)
so the auth check surfaces a clean Unauthorized without leaking
transport detail.
- AltchaGrpcChallengeProvider lets create-challenge failures bubble
(challenge endpoint should 5xx if altcha is down — clients retry).
- AltchaGrpcOptions.TokenProvider hook for consumer-supplied HMAC
service-token minting (plan-b will plug in ServiceTokenIssuer).
- AddGrpcClient<AltchaServiceClient> registered with HttpClientFactory.
AddSvrntyAltchaGrpcVerifier(Action<...>) and overload binding from
IConfiguration cover both wiring styles.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
67 lines
2.6 KiB
C#
67 lines
2.6 KiB
C#
using Grpc.Core;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using Svrnty.CQRS.Altcha.Abstractions;
|
|
|
|
namespace Svrnty.CQRS.Altcha.Grpc;
|
|
|
|
/// <summary>
|
|
/// Default <see cref="IAltchaVerifier"/> backed by gRPC. Calls
|
|
/// <c>AltchaService.VerifyChallenge</c> on the configured endpoint and
|
|
/// maps any failure (transport error, deadline, server-reported failure)
|
|
/// to <see cref="AltchaVerifyResult.Fail"/>. Verification failures are
|
|
/// safe defaults — callers see an <c>Unauthorized</c> outcome from the
|
|
/// auth check.
|
|
/// </summary>
|
|
public sealed class AltchaGrpcVerifier : IAltchaVerifier
|
|
{
|
|
private readonly AltchaService.AltchaServiceClient _client;
|
|
private readonly IOptions<AltchaGrpcOptions> _options;
|
|
private readonly ILogger<AltchaGrpcVerifier> _logger;
|
|
|
|
public AltchaGrpcVerifier(
|
|
AltchaService.AltchaServiceClient client,
|
|
IOptions<AltchaGrpcOptions> options,
|
|
ILogger<AltchaGrpcVerifier> logger)
|
|
{
|
|
_client = client;
|
|
_options = options;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<AltchaVerifyResult> VerifyAsync(string payload, CancellationToken cancellationToken = default)
|
|
{
|
|
var opts = _options.Value;
|
|
try
|
|
{
|
|
var metadata = await AltchaCallCredentials.BuildMetadataAsync(opts, cancellationToken);
|
|
var deadline = DateTime.UtcNow.Add(opts.CallTimeout);
|
|
|
|
var response = await _client.VerifyChallengeAsync(
|
|
new VerifyChallengeRequest { Payload = payload },
|
|
headers: metadata,
|
|
deadline: deadline,
|
|
cancellationToken: cancellationToken);
|
|
|
|
return response.Ok
|
|
? AltchaVerifyResult.Success
|
|
: AltchaVerifyResult.Fail(string.IsNullOrEmpty(response.Reason) ? "invalid" : response.Reason);
|
|
}
|
|
catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded)
|
|
{
|
|
_logger.LogWarning(ex, "Altcha verify timed out against {Endpoint}.", opts.Endpoint);
|
|
return AltchaVerifyResult.Fail("verify-timeout");
|
|
}
|
|
catch (RpcException ex) when (ex.StatusCode == StatusCode.Unavailable)
|
|
{
|
|
_logger.LogWarning(ex, "Altcha service unavailable at {Endpoint}.", opts.Endpoint);
|
|
return AltchaVerifyResult.Fail("service-unavailable");
|
|
}
|
|
catch (RpcException ex)
|
|
{
|
|
_logger.LogWarning(ex, "Altcha verify failed against {Endpoint}: {Status}", opts.Endpoint, ex.StatusCode);
|
|
return AltchaVerifyResult.Fail("rpc-error");
|
|
}
|
|
}
|
|
}
|