Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

-TimeoutSeconds for Managed Identity, Managed Identity auth as first option in noninteractive auth #89

Merged
merged 6 commits into from
Aug 21, 2024
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
12 changes: 11 additions & 1 deletion .github/actions/build/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,21 @@ runs:
shell: pwsh
run: ./build.ps1 -ResolveDependency -Task noop

# Replace dot in semVer with dash, for Sampler validation in pre-release
- name: Format semVer for Sampler
id: formatSemVer
shell: pwsh
run: |
$SemVer = '${{ steps.gitversion.outputs.semVer }}'
# Replace last dot with dash for Sampler to accept it as pre-release, does not allow dots in pre-release name
$SemVer = $SemVer -replace '^([\d\.]+\-\w+)\.(\d+)$','$1-$2'
Add-Content -Path $env:GITHUB_OUTPUT -Value "formattedSemVer=$SemVer"

- name: Build module
shell: pwsh
run: ./build.ps1 -tasks pack
env:
ModuleVersion: ${{ env.gitVersion.NuGetVersionV2 }}
ModuleVersion: ${{ steps.formatSemVer.outputs.formattedSemVer }}

- name: Publish build artifacts
uses: actions/upload-artifact@v4
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ The format is based on and uses the types of changes according to [Keep a Change
### Added

- Adds related links links to blog posts for Get-AzToken and the parameters -WorkloadIdentity & -ExternalToken
- Added `-TimeoutSeconds` parameter for Managed Identity authentication and non-interactive authentication
- Added Managed Identity authentication as first option of non-interactive login

## [2.2.10] - 2024-05-22

Expand Down
131 changes: 109 additions & 22 deletions GitVersion.yml
Original file line number Diff line number Diff line change
@@ -1,29 +1,116 @@
strategies:
- Mainline
next-version: 0.0.1
major-version-bump-message: '(breaking\schange|breaking|major)\b'
minor-version-bump-message: '(adds?|features?|minor)\b'
patch-version-bump-message: '\s?(fix|patch)'
assembly-versioning-scheme: MajorMinorPatch
assembly-file-versioning-scheme: MajorMinorPatch
tag-prefix: '[vV]?'
version-in-branch-pattern: (?<version>[vV]?\d+(\.\d+)?(\.\d+)?).*
major-version-bump-message: '\+semver:\s?(breaking|major)'
minor-version-bump-message: '\+semver:\s?(feature|minor)'
patch-version-bump-message: '\+semver:\s?(fix|patch)'
no-bump-message: '\+semver:\s?(none|skip)'
assembly-informational-format: 'MajorMinorPatch'
tag-pre-release-weight: 60000
commit-date-format: yyyy-MM-dd
merge-message-formats: {}
update-build-number: true
semantic-version-format: Strict
strategies:
- Fallback
- ConfiguredNextVersion
- MergeMessage
- TaggedCommit
- TrackReleaseBranches
- VersionInBranchName
branches:
main:
label: preview
regex: ^main$
label: 'preview'
increment: Patch
pull-request:
label: PR
feature:
label: useBranchName
increment: Minor
regex: f(eature(s)?)?[\/-]
source-branches: ['main']
hotfix:
label: fix
prevent-increment:
of-merged-branch: true
track-merge-target: false
track-merge-message: true
regex: ^master$|^main$
source-branches: []
is-source-branch-for: []
tracks-release-branches: false
is-release-branch: false
is-main-branch: true
pre-release-weight: 55000
release:
mode: ManualDeployment
label: beta
increment: Patch
regex: (hot)?fix(es)?[\/-]
source-branches: ['main']

prevent-increment:
of-merged-branch: true
when-branch-merged: false
when-current-commit-tagged: false
track-merge-target: false
track-merge-message: true
regex: ^releases?[/-](?<BranchName>.+)
source-branches:
- main
is-source-branch-for: []
tracks-release-branches: false
is-release-branch: true
is-main-branch: false
pre-release-weight: 30000
feature:
mode: ManualDeployment
label: '{BranchName}'
increment: Inherit
prevent-increment:
when-current-commit-tagged: false
track-merge-message: true
regex: ^features?[/-](?<BranchName>.+)
source-branches:
- main
- release
is-source-branch-for: []
is-main-branch: false
pre-release-weight: 30000
pull-request:
mode: ContinuousDelivery
label: PullRequest
increment: Inherit
prevent-increment:
of-merged-branch: true
when-current-commit-tagged: false
label-number-pattern: '[/-](?<number>\d+)'
track-merge-message: true
regex: ^(pull|pull\-requests|pr)[/-]
source-branches:
- main
- release
- feature
is-source-branch-for: []
pre-release-weight: 30000
unknown:
mode: ManualDeployment
label: '{BranchName}'
increment: Inherit
prevent-increment:
when-current-commit-tagged: false
track-merge-message: false
regex: (?<BranchName>.+)
source-branches:
- main
- release
- feature
- pull-request
is-source-branch-for: []
is-main-branch: false
ignore:
sha: []
merge-message-formats: {}
mode: ContinuousDelivery
label: '{BranchName}'
increment: Inherit
prevent-increment:
of-merged-branch: false
when-branch-merged: false
when-current-commit-tagged: true
track-merge-target: false
track-merge-message: true
commit-message-incrementing: Enabled
regex: ''
source-branches: []
is-source-branch-for: []
tracks-release-branches: false
is-release-branch: false
is-main-branch: false
9 changes: 5 additions & 4 deletions docs/help/Get-AzToken.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ Gets a new Azure access token.

### NonInteractive (Default)
```
Get-AzToken [[-Resource] <String>] [[-Scope] <String[]>] [-TenantId <String>] [-Claim <String>] [-Force]
[<CommonParameters>]
Get-AzToken [[-Resource] <String>] [[-Scope] <String[]>] [-TenantId <String>] [-Claim <String>]
[-TimeoutSeconds <Int32>] [-Force] [<CommonParameters>]
```

### Cache
Expand All @@ -43,7 +43,8 @@ Get-AzToken [[-Resource] <String>] [[-Scope] <String[]>] [-TenantId <String>] [-
### ManagedIdentity
```
Get-AzToken [[-Resource] <String>] [[-Scope] <String[]>] [-TenantId <String>] [-Claim <String>]
[-ClientId <String>] [-ManagedIdentity] [-Force] [<CommonParameters>]
[-ClientId <String>] [-TimeoutSeconds <Int32>] [-ManagedIdentity] [-Force]
[<CommonParameters>]
```

### WorkloadIdentity
Expand Down Expand Up @@ -409,7 +410,7 @@ The number of seconds to wait until the login times out.

```yaml
Type: Int32
Parameter Sets: Interactive, DeviceCode
Parameter Sets: NonInteractive, Interactive, DeviceCode, ManagedIdentity
Aliases:

Required: False
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ internal static partial class TokenManager
/// <summary>
/// Gets token as a managed identity.
/// </summary>
internal static AzToken GetTokenManagedIdentity(string resource, string[] scopes, string? claims, string? clientId, string? tenantId, CancellationToken cancellationToken) =>
taskFactory.Run(() => GetTokenManagedIdentityAsync(resource, scopes, claims, clientId, tenantId, cancellationToken));
internal static AzToken GetTokenManagedIdentity(string resource, string[] scopes, string? claims, string? clientId, string? tenantId, int timeoutSeconds, CancellationToken cancellationToken) =>
taskFactory.Run(() => GetTokenManagedIdentityAsync(resource, scopes, claims, clientId, tenantId, timeoutSeconds, cancellationToken));

/// <summary>
/// Gets token as a managed identity.
Expand All @@ -20,6 +20,7 @@ internal static async Task<AzToken> GetTokenManagedIdentityAsync(
string? claims,
string? clientId,
string? tenantId,
int timeoutSeconds,
CancellationToken cancellationToken)
{
var fullScopes = scopes.Select(s => $"{resource.TrimEnd('/')}/{s}").ToArray();
Expand All @@ -28,7 +29,14 @@ internal static async Task<AzToken> GetTokenManagedIdentityAsync(
// Re-use the previous managed identity credential if client id didn't change
if (credential is not ManagedIdentityCredential || previousClientId != clientId)
{
credential = new ManagedIdentityCredential(clientId);
credential = new ManagedIdentityCredential(clientId, options: new TokenCredentialOptions{
Retry = {
NetworkTimeout = TimeSpan.FromSeconds(timeoutSeconds),
MaxRetries = 0,
Delay = TimeSpan.Zero,
MaxDelay = TimeSpan.Zero
}
});
}

previousClientId = clientId;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using Azure.Core;
using Azure.Core.Diagnostics;
using Azure.Core.Pipeline;
using Azure.Identity;
using System.Diagnostics.Tracing;
using System.Text.RegularExpressions;

namespace PipeHow.AzAuth;
Expand All @@ -9,8 +12,8 @@ internal static partial class TokenManager
/// <summary>
/// Gets token noninteractively.
/// </summary>
internal static AzToken GetTokenNonInteractive(string resource, string[] scopes, string? claims, string? tenantId, CancellationToken cancellationToken) =>
taskFactory.Run(() => GetTokenNonInteractiveAsync(resource, scopes, claims, tenantId, cancellationToken));
internal static AzToken GetTokenNonInteractive(string resource, string[] scopes, string? claims, string? tenantId, int? timeoutSeconds, int managedIdentityTimeoutSeconds, CancellationToken cancellationToken) =>
taskFactory.Run(() => GetTokenNonInteractiveAsync(resource, scopes, claims, tenantId, timeoutSeconds, managedIdentityTimeoutSeconds, cancellationToken));

/// <summary>
/// Gets token noninteractively.
Expand All @@ -20,19 +23,42 @@ internal static async Task<AzToken> GetTokenNonInteractiveAsync(
string[] scopes,
string? claims,
string? tenantId,
int? timeoutSeconds,
int managedIdentityTimeoutSeconds,
CancellationToken cancellationToken)
{
var fullScopes = scopes.Select(s => $"{resource.TrimEnd('/')}/{s}").ToArray();

// If timeoutSeconds is not null, create a new TokenCredentialOptions with the specified timeout
// Otherwise, set to null to use default timeout
TokenCredentialOptions? genericTimeoutOptions = timeoutSeconds.HasValue ? new TokenCredentialOptions
{
Retry = {
NetworkTimeout = TimeSpan.FromSeconds(timeoutSeconds.Value),
MaxRetries = 0,
Delay = TimeSpan.Zero,
MaxDelay = TimeSpan.Zero
}
} : null;

// Create our own credential chain because we want to change the order
var sources = new List<TokenCredential>()
{
new EnvironmentCredential(),
new AzurePowerShellCredential(),
new AzureCliCredential(),
new VisualStudioCodeCredential(),
new VisualStudioCredential(),
new SharedTokenCacheCredential()
// ManagedIdentityCredential with custom timeout
new ManagedIdentityCredential(options: new TokenCredentialOptions{
Retry = {
NetworkTimeout = TimeSpan.FromSeconds(managedIdentityTimeoutSeconds),
MaxRetries = 0,
Delay = TimeSpan.Zero,
MaxDelay = TimeSpan.Zero
}
}),
new EnvironmentCredential(genericTimeoutOptions),
new AzurePowerShellCredential(genericTimeoutOptions as AzurePowerShellCredentialOptions),
new AzureCliCredential(genericTimeoutOptions as AzureCliCredentialOptions),
new VisualStudioCodeCredential(genericTimeoutOptions as VisualStudioCodeCredentialOptions),
new VisualStudioCredential(genericTimeoutOptions as VisualStudioCredentialOptions),
new SharedTokenCacheCredential(genericTimeoutOptions as SharedTokenCacheCredentialOptions)
};

// If user authenticated interactively in the same session and tenant didn't change, add it as the first option to find tokens from
Expand Down
21 changes: 19 additions & 2 deletions source/AzAuth.PS/Cmdlets/GetAzToken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ public class GetAzToken : PSLoggerCmdletBase
[ArgumentCompleter(typeof(ExistingAccounts))]
public string Username { get; set; }

[Parameter(ParameterSetName = "NonInteractive")]
[Parameter(ParameterSetName = "Interactive")]
[Parameter(ParameterSetName = "ManagedIdentity")]
[Parameter(ParameterSetName = "DeviceCode")]
[ValidateRange(1, int.MaxValue)]
public int TimeoutSeconds { get; set; } = 120;
Expand Down Expand Up @@ -146,15 +148,25 @@ protected override void EndProcessing()

if (ParameterSetName == "NonInteractive")
{
// If user didn't specify a timeout, default to 1 second for managed identity
int managedIdentityTimeoutSeconds = 1;
int? noninteractiveTimeoutSeconds = null;
if (MyInvocation.BoundParameters.ContainsKey("TimeoutSeconds"))
{
managedIdentityTimeoutSeconds = TimeoutSeconds;
noninteractiveTimeoutSeconds = TimeoutSeconds;
}

WriteVerbose(@"Looking for a token from the following sources:
Managed Identity (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.managedidentitycredential)
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)
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)
");
WriteObject(TokenManager.GetTokenNonInteractive(Resource, Scope, Claim, TenantId, stopProcessing.Token));
WriteObject(TokenManager.GetTokenNonInteractive(Resource, Scope, Claim, TenantId, noninteractiveTimeoutSeconds, managedIdentityTimeoutSeconds, stopProcessing.Token));
}
else if (ParameterSetName == "Cache")
{
Expand Down Expand Up @@ -190,8 +202,13 @@ Shared token cache (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.
}
else if (ManagedIdentity.IsPresent)
{
// If user didn't specify a timeout, default to 1 second for managed identity
if (!MyInvocation.BoundParameters.ContainsKey("TimeoutSeconds"))
{
TimeoutSeconds = 1;
}
WriteVerbose("Getting token using a managed identity (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.managedidentitycredential).");
WriteObject(TokenManager.GetTokenManagedIdentity(Resource, Scope, Claim, ClientId, TenantId, stopProcessing.Token));
WriteObject(TokenManager.GetTokenManagedIdentity(Resource, Scope, Claim, ClientId, TenantId, TimeoutSeconds, stopProcessing.Token));
}
else if (WorkloadIdentity.IsPresent)
{
Expand Down
2 changes: 2 additions & 0 deletions tests/Get-AzToken.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ BeforeDiscovery {
Name = 'TimeoutSeconds'
Type = 'int'
ParameterSets = @(
@{ Name = 'NonInteractive'; Mandatory = $false }
@{ Name = 'Interactive'; Mandatory = $false }
@{ Name = 'ManagedIdentity'; Mandatory = $false }
@{ Name = 'DeviceCode'; Mandatory = $false }
)
}
Expand Down
Loading