-
-
Notifications
You must be signed in to change notification settings - Fork 1
docs: add README.md for PPDS.Auth package #29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,252 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # PPDS.Auth | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Authentication profile management for Power Platform with encrypted credential storage and multi-cloud support. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ## Installation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ```bash | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dotnet add package PPDS.Auth | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ## Quick Start | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ```csharp | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 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); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+16
to
+21
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 2. Authenticate and get a ServiceClient | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var provider = CredentialProviderFactory.Create(profile); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var client = await ServiceClientFactory.CreateAsync(provider, profile); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+14
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
This suggestion corrects the code to align with the library's API.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var client = await ServiceClientFactory.CreateAsync(provider, profile); | |
| var factory = new ServiceClientFactory(); | |
| var client = await factory.CreateFromProfileAsync(profile); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
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
AI
Dec 29, 2025
There was a problem hiding this comment.
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.
| AuthMethod = AuthMethod.DeviceCode, | |
| ClientId = "51f81489-12ee-4a9e-aaae-a2591f45987d" | |
| AuthMethod = AuthMethod.DeviceCode |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This example for environment discovery is incorrect. The GlobalDiscoveryService.GetEnvironmentsAsync method does not take any parameters as it handles authentication internally. Also, the ICredentialProvider is not needed for discovery.
| var provider = CredentialProviderFactory.Create(profile); | |
| var discovery = new GlobalDiscoveryService(); | |
| var environments = await discovery.GetEnvironmentsAsync(provider.GetTokenAsync); | |
| // The discovery service can be created from a profile to use its cloud/tenant settings. | |
| var discovery = GlobalDiscoveryService.FromProfile(profile); | |
| var environments = await discovery.GetEnvironmentsAsync(); |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
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.
| var environments = await discovery.GetEnvironmentsAsync(provider.GetTokenAsync); | |
| var environments = await discovery.DiscoverEnvironmentsAsync(provider.GetTokenAsync); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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"); |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The EnvironmentResolver.Resolve method is synchronous, not asynchronous. The examples should use Resolve instead of ResolveAsync, and remove the await keyword. For example:
var env = resolver.Resolve("https://org.crm.dynamics.com");Note that EnvironmentResolver is also a static class, so you would call it as EnvironmentResolver.Resolve(environments, identifier) without creating an instance.
| 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"); | |
| // `environments` is the collection of environments returned from discovery. | |
| // By URL | |
| var env = EnvironmentResolver.Resolve(environments, "https://org.crm.dynamics.com"); | |
| // By unique name | |
| var env = EnvironmentResolver.Resolve(environments, "org"); | |
| // By environment ID | |
| var env = EnvironmentResolver.Resolve(environments, "00000000-0000-0000-0000-000000000000"); | |
| // By partial friendly name (fuzzy match) | |
| var env = EnvironmentResolver.Resolve(environments, "Production"); |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
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.
| // - CloudEnvironment.UsGovDoD (DoD) | |
| // - CloudEnvironment.UsGovDod (DoD) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CloudEnvironment enum does not include UsNat and UsSec values. The actual enum only contains: Public, UsGov, UsGovHigh, UsGovDod, and China. Remove the references to CloudEnvironment.UsNat and CloudEnvironment.UsSec.
| // - CloudEnvironment.UsNat | |
| // - CloudEnvironment.UsSec | |
| ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| var source = new ProfileConnectionSource(profile, provider); | |
| var source = ProfileConnectionSource.FromProfile(profile); |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ProfileConnectionSource constructor signature is incorrect in this example. The constructor requires (AuthProfile profile, string environmentUrl, int maxPoolSize = 52, Action? deviceCodeCallback = null, string? environmentDisplayName = null). The provider parameter is not part of the constructor signature. Update the example to pass the required environmentUrl parameter instead of the provider.
| var provider = CredentialProviderFactory.Create(profile); | |
| var source = new ProfileConnectionSource(profile, provider); | |
| var source = new ProfileConnectionSource(profile, "https://yourorg.crm.dynamics.com"); |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
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.
| 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!); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 */ }
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| - **Linux/macOS**: Base64 encoding (use OS-level file permissions) | |
| - **Linux/macOS**: Simple obfuscation (use OS-level file permissions) |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
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.
| ### 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
AI
Dec 29, 2025
There was a problem hiding this comment.
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.
| TokenCacheType = TokenCacheType.Persistent // Default | |
| // TokenCacheType = TokenCacheType.InMemory // Per-process only | |
| TokenCacheType = TokenCacheType.OperatingSystem // Default | |
| // TokenCacheType = TokenCacheType.Memory // Per-process only |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
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.