Initial commit: scadaproj umbrella — sister-project index, auth component normalization (design + GAPS), and the built ZB.MOM.WW.Auth shared library (0.1.0, flattened in).
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
using ZB.MOM.WW.Auth.Abstractions.ApiKeys;
|
||||
|
||||
namespace ZB.MOM.WW.Auth.ApiKeys;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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 <c>ConstraintsJson</c> blob (which the verifier does not interpret); it never carries the
|
||||
/// presented secret, the pepper, or the stored secret hash.
|
||||
/// </remarks>
|
||||
public sealed class ApiKeyVerifier(
|
||||
ApiKeyOptions options,
|
||||
IApiKeyStore store,
|
||||
IApiKeyPepperProvider pepperProvider,
|
||||
TimeProvider? timeProvider = null) : IApiKeyVerifier
|
||||
{
|
||||
private readonly TimeProvider _timeProvider = timeProvider ?? TimeProvider.System;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ApiKeyVerification> 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);
|
||||
}
|
||||
Reference in New Issue
Block a user