Skip to content

Commit

Permalink
feat: Move all authentication to single command (#100)
Browse files Browse the repository at this point in the history
- Added `Connect-WtWinTuner` and `Disconnect-WtWinTuner` commands
- Authentication related parameters removed from all other commands
- Added `Get-WtToken` to allow getting a toking using the set authentication, to use in other scripts for instance.
- Packages updated

**BREAKING CHANGE**: Scripts that rely upon the authentication parameters in all other commandlets will all break!
  • Loading branch information
svrooij authored Aug 21, 2024
1 parent 6c877cd commit f2d5bc8
Show file tree
Hide file tree
Showing 37 changed files with 2,696 additions and 2,774 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ on:
branches:
- main
workflow_dispatch:


jobs:
api:
Expand Down Expand Up @@ -158,7 +159,8 @@ jobs:
publish-nuget:
name: 📦 Publish WinTuner to nuget
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
#if: startsWith(github.ref, 'refs/tags/')
if: ${{ false }}
needs: [test, testps]
steps:
- name: 👨‍💻 Check-out code
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -372,3 +372,6 @@ TestResults.xml

# Ignore all files that are generated by Kiota before restore
/src/WinTuner.Proxy.Client/Generated

# What is with these files Rider?
.idea/
229 changes: 24 additions & 205 deletions src/Svrooij.WinTuner.CmdLets/Commands/BaseIntuneCmdlet.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
using Microsoft.Extensions.Options;
using Microsoft.Graph.Beta;
using Microsoft.Kiota.Abstractions.Authentication;
using Microsoft.Kiota.Abstractions.Authentication;
using Svrooij.PowerShell.DependencyInjection;
using System;
using System.Management.Automation;
using System.Runtime.InteropServices;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

Expand All @@ -14,218 +11,40 @@ namespace Svrooij.WinTuner.CmdLets.Commands;
/// </summary>
public abstract class BaseIntuneCmdlet : DependencyCmdlet<Startup>
{
private const string DefaultClientId = "d5a8a406-3b1d-4069-91cc-d76acdd812fe";
private const string DefaultClientCredentialScope = "https://graph.microsoft.com/.default";

/// <summary>
///
/// </summary>
[Parameter(
Mandatory = false,
Position = 20,
ValueFromPipeline = false,
ValueFromPipelineByPropertyName = false,
HelpMessage = "Use a managed identity to connect to Intune")]
public bool UseManagedIdentity { get; set; } = Environment.GetEnvironmentVariable("AZURE_USE_MANAGED_IDENTITY")?.Equals("true", StringComparison.OrdinalIgnoreCase) == true;

/// <summary>
///
/// </summary>
[Parameter(
Mandatory = false,
Position = 21,
ValueFromPipeline = false,
ValueFromPipelineByPropertyName = false,
HelpMessage = "Use default Azure Credentials from Azure.Identity to connect to Intune")]
public bool UseDefaultAzureCredential { get; set; } = Environment.GetEnvironmentVariable("AZURE_USE_DEFAULT_CREDENTIALS")?.Equals("true", StringComparison.OrdinalIgnoreCase) == true;

/// <summary>
///
/// </summary>
[Parameter(
Mandatory = false,
Position = 22,
ValueFromPipeline = false,
ValueFromPipelineByPropertyName = false,
HelpMessage = "Use a token from another source to connect to Intune")]
public string? Token { get; set; } = Environment.GetEnvironmentVariable("AZURE_TOKEN");

/// <summary>
///
/// </summary>
[Parameter(
Mandatory = false,
Position = 24,
ValueFromPipeline = false,
ValueFromPipelineByPropertyName = false,
HelpMessage = "Disable Windows authentication broker")]
public bool NoBroker { get; set; }

/// <summary>
///
/// </summary>
[Parameter(
Mandatory = false,
Position = 25,
ValueFromPipeline = false,
ValueFromPipelineByPropertyName = false,
HelpMessage = "Use a username to trigger interactive login or SSO")]
public string? Username { get; set; }

/// <summary>
///
/// Executes the cmdlet asynchronously.
/// </summary>
[Parameter(
Mandatory = false,
Position = 26,
ValueFromPipeline = false,
ValueFromPipelineByPropertyName = false,
HelpMessage = "Specify the tenant ID, optional for interactive, mandatory for Client Credentials flow. Loaded from `AZURE_TENANT_ID`")]
public string? TenantId { get; set; } = Environment.GetEnvironmentVariable("AZURE_TENANT_ID");

/// <summary>
///
/// </summary>
[Parameter(
Mandatory = false,
Position = 27,
ValueFromPipeline = false,
ValueFromPipelineByPropertyName = false,
HelpMessage = "Specify the client ID, optional for interactive, mandatory for Client Credentials flow. Loaded from `AZURE_CLIENT_ID`")]
public string? ClientId { get; set; } = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID");


/// <summary>
///
/// </summary>
[Parameter(
Mandatory = false,
Position = 28,
ValueFromPipeline = false,
ValueFromPipelineByPropertyName = false,
HelpMessage = "Specify the client secret, mandatory for Client Credentials flow. Loaded from `AZURE_CLIENT_SECRET`")]
public string? ClientSecret { get; set; } = Environment.GetEnvironmentVariable("AZURE_CLIENT_SECRET");

/// <summary>
///
/// </summary>
[Parameter(
Mandatory = false,
Position = 40,
ValueFromPipeline = false,
ValueFromPipelineByPropertyName = false,
HelpMessage = "Specify the scopes to request, default is `DeviceManagementConfiguration.ReadWrite.All`, `DeviceManagementApps.ReadWrite.All`")]
public string[]? Scopes { get; set; } = Environment.GetEnvironmentVariable("AZURE_SCOPES")?.Split(' ');
/// <param name="cancellationToken"></param>
/// <remarks>This is sealed, override ProcessAuthenticatedAsync</remarks>
public sealed override async Task ProcessRecordAsync(CancellationToken cancellationToken)
{
ValidateAuthenticationParameters();
await ProcessAuthenticatedAsync(CreateAuthenticationProvider(cancellationToken), cancellationToken);
}

/// <summary>
///
/// Execute a cmdlet with required authentication provider.
/// </summary>
internal static string[] DefaultScopes { get; } = new[] { "DeviceManagementConfiguration.ReadWrite.All", "DeviceManagementApps.ReadWrite.All" };

internal void ValidateAuthenticationParameters()
/// <param name="provider">Authentication provider, use <see cref="ConnectWtWinTuner"/> to connect to Intune</param>
/// <param name="cancellationToken">CancallationToken that will cancel if the user pressed ctrl + c, during execution</param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
protected virtual Task ProcessAuthenticatedAsync(IAuthenticationProvider provider, CancellationToken cancellationToken)
{
if (!string.IsNullOrEmpty(Token))
{
return;
}

if (UseManagedIdentity || UseDefaultAzureCredential)
{
Scopes ??= new[] { DefaultClientCredentialScope };
return;
}
throw new NotImplementedException();
}

if (!string.IsNullOrEmpty(Username))
{
return;
}

if (!string.IsNullOrEmpty(ClientId) && !string.IsNullOrEmpty(ClientSecret) && !string.IsNullOrEmpty(TenantId))
private void ValidateAuthenticationParameters()
{
if (ConnectWtWinTuner.AuthenticationProvider is null)
{
Scopes ??= new[] { DefaultClientCredentialScope };
return;
throw new InvalidDataException("Not logged in");
}

throw new ArgumentException($"Use `{nameof(Token)}`, `{nameof(UseManagedIdentity)}`, `{nameof(UseDefaultAzureCredential)}` or `{nameof(Username)}` to select the graph connection type", nameof(ParameterSetName));
}

internal IAuthenticationProvider CreateAuthenticationProvider(string[]? scopes = null, CancellationToken cancellationToken = default)
private IAuthenticationProvider CreateAuthenticationProvider(CancellationToken cancellationToken = default)
{
if (!string.IsNullOrEmpty(Token))
{
return new WingetIntune.Internal.Msal.StaticAuthenticationProvider(Token);
}

var scope = (Scopes ?? scopes ?? DefaultScopes)[0];

if (UseManagedIdentity || UseDefaultAzureCredential)
{
// Maybe make which credentials to use configurable
Azure.Core.TokenCredential credentials = UseManagedIdentity
? new Azure.Identity.ManagedIdentityCredential(ClientId)
: new Azure.Identity.DefaultAzureCredential();
return new Microsoft.Graph.Authentication.AzureIdentityAuthenticationProvider(credentials, null, null, scope);
}

if (!string.IsNullOrEmpty(ClientId) && !string.IsNullOrEmpty(TenantId))
{
if (!string.IsNullOrEmpty(ClientSecret))
{
return new Microsoft.Graph.Authentication.AzureIdentityAuthenticationProvider(new Azure.Identity.ClientSecretCredential(TenantId, ClientId, ClientSecret, new Azure.Identity.ClientSecretCredentialOptions
{
TokenCachePersistenceOptions = new Azure.Identity.TokenCachePersistenceOptions
{
Name = "WinTuner-PowerShell-CC",
UnsafeAllowUnencryptedStorage = true,
}
}), scopes: scope);
}

}

// Alternative interactive authentication in case the broker is not working as expected.
if (!string.IsNullOrEmpty(Username) && (NoBroker || RuntimeInformation.IsOSPlatform(OSPlatform.Windows) == false))
{
var interactiveOptions = new Azure.Identity.InteractiveBrowserCredentialOptions
{

TenantId = TenantId,
ClientId = ClientId ?? DefaultClientId,
LoginHint = Username,
RedirectUri = new Uri("http://localhost:12228/"),
TokenCachePersistenceOptions = new Azure.Identity.TokenCachePersistenceOptions
{
Name = "WinTuner-PowerShell",
UnsafeAllowUnencryptedStorage = true,
},
DisableAutomaticAuthentication = false,

};
interactiveOptions.AdditionallyAllowedTenants.Add("*");

var credential = new Azure.Identity.InteractiveBrowserCredential(interactiveOptions);

// This is to make sure it will get a token before we start using it.
// This will trigger the login screen early in the process.
//var result = credential.Authenticate(new Azure.Core.TokenRequestContext(scopes!, tenantId: TenantId), cancellationToken);

return new Microsoft.Graph.Authentication.AzureIdentityAuthenticationProvider(credential, scopes: scopes ?? DefaultScopes);
}

// Interactive authentication with broker, seem to not always work as expected.
if (!string.IsNullOrEmpty(Username))
{
return new WingetIntune.Internal.Msal.InteractiveAuthenticationProvider(new WingetIntune.Internal.Msal.InteractiveAuthenticationProviderOptions
{
ClientId = ClientId,
TenantId = TenantId,
Username = Username,
Scopes = scopes ?? DefaultScopes,
});
}

// This should never happen, but just in case.
// The ValidateAuthenticationParameters should have caught this.
throw new NotImplementedException();
return ConnectWtWinTuner.AuthenticationProvider!;
}
}
Loading

0 comments on commit f2d5bc8

Please sign in to comment.