NextIteration.SpectreConsole.Auth
0.2.0
See the version list below for details.
dotnet add package NextIteration.SpectreConsole.Auth --version 0.2.0
NuGet\Install-Package NextIteration.SpectreConsole.Auth -Version 0.2.0
<PackageReference Include="NextIteration.SpectreConsole.Auth" Version="0.2.0" />
<PackageVersion Include="NextIteration.SpectreConsole.Auth" Version="0.2.0" />
<PackageReference Include="NextIteration.SpectreConsole.Auth" />
paket add NextIteration.SpectreConsole.Auth --version 0.2.0
#r "nuget: NextIteration.SpectreConsole.Auth, 0.2.0"
#:package NextIteration.SpectreConsole.Auth@0.2.0
#addin nuget:?package=NextIteration.SpectreConsole.Auth&version=0.2.0
#tool nuget:?package=NextIteration.SpectreConsole.Auth&version=0.2.0
NextIteration.SpectreConsole.Auth
Encrypted credential storage and ready-made accounts commands for CLI tools built on Spectre.Console.
Stop copy-pasting the same ~/.app/creds.json + AES boilerplate into every CLI you build. Drop this package in, register your provider, and my-cli accounts add / list / select / delete just works — with AES-GCM encryption, atomic writes, hardened filesystem permissions, and a pluggable model for any provider your tool talks to.
Features
accountscommand branch —add,list,select,deletewired into your existingCommandAppwith a single call.- AES-GCM authenticated encryption — tamper detection on every read, no padding-oracle surface.
- Hardened storage — Unix mode
0600on credential files, Windows ACL stripped of inheritance so only the current user + SYSTEM can read the credentials directory. - Atomic writes — crash mid-write never leaves a half-written credential or keystore on disk.
- Provider-aware list rendering — your
accounts listoutput shows provider-specific columns (masked token, base URL, actor, whatever you need) instead of a flat table. - Extensible — bring your own provider by implementing three small interfaces. Adobe, Airtable, and SoftwareOne provider packages ship separately.
- DPAPI option on Windows — swap the default cross-platform backend for Windows DPAPI with one factory call.
- Zero compiler warnings, fully documented public surface —
<GenerateDocumentationFile>on, analyzers on,TreatWarningsAsErrorson.
Install
dotnet add package NextIteration.SpectreConsole.Auth
Pair it with one or more provider packages (or write your own — see Extending):
dotnet add package NextIteration.SpectreConsole.Auth.Providers.Adobe
dotnet add package NextIteration.SpectreConsole.Auth.Providers.Airtable
dotnet add package NextIteration.SpectreConsole.Auth.Providers.SoftwareOne
Targets net10.0.
Quick start
Inside your Program.cs — assuming you already have a DI container and a Spectre.Console.Cli CommandApp wired up:
using NextIteration.SpectreConsole.Auth;
using NextIteration.SpectreConsole.Auth.Providers.Adobe;
// 1. Register the credential store, pointing at a per-app directory
services.AddCredentialStore(opts =>
{
opts.CredentialsDirectory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".my-cli", "credentials");
});
// 2. Register the provider(s) you care about
services.AddAdobeAuthProvider();
// 3. Hook the `accounts` branch into your command configurator
app.Configure(config =>
{
config.AddAccountsBranch();
// ... your other commands
});
That's it. Running your CLI now:
$ my-cli accounts add --provider Adobe --name prod
Enter IMS URL [https://ims-na1.adobelogin.com/]:
Enter API Key: ********
Enter Client Secret: ********
Enter Base URL [https://partners.adobe.io/]:
Select environment:
> Production
Sandbox
Successfully added credential with ID: 8f4e...
Do you want to set this as the active credential for this environment? [y/N]: y
And from inside any of your command handlers:
public sealed class SyncCommand(AdobeAuthenticationService auth) : AsyncCommand
{
public override async Task<int> ExecuteAsync(CommandContext context)
{
var token = await auth.AuthenticateAsync();
// use token.GetAuthorizationHeader() on outgoing requests
return 0;
}
}
The accounts branch
| Command | Description |
|---|---|
accounts add |
Interactive: pick a provider, name the credential, fill in provider-specific fields. |
accounts list |
Table of stored credentials, grouped by provider, with provider-specific columns (masked tokens, URLs, etc.). |
accounts select [id] |
Mark one credential as the active one for its provider. Subsequent AuthenticateAsync() calls use it. |
accounts delete [id] [--force] |
Remove a credential. Clears the selection if it pointed at the deleted entry. |
Every command accepts -v / --verbose for full stack-trace output when something goes wrong.
Security model
Credentials are encrypted with AES-GCM (authenticated — tampering is detected on decrypt). The data-encryption key is itself encrypted and stored in a .keystore file inside your credentials directory. The key-encryption key (KEK) is derived from machine + user identifiers via PBKDF2-HMAC-SHA256 (600,000 iterations).
What this protects against:
- Other users on the same machine reading your credentials (filesystem permissions on the credentials directory enforce this).
- A casual attacker who ends up with a copy of the
.keystorefile but lacks knowledge of the originating machine and user. - Undetected tampering of credential files (AES-GCM's authentication tag refuses decryption on any modification).
What it does not protect against:
- A local attacker who has read access to the credentials directory and knows the machine hostname + username — the KEK is deterministic given those inputs. For stronger isolation on Windows, use
CredentialEncryptionFactory.CreateDpapi()to encrypt against the current user's DPAPI keychain instead. - A compromised running process: once your CLI has decrypted a credential in memory, it's in memory.
The real security boundary is the filesystem permissions on the credentials directory. On first creation the library sets:
- Unix: mode
0700on the directory,0600on every file. - Windows: ACL inheritance disabled, explicit
FullControlfor the current user andSYSTEMonly.
For cryptographically stronger cross-platform isolation, the project TODO tracks plans for Apple Keychain and libsecret backends (PRs welcome).
Extending with a custom provider
Three interfaces to implement, one DI registration. Here's a GitHub PAT provider end-to-end:
using System.Text.Json;
using NextIteration.SpectreConsole.Auth.Commands;
using NextIteration.SpectreConsole.Auth.Credentials;
using NextIteration.SpectreConsole.Auth.Persistence;
using NextIteration.SpectreConsole.Auth.Services;
using NextIteration.SpectreConsole.Auth.Tokens;
using Spectre.Console;
// 1. The credential — what you persist on disk (encrypted).
public sealed class GitHubCredential : ICredential
{
public static string ProviderName => "GitHub";
public static List<string> SupportedEnvironments => ["Production"];
public required string PersonalAccessToken { get; init; }
public required string Environment { get; init; }
}
// 2. The token — what AuthenticateAsync returns.
public sealed class GitHubToken : IToken
{
public required string AccessToken { get; init; }
public bool IsExpired => false;
public string GetAuthorizationHeader() => $"Bearer {AccessToken}";
}
// 3. The authentication service — exchanges credential for token.
public sealed class GitHubAuthenticationService(ICredentialManager manager)
: IAuthenticationService<GitHubCredential, GitHubToken>
{
public async Task<GitHubToken> AuthenticateAsync()
{
var json = await manager.GetSelectedCredentialAsync(GitHubCredential.ProviderName)
?? throw new InvalidOperationException("No GitHub credential selected");
var credential = JsonSerializer.Deserialize<GitHubCredential>(json)!;
return await AuthenticateAsync(credential);
}
public Task<GitHubToken> AuthenticateAsync(GitHubCredential credential) =>
Task.FromResult(new GitHubToken { AccessToken = credential.PersonalAccessToken });
public Task<bool> ValidateTokenAsync(GitHubToken token) =>
Task.FromResult(!token.IsExpired);
}
// 4. The collector — prompts the user during `accounts add`.
public sealed class GitHubCredentialCollector : ICredentialCollector
{
public string ProviderName => GitHubCredential.ProviderName;
public async Task<(string credentialData, string environment)> CollectAsync()
{
var pat = await AnsiConsole.PromptAsync(
new TextPrompt<string>("GitHub personal access token:").Secret());
var credential = new GitHubCredential
{
PersonalAccessToken = pat,
Environment = "Production",
};
return (JsonSerializer.Serialize(credential), credential.Environment);
}
}
// 5. (Optional) The summary provider — columns in `accounts list`.
public sealed class GitHubCredentialSummaryProvider : ICredentialSummaryProvider
{
public string ProviderName => GitHubCredential.ProviderName;
public IReadOnlyList<KeyValuePair<string, string>> GetDisplayFields(string decryptedJson)
{
var c = JsonSerializer.Deserialize<GitHubCredential>(decryptedJson)!;
var masked = c.PersonalAccessToken[..4] + "..." + c.PersonalAccessToken[^4..];
return [new("Token", masked)];
}
}
// 6. Register in DI.
services.AddSingleton<GitHubAuthenticationService>();
services.AddSingleton<ICredentialCollector, GitHubCredentialCollector>();
services.AddSingleton<ICredentialSummaryProvider, GitHubCredentialSummaryProvider>();
That's everything. my-cli accounts add now shows GitHub as a provider option, stores an encrypted GitHubCredential, and my-cli accounts list renders the masked token.
See the companion provider packages repo for fuller examples (OAuth2 client-credentials, base-URL routing, actor-role scoping).
Official provider packages
| Package | Provider | Auth style |
|---|---|---|
| NextIteration.SpectreConsole.Auth.Providers.Adobe | Adobe IMS | OAuth2 client-credentials |
| NextIteration.SpectreConsole.Auth.Providers.Airtable | Airtable | Personal access token (pass-through) |
| NextIteration.SpectreConsole.Auth.Providers.SoftwareOne | SoftwareOne | API token (pass-through, actor-scoped) |
Advanced
Switching to DPAPI on Windows
CredentialEncryptionFactory.Create(path) returns the cross-platform backend by default. For DPAPI-backed storage on Windows:
services.AddSingleton<ICredentialEncryption>(_ => CredentialEncryptionFactory.CreateDpapi());
services.AddSingleton<ICredentialManager, FileCredentialManager>();
Multiple credentials per provider
You can store as many credentials per provider as you like. accounts select activates one at a time per provider — so Adobe production and Adobe sandbox live side-by-side, and a quick accounts select <id> swaps which one your auth service resolves.
Custom encryption backend
Implement ICredentialEncryption and register it before calling AddCredentialStore. FileCredentialManager will pick up whatever backend is registered.
Requirements
- .NET 10.0 or later
- Spectre.Console 0.54+ and Spectre.Console.Cli 0.53+
- Microsoft.Extensions.DependencyInjection.Abstractions 10.0+
Everything else is transitive.
Contributing
Issues and PRs welcome. The TODO tracks the current roadmap — notably the planned Apple Keychain and libsecret backends for stronger cross-platform isolation.
When contributing code, please keep the zero-warning, fully-documented public surface. TreatWarningsAsErrors is on for a reason.
License
MIT © Stuart Meeks
Built for — and unaffiliated with — the excellent Spectre.Console project.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.5)
- Microsoft.Extensions.Http (>= 10.0.5)
- Spectre.Console (>= 0.54.0)
- Spectre.Console.Cli (>= 0.53.1)
- System.Security.Cryptography.ProtectedData (>= 10.0.5)
NuGet packages (4)
Showing the top 4 NuGet packages that depend on NextIteration.SpectreConsole.Auth:
| Package | Downloads |
|---|---|
|
NextIteration.SpectreConsole.Auth.Providers.SoftwareOne
SoftwareOne Marketplace credential provider for NextIteration.SpectreConsole.Auth. Ships SoftwareOneCredential, SoftwareOneToken, SoftwareOneAuthenticationService, and the Spectre.Console collector that drives the accounts-add prompt. The collector performs a live lookup against the Marketplace API at add-time to validate the token and enrich the credential with the account and token metadata. |
|
|
NextIteration.SpectreConsole.Auth.Providers.Adobe
Adobe VIP Marketplace credential provider for NextIteration.SpectreConsole.Auth. Ships AdobeCredential, AdobeToken, AdobeAuthenticationService (OAuth2 client-credentials against Adobe IMS), and the Spectre.Console collector that drives the accounts-add prompt. |
|
|
NextIteration.SpectreConsole.Auth.Providers.Airtable
Airtable credential provider for NextIteration.SpectreConsole.Auth. Ships AirtableCredential, AirtableToken, AirtableAuthenticationService (pass-through personal access token), and the Spectre.Console collector that drives the accounts-add prompt. |
|
|
NextIteration.SpectreConsole.Auth.Providers.GitHub
GitHub credential provider for NextIteration.SpectreConsole.Auth. Ships GitHubCredential, GitHubToken, GitHubAuthenticationService, and the Spectre.Console collector that drives the accounts-add prompt. The collector runs the OAuth device flow (the same flow gh auth login uses), then validates and enriches the credential with the authenticated user's identity. |
GitHub repositories
This package is not used by any popular GitHub repositories.