Skip to content

Commit

Permalink
Implements ClientCertificate auth (#53)
Browse files Browse the repository at this point in the history
  • Loading branch information
PalmEmanuel committed Jan 22, 2024
2 parents 561f597 + a1e45c4 commit 7f91cce
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 6 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ The format is based on and uses the types of changes according to [Keep a Change

## [Unreleased]

### Added

- Added support for ClientCertificate authentication [#16](https://github.com/PalmEmanuel/AzAuth/issues/16)

## [2.2.8] - 2024-01-11

### Added
Expand Down
68 changes: 65 additions & 3 deletions docs/help/Get-AzToken.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,20 @@ Get-AzToken [[-Resource] <String>] [[-Scope] <String[]>] -TenantId <String> [-Cl
-ClientId <String> -ClientSecret <String> [-Force] [<CommonParameters>]
```

### ClientCertificate
```
Get-AzToken [[-Resource] <String>] [[-Scope] <String[]>] -TenantId <String> [-Claim <String>]
-ClientId <String> -ClientCertificate <X509Certificate2> [-Force]
[<CommonParameters>]
```

### ClientCertificatePath
```
Get-AzToken [[-Resource] <String>] [[-Scope] <String[]>] -TenantId <String> [-Claim <String>]
-ClientId <String> -ClientCertificatePath <String> [-Force]
[<CommonParameters>]
```

## DESCRIPTION

Gets a new Azure access token.
Expand Down Expand Up @@ -123,6 +137,22 @@ PS C:\> Get-AzToken -ClientId $ClientId -ClientSecret $ClientSecret -TenantId $T

Gets a new Azure access token for a client using the client credentials flow by specifying a client secret, valid for the default Microsoft Graph scope, also specifying the tenant as a mandatory parameter.

### Example 7

```powershell
PS C:\> Get-AzToken -ClientCertificate (Get-Item "Cert:\CurrentUser\My\$Thumbprint") -ClientId $ClientId -TenantId $TenantId
```

Gets a new Azure access token for a client using the client certificate flow by getting and providing an installed certificate from the user certificate store.

### Example 8

```powershell
PS C:\> Get-AzToken -ClientCertificatePath ".\certAndPrivateKey.pem" -ClientId $ClientId -TenantId $TenantId
```

Gets a new 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.

## PARAMETERS

### -Claim
Expand All @@ -141,6 +171,38 @@ Accept pipeline input: False
Accept wildcard characters: False
```

### -ClientCertificate

The certificate to be used for getting a token with the client certificate flow.

```yaml
Type: X509Certificate2
Parameter Sets: ClientCertificate
Aliases:

Required: True
Position: Named
Default value: None
Accept pipeline input: False
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.

```yaml
Type: String
Parameter Sets: ClientCertificatePath
Aliases:

Required: True
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```

### -ClientId

The client id of the application used to authenticate the user or identity. If not specified the user will be authenticated with an Azure development application.
Expand All @@ -159,7 +221,7 @@ Accept wildcard characters: False

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

Required: True
Expand Down Expand Up @@ -225,7 +287,7 @@ This may be required when combining interactive and non-interactive authenticati

```yaml
Type: SwitchParameter
Parameter Sets: NonInteractive, Interactive, DeviceCode, ManagedIdentity, WorkloadIdentity, ClientSecret
Parameter Sets: NonInteractive, Interactive, DeviceCode, ManagedIdentity, WorkloadIdentity, ClientSecret, ClientCertificate, ClientCertificatePath
Aliases:

Required: False
Expand Down Expand Up @@ -323,7 +385,7 @@ Accept wildcard characters: False

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

Required: True
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using Azure.Core;
using Azure.Identity;
using System.Security.Cryptography.X509Certificates;

namespace PipeHow.AzAuth;

internal static partial class TokenManager
{
/// <summary>
/// Gets token with a client certificate.
/// </summary>
internal static AzToken GetTokenClientCertificate(string resource, string[] scopes, string? claims, string clientId, string tenantId, X509Certificate2 clientCertificate, CancellationToken cancellationToken) =>
taskFactory.Run(() => GetTokenClientCertificateAsync(resource, scopes, claims, clientId, tenantId, clientCertificate, cancellationToken));

/// <summary>
/// Gets token with a client certificate from a file path.
/// </summary>
internal static AzToken GetTokenClientCertificate(string resource, string[] scopes, string? claims, string clientId, string tenantId, string clientCertificatePath, CancellationToken cancellationToken) =>
taskFactory.Run(() => GetTokenClientCertificateAsync(resource, scopes, claims, clientId, tenantId, clientCertificatePath, cancellationToken));

/// <summary>
/// Gets token with a client certificate.
/// </summary>
internal static async Task<AzToken> GetTokenClientCertificateAsync(
string resource,
string[] scopes,
string? claims,
string clientId,
string tenantId,
X509Certificate2 clientCertificate,
CancellationToken cancellationToken)
{
var fullScopes = scopes.Select(s => $"{resource.TrimEnd('/')}/{s}").ToArray();
var tokenRequestContext = new TokenRequestContext(fullScopes, null, claims, tenantId);

credential = new ClientCertificateCredential(tenantId, clientId, clientCertificate);

previousClientId = clientId;

return await GetTokenAsync(tokenRequestContext, cancellationToken);
}

/// <summary>
/// Gets token with a client certificate from a file path.
/// </summary>
internal static async Task<AzToken> GetTokenClientCertificateAsync(
string resource,
string[] scopes,
string? claims,
string clientId,
string tenantId,
string clientCertificatePath,
CancellationToken cancellationToken)
{
var fullScopes = scopes.Select(s => $"{resource.TrimEnd('/')}/{s}").ToArray();
var tokenRequestContext = new TokenRequestContext(fullScopes, null, claims, tenantId);

credential = new ClientCertificateCredential(tenantId, clientId, clientCertificatePath);

previousClientId = clientId;

return await GetTokenAsync(tokenRequestContext, cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ internal static partial class TokenManager
/// <summary>
/// Gets token with a client secret.
/// </summary>
internal static AzToken GetTokenClientSecret(string resource, string[] scopes, string? claims, string? clientId, string tenantId, string clientSecret, CancellationToken cancellationToken) =>
internal static AzToken GetTokenClientSecret(string resource, string[] scopes, string? claims, string clientId, string tenantId, string clientSecret, CancellationToken cancellationToken) =>
taskFactory.Run(() => GetTokenClientSecretAsync(resource, scopes, claims, clientId, tenantId, clientSecret, cancellationToken));

/// <summary>
Expand All @@ -18,7 +18,7 @@ internal static async Task<AzToken> GetTokenClientSecretAsync(
string resource,
string[] scopes,
string? claims,
string? clientId,
string clientId,
string tenantId,
string clientSecret,
CancellationToken cancellationToken)
Expand Down
32 changes: 32 additions & 0 deletions source/AzAuth.PS/Cmdlets/GetAzToken.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.Concurrent;
using System.Management.Automation;
using System.Security.Cryptography.X509Certificates;

#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
namespace PipeHow.AzAuth;
Expand All @@ -14,6 +15,8 @@ public class GetAzToken : PSLoggerCmdletBase
[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)]
[ValidateNotNullOrEmpty]
[Alias("ResourceId", "ResourceUrl")]
public string Resource { get; set; } = "https://graph.microsoft.com";
Expand All @@ -25,6 +28,8 @@ public class GetAzToken : PSLoggerCmdletBase
[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)]
[ValidateNotNullOrEmpty]
public string[] Scope { get; set; } = new[] { ".default" };

Expand All @@ -35,6 +40,8 @@ public class GetAzToken : PSLoggerCmdletBase
[Parameter(ParameterSetName = "ManagedIdentity")]
[Parameter(ParameterSetName = "WorkloadIdentity", Mandatory = true)]
[Parameter(ParameterSetName = "ClientSecret", Mandatory = true)]
[Parameter(ParameterSetName = "ClientCertificate", Mandatory = true)]
[Parameter(ParameterSetName = "ClientCertificatePath", Mandatory = true)]
[ValidateNotNullOrEmpty]
public string TenantId { get; set; }

Expand All @@ -45,6 +52,8 @@ public class GetAzToken : PSLoggerCmdletBase
[Parameter(ParameterSetName = "ManagedIdentity")]
[Parameter(ParameterSetName = "WorkloadIdentity")]
[Parameter(ParameterSetName = "ClientSecret")]
[Parameter(ParameterSetName = "ClientCertificate")]
[Parameter(ParameterSetName = "ClientCertificatePath")]
[ValidateNotNullOrEmpty]
public string Claim { get; set; }

Expand All @@ -54,6 +63,8 @@ public class GetAzToken : PSLoggerCmdletBase
[Parameter(ParameterSetName = "Cache")]
[Parameter(ParameterSetName = "WorkloadIdentity", Mandatory = true)]
[Parameter(ParameterSetName = "ClientSecret", Mandatory = true)]
[Parameter(ParameterSetName = "ClientCertificate", Mandatory = true)]
[Parameter(ParameterSetName = "ClientCertificatePath", Mandatory = true)]
[ValidateNotNullOrEmpty]
public string ClientId { get; set; }

Expand Down Expand Up @@ -93,12 +104,23 @@ public class GetAzToken : PSLoggerCmdletBase
[ValidateNotNullOrEmpty]
public string ClientSecret { get; set; }

[Parameter(ParameterSetName = "ClientCertificate", Mandatory = true)]
[ValidateNotNullOrEmpty]
public X509Certificate2 ClientCertificate { get; set; }

[Parameter(ParameterSetName = "ClientCertificatePath", Mandatory = true)]
[ValidateNotNullOrEmpty]
[ValidateCertificatePath]
public string ClientCertificatePath { get; set; }

[Parameter(ParameterSetName = "NonInteractive")]
[Parameter(ParameterSetName = "Interactive")]
[Parameter(ParameterSetName = "DeviceCode")]
[Parameter(ParameterSetName = "ManagedIdentity")]
[Parameter(ParameterSetName = "WorkloadIdentity")]
[Parameter(ParameterSetName = "ClientSecret")]
[Parameter(ParameterSetName = "ClientCertificate")]
[Parameter(ParameterSetName = "ClientCertificatePath")]
public SwitchParameter Force { get; set; }

// If user specifies Force, disregard earlier authentication
Expand Down Expand Up @@ -181,6 +203,16 @@ Shared token cache (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.
WriteVerbose($"Getting token using client secret for client \"{ClientId}\" (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.clientsecretcredential).");
WriteObject(TokenManager.GetTokenClientSecret(Resource, Scope, Claim, ClientId, TenantId, ClientSecret, stopProcessing.Token));
}
else if (ParameterSetName == "ClientCertificate")
{
WriteVerbose($"Getting token using client certificate for client \"{ClientId}\" (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.clientcertificatecredential).");
WriteObject(TokenManager.GetTokenClientCertificate(Resource, Scope, Claim, ClientId, TenantId, ClientCertificate, stopProcessing.Token));
}
else if (ParameterSetName == "ClientCertificatePath")
{
WriteVerbose($"Getting token using client certificate for client \"{ClientId}\" (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.clientcertificatecredential).");
WriteObject(TokenManager.GetTokenClientCertificate(Resource, Scope, Claim, ClientId, TenantId, ClientCertificatePath, stopProcessing.Token));
}
else
{
throw new ArgumentException("Invalid parameter combination!");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Collections;
using System.Management.Automation;
using System.Management.Automation.Language;
using System.Text.RegularExpressions;

namespace PipeHow.AzAuth;

Expand All @@ -24,3 +25,21 @@ public IEnumerable<CompletionResult> CompleteArgument(
return results;
}
}

public class ValidateCertificatePathAttribute : ValidateArgumentsAttribute
{
protected override void Validate(object arguments, EngineIntrinsics engineIntrinsics)
{
var path = arguments as string;

if (!Regex.Match(path, "\\.(pfx|pem)$").Success)
{
throw new ArgumentException("Only .pfx and .pem files are supported!");
}

if (!File.Exists(path))
{
throw new ArgumentException($"File '{path}' does not exist.");
}
}
}
2 changes: 1 addition & 1 deletion source/AzAuth.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
RootModule = 'AzAuth.PS.dll'

# Version number of this module.
ModuleVersion = '2.2.8'
ModuleVersion = '2.2.9'

# Supported PSEditions
CompatiblePSEditions = 'Core'
Expand Down
Loading

0 comments on commit 7f91cce

Please sign in to comment.