Skip to content

docs: add README.md for PPDS.Auth package#29

Merged
joshsmithxrm merged 1 commit intomainfrom
fix/auth-readme
Dec 29, 2025
Merged

docs: add README.md for PPDS.Auth package#29
joshsmithxrm merged 1 commit intomainfrom
fix/auth-readme

Conversation

@joshsmithxrm
Copy link
Owner

@joshsmithxrm joshsmithxrm commented Dec 29, 2025

Summary

  • Add missing README.md for PPDS.Auth package
  • Required for NuGet package publishing (PackageReadmeFile property in csproj)

Context

The Auth-v1.0.0-beta.1 release failed because the package was missing the README.md file specified in the project file.

Test plan

  • Merge PR
  • Delete and recreate Auth-v1.0.0-beta.1 release to trigger publish workflow

🤖 Generated with Claude Code

Required for NuGet package publishing (PackageReadmeFile).

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings December 29, 2025 18:15
@gemini-code-assist
Copy link

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 resolves a critical issue that prevented the Auth-v1.0.0-beta.1 NuGet package from publishing successfully. The core change involves adding a comprehensive README.md file to the PPDS.Auth package, which was a missing requirement for the NuGet publishing process. This ensures that future releases of the package can be published without encountering the same failure.

Highlights

  • New README.md for PPDS.Auth: A new README.md file has been added to the src/PPDS.Auth package directory.
  • NuGet Publishing Requirement: This addition addresses a critical requirement for NuGet package publishing, as the PackageReadmeFile property in the .csproj file mandates its presence.
  • Comprehensive Package Documentation: The new README provides detailed documentation for the PPDS.Auth package, including installation instructions, quick start guides, various authentication methods, profile storage, environment discovery and resolution, multi-cloud support, integration with PPDS.Dataverse, custom credential providers, and security considerations.
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.

@joshsmithxrm joshsmithxrm merged commit 9370d52 into main Dec 29, 2025
7 checks passed
Copy link

@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 adds a README.md file for the PPDS.Auth package. The README provides a good overview of the library's features. However, almost all of the code examples are incorrect and do not align with the actual API surface provided in the C# source files. This includes invalid constructor calls, incorrect method signatures, usage of non-existent methods and properties, and logically flawed examples. These errors are critical, especially in the Quick Start and Custom Credential Provider sections, as they will prevent users from successfully using the library. I have provided detailed comments and suggestions to correct these examples throughout the file.

Comment on lines +14 to +25
// 1. Create and store a profile
var store = new ProfileStore();
var profile = new AuthProfile("dev")
{
AuthMethod = AuthMethod.InteractiveBrowser,
ClientId = "51f81489-12ee-4a9e-aaae-a2591f45987d" // Default Power Platform client
};
await store.SaveAsync(profile);

// 2. Authenticate and get a ServiceClient
var provider = CredentialProviderFactory.Create(profile);
var client = await ServiceClientFactory.CreateAsync(provider, profile);

Choose a reason for hiding this comment

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

critical

The Quick Start example contains several errors that will prevent a user from getting started successfully. It appears to be based on an outdated or incorrect version of the API.

  1. new AuthProfile("dev") is not a valid constructor. AuthProfile should be initialized using an object initializer like new AuthProfile { Name = "dev" }.
  2. store.SaveAsync expects a ProfileCollection, but is called with a single AuthProfile.
  3. ServiceClientFactory.CreateAsync is not a valid static method. To get a ServiceClient, you should use provider.CreateServiceClientAsync(environmentUrl).
  4. The example is missing the environment URL, which is a required parameter.

This suggestion corrects the code to align with the library's API.

Suggested change
// 1. Create and store a profile
var store = new ProfileStore();
var profile = new AuthProfile("dev")
{
AuthMethod = AuthMethod.InteractiveBrowser,
ClientId = "51f81489-12ee-4a9e-aaae-a2591f45987d" // Default Power Platform client
};
await store.SaveAsync(profile);
// 2. Authenticate and get a ServiceClient
var provider = CredentialProviderFactory.Create(profile);
var client = await ServiceClientFactory.CreateAsync(provider, profile);
// 1. Create a profile
var profile = new AuthProfile
{
Name = "dev",
AuthMethod = AuthMethod.InteractiveBrowser,
ClientId = "51f81489-12ee-4a9e-aaae-a2591f45987d" // Default Power Platform client
};
// 2. Authenticate and get a ServiceClient
var provider = CredentialProviderFactory.Create(profile);
var client = await provider.CreateServiceClientAsync("https://your-org.crm.dynamics.com");

