Class InMemoryApiKeyStore
Thread-safe in-memory IApiKeyStore for tests — the drop-in double for the EF-backed key store, mirroring its contract semantics: canonical raw keys, machine keys materialized as memberships of their tenant, personal access tokens guarded by the bound user's active membership, fail-closed validation, and the idempotent import path.
public sealed class InMemoryApiKeyStore : IApiKeyStore
- Inheritance
-
InMemoryApiKeyStore
- Implements
- Inherited Members
- Extension Methods
Examples
Seed a known key and authenticate with it:
var keys = new InMemoryApiKeyStore();
var rawKey = ApiKeyFormat.CreateRawKey();
await keys.ImportAsync(new ApiKeyImportRequest(rawKey, TestTenants.Of("primary"), "ci", ["Deployer"]));
var descriptor = await keys.ValidateAsync(rawKey);
Remarks
The double keeps raw keys in memory instead of hashing them — the hash is a storage concern, and nothing in the contract exposes it. Everything a test can observe (descriptors, memberships, validation outcomes) behaves as it does in production.
Constructors
InMemoryApiKeyStore(ITenantMembershipStore?, TimeProvider?)
Creates the store, optionally sharing the membership store that machine keys materialize into and that personal access tokens are guarded against.
public InMemoryApiKeyStore(ITenantMembershipStore? memberships = null, TimeProvider? clock = null)
Parameters
membershipsITenantMembershipStoreThe membership plane to write machine memberships into; a private InMemoryTenantMembershipStore is created when omitted.
clockTimeProviderClock backing issuance timestamps and expiry checks.
Properties
Memberships
The membership plane this store materializes machine keys into — inspect it to assert that a key carries the roles it should.
public ITenantMembershipStore Memberships { get; }
Property Value
Methods
GetForTenantAsync(Guid, CancellationToken)
Lists the tenant's keys (descriptors only — raw keys are unrecoverable).
public Task<IReadOnlyList<ApiKeyDescriptor>> GetForTenantAsync(Guid tenantId, CancellationToken cancellationToken = default)
Parameters
tenantIdGuidThe tenant whose keys to list.
cancellationTokenCancellationTokenToken to observe while listing.
Returns
- Task<IReadOnlyList<ApiKeyDescriptor>>
The tenant's keys; empty when none exist.
ImportAsync(ApiKeyImportRequest, CancellationToken)
Stores a machine key whose raw value the caller already holds — for setups where the key must be known before the store exists: container orchestration, CI provisioning, self-hosted bundles, end-to-end test hosts. Idempotent, so it can run on every boot.
public Task<ApiKeyDescriptor> ImportAsync(ApiKeyImportRequest request, CancellationToken cancellationToken = default)
Parameters
requestApiKeyImportRequestThe import parameters, including the raw key.
cancellationTokenCancellationTokenToken to observe while importing.
Returns
- Task<ApiKeyDescriptor>
The stored descriptor — newly created, or the existing one on a repeat import.
Remarks
The supplied value must match ApiKeyFormat — generate it with CreateRawKey(). The shape requirement is load-bearing: stores keep the key's digest unsalted because a generated key carries 256 bits of entropy, and a hand-picked value would quietly invalidate that.
Importing a value that is already stored is a no-op that returns the existing descriptor. The stored key is never mutated — a differing name, role set, or expiry leaves the stored key as it is, so a changed configuration can neither escalate a key's roles nor extend its life unnoticed. Compare the returned descriptor when that matters. Nothing about the "shown once" guarantee of IssueAsync(ApiKeyIssueRequest, CancellationToken) changes: import returns no raw key, because the caller already has it.
Exceptions
- ArgumentException
The raw key does not match the canonical format, or the name is empty.
- InvalidOperationException
The key value is already stored for a different tenant, as a personal access token, or in a revoked or expired state — none of which an import may silently adopt.
IssueAsync(ApiKeyIssueRequest, CancellationToken)
Issues a new key. The returned RawKey is shown once and never persisted. Personal access tokens (a bound user) require the user to hold an active membership in the target tenant.
public Task<IssuedApiKey> IssueAsync(ApiKeyIssueRequest request, CancellationToken cancellationToken = default)
Parameters
requestApiKeyIssueRequestThe issuance parameters.
cancellationTokenCancellationTokenToken to observe while issuing.
Returns
- Task<IssuedApiKey>
The raw secret plus the stored descriptor.
Exceptions
- InvalidOperationException
A personal access token was requested for a user without an active membership in the tenant, or roles were supplied for a personal access token.
RemoveAllForTenantAsync(Guid, CancellationToken)
Removes every key bound to the tenant — the key-plane step of a tenant-erasure sweep.
public Task RemoveAllForTenantAsync(Guid tenantId, CancellationToken cancellationToken = default)
Parameters
tenantIdGuidThe tenant whose keys to remove.
cancellationTokenCancellationTokenToken to observe while removing.
Returns
RemoveAllForUserAsync(Guid, CancellationToken)
Removes every personal access token bound to the user — the key-plane step of a user-erasure sweep.
public Task RemoveAllForUserAsync(Guid userId, CancellationToken cancellationToken = default)
Parameters
userIdGuidThe user whose personal access tokens to remove.
cancellationTokenCancellationTokenToken to observe while removing.
Returns
RevokeAsync(Guid, CancellationToken)
Revokes a key; it never validates again.
public Task RevokeAsync(Guid apiKeyId, CancellationToken cancellationToken = default)
Parameters
apiKeyIdGuidThe key to revoke.
cancellationTokenCancellationTokenToken to observe while revoking.
Returns
ValidateAsync(string, CancellationToken)
Validates a presented raw key: hash lookup, then revocation and expiry checks.
public Task<ApiKeyDescriptor?> ValidateAsync(string rawKey, CancellationToken cancellationToken = default)
Parameters
rawKeystringThe presented plaintext key.
cancellationTokenCancellationTokenToken to observe while validating.
Returns
- Task<ApiKeyDescriptor>
The key's descriptor, or
nullfor unknown/revoked/expired keys.