NextIteration.SpectreConsole.Auth 0.2.0

There is a newer version of this package available.
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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="NextIteration.SpectreConsole.Auth" Version="0.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="NextIteration.SpectreConsole.Auth" Version="0.2.0" />
                    
Directory.Packages.props
<PackageReference Include="NextIteration.SpectreConsole.Auth" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add NextIteration.SpectreConsole.Auth --version 0.2.0
                    
#r "nuget: NextIteration.SpectreConsole.Auth, 0.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package NextIteration.SpectreConsole.Auth@0.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=NextIteration.SpectreConsole.Auth&version=0.2.0
                    
Install as a Cake Addin
#tool nuget:?package=NextIteration.SpectreConsole.Auth&version=0.2.0
                    
Install as a Cake Tool

NextIteration.SpectreConsole.Auth

NuGet Downloads License: MIT .NET CI

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

  • accounts command branchadd, list, select, delete wired into your existing CommandApp with a single call.
  • AES-GCM authenticated encryption — tamper detection on every read, no padding-oracle surface.
  • Hardened storage — Unix mode 0600 on 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 list output 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, TreatWarningsAsErrors on.

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 .keystore file 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 0700 on the directory, 0600 on every file.
  • Windows: ACL inheritance disabled, explicit FullControl for the current user and SYSTEM only.

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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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.

Version Downloads Last Updated
0.7.0 258 6/20/2026
0.6.3 267 6/10/2026
0.6.2 399 5/3/2026
0.6.1 295 5/3/2026
0.6.0 202 4/18/2026
0.5.0 144 4/18/2026
0.4.2 107 4/17/2026
0.4.1 173 4/17/2026
0.2.0 110 4/16/2026