Skip to content

feat: Profile storage v2 with secure credential storage (#107)#111

Merged
joshsmithxrm merged 11 commits intomainfrom
feat/profile-storage-v2
Jan 3, 2026
Merged

feat: Profile storage v2 with secure credential storage (#107)#111
joshsmithxrm merged 11 commits intomainfrom
feat/profile-storage-v2

Conversation

@joshsmithxrm
Copy link
Copy Markdown
Owner

@joshsmithxrm joshsmithxrm commented Jan 3, 2026

Summary

  • Secure credential storage - Secrets (client secrets, certificate passwords) are now stored in platform-native secure storage (DPAPI on Windows, Keychain on macOS, libsecret on Linux) instead of in the profile JSON file
  • Schema v2 migration - Profiles now use array storage with name-based active profile tracking. Existing v1 profiles are automatically deleted on first load
  • Credential cleanup - auth delete and auth clear now properly remove stored credentials from secure storage
  • Linux cleartext fallback - Added --accept-cleartext-caching flag for Linux systems without libsecret installed
  • Removed dead fields - Removed UserCountry, TenantCountry from profiles (JWT claims not available), removed redundant Environment.Id field

Changes

PPDS.Auth

  • SecureCredentialStore - Platform-native credential storage using MSAL.Extensions
  • AuthProfile - Removed UserCountry, TenantCountry properties
  • EnvironmentInfo - Removed redundant Id property
  • JwtClaimsParser - Simplified to only extract PUID claim
  • Better error message on Linux when libsecret unavailable

PPDS.Cli

  • auth create - Added --accept-cleartext-caching flag (Linux-only visibility)
  • auth delete - Now removes stored credentials from SecureCredentialStore
  • auth clear - Now clears SecureCredentialStore in addition to MSAL caches
  • auth who - Reordered output to match PAC CLI format

Tests

  • Added IsCleartextCachingEnabled unit tests for SecureCredentialStore
  • Added E2E tests for credential cleanup on auth delete and auth clear

Test plan

  • Build passes with --warnaserror
  • All unit tests pass
  • Tested on Windows (secure storage works)
  • Tested on Linux/WSL without libsecret (helpful error shown)
  • Tested on Linux/WSL with --accept-cleartext-caching (warning shown, proceeds)
  • E2E tests with real Dataverse credentials (CI)

Closes #107

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings January 3, 2026 09:55
@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello @joshsmithxrm, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant overhaul to how authentication profiles and sensitive credentials are managed. The primary goal is to bolster security by moving secrets from plain JSON files into platform-native secure storage mechanisms. This change also modernizes the profile storage schema for better maintainability and ensures that credential cleanup is robustly handled when profiles are deleted or cleared. These updates provide a more secure and streamlined authentication experience across different operating systems.

Highlights

  • Secure Credential Storage: Secrets like client secrets and certificate passwords are now stored in platform-native secure storage (DPAPI on Windows, Keychain on macOS, libsecret on Linux) instead of directly in the profile JSON file, significantly enhancing security.
  • Profile Storage Schema v2: The profile storage format has been modernized to use an array-based structure with name-based active profile tracking. Existing v1 profiles are automatically deleted on first load, requiring users to re-authenticate.
  • Credential Cleanup: The auth delete and auth clear commands now properly remove associated credentials from the secure storage, ensuring no lingering sensitive data.
  • Linux Cleartext Fallback: A new --accept-cleartext-caching flag has been added for Linux systems, allowing cleartext storage of credentials if libsecret is not installed, with a warning to the user.
  • Profile Field Rationalization: Redundant or unused fields such as UserCountry, TenantCountry, and Environment.Id have been removed from profiles to streamline the data structure.
  • Auth Who Output Reordering: The output of the auth who command has been reordered to align with the format used by the PAC CLI for consistency.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a major and well-executed enhancement by migrating profile secrets to platform-native secure storage, significantly improving security. The profile storage schema is also updated to version 2, and the v1-to-v2 migration path is handled by deleting old profiles, which is clearly documented as a breaking change. The changes are comprehensive, touching everything from credential providers and profile storage to CLI commands and tests. The new SecureCredentialStore is robust, using MsalCacheHelper for cross-platform encryption and including good practices like atomic file writes. The test suite has been updated thoroughly, with new unit tests for the secure store and E2E tests for credential cleanup, which is excellent.

I have found one high-severity issue in the credential cleanup logic for the auth delete command, which could cause credentials to be deleted prematurely if shared between profiles. My review includes a detailed comment and a code suggestion to fix this.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a comprehensive overhaul of profile storage, introducing schema v2 with platform-native secure credential storage and automatic migration from v1 profiles.

Key Changes:

  • Secure credential storage: Secrets (client secrets, certificate passwords, user passwords) are now stored separately using MSAL.Extensions in platform-native secure storage (DPAPI on Windows, Keychain on macOS, libsecret on Linux with cleartext fallback)
  • Schema v2 migration: Profiles now use array-based storage with name-based active profile tracking instead of dictionary with numeric index; v1 profiles are automatically deleted on first load
  • Enhanced credential cleanup: auth delete and auth clear commands now properly remove credentials from secure storage

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/PPDS.LiveTests/Cli/AuthCommandE2ETests.cs Added E2E tests for credential cleanup on auth delete and auth clear
tests/PPDS.Auth.Tests/Profiles/ProfileStoreTests.cs Updated tests for v2 schema (array-based profiles, name-based active profile) and v1 migration logic
tests/PPDS.Auth.Tests/Profiles/ProfileCollectionTests.cs Updated tests to use ActiveProfileName instead of ActiveIndex
tests/PPDS.Auth.Tests/Profiles/EnvironmentInfoTests.cs Removed tests for Id property (removed field)
tests/PPDS.Auth.Tests/Profiles/AuthProfileTests.cs Removed validation tests for fields now stored in secure credential store
tests/PPDS.Auth.Tests/Credentials/SecureCredentialStoreTests.cs New comprehensive unit tests for SecureCredentialStore functionality
tests/PPDS.Auth.Tests/Credentials/CredentialProviderFactoryTests.cs Updated tests for async factory methods and environment variable support
src/PPDS.Cli/Commands/Auth/AuthCommandGroup.cs Updated auth commands to use secure credential store; added --accept-cleartext-caching option; reordered auth who output
src/PPDS.Cli/CHANGELOG.md Documented breaking changes and new features
src/PPDS.Auth/Profiles/ProfileStore.cs Implemented v1 schema detection and deletion; removed encryption logic (now in SecureCredentialStore)
src/PPDS.Auth/Profiles/ProfilePaths.cs Added CredentialCacheFileName constant
src/PPDS.Auth/Profiles/ProfileCollection.cs Changed from dictionary to array-based storage; uses ActiveProfileName instead of ActiveIndex
src/PPDS.Auth/Profiles/EnvironmentInfo.cs Removed redundant Id property
src/PPDS.Auth/Profiles/AuthProfile.cs Removed ClientSecret, CertificatePassword, Password properties; added Authority property
src/PPDS.Auth/Credentials/SecureCredentialStore.cs New implementation of platform-native secure credential storage using MSAL.Extensions
src/PPDS.Auth/Credentials/JwtClaimsParser.cs Simplified to only extract PUID claim
src/PPDS.Auth/Credentials/ISecureCredentialStore.cs New interface and DTOs for secure credential storage
src/PPDS.Auth/Credentials/CredentialProviderFactory.cs Added async CreateAsync method; support for PPDS_SPN_SECRET environment variable
src/PPDS.Auth/Credentials/ClientSecretCredentialProvider.cs Updated factory methods to accept credentials from secure store or environment variable
src/PPDS.Auth/Credentials/CertificateFileCredentialProvider.cs Updated factory methods to accept certificate password from secure store
src/PPDS.Auth/CHANGELOG.md Documented breaking changes and new features

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

joshsmithxrm and others added 9 commits January 3, 2026 05:38
…torage (#107)

## Summary
- Add SecureCredentialStore using MSAL.Extensions for platform-native credential storage
- Migrate profile schema from v1 (dict) to v2 (array with name-based active profile)
- Add geographic fields (userCountry, tenantCountry) from JWT claims
- Move secrets (clientSecret, certificatePassword, password) to secure store

## Key Changes
- **SecureCredentialStore**: Windows DPAPI, macOS Keychain, Linux libsecret
- **Schema v2**: profiles as array, activeProfile by name, authority field
- **v1 Migration**: Pre-release breaking change - detects and deletes v1 files
- **PPDS_SPN_SECRET**: Env var bypasses secure store for CI/CD scenarios

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Reorder the fields in ppds auth who to match pac auth who:
- Add TenantCountry after TenantId
- Move Entra ID Object Id before PUID
- Add User Country/Region after PUID
- Move Token Expires before Authority
- Reorder Environment section (Geo, Id, Type, Org Id, Unique Name, Friendly Name)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove UserCountry and TenantCountry from AuthProfile (claims not
  available in token without app manifest configuration)
- Remove environment.id from EnvironmentInfo (redundant with
  OrganizationId and EnvironmentId, was never populated)
- Simplify JwtClaimsParser to only extract PUID
- Update EnvironmentInfo.Create() to take url and displayName only
- Update all related tests

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Document breaking changes for #107:
- Schema v2 with array storage and name-based active profile
- Secure credential storage using MSAL.Extensions
- Removed EnvironmentInfo.Id, UserCountry, TenantCountry
- auth who output ordering to match PAC CLI

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add --accept-cleartext-caching flag to auth create (Linux-only)
  Shows warning when secure storage unavailable and cleartext is used
- auth delete now removes stored credentials from SecureCredentialStore
- auth clear now clears SecureCredentialStore in addition to MSAL caches

Addresses missing items from #107 review.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add IsCleartextCachingEnabled property unit tests for SecureCredentialStore
- Add E2E test verifying auth delete removes stored credentials
- Add E2E test verifying auth clear removes credential store

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
MSAL.Extensions doesn't allow both WithLinuxKeyring() and
WithLinuxUnprotectedFile() together - they're mutually exclusive.

When --accept-cleartext-caching is passed, now uses ONLY
WithLinuxUnprotectedFile(). Otherwise uses ONLY WithLinuxKeyring().

Found via WSL testing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Check if other profiles share credentials before deleting them
  when a profile is removed (prevents breaking other profiles)
- Clean up stored credentials if auth fails after storing them
  (prevents orphaned credentials in secure storage)
- Add null check for DisplayName in auth who output (consistency)
- Add test verifying shared credential preservation behavior

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add missing fixes from bot review:
- Auth: Linux cleartext storage uses exclusive MSAL config
- Cli: Shared credential preservation on profile delete
- Cli: Credential cleanup on auth failure
- Cli: DisplayName null check in auth who

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…l lookup

ProfileConnectionSource and ConnectionResolver were calling the sync
CredentialProviderFactory.Create() which only checks PPDS_SPN_SECRET
env var. This caused integration tests to fail with "Failed to create
seed after 3 attempts" because credentials stored via `auth create`
could not be retrieved.

Changes:
- ProfileConnectionSource: Accept ISecureCredentialStore parameter,
  use CreateAsync() for credential lookup
- ConnectionResolver: Pass credential store to ProfileConnectionSource
- ProfileServiceFactory: Create SecureCredentialStore and register in DI
- Add E2E test for PPDS_SPN_SECRET env var bypass path

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…ential lookup

EnvironmentResolutionService.TryDirectConnectionAsync() was calling the
sync CredentialProviderFactory.Create() which doesn't support secure
store lookups. This caused env select and env who commands to fail with
"No client secret found" when using profiles created with auth create.

Changes:
- EnvironmentResolutionService: Accept ISecureCredentialStore parameter,
  use CreateAsync() for credential lookup
- EnvCommandGroup: Pass SecureCredentialStore to resolution service
- AuthCommandGroup: Same for auth update command

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@joshsmithxrm joshsmithxrm merged commit 38fd4b4 into main Jan 3, 2026
8 checks passed
@joshsmithxrm joshsmithxrm deleted the feat/profile-storage-v2 branch January 3, 2026 18:02
@github-project-automation github-project-automation bot moved this from Todo to Done in PPDS Roadmap Jan 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Profile storage overhaul: Schema v2 + secure cross-platform credential storage

3 participants