using System.Security.Cryptography; namespace MxGateway.Server.Security.Authentication; public sealed class ApiKeyVerifier( IApiKeyParser parser, IApiKeySecretHasher hasher, IApiKeyStore keyStore) : IApiKeyVerifier { public async Task VerifyAsync( string? authorizationHeader, CancellationToken cancellationToken) { if (!parser.TryParseAuthorizationHeader(authorizationHeader, out ParsedApiKey? parsedKey) || parsedKey is null) { return ApiKeyVerificationResult.Fail(ApiKeyVerificationFailure.MissingOrMalformedCredentials); } ApiKeyRecord? storedKey = await keyStore.FindByKeyIdAsync(parsedKey.KeyId, cancellationToken) .ConfigureAwait(false); if (storedKey is null) { return ApiKeyVerificationResult.Fail(ApiKeyVerificationFailure.KeyNotFound); } if (storedKey.RevokedUtc is not null) { return ApiKeyVerificationResult.Fail(ApiKeyVerificationFailure.KeyRevoked); } byte[] presentedHash; try { presentedHash = hasher.HashSecret(parsedKey.Secret); } catch (ApiKeyPepperUnavailableException) { return ApiKeyVerificationResult.Fail(ApiKeyVerificationFailure.PepperUnavailable); } if (!CryptographicOperations.FixedTimeEquals(presentedHash, storedKey.SecretHash)) { return ApiKeyVerificationResult.Fail(ApiKeyVerificationFailure.SecretMismatch); } await keyStore.MarkKeyUsedAsync(storedKey.KeyId, DateTimeOffset.UtcNow, cancellationToken) .ConfigureAwait(false); return ApiKeyVerificationResult.Success(new ApiKeyIdentity( KeyId: storedKey.KeyId, KeyPrefix: storedKey.KeyPrefix, DisplayName: storedKey.DisplayName, Scopes: storedKey.Scopes)); } }