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>
70 lines
2.7 KiB
Protocol Buffer
70 lines
2.7 KiB
Protocol Buffer
syntax = "proto3";
|
|
|
|
package svrnty.cqrs.altcha.v1;
|
|
|
|
option go_package = "svrnty.cqrs.altcha.v1;altchapb";
|
|
option csharp_namespace = "Svrnty.CQRS.Altcha.Grpc";
|
|
|
|
// AltchaService is the wire contract between any backend that gates
|
|
// commands/queries with the [Altcha] attribute and a self-hosted Altcha
|
|
// implementation. The default .NET client lives in this package; a
|
|
// reference Go server lives in plan-b's projects/altcha (which vendors
|
|
// this proto). Keep this file the source of truth for both sides.
|
|
service AltchaService {
|
|
// Mint a fresh challenge for the widget to solve. Server-stateless:
|
|
// the response embeds an HMAC signature that VerifyChallenge re-checks.
|
|
rpc CreateChallenge(CreateChallengeRequest) returns (Challenge);
|
|
|
|
// Verify a widget-produced solution payload. The server checks
|
|
// signature + expiry + PoW correctness, then atomically claims the
|
|
// challenge in a replay-protection cache (e.g. Redis SETNX) so each
|
|
// solution is single-use across all server replicas.
|
|
rpc VerifyChallenge(VerifyChallengeRequest) returns (VerifyChallengeResponse);
|
|
}
|
|
|
|
message CreateChallengeRequest {
|
|
// Optional per-request complexity override (PoW search-space upper
|
|
// bound). Higher = slower client solve. Server clamps to its
|
|
// configured min/max. Omit to use the server default.
|
|
optional uint32 complexity = 1;
|
|
}
|
|
|
|
message Challenge {
|
|
// Hashing algorithm — e.g. "SHA-256" (v2 default). Future versions may
|
|
// upgrade to PBKDF2 / Argon2; the field lets the widget pick the
|
|
// right solver.
|
|
string algorithm = 1;
|
|
|
|
// Hex-encoded hash the client must find a preimage for. Field is
|
|
// named "challenge_hash" rather than "challenge" to avoid a C#
|
|
// property/class name collision (the generated message class is
|
|
// also named Challenge); JSON projection for the widget remaps it.
|
|
string challenge_hash = 2;
|
|
|
|
// Hex-encoded random salt. Typically embeds the expiry timestamp so
|
|
// verifiers can re-derive the TTL without server state.
|
|
string salt = 3;
|
|
|
|
// HMAC-SHA256(secret, algorithm|challenge|salt|maxnumber). Lets
|
|
// VerifyChallenge confirm the challenge was issued by this server.
|
|
string signature = 4;
|
|
|
|
// PoW search-space upper bound. Equals the complexity used.
|
|
uint32 maxnumber = 5;
|
|
}
|
|
|
|
message VerifyChallengeRequest {
|
|
// Base64-encoded JSON payload produced by the Altcha widget — the
|
|
// value carried over the wire on IHasAltchaSolution.AltchaSolution.
|
|
string payload = 1;
|
|
}
|
|
|
|
message VerifyChallengeResponse {
|
|
bool ok = 1;
|
|
|
|
// Diagnostic only; not surfaced to end users. Suggested values:
|
|
// "signature-invalid", "expired", "pow-incorrect", "replayed",
|
|
// "redis-unreachable", "malformed".
|
|
string reason = 2;
|
|
}
|