Comment on lines +201 to +222
```csharp
public class CustomCredentialProvider : ICredentialProvider
{
public string Name => "Custom";
public bool RequiresInteraction => false;

public Task<string> GetTokenAsync(string resource, CancellationToken ct)
{
// Your custom token acquisition logic
return Task.FromResult("your-token");
}

public Task<TokenInfo> GetTokenInfoAsync(CancellationToken ct)
{
return Task.FromResult(new TokenInfo
{
UserPrincipalName = "user@example.com",
TenantId = "..."
});
}
}
```

Choose a reason for hiding this comment

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

critical

The example for implementing ICredentialProvider is incorrect and does not match the interface definition. The ICredentialProvider interface requires implementing properties like AuthMethod and the method CreateServiceClientAsync, not GetTokenAsync or GetTokenInfoAsync.

A correct, minimal implementation would look something like this:

public class CustomCredentialProvider : ICredentialProvider
{
    public AuthMethod AuthMethod => AuthMethod.InteractiveBrowser; // Or a custom value if you extend the enum
    public string? Identity { get; private set; }
    public DateTimeOffset? TokenExpiresAt { get; private set; }
    public string? TenantId { get; private set; }
    public string? ObjectId { get; private set; }
    public string? AccessToken { get; private set; }
    public System.Security.Claims.ClaimsPrincipal? IdTokenClaims { get; private set; }

    public async Task<ServiceClient> CreateServiceClientAsync(
        string environmentUrl,
        CancellationToken cancellationToken = default,
        bool forceInteractive = false)
    {
        // Your custom token acquisition logic to get a token string
        var token = "your-token";
        this.AccessToken = token;
        this.Identity = "user@example.com";

        var client = new ServiceClient(
            new Uri(environmentUrl),
            _ => Task.FromResult(token),
            useUniqueInstance: true);

        if (!client.IsReady)
        {
            throw new Exception("Failed to connect");
        }
        return client;
    }

    public void Dispose() { /* Cleanup */ }
}

var store = new ProfileStore();

// Save a profile
await store.SaveAsync(profile);

Choose a reason for hiding this comment

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

high

The SaveAsync method on ProfileStore expects a ProfileCollection object, not a single AuthProfile. To save a new profile, you should first load the existing collection, add the new profile to it, and then save the collection.


