using ZB.MOM.WW.Auth.Abstractions.ApiKeys; namespace ZB.MOM.WW.Auth.ApiKeys; /// /// Verifies presented API-key credentials against the key store, returning a structured, /// discriminated result. The pipeline is fail-closed: any inability to positively verify a /// credential yields a failure rather than a success. /// /// /// The failure reason is discriminated for the caller/audit pipeline, but the verifier returns a /// structured result rather than throwing (the caller decides the opaque client-facing message). /// The only exception path is cancellation. A successful identity carries the key's scopes and the /// opaque ConstraintsJson blob (which the verifier does not interpret); it never carries the /// presented secret, the pepper, or the stored secret hash. /// public sealed class ApiKeyVerifier( ApiKeyOptions options, IApiKeyStore store, IApiKeyPepperProvider pepperProvider, TimeProvider? timeProvider = null) : IApiKeyVerifier { private readonly TimeProvider _timeProvider = timeProvider ?? TimeProvider.System; /// public async Task VerifyAsync(string authorizationHeader, CancellationToken ct) { ct.ThrowIfCancellationRequested(); // 1. Parse the header/token. Malformed or wrong-prefix credentials are indistinguishable // from a missing credential and are reported uniformly. ParsedApiKey? parsed = ApiKeyParser.TryParse(authorizationHeader, options.TokenPrefix); if (parsed is null) { return Fail(ApiKeyFailure.MissingOrMalformed); } // 2. Resolve the pepper before touching the store. Without it, no verification is possible, // so we fail closed (and avoid an unnecessary store lookup). string? pepper = pepperProvider.GetPepper(); if (string.IsNullOrWhiteSpace(pepper)) { return Fail(ApiKeyFailure.PepperUnavailable); } // 3. Look up the record (including revoked ones) so we can discriminate not-found vs revoked. ApiKeyRecord? record = await store.FindByKeyIdAsync(parsed.KeyId, ct).ConfigureAwait(false); if (record is null) { return Fail(ApiKeyFailure.KeyNotFound); } // 4. Reject revoked keys. if (record.RevokedUtc is not null) { return Fail(ApiKeyFailure.KeyRevoked); } // 5. Constant-time secret comparison. if (!ApiKeySecretHasher.Verify(parsed.Secret, pepper, record.SecretHash)) { return Fail(ApiKeyFailure.SecretMismatch); } // 6. Record successful use, then return the identity (no secret/hash/pepper included). await store.MarkUsedAsync(record.KeyId, _timeProvider.GetUtcNow(), ct).ConfigureAwait(false); return new ApiKeyVerification( Succeeded: true, Identity: new ApiKeyIdentity(record.KeyId, record.DisplayName, record.Scopes, record.ConstraintsJson), Failure: null); } private static ApiKeyVerification Fail(ApiKeyFailure failure) => new(false, null, failure); }