From f2a69540c4657f96adb92844a8fe1430ea0c7fe5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Mar 2026 05:25:22 +0000 Subject: [PATCH 1/7] Initial plan From 8d57be4039610c8c39727bd3c0d7f1a1ad09aa5a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Mar 2026 05:32:16 +0000 Subject: [PATCH 2/7] Remove deprecated SharedTokenCache and add AzureDeveloperCli to NonInteractive chain - Remove SharedTokenCacheCredential from NonInteractive chain (deprecated in Azure.Identity) - Add AzureDeveloperCliCredential to NonInteractive chain (available since Azure.Identity 1.10.0) - Update ValidateSet, default CredentialPrecedence, switch statement, doc URLs, and help docs - Update CHANGELOG with unreleased entries Agent-Logs-Url: https://github.com/PalmEmanuel/AzAuth/sessions/604aff3f-8096-4773-8798-43fa246ec7d2 Co-authored-by: PalmEmanuel <3384251+PalmEmanuel@users.noreply.github.com> --- CHANGELOG.md | 8 ++++++++ docs/help/Get-AzToken.md | 2 +- source/AzAuth.Core/TokenManager.cs | 2 +- .../TokenManager.NonInteractive.cs | 2 +- source/AzAuth.PS/Cmdlets/GetAzToken.cs | 4 ++-- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2902d67..0bed770 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ The format is based on and uses the types of changes according to [Keep a Change ## [Unreleased] +### Added + +- Added `AzureDeveloperCli` to the `CredentialPrecedence` parameter for non-interactive authentication, allowing use of Azure Developer CLI (`azd`) credentials + +### Removed + +- Removed deprecated `SharedTokenCache` from the `CredentialPrecedence` parameter and default credential chain, as `SharedTokenCacheCredential` is deprecated in Azure.Identity + ## [2.8.0] - 2026-03-05 - Improve error handling by replacing exceptions with WriteError diff --git a/docs/help/Get-AzToken.md b/docs/help/Get-AzToken.md index 3e562e0..cc1a387 100644 --- a/docs/help/Get-AzToken.md +++ b/docs/help/Get-AzToken.md @@ -292,7 +292,7 @@ The order of precedence for the credentials to be used for getting a token non-i Type: String[] Parameter Sets: NonInteractive Aliases: -Accepted values: ManagedIdentity, Environment, AzurePowerShell, AzureCLI, VisualStudio, SharedTokenCache +Accepted values: ManagedIdentity, Environment, AzurePowerShell, AzureCLI, AzureDeveloperCli, VisualStudio Required: False Position: Named diff --git a/source/AzAuth.Core/TokenManager.cs b/source/AzAuth.Core/TokenManager.cs index 5b3cca9..8f1793d 100644 --- a/source/AzAuth.Core/TokenManager.cs +++ b/source/AzAuth.Core/TokenManager.cs @@ -21,8 +21,8 @@ internal static string GetCredentialDocumentationUrl(string credentialType) "Environment" => "https://learn.microsoft.com/en-us/dotnet/api/azure.identity.environmentcredential", "AzurePowerShell" => "https://learn.microsoft.com/en-us/dotnet/api/azure.identity.azurepowershellcredential", "AzureCLI" => "https://learn.microsoft.com/en-us/dotnet/api/azure.identity.azureclicredential", + "AzureDeveloperCli" => "https://learn.microsoft.com/en-us/dotnet/api/azure.identity.azuredeveloperclicredential", "VisualStudio" => "https://learn.microsoft.com/en-us/dotnet/api/azure.identity.visualstudiocredential", - "SharedTokenCache" => "https://learn.microsoft.com/en-us/dotnet/api/azure.identity.sharedtokencachecredential", _ => throw new ArgumentException("Invalid credential type", nameof(credentialType)) }; } diff --git a/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.NonInteractive.cs b/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.NonInteractive.cs index d3de57a..7a91439 100644 --- a/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.NonInteractive.cs +++ b/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.NonInteractive.cs @@ -58,8 +58,8 @@ internal static async Task GetTokenNonInteractiveAsync( "Environment" => new EnvironmentCredential(genericTimeoutOptions), "AzurePowerShell" => new AzurePowerShellCredential(genericTimeoutOptions as AzurePowerShellCredentialOptions), "AzureCLI" => new AzureCliCredential(genericTimeoutOptions as AzureCliCredentialOptions), + "AzureDeveloperCli" => new AzureDeveloperCliCredential(genericTimeoutOptions as AzureDeveloperCliCredentialOptions), "VisualStudio" => new VisualStudioCredential(genericTimeoutOptions as VisualStudioCredentialOptions), - "SharedTokenCache" => new SharedTokenCacheCredential(genericTimeoutOptions as SharedTokenCacheCredentialOptions), _ => throw new ArgumentException("Invalid credential type", nameof(credentialType)) }); } diff --git a/source/AzAuth.PS/Cmdlets/GetAzToken.cs b/source/AzAuth.PS/Cmdlets/GetAzToken.cs index b77708f..5dd301f 100644 --- a/source/AzAuth.PS/Cmdlets/GetAzToken.cs +++ b/source/AzAuth.PS/Cmdlets/GetAzToken.cs @@ -101,10 +101,10 @@ public class GetAzToken : PSLoggerCmdletBase public int TimeoutSeconds { get; set; } = 120; [Parameter(ParameterSetName = "NonInteractive")] - [ValidateSet("ManagedIdentity", "Environment", "AzurePowerShell", "AzureCLI", "VisualStudio", "SharedTokenCache")] + [ValidateSet("ManagedIdentity", "Environment", "AzurePowerShell", "AzureCLI", "AzureDeveloperCli", "VisualStudio")] [ValidateNotNullOrEmpty()] // TODO: Change back ManagedIdentity to first position in the chain once issue #112 is solved, likely in Azure.Identity 1.14.2 or later - public string[] CredentialPrecedence { get; set; } = ["Environment", "AzurePowerShell", "AzureCLI", "VisualStudio", "SharedTokenCache", "ManagedIdentity"]; + public string[] CredentialPrecedence { get; set; } = ["Environment", "AzurePowerShell", "AzureCLI", "AzureDeveloperCli", "VisualStudio", "ManagedIdentity"]; [Parameter(ParameterSetName = "Interactive", Mandatory = true)] public SwitchParameter Interactive { get; set; } From 2a537438bb5d49a25e361c3ccd3fb30e217f3784 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Mar 2026 15:41:20 +0000 Subject: [PATCH 3/7] Add missing Azure.Identity credential types: WorkloadIdentity, AzurePipelines, OnBehalfOf, AuthorizationCode After auditing all credential types in Azure.Identity 1.19.0, the following were missing: - WorkloadIdentityCredential added to NonInteractive chain (env-var based, Kubernetes workload identity) - AzurePipelinesCredential added as -AzurePipelines parameter set (Azure Pipelines OIDC) - OnBehalfOfCredential added as -OnBehalfOf parameter set (3 variants: secret, cert, cert path) - AuthorizationCodeCredential added as -AuthorizationCode parameter set (OAuth 2.0 auth code flow) UsernamePasswordCredential was excluded as it is deprecated in Azure.Identity 1.19.0. Agent-Logs-Url: https://github.com/PalmEmanuel/AzAuth/sessions/5901a382-f953-4e5d-8291-5649a5af1165 Co-authored-by: PalmEmanuel <3384251+PalmEmanuel@users.noreply.github.com> --- CHANGELOG.md | 4 + docs/help/Get-AzToken.md | 167 ++++++++++++++++-- source/AzAuth.Core/TokenManager.cs | 1 + .../TokenManager.AuthorizationCode.cs | 46 +++++ .../TokenManager.AzurePipelines.cs | 40 +++++ .../TokenManager.NonInteractive.cs | 1 + .../TokenManager.OnBehalfOf.cs | 93 ++++++++++ source/AzAuth.PS/Cmdlets/GetAzToken.cs | 137 +++++++++++++- tests/Get-AzToken.Tests.ps1 | 87 +++++++++ 9 files changed, 562 insertions(+), 14 deletions(-) create mode 100644 source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.AuthorizationCode.cs create mode 100644 source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.AzurePipelines.cs create mode 100644 source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.OnBehalfOf.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bed770..b61f64e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ The format is based on and uses the types of changes according to [Keep a Change ### Added - Added `AzureDeveloperCli` to the `CredentialPrecedence` parameter for non-interactive authentication, allowing use of Azure Developer CLI (`azd`) credentials +- Added `WorkloadIdentity` to the `CredentialPrecedence` parameter for non-interactive authentication, using `WorkloadIdentityCredential` which reads from environment variables set by Kubernetes workload identity +- Added `-AzurePipelines` parameter set for Azure Pipelines service connection OIDC authentication (`AzurePipelinesCredential`) +- Added `-OnBehalfOf` parameter set (with `-ClientSecret`, `-ClientCertificate`, and `-ClientCertificatePath` variants) for the OAuth 2.0 on-behalf-of flow (`OnBehalfOfCredential`) +- Added `-AuthorizationCode` parameter set for the OAuth 2.0 authorization code flow (`AuthorizationCodeCredential`), with optional `-RedirectUri` parameter ### Removed diff --git a/docs/help/Get-AzToken.md b/docs/help/Get-AzToken.md index cc1a387..5e698a6 100644 --- a/docs/help/Get-AzToken.md +++ b/docs/help/Get-AzToken.md @@ -78,6 +78,36 @@ Get-AzToken [[-Resource] ] [[-Scope] ] -Tenant [-Clai -ClientCertificatePath [-Force] [] ``` +### AzurePipelines +``` +Get-AzToken [[-Resource] ] [[-Scope] ] -Tenant [-Claim ] -ClientId + [-AzurePipelines] -ServiceConnectionId -SystemAccessToken [-Force] [] +``` + +### OnBehalfOf +``` +Get-AzToken [[-Resource] ] [[-Scope] ] -Tenant [-Claim ] -ClientId + [-OnBehalfOf] -UserAssertion -ClientSecret [-Force] [] +``` + +### OnBehalfOfCertificate +``` +Get-AzToken [[-Resource] ] [[-Scope] ] -Tenant [-Claim ] -ClientId + [-OnBehalfOf] -UserAssertion -ClientCertificate [-Force] [] +``` + +### OnBehalfOfCertificatePath +``` +Get-AzToken [[-Resource] ] [[-Scope] ] -Tenant [-Claim ] -ClientId + [-OnBehalfOf] -UserAssertion -ClientCertificatePath [-Force] [] +``` + +### AuthorizationCode +``` +Get-AzToken [[-Resource] ] [[-Scope] ] -Tenant [-Claim ] -ClientId + -ClientSecret -AuthorizationCode [-RedirectUri ] [-Force] [] +``` + ## DESCRIPTION Gets a new Azure access token. @@ -86,11 +116,12 @@ The token can be retrieved from an existing named cache, interactively from a br - Saved interactive credential if the command was used interactively in the same session (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.interactivebrowsercredential) - Environment variables (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.environmentcredential) +- Workload identity (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.workloadidentitycredential) - Azure PowerShell (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.azurepowershellcredential) - Azure CLI (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.azureclicredential) -- Visual Studio Code (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.visualstudiocodecredential) +- Azure Developer CLI (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.azuredeveloperclicredential) - Visual Studio (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.visualstudiocredential) -- Shared token cache (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.sharedtokencachecredential) +- Managed identity (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.managedidentitycredential) ## EXAMPLES @@ -210,11 +241,11 @@ Accept wildcard characters: False ### -ClientCertificate -The certificate to be used for getting a token with the client certificate flow. +The certificate to be used for getting a token with the client certificate flow, or the on-behalf-of flow. ```yaml Type: X509Certificate2 -Parameter Sets: ClientCertificate +Parameter Sets: ClientCertificate, OnBehalfOfCertificate Aliases: Required: True @@ -226,11 +257,11 @@ Accept wildcard characters: False ### -ClientCertificatePath -The path to a file containing both the certificate and private key, used for getting a token with the client certificate flow. +The path to a file containing both the certificate and private key, used for getting a token with the client certificate flow or the on-behalf-of flow. ```yaml Type: String -Parameter Sets: ClientCertificatePath +Parameter Sets: ClientCertificatePath, OnBehalfOfCertificatePath Aliases: Required: True @@ -258,7 +289,7 @@ Accept wildcard characters: False ```yaml Type: String -Parameter Sets: WorkloadIdentity, ClientSecret, ClientCertificate, ClientCertificatePath +Parameter Sets: WorkloadIdentity, ClientSecret, ClientCertificate, ClientCertificatePath, AzurePipelines, OnBehalfOf, OnBehalfOfCertificate, OnBehalfOfCertificatePath, AuthorizationCode Aliases: Required: True @@ -270,11 +301,11 @@ Accept wildcard characters: False ### -ClientSecret -The client secret to use for getting a token with the client credentials flow. +The client secret to use for getting a token with the client credentials flow, the on-behalf-of flow, or the authorization code flow. ```yaml Type: String -Parameter Sets: ClientSecret +Parameter Sets: ClientSecret, OnBehalfOf, AuthorizationCode Aliases: Required: True @@ -292,7 +323,7 @@ The order of precedence for the credentials to be used for getting a token non-i Type: String[] Parameter Sets: NonInteractive Aliases: -Accepted values: ManagedIdentity, Environment, AzurePowerShell, AzureCLI, AzureDeveloperCli, VisualStudio +Accepted values: ManagedIdentity, WorkloadIdentity, Environment, AzurePowerShell, AzureCLI, AzureDeveloperCli, VisualStudio Required: False Position: Named @@ -341,7 +372,7 @@ This may be required when combining interactive and non-interactive authenticati ```yaml Type: SwitchParameter -Parameter Sets: NonInteractive, Interactive, DeviceCode, ManagedIdentity, WorkloadIdentity, ClientSecret, ClientCertificate, ClientCertificatePath +Parameter Sets: NonInteractive, Interactive, DeviceCode, ManagedIdentity, WorkloadIdentity, ClientSecret, ClientCertificate, ClientCertificatePath, AzurePipelines, OnBehalfOf, OnBehalfOfCertificate, OnBehalfOfCertificatePath, AuthorizationCode Aliases: Required: False @@ -438,7 +469,7 @@ Accept wildcard characters: False ```yaml Type: String -Parameter Sets: WorkloadIdentity, ClientSecret, ClientCertificate, ClientCertificatePath +Parameter Sets: WorkloadIdentity, ClientSecret, ClientCertificate, ClientCertificatePath, AzurePipelines, OnBehalfOf, OnBehalfOfCertificate, OnBehalfOfCertificatePath, AuthorizationCode Aliases: TenantId Required: True @@ -540,6 +571,118 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -AzurePipelines + +Get a token using an Azure Pipelines service connection with OIDC (OpenID Connect). + +```yaml +Type: SwitchParameter +Parameter Sets: AzurePipelines +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ServiceConnectionId + +The service connection id configured in Azure Pipelines, used together with -AzurePipelines. + +```yaml +Type: String +Parameter Sets: AzurePipelines +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -SystemAccessToken + +The pipeline system access token (`$(System.AccessToken)` or `$env:SYSTEM_ACCESSTOKEN`), used together with -AzurePipelines. + +```yaml +Type: String +Parameter Sets: AzurePipelines +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -OnBehalfOf + +Get a token using the OAuth 2.0 on-behalf-of (OBO) flow. The client application receives a token with user permissions and exchanges it for a new token to call a downstream API. The client application must authenticate itself using either -ClientSecret, -ClientCertificate, or -ClientCertificatePath, and the original user token is provided via -UserAssertion. + +```yaml +Type: SwitchParameter +Parameter Sets: OnBehalfOf, OnBehalfOfCertificate, OnBehalfOfCertificatePath +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -UserAssertion + +The access token representing the user, used as the assertion in the on-behalf-of flow. + +```yaml +Type: String +Parameter Sets: OnBehalfOf, OnBehalfOfCertificate, OnBehalfOfCertificatePath +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -AuthorizationCode + +The OAuth 2.0 authorization code received from the authorization endpoint redirect, used together with -ClientSecret to exchange for an access token. + +```yaml +Type: String +Parameter Sets: AuthorizationCode +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -RedirectUri + +The redirect URI used when obtaining the authorization code. If not specified, the default native client redirect URI is used. + +```yaml +Type: Uri +Parameter Sets: AuthorizationCode +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### CommonParameters This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutBuffer, -OutVariable, -PipelineVariable, -ProgressAction, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). diff --git a/source/AzAuth.Core/TokenManager.cs b/source/AzAuth.Core/TokenManager.cs index 8f1793d..afdb40f 100644 --- a/source/AzAuth.Core/TokenManager.cs +++ b/source/AzAuth.Core/TokenManager.cs @@ -18,6 +18,7 @@ internal static string GetCredentialDocumentationUrl(string credentialType) return credentialType switch { "ManagedIdentity" => "https://learn.microsoft.com/en-us/dotnet/api/azure.identity.managedidentitycredential", + "WorkloadIdentity" => "https://learn.microsoft.com/en-us/dotnet/api/azure.identity.workloadidentitycredential", "Environment" => "https://learn.microsoft.com/en-us/dotnet/api/azure.identity.environmentcredential", "AzurePowerShell" => "https://learn.microsoft.com/en-us/dotnet/api/azure.identity.azurepowershellcredential", "AzureCLI" => "https://learn.microsoft.com/en-us/dotnet/api/azure.identity.azureclicredential", diff --git a/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.AuthorizationCode.cs b/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.AuthorizationCode.cs new file mode 100644 index 0000000..df36f07 --- /dev/null +++ b/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.AuthorizationCode.cs @@ -0,0 +1,46 @@ +using Azure.Core; +using Azure.Identity; + +namespace PipeHow.AzAuth; + +internal static partial class TokenManager +{ + /// + /// Gets token using the OAuth 2.0 authorization code flow. + /// + internal static AzToken GetTokenAuthorizationCode(string resource, string[] scopes, string? claims, string clientId, string tenantId, string clientSecret, string authorizationCode, Uri? redirectUri, CancellationToken cancellationToken) => + taskFactory.Run(() => GetTokenAuthorizationCodeAsync(resource, scopes, claims, clientId, tenantId, clientSecret, authorizationCode, redirectUri, cancellationToken)); + + /// + /// Gets token using the OAuth 2.0 authorization code flow. + /// + internal static async Task GetTokenAuthorizationCodeAsync( + string resource, + string[] scopes, + string? claims, + string clientId, + string tenantId, + string clientSecret, + string authorizationCode, + Uri? redirectUri, + CancellationToken cancellationToken) + { + var fullScopes = scopes.Select(s => $"{resource.TrimEnd('/')}/{s}").ToArray(); + var tokenRequestContext = new TokenRequestContext(fullScopes, null, claims, tenantId); + + AuthorizationCodeCredential cred; + if (redirectUri is not null) + { + cred = new AuthorizationCodeCredential(tenantId, clientId, clientSecret, authorizationCode, new AuthorizationCodeCredentialOptions { RedirectUri = redirectUri }); + } + else + { + cred = new AuthorizationCodeCredential(tenantId, clientId, clientSecret, authorizationCode); + } + + credential = cred; + previousClientId = clientId; + + return await GetTokenAsync(tokenRequestContext, cancellationToken); + } +} diff --git a/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.AzurePipelines.cs b/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.AzurePipelines.cs new file mode 100644 index 0000000..4f2e564 --- /dev/null +++ b/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.AzurePipelines.cs @@ -0,0 +1,40 @@ +using Azure.Core; +using Azure.Identity; + +namespace PipeHow.AzAuth; + +internal static partial class TokenManager +{ + /// + /// Gets token using Azure Pipelines service connection OIDC. + /// + internal static AzToken GetTokenAzurePipelines(string resource, string[] scopes, string? claims, string clientId, string tenantId, string serviceConnectionId, string systemAccessToken, CancellationToken cancellationToken) => + taskFactory.Run(() => GetTokenAzurePipelinesAsync(resource, scopes, claims, clientId, tenantId, serviceConnectionId, systemAccessToken, cancellationToken)); + + /// + /// Gets token using Azure Pipelines service connection OIDC. + /// + internal static async Task GetTokenAzurePipelinesAsync( + string resource, + string[] scopes, + string? claims, + string clientId, + string tenantId, + string serviceConnectionId, + string systemAccessToken, + CancellationToken cancellationToken) + { + var fullScopes = scopes.Select(s => $"{resource.TrimEnd('/')}/{s}").ToArray(); + var tokenRequestContext = new TokenRequestContext(fullScopes, null, claims, tenantId); + + // Re-use the previous credential if client id didn't change + if (credential is not AzurePipelinesCredential || previousClientId != clientId) + { + credential = new AzurePipelinesCredential(tenantId, clientId, serviceConnectionId, systemAccessToken); + } + + previousClientId = clientId; + + return await GetTokenAsync(tokenRequestContext, cancellationToken); + } +} diff --git a/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.NonInteractive.cs b/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.NonInteractive.cs index 7a91439..85c8880 100644 --- a/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.NonInteractive.cs +++ b/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.NonInteractive.cs @@ -55,6 +55,7 @@ internal static async Task GetTokenNonInteractiveAsync( MaxDelay = TimeSpan.Zero } }), + "WorkloadIdentity" => new WorkloadIdentityCredential(genericTimeoutOptions as WorkloadIdentityCredentialOptions), "Environment" => new EnvironmentCredential(genericTimeoutOptions), "AzurePowerShell" => new AzurePowerShellCredential(genericTimeoutOptions as AzurePowerShellCredentialOptions), "AzureCLI" => new AzureCliCredential(genericTimeoutOptions as AzureCliCredentialOptions), diff --git a/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.OnBehalfOf.cs b/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.OnBehalfOf.cs new file mode 100644 index 0000000..996bb3b --- /dev/null +++ b/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.OnBehalfOf.cs @@ -0,0 +1,93 @@ +using Azure.Core; +using Azure.Identity; +using System.Security.Cryptography.X509Certificates; + +namespace PipeHow.AzAuth; + +internal static partial class TokenManager +{ + /// + /// Gets token using the on-behalf-of flow with a client secret. + /// + internal static AzToken GetTokenOnBehalfOf(string resource, string[] scopes, string? claims, string clientId, string tenantId, string clientSecret, string userAssertion, CancellationToken cancellationToken) => + taskFactory.Run(() => GetTokenOnBehalfOfAsync(resource, scopes, claims, clientId, tenantId, clientSecret, userAssertion, cancellationToken)); + + /// + /// Gets token using the on-behalf-of flow with a client certificate. + /// + internal static AzToken GetTokenOnBehalfOfCertificate(string resource, string[] scopes, string? claims, string clientId, string tenantId, X509Certificate2 clientCertificate, string userAssertion, CancellationToken cancellationToken) => + taskFactory.Run(() => GetTokenOnBehalfOfCertificateAsync(resource, scopes, claims, clientId, tenantId, clientCertificate, userAssertion, cancellationToken)); + + /// + /// Gets token using the on-behalf-of flow with a client certificate from a file path. + /// + internal static AzToken GetTokenOnBehalfOfCertificatePath(string resource, string[] scopes, string? claims, string clientId, string tenantId, string clientCertificatePath, string userAssertion, CancellationToken cancellationToken) => + taskFactory.Run(() => GetTokenOnBehalfOfCertificatePathAsync(resource, scopes, claims, clientId, tenantId, clientCertificatePath, userAssertion, cancellationToken)); + + /// + /// Gets token using the on-behalf-of flow with a client secret. + /// + internal static async Task GetTokenOnBehalfOfAsync( + string resource, + string[] scopes, + string? claims, + string clientId, + string tenantId, + string clientSecret, + string userAssertion, + CancellationToken cancellationToken) + { + var fullScopes = scopes.Select(s => $"{resource.TrimEnd('/')}/{s}").ToArray(); + var tokenRequestContext = new TokenRequestContext(fullScopes, null, claims, tenantId); + + credential = new OnBehalfOfCredential(tenantId, clientId, clientSecret, userAssertion); + previousClientId = clientId; + + return await GetTokenAsync(tokenRequestContext, cancellationToken); + } + + /// + /// Gets token using the on-behalf-of flow with a client certificate. + /// + internal static async Task GetTokenOnBehalfOfCertificateAsync( + string resource, + string[] scopes, + string? claims, + string clientId, + string tenantId, + X509Certificate2 clientCertificate, + string userAssertion, + CancellationToken cancellationToken) + { + var fullScopes = scopes.Select(s => $"{resource.TrimEnd('/')}/{s}").ToArray(); + var tokenRequestContext = new TokenRequestContext(fullScopes, null, claims, tenantId); + + credential = new OnBehalfOfCredential(tenantId, clientId, clientCertificate, userAssertion); + previousClientId = clientId; + + return await GetTokenAsync(tokenRequestContext, cancellationToken); + } + + /// + /// Gets token using the on-behalf-of flow with a client certificate from a file path. + /// + internal static async Task GetTokenOnBehalfOfCertificatePathAsync( + string resource, + string[] scopes, + string? claims, + string clientId, + string tenantId, + string clientCertificatePath, + string userAssertion, + CancellationToken cancellationToken) + { + var fullScopes = scopes.Select(s => $"{resource.TrimEnd('/')}/{s}").ToArray(); + var tokenRequestContext = new TokenRequestContext(fullScopes, null, claims, tenantId); + + var cert = new X509Certificate2(clientCertificatePath); + credential = new OnBehalfOfCredential(tenantId, clientId, cert, userAssertion); + previousClientId = clientId; + + return await GetTokenAsync(tokenRequestContext, cancellationToken); + } +} diff --git a/source/AzAuth.PS/Cmdlets/GetAzToken.cs b/source/AzAuth.PS/Cmdlets/GetAzToken.cs index 5dd301f..f73da53 100644 --- a/source/AzAuth.PS/Cmdlets/GetAzToken.cs +++ b/source/AzAuth.PS/Cmdlets/GetAzToken.cs @@ -19,6 +19,11 @@ public class GetAzToken : PSLoggerCmdletBase [Parameter(ParameterSetName = "ClientSecret", Position = 0)] [Parameter(ParameterSetName = "ClientCertificate", Position = 0)] [Parameter(ParameterSetName = "ClientCertificatePath", Position = 0)] + [Parameter(ParameterSetName = "AzurePipelines", Position = 0)] + [Parameter(ParameterSetName = "OnBehalfOf", Position = 0)] + [Parameter(ParameterSetName = "OnBehalfOfCertificate", Position = 0)] + [Parameter(ParameterSetName = "OnBehalfOfCertificatePath", Position = 0)] + [Parameter(ParameterSetName = "AuthorizationCode", Position = 0)] [ValidateNotNullOrEmpty] [Alias("ResourceId", "ResourceUrl")] public string Resource { get; set; } = "https://graph.microsoft.com"; @@ -33,6 +38,11 @@ public class GetAzToken : PSLoggerCmdletBase [Parameter(ParameterSetName = "ClientSecret", Position = 1)] [Parameter(ParameterSetName = "ClientCertificate", Position = 1)] [Parameter(ParameterSetName = "ClientCertificatePath", Position = 1)] + [Parameter(ParameterSetName = "AzurePipelines", Position = 1)] + [Parameter(ParameterSetName = "OnBehalfOf", Position = 1)] + [Parameter(ParameterSetName = "OnBehalfOfCertificate", Position = 1)] + [Parameter(ParameterSetName = "OnBehalfOfCertificatePath", Position = 1)] + [Parameter(ParameterSetName = "AuthorizationCode", Position = 1)] [ValidateNotNullOrEmpty] public string[] Scope { get; set; } = new[] { ".default" }; @@ -46,6 +56,11 @@ public class GetAzToken : PSLoggerCmdletBase [Parameter(ParameterSetName = "ClientSecret", Mandatory = true)] [Parameter(ParameterSetName = "ClientCertificate", Mandatory = true)] [Parameter(ParameterSetName = "ClientCertificatePath", Mandatory = true)] + [Parameter(ParameterSetName = "AzurePipelines", Mandatory = true)] + [Parameter(ParameterSetName = "OnBehalfOf", Mandatory = true)] + [Parameter(ParameterSetName = "OnBehalfOfCertificate", Mandatory = true)] + [Parameter(ParameterSetName = "OnBehalfOfCertificatePath", Mandatory = true)] + [Parameter(ParameterSetName = "AuthorizationCode", Mandatory = true)] [ValidateNotNullOrEmpty] [Alias("TenantId")] public string Tenant { get; set; } @@ -60,6 +75,11 @@ public class GetAzToken : PSLoggerCmdletBase [Parameter(ParameterSetName = "ClientSecret")] [Parameter(ParameterSetName = "ClientCertificate")] [Parameter(ParameterSetName = "ClientCertificatePath")] + [Parameter(ParameterSetName = "AzurePipelines")] + [Parameter(ParameterSetName = "OnBehalfOf")] + [Parameter(ParameterSetName = "OnBehalfOfCertificate")] + [Parameter(ParameterSetName = "OnBehalfOfCertificatePath")] + [Parameter(ParameterSetName = "AuthorizationCode")] [ValidateNotNullOrEmpty] public string Claim { get; set; } @@ -73,6 +93,11 @@ public class GetAzToken : PSLoggerCmdletBase [Parameter(ParameterSetName = "ClientSecret", Mandatory = true)] [Parameter(ParameterSetName = "ClientCertificate", Mandatory = true)] [Parameter(ParameterSetName = "ClientCertificatePath", Mandatory = true)] + [Parameter(ParameterSetName = "AzurePipelines", Mandatory = true)] + [Parameter(ParameterSetName = "OnBehalfOf", Mandatory = true)] + [Parameter(ParameterSetName = "OnBehalfOfCertificate", Mandatory = true)] + [Parameter(ParameterSetName = "OnBehalfOfCertificatePath", Mandatory = true)] + [Parameter(ParameterSetName = "AuthorizationCode", Mandatory = true)] [ValidateNotNullOrEmpty] public string ClientId { get; set; } @@ -101,10 +126,10 @@ public class GetAzToken : PSLoggerCmdletBase public int TimeoutSeconds { get; set; } = 120; [Parameter(ParameterSetName = "NonInteractive")] - [ValidateSet("ManagedIdentity", "Environment", "AzurePowerShell", "AzureCLI", "AzureDeveloperCli", "VisualStudio")] + [ValidateSet("ManagedIdentity", "WorkloadIdentity", "Environment", "AzurePowerShell", "AzureCLI", "AzureDeveloperCli", "VisualStudio")] [ValidateNotNullOrEmpty()] // TODO: Change back ManagedIdentity to first position in the chain once issue #112 is solved, likely in Azure.Identity 1.14.2 or later - public string[] CredentialPrecedence { get; set; } = ["Environment", "AzurePowerShell", "AzureCLI", "AzureDeveloperCli", "VisualStudio", "ManagedIdentity"]; + public string[] CredentialPrecedence { get; set; } = ["Environment", "WorkloadIdentity", "AzurePowerShell", "AzureCLI", "AzureDeveloperCli", "VisualStudio", "ManagedIdentity"]; [Parameter(ParameterSetName = "Interactive", Mandatory = true)] public SwitchParameter Interactive { get; set; } @@ -125,19 +150,53 @@ public class GetAzToken : PSLoggerCmdletBase [ValidateNotNullOrEmpty] public string ExternalToken { get; set; } + [Parameter(ParameterSetName = "AzurePipelines", Mandatory = true)] + public SwitchParameter AzurePipelines { get; set; } + + [Parameter(ParameterSetName = "AzurePipelines", Mandatory = true)] + [ValidateNotNullOrEmpty] + public string ServiceConnectionId { get; set; } + + [Parameter(ParameterSetName = "AzurePipelines", Mandatory = true)] + [ValidateNotNullOrEmpty] + public string SystemAccessToken { get; set; } + + [Parameter(ParameterSetName = "OnBehalfOf", Mandatory = true)] + [Parameter(ParameterSetName = "OnBehalfOfCertificate", Mandatory = true)] + [Parameter(ParameterSetName = "OnBehalfOfCertificatePath", Mandatory = true)] + public SwitchParameter OnBehalfOf { get; set; } + + [Parameter(ParameterSetName = "OnBehalfOf", Mandatory = true)] + [Parameter(ParameterSetName = "OnBehalfOfCertificate", Mandatory = true)] + [Parameter(ParameterSetName = "OnBehalfOfCertificatePath", Mandatory = true)] + [ValidateNotNullOrEmpty] + public string UserAssertion { get; set; } + [Parameter(ParameterSetName = "ClientSecret", Mandatory = true)] + [Parameter(ParameterSetName = "OnBehalfOf", Mandatory = true)] + [Parameter(ParameterSetName = "AuthorizationCode", Mandatory = true)] [ValidateNotNullOrEmpty] public string ClientSecret { get; set; } [Parameter(ParameterSetName = "ClientCertificate", Mandatory = true)] + [Parameter(ParameterSetName = "OnBehalfOfCertificate", Mandatory = true)] [ValidateNotNullOrEmpty] public X509Certificate2 ClientCertificate { get; set; } [Parameter(ParameterSetName = "ClientCertificatePath", Mandatory = true)] + [Parameter(ParameterSetName = "OnBehalfOfCertificatePath", Mandatory = true)] [ValidateNotNullOrEmpty] [ValidateCertificatePath] public string ClientCertificatePath { get; set; } + [Parameter(ParameterSetName = "AuthorizationCode", Mandatory = true)] + [ValidateNotNullOrEmpty] + public string AuthorizationCode { get; set; } + + [Parameter(ParameterSetName = "AuthorizationCode")] + [ValidateNotNullOrEmpty] + public Uri RedirectUri { get; set; } + [Parameter(ParameterSetName = "NonInteractive")] [Parameter(ParameterSetName = "Interactive")] [Parameter(ParameterSetName = "DeviceCode")] @@ -146,6 +205,11 @@ public class GetAzToken : PSLoggerCmdletBase [Parameter(ParameterSetName = "ClientSecret")] [Parameter(ParameterSetName = "ClientCertificate")] [Parameter(ParameterSetName = "ClientCertificatePath")] + [Parameter(ParameterSetName = "AzurePipelines")] + [Parameter(ParameterSetName = "OnBehalfOf")] + [Parameter(ParameterSetName = "OnBehalfOfCertificate")] + [Parameter(ParameterSetName = "OnBehalfOfCertificatePath")] + [Parameter(ParameterSetName = "AuthorizationCode")] public SwitchParameter Force { get; set; } // If user specifies Force, disregard earlier authentication @@ -338,6 +402,61 @@ should be return; } } + else if (AzurePipelines.IsPresent) + { + WriteVerbose($"Getting token using Azure Pipelines service connection OIDC for client '{ClientId}' (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.azurepipelinescredential)."); + try + { + WriteObject(TokenManager.GetTokenAzurePipelines(Resource, Scope, Claim, ClientId, Tenant, ServiceConnectionId, SystemAccessToken, stopProcessing.Token)); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "AzurePipelinesTokenError", ErrorCategory.AuthenticationError, null)); + return; + } + } + else if (OnBehalfOf.IsPresent) + { + if (ParameterSetName == "OnBehalfOf") + { + WriteVerbose($"Getting token using on-behalf-of flow with client secret for client '{ClientId}' (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.onbehalfofcredential)."); + try + { + WriteObject(TokenManager.GetTokenOnBehalfOf(Resource, Scope, Claim, ClientId, Tenant, ClientSecret, UserAssertion, stopProcessing.Token)); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "OnBehalfOfTokenError", ErrorCategory.AuthenticationError, null)); + return; + } + } + else if (ParameterSetName == "OnBehalfOfCertificate") + { + WriteVerbose($"Getting token using on-behalf-of flow with client certificate for client '{ClientId}' (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.onbehalfofcredential)."); + try + { + WriteObject(TokenManager.GetTokenOnBehalfOfCertificate(Resource, Scope, Claim, ClientId, Tenant, ClientCertificate, UserAssertion, stopProcessing.Token)); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "OnBehalfOfCertificateTokenError", ErrorCategory.AuthenticationError, null)); + return; + } + } + else if (ParameterSetName == "OnBehalfOfCertificatePath") + { + WriteVerbose($"Getting token using on-behalf-of flow with client certificate for client '{ClientId}' (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.onbehalfofcredential)."); + try + { + WriteObject(TokenManager.GetTokenOnBehalfOfCertificatePath(Resource, Scope, Claim, ClientId, Tenant, ClientCertificatePath, UserAssertion, stopProcessing.Token)); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "OnBehalfOfCertificatePathTokenError", ErrorCategory.AuthenticationError, null)); + return; + } + } + } else if (ParameterSetName == "ClientSecret") { WriteVerbose($"Getting token using client secret for client '{ClientId}' (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.clientsecretcredential)."); @@ -377,6 +496,20 @@ should be return; } } + else if (ParameterSetName == "AuthorizationCode") + { + WriteVerbose($"Getting token using authorization code flow for client '{ClientId}' (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.authorizationcodecredential)."); + try + { + Uri? redirectUri = MyInvocation.BoundParameters.ContainsKey("RedirectUri") ? RedirectUri : null; + WriteObject(TokenManager.GetTokenAuthorizationCode(Resource, Scope, Claim, ClientId, Tenant, ClientSecret, AuthorizationCode, redirectUri, stopProcessing.Token)); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "AuthorizationCodeTokenError", ErrorCategory.AuthenticationError, null)); + return; + } + } else { WriteError(new ErrorRecord(new ArgumentException("Invalid parameter combination!"), "InvalidParameters", ErrorCategory.WriteError, null)); diff --git a/tests/Get-AzToken.Tests.ps1 b/tests/Get-AzToken.Tests.ps1 index 00373d0..9748820 100644 --- a/tests/Get-AzToken.Tests.ps1 +++ b/tests/Get-AzToken.Tests.ps1 @@ -14,6 +14,11 @@ BeforeDiscovery { @{ Name = 'ClientSecret'; Mandatory = $false } @{ Name = 'ClientCertificate'; Mandatory = $false } @{ Name = 'ClientCertificatePath'; Mandatory = $false } + @{ Name = 'AzurePipelines'; Mandatory = $false } + @{ Name = 'OnBehalfOf'; Mandatory = $false } + @{ Name = 'OnBehalfOfCertificate'; Mandatory = $false } + @{ Name = 'OnBehalfOfCertificatePath'; Mandatory = $false } + @{ Name = 'AuthorizationCode'; Mandatory = $false } ) } @{ @@ -30,6 +35,11 @@ BeforeDiscovery { @{ Name = 'ClientSecret'; Mandatory = $false } @{ Name = 'ClientCertificate'; Mandatory = $false } @{ Name = 'ClientCertificatePath'; Mandatory = $false } + @{ Name = 'AzurePipelines'; Mandatory = $false } + @{ Name = 'OnBehalfOf'; Mandatory = $false } + @{ Name = 'OnBehalfOfCertificate'; Mandatory = $false } + @{ Name = 'OnBehalfOfCertificatePath'; Mandatory = $false } + @{ Name = 'AuthorizationCode'; Mandatory = $false } ) } @{ @@ -46,6 +56,11 @@ BeforeDiscovery { @{ Name = 'ClientSecret'; Mandatory = $true } @{ Name = 'ClientCertificate'; Mandatory = $true } @{ Name = 'ClientCertificatePath'; Mandatory = $true } + @{ Name = 'AzurePipelines'; Mandatory = $true } + @{ Name = 'OnBehalfOf'; Mandatory = $true } + @{ Name = 'OnBehalfOfCertificate'; Mandatory = $true } + @{ Name = 'OnBehalfOfCertificatePath'; Mandatory = $true } + @{ Name = 'AuthorizationCode'; Mandatory = $true } ) } @{ @@ -62,6 +77,11 @@ BeforeDiscovery { @{ Name = 'ClientSecret'; Mandatory = $false } @{ Name = 'ClientCertificate'; Mandatory = $false } @{ Name = 'ClientCertificatePath'; Mandatory = $false } + @{ Name = 'AzurePipelines'; Mandatory = $false } + @{ Name = 'OnBehalfOf'; Mandatory = $false } + @{ Name = 'OnBehalfOfCertificate'; Mandatory = $false } + @{ Name = 'OnBehalfOfCertificatePath'; Mandatory = $false } + @{ Name = 'AuthorizationCode'; Mandatory = $false } ) } @{ @@ -78,6 +98,11 @@ BeforeDiscovery { @{ Name = 'ClientSecret'; Mandatory = $true } @{ Name = 'ClientCertificate'; Mandatory = $true } @{ Name = 'ClientCertificatePath'; Mandatory = $true } + @{ Name = 'AzurePipelines'; Mandatory = $true } + @{ Name = 'OnBehalfOf'; Mandatory = $true } + @{ Name = 'OnBehalfOfCertificate'; Mandatory = $true } + @{ Name = 'OnBehalfOfCertificatePath'; Mandatory = $true } + @{ Name = 'AuthorizationCode'; Mandatory = $true } ) } @{ @@ -169,6 +194,8 @@ BeforeDiscovery { Type = 'string' ParameterSets = @( @{ Name = 'ClientSecret'; Mandatory = $true } + @{ Name = 'OnBehalfOf'; Mandatory = $true } + @{ Name = 'AuthorizationCode'; Mandatory = $true } ) } @{ @@ -176,6 +203,7 @@ BeforeDiscovery { Type = 'System.Security.Cryptography.X509Certificates.X509Certificate2' ParameterSets = @( @{ Name = 'ClientCertificate'; Mandatory = $true } + @{ Name = 'OnBehalfOfCertificate'; Mandatory = $true } ) } @{ @@ -183,6 +211,60 @@ BeforeDiscovery { Type = 'string' ParameterSets = @( @{ Name = 'ClientCertificatePath'; Mandatory = $true } + @{ Name = 'OnBehalfOfCertificatePath'; Mandatory = $true } + ) + } + @{ + Name = 'AzurePipelines' + Type = 'System.Management.Automation.SwitchParameter' + ParameterSets = @( + @{ Name = 'AzurePipelines'; Mandatory = $true } + ) + } + @{ + Name = 'ServiceConnectionId' + Type = 'string' + ParameterSets = @( + @{ Name = 'AzurePipelines'; Mandatory = $true } + ) + } + @{ + Name = 'SystemAccessToken' + Type = 'string' + ParameterSets = @( + @{ Name = 'AzurePipelines'; Mandatory = $true } + ) + } + @{ + Name = 'OnBehalfOf' + Type = 'System.Management.Automation.SwitchParameter' + ParameterSets = @( + @{ Name = 'OnBehalfOf'; Mandatory = $true } + @{ Name = 'OnBehalfOfCertificate'; Mandatory = $true } + @{ Name = 'OnBehalfOfCertificatePath'; Mandatory = $true } + ) + } + @{ + Name = 'UserAssertion' + Type = 'string' + ParameterSets = @( + @{ Name = 'OnBehalfOf'; Mandatory = $true } + @{ Name = 'OnBehalfOfCertificate'; Mandatory = $true } + @{ Name = 'OnBehalfOfCertificatePath'; Mandatory = $true } + ) + } + @{ + Name = 'AuthorizationCode' + Type = 'string' + ParameterSets = @( + @{ Name = 'AuthorizationCode'; Mandatory = $true } + ) + } + @{ + Name = 'RedirectUri' + Type = 'System.Uri' + ParameterSets = @( + @{ Name = 'AuthorizationCode'; Mandatory = $false } ) } @{ @@ -197,6 +279,11 @@ BeforeDiscovery { @{ Name = 'ClientSecret'; Mandatory = $false } @{ Name = 'ClientCertificate'; Mandatory = $false } @{ Name = 'ClientCertificatePath'; Mandatory = $false } + @{ Name = 'AzurePipelines'; Mandatory = $false } + @{ Name = 'OnBehalfOf'; Mandatory = $false } + @{ Name = 'OnBehalfOfCertificate'; Mandatory = $false } + @{ Name = 'OnBehalfOfCertificatePath'; Mandatory = $false } + @{ Name = 'AuthorizationCode'; Mandatory = $false } ) } ) From 2a1839cc1324cbe5a849fa63666b85bfd749d457 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Mar 2026 16:43:31 +0000 Subject: [PATCH 4/7] Remove OnBehalfOf and AuthorizationCode flows, keep AzurePipelines Agent-Logs-Url: https://github.com/PalmEmanuel/AzAuth/sessions/369ca9aa-1bb3-4150-8e56-1fee519eeb63 Co-authored-by: PalmEmanuel <3384251+PalmEmanuel@users.noreply.github.com> --- docs/help/Get-AzToken.md | 106 ++---------------- .../TokenManager.AuthorizationCode.cs | 46 -------- .../TokenManager.OnBehalfOf.cs | 93 --------------- source/AzAuth.PS/Cmdlets/GetAzToken.cs | 103 ----------------- tests/Get-AzToken.Tests.ps1 | 60 ---------- 5 files changed, 9 insertions(+), 399 deletions(-) delete mode 100644 source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.AuthorizationCode.cs delete mode 100644 source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.OnBehalfOf.cs diff --git a/docs/help/Get-AzToken.md b/docs/help/Get-AzToken.md index 5e698a6..e6f99a5 100644 --- a/docs/help/Get-AzToken.md +++ b/docs/help/Get-AzToken.md @@ -84,30 +84,6 @@ Get-AzToken [[-Resource] ] [[-Scope] ] -Tenant [-Clai [-AzurePipelines] -ServiceConnectionId -SystemAccessToken [-Force] [] ``` -### OnBehalfOf -``` -Get-AzToken [[-Resource] ] [[-Scope] ] -Tenant [-Claim ] -ClientId - [-OnBehalfOf] -UserAssertion -ClientSecret [-Force] [] -``` - -### OnBehalfOfCertificate -``` -Get-AzToken [[-Resource] ] [[-Scope] ] -Tenant [-Claim ] -ClientId - [-OnBehalfOf] -UserAssertion -ClientCertificate [-Force] [] -``` - -### OnBehalfOfCertificatePath -``` -Get-AzToken [[-Resource] ] [[-Scope] ] -Tenant [-Claim ] -ClientId - [-OnBehalfOf] -UserAssertion -ClientCertificatePath [-Force] [] -``` - -### AuthorizationCode -``` -Get-AzToken [[-Resource] ] [[-Scope] ] -Tenant [-Claim ] -ClientId - -ClientSecret -AuthorizationCode [-RedirectUri ] [-Force] [] -``` - ## DESCRIPTION Gets a new Azure access token. @@ -241,11 +217,11 @@ Accept wildcard characters: False ### -ClientCertificate -The certificate to be used for getting a token with the client certificate flow, or the on-behalf-of flow. +The certificate to be used for getting a token with the client certificate flow. ```yaml Type: X509Certificate2 -Parameter Sets: ClientCertificate, OnBehalfOfCertificate +Parameter Sets: ClientCertificate Aliases: Required: True @@ -257,11 +233,11 @@ Accept wildcard characters: False ### -ClientCertificatePath -The path to a file containing both the certificate and private key, used for getting a token with the client certificate flow or the on-behalf-of flow. +The path to a file containing both the certificate and private key, used for getting a token with the client certificate flow. ```yaml Type: String -Parameter Sets: ClientCertificatePath, OnBehalfOfCertificatePath +Parameter Sets: ClientCertificatePath Aliases: Required: True @@ -289,7 +265,7 @@ Accept wildcard characters: False ```yaml Type: String -Parameter Sets: WorkloadIdentity, ClientSecret, ClientCertificate, ClientCertificatePath, AzurePipelines, OnBehalfOf, OnBehalfOfCertificate, OnBehalfOfCertificatePath, AuthorizationCode +Parameter Sets: WorkloadIdentity, ClientSecret, ClientCertificate, ClientCertificatePath, AzurePipelines Aliases: Required: True @@ -301,11 +277,11 @@ Accept wildcard characters: False ### -ClientSecret -The client secret to use for getting a token with the client credentials flow, the on-behalf-of flow, or the authorization code flow. +The client secret to use for getting a token with the client credentials flow. ```yaml Type: String -Parameter Sets: ClientSecret, OnBehalfOf, AuthorizationCode +Parameter Sets: ClientSecret Aliases: Required: True @@ -372,7 +348,7 @@ This may be required when combining interactive and non-interactive authenticati ```yaml Type: SwitchParameter -Parameter Sets: NonInteractive, Interactive, DeviceCode, ManagedIdentity, WorkloadIdentity, ClientSecret, ClientCertificate, ClientCertificatePath, AzurePipelines, OnBehalfOf, OnBehalfOfCertificate, OnBehalfOfCertificatePath, AuthorizationCode +Parameter Sets: NonInteractive, Interactive, DeviceCode, ManagedIdentity, WorkloadIdentity, ClientSecret, ClientCertificate, ClientCertificatePath, AzurePipelines Aliases: Required: False @@ -469,7 +445,7 @@ Accept wildcard characters: False ```yaml Type: String -Parameter Sets: WorkloadIdentity, ClientSecret, ClientCertificate, ClientCertificatePath, AzurePipelines, OnBehalfOf, OnBehalfOfCertificate, OnBehalfOfCertificatePath, AuthorizationCode +Parameter Sets: WorkloadIdentity, ClientSecret, ClientCertificate, ClientCertificatePath, AzurePipelines Aliases: TenantId Required: True @@ -619,70 +595,6 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -OnBehalfOf - -Get a token using the OAuth 2.0 on-behalf-of (OBO) flow. The client application receives a token with user permissions and exchanges it for a new token to call a downstream API. The client application must authenticate itself using either -ClientSecret, -ClientCertificate, or -ClientCertificatePath, and the original user token is provided via -UserAssertion. - -```yaml -Type: SwitchParameter -Parameter Sets: OnBehalfOf, OnBehalfOfCertificate, OnBehalfOfCertificatePath -Aliases: - -Required: True -Position: Named -Default value: None -Accept pipeline input: False -Accept wildcard characters: False -``` - -### -UserAssertion - -The access token representing the user, used as the assertion in the on-behalf-of flow. - -```yaml -Type: String -Parameter Sets: OnBehalfOf, OnBehalfOfCertificate, OnBehalfOfCertificatePath -Aliases: - -Required: True -Position: Named -Default value: None -Accept pipeline input: False -Accept wildcard characters: False -``` - -### -AuthorizationCode - -The OAuth 2.0 authorization code received from the authorization endpoint redirect, used together with -ClientSecret to exchange for an access token. - -```yaml -Type: String -Parameter Sets: AuthorizationCode -Aliases: - -Required: True -Position: Named -Default value: None -Accept pipeline input: False -Accept wildcard characters: False -``` - -### -RedirectUri - -The redirect URI used when obtaining the authorization code. If not specified, the default native client redirect URI is used. - -```yaml -Type: Uri -Parameter Sets: AuthorizationCode -Aliases: - -Required: False -Position: Named -Default value: None -Accept pipeline input: False -Accept wildcard characters: False -``` - ### CommonParameters This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutBuffer, -OutVariable, -PipelineVariable, -ProgressAction, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). diff --git a/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.AuthorizationCode.cs b/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.AuthorizationCode.cs deleted file mode 100644 index df36f07..0000000 --- a/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.AuthorizationCode.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Azure.Core; -using Azure.Identity; - -namespace PipeHow.AzAuth; - -internal static partial class TokenManager -{ - /// - /// Gets token using the OAuth 2.0 authorization code flow. - /// - internal static AzToken GetTokenAuthorizationCode(string resource, string[] scopes, string? claims, string clientId, string tenantId, string clientSecret, string authorizationCode, Uri? redirectUri, CancellationToken cancellationToken) => - taskFactory.Run(() => GetTokenAuthorizationCodeAsync(resource, scopes, claims, clientId, tenantId, clientSecret, authorizationCode, redirectUri, cancellationToken)); - - /// - /// Gets token using the OAuth 2.0 authorization code flow. - /// - internal static async Task GetTokenAuthorizationCodeAsync( - string resource, - string[] scopes, - string? claims, - string clientId, - string tenantId, - string clientSecret, - string authorizationCode, - Uri? redirectUri, - CancellationToken cancellationToken) - { - var fullScopes = scopes.Select(s => $"{resource.TrimEnd('/')}/{s}").ToArray(); - var tokenRequestContext = new TokenRequestContext(fullScopes, null, claims, tenantId); - - AuthorizationCodeCredential cred; - if (redirectUri is not null) - { - cred = new AuthorizationCodeCredential(tenantId, clientId, clientSecret, authorizationCode, new AuthorizationCodeCredentialOptions { RedirectUri = redirectUri }); - } - else - { - cred = new AuthorizationCodeCredential(tenantId, clientId, clientSecret, authorizationCode); - } - - credential = cred; - previousClientId = clientId; - - return await GetTokenAsync(tokenRequestContext, cancellationToken); - } -} diff --git a/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.OnBehalfOf.cs b/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.OnBehalfOf.cs deleted file mode 100644 index 996bb3b..0000000 --- a/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.OnBehalfOf.cs +++ /dev/null @@ -1,93 +0,0 @@ -using Azure.Core; -using Azure.Identity; -using System.Security.Cryptography.X509Certificates; - -namespace PipeHow.AzAuth; - -internal static partial class TokenManager -{ - /// - /// Gets token using the on-behalf-of flow with a client secret. - /// - internal static AzToken GetTokenOnBehalfOf(string resource, string[] scopes, string? claims, string clientId, string tenantId, string clientSecret, string userAssertion, CancellationToken cancellationToken) => - taskFactory.Run(() => GetTokenOnBehalfOfAsync(resource, scopes, claims, clientId, tenantId, clientSecret, userAssertion, cancellationToken)); - - /// - /// Gets token using the on-behalf-of flow with a client certificate. - /// - internal static AzToken GetTokenOnBehalfOfCertificate(string resource, string[] scopes, string? claims, string clientId, string tenantId, X509Certificate2 clientCertificate, string userAssertion, CancellationToken cancellationToken) => - taskFactory.Run(() => GetTokenOnBehalfOfCertificateAsync(resource, scopes, claims, clientId, tenantId, clientCertificate, userAssertion, cancellationToken)); - - /// - /// Gets token using the on-behalf-of flow with a client certificate from a file path. - /// - internal static AzToken GetTokenOnBehalfOfCertificatePath(string resource, string[] scopes, string? claims, string clientId, string tenantId, string clientCertificatePath, string userAssertion, CancellationToken cancellationToken) => - taskFactory.Run(() => GetTokenOnBehalfOfCertificatePathAsync(resource, scopes, claims, clientId, tenantId, clientCertificatePath, userAssertion, cancellationToken)); - - /// - /// Gets token using the on-behalf-of flow with a client secret. - /// - internal static async Task GetTokenOnBehalfOfAsync( - string resource, - string[] scopes, - string? claims, - string clientId, - string tenantId, - string clientSecret, - string userAssertion, - CancellationToken cancellationToken) - { - var fullScopes = scopes.Select(s => $"{resource.TrimEnd('/')}/{s}").ToArray(); - var tokenRequestContext = new TokenRequestContext(fullScopes, null, claims, tenantId); - - credential = new OnBehalfOfCredential(tenantId, clientId, clientSecret, userAssertion); - previousClientId = clientId; - - return await GetTokenAsync(tokenRequestContext, cancellationToken); - } - - /// - /// Gets token using the on-behalf-of flow with a client certificate. - /// - internal static async Task GetTokenOnBehalfOfCertificateAsync( - string resource, - string[] scopes, - string? claims, - string clientId, - string tenantId, - X509Certificate2 clientCertificate, - string userAssertion, - CancellationToken cancellationToken) - { - var fullScopes = scopes.Select(s => $"{resource.TrimEnd('/')}/{s}").ToArray(); - var tokenRequestContext = new TokenRequestContext(fullScopes, null, claims, tenantId); - - credential = new OnBehalfOfCredential(tenantId, clientId, clientCertificate, userAssertion); - previousClientId = clientId; - - return await GetTokenAsync(tokenRequestContext, cancellationToken); - } - - /// - /// Gets token using the on-behalf-of flow with a client certificate from a file path. - /// - internal static async Task GetTokenOnBehalfOfCertificatePathAsync( - string resource, - string[] scopes, - string? claims, - string clientId, - string tenantId, - string clientCertificatePath, - string userAssertion, - CancellationToken cancellationToken) - { - var fullScopes = scopes.Select(s => $"{resource.TrimEnd('/')}/{s}").ToArray(); - var tokenRequestContext = new TokenRequestContext(fullScopes, null, claims, tenantId); - - var cert = new X509Certificate2(clientCertificatePath); - credential = new OnBehalfOfCredential(tenantId, clientId, cert, userAssertion); - previousClientId = clientId; - - return await GetTokenAsync(tokenRequestContext, cancellationToken); - } -} diff --git a/source/AzAuth.PS/Cmdlets/GetAzToken.cs b/source/AzAuth.PS/Cmdlets/GetAzToken.cs index f73da53..c6feb6f 100644 --- a/source/AzAuth.PS/Cmdlets/GetAzToken.cs +++ b/source/AzAuth.PS/Cmdlets/GetAzToken.cs @@ -20,10 +20,6 @@ public class GetAzToken : PSLoggerCmdletBase [Parameter(ParameterSetName = "ClientCertificate", Position = 0)] [Parameter(ParameterSetName = "ClientCertificatePath", Position = 0)] [Parameter(ParameterSetName = "AzurePipelines", Position = 0)] - [Parameter(ParameterSetName = "OnBehalfOf", Position = 0)] - [Parameter(ParameterSetName = "OnBehalfOfCertificate", Position = 0)] - [Parameter(ParameterSetName = "OnBehalfOfCertificatePath", Position = 0)] - [Parameter(ParameterSetName = "AuthorizationCode", Position = 0)] [ValidateNotNullOrEmpty] [Alias("ResourceId", "ResourceUrl")] public string Resource { get; set; } = "https://graph.microsoft.com"; @@ -39,10 +35,6 @@ public class GetAzToken : PSLoggerCmdletBase [Parameter(ParameterSetName = "ClientCertificate", Position = 1)] [Parameter(ParameterSetName = "ClientCertificatePath", Position = 1)] [Parameter(ParameterSetName = "AzurePipelines", Position = 1)] - [Parameter(ParameterSetName = "OnBehalfOf", Position = 1)] - [Parameter(ParameterSetName = "OnBehalfOfCertificate", Position = 1)] - [Parameter(ParameterSetName = "OnBehalfOfCertificatePath", Position = 1)] - [Parameter(ParameterSetName = "AuthorizationCode", Position = 1)] [ValidateNotNullOrEmpty] public string[] Scope { get; set; } = new[] { ".default" }; @@ -57,10 +49,6 @@ public class GetAzToken : PSLoggerCmdletBase [Parameter(ParameterSetName = "ClientCertificate", Mandatory = true)] [Parameter(ParameterSetName = "ClientCertificatePath", Mandatory = true)] [Parameter(ParameterSetName = "AzurePipelines", Mandatory = true)] - [Parameter(ParameterSetName = "OnBehalfOf", Mandatory = true)] - [Parameter(ParameterSetName = "OnBehalfOfCertificate", Mandatory = true)] - [Parameter(ParameterSetName = "OnBehalfOfCertificatePath", Mandatory = true)] - [Parameter(ParameterSetName = "AuthorizationCode", Mandatory = true)] [ValidateNotNullOrEmpty] [Alias("TenantId")] public string Tenant { get; set; } @@ -76,10 +64,6 @@ public class GetAzToken : PSLoggerCmdletBase [Parameter(ParameterSetName = "ClientCertificate")] [Parameter(ParameterSetName = "ClientCertificatePath")] [Parameter(ParameterSetName = "AzurePipelines")] - [Parameter(ParameterSetName = "OnBehalfOf")] - [Parameter(ParameterSetName = "OnBehalfOfCertificate")] - [Parameter(ParameterSetName = "OnBehalfOfCertificatePath")] - [Parameter(ParameterSetName = "AuthorizationCode")] [ValidateNotNullOrEmpty] public string Claim { get; set; } @@ -94,10 +78,6 @@ public class GetAzToken : PSLoggerCmdletBase [Parameter(ParameterSetName = "ClientCertificate", Mandatory = true)] [Parameter(ParameterSetName = "ClientCertificatePath", Mandatory = true)] [Parameter(ParameterSetName = "AzurePipelines", Mandatory = true)] - [Parameter(ParameterSetName = "OnBehalfOf", Mandatory = true)] - [Parameter(ParameterSetName = "OnBehalfOfCertificate", Mandatory = true)] - [Parameter(ParameterSetName = "OnBehalfOfCertificatePath", Mandatory = true)] - [Parameter(ParameterSetName = "AuthorizationCode", Mandatory = true)] [ValidateNotNullOrEmpty] public string ClientId { get; set; } @@ -161,42 +141,19 @@ public class GetAzToken : PSLoggerCmdletBase [ValidateNotNullOrEmpty] public string SystemAccessToken { get; set; } - [Parameter(ParameterSetName = "OnBehalfOf", Mandatory = true)] - [Parameter(ParameterSetName = "OnBehalfOfCertificate", Mandatory = true)] - [Parameter(ParameterSetName = "OnBehalfOfCertificatePath", Mandatory = true)] - public SwitchParameter OnBehalfOf { get; set; } - - [Parameter(ParameterSetName = "OnBehalfOf", Mandatory = true)] - [Parameter(ParameterSetName = "OnBehalfOfCertificate", Mandatory = true)] - [Parameter(ParameterSetName = "OnBehalfOfCertificatePath", Mandatory = true)] - [ValidateNotNullOrEmpty] - public string UserAssertion { get; set; } - [Parameter(ParameterSetName = "ClientSecret", Mandatory = true)] - [Parameter(ParameterSetName = "OnBehalfOf", Mandatory = true)] - [Parameter(ParameterSetName = "AuthorizationCode", Mandatory = true)] [ValidateNotNullOrEmpty] public string ClientSecret { get; set; } [Parameter(ParameterSetName = "ClientCertificate", Mandatory = true)] - [Parameter(ParameterSetName = "OnBehalfOfCertificate", Mandatory = true)] [ValidateNotNullOrEmpty] public X509Certificate2 ClientCertificate { get; set; } [Parameter(ParameterSetName = "ClientCertificatePath", Mandatory = true)] - [Parameter(ParameterSetName = "OnBehalfOfCertificatePath", Mandatory = true)] [ValidateNotNullOrEmpty] [ValidateCertificatePath] public string ClientCertificatePath { get; set; } - [Parameter(ParameterSetName = "AuthorizationCode", Mandatory = true)] - [ValidateNotNullOrEmpty] - public string AuthorizationCode { get; set; } - - [Parameter(ParameterSetName = "AuthorizationCode")] - [ValidateNotNullOrEmpty] - public Uri RedirectUri { get; set; } - [Parameter(ParameterSetName = "NonInteractive")] [Parameter(ParameterSetName = "Interactive")] [Parameter(ParameterSetName = "DeviceCode")] @@ -206,10 +163,6 @@ public class GetAzToken : PSLoggerCmdletBase [Parameter(ParameterSetName = "ClientCertificate")] [Parameter(ParameterSetName = "ClientCertificatePath")] [Parameter(ParameterSetName = "AzurePipelines")] - [Parameter(ParameterSetName = "OnBehalfOf")] - [Parameter(ParameterSetName = "OnBehalfOfCertificate")] - [Parameter(ParameterSetName = "OnBehalfOfCertificatePath")] - [Parameter(ParameterSetName = "AuthorizationCode")] public SwitchParameter Force { get; set; } // If user specifies Force, disregard earlier authentication @@ -415,48 +368,6 @@ should be return; } } - else if (OnBehalfOf.IsPresent) - { - if (ParameterSetName == "OnBehalfOf") - { - WriteVerbose($"Getting token using on-behalf-of flow with client secret for client '{ClientId}' (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.onbehalfofcredential)."); - try - { - WriteObject(TokenManager.GetTokenOnBehalfOf(Resource, Scope, Claim, ClientId, Tenant, ClientSecret, UserAssertion, stopProcessing.Token)); - } - catch (Exception ex) - { - WriteError(new ErrorRecord(ex, "OnBehalfOfTokenError", ErrorCategory.AuthenticationError, null)); - return; - } - } - else if (ParameterSetName == "OnBehalfOfCertificate") - { - WriteVerbose($"Getting token using on-behalf-of flow with client certificate for client '{ClientId}' (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.onbehalfofcredential)."); - try - { - WriteObject(TokenManager.GetTokenOnBehalfOfCertificate(Resource, Scope, Claim, ClientId, Tenant, ClientCertificate, UserAssertion, stopProcessing.Token)); - } - catch (Exception ex) - { - WriteError(new ErrorRecord(ex, "OnBehalfOfCertificateTokenError", ErrorCategory.AuthenticationError, null)); - return; - } - } - else if (ParameterSetName == "OnBehalfOfCertificatePath") - { - WriteVerbose($"Getting token using on-behalf-of flow with client certificate for client '{ClientId}' (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.onbehalfofcredential)."); - try - { - WriteObject(TokenManager.GetTokenOnBehalfOfCertificatePath(Resource, Scope, Claim, ClientId, Tenant, ClientCertificatePath, UserAssertion, stopProcessing.Token)); - } - catch (Exception ex) - { - WriteError(new ErrorRecord(ex, "OnBehalfOfCertificatePathTokenError", ErrorCategory.AuthenticationError, null)); - return; - } - } - } else if (ParameterSetName == "ClientSecret") { WriteVerbose($"Getting token using client secret for client '{ClientId}' (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.clientsecretcredential)."); @@ -496,20 +407,6 @@ should be return; } } - else if (ParameterSetName == "AuthorizationCode") - { - WriteVerbose($"Getting token using authorization code flow for client '{ClientId}' (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.authorizationcodecredential)."); - try - { - Uri? redirectUri = MyInvocation.BoundParameters.ContainsKey("RedirectUri") ? RedirectUri : null; - WriteObject(TokenManager.GetTokenAuthorizationCode(Resource, Scope, Claim, ClientId, Tenant, ClientSecret, AuthorizationCode, redirectUri, stopProcessing.Token)); - } - catch (Exception ex) - { - WriteError(new ErrorRecord(ex, "AuthorizationCodeTokenError", ErrorCategory.AuthenticationError, null)); - return; - } - } else { WriteError(new ErrorRecord(new ArgumentException("Invalid parameter combination!"), "InvalidParameters", ErrorCategory.WriteError, null)); diff --git a/tests/Get-AzToken.Tests.ps1 b/tests/Get-AzToken.Tests.ps1 index 9748820..2a79636 100644 --- a/tests/Get-AzToken.Tests.ps1 +++ b/tests/Get-AzToken.Tests.ps1 @@ -15,10 +15,6 @@ BeforeDiscovery { @{ Name = 'ClientCertificate'; Mandatory = $false } @{ Name = 'ClientCertificatePath'; Mandatory = $false } @{ Name = 'AzurePipelines'; Mandatory = $false } - @{ Name = 'OnBehalfOf'; Mandatory = $false } - @{ Name = 'OnBehalfOfCertificate'; Mandatory = $false } - @{ Name = 'OnBehalfOfCertificatePath'; Mandatory = $false } - @{ Name = 'AuthorizationCode'; Mandatory = $false } ) } @{ @@ -36,10 +32,6 @@ BeforeDiscovery { @{ Name = 'ClientCertificate'; Mandatory = $false } @{ Name = 'ClientCertificatePath'; Mandatory = $false } @{ Name = 'AzurePipelines'; Mandatory = $false } - @{ Name = 'OnBehalfOf'; Mandatory = $false } - @{ Name = 'OnBehalfOfCertificate'; Mandatory = $false } - @{ Name = 'OnBehalfOfCertificatePath'; Mandatory = $false } - @{ Name = 'AuthorizationCode'; Mandatory = $false } ) } @{ @@ -57,10 +49,6 @@ BeforeDiscovery { @{ Name = 'ClientCertificate'; Mandatory = $true } @{ Name = 'ClientCertificatePath'; Mandatory = $true } @{ Name = 'AzurePipelines'; Mandatory = $true } - @{ Name = 'OnBehalfOf'; Mandatory = $true } - @{ Name = 'OnBehalfOfCertificate'; Mandatory = $true } - @{ Name = 'OnBehalfOfCertificatePath'; Mandatory = $true } - @{ Name = 'AuthorizationCode'; Mandatory = $true } ) } @{ @@ -78,10 +66,6 @@ BeforeDiscovery { @{ Name = 'ClientCertificate'; Mandatory = $false } @{ Name = 'ClientCertificatePath'; Mandatory = $false } @{ Name = 'AzurePipelines'; Mandatory = $false } - @{ Name = 'OnBehalfOf'; Mandatory = $false } - @{ Name = 'OnBehalfOfCertificate'; Mandatory = $false } - @{ Name = 'OnBehalfOfCertificatePath'; Mandatory = $false } - @{ Name = 'AuthorizationCode'; Mandatory = $false } ) } @{ @@ -99,10 +83,6 @@ BeforeDiscovery { @{ Name = 'ClientCertificate'; Mandatory = $true } @{ Name = 'ClientCertificatePath'; Mandatory = $true } @{ Name = 'AzurePipelines'; Mandatory = $true } - @{ Name = 'OnBehalfOf'; Mandatory = $true } - @{ Name = 'OnBehalfOfCertificate'; Mandatory = $true } - @{ Name = 'OnBehalfOfCertificatePath'; Mandatory = $true } - @{ Name = 'AuthorizationCode'; Mandatory = $true } ) } @{ @@ -194,8 +174,6 @@ BeforeDiscovery { Type = 'string' ParameterSets = @( @{ Name = 'ClientSecret'; Mandatory = $true } - @{ Name = 'OnBehalfOf'; Mandatory = $true } - @{ Name = 'AuthorizationCode'; Mandatory = $true } ) } @{ @@ -203,7 +181,6 @@ BeforeDiscovery { Type = 'System.Security.Cryptography.X509Certificates.X509Certificate2' ParameterSets = @( @{ Name = 'ClientCertificate'; Mandatory = $true } - @{ Name = 'OnBehalfOfCertificate'; Mandatory = $true } ) } @{ @@ -211,7 +188,6 @@ BeforeDiscovery { Type = 'string' ParameterSets = @( @{ Name = 'ClientCertificatePath'; Mandatory = $true } - @{ Name = 'OnBehalfOfCertificatePath'; Mandatory = $true } ) } @{ @@ -235,38 +211,6 @@ BeforeDiscovery { @{ Name = 'AzurePipelines'; Mandatory = $true } ) } - @{ - Name = 'OnBehalfOf' - Type = 'System.Management.Automation.SwitchParameter' - ParameterSets = @( - @{ Name = 'OnBehalfOf'; Mandatory = $true } - @{ Name = 'OnBehalfOfCertificate'; Mandatory = $true } - @{ Name = 'OnBehalfOfCertificatePath'; Mandatory = $true } - ) - } - @{ - Name = 'UserAssertion' - Type = 'string' - ParameterSets = @( - @{ Name = 'OnBehalfOf'; Mandatory = $true } - @{ Name = 'OnBehalfOfCertificate'; Mandatory = $true } - @{ Name = 'OnBehalfOfCertificatePath'; Mandatory = $true } - ) - } - @{ - Name = 'AuthorizationCode' - Type = 'string' - ParameterSets = @( - @{ Name = 'AuthorizationCode'; Mandatory = $true } - ) - } - @{ - Name = 'RedirectUri' - Type = 'System.Uri' - ParameterSets = @( - @{ Name = 'AuthorizationCode'; Mandatory = $false } - ) - } @{ Name = 'Force' Type = 'System.Management.Automation.SwitchParameter' @@ -280,10 +224,6 @@ BeforeDiscovery { @{ Name = 'ClientCertificate'; Mandatory = $false } @{ Name = 'ClientCertificatePath'; Mandatory = $false } @{ Name = 'AzurePipelines'; Mandatory = $false } - @{ Name = 'OnBehalfOf'; Mandatory = $false } - @{ Name = 'OnBehalfOfCertificate'; Mandatory = $false } - @{ Name = 'OnBehalfOfCertificatePath'; Mandatory = $false } - @{ Name = 'AuthorizationCode'; Mandatory = $false } ) } ) From 6f20c1af00b372832a4526c76963ce50741d9b9d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Mar 2026 05:17:52 +0000 Subject: [PATCH 5/7] Fix changelog: remove stale OnBehalfOf and AuthorizationCode entries Agent-Logs-Url: https://github.com/PalmEmanuel/AzAuth/sessions/9a2ffbfa-d285-4965-8eaa-dbafee502a29 Co-authored-by: PalmEmanuel <3384251+PalmEmanuel@users.noreply.github.com> --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b61f64e..ebe490b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,6 @@ The format is based on and uses the types of changes according to [Keep a Change - Added `AzureDeveloperCli` to the `CredentialPrecedence` parameter for non-interactive authentication, allowing use of Azure Developer CLI (`azd`) credentials - Added `WorkloadIdentity` to the `CredentialPrecedence` parameter for non-interactive authentication, using `WorkloadIdentityCredential` which reads from environment variables set by Kubernetes workload identity - Added `-AzurePipelines` parameter set for Azure Pipelines service connection OIDC authentication (`AzurePipelinesCredential`) -- Added `-OnBehalfOf` parameter set (with `-ClientSecret`, `-ClientCertificate`, and `-ClientCertificatePath` variants) for the OAuth 2.0 on-behalf-of flow (`OnBehalfOfCredential`) -- Added `-AuthorizationCode` parameter set for the OAuth 2.0 authorization code flow (`AuthorizationCodeCredential`), with optional `-RedirectUri` parameter ### Removed From cb689aa40fbac35bd3939590ff7c0f2dee7991a9 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Mar 2026 08:09:52 -0700 Subject: [PATCH 6/7] Remove WorkloadIdentity from non-interactive credential chain (#207) * Initial plan * Remove WorkloadIdentity credential (parameter set, non-interactive chain, tests, docs) Agent-Logs-Url: https://github.com/PalmEmanuel/AzAuth/sessions/e86bc179-5996-4037-85a9-3bbccd08416e Co-authored-by: PalmEmanuel <3384251+PalmEmanuel@users.noreply.github.com> * Fix: only remove WorkloadIdentity from non-interactive chain, preserve explicit parameter set Agent-Logs-Url: https://github.com/PalmEmanuel/AzAuth/sessions/9c68a216-fa3e-40c3-ac12-94538d419043 Co-authored-by: PalmEmanuel <3384251+PalmEmanuel@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: PalmEmanuel <3384251+PalmEmanuel@users.noreply.github.com> --- docs/help/Get-AzToken.md | 3 +-- source/AzAuth.Core/TokenManager.cs | 1 - .../TokenManagerAuthMethods/TokenManager.NonInteractive.cs | 1 - source/AzAuth.PS/Cmdlets/GetAzToken.cs | 4 ++-- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/help/Get-AzToken.md b/docs/help/Get-AzToken.md index e6f99a5..6e24d78 100644 --- a/docs/help/Get-AzToken.md +++ b/docs/help/Get-AzToken.md @@ -92,7 +92,6 @@ The token can be retrieved from an existing named cache, interactively from a br - Saved interactive credential if the command was used interactively in the same session (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.interactivebrowsercredential) - Environment variables (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.environmentcredential) -- Workload identity (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.workloadidentitycredential) - Azure PowerShell (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.azurepowershellcredential) - Azure CLI (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.azureclicredential) - Azure Developer CLI (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.azuredeveloperclicredential) @@ -299,7 +298,7 @@ The order of precedence for the credentials to be used for getting a token non-i Type: String[] Parameter Sets: NonInteractive Aliases: -Accepted values: ManagedIdentity, WorkloadIdentity, Environment, AzurePowerShell, AzureCLI, AzureDeveloperCli, VisualStudio +Accepted values: ManagedIdentity, Environment, AzurePowerShell, AzureCLI, AzureDeveloperCli, VisualStudio Required: False Position: Named diff --git a/source/AzAuth.Core/TokenManager.cs b/source/AzAuth.Core/TokenManager.cs index afdb40f..8f1793d 100644 --- a/source/AzAuth.Core/TokenManager.cs +++ b/source/AzAuth.Core/TokenManager.cs @@ -18,7 +18,6 @@ internal static string GetCredentialDocumentationUrl(string credentialType) return credentialType switch { "ManagedIdentity" => "https://learn.microsoft.com/en-us/dotnet/api/azure.identity.managedidentitycredential", - "WorkloadIdentity" => "https://learn.microsoft.com/en-us/dotnet/api/azure.identity.workloadidentitycredential", "Environment" => "https://learn.microsoft.com/en-us/dotnet/api/azure.identity.environmentcredential", "AzurePowerShell" => "https://learn.microsoft.com/en-us/dotnet/api/azure.identity.azurepowershellcredential", "AzureCLI" => "https://learn.microsoft.com/en-us/dotnet/api/azure.identity.azureclicredential", diff --git a/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.NonInteractive.cs b/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.NonInteractive.cs index 85c8880..7a91439 100644 --- a/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.NonInteractive.cs +++ b/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.NonInteractive.cs @@ -55,7 +55,6 @@ internal static async Task GetTokenNonInteractiveAsync( MaxDelay = TimeSpan.Zero } }), - "WorkloadIdentity" => new WorkloadIdentityCredential(genericTimeoutOptions as WorkloadIdentityCredentialOptions), "Environment" => new EnvironmentCredential(genericTimeoutOptions), "AzurePowerShell" => new AzurePowerShellCredential(genericTimeoutOptions as AzurePowerShellCredentialOptions), "AzureCLI" => new AzureCliCredential(genericTimeoutOptions as AzureCliCredentialOptions), diff --git a/source/AzAuth.PS/Cmdlets/GetAzToken.cs b/source/AzAuth.PS/Cmdlets/GetAzToken.cs index c6feb6f..92d39ae 100644 --- a/source/AzAuth.PS/Cmdlets/GetAzToken.cs +++ b/source/AzAuth.PS/Cmdlets/GetAzToken.cs @@ -106,10 +106,10 @@ public class GetAzToken : PSLoggerCmdletBase public int TimeoutSeconds { get; set; } = 120; [Parameter(ParameterSetName = "NonInteractive")] - [ValidateSet("ManagedIdentity", "WorkloadIdentity", "Environment", "AzurePowerShell", "AzureCLI", "AzureDeveloperCli", "VisualStudio")] + [ValidateSet("ManagedIdentity", "Environment", "AzurePowerShell", "AzureCLI", "AzureDeveloperCli", "VisualStudio")] [ValidateNotNullOrEmpty()] // TODO: Change back ManagedIdentity to first position in the chain once issue #112 is solved, likely in Azure.Identity 1.14.2 or later - public string[] CredentialPrecedence { get; set; } = ["Environment", "WorkloadIdentity", "AzurePowerShell", "AzureCLI", "AzureDeveloperCli", "VisualStudio", "ManagedIdentity"]; + public string[] CredentialPrecedence { get; set; } = ["Environment", "AzurePowerShell", "AzureCLI", "AzureDeveloperCli", "VisualStudio", "ManagedIdentity"]; [Parameter(ParameterSetName = "Interactive", Mandatory = true)] public SwitchParameter Interactive { get; set; } From fe921344569dbb912b02cad45cbc1a905581b866 Mon Sep 17 00:00:00 2001 From: Emanuel Palm Date: Sat, 28 Mar 2026 08:15:07 -0700 Subject: [PATCH 7/7] Remove workload identity info --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebe490b..247b9a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,6 @@ The format is based on and uses the types of changes according to [Keep a Change ### Added - Added `AzureDeveloperCli` to the `CredentialPrecedence` parameter for non-interactive authentication, allowing use of Azure Developer CLI (`azd`) credentials -- Added `WorkloadIdentity` to the `CredentialPrecedence` parameter for non-interactive authentication, using `WorkloadIdentityCredential` which reads from environment variables set by Kubernetes workload identity - Added `-AzurePipelines` parameter set for Azure Pipelines service connection OIDC authentication (`AzurePipelinesCredential`) ### Removed