```csharp
// Interactive Browser (opens browser for OAuth)
var profile = new AuthProfile("dev")

Choose a reason for hiding this comment

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

high

AuthProfile does not have a constructor that accepts a name. This should be initialized using an object initializer, e.g., new AuthProfile { Name = "dev", ... }. This issue is repeated in all subsequent examples in this section.

AuthMethod = AuthMethod.AzureDevOpsFederated,
ApplicationId = "your-app-id",
TenantId = "your-tenant-id",
AzureDevOpsServiceConnectionId = "service-connection-id"

Choose a reason for hiding this comment

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

high

The AzureDevOpsServiceConnectionId property does not exist on the AuthProfile class. The credential provider for Azure DevOps retrieves this information from environment variables, so this line should be removed.

Comment on lines +145 to +157
var resolver = new EnvironmentResolver(discovery, provider.GetTokenAsync);

// By URL
var env = await resolver.ResolveAsync("https://org.crm.dynamics.com");

// By unique name
var env = await resolver.ResolveAsync("org");

// By environment ID
var env = await resolver.ResolveAsync("00000000-0000-0000-0000-000000000000");

// By partial friendly name (fuzzy match)
var env = await resolver.ResolveAsync("Production");

Choose a reason for hiding this comment

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

high

This example for environment resolution is incorrect. EnvironmentResolver is a static class and does not have a constructor or an ResolveAsync method. You should first get a list of environments and then pass it to the static EnvironmentResolver.Resolve method.

Suggested change
var resolver = new EnvironmentResolver(discovery, provider.GetTokenAsync);
// By URL
var env = await resolver.ResolveAsync("https://org.crm.dynamics.com");
// By unique name
var env = await resolver.ResolveAsync("org");
// By environment ID
var env = await resolver.ResolveAsync("00000000-0000-0000-0000-000000000000");
// By partial friendly name (fuzzy match)
var env = await resolver.ResolveAsync("Production");
// First, get the list of available environments
var discovery = new GlobalDiscoveryService();
var environments = await discovery.GetEnvironmentsAsync();
// Then, resolve a specific environment from the list
// By URL
var envByUrl = EnvironmentResolver.Resolve(environments, "https://org.crm.dynamics.com");
// By unique name
var envByUniqueName = EnvironmentResolver.Resolve(environments, "org");
// By environment ID
var envById = EnvironmentResolver.Resolve(environments, "00000000-0000-0000-0000-000000000000");
// By partial friendly name (fuzzy match)
var envByFriendlyName = EnvironmentResolver.Resolve(environments, "Production");

var profile = profiles.GetActiveProfile();

var provider = CredentialProviderFactory.Create(profile);
var source = new ProfileConnectionSource(profile, provider);

Choose a reason for hiding this comment

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

high

The constructor for ProfileConnectionSource is incorrect. It does not accept an ICredentialProvider. You should use the static factory method ProfileConnectionSource.FromProfile(profile), which requires the profile to have an environment selected.

Suggested change
var source = new ProfileConnectionSource(profile, provider);
var source = ProfileConnectionSource.FromProfile(profile);

Comment on lines +237 to +241
var profile = new AuthProfile("dev")
{
TokenCacheType = TokenCacheType.Persistent // Default
// TokenCacheType = TokenCacheType.InMemory // Per-process only
};

Choose a reason for hiding this comment

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

high

This example is incorrect. The AuthProfile class does not have a TokenCacheType property. Additionally, the TokenCacheType enum does not contain a Persistent value. Token caching is handled internally by the credential providers and is persistent by default where possible. This section should probably be removed or corrected to explain the automatic caching behavior.

Comment on lines +175 to +176
// - CloudEnvironment.UsNat
// - CloudEnvironment.UsSec

Choose a reason for hiding this comment

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

medium

The list of supported clouds includes UsNat and UsSec, which are not defined in the CloudEnvironment enum. These should be removed to reflect the actual supported clouds.


Secrets (client secrets, passwords, etc.) are encrypted at rest:
- **Windows**: DPAPI (user-scoped encryption)
- **Linux/macOS**: Base64 encoding (use OS-level file permissions)

Choose a reason for hiding this comment

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

medium

Describing the non-Windows encryption as 'Base64 encoding' is misleading. ProfileEncryption.cs shows it uses XOR obfuscation with a machine-specific key, and then Base64 encodes the result. It would be more accurate to describe this as 'simple obfuscation' to avoid giving the impression that secrets are stored in plain Base64.

Suggested change
- **Linux/macOS**: Base64 encoding (use OS-level file permissions)
- **Linux/macOS**: Simple obfuscation (use OS-level file permissions)

Copy link

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 adds a README.md file to the PPDS.Auth package, which is required for NuGet package publishing (specified via the PackageReadmeFile property in the project file). The Auth-v1.0.0-beta.1 release previously failed because this file was missing.

Key changes:

  • Comprehensive documentation for the PPDS.Auth authentication package including installation, features, and API examples
  • Coverage of all authentication methods, profile storage, environment discovery, and cloud support
  • Integration examples with PPDS.Dataverse connection pooling

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

Comment on lines +16 to +21
var profile = new AuthProfile("dev")
{
AuthMethod = AuthMethod.InteractiveBrowser,
ClientId = "51f81489-12ee-4a9e-aaae-a2591f45987d" // Default Power Platform client
};
await store.SaveAsync(profile);
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The AuthProfile class does not have a constructor that accepts a string parameter. The correct initialization pattern should use property initializers without passing the name to the constructor. For example:

var profile = new AuthProfile
{
    Name = "dev",
    AuthMethod = AuthMethod.InteractiveBrowser,
    ClientId = "51f81489-12ee-4a9e-aaae-a2591f45987d"
};

This issue appears throughout the README in all code examples that create AuthProfile instances.

Copilot uses AI. Check for mistakes.

// 2. Authenticate and get a ServiceClient
var provider = CredentialProviderFactory.Create(profile);
var client = await ServiceClientFactory.CreateAsync(provider, profile);
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

ServiceClientFactory is not a static class and does not have a static CreateAsync method. The correct usage requires creating an instance first and then calling instance methods like CreateFromProfileAsync. For example:

var factory = new ServiceClientFactory();
var client = await factory.CreateFromProfileAsync(profile);

Or more typically, you would use the credential provider directly via the ICredentialProvider.CreateServiceClientAsync method.

Suggested change
var client = await ServiceClientFactory.CreateAsync(provider, profile);
var factory = new ServiceClientFactory();
var client = await factory.CreateFromProfileAsync(profile);

Copilot uses AI. Check for mistakes.
var provider = CredentialProviderFactory.Create(profile);
var discovery = new GlobalDiscoveryService();

var environments = await discovery.GetEnvironmentsAsync(provider.GetTokenAsync);
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The GlobalDiscoveryService class does not have a GetEnvironmentsAsync method. The correct method name is DiscoverEnvironmentsAsync. Update the example to use the correct method name.

Suggested change
var environments = await discovery.GetEnvironmentsAsync(provider.GetTokenAsync);
var environments = await discovery.DiscoverEnvironmentsAsync(provider.GetTokenAsync);

Copilot uses AI. Check for mistakes.
Comment on lines +66 to +67
AuthMethod = AuthMethod.DeviceCode,
ClientId = "51f81489-12ee-4a9e-aaae-a2591f45987d"
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The AuthProfile class does not have a ClientId property. Remove this property assignment as it's not valid. The provider uses a hardcoded Microsoft public client ID for device code authentication.

Suggested change
AuthMethod = AuthMethod.DeviceCode,
ClientId = "51f81489-12ee-4a9e-aaae-a2591f45987d"
AuthMethod = AuthMethod.DeviceCode

Copilot uses AI. Check for mistakes.
// - CloudEnvironment.Public (default)
// - CloudEnvironment.UsGov (GCC)
// - CloudEnvironment.UsGovHigh (GCC High)
// - CloudEnvironment.UsGovDoD (DoD)
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The correct enum value is CloudEnvironment.UsGovDod (with lowercase 'od'), not UsGovDoD. Update the comment to match the actual enum value.

Suggested change
// - CloudEnvironment.UsGovDoD (DoD)
// - CloudEnvironment.UsGovDod (DoD)

Copilot uses AI. Check for mistakes.
Comment on lines +239 to +240
TokenCacheType = TokenCacheType.Persistent // Default
// TokenCacheType = TokenCacheType.InMemory // Per-process only
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The TokenCacheType enum values shown in the example are incorrect. The actual enum values are: OperatingSystem, File, and Memory (not Persistent and InMemory). Update the example to use the correct enum values.

Suggested change
TokenCacheType = TokenCacheType.Persistent // Default
// TokenCacheType = TokenCacheType.InMemory // Per-process only
TokenCacheType = TokenCacheType.OperatingSystem // Default
// TokenCacheType = TokenCacheType.Memory // Per-process only

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +19
AuthMethod = AuthMethod.InteractiveBrowser,
ClientId = "51f81489-12ee-4a9e-aaae-a2591f45987d" // Default Power Platform client
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The AuthProfile class does not have a ClientId property. For interactive browser and device code authentication methods, the provider uses a hardcoded Microsoft public client ID (51f81489-12ee-4a9e-aaae-a2591f45987d). Remove the ClientId property from these examples as it's not a valid property of AuthProfile.

Suggested change
AuthMethod = AuthMethod.InteractiveBrowser,
ClientId = "51f81489-12ee-4a9e-aaae-a2591f45987d" // Default Power Platform client
AuthMethod = AuthMethod.InteractiveBrowser

Copilot uses AI. Check for mistakes.
var profile = new AuthProfile("dev")
{
AuthMethod = AuthMethod.InteractiveBrowser,
ClientId = "51f81489-12ee-4a9e-aaae-a2591f45987d"
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The AuthProfile class does not have a ClientId property. Remove this property assignment as it's not valid. The provider uses a hardcoded Microsoft public client ID for interactive browser authentication.

Copilot uses AI. Check for mistakes.
Comment on lines +207 to +219
public Task<string> GetTokenAsync(string resource, CancellationToken ct)
{
// Your custom token acquisition logic
return Task.FromResult("your-token");
}

public Task<TokenInfo> GetTokenInfoAsync(CancellationToken ct)
{
return Task.FromResult(new TokenInfo
{
UserPrincipalName = "user@example.com",
TenantId = "..."
});
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The ICredentialProvider interface does not have GetTokenAsync or GetTokenInfoAsync methods. The actual interface requires implementing CreateServiceClientAsync method and properties like Identity, TokenExpiresAt, TenantId, ObjectId, AccessToken, and IdTokenClaims. Update this example to reflect the actual interface definition.

Suggested change
public Task<string> GetTokenAsync(string resource, CancellationToken ct)
{
// Your custom token acquisition logic
return Task.FromResult("your-token");
}
public Task<TokenInfo> GetTokenInfoAsync(CancellationToken ct)
{
return Task.FromResult(new TokenInfo
{
UserPrincipalName = "user@example.com",
TenantId = "..."
});
// Required properties
public string Identity { get; private set; } = "user@example.com";
public DateTimeOffset TokenExpiresAt { get; private set; } = DateTimeOffset.UtcNow.AddHours(1);
public string TenantId { get; private set; } = "00000000-0000-0000-0000-000000000000";
public string ObjectId { get; private set; } = "11111111-1111-1111-1111-111111111111";
public string AccessToken { get; private set; } = "your-access-token";
public IDictionary<string, object> IdTokenClaims { get; private set; } =
new Dictionary<string, object>
{
["name"] = "Example User",
["preferred_username"] = "user@example.com"
};
// Required method
public Task<object> CreateServiceClientAsync(CancellationToken cancellationToken = default)
{
// TODO: Replace 'object' with your actual service client type and
// implement the logic to create and return an authenticated client.
//
// For example:
// var client = new MyServiceClient(new Uri("https://example.service/"), AccessToken);
// return Task.FromResult<object>(client);
return Task.FromResult<object>(null!);

Copilot uses AI. Check for mistakes.
Comment on lines +226 to +230
### Credential Encryption

Secrets (client secrets, passwords, etc.) are encrypted at rest:
- **Windows**: DPAPI (user-scoped encryption)
- **Linux/macOS**: Base64 encoding (use OS-level file permissions)
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The statement that secrets are "encrypted at rest" on Linux/macOS using Base64 is misleading given the current implementation: ProfileEncryption only applies simple XOR-based obfuscation with a machine-derived key and Base64-encodes the result, which is not cryptographically secure. An attacker who can obtain the profile file (e.g., via backup leakage or local file read) can deterministically recover client secrets and passwords, enabling service principal compromise. Consider using real encryption backed by platform keychains (e.g., libsecret/Keychain) or a strong key management scheme, and update the documentation to accurately describe the protection guarantees until this is in place.

Suggested change
### Credential Encryption
Secrets (client secrets, passwords, etc.) are encrypted at rest:
- **Windows**: DPAPI (user-scoped encryption)
- **Linux/macOS**: Base64 encoding (use OS-level file permissions)
### Credential Storage & Protection
Secrets (client secrets, passwords, etc.) are stored at rest as follows:
- **Windows**: DPAPI (user-scoped encryption)
- **Linux/macOS**: Lightweight obfuscation (XOR with a machine-derived key and Base64). This is **not** cryptographically secure; anyone who can read the profile file can recover the secrets. Rely on strict OS-level file permissions and treat the profile file as sensitive.

Copilot uses AI. Check for mistakes.
@joshsmithxrm joshsmithxrm deleted the fix/auth-readme branch December 29, 2025 18:53
joshsmithxrm added a commit that referenced this pull request Jan 8, 2026
feat: add user configuration system with VS Code Settings integration
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants