From 81dcb209f91f6f61d32d5f607bb1e32f25b37b00 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Mar 2026 14:42:37 +0000 Subject: [PATCH 1/3] Initial plan From c31e4f081d26bdf085c5021a6741df1eade30fb3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Mar 2026 14:48:39 +0000 Subject: [PATCH 2/3] 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> --- docs/help/Get-AzToken.md | 57 ++----------------- source/AzAuth.Core/TokenManager.cs | 1 - .../TokenManager.NonInteractive.cs | 1 - .../TokenManager.WorkloadIdentity.cs | 39 ------------- source/AzAuth.PS/Cmdlets/GetAzToken.cs | 30 +--------- source/AzAuth.psd1 | 2 - tests/Get-AzToken.Tests.ps1 | 20 ------- 7 files changed, 6 insertions(+), 144 deletions(-) delete mode 100644 source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.WorkloadIdentity.cs diff --git a/docs/help/Get-AzToken.md b/docs/help/Get-AzToken.md index e6f99a5..19c6168 100644 --- a/docs/help/Get-AzToken.md +++ b/docs/help/Get-AzToken.md @@ -54,12 +54,6 @@ Get-AzToken [[-Resource] ] [[-Scope] ] [-Tenant ] [-Cl [] ``` -### WorkloadIdentity -``` -Get-AzToken [[-Resource] ] [[-Scope] ] -Tenant [-Claim ] -ClientId - [-WorkloadIdentity] -ExternalToken [-Force] [] -``` - ### ClientSecret ``` Get-AzToken [[-Resource] ] [[-Scope] ] -Tenant [-Claim ] -ClientId @@ -92,7 +86,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) @@ -173,14 +166,6 @@ PS C:\> Get-AzToken -ClientCertificatePath ".\certAndPrivateKey.pem" -ClientId $ Gets an Azure access token for a client using the client certificate flow by specifying a path to a file containing both the certificate and the private key. -### Example 10 - -```powershell -PS C:\> Get-AzToken -WorkloadIdentity -ExternalToken $OidcToken -ClientId $ClientId -TenantId $TenantId -``` - -Gets an Azure access token for a client using the workload identity federation pattern by specifying a valid id token. For more details, see blog post in related links of this command. - ## PARAMETERS ### -Broker @@ -265,7 +250,7 @@ Accept wildcard characters: False ```yaml Type: String -Parameter Sets: WorkloadIdentity, ClientSecret, ClientCertificate, ClientCertificatePath, AzurePipelines +Parameter Sets: ClientSecret, ClientCertificate, ClientCertificatePath, AzurePipelines Aliases: Required: True @@ -299,7 +284,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 @@ -324,22 +309,6 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -ExternalToken - -The external token used for the federated credential of the workload identity, used together with parameter -WorkloadIdentity for the client assertion flow. For more details, see blog post in related links of this command. - -```yaml -Type: String -Parameter Sets: WorkloadIdentity -Aliases: - -Required: True -Position: Named -Default value: None -Accept pipeline input: False -Accept wildcard characters: False -``` - ### -Force Disregard any previous authentication made in this session. @@ -348,7 +317,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 +Parameter Sets: NonInteractive, Interactive, DeviceCode, ManagedIdentity, ClientSecret, ClientCertificate, ClientCertificatePath, AzurePipelines Aliases: Required: False @@ -445,7 +414,7 @@ Accept wildcard characters: False ```yaml Type: String -Parameter Sets: WorkloadIdentity, ClientSecret, ClientCertificate, ClientCertificatePath, AzurePipelines +Parameter Sets: ClientSecret, ClientCertificate, ClientCertificatePath, AzurePipelines Aliases: TenantId Required: True @@ -531,22 +500,6 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -WorkloadIdentity - -Get a token using a federated credential, or "workload identity federation". For an example of how to use this in a pipeline, see related links of this command. - -```yaml -Type: SwitchParameter -Parameter Sets: WorkloadIdentity -Aliases: - -Required: True -Position: Named -Default value: None -Accept pipeline input: False -Accept wildcard characters: False -``` - ### -AzurePipelines Get a token using an Azure Pipelines service connection with OIDC (OpenID Connect). @@ -611,5 +564,3 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable ## RELATED LINKS [Blog Post "OAuth 2.0 Fundamentals for Azure APIs"](https://pipe.how/connect-azure/) - -[Blog Post "Azure Workload Identity Federation"](https://pipe.how/get-oidctoken/) 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.Core/TokenManagerAuthMethods/TokenManager.WorkloadIdentity.cs b/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.WorkloadIdentity.cs deleted file mode 100644 index 20fb814..0000000 --- a/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.WorkloadIdentity.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Azure.Core; -using Azure.Identity; - -namespace PipeHow.AzAuth; - -internal static partial class TokenManager -{ - /// - /// Gets token as a workload identity. - /// - internal static AzToken GetTokenWorkloadIdentity(string resource, string[] scopes, string? claims, string? clientId, string tenantId, string externalToken, CancellationToken cancellationToken) => - taskFactory.Run(() => GetTokenWorkloadIdentityAsync(resource, scopes, claims, clientId, tenantId, externalToken, cancellationToken)); - - /// - /// Gets token as a workload identity. - /// - internal static async Task GetTokenWorkloadIdentityAsync( - string resource, - string[] scopes, - string? claims, - string? clientId, - string tenantId, - string externalToken, - 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 ClientAssertionCredential || previousClientId != clientId) - { - credential = new ClientAssertionCredential(tenantId, clientId, () => externalToken); - } - - previousClientId = clientId; - - return await GetTokenAsync(tokenRequestContext, cancellationToken); - } -} \ No newline at end of file diff --git a/source/AzAuth.PS/Cmdlets/GetAzToken.cs b/source/AzAuth.PS/Cmdlets/GetAzToken.cs index c6feb6f..c52181b 100644 --- a/source/AzAuth.PS/Cmdlets/GetAzToken.cs +++ b/source/AzAuth.PS/Cmdlets/GetAzToken.cs @@ -15,7 +15,6 @@ public class GetAzToken : PSLoggerCmdletBase [Parameter(ParameterSetName = "Broker", Position = 0)] [Parameter(ParameterSetName = "DeviceCode", Position = 0)] [Parameter(ParameterSetName = "ManagedIdentity", Position = 0)] - [Parameter(ParameterSetName = "WorkloadIdentity", Position = 0)] [Parameter(ParameterSetName = "ClientSecret", Position = 0)] [Parameter(ParameterSetName = "ClientCertificate", Position = 0)] [Parameter(ParameterSetName = "ClientCertificatePath", Position = 0)] @@ -30,7 +29,6 @@ public class GetAzToken : PSLoggerCmdletBase [Parameter(ParameterSetName = "Broker", Position = 1)] [Parameter(ParameterSetName = "DeviceCode", Position = 1)] [Parameter(ParameterSetName = "ManagedIdentity", Position = 1)] - [Parameter(ParameterSetName = "WorkloadIdentity", Position = 1)] [Parameter(ParameterSetName = "ClientSecret", Position = 1)] [Parameter(ParameterSetName = "ClientCertificate", Position = 1)] [Parameter(ParameterSetName = "ClientCertificatePath", Position = 1)] @@ -44,7 +42,6 @@ public class GetAzToken : PSLoggerCmdletBase [Parameter(ParameterSetName = "Broker")] [Parameter(ParameterSetName = "DeviceCode")] [Parameter(ParameterSetName = "ManagedIdentity")] - [Parameter(ParameterSetName = "WorkloadIdentity", Mandatory = true)] [Parameter(ParameterSetName = "ClientSecret", Mandatory = true)] [Parameter(ParameterSetName = "ClientCertificate", Mandatory = true)] [Parameter(ParameterSetName = "ClientCertificatePath", Mandatory = true)] @@ -59,7 +56,6 @@ public class GetAzToken : PSLoggerCmdletBase [Parameter(ParameterSetName = "Broker")] [Parameter(ParameterSetName = "DeviceCode")] [Parameter(ParameterSetName = "ManagedIdentity")] - [Parameter(ParameterSetName = "WorkloadIdentity")] [Parameter(ParameterSetName = "ClientSecret")] [Parameter(ParameterSetName = "ClientCertificate")] [Parameter(ParameterSetName = "ClientCertificatePath")] @@ -73,7 +69,6 @@ public class GetAzToken : PSLoggerCmdletBase [Parameter(ParameterSetName = "DeviceCode")] [Parameter(ParameterSetName = "ManagedIdentity")] [Parameter(ParameterSetName = "Cache")] - [Parameter(ParameterSetName = "WorkloadIdentity", Mandatory = true)] [Parameter(ParameterSetName = "ClientSecret", Mandatory = true)] [Parameter(ParameterSetName = "ClientCertificate", Mandatory = true)] [Parameter(ParameterSetName = "ClientCertificatePath", Mandatory = true)] @@ -106,10 +101,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; } @@ -123,13 +118,6 @@ public class GetAzToken : PSLoggerCmdletBase [Parameter(ParameterSetName = "ManagedIdentity", Mandatory = true)] public SwitchParameter ManagedIdentity { get; set; } - [Parameter(ParameterSetName = "WorkloadIdentity", Mandatory = true)] - public SwitchParameter WorkloadIdentity { get; set; } - - [Parameter(ParameterSetName = "WorkloadIdentity", Mandatory = true)] - [ValidateNotNullOrEmpty] - public string ExternalToken { get; set; } - [Parameter(ParameterSetName = "AzurePipelines", Mandatory = true)] public SwitchParameter AzurePipelines { get; set; } @@ -158,7 +146,6 @@ public class GetAzToken : PSLoggerCmdletBase [Parameter(ParameterSetName = "Interactive")] [Parameter(ParameterSetName = "DeviceCode")] [Parameter(ParameterSetName = "ManagedIdentity")] - [Parameter(ParameterSetName = "WorkloadIdentity")] [Parameter(ParameterSetName = "ClientSecret")] [Parameter(ParameterSetName = "ClientCertificate")] [Parameter(ParameterSetName = "ClientCertificatePath")] @@ -342,19 +329,6 @@ should be return; } } - else if (WorkloadIdentity.IsPresent) - { - WriteVerbose($"Getting token using workload identity federation (using client assertion) for client '{ClientId}' (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.clientassertioncredential)."); - try - { - WriteObject(TokenManager.GetTokenWorkloadIdentity(Resource, Scope, Claim, ClientId, Tenant, ExternalToken, stopProcessing.Token)); - } - catch (Exception ex) - { - WriteError(new ErrorRecord(ex, "WorkloadIdentityTokenError", ErrorCategory.AuthenticationError, null)); - 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)."); diff --git a/source/AzAuth.psd1 b/source/AzAuth.psd1 index ff43211..fa2eb61 100644 --- a/source/AzAuth.psd1 +++ b/source/AzAuth.psd1 @@ -104,8 +104,6 @@ PrivateData = @{ 'serviceprincipal', 'spn', 'managedidentity', - 'workload', - 'workloadidentity', 'federation', 'credential' 'OIDC' diff --git a/tests/Get-AzToken.Tests.ps1 b/tests/Get-AzToken.Tests.ps1 index 2a79636..b9fe0b2 100644 --- a/tests/Get-AzToken.Tests.ps1 +++ b/tests/Get-AzToken.Tests.ps1 @@ -10,7 +10,6 @@ BeforeDiscovery { @{ Name = 'Broker'; Mandatory = $false } @{ Name = 'DeviceCode'; Mandatory = $false } @{ Name = 'ManagedIdentity'; Mandatory = $false } - @{ Name = 'WorkloadIdentity'; Mandatory = $false } @{ Name = 'ClientSecret'; Mandatory = $false } @{ Name = 'ClientCertificate'; Mandatory = $false } @{ Name = 'ClientCertificatePath'; Mandatory = $false } @@ -27,7 +26,6 @@ BeforeDiscovery { @{ Name = 'Broker'; Mandatory = $false } @{ Name = 'DeviceCode'; Mandatory = $false } @{ Name = 'ManagedIdentity'; Mandatory = $false } - @{ Name = 'WorkloadIdentity'; Mandatory = $false } @{ Name = 'ClientSecret'; Mandatory = $false } @{ Name = 'ClientCertificate'; Mandatory = $false } @{ Name = 'ClientCertificatePath'; Mandatory = $false } @@ -44,7 +42,6 @@ BeforeDiscovery { @{ Name = 'Broker'; Mandatory = $false } @{ Name = 'DeviceCode'; Mandatory = $false } @{ Name = 'ManagedIdentity'; Mandatory = $false } - @{ Name = 'WorkloadIdentity'; Mandatory = $true } @{ Name = 'ClientSecret'; Mandatory = $true } @{ Name = 'ClientCertificate'; Mandatory = $true } @{ Name = 'ClientCertificatePath'; Mandatory = $true } @@ -61,7 +58,6 @@ BeforeDiscovery { @{ Name = 'Broker'; Mandatory = $false } @{ Name = 'DeviceCode'; Mandatory = $false } @{ Name = 'ManagedIdentity'; Mandatory = $false } - @{ Name = 'WorkloadIdentity'; Mandatory = $false } @{ Name = 'ClientSecret'; Mandatory = $false } @{ Name = 'ClientCertificate'; Mandatory = $false } @{ Name = 'ClientCertificatePath'; Mandatory = $false } @@ -78,7 +74,6 @@ BeforeDiscovery { @{ Name = 'DeviceCode'; Mandatory = $false } @{ Name = 'ManagedIdentity'; Mandatory = $false } @{ Name = 'Cache'; Mandatory = $false } - @{ Name = 'WorkloadIdentity'; Mandatory = $true } @{ Name = 'ClientSecret'; Mandatory = $true } @{ Name = 'ClientCertificate'; Mandatory = $true } @{ Name = 'ClientCertificatePath'; Mandatory = $true } @@ -155,20 +150,6 @@ BeforeDiscovery { @{ Name = 'ManagedIdentity'; Mandatory = $true } ) } - @{ - Name = 'WorkloadIdentity' - Type = 'System.Management.Automation.SwitchParameter' - ParameterSets = @( - @{ Name = 'WorkloadIdentity'; Mandatory = $true } - ) - } - @{ - Name = 'ExternalToken' - Type = 'string' - ParameterSets = @( - @{ Name = 'WorkloadIdentity'; Mandatory = $true } - ) - } @{ Name = 'ClientSecret' Type = 'string' @@ -219,7 +200,6 @@ BeforeDiscovery { @{ Name = 'Interactive'; Mandatory = $false } @{ Name = 'DeviceCode'; Mandatory = $false } @{ Name = 'ManagedIdentity'; Mandatory = $false } - @{ Name = 'WorkloadIdentity'; Mandatory = $false } @{ Name = 'ClientSecret'; Mandatory = $false } @{ Name = 'ClientCertificate'; Mandatory = $false } @{ Name = 'ClientCertificatePath'; Mandatory = $false } From 2a4be742d65e8c2e422973b108f655410769a2f7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Mar 2026 14:54:52 +0000 Subject: [PATCH 3/3] 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> --- docs/help/Get-AzToken.md | 54 +++++++++++++++++-- .../TokenManager.WorkloadIdentity.cs | 39 ++++++++++++++ source/AzAuth.PS/Cmdlets/GetAzToken.cs | 26 +++++++++ source/AzAuth.psd1 | 2 + tests/Get-AzToken.Tests.ps1 | 20 +++++++ 5 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.WorkloadIdentity.cs diff --git a/docs/help/Get-AzToken.md b/docs/help/Get-AzToken.md index 19c6168..6e24d78 100644 --- a/docs/help/Get-AzToken.md +++ b/docs/help/Get-AzToken.md @@ -54,6 +54,12 @@ Get-AzToken [[-Resource] ] [[-Scope] ] [-Tenant ] [-Cl [] ``` +### WorkloadIdentity +``` +Get-AzToken [[-Resource] ] [[-Scope] ] -Tenant [-Claim ] -ClientId + [-WorkloadIdentity] -ExternalToken [-Force] [] +``` + ### ClientSecret ``` Get-AzToken [[-Resource] ] [[-Scope] ] -Tenant [-Claim ] -ClientId @@ -166,6 +172,14 @@ PS C:\> Get-AzToken -ClientCertificatePath ".\certAndPrivateKey.pem" -ClientId $ Gets an Azure access token for a client using the client certificate flow by specifying a path to a file containing both the certificate and the private key. +### Example 10 + +```powershell +PS C:\> Get-AzToken -WorkloadIdentity -ExternalToken $OidcToken -ClientId $ClientId -TenantId $TenantId +``` + +Gets an Azure access token for a client using the workload identity federation pattern by specifying a valid id token. For more details, see blog post in related links of this command. + ## PARAMETERS ### -Broker @@ -250,7 +264,7 @@ Accept wildcard characters: False ```yaml Type: String -Parameter Sets: ClientSecret, ClientCertificate, ClientCertificatePath, AzurePipelines +Parameter Sets: WorkloadIdentity, ClientSecret, ClientCertificate, ClientCertificatePath, AzurePipelines Aliases: Required: True @@ -309,6 +323,22 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -ExternalToken + +The external token used for the federated credential of the workload identity, used together with parameter -WorkloadIdentity for the client assertion flow. For more details, see blog post in related links of this command. + +```yaml +Type: String +Parameter Sets: WorkloadIdentity +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### -Force Disregard any previous authentication made in this session. @@ -317,7 +347,7 @@ This may be required when combining interactive and non-interactive authenticati ```yaml Type: SwitchParameter -Parameter Sets: NonInteractive, Interactive, DeviceCode, ManagedIdentity, ClientSecret, ClientCertificate, ClientCertificatePath, AzurePipelines +Parameter Sets: NonInteractive, Interactive, DeviceCode, ManagedIdentity, WorkloadIdentity, ClientSecret, ClientCertificate, ClientCertificatePath, AzurePipelines Aliases: Required: False @@ -414,7 +444,7 @@ Accept wildcard characters: False ```yaml Type: String -Parameter Sets: ClientSecret, ClientCertificate, ClientCertificatePath, AzurePipelines +Parameter Sets: WorkloadIdentity, ClientSecret, ClientCertificate, ClientCertificatePath, AzurePipelines Aliases: TenantId Required: True @@ -500,6 +530,22 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -WorkloadIdentity + +Get a token using a federated credential, or "workload identity federation". For an example of how to use this in a pipeline, see related links of this command. + +```yaml +Type: SwitchParameter +Parameter Sets: WorkloadIdentity +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### -AzurePipelines Get a token using an Azure Pipelines service connection with OIDC (OpenID Connect). @@ -564,3 +610,5 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable ## RELATED LINKS [Blog Post "OAuth 2.0 Fundamentals for Azure APIs"](https://pipe.how/connect-azure/) + +[Blog Post "Azure Workload Identity Federation"](https://pipe.how/get-oidctoken/) diff --git a/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.WorkloadIdentity.cs b/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.WorkloadIdentity.cs new file mode 100644 index 0000000..20fb814 --- /dev/null +++ b/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.WorkloadIdentity.cs @@ -0,0 +1,39 @@ +using Azure.Core; +using Azure.Identity; + +namespace PipeHow.AzAuth; + +internal static partial class TokenManager +{ + /// + /// Gets token as a workload identity. + /// + internal static AzToken GetTokenWorkloadIdentity(string resource, string[] scopes, string? claims, string? clientId, string tenantId, string externalToken, CancellationToken cancellationToken) => + taskFactory.Run(() => GetTokenWorkloadIdentityAsync(resource, scopes, claims, clientId, tenantId, externalToken, cancellationToken)); + + /// + /// Gets token as a workload identity. + /// + internal static async Task GetTokenWorkloadIdentityAsync( + string resource, + string[] scopes, + string? claims, + string? clientId, + string tenantId, + string externalToken, + 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 ClientAssertionCredential || previousClientId != clientId) + { + credential = new ClientAssertionCredential(tenantId, clientId, () => externalToken); + } + + previousClientId = clientId; + + return await GetTokenAsync(tokenRequestContext, cancellationToken); + } +} \ No newline at end of file diff --git a/source/AzAuth.PS/Cmdlets/GetAzToken.cs b/source/AzAuth.PS/Cmdlets/GetAzToken.cs index c52181b..92d39ae 100644 --- a/source/AzAuth.PS/Cmdlets/GetAzToken.cs +++ b/source/AzAuth.PS/Cmdlets/GetAzToken.cs @@ -15,6 +15,7 @@ public class GetAzToken : PSLoggerCmdletBase [Parameter(ParameterSetName = "Broker", Position = 0)] [Parameter(ParameterSetName = "DeviceCode", Position = 0)] [Parameter(ParameterSetName = "ManagedIdentity", Position = 0)] + [Parameter(ParameterSetName = "WorkloadIdentity", Position = 0)] [Parameter(ParameterSetName = "ClientSecret", Position = 0)] [Parameter(ParameterSetName = "ClientCertificate", Position = 0)] [Parameter(ParameterSetName = "ClientCertificatePath", Position = 0)] @@ -29,6 +30,7 @@ public class GetAzToken : PSLoggerCmdletBase [Parameter(ParameterSetName = "Broker", Position = 1)] [Parameter(ParameterSetName = "DeviceCode", Position = 1)] [Parameter(ParameterSetName = "ManagedIdentity", Position = 1)] + [Parameter(ParameterSetName = "WorkloadIdentity", Position = 1)] [Parameter(ParameterSetName = "ClientSecret", Position = 1)] [Parameter(ParameterSetName = "ClientCertificate", Position = 1)] [Parameter(ParameterSetName = "ClientCertificatePath", Position = 1)] @@ -42,6 +44,7 @@ public class GetAzToken : PSLoggerCmdletBase [Parameter(ParameterSetName = "Broker")] [Parameter(ParameterSetName = "DeviceCode")] [Parameter(ParameterSetName = "ManagedIdentity")] + [Parameter(ParameterSetName = "WorkloadIdentity", Mandatory = true)] [Parameter(ParameterSetName = "ClientSecret", Mandatory = true)] [Parameter(ParameterSetName = "ClientCertificate", Mandatory = true)] [Parameter(ParameterSetName = "ClientCertificatePath", Mandatory = true)] @@ -56,6 +59,7 @@ public class GetAzToken : PSLoggerCmdletBase [Parameter(ParameterSetName = "Broker")] [Parameter(ParameterSetName = "DeviceCode")] [Parameter(ParameterSetName = "ManagedIdentity")] + [Parameter(ParameterSetName = "WorkloadIdentity")] [Parameter(ParameterSetName = "ClientSecret")] [Parameter(ParameterSetName = "ClientCertificate")] [Parameter(ParameterSetName = "ClientCertificatePath")] @@ -69,6 +73,7 @@ public class GetAzToken : PSLoggerCmdletBase [Parameter(ParameterSetName = "DeviceCode")] [Parameter(ParameterSetName = "ManagedIdentity")] [Parameter(ParameterSetName = "Cache")] + [Parameter(ParameterSetName = "WorkloadIdentity", Mandatory = true)] [Parameter(ParameterSetName = "ClientSecret", Mandatory = true)] [Parameter(ParameterSetName = "ClientCertificate", Mandatory = true)] [Parameter(ParameterSetName = "ClientCertificatePath", Mandatory = true)] @@ -118,6 +123,13 @@ public class GetAzToken : PSLoggerCmdletBase [Parameter(ParameterSetName = "ManagedIdentity", Mandatory = true)] public SwitchParameter ManagedIdentity { get; set; } + [Parameter(ParameterSetName = "WorkloadIdentity", Mandatory = true)] + public SwitchParameter WorkloadIdentity { get; set; } + + [Parameter(ParameterSetName = "WorkloadIdentity", Mandatory = true)] + [ValidateNotNullOrEmpty] + public string ExternalToken { get; set; } + [Parameter(ParameterSetName = "AzurePipelines", Mandatory = true)] public SwitchParameter AzurePipelines { get; set; } @@ -146,6 +158,7 @@ public class GetAzToken : PSLoggerCmdletBase [Parameter(ParameterSetName = "Interactive")] [Parameter(ParameterSetName = "DeviceCode")] [Parameter(ParameterSetName = "ManagedIdentity")] + [Parameter(ParameterSetName = "WorkloadIdentity")] [Parameter(ParameterSetName = "ClientSecret")] [Parameter(ParameterSetName = "ClientCertificate")] [Parameter(ParameterSetName = "ClientCertificatePath")] @@ -329,6 +342,19 @@ should be return; } } + else if (WorkloadIdentity.IsPresent) + { + WriteVerbose($"Getting token using workload identity federation (using client assertion) for client '{ClientId}' (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.clientassertioncredential)."); + try + { + WriteObject(TokenManager.GetTokenWorkloadIdentity(Resource, Scope, Claim, ClientId, Tenant, ExternalToken, stopProcessing.Token)); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "WorkloadIdentityTokenError", ErrorCategory.AuthenticationError, null)); + 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)."); diff --git a/source/AzAuth.psd1 b/source/AzAuth.psd1 index fa2eb61..ff43211 100644 --- a/source/AzAuth.psd1 +++ b/source/AzAuth.psd1 @@ -104,6 +104,8 @@ PrivateData = @{ 'serviceprincipal', 'spn', 'managedidentity', + 'workload', + 'workloadidentity', 'federation', 'credential' 'OIDC' diff --git a/tests/Get-AzToken.Tests.ps1 b/tests/Get-AzToken.Tests.ps1 index b9fe0b2..2a79636 100644 --- a/tests/Get-AzToken.Tests.ps1 +++ b/tests/Get-AzToken.Tests.ps1 @@ -10,6 +10,7 @@ BeforeDiscovery { @{ Name = 'Broker'; Mandatory = $false } @{ Name = 'DeviceCode'; Mandatory = $false } @{ Name = 'ManagedIdentity'; Mandatory = $false } + @{ Name = 'WorkloadIdentity'; Mandatory = $false } @{ Name = 'ClientSecret'; Mandatory = $false } @{ Name = 'ClientCertificate'; Mandatory = $false } @{ Name = 'ClientCertificatePath'; Mandatory = $false } @@ -26,6 +27,7 @@ BeforeDiscovery { @{ Name = 'Broker'; Mandatory = $false } @{ Name = 'DeviceCode'; Mandatory = $false } @{ Name = 'ManagedIdentity'; Mandatory = $false } + @{ Name = 'WorkloadIdentity'; Mandatory = $false } @{ Name = 'ClientSecret'; Mandatory = $false } @{ Name = 'ClientCertificate'; Mandatory = $false } @{ Name = 'ClientCertificatePath'; Mandatory = $false } @@ -42,6 +44,7 @@ BeforeDiscovery { @{ Name = 'Broker'; Mandatory = $false } @{ Name = 'DeviceCode'; Mandatory = $false } @{ Name = 'ManagedIdentity'; Mandatory = $false } + @{ Name = 'WorkloadIdentity'; Mandatory = $true } @{ Name = 'ClientSecret'; Mandatory = $true } @{ Name = 'ClientCertificate'; Mandatory = $true } @{ Name = 'ClientCertificatePath'; Mandatory = $true } @@ -58,6 +61,7 @@ BeforeDiscovery { @{ Name = 'Broker'; Mandatory = $false } @{ Name = 'DeviceCode'; Mandatory = $false } @{ Name = 'ManagedIdentity'; Mandatory = $false } + @{ Name = 'WorkloadIdentity'; Mandatory = $false } @{ Name = 'ClientSecret'; Mandatory = $false } @{ Name = 'ClientCertificate'; Mandatory = $false } @{ Name = 'ClientCertificatePath'; Mandatory = $false } @@ -74,6 +78,7 @@ BeforeDiscovery { @{ Name = 'DeviceCode'; Mandatory = $false } @{ Name = 'ManagedIdentity'; Mandatory = $false } @{ Name = 'Cache'; Mandatory = $false } + @{ Name = 'WorkloadIdentity'; Mandatory = $true } @{ Name = 'ClientSecret'; Mandatory = $true } @{ Name = 'ClientCertificate'; Mandatory = $true } @{ Name = 'ClientCertificatePath'; Mandatory = $true } @@ -150,6 +155,20 @@ BeforeDiscovery { @{ Name = 'ManagedIdentity'; Mandatory = $true } ) } + @{ + Name = 'WorkloadIdentity' + Type = 'System.Management.Automation.SwitchParameter' + ParameterSets = @( + @{ Name = 'WorkloadIdentity'; Mandatory = $true } + ) + } + @{ + Name = 'ExternalToken' + Type = 'string' + ParameterSets = @( + @{ Name = 'WorkloadIdentity'; Mandatory = $true } + ) + } @{ Name = 'ClientSecret' Type = 'string' @@ -200,6 +219,7 @@ BeforeDiscovery { @{ Name = 'Interactive'; Mandatory = $false } @{ Name = 'DeviceCode'; Mandatory = $false } @{ Name = 'ManagedIdentity'; Mandatory = $false } + @{ Name = 'WorkloadIdentity'; Mandatory = $false } @{ Name = 'ClientSecret'; Mandatory = $false } @{ Name = 'ClientCertificate'; Mandatory = $false } @{ Name = 'ClientCertificatePath'; Mandatory = $false }