Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ 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
- Added `-AzurePipelines` parameter set for Azure Pipelines service connection OIDC authentication (`AzurePipelinesCredential`)

### 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
Expand Down
66 changes: 60 additions & 6 deletions docs/help/Get-AzToken.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ Get-AzToken [[-Resource] <String>] [[-Scope] <String[]>] -Tenant <String> [-Clai
-ClientCertificatePath <String> [-Force] [<CommonParameters>]
```

### AzurePipelines
```
Get-AzToken [[-Resource] <String>] [[-Scope] <String[]>] -Tenant <String> [-Claim <String>] -ClientId <String>
[-AzurePipelines] -ServiceConnectionId <String> -SystemAccessToken <String> [-Force] [<CommonParameters>]
```

## DESCRIPTION

Gets a new Azure access token.
Expand All @@ -88,9 +94,9 @@ The token can be retrieved from an existing named cache, interactively from a br
- Environment variables (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.environmentcredential)
- 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

Expand Down Expand Up @@ -258,7 +264,7 @@ Accept wildcard characters: False

```yaml
Type: String
Parameter Sets: WorkloadIdentity, ClientSecret, ClientCertificate, ClientCertificatePath
Parameter Sets: WorkloadIdentity, ClientSecret, ClientCertificate, ClientCertificatePath, AzurePipelines
Aliases:

Required: True
Expand Down Expand Up @@ -292,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, Environment, AzurePowerShell, AzureCLI, VisualStudio, SharedTokenCache
Accepted values: ManagedIdentity, Environment, AzurePowerShell, AzureCLI, AzureDeveloperCli, VisualStudio

Required: False
Position: Named
Expand Down Expand Up @@ -341,7 +347,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
Aliases:

Required: False
Expand Down Expand Up @@ -438,7 +444,7 @@ Accept wildcard characters: False

```yaml
Type: String
Parameter Sets: WorkloadIdentity, ClientSecret, ClientCertificate, ClientCertificatePath
Parameter Sets: WorkloadIdentity, ClientSecret, ClientCertificate, ClientCertificatePath, AzurePipelines
Aliases: TenantId

Required: True
Expand Down Expand Up @@ -540,6 +546,54 @@ 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
```

### 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).

Expand Down
2 changes: 1 addition & 1 deletion source/AzAuth.Core/TokenManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Azure.Core;
using Azure.Identity;

namespace PipeHow.AzAuth;

internal static partial class TokenManager
{
/// <summary>
/// Gets token using Azure Pipelines service connection OIDC.
/// </summary>
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));

/// <summary>
/// Gets token using Azure Pipelines service connection OIDC.
/// </summary>
internal static async Task<AzToken> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ internal static async Task<AzToken> 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))
});
}
Expand Down
34 changes: 32 additions & 2 deletions source/AzAuth.PS/Cmdlets/GetAzToken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class GetAzToken : PSLoggerCmdletBase
[Parameter(ParameterSetName = "ClientSecret", Position = 0)]
[Parameter(ParameterSetName = "ClientCertificate", Position = 0)]
[Parameter(ParameterSetName = "ClientCertificatePath", Position = 0)]
[Parameter(ParameterSetName = "AzurePipelines", Position = 0)]
[ValidateNotNullOrEmpty]
[Alias("ResourceId", "ResourceUrl")]
public string Resource { get; set; } = "https://graph.microsoft.com";
Expand All @@ -33,6 +34,7 @@ public class GetAzToken : PSLoggerCmdletBase
[Parameter(ParameterSetName = "ClientSecret", Position = 1)]
[Parameter(ParameterSetName = "ClientCertificate", Position = 1)]
[Parameter(ParameterSetName = "ClientCertificatePath", Position = 1)]
[Parameter(ParameterSetName = "AzurePipelines", Position = 1)]
[ValidateNotNullOrEmpty]
public string[] Scope { get; set; } = new[] { ".default" };

Expand All @@ -46,6 +48,7 @@ public class GetAzToken : PSLoggerCmdletBase
[Parameter(ParameterSetName = "ClientSecret", Mandatory = true)]
[Parameter(ParameterSetName = "ClientCertificate", Mandatory = true)]
[Parameter(ParameterSetName = "ClientCertificatePath", Mandatory = true)]
[Parameter(ParameterSetName = "AzurePipelines", Mandatory = true)]
[ValidateNotNullOrEmpty]
[Alias("TenantId")]
public string Tenant { get; set; }
Expand All @@ -60,6 +63,7 @@ public class GetAzToken : PSLoggerCmdletBase
[Parameter(ParameterSetName = "ClientSecret")]
[Parameter(ParameterSetName = "ClientCertificate")]
[Parameter(ParameterSetName = "ClientCertificatePath")]
[Parameter(ParameterSetName = "AzurePipelines")]
[ValidateNotNullOrEmpty]
public string Claim { get; set; }

Expand All @@ -73,6 +77,7 @@ public class GetAzToken : PSLoggerCmdletBase
[Parameter(ParameterSetName = "ClientSecret", Mandatory = true)]
[Parameter(ParameterSetName = "ClientCertificate", Mandatory = true)]
[Parameter(ParameterSetName = "ClientCertificatePath", Mandatory = true)]
[Parameter(ParameterSetName = "AzurePipelines", Mandatory = true)]
[ValidateNotNullOrEmpty]
public string ClientId { get; set; }

Expand Down Expand Up @@ -101,10 +106,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; }
Expand All @@ -125,6 +130,17 @@ 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 = "ClientSecret", Mandatory = true)]
[ValidateNotNullOrEmpty]
public string ClientSecret { get; set; }
Expand All @@ -146,6 +162,7 @@ public class GetAzToken : PSLoggerCmdletBase
[Parameter(ParameterSetName = "ClientSecret")]
[Parameter(ParameterSetName = "ClientCertificate")]
[Parameter(ParameterSetName = "ClientCertificatePath")]
[Parameter(ParameterSetName = "AzurePipelines")]
public SwitchParameter Force { get; set; }

// If user specifies Force, disregard earlier authentication
Expand Down Expand Up @@ -338,6 +355,19 @@ 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 (ParameterSetName == "ClientSecret")
{
WriteVerbose($"Getting token using client secret for client '{ClientId}' (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.clientsecretcredential).");
Expand Down
27 changes: 27 additions & 0 deletions tests/Get-AzToken.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ BeforeDiscovery {
@{ Name = 'ClientSecret'; Mandatory = $false }
@{ Name = 'ClientCertificate'; Mandatory = $false }
@{ Name = 'ClientCertificatePath'; Mandatory = $false }
@{ Name = 'AzurePipelines'; Mandatory = $false }
)
}
@{
Expand All @@ -30,6 +31,7 @@ BeforeDiscovery {
@{ Name = 'ClientSecret'; Mandatory = $false }
@{ Name = 'ClientCertificate'; Mandatory = $false }
@{ Name = 'ClientCertificatePath'; Mandatory = $false }
@{ Name = 'AzurePipelines'; Mandatory = $false }
)
}
@{
Expand All @@ -46,6 +48,7 @@ BeforeDiscovery {
@{ Name = 'ClientSecret'; Mandatory = $true }
@{ Name = 'ClientCertificate'; Mandatory = $true }
@{ Name = 'ClientCertificatePath'; Mandatory = $true }
@{ Name = 'AzurePipelines'; Mandatory = $true }
)
}
@{
Expand All @@ -62,6 +65,7 @@ BeforeDiscovery {
@{ Name = 'ClientSecret'; Mandatory = $false }
@{ Name = 'ClientCertificate'; Mandatory = $false }
@{ Name = 'ClientCertificatePath'; Mandatory = $false }
@{ Name = 'AzurePipelines'; Mandatory = $false }
)
}
@{
Expand All @@ -78,6 +82,7 @@ BeforeDiscovery {
@{ Name = 'ClientSecret'; Mandatory = $true }
@{ Name = 'ClientCertificate'; Mandatory = $true }
@{ Name = 'ClientCertificatePath'; Mandatory = $true }
@{ Name = 'AzurePipelines'; Mandatory = $true }
)
}
@{
Expand Down Expand Up @@ -185,6 +190,27 @@ BeforeDiscovery {
@{ Name = 'ClientCertificatePath'; 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 = 'Force'
Type = 'System.Management.Automation.SwitchParameter'
Expand All @@ -197,6 +223,7 @@ BeforeDiscovery {
@{ Name = 'ClientSecret'; Mandatory = $false }
@{ Name = 'ClientCertificate'; Mandatory = $false }
@{ Name = 'ClientCertificatePath'; Mandatory = $false }
@{ Name = 'AzurePipelines'; Mandatory = $false }
)
}
)
Expand Down
Loading