From fa60860982e24e9f64c39ee2d07ecf3a3eab2423 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sat, 15 Mar 2025 14:03:17 -0500 Subject: [PATCH 01/15] fix: authentication and oauth redirect --- CHANGELOG.md | 10 ++++++++++ README.md | 2 +- source/Private/Connect-TkMsService.ps1 | 4 ++-- source/Private/New-TkAppRegistration.ps1 | 5 ++++- ...tration.ps1 => New-TkAppSpOauth2Registration.ps1} | 8 +++++--- source/Public/Publish-TkEmailApp.ps1 | 2 +- source/Public/Publish-TkM365AuditApp.ps1 | 2 +- source/Public/Publish-TkMemPolicyManagerApp.ps1 | 2 +- ...s.ps1 => New-TkAppSpOauth2Registration.tests.ps1} | 12 ++++++------ 9 files changed, 31 insertions(+), 16 deletions(-) rename source/Private/{Initialize-TkAppSpRegistration.ps1 => New-TkAppSpOauth2Registration.ps1} (95%) rename tests/Unit/Private/{Initialize-TkAppSpRegistration.tests.ps1 => New-TkAppSpOauth2Registration.tests.ps1} (76%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 142d3ac..a28ee24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Fixed authentication context for mggraph. + +### Changed + +- Updated private function names to be more descriptive. + +## [0.2.0] - 2025-03-14 + ### Added - Updated docs for the module. diff --git a/README.md b/README.md index 441d7b7..c7762b4 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ The following Private Functions support the module’s internal processes and ar - **Connect-TkMsService** - **ConvertTo-ParameterSplat** - **Initialize-TkAppAuthCertificate** -- **Initialize-TkAppSpRegistration** +- **New-TkAppSpOauth2Registration** - **Initialize-TkModuleEnv** - **Initialize-TkAppName** - **New-TkAppRegistration** diff --git a/source/Private/Connect-TkMsService.ps1 b/source/Private/Connect-TkMsService.ps1 index fef53a9..42d742e 100644 --- a/source/Private/Connect-TkMsService.ps1 +++ b/source/Private/Connect-TkMsService.ps1 @@ -102,7 +102,7 @@ function Connect-TkMsService { # Remove the old context so we can connect fresh Remove-MgContext -ErrorAction SilentlyContinue Write-AuditLog 'Creating a new Microsoft Graph session.' - Connect-MgGraph -Scopes $scopesNeeded ` + Connect-MgGraph -ContextScope Process -Scopes $scopesNeeded ` -ErrorAction Stop Write-AuditLog 'Connected to Microsoft Graph.' } @@ -110,7 +110,7 @@ function Connect-TkMsService { else { # No valid session, so just connect Write-AuditLog 'No valid Microsoft Graph session found. Connecting...' - Connect-MgGraph -Scopes $scopesNeeded ` + Connect-MgGraph -ContextScope Process -Scopes $scopesNeeded ` -ErrorAction Stop Write-AuditLog 'Connected to Microsoft Graph.' } diff --git a/source/Private/New-TkAppRegistration.ps1 b/source/Private/New-TkAppRegistration.ps1 index d24738b..c7ed879 100644 --- a/source/Private/New-TkAppRegistration.ps1 +++ b/source/Private/New-TkAppRegistration.ps1 @@ -89,7 +89,7 @@ function New-TkAppRegistration { throw "Certificate with thumbprint $CertThumbprint not found in $CertStoreLocation." } $shouldProcessTarget = "'$DisplayName' for sign-in audience '$SignInAudience' with certificate thumbprint $CertThumbprint." - $shouldProcessOperation = "New-MgApplication" + $shouldProcessOperation = 'New-MgApplication' if ($PSCmdlet.ShouldProcess($shouldProcessTarget , $shouldProcessOperation )) { $MgApplicationParams = @{ DisplayName = $DisplayName @@ -104,6 +104,9 @@ function New-TkAppRegistration { Key = $Cert.RawData } ) + Web = @{ + RedirectUris = @('https://login.microsoftonline.com/common/oauth2/nativeclient') + } } $AppRegistration = New-MgApplication @MgApplicationParams } diff --git a/source/Private/Initialize-TkAppSpRegistration.ps1 b/source/Private/New-TkAppSpOauth2Registration.ps1 similarity index 95% rename from source/Private/Initialize-TkAppSpRegistration.ps1 rename to source/Private/New-TkAppSpOauth2Registration.ps1 index 11ed333..6fd3c99 100644 --- a/source/Private/Initialize-TkAppSpRegistration.ps1 +++ b/source/Private/New-TkAppSpOauth2Registration.ps1 @@ -21,12 +21,12 @@ $AppRegistration = Get-MgApplication -AppId "your-app-id" $RequiredResourceAccessList = @() $Context = [PSCustomObject]@{ TenantId = "your-tenant-id" } - Initialize-TkAppSpRegistration -AppRegistration $AppRegistration -RequiredResourceAccessList $RequiredResourceAccessList -Context $Context + New-TkAppSpOauth2Registration -AppRegistration $AppRegistration -RequiredResourceAccessList $RequiredResourceAccessList -Context $Context .NOTES This function requires the Microsoft.Graph PowerShell module. #> -function Initialize-TkAppSpRegistration { +function New-TkAppSpOauth2Registration { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param( [Parameter( @@ -156,12 +156,14 @@ function Initialize-TkAppSpRegistration { $i++ } } + $RedirectUri = "&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient" # 5. Build the admin consent URL $adminConsentUrl = ` 'https://login.microsoftonline.com/' ` + $Context.TenantId ` + '/adminconsent?client_id=' ` - + $AppRegistration.AppId + + $AppRegistration.AppId ` + + $RedirectUri Write-Verbose 'Please go to the following URL in your browser to provide admin consent:' -Verbose Write-AuditLog "`n`n$adminConsentUrl`n" -Severity information # For each end diff --git a/source/Public/Publish-TkEmailApp.ps1 b/source/Public/Publish-TkEmailApp.ps1 index 89c3e5f..b55c10b 100644 --- a/source/Public/Publish-TkEmailApp.ps1 +++ b/source/Public/Publish-TkEmailApp.ps1 @@ -360,7 +360,7 @@ function Publish-TkEmailApp { CertThumbprint = $CertDetails.CertThumbprint ErrorAction = 'Stop' } - $ConsentUrl = Initialize-TkAppSpRegistration @AppSpRegistrationParams + $ConsentUrl = New-TkAppSpOauth2Registration @AppSpRegistrationParams [void](Read-Host 'Provide admin consent now, or copy the url and provide admin consent later. Press Enter to continue.') # 8) Create the Exchange Online policy restricting send New-TkExchangeEmailAppPolicy ` diff --git a/source/Public/Publish-TkM365AuditApp.ps1 b/source/Public/Publish-TkM365AuditApp.ps1 index 0749c99..5cb517b 100644 --- a/source/Public/Publish-TkM365AuditApp.ps1 +++ b/source/Public/Publish-TkM365AuditApp.ps1 @@ -230,7 +230,7 @@ function Publish-TkM365AuditApp { CertThumbprint = $CertDetails.CertThumbprint ErrorAction = 'Stop' } - $ConsentUrl = Initialize-TkAppSpRegistration @AppSpRegistrationParams + $ConsentUrl = New-TkAppSpOauth2Registration @AppSpRegistrationParams [void](Read-Host 'Provide admin consent now, or copy the url and provide admin consent later. Press Enter to continue.') Write-AuditLog 'Appending Exchange Administrator role to the app.' $exoAdminRole = Get-MgDirectoryRole -Filter "displayName eq 'Exchange Administrator'" -ErrorAction Stop diff --git a/source/Public/Publish-TkMemPolicyManagerApp.ps1 b/source/Public/Publish-TkMemPolicyManagerApp.ps1 index ab22d4e..ea2e0c1 100644 --- a/source/Public/Publish-TkMemPolicyManagerApp.ps1 +++ b/source/Public/Publish-TkMemPolicyManagerApp.ps1 @@ -241,7 +241,7 @@ function Publish-TkMemPolicyManagerApp { CertThumbprint = $CertDetails.CertThumbprint ErrorAction = 'Stop' } - $ConsentUrl = Initialize-TkAppSpRegistration @AppSpRegistrationParams + $ConsentUrl = New-TkAppSpOauth2Registration @AppSpRegistrationParams [void](Read-Host 'Provide admin consent now, or copy the url and provide admin consent later. Press Enter to continue.') # 8) Build a final PSCustomObject to store in the secret vault $TkMemPolicyManagerAppParams = @{ diff --git a/tests/Unit/Private/Initialize-TkAppSpRegistration.tests.ps1 b/tests/Unit/Private/New-TkAppSpOauth2Registration.tests.ps1 similarity index 76% rename from tests/Unit/Private/Initialize-TkAppSpRegistration.tests.ps1 rename to tests/Unit/Private/New-TkAppSpOauth2Registration.tests.ps1 index a43beae..69ae07e 100644 --- a/tests/Unit/Private/Initialize-TkAppSpRegistration.tests.ps1 +++ b/tests/Unit/Private/New-TkAppSpOauth2Registration.tests.ps1 @@ -8,7 +8,7 @@ $ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{ Import-Module $ProjectName InModuleScope $ProjectName { - Describe 'Initialize-TkAppSpRegistration' { + Describe 'New-TkAppSpOauth2Registration' { Mock -CommandName Write-AuditLog Mock -CommandName Get-ChildItem Mock -CommandName New-MgServicePrincipal @@ -19,7 +19,7 @@ InModuleScope $ProjectName { $AppRegistration = [PSCustomObject]@{ AppId = 'test-app-id' } $RequiredResourceAccessList = @() $Context = [PSCustomObject]@{ TenantId = 'test-tenant-id' } - { Initialize-TkAppSpRegistration -AppRegistration $AppRegistration -RequiredResourceAccessList $RequiredResourceAccessList -Context $Context -AuthMethod 'Certificate' } | Should -Throw "CertThumbprint is required when AuthMethod is 'Certificate'." + { New-TkAppSpOauth2Registration -AppRegistration $AppRegistration -RequiredResourceAccessList $RequiredResourceAccessList -Context $Context -AuthMethod 'Certificate' } | Should -Throw "CertThumbprint is required when AuthMethod is 'Certificate'." } } Context 'When AuthMethod is Certificate and CertThumbprint is provided' { @@ -31,7 +31,7 @@ InModuleScope $ProjectName { $Cert = [PSCustomObject]@{ Thumbprint = $CertThumbprint; SubjectName = [PSCustomObject]@{ Name = 'test-cert' } } Mock -CommandName Get-ChildItem -MockWith { $Cert } Mock -CommandName Get-MgServicePrincipal -MockWith { [PSCustomObject]@{ Id = 'test-sp-id'; DisplayName = 'test-sp' } } - Initialize-TkAppSpRegistration -AppRegistration $AppRegistration -RequiredResourceAccessList $RequiredResourceAccessList -Context $Context -AuthMethod 'Certificate' -CertThumbprint $CertThumbprint + New-TkAppSpOauth2Registration -AppRegistration $AppRegistration -RequiredResourceAccessList $RequiredResourceAccessList -Context $Context -AuthMethod 'Certificate' -CertThumbprint $CertThumbprint Assert-MockCalled -CommandName Get-ChildItem -Times 1 Assert-MockCalled -CommandName New-MgServicePrincipal -Times 1 Assert-MockCalled -CommandName Get-MgServicePrincipal -Times 1 @@ -42,7 +42,7 @@ InModuleScope $ProjectName { $AppRegistration = [PSCustomObject]@{ AppId = 'test-app-id' } $RequiredResourceAccessList = @() $Context = [PSCustomObject]@{ TenantId = 'test-tenant-id' } - { Initialize-TkAppSpRegistration -AppRegistration $AppRegistration -RequiredResourceAccessList $RequiredResourceAccessList -Context $Context -AuthMethod 'ClientSecret' } | Should -Throw "AuthMethod ClientSecret is not yet implemented." + { New-TkAppSpOauth2Registration -AppRegistration $AppRegistration -RequiredResourceAccessList $RequiredResourceAccessList -Context $Context -AuthMethod 'ClientSecret' } | Should -Throw "AuthMethod ClientSecret is not yet implemented." } } Context 'When RequiredResourceAccessList has too many resources' { @@ -54,7 +54,7 @@ InModuleScope $ProjectName { [PSCustomObject]@{ ResourceAppId = 'resource3'; ResourceAccess = @() } ) $Context = [PSCustomObject]@{ TenantId = 'test-tenant-id' } - { Initialize-TkAppSpRegistration -AppRegistration $AppRegistration -RequiredResourceAccessList $RequiredResourceAccessList -Context $Context } | Should -Throw 'Too many resources in RequiredResourceAccessList.' + { New-TkAppSpOauth2Registration -AppRegistration $AppRegistration -RequiredResourceAccessList $RequiredResourceAccessList -Context $Context } | Should -Throw 'Too many resources in RequiredResourceAccessList.' } } Context 'When RequiredResourceAccessList is valid' { @@ -69,7 +69,7 @@ InModuleScope $ProjectName { Mock -CommandName Get-ChildItem -MockWith { $Cert } Mock -CommandName Get-MgServicePrincipal -MockWith { [PSCustomObject]@{ Id = 'test-sp-id'; DisplayName = 'test-sp' } } Mock -CommandName New-MgOauth2PermissionGrant - $result = Initialize-TkAppSpRegistration -AppRegistration $AppRegistration -RequiredResourceAccessList $RequiredResourceAccessList -Context $Context -AuthMethod 'Certificate' -CertThumbprint $CertThumbprint + $result = New-TkAppSpOauth2Registration -AppRegistration $AppRegistration -RequiredResourceAccessList $RequiredResourceAccessList -Context $Context -AuthMethod 'Certificate' -CertThumbprint $CertThumbprint Assert-MockCalled -CommandName New-MgOauth2PermissionGrant -Times 1 $result | Should -Be "https://login.microsoftonline.com/test-tenant-id/adminconsent?client_id=test-app-id" } From e3083bf365fbc9ee5dc6960675c99726eea4d1f3 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sat, 15 Mar 2025 16:42:20 -0500 Subject: [PATCH 02/15] removed: msal in favor of api call --- source/Private/Get-TkExistingCert.ps1 | 7 + source/Private/Get-TkMsalToken.ps1 | 146 +++++++++++++++++++ source/Private/Set-TkJsonSecret.ps1 | 1 - source/Public/Send-TkEmailAppMessage.ps1 | 14 +- tests/Unit/Private/Get-TkMsalToken.tests.ps1 | 140 ++++++++++++++++++ 5 files changed, 304 insertions(+), 4 deletions(-) create mode 100644 source/Private/Get-TkMsalToken.ps1 create mode 100644 tests/Unit/Private/Get-TkMsalToken.tests.ps1 diff --git a/source/Private/Get-TkExistingCert.ps1 b/source/Private/Get-TkExistingCert.ps1 index 1c2f01d..ad939a5 100644 --- a/source/Private/Get-TkExistingCert.ps1 +++ b/source/Private/Get-TkExistingCert.ps1 @@ -21,6 +21,12 @@ function Get-TkExistingCert { [Parameter(Mandatory = $true)] [string]$CertName ) + if (-not $script:LogString) { + Write-AuditLog -Start + } + else { + Write-AuditLog -BeginFunction + } $ExistingCert = Get-ChildItem -Path Cert:\CurrentUser\My -ErrorAction SilentlyContinue | Where-Object { $_.Subject -eq $CertName } -ErrorAction SilentlyContinue if ( $ExistingCert) { @@ -47,4 +53,5 @@ function Get-TkExistingCert { else { Write-AuditLog "Certificate with subject '$CertName' does not exist in the certificate store. Continuing..." } + Write-AuditLog -EndFunction } \ No newline at end of file diff --git a/source/Private/Get-TkMsalToken.ps1 b/source/Private/Get-TkMsalToken.ps1 new file mode 100644 index 0000000..19175cc --- /dev/null +++ b/source/Private/Get-TkMsalToken.ps1 @@ -0,0 +1,146 @@ +<# + .SYNOPSIS + Retrieves an OAuth 2.0 token using a client certificate for authentication. + .DESCRIPTION + The Get-TkMsalToken function generates a JSON Web Token (JWT) signed with a provided X.509 certificate and uses it to request an OAuth 2.0 token from Azure Active Directory (AAD). The token can be used to authenticate API requests to services like Microsoft Graph. + .PARAMETER ClientCertificate + The X.509 certificate used for authentication. Example: + $ClientCertificate = Get-Item Cert:\CurrentUser\My\ + .PARAMETER ClientId + The Azure AD application (client) ID. Must be a valid GUID. + .PARAMETER TenantId + The Azure AD tenant ID (GUID). Example: 12345678-1234-1234-1234-1234567890ab. + .PARAMETER Scope + The API scope for token access. Default is Microsoft Graph. Must be a valid URL. + .PARAMETER AuthorityType + The authority type to use for authentication. Valid values are 'Global', 'AzureGov', and 'China'. Default is 'Global'. + .EXAMPLE + $ClientCertificate = Get-Item Cert:\CurrentUser\My\ + $ClientId = "your-client-id" + $TenantId = "your-tenant-id" + $Token = Get-TkMsalToken -ClientCertificate $ClientCertificate -ClientId $ClientId -TenantId $TenantId + .NOTES + This function requires the 'Invoke-RestMethod' cmdlet to be available. + The function logs the token expiration time using a custom Write-AuditLog function. +#> +function Get-TkMsalToken { + [CmdletBinding()] + param ( + [Parameter( + Mandatory = $true, + HelpMessage = ` + "The X.509 certificate used for authentication. Example: `n`$ClientCertificate = Get-Item Cert:\CurrentUser\My\" + )] + [System.Security.Cryptography.X509Certificates.X509Certificate2] + $ClientCertificate, + # + [Parameter( + Mandatory = $true, + HelpMessage = ` + 'The Azure AD application (client) ID.' + )] + [ValidatePattern('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')] + [string] + $ClientId, + # + [Parameter( + Mandatory = $true, + HelpMessage = ` + 'The Azure AD tenant ID (GUID). Example: 12345678-1234-1234-1234-1234567890ab.')] + [ValidatePattern('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')] + [string] + $TenantId, + # + [Parameter( + HelpMessage = ` + 'The API scope for token access. Default is Microsoft Graph.' + )] + [ValidatePattern('^https:\/\/[a-zA-Z0-9.-]+\/[a-zA-Z0-9.-]+$')] + [string] + $Scope = 'https://graph.microsoft.com/.default', + # + [Parameter( + Mandatory = $false, + HelpMessage = ` + 'The authority type to use for authentication.' + )] + [ValidateSet('Global', 'AzureGov', 'China')] # Removed invalid options + [string] + $AuthorityType = 'Global' + ) + begin { + if (-not $script:LogString) { + Write-AuditLog -Start + } + else { + Write-AuditLog -BeginFunction + } + # Define Authority URL based on the chosen AuthorityType + switch ($AuthorityType) { + 'Global' { $Authority = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token" } + 'AzureGov' { $Authority = "https://login.microsoftonline.us/$TenantId/oauth2/v2.0/token" } + 'China' { $Authority = "https://login.chinacloudapi.cn/$TenantId/oauth2/v2.0/token" } + } + # Generate JWT Header + $JwtHeader = @{ + alg = 'RS256' + typ = 'JWT' + x5t = [Convert]::ToBase64String($ClientCertificate.GetCertHash()) -replace '\+', '-' -replace '/', '_' -replace '=' + } + # Generate Unix timestamps for `iat` and `exp` + $IatTime = [int](Get-Date (Get-Date).ToUniversalTime() -UFormat %s) + $ExpTime = $IatTime + 600 # JWT Expiration time (10 minutes from `iat`) + # Generate JWT Payload + $JwtPayload = @{ + aud = $Authority + exp = $ExpTime + iat = $IatTime + iss = $ClientId + sub = $ClientId + jti = [guid]::NewGuid().ToString() + } + } + process { + # Convert JWT Header & Payload to Base64URL Encoding + $Base64UrlEncode = { param ($String) [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($String)) -replace '\+', '-' -replace '/', '_' -replace '=' } + $JwtHeaderEncoded = &$Base64UrlEncode (ConvertTo-Json $JwtHeader -Compress) + $JwtPayloadEncoded = &$Base64UrlEncode (ConvertTo-Json $JwtPayload -Compress) + # Combine Header & Payload + $JwtToSign = "$JwtHeaderEncoded.$JwtPayloadEncoded" + # Sign JWT with Certificate Private Key + $Csp = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($ClientCertificate) + $Signature = [Convert]::ToBase64String( + $Csp.SignData( + [System.Text.Encoding]::UTF8.GetBytes($JwtToSign), + [System.Security.Cryptography.HashAlgorithmName]::SHA256, + [System.Security.Cryptography.RSASignaturePadding]::Pkcs1 + ) + ) -replace '\+', '-' -replace '/', '_' -replace '=' + $ClientAssertion = "$JwtToSign.$Signature" + # Prepare Token Request + $Body = @{ + client_id = $ClientId + client_assertion = $ClientAssertion + client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer' + grant_type = 'client_credentials' + scope = $Scope + } + } + end { + try { + # Get Token Using Invoke-RestMethod + $Response = Invoke-RestMethod -Method Post -Uri $Authority -ContentType 'application/x-www-form-urlencoded' -Body $Body -ErrorAction Stop + $tokenExpires = [System.TimeSpan]::FromSeconds($Response.expires_in) + $tokenExpMinutes = $tokenExpires.Minutes + $tokenExpTime = (Get-Date).AddMinutes($tokenExpMinutes) + Write-AuditLog "The token expires today $($tokenExpTime.ToShortDateString()) in $tokenExpMinutes minutes at $($tokenExpTime.ToShortTimeString())." + Write-AuditLog -EndFunction + return $Response.access_token + } + catch { + Write-Error "Failed to obtain token: $_" + return $null + } + } +} + diff --git a/source/Private/Set-TkJsonSecret.ps1 b/source/Private/Set-TkJsonSecret.ps1 index f45db74..70cbc55 100644 --- a/source/Private/Set-TkJsonSecret.ps1 +++ b/source/Private/Set-TkJsonSecret.ps1 @@ -101,4 +101,3 @@ function Set-TkJsonSecret { throw } } -$WarningPreference = 'Continue' diff --git a/source/Public/Send-TkEmailAppMessage.ps1 b/source/Public/Send-TkEmailAppMessage.ps1 index 56391a9..3697051 100644 --- a/source/Public/Send-TkEmailAppMessage.ps1 +++ b/source/Public/Send-TkEmailAppMessage.ps1 @@ -185,7 +185,7 @@ function Send-TkEmailAppMessage { Write-AuditLog '###########################################################' # Install and import the Microsoft.Graph module. Tested: 1.22.0 $PublicMods = ` - 'Microsoft.PowerShell.SecretManagement', 'SecretManagement.JustinGrote.CredMan', 'MSAL.PS' + 'Microsoft.PowerShell.SecretManagement', 'SecretManagement.JustinGrote.CredMan' $PublicVers = ` '1.1.2', '1.0.0', '4.37.0.0' $params1 = @{ @@ -240,14 +240,22 @@ function Send-TkEmailAppMessage { Write-AuditLog "$CertThumbprint" } # End Region Begin Process { + # https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow#second-case-access-token-request-with-a-certificate # Authenticate with Azure AD and obtain an access token for the Microsoft Graph API using the certificate - $MSToken = Get-MsalToken ` + $MSToken = Get-TkMsalToken ` + -ClientCertificate $Cert ` + -ClientId $AppId ` + -TenantId $Tenant ` + -ErrorAction Stop + <# + $MSToken = Get-MsalToken ` -ClientCertificate $Cert ` -ClientId $AppId ` -Authority "https://login.microsoftonline.com/$Tenant/oauth2/v2.0/token" ` -ErrorAction Stop + #> # Set up the request headers - $authHeader = @{Authorization = "Bearer $($MSToken.AccessToken)" } + $authHeader = @{Authorization = "Bearer $($MSToken)" } # Set up the request URL $url = "https://graph.microsoft.com/v1.0/users/$($FromAddress)/sendMail" # Build the message body diff --git a/tests/Unit/Private/Get-TkMsalToken.tests.ps1 b/tests/Unit/Private/Get-TkMsalToken.tests.ps1 new file mode 100644 index 0000000..0f07ebd --- /dev/null +++ b/tests/Unit/Private/Get-TkMsalToken.tests.ps1 @@ -0,0 +1,140 @@ +$ProjectPath = "$PSScriptRoot\..\..\.." | Convert-Path +$ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{ + ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and + $(try { Test-ModuleManifest $_.FullName -ErrorAction Stop } catch { $false } ) + }).BaseName + + +Import-Module $ProjectName + +InModuleScope $ProjectName { + Describe "Get-TkMsalToken" { + Context "When called with valid parameters" { + It "Should return a valid token" { + # Arrange + $ClientCertificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 + $ClientId = "12345678-1234-1234-1234-1234567890ab" + $TenantId = "12345678-1234-1234-1234-1234567890ab" + $Scope = "https://graph.microsoft.com/.default" + $AuthorityType = "Global" + + # Mock Invoke-RestMethod to return a fake token response + Mock -CommandName Invoke-RestMethod -MockWith { + @{ + access_token = "fake_token" + expires_in = 3600 + } + } + + # Act + $Token = Get-TkMsalToken -ClientCertificate $ClientCertificate -ClientId $ClientId -TenantId $TenantId -Scope $Scope -AuthorityType $AuthorityType + + # Assert + $Token | Should -Be "fake_token" + } + } + + Context "When called with invalid parameters" { + It "Should throw an error for invalid ClientId" { + # Arrange + $ClientCertificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 + $ClientId = "invalid-client-id" + $TenantId = "12345678-1234-1234-1234-1234567890ab" + $Scope = "https://graph.microsoft.com/.default" + $AuthorityType = "Global" + + # Act & Assert + { Get-TkMsalToken -ClientCertificate $ClientCertificate -ClientId $ClientId -TenantId $TenantId -Scope $Scope -AuthorityType $AuthorityType } | Should -Throw + } + + It "Should throw an error for invalid TenantId" { + # Arrange + $ClientCertificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 + $ClientId = "12345678-1234-1234-1234-1234567890ab" + $TenantId = "invalid-tenant-id" + $Scope = "https://graph.microsoft.com/.default" + $AuthorityType = "Global" + + # Act & Assert + { Get-TkMsalToken -ClientCertificate $ClientCertificate -ClientId $ClientId -TenantId $TenantId -Scope $Scope -AuthorityType $AuthorityType } | Should -Throw + } + } + + Context "When called with different AuthorityTypes" { + It "Should use the correct authority URL for Global" { + # Arrange + $ClientCertificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 + $ClientId = "12345678-1234-1234-1234-1234567890ab" + $TenantId = "12345678-1234-1234-1234-1234567890ab" + $Scope = "https://graph.microsoft.com/.default" + $AuthorityType = "Global" + + # Mock Invoke-RestMethod to return a fake token response + Mock -CommandName Invoke-RestMethod -MockWith { + @{ + access_token = "fake_token" + expires_in = 3600 + } + } + + # Act + $Token = Get-TkMsalToken -ClientCertificate $ClientCertificate -ClientId $ClientId -TenantId $TenantId -Scope $Scope -AuthorityType $AuthorityType + + # Assert + Assert-MockCalled -CommandName Invoke-RestMethod -Exactly 1 -Scope It -ParameterFilter { + $_.Uri -eq "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token" + } + } + + It "Should use the correct authority URL for AzureGov" { + # Arrange + $ClientCertificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 + $ClientId = "12345678-1234-1234-1234-1234567890ab" + $TenantId = "12345678-1234-1234-1234-1234567890ab" + $Scope = "https://graph.microsoft.com/.default" + $AuthorityType = "AzureGov" + + # Mock Invoke-RestMethod to return a fake token response + Mock -CommandName Invoke-RestMethod -MockWith { + @{ + access_token = "fake_token" + expires_in = 3600 + } + } + + # Act + $Token = Get-TkMsalToken -ClientCertificate $ClientCertificate -ClientId $ClientId -TenantId $TenantId -Scope $Scope -AuthorityType $AuthorityType + + # Assert + Assert-MockCalled -CommandName Invoke-RestMethod -Exactly 1 -Scope It -ParameterFilter { + $_.Uri -eq "https://login.microsoftonline.us/$TenantId/oauth2/v2.0/token" + } + } + + It "Should use the correct authority URL for China" { + # Arrange + $ClientCertificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 + $ClientId = "12345678-1234-1234-1234-1234567890ab" + $TenantId = "12345678-1234-1234-1234-1234567890ab" + $Scope = "https://graph.microsoft.com/.default" + $AuthorityType = "China" + + # Mock Invoke-RestMethod to return a fake token response + Mock -CommandName Invoke-RestMethod -MockWith { + @{ + access_token = "fake_token" + expires_in = 3600 + } + } + + # Act + $Token = Get-TkMsalToken -ClientCertificate $ClientCertificate -ClientId $ClientId -TenantId $TenantId -Scope $Scope -AuthorityType $AuthorityType + + # Assert + Assert-MockCalled -CommandName Invoke-RestMethod -Exactly 1 -Scope It -ParameterFilter { + $_.Uri -eq "https://login.chinacloudapi.cn/$TenantId/oauth2/v2.0/token" + } + } + } + } +} \ No newline at end of file From e0efc543570b2f676a7436e7983c824d894f92b2 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sat, 15 Mar 2025 17:08:05 -0500 Subject: [PATCH 03/15] fix: formatting --- source/Private/Initialize-TkAppName.ps1 | 4 +- .../Initialize-TkEmailAppParamsObject.ps1 | 10 +-- .../Public/Publish-TkMemPolicyManagerApp.ps1 | 2 +- .../Private/Connect-TkMsService.tests.ps1 | 69 ++++++++----------- 4 files changed, 38 insertions(+), 47 deletions(-) diff --git a/source/Private/Initialize-TkAppName.ps1 b/source/Private/Initialize-TkAppName.ps1 index 1180f66..33ab18b 100644 --- a/source/Private/Initialize-TkAppName.ps1 +++ b/source/Private/Initialize-TkAppName.ps1 @@ -7,9 +7,11 @@ .PARAMETER Prefix A short prefix for your app name (2-4 alphanumeric characters). This parameter is mandatory. .PARAMETER ScenarioName - An optional scenario name to include in the app name (e.g., AuditGraphEmail, MemPolicy, etc.). Defaults to "GraphApp". + An optional scenario name to include in the app name (e.g., AuditGraphEmail, MemPolicy, etc.). Defaults to "TkEmailApp". .PARAMETER UserId An optional user email to append an "As-[username]" suffix to the app name. The email must be in a valid format. + .PARAMETER DoNotUseDomainSuffix + A switch to add session domain suffix to the app name. If not specified, the domain suffix is derived from USERDNSDOMAIN. .EXAMPLE PS> Initialize-TkAppName -Prefix "MSN" Generates an app name with the prefix "MSN" and default scenario name "GraphApp". diff --git a/source/Private/Initialize-TkEmailAppParamsObject.ps1 b/source/Private/Initialize-TkEmailAppParamsObject.ps1 index c58b05e..e7d08dc 100644 --- a/source/Private/Initialize-TkEmailAppParamsObject.ps1 +++ b/source/Private/Initialize-TkEmailAppParamsObject.ps1 @@ -4,13 +4,13 @@ .DESCRIPTION The Initialize-TkEmailAppParamsObject function creates and returns a new instance of the TkEmailAppParams class using the provided parameters. This function ensures that all necessary parameters are provided and initializes the object accordingly. .PARAMETER AppId - The application ID used to identify the email application. + The application ID used to uniquely identify the email application. .PARAMETER Id - The unique identifier for the email application instance. + The unique identifier for the specific email application instance. .PARAMETER AppName The name of the email application being initialized. - .PARAMETER ClientCertName - The name of the client certificate used by the email application. + .PARAMETER CertificateSubject + The subject name of the client certificate used by the email application. .PARAMETER AppRestrictedSendGroup The group that is restricted from sending emails within the application. .PARAMETER CertExpires @@ -31,7 +31,7 @@ [TkEmailAppParams] Returns a new instance of the TkEmailAppParams class initialized with the provided parameters. .EXAMPLE - $tkEmailAppParams = Initialize-TkEmailAppParamsObject -AppId "12345" -Id "67890" -AppName "MyEmailApp" -AppRestrictedSendGroup "RestrictedGroup" -CertExpires "2023-12-31" -CertThumbprint "ABCDEF123456" -ConsentUrl "https://consent.url" -DefaultDomain "example.com" -SendAsUser "user@example.com" -SendAsUserEmail "user@example.com" -TenantID "tenant123" + $tkEmailAppParams = Initialize-TkEmailAppParamsObject -AppId "12345" -Id "67890" -AppName "MyEmailApp" -CertificateSubject "CN=MyCert" -AppRestrictedSendGroup "RestrictedGroup" -CertExpires "2023-12-31" -CertThumbprint "ABCDEF123456" -ConsentUrl "https://consent.url" -DefaultDomain "example.com" -SendAsUser "user@example.com" -SendAsUserEmail "user@example.com" -TenantID "tenant123" This example initializes a TkEmailAppParams object with the specified parameters. #> diff --git a/source/Public/Publish-TkMemPolicyManagerApp.ps1 b/source/Public/Publish-TkMemPolicyManagerApp.ps1 index ea2e0c1..02593d2 100644 --- a/source/Public/Publish-TkMemPolicyManagerApp.ps1 +++ b/source/Public/Publish-TkMemPolicyManagerApp.ps1 @@ -1,4 +1,4 @@ -<# +<# .SYNOPSIS Publishes a new MEM (Intune) Policy Manager App in Azure AD with read-only or read-write permissions. .DESCRIPTION diff --git a/tests/Unit/Private/Connect-TkMsService.tests.ps1 b/tests/Unit/Private/Connect-TkMsService.tests.ps1 index 28e6fba..d7946c8 100644 --- a/tests/Unit/Private/Connect-TkMsService.tests.ps1 +++ b/tests/Unit/Private/Connect-TkMsService.tests.ps1 @@ -8,82 +8,71 @@ Import-Module $ProjectName InModuleScope $ProjectName { Describe 'Connect-TkMsService' { - - # -- Mock your own function from your module: - Mock -CommandName 'Write-AuditLog' -ModuleName 'GraphAppToolkit' - - # -- Microsoft.Graph commands: - Mock -CommandName 'Get-MgUser' -ModuleName 'Microsoft.Graph' - Mock -CommandName 'Get-MgContext' -ModuleName 'Microsoft.Graph' - Mock -CommandName 'Connect-MgGraph' -ModuleName 'Microsoft.Graph' - Mock -CommandName 'Remove-MgContext' -ModuleName 'Microsoft.Graph' - Mock -CommandName 'Get-MgOrganization' -ModuleName 'Microsoft.Graph' - - # -- ExchangeOnlineManagement commands: - Mock -CommandName 'Get-OrganizationConfig' -ModuleName 'ExchangeOnlineManagement' - Mock -CommandName 'Connect-ExchangeOnline' -ModuleName 'ExchangeOnlineManagement' - Mock -CommandName 'Disconnect-ExchangeOnline' -ModuleName 'ExchangeOnlineManagement' + BeforeAll { + # Mocks are now set up once for the entire Describe block + Mock -CommandName 'Write-AuditLog' -ModuleName GraphAppToolkit + Mock -CommandName 'Get-MgUser' -ModuleName GraphAppToolkit + Mock -CommandName 'Get-MgContext' -ModuleName GraphAppToolkit + Mock -CommandName 'Get-MgOrganization' -ModuleName GraphAppToolkit + Mock -CommandName 'Remove-MgContext' -ModuleName GraphAppToolkit + Mock -CommandName 'Connect-MgGraph' -ModuleName GraphAppToolkit + Mock -CommandName 'Get-OrganizationConfig' -ModuleName GraphAppToolkit + Mock -CommandName 'Disconnect-ExchangeOnline' -ModuleName GraphAppToolkit + Mock -CommandName 'Connect-ExchangeOnline' -ModuleName GraphAppToolkit + } Context 'When connecting to Microsoft Graph' { - It 'Should connect to Microsoft Graph with specified scopes' { $params = @{ - MgGraph = $true + MgGraph = $true GraphAuthScopes = @('User.Read', 'Mail.Read') - Confirm = $false } + Connect-TkMsService @params - # Notice the -ModuleName arguments below, matching the mocks - Assert-MockCalled -CommandName 'Connect-MgGraph' -ModuleName 'Microsoft.Graph' -Times 1 - Assert-MockCalled -CommandName 'Write-AuditLog' -ModuleName 'GraphAppToolkit' -Times 1 ` - -ParameterFilter { $_ -eq 'Connected to Microsoft Graph.' } + Assert-MockCalled -CommandName 'Connect-MgGraph' -ModuleName GraphAppToolkit -Exactly -Times 1 + Assert-MockCalled -CommandName 'Write-AuditLog' -ModuleName GraphAppToolkit -Exactly -Times 1 -Scope It -ParameterFilter { $_ -eq 'Connected to Microsoft Graph.' } } It 'Should reuse existing Microsoft Graph session if valid' { - Mock -CommandName 'Get-MgUser' -ModuleName 'Microsoft.Graph' -MockWith { $null } - Mock -CommandName 'Get-MgContext' -ModuleName 'Microsoft.Graph' -MockWith { @{ Scopes = @('User.Read', 'Mail.Read') } } + Mock -CommandName 'Get-MgUser' -ModuleName GraphAppToolkit -MockWith { } + Mock -CommandName 'Get-MgContext' -ModuleName GraphAppToolkit -MockWith { @{ Scopes = @('User.Read', 'Mail.Read') } } $params = @{ - MgGraph = $true + MgGraph = $true GraphAuthScopes = @('User.Read', 'Mail.Read') - Confirm = $false } + Connect-TkMsService @params - Assert-MockCalled -CommandName 'Get-MgUser' -ModuleName 'Microsoft.Graph' -Times 1 - Assert-MockCalled -CommandName 'Write-AuditLog' -ModuleName 'GraphAppToolkit' -Times 1 ` - -ParameterFilter { $_ -eq 'An active Microsoft Graph session is detected and all required scopes are present.' } + Assert-MockCalled -CommandName 'Get-MgUser' -ModuleName GraphAppToolkit -Exactly -Times 1 + Assert-MockCalled -CommandName 'Write-AuditLog' -ModuleName GraphAppToolkit -Exactly -Times 1 -Scope It -ParameterFilter { $_ -eq 'Using existing Microsoft Graph session.' } } } Context 'When connecting to Exchange Online' { - It 'Should connect to Exchange Online' { $params = @{ ExchangeOnline = $true - Confirm = $false } + Connect-TkMsService @params - Assert-MockCalled -CommandName 'Connect-ExchangeOnline' -ModuleName 'ExchangeOnlineManagement' -Times 1 - Assert-MockCalled -CommandName 'Write-AuditLog' -ModuleName 'GraphAppToolkit' -Times 1 ` - -ParameterFilter { $_ -eq 'Connected to Exchange Online.' } + Assert-MockCalled -CommandName 'Connect-ExchangeOnline' -ModuleName GraphAppToolkit -Exactly -Times 1 + Assert-MockCalled -CommandName 'Write-AuditLog' -ModuleName GraphAppToolkit -Exactly -Times 1 -Scope It -ParameterFilter { $_ -eq 'Connected to Exchange Online.' } } It 'Should reuse existing Exchange Online session if valid' { - # Provide a mock for Get-OrganizationConfig from ExchangeOnlineManagement - Mock -CommandName 'Get-OrganizationConfig' -ModuleName 'ExchangeOnlineManagement' -MockWith { @{ Identity = 'TestOrg' } } + Mock -CommandName 'Get-OrganizationConfig' -ModuleName GraphAppToolkit -MockWith { } $params = @{ ExchangeOnline = $true - Confirm = $false } + Connect-TkMsService @params - Assert-MockCalled -CommandName 'Get-OrganizationConfig' -ModuleName 'ExchangeOnlineManagement' -Times 1 - Assert-MockCalled -CommandName 'Write-AuditLog' -ModuleName 'GraphAppToolkit' -Times 1 ` - -ParameterFilter { $_ -eq 'An active Exchange Online session is detected.' } + Assert-MockCalled -CommandName 'Get-OrganizationConfig' -ModuleName GraphAppToolkit -Exactly -Times 1 + Assert-MockCalled -CommandName 'Write-AuditLog' -ModuleName GraphAppToolkit -Exactly -Times 1 -Scope It -ParameterFilter { $_ -eq 'Using existing Exchange Online session.' } } } } From 9e342e63d95cdf6ec1a55fc1f29b77c708300d88 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sat, 15 Mar 2025 17:09:46 -0500 Subject: [PATCH 04/15] docs: Update CHANGELOG --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a28ee24..5619d65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Added Get-TkMsalToken cmdlet to retrieve an MSAL token using API calls. + ### Fixed -- Fixed authentication context for mggraph. +- Fixed authentication context for MgGraph. ### Changed - Updated private function names to be more descriptive. +- Removed MSAL.PS dependency from Send-TkEmailAppMessage function. ## [0.2.0] - 2025-03-14 From 386aca2d4649b3948dc08995e605b404ab616fb3 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sat, 15 Mar 2025 17:25:56 -0500 Subject: [PATCH 05/15] docs: Update Help Docs --- README.md | 10 +- README2.md | 10 +- help/GraphAppToolkit.md | 1 - help/New-MailEnabledSendingGroup.md | 60 +-- help/Publish-TkEmailApp.md | 78 +-- help/Publish-TkM365AuditApp.md | 56 +-- help/Publish-TkMemPolicyManagerApp.md | 60 +-- help/Send-TkEmailAppMessage.md | 106 ++-- source/en-US/GraphAppToolkit-help.xml | 680 +++++++++++++------------- 9 files changed, 530 insertions(+), 531 deletions(-) diff --git a/README.md b/README.md index c7762b4..d9a7556 100644 --- a/README.md +++ b/README.md @@ -531,16 +531,16 @@ Send-TkEmailAppMessage -AppId -TenantId -CertThumbprint AppName | | \\[Vault Parameter Set Only\\] The name of the pre-created Microsoft Graph Email App \(stored in GraphEmailAppLocalStore\\). Used only if the 'Vault' parameter set is chosen. The function retrieves the AppId, TenantId, and certificate thumbprint from the vault entry. | true | false | | -| AppId | | \\[Manual Parameter Set Only\\] The Azure AD application \(client\\) ID to use for sending the email. Must be used together with TenantId and CertThumbprint in the 'Manual' parameter set. | true | false | | -| TenantId | | \\[Manual Parameter Set Only\\] The Azure AD tenant ID \(GUID or domain name\\). Must be used together with AppId and CertThumbprint in the 'Manual' parameter set. | true | false | | -| CertThumbprint | | \\[Manual Parameter Set Only\\] The certificate thumbprint \(in Cert:\\CurrentUser\\My\\) used for authenticating as the Azure AD app. Must be used together with AppId and TenantId in the 'Manual' parameter set. | true | false | | +| AppName | | \[Vault Parameter Set Only\] The name of the pre-created Microsoft Graph Email App \(stored in GraphEmailAppLocalStore\\). Used only if the 'Vault' parameter set is chosen. The function retrieves the AppId, TenantId, and certificate thumbprint from the vault entry. | true | false | | +| AppId | | \[Manual Parameter Set Only\] The Azure AD application \(client\\) ID to use for sending the email. Must be used together with TenantId and CertThumbprint in the 'Manual' parameter set. | true | false | | +| TenantId | | \[Manual Parameter Set Only\] The Azure AD tenant ID \(GUID or domain name\\). Must be used together with AppId and CertThumbprint in the 'Manual' parameter set. | true | false | | +| CertThumbprint | | \[Manual Parameter Set Only\] The certificate thumbprint \(in Cert:\\CurrentUser\\My\\) used for authenticating as the Azure AD app. Must be used together with AppId and TenantId in the 'Manual' parameter set. | true | false | | | To | | The email address of the recipient. | true | false | | | FromAddress | | The email address of the sender who is authorized to send email as configured in the Graph Email App. | true | false | | | Subject | | The subject line of the email. | true | false | | | EmailBody | | The body text of the email. | true | false | | | AttachmentPath | | An array of file paths for any attachments to include in the email. Each path must exist as a leaf file. | false | false | | -| VaultName | | \\[Vault Parameter Set Only\\] The name of the vault to retrieve the GraphEmailApp object. Default is 'GraphEmailAppLocalStore'. | false | false | GraphEmailAppLocalStore | +| VaultName | | \[Vault Parameter Set Only\] The name of the vault to retrieve the GraphEmailApp object. Default is 'GraphEmailAppLocalStore'. | false | false | GraphEmailAppLocalStore | | WhatIf | wi | | false | false | | | Confirm | cf | | false | false | | ### Note diff --git a/README2.md b/README2.md index 22ee117..fcf6fc1 100644 --- a/README2.md +++ b/README2.md @@ -270,16 +270,16 @@ Send-TkEmailAppMessage -AppId -TenantId -CertThumbprint AppName | | \\[Vault Parameter Set Only\\] The name of the pre-created Microsoft Graph Email App \(stored in GraphEmailAppLocalStore\\). Used only if the 'Vault' parameter set is chosen. The function retrieves the AppId, TenantId, and certificate thumbprint from the vault entry. | true | false | | -| AppId | | \\[Manual Parameter Set Only\\] The Azure AD application \(client\\) ID to use for sending the email. Must be used together with TenantId and CertThumbprint in the 'Manual' parameter set. | true | false | | -| TenantId | | \\[Manual Parameter Set Only\\] The Azure AD tenant ID \(GUID or domain name\\). Must be used together with AppId and CertThumbprint in the 'Manual' parameter set. | true | false | | -| CertThumbprint | | \\[Manual Parameter Set Only\\] The certificate thumbprint \(in Cert:\\CurrentUser\\My\\) used for authenticating as the Azure AD app. Must be used together with AppId and TenantId in the 'Manual' parameter set. | true | false | | +| AppName | | \[Vault Parameter Set Only\] The name of the pre-created Microsoft Graph Email App \(stored in GraphEmailAppLocalStore\\). Used only if the 'Vault' parameter set is chosen. The function retrieves the AppId, TenantId, and certificate thumbprint from the vault entry. | true | false | | +| AppId | | \[Manual Parameter Set Only\] The Azure AD application \(client\\) ID to use for sending the email. Must be used together with TenantId and CertThumbprint in the 'Manual' parameter set. | true | false | | +| TenantId | | \[Manual Parameter Set Only\] The Azure AD tenant ID \(GUID or domain name\\). Must be used together with AppId and CertThumbprint in the 'Manual' parameter set. | true | false | | +| CertThumbprint | | \[Manual Parameter Set Only\] The certificate thumbprint \(in Cert:\\CurrentUser\\My\\) used for authenticating as the Azure AD app. Must be used together with AppId and TenantId in the 'Manual' parameter set. | true | false | | | To | | The email address of the recipient. | true | false | | | FromAddress | | The email address of the sender who is authorized to send email as configured in the Graph Email App. | true | false | | | Subject | | The subject line of the email. | true | false | | | EmailBody | | The body text of the email. | true | false | | | AttachmentPath | | An array of file paths for any attachments to include in the email. Each path must exist as a leaf file. | false | false | | -| VaultName | | \\[Vault Parameter Set Only\\] The name of the vault to retrieve the GraphEmailApp object. Default is 'GraphEmailAppLocalStore'. | false | false | GraphEmailAppLocalStore | +| VaultName | | \[Vault Parameter Set Only\] The name of the vault to retrieve the GraphEmailApp object. Default is 'GraphEmailAppLocalStore'. | false | false | GraphEmailAppLocalStore | | WhatIf | wi | | false | false | | | Confirm | cf | | false | false | | ### Note diff --git a/help/GraphAppToolkit.md b/help/GraphAppToolkit.md index 8ae9d25..6cb3b94 100644 --- a/help/GraphAppToolkit.md +++ b/help/GraphAppToolkit.md @@ -26,4 +26,3 @@ Publishes a new MEM (Intune) Policy Manager App in Azure AD with read-only or re ### [Send-TkEmailAppMessage](Send-TkEmailAppMessage) Sends an email using the Microsoft Graph API, either by retrieving app credentials from a local vault or by specifying them manually. - diff --git a/help/New-MailEnabledSendingGroup.md b/help/New-MailEnabledSendingGroup.md index 3b23b05..d1065f1 100644 --- a/help/New-MailEnabledSendingGroup.md +++ b/help/New-MailEnabledSendingGroup.md @@ -1,4 +1,4 @@ ---- +--- external help file: GraphAppToolkit-help.xml Module Name: GraphAppToolkit online version: @@ -56,47 +56,47 @@ and a primary SMTP address of Senders@customdomain.org. ## PARAMETERS -### -Name -The name of the mail-enabled security group to create or retrieve. -This is also used as -the alias if no separate Alias parameter is provided. +### -Alias +An optional alias for the group. +If omitted, the group name is used as the alias. ```yaml Type: String Parameter Sets: (All) Aliases: -Required: True +Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` -### -Alias -An optional alias for the group. -If omitted, the group name is used as the alias. +### -DefaultDomain +(DefaultDomain parameter set) The domain portion to be appended to the group alias (e.g. +"Alias@DefaultDomain"). +This parameter is mandatory when using the 'DefaultDomain' parameter set. ```yaml Type: String -Parameter Sets: (All) +Parameter Sets: DefaultDomain Aliases: -Required: False +Required: True Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` -### -PrimarySmtpAddress -(CustomDomain parameter set) The full SMTP address for the group (e.g. -"MyGroup@contoso.com"). -This parameter is mandatory when using the 'CustomDomain' parameter set. +### -Name +The name of the mail-enabled security group to create or retrieve. +This is also used as +the alias if no separate Alias parameter is provided. ```yaml Type: String -Parameter Sets: CustomDomain +Parameter Sets: (All) Aliases: Required: True @@ -106,14 +106,14 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -DefaultDomain -(DefaultDomain parameter set) The domain portion to be appended to the group alias (e.g. -"Alias@DefaultDomain"). -This parameter is mandatory when using the 'DefaultDomain' parameter set. +### -PrimarySmtpAddress +(CustomDomain parameter set) The full SMTP address for the group (e.g. +"MyGroup@contoso.com"). +This parameter is mandatory when using the 'CustomDomain' parameter set. ```yaml Type: String -Parameter Sets: DefaultDomain +Parameter Sets: CustomDomain Aliases: Required: True @@ -123,14 +123,13 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -WhatIf -Shows what would happen if the cmdlet runs. -The cmdlet is not run. +### -ProgressAction +{{ Fill ProgressAction Description }} ```yaml -Type: SwitchParameter +Type: ActionPreference Parameter Sets: (All) -Aliases: wi +Aliases: proga Required: False Position: Named @@ -154,13 +153,14 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -ProgressAction -{{ Fill ProgressAction Description }} +### -WhatIf +Shows what would happen if the cmdlet runs. +The cmdlet is not run. ```yaml -Type: ActionPreference +Type: SwitchParameter Parameter Sets: (All) -Aliases: proga +Aliases: wi Required: False Position: Named diff --git a/help/Publish-TkEmailApp.md b/help/Publish-TkEmailApp.md index 2705c74..9fe16bf 100644 --- a/help/Publish-TkEmailApp.md +++ b/help/Publish-TkEmailApp.md @@ -1,4 +1,4 @@ ---- +--- external help file: GraphAppToolkit-help.xml Module Name: GraphAppToolkit online version: @@ -162,26 +162,21 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -MailEnabledSendingGroup -The mail-enabled security group. -Must be a valid email address. +### -CertPrefix +Prefix to add to the certificate subject for the existing app. ```yaml Type: String Parameter Sets: CreateNewApp Aliases: -Required: True +Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` -### -ExistingAppObjectId -The AppId of the existing App Registration to which you want to attach a certificate. -Must be a valid GUID. - ```yaml Type: String Parameter Sets: UseExistingApp @@ -194,12 +189,13 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -CertPrefix -Prefix to add to the certificate subject for the existing app. +### -CertThumbprint +The thumbprint of the certificate to be retrieved. +Must be a valid 40-character hexadecimal string. ```yaml Type: String -Parameter Sets: CreateNewApp +Parameter Sets: (All) Aliases: Required: False @@ -209,28 +205,31 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -DoNotUseDomainSuffix +Switch to add session domain suffix to the app name. + ```yaml -Type: String -Parameter Sets: UseExistingApp +Type: SwitchParameter +Parameter Sets: (All) Aliases: -Required: True +Required: False Position: Named -Default value: None +Default value: False Accept pipeline input: False Accept wildcard characters: False ``` -### -CertThumbprint -The thumbprint of the certificate to be retrieved. -Must be a valid 40-character hexadecimal string. +### -ExistingAppObjectId +The AppId of the existing App Registration to which you want to attach a certificate. +Must be a valid GUID. ```yaml Type: String -Parameter Sets: (All) +Parameter Sets: UseExistingApp Aliases: -Required: False +Required: True Position: Named Default value: None Accept pipeline input: False @@ -254,18 +253,18 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -VaultName -If specified, use a custom vault name. -Otherwise, use the default 'GraphEmailAppLocalStore'. +### -MailEnabledSendingGroup +The mail-enabled security group. +Must be a valid email address. ```yaml Type: String -Parameter Sets: (All) +Parameter Sets: CreateNewApp Aliases: -Required: False +Required: True Position: Named -Default value: GraphEmailAppLocalStore +Default value: None Accept pipeline input: False Accept wildcard characters: False ``` @@ -285,23 +284,23 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -ReturnParamSplat -If specified, return the parameter splat for use in other functions. +### -ProgressAction +{{ Fill ProgressAction Description }} ```yaml -Type: SwitchParameter +Type: ActionPreference Parameter Sets: (All) -Aliases: +Aliases: proga Required: False Position: Named -Default value: False +Default value: None Accept pipeline input: False Accept wildcard characters: False ``` -### -DoNotUseDomainSuffix -Switch to add session domain suffix to the app name. +### -ReturnParamSplat +If specified, return the parameter splat for use in other functions. ```yaml Type: SwitchParameter @@ -315,17 +314,18 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -ProgressAction -{{ Fill ProgressAction Description }} +### -VaultName +If specified, use a custom vault name. +Otherwise, use the default 'GraphEmailAppLocalStore'. ```yaml -Type: ActionPreference +Type: String Parameter Sets: (All) -Aliases: proga +Aliases: Required: False Position: Named -Default value: None +Default value: GraphEmailAppLocalStore Accept pipeline input: False Accept wildcard characters: False ``` diff --git a/help/Publish-TkM365AuditApp.md b/help/Publish-TkM365AuditApp.md index 2547b01..ddeafe5 100644 --- a/help/Publish-TkM365AuditApp.md +++ b/help/Publish-TkM365AuditApp.md @@ -1,4 +1,4 @@ ---- +--- external help file: GraphAppToolkit-help.xml Module Name: GraphAppToolkit online version: @@ -73,27 +73,25 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -KeyExportPolicy -Specifies whether the newly created certificate (if no thumbprint is provided) is -'Exportable' or 'NonExportable'. -Defaults to 'NonExportable'. +### -DoNotUseDomainSuffix +If specified, does not append the domain suffix to the app name. ```yaml -Type: String +Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False -Position: 3 -Default value: NonExportable +Position: Named +Default value: False Accept pipeline input: False Accept wildcard characters: False ``` -### -VaultName -The SecretManagement vault name in which to store the app credentials. -Defaults to -"M365AuditAppLocalStore" if not specified. +### -KeyExportPolicy +Specifies whether the newly created certificate (if no thumbprint is provided) is +'Exportable' or 'NonExportable'. +Defaults to 'NonExportable'. ```yaml Type: String @@ -101,8 +99,8 @@ Parameter Sets: (All) Aliases: Required: False -Position: 4 -Default value: M365AuditAppLocalStore +Position: 3 +Default value: NonExportable Accept pipeline input: False Accept wildcard characters: False ``` @@ -122,24 +120,24 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -ReturnParamSplat -If specified, returns a parameter splat string for use in other functions, instead of the -default PSCustomObject containing the app details. +### -ProgressAction +{{ Fill ProgressAction Description }} ```yaml -Type: SwitchParameter +Type: ActionPreference Parameter Sets: (All) -Aliases: +Aliases: proga Required: False Position: Named -Default value: False +Default value: None Accept pipeline input: False Accept wildcard characters: False ``` -### -DoNotUseDomainSuffix -If specified, does not append the domain suffix to the app name. +### -ReturnParamSplat +If specified, returns a parameter splat string for use in other functions, instead of the +default PSCustomObject containing the app details. ```yaml Type: SwitchParameter @@ -153,17 +151,19 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -ProgressAction -{{ Fill ProgressAction Description }} +### -VaultName +The SecretManagement vault name in which to store the app credentials. +Defaults to +"M365AuditAppLocalStore" if not specified. ```yaml -Type: ActionPreference +Type: String Parameter Sets: (All) -Aliases: proga +Aliases: Required: False -Position: Named -Default value: None +Position: 4 +Default value: M365AuditAppLocalStore Accept pipeline input: False Accept wildcard characters: False ``` diff --git a/help/Publish-TkMemPolicyManagerApp.md b/help/Publish-TkMemPolicyManagerApp.md index e93bfcb..a102903 100644 --- a/help/Publish-TkMemPolicyManagerApp.md +++ b/help/Publish-TkMemPolicyManagerApp.md @@ -1,4 +1,4 @@ ---- +--- external help file: GraphAppToolkit-help.xml Module Name: GraphAppToolkit online version: @@ -70,25 +70,24 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -KeyExportPolicy -Specifies whether the newly created certificate is 'Exportable' or 'NonExportable'. -Defaults to 'NonExportable' if not specified. +### -DoNotUseDomainSuffix +If specified, the function does not append the domain suffix to the app name. ```yaml -Type: String +Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False -Position: 3 -Default value: NonExportable +Position: Named +Default value: False Accept pipeline input: False Accept wildcard characters: False ``` -### -VaultName -The name of the SecretManagement vault in which to store the app credentials. -Defaults to 'MemPolicyManagerLocalStore'. +### -KeyExportPolicy +Specifies whether the newly created certificate is 'Exportable' or 'NonExportable'. +Defaults to 'NonExportable' if not specified. ```yaml Type: String @@ -96,8 +95,8 @@ Parameter Sets: (All) Aliases: Required: False -Position: 4 -Default value: MemPolicyManagerLocalStore +Position: 3 +Default value: NonExportable Accept pipeline input: False Accept wildcard characters: False ``` @@ -117,26 +116,24 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -ReadWrite -If specified, grants read-write MEM/Intune permissions. -Otherwise, read-only permissions are granted. +### -ProgressAction +{{ Fill ProgressAction Description }} ```yaml -Type: SwitchParameter +Type: ActionPreference Parameter Sets: (All) -Aliases: +Aliases: proga Required: False Position: Named -Default value: False +Default value: None Accept pipeline input: False Accept wildcard characters: False ``` -### -ReturnParamSplat -If specified, returns a parameter splat string for use in other functions. -Otherwise, returns -a PSCustomObject containing the app details. +### -ReadWrite +If specified, grants read-write MEM/Intune permissions. +Otherwise, read-only permissions are granted. ```yaml Type: SwitchParameter @@ -150,8 +147,10 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -DoNotUseDomainSuffix -If specified, the function does not append the domain suffix to the app name. +### -ReturnParamSplat +If specified, returns a parameter splat string for use in other functions. +Otherwise, returns +a PSCustomObject containing the app details. ```yaml Type: SwitchParameter @@ -165,17 +164,18 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -ProgressAction -{{ Fill ProgressAction Description }} +### -VaultName +The name of the SecretManagement vault in which to store the app credentials. +Defaults to 'MemPolicyManagerLocalStore'. ```yaml -Type: ActionPreference +Type: String Parameter Sets: (All) -Aliases: proga +Aliases: Required: False -Position: Named -Default value: None +Position: 4 +Default value: MemPolicyManagerLocalStore Accept pipeline input: False Accept wildcard characters: False ``` diff --git a/help/Send-TkEmailAppMessage.md b/help/Send-TkEmailAppMessage.md index f97d110..6685694 100644 --- a/help/Send-TkEmailAppMessage.md +++ b/help/Send-TkEmailAppMessage.md @@ -1,4 +1,4 @@ ---- +--- external help file: GraphAppToolkit-help.xml Module Name: GraphAppToolkit online version: @@ -63,17 +63,15 @@ Uses the provided AppId, TenantId, and CertThumbprint directly (no vault) to obt ## PARAMETERS -### -AppName -\[Vault Parameter Set Only\] -The name of the pre-created Microsoft Graph Email App (stored in GraphEmailAppLocalStore). -Used only -if the 'Vault' parameter set is chosen. -The function retrieves the AppId, TenantId, and certificate -thumbprint from the vault entry. +### -AppId +\[Manual Parameter Set Only\] +The Azure AD application (client) ID to use for sending the email. +Must be used together with TenantId +and CertThumbprint in the 'Manual' parameter set. ```yaml Type: String -Parameter Sets: Vault +Parameter Sets: Manual Aliases: Required: True @@ -83,15 +81,17 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -AppId -\[Manual Parameter Set Only\] -The Azure AD application (client) ID to use for sending the email. -Must be used together with TenantId -and CertThumbprint in the 'Manual' parameter set. +### -AppName +\[Vault Parameter Set Only\] +The name of the pre-created Microsoft Graph Email App (stored in GraphEmailAppLocalStore). +Used only +if the 'Vault' parameter set is chosen. +The function retrieves the AppId, TenantId, and certificate +thumbprint from the vault entry. ```yaml Type: String -Parameter Sets: Manual +Parameter Sets: Vault Aliases: Required: True @@ -101,18 +101,16 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -TenantId -\[Manual Parameter Set Only\] -The Azure AD tenant ID (GUID or domain name). -Must be used together with AppId and CertThumbprint -in the 'Manual' parameter set. +### -AttachmentPath +An array of file paths for any attachments to include in the email. +Each path must exist as a leaf file. ```yaml -Type: String -Parameter Sets: Manual +Type: String[] +Parameter Sets: (All) Aliases: -Required: True +Required: False Position: Named Default value: None Accept pipeline input: False @@ -136,8 +134,8 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -To -The email address of the recipient. +### -EmailBody +The body text of the email. ```yaml Type: String @@ -166,6 +164,21 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -ProgressAction +{{ Fill ProgressAction Description }} + +```yaml +Type: ActionPreference +Parameter Sets: (All) +Aliases: proga + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### -Subject The subject line of the email. @@ -181,12 +194,15 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -EmailBody -The body text of the email. +### -TenantId +\[Manual Parameter Set Only\] +The Azure AD tenant ID (GUID or domain name). +Must be used together with AppId and CertThumbprint +in the 'Manual' parameter set. ```yaml Type: String -Parameter Sets: (All) +Parameter Sets: Manual Aliases: Required: True @@ -196,16 +212,15 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -AttachmentPath -An array of file paths for any attachments to include in the email. -Each path must exist as a leaf file. +### -To +The email address of the recipient. ```yaml -Type: String[] +Type: String Parameter Sets: (All) Aliases: -Required: False +Required: True Position: Named Default value: None Accept pipeline input: False @@ -229,22 +244,6 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -WhatIf -Shows what would happen if the cmdlet runs. -The cmdlet is not run. - -```yaml -Type: SwitchParameter -Parameter Sets: (All) -Aliases: wi - -Required: False -Position: Named -Default value: None -Accept pipeline input: False -Accept wildcard characters: False -``` - ### -Confirm Prompts you for confirmation before running the cmdlet. @@ -260,13 +259,14 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -ProgressAction -{{ Fill ProgressAction Description }} +### -WhatIf +Shows what would happen if the cmdlet runs. +The cmdlet is not run. ```yaml -Type: ActionPreference +Type: SwitchParameter Parameter Sets: (All) -Aliases: proga +Aliases: wi Required: False Position: Named diff --git a/source/en-US/GraphAppToolkit-help.xml b/source/en-US/GraphAppToolkit-help.xml index c22b87d..4cde360 100644 --- a/source/en-US/GraphAppToolkit-help.xml +++ b/source/en-US/GraphAppToolkit-help.xml @@ -15,10 +15,10 @@ New-MailEnabledSendingGroup - - Name + + Alias - The name of the mail-enabled security group to create or retrieve. This is also used as the alias if no separate Alias parameter is provided. + An optional alias for the group. If omitted, the group name is used as the alias. String @@ -27,10 +27,10 @@ None - - Alias + + DefaultDomain - An optional alias for the group. If omitted, the group name is used as the alias. + (DefaultDomain parameter set) The domain portion to be appended to the group alias (e.g. "Alias@DefaultDomain"). This parameter is mandatory when using the 'DefaultDomain' parameter set. String @@ -40,9 +40,9 @@ None - PrimarySmtpAddress + Name - (CustomDomain parameter set) The full SMTP address for the group (e.g. "MyGroup@contoso.com"). This parameter is mandatory when using the 'CustomDomain' parameter set. + The name of the mail-enabled security group to create or retrieve. This is also used as the alias if no separate Alias parameter is provided. String @@ -51,16 +51,17 @@ None - - WhatIf + + ProgressAction - Shows what would happen if the cmdlet runs. The cmdlet is not run. + {{ Fill ProgressAction Description }} + ActionPreference - SwitchParameter + ActionPreference - False + None Confirm @@ -73,25 +74,24 @@ False - - ProgressAction + + WhatIf - {{ Fill ProgressAction Description }} + Shows what would happen if the cmdlet runs. The cmdlet is not run. - ActionPreference - ActionPreference + SwitchParameter - None + False New-MailEnabledSendingGroup - - Name + + Alias - The name of the mail-enabled security group to create or retrieve. This is also used as the alias if no separate Alias parameter is provided. + An optional alias for the group. If omitted, the group name is used as the alias. String @@ -100,10 +100,10 @@ None - - Alias + + Name - An optional alias for the group. If omitted, the group name is used as the alias. + The name of the mail-enabled security group to create or retrieve. This is also used as the alias if no separate Alias parameter is provided. String @@ -113,9 +113,9 @@ None - DefaultDomain + PrimarySmtpAddress - (DefaultDomain parameter set) The domain portion to be appended to the group alias (e.g. "Alias@DefaultDomain"). This parameter is mandatory when using the 'DefaultDomain' parameter set. + (CustomDomain parameter set) The full SMTP address for the group (e.g. "MyGroup@contoso.com"). This parameter is mandatory when using the 'CustomDomain' parameter set. String @@ -124,16 +124,17 @@ None - - WhatIf + + ProgressAction - Shows what would happen if the cmdlet runs. The cmdlet is not run. + {{ Fill ProgressAction Description }} + ActionPreference - SwitchParameter + ActionPreference - False + None Confirm @@ -146,25 +147,24 @@ False - - ProgressAction + + WhatIf - {{ Fill ProgressAction Description }} + Shows what would happen if the cmdlet runs. The cmdlet is not run. - ActionPreference - ActionPreference + SwitchParameter - None + False - - Name + + Alias - The name of the mail-enabled security group to create or retrieve. This is also used as the alias if no separate Alias parameter is provided. + An optional alias for the group. If omitted, the group name is used as the alias. String @@ -173,10 +173,10 @@ None - - Alias + + DefaultDomain - An optional alias for the group. If omitted, the group name is used as the alias. + (DefaultDomain parameter set) The domain portion to be appended to the group alias (e.g. "Alias@DefaultDomain"). This parameter is mandatory when using the 'DefaultDomain' parameter set. String @@ -186,9 +186,9 @@ None - PrimarySmtpAddress + Name - (CustomDomain parameter set) The full SMTP address for the group (e.g. "MyGroup@contoso.com"). This parameter is mandatory when using the 'CustomDomain' parameter set. + The name of the mail-enabled security group to create or retrieve. This is also used as the alias if no separate Alias parameter is provided. String @@ -198,9 +198,9 @@ None - DefaultDomain + PrimarySmtpAddress - (DefaultDomain parameter set) The domain portion to be appended to the group alias (e.g. "Alias@DefaultDomain"). This parameter is mandatory when using the 'DefaultDomain' parameter set. + (CustomDomain parameter set) The full SMTP address for the group (e.g. "MyGroup@contoso.com"). This parameter is mandatory when using the 'CustomDomain' parameter set. String @@ -209,17 +209,17 @@ None - - WhatIf + + ProgressAction - Shows what would happen if the cmdlet runs. The cmdlet is not run. + {{ Fill ProgressAction Description }} - SwitchParameter + ActionPreference - SwitchParameter + ActionPreference - False + None Confirm @@ -233,17 +233,17 @@ False - - ProgressAction + + WhatIf - {{ Fill ProgressAction Description }} + Shows what would happen if the cmdlet runs. The cmdlet is not run. - ActionPreference + SwitchParameter - ActionPreference + SwitchParameter - None + False @@ -342,10 +342,10 @@ and a primary SMTP address of Senders@customdomain.org. None - - MailEnabledSendingGroup + + CertPrefix - The mail-enabled security group. Must be a valid email address. + Prefix to add to the certificate subject for the existing app. String @@ -355,9 +355,9 @@ and a primary SMTP address of Senders@customdomain.org. None - CertPrefix + CertThumbprint - Prefix to add to the certificate subject for the existing app. + The thumbprint of the certificate to be retrieved. Must be a valid 40-character hexadecimal string. String @@ -367,16 +367,15 @@ and a primary SMTP address of Senders@customdomain.org. None - CertThumbprint + DoNotUseDomainSuffix - The thumbprint of the certificate to be retrieved. Must be a valid 40-character hexadecimal string. + Switch to add session domain suffix to the app name. - String - String + SwitchParameter - None + False KeyExportPolicy @@ -390,17 +389,17 @@ and a primary SMTP address of Senders@customdomain.org. NonExportable - - VaultName + + MailEnabledSendingGroup - If specified, use a custom vault name. Otherwise, use the default 'GraphEmailAppLocalStore'. + The mail-enabled security group. Must be a valid email address. String String - GraphEmailAppLocalStore + None OverwriteVaultSecret @@ -413,21 +412,22 @@ and a primary SMTP address of Senders@customdomain.org. False - - ReturnParamSplat + + ProgressAction - If specified, return the parameter splat for use in other functions. + {{ Fill ProgressAction Description }} + ActionPreference - SwitchParameter + ActionPreference - False + None - DoNotUseDomainSuffix + ReturnParamSplat - Switch to add session domain suffix to the app name. + If specified, return the parameter splat for use in other functions. SwitchParameter @@ -435,25 +435,25 @@ and a primary SMTP address of Senders@customdomain.org. False - - ProgressAction + + VaultName - {{ Fill ProgressAction Description }} + If specified, use a custom vault name. Otherwise, use the default 'GraphEmailAppLocalStore'. - ActionPreference + String - ActionPreference + String - None + GraphEmailAppLocalStore Publish-TkEmailApp - ExistingAppObjectId + CertPrefix - The AppId of the existing App Registration to which you want to attach a certificate. Must be a valid GUID. + Prefix to add to the certificate subject for the existing app. String @@ -462,10 +462,10 @@ and a primary SMTP address of Senders@customdomain.org. None - - CertPrefix + + CertThumbprint - Prefix to add to the certificate subject for the existing app. + The thumbprint of the certificate to be retrieved. Must be a valid 40-character hexadecimal string. String @@ -475,40 +475,39 @@ and a primary SMTP address of Senders@customdomain.org. None - CertThumbprint + DoNotUseDomainSuffix - The thumbprint of the certificate to be retrieved. Must be a valid 40-character hexadecimal string. + Switch to add session domain suffix to the app name. - String - String + SwitchParameter - None + False - - KeyExportPolicy + + ExistingAppObjectId - Key export policy for the certificate. Valid values are 'Exportable' and 'NonExportable'. Default is 'NonExportable'. + The AppId of the existing App Registration to which you want to attach a certificate. Must be a valid GUID. String String - NonExportable + None - VaultName + KeyExportPolicy - If specified, use a custom vault name. Otherwise, use the default 'GraphEmailAppLocalStore'. + Key export policy for the certificate. Valid values are 'Exportable' and 'NonExportable'. Default is 'NonExportable'. String String - GraphEmailAppLocalStore + NonExportable OverwriteVaultSecret @@ -521,21 +520,22 @@ and a primary SMTP address of Senders@customdomain.org. False - - ReturnParamSplat + + ProgressAction - If specified, return the parameter splat for use in other functions. + {{ Fill ProgressAction Description }} + ActionPreference - SwitchParameter + ActionPreference - False + None - DoNotUseDomainSuffix + ReturnParamSplat - Switch to add session domain suffix to the app name. + If specified, return the parameter splat for use in other functions. SwitchParameter @@ -543,17 +543,17 @@ and a primary SMTP address of Senders@customdomain.org. False - - ProgressAction + + VaultName - {{ Fill ProgressAction Description }} + If specified, use a custom vault name. Otherwise, use the default 'GraphEmailAppLocalStore'. - ActionPreference + String - ActionPreference + String - None + GraphEmailAppLocalStore @@ -582,10 +582,10 @@ and a primary SMTP address of Senders@customdomain.org. None - - MailEnabledSendingGroup + + CertPrefix - The mail-enabled security group. Must be a valid email address. + Prefix to add to the certificate subject for the existing app. String @@ -594,10 +594,10 @@ and a primary SMTP address of Senders@customdomain.org. None - - ExistingAppObjectId + + CertThumbprint - The AppId of the existing App Registration to which you want to attach a certificate. Must be a valid GUID. + The thumbprint of the certificate to be retrieved. Must be a valid 40-character hexadecimal string. String @@ -607,21 +607,21 @@ and a primary SMTP address of Senders@customdomain.org. None - CertPrefix + DoNotUseDomainSuffix - Prefix to add to the certificate subject for the existing app. + Switch to add session domain suffix to the app name. - String + SwitchParameter - String + SwitchParameter - None + False - - CertThumbprint + + ExistingAppObjectId - The thumbprint of the certificate to be retrieved. Must be a valid 40-character hexadecimal string. + The AppId of the existing App Registration to which you want to attach a certificate. Must be a valid GUID. String @@ -642,17 +642,17 @@ and a primary SMTP address of Senders@customdomain.org. NonExportable - - VaultName + + MailEnabledSendingGroup - If specified, use a custom vault name. Otherwise, use the default 'GraphEmailAppLocalStore'. + The mail-enabled security group. Must be a valid email address. String String - GraphEmailAppLocalStore + None OverwriteVaultSecret @@ -666,22 +666,22 @@ and a primary SMTP address of Senders@customdomain.org. False - - ReturnParamSplat + + ProgressAction - If specified, return the parameter splat for use in other functions. + {{ Fill ProgressAction Description }} - SwitchParameter + ActionPreference - SwitchParameter + ActionPreference - False + None - DoNotUseDomainSuffix + ReturnParamSplat - Switch to add session domain suffix to the app name. + If specified, return the parameter splat for use in other functions. SwitchParameter @@ -690,17 +690,17 @@ and a primary SMTP address of Senders@customdomain.org. False - - ProgressAction + + VaultName - {{ Fill ProgressAction Description }} + If specified, use a custom vault name. Otherwise, use the default 'GraphEmailAppLocalStore'. - ActionPreference + String - ActionPreference + String - None + GraphEmailAppLocalStore @@ -872,20 +872,9 @@ and a primary SMTP address of Senders@customdomain.org. M365AuditAppLocalStore - OverwriteVaultSecret - - If specified, overwrites an existing secret in the specified vault if it already exists. - - - SwitchParameter - - - False - - - ReturnParamSplat + DoNotUseDomainSuffix - If specified, returns a parameter splat string for use in other functions, instead of the default PSCustomObject containing the app details. + If specified, does not append the domain suffix to the app name. SwitchParameter @@ -894,9 +883,9 @@ and a primary SMTP address of Senders@customdomain.org. False - DoNotUseDomainSuffix + OverwriteVaultSecret - If specified, does not append the domain suffix to the app name. + If specified, overwrites an existing secret in the specified vault if it already exists. SwitchParameter @@ -916,6 +905,17 @@ and a primary SMTP address of Senders@customdomain.org. None + + ReturnParamSplat + + If specified, returns a parameter splat string for use in other functions, instead of the default PSCustomObject containing the app details. + + + SwitchParameter + + + False + @@ -943,29 +943,29 @@ and a primary SMTP address of Senders@customdomain.org. None - - KeyExportPolicy + + DoNotUseDomainSuffix - Specifies whether the newly created certificate (if no thumbprint is provided) is 'Exportable' or 'NonExportable'. Defaults to 'NonExportable'. + If specified, does not append the domain suffix to the app name. - String + SwitchParameter - String + SwitchParameter - NonExportable + False - - VaultName + + KeyExportPolicy - The SecretManagement vault name in which to store the app credentials. Defaults to "M365AuditAppLocalStore" if not specified. + Specifies whether the newly created certificate (if no thumbprint is provided) is 'Exportable' or 'NonExportable'. Defaults to 'NonExportable'. String String - M365AuditAppLocalStore + NonExportable OverwriteVaultSecret @@ -979,22 +979,22 @@ and a primary SMTP address of Senders@customdomain.org. False - - ReturnParamSplat + + ProgressAction - If specified, returns a parameter splat string for use in other functions, instead of the default PSCustomObject containing the app details. + {{ Fill ProgressAction Description }} - SwitchParameter + ActionPreference - SwitchParameter + ActionPreference - False + None - DoNotUseDomainSuffix + ReturnParamSplat - If specified, does not append the domain suffix to the app name. + If specified, returns a parameter splat string for use in other functions, instead of the default PSCustomObject containing the app details. SwitchParameter @@ -1003,17 +1003,17 @@ and a primary SMTP address of Senders@customdomain.org. False - - ProgressAction + + VaultName - {{ Fill ProgressAction Description }} + The SecretManagement vault name in which to store the app credentials. Defaults to "M365AuditAppLocalStore" if not specified. - ActionPreference + String - ActionPreference + String - None + M365AuditAppLocalStore @@ -1135,9 +1135,9 @@ the credentials in the default vault. MemPolicyManagerLocalStore - OverwriteVaultSecret + DoNotUseDomainSuffix - If specified, overwrites any existing secret of the same name in the vault. + If specified, the function does not append the domain suffix to the app name. SwitchParameter @@ -1146,9 +1146,9 @@ the credentials in the default vault. False - ReadWrite + OverwriteVaultSecret - If specified, grants read-write MEM/Intune permissions. Otherwise, read-only permissions are granted. + If specified, overwrites any existing secret of the same name in the vault. SwitchParameter @@ -1156,21 +1156,22 @@ the credentials in the default vault. False - - ReturnParamSplat + + ProgressAction - If specified, returns a parameter splat string for use in other functions. Otherwise, returns a PSCustomObject containing the app details. + {{ Fill ProgressAction Description }} + ActionPreference - SwitchParameter + ActionPreference - False + None - DoNotUseDomainSuffix + ReadWrite - If specified, the function does not append the domain suffix to the app name. + If specified, grants read-write MEM/Intune permissions. Otherwise, read-only permissions are granted. SwitchParameter @@ -1178,17 +1179,16 @@ the credentials in the default vault. False - - ProgressAction + + ReturnParamSplat - {{ Fill ProgressAction Description }} + If specified, returns a parameter splat string for use in other functions. Otherwise, returns a PSCustomObject containing the app details. - ActionPreference - ActionPreference + SwitchParameter - None + False @@ -1217,29 +1217,29 @@ the credentials in the default vault. None - - KeyExportPolicy + + DoNotUseDomainSuffix - Specifies whether the newly created certificate is 'Exportable' or 'NonExportable'. Defaults to 'NonExportable' if not specified. + If specified, the function does not append the domain suffix to the app name. - String + SwitchParameter - String + SwitchParameter - NonExportable + False - - VaultName + + KeyExportPolicy - The name of the SecretManagement vault in which to store the app credentials. Defaults to 'MemPolicyManagerLocalStore'. + Specifies whether the newly created certificate is 'Exportable' or 'NonExportable'. Defaults to 'NonExportable' if not specified. String String - MemPolicyManagerLocalStore + NonExportable OverwriteVaultSecret @@ -1253,22 +1253,22 @@ the credentials in the default vault. False - - ReadWrite + + ProgressAction - If specified, grants read-write MEM/Intune permissions. Otherwise, read-only permissions are granted. + {{ Fill ProgressAction Description }} - SwitchParameter + ActionPreference - SwitchParameter + ActionPreference - False + None - ReturnParamSplat + ReadWrite - If specified, returns a parameter splat string for use in other functions. Otherwise, returns a PSCustomObject containing the app details. + If specified, grants read-write MEM/Intune permissions. Otherwise, read-only permissions are granted. SwitchParameter @@ -1278,9 +1278,9 @@ the credentials in the default vault. False - DoNotUseDomainSuffix + ReturnParamSplat - If specified, the function does not append the domain suffix to the app name. + If specified, returns a parameter splat string for use in other functions. Otherwise, returns a PSCustomObject containing the app details. SwitchParameter @@ -1289,17 +1289,17 @@ the credentials in the default vault. False - - ProgressAction + + VaultName - {{ Fill ProgressAction Description }} + The name of the SecretManagement vault in which to store the app credentials. Defaults to 'MemPolicyManagerLocalStore'. - ActionPreference + String - ActionPreference + String - None + MemPolicyManagerLocalStore @@ -1375,9 +1375,9 @@ creates a certificate, and stores the credentials in the default vault. Send-TkEmailAppMessage - AppName + AppId - [Vault Parameter Set Only] The name of the pre-created Microsoft Graph Email App (stored in GraphEmailAppLocalStore). Used only if the 'Vault' parameter set is chosen. The function retrieves the AppId, TenantId, and certificate thumbprint from the vault entry. + [Manual Parameter Set Only] The Azure AD application (client) ID to use for sending the email. Must be used together with TenantId and CertThumbprint in the 'Manual' parameter set. String @@ -1386,22 +1386,22 @@ creates a certificate, and stores the credentials in the default vault. None - - To + + AttachmentPath - The email address of the recipient. + An array of file paths for any attachments to include in the email. Each path must exist as a leaf file. - String + String[] - String + String[] None - FromAddress + CertThumbprint - The email address of the sender who is authorized to send email as configured in the Graph Email App. + [Manual Parameter Set Only] The certificate thumbprint (in Cert:\CurrentUser\My) used for authenticating as the Azure AD app. Must be used together with AppId and TenantId in the 'Manual' parameter set. String @@ -1411,9 +1411,9 @@ creates a certificate, and stores the credentials in the default vault.None - Subject + EmailBody - The subject line of the email. + The body text of the email. String @@ -1423,9 +1423,9 @@ creates a certificate, and stores the credentials in the default vault.None - EmailBody + FromAddress - The body text of the email. + The email address of the sender who is authorized to send email as configured in the Graph Email App. String @@ -1434,40 +1434,53 @@ creates a certificate, and stores the credentials in the default vault. None - - AttachmentPath + + ProgressAction - An array of file paths for any attachments to include in the email. Each path must exist as a leaf file. + {{ Fill ProgressAction Description }} - String[] + ActionPreference - String[] + ActionPreference None - - VaultName + + Subject - [Vault Parameter Set Only] The name of the vault to retrieve the GraphEmailApp object. Default is 'GraphEmailAppLocalStore'. + The subject line of the email. String String - GraphEmailAppLocalStore + None - - WhatIf + + TenantId - Shows what would happen if the cmdlet runs. The cmdlet is not run. + [Manual Parameter Set Only] The Azure AD tenant ID (GUID or domain name). Must be used together with AppId and CertThumbprint in the 'Manual' parameter set. + String - SwitchParameter + String - False + None + + + To + + The email address of the recipient. + + String + + String + + + None Confirm @@ -1480,25 +1493,24 @@ creates a certificate, and stores the credentials in the default vault. False - - ProgressAction + + WhatIf - {{ Fill ProgressAction Description }} + Shows what would happen if the cmdlet runs. The cmdlet is not run. - ActionPreference - ActionPreference + SwitchParameter - None + False Send-TkEmailAppMessage - AppId + AppName - [Manual Parameter Set Only] The Azure AD application (client) ID to use for sending the email. Must be used together with TenantId and CertThumbprint in the 'Manual' parameter set. + [Vault Parameter Set Only] The name of the pre-created Microsoft Graph Email App (stored in GraphEmailAppLocalStore). Used only if the 'Vault' parameter set is chosen. The function retrieves the AppId, TenantId, and certificate thumbprint from the vault entry. String @@ -1507,22 +1519,22 @@ creates a certificate, and stores the credentials in the default vault. None - - TenantId + + AttachmentPath - [Manual Parameter Set Only] The Azure AD tenant ID (GUID or domain name). Must be used together with AppId and CertThumbprint in the 'Manual' parameter set. + An array of file paths for any attachments to include in the email. Each path must exist as a leaf file. - String + String[] - String + String[] None - CertThumbprint + EmailBody - [Manual Parameter Set Only] The certificate thumbprint (in Cert:\CurrentUser\My) used for authenticating as the Azure AD app. Must be used together with AppId and TenantId in the 'Manual' parameter set. + The body text of the email. String @@ -1532,9 +1544,9 @@ creates a certificate, and stores the credentials in the default vault.None - To + FromAddress - The email address of the recipient. + The email address of the sender who is authorized to send email as configured in the Graph Email App. String @@ -1543,14 +1555,14 @@ creates a certificate, and stores the credentials in the default vault. None - - FromAddress + + ProgressAction - The email address of the sender who is authorized to send email as configured in the Graph Email App. + {{ Fill ProgressAction Description }} - String + ActionPreference - String + ActionPreference None @@ -1568,9 +1580,9 @@ creates a certificate, and stores the credentials in the default vault.None - EmailBody + To - The body text of the email. + The email address of the recipient. String @@ -1580,27 +1592,16 @@ creates a certificate, and stores the credentials in the default vault.None - AttachmentPath - - An array of file paths for any attachments to include in the email. Each path must exist as a leaf file. - - String[] - - String[] - - - None - - - WhatIf + VaultName - Shows what would happen if the cmdlet runs. The cmdlet is not run. + [Vault Parameter Set Only] The name of the vault to retrieve the GraphEmailApp object. Default is 'GraphEmailAppLocalStore'. + String - SwitchParameter + String - False + GraphEmailAppLocalStore Confirm @@ -1613,25 +1614,24 @@ creates a certificate, and stores the credentials in the default vault. False - - ProgressAction + + WhatIf - {{ Fill ProgressAction Description }} + Shows what would happen if the cmdlet runs. The cmdlet is not run. - ActionPreference - ActionPreference + SwitchParameter - None + False - AppName + AppId - [Vault Parameter Set Only] The name of the pre-created Microsoft Graph Email App (stored in GraphEmailAppLocalStore). Used only if the 'Vault' parameter set is chosen. The function retrieves the AppId, TenantId, and certificate thumbprint from the vault entry. + [Manual Parameter Set Only] The Azure AD application (client) ID to use for sending the email. Must be used together with TenantId and CertThumbprint in the 'Manual' parameter set. String @@ -1641,9 +1641,9 @@ creates a certificate, and stores the credentials in the default vault.None - AppId + AppName - [Manual Parameter Set Only] The Azure AD application (client) ID to use for sending the email. Must be used together with TenantId and CertThumbprint in the 'Manual' parameter set. + [Vault Parameter Set Only] The name of the pre-created Microsoft Graph Email App (stored in GraphEmailAppLocalStore). Used only if the 'Vault' parameter set is chosen. The function retrieves the AppId, TenantId, and certificate thumbprint from the vault entry. String @@ -1652,14 +1652,14 @@ creates a certificate, and stores the credentials in the default vault. None - - TenantId + + AttachmentPath - [Manual Parameter Set Only] The Azure AD tenant ID (GUID or domain name). Must be used together with AppId and CertThumbprint in the 'Manual' parameter set. + An array of file paths for any attachments to include in the email. Each path must exist as a leaf file. - String + String[] - String + String[] None @@ -1677,9 +1677,9 @@ creates a certificate, and stores the credentials in the default vault.None - To + EmailBody - The email address of the recipient. + The body text of the email. String @@ -1700,6 +1700,18 @@ creates a certificate, and stores the credentials in the default vault. None + + ProgressAction + + {{ Fill ProgressAction Description }} + + ActionPreference + + ActionPreference + + + None + Subject @@ -1713,9 +1725,9 @@ creates a certificate, and stores the credentials in the default vault.None - EmailBody + TenantId - The body text of the email. + [Manual Parameter Set Only] The Azure AD tenant ID (GUID or domain name). Must be used together with AppId and CertThumbprint in the 'Manual' parameter set. String @@ -1724,14 +1736,14 @@ creates a certificate, and stores the credentials in the default vault. None - - AttachmentPath + + To - An array of file paths for any attachments to include in the email. Each path must exist as a leaf file. + The email address of the recipient. - String[] + String - String[] + String None @@ -1748,18 +1760,6 @@ creates a certificate, and stores the credentials in the default vault. GraphEmailAppLocalStore - - WhatIf - - Shows what would happen if the cmdlet runs. The cmdlet is not run. - - SwitchParameter - - SwitchParameter - - - False - Confirm @@ -1772,17 +1772,17 @@ creates a certificate, and stores the credentials in the default vault. False - - ProgressAction + + WhatIf - {{ Fill ProgressAction Description }} + Shows what would happen if the cmdlet runs. The cmdlet is not run. - ActionPreference + SwitchParameter - ActionPreference + SwitchParameter - None + False From 3315ddafa0e563bdcfb854b7a5c332ab2b2df0e1 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sat, 15 Mar 2025 17:27:10 -0500 Subject: [PATCH 06/15] docs: Update Help Docs --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index d9a7556..980bff2 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ The **GraphAppToolkit** module provides a set of functions and classes to quickl - Microsoft.Graph - Microsoft.PowerShell.SecretManagement - SecretManagement.JustinGrote.CredMan -- MSAL.PS ### Requirements From 7de7ec96661888bfc0dfc3217d8df42117a384a6 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sun, 16 Mar 2025 13:50:43 -0500 Subject: [PATCH 07/15] add: managed identity support to token grab --- CHANGELOG.md | 1 + source/Private/Get-TkMsalToken.ps1 | 216 ++++++++++++++++++----------- 2 files changed, 136 insertions(+), 81 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5619d65..e9562d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added Get-TkMsalToken cmdlet to retrieve an MSAL token using API calls. +- Added Managed Identity support for Get-TkMsalToken cmdlet (Needs to be tested). ### Fixed diff --git a/source/Private/Get-TkMsalToken.ps1 b/source/Private/Get-TkMsalToken.ps1 index 19175cc..6de68d5 100644 --- a/source/Private/Get-TkMsalToken.ps1 +++ b/source/Private/Get-TkMsalToken.ps1 @@ -1,146 +1,200 @@ <# .SYNOPSIS - Retrieves an OAuth 2.0 token using a client certificate for authentication. + Retrieves an OAuth2 token using various authentication methods. .DESCRIPTION - The Get-TkMsalToken function generates a JSON Web Token (JWT) signed with a provided X.509 certificate and uses it to request an OAuth 2.0 token from Azure Active Directory (AAD). The token can be used to authenticate API requests to services like Microsoft Graph. + The Get-TkMsalToken function retrieves an OAuth2 token for accessing APIs such as Microsoft Graph. + It supports multiple authentication methods including client certificate, client secret, and managed identity. .PARAMETER ClientCertificate The X.509 certificate used for authentication. Example: $ClientCertificate = Get-Item Cert:\CurrentUser\My\ + .PARAMETER ClientSecret + The client secret used for authentication. + .PARAMETER UseManagedIdentity + Use Azure Managed Identity for authentication (only works in Azure-hosted environments). .PARAMETER ClientId - The Azure AD application (client) ID. Must be a valid GUID. + The Azure AD application (client) ID. .PARAMETER TenantId - The Azure AD tenant ID (GUID). Example: 12345678-1234-1234-1234-1234567890ab. + The Azure AD tenant ID (GUID). .PARAMETER Scope - The API scope for token access. Default is Microsoft Graph. Must be a valid URL. + The API scope for token access. Default is Microsoft Graph. .PARAMETER AuthorityType - The authority type to use for authentication. Valid values are 'Global', 'AzureGov', and 'China'. Default is 'Global'. + The authority type to use for authentication. Valid values are 'Global', 'AzureGov', and 'China'. .EXAMPLE - $ClientCertificate = Get-Item Cert:\CurrentUser\My\ - $ClientId = "your-client-id" - $TenantId = "your-tenant-id" - $Token = Get-TkMsalToken -ClientCertificate $ClientCertificate -ClientId $ClientId -TenantId $TenantId + $token = Get-TkMsalToken -ClientId 'your-client-id' -TenantId 'your-tenant-id' -ClientSecret $secureClientSecret + .EXAMPLE + $token = Get-TkMsalToken -ClientId 'your-client-id' -TenantId 'your-tenant-id' -ClientCertificate $cert + .EXAMPLE + $token = Get-TkMsalToken -ClientId 'your-client-id' -TenantId 'your-tenant-id' -UseManagedIdentity + .NOTES - This function requires the 'Invoke-RestMethod' cmdlet to be available. - The function logs the token expiration time using a custom Write-AuditLog function. + This function requires the MSAL.PS module for token acquisition. #> function Get-TkMsalToken { - [CmdletBinding()] + [CmdletBinding(DefaultParameterSetName = 'ClientCertificate')] param ( + # Client Certificate [Parameter( + ParameterSetName = 'ClientCertificate', Mandatory = $true, HelpMessage = ` - "The X.509 certificate used for authentication. Example: `n`$ClientCertificate = Get-Item Cert:\CurrentUser\My\" + "The X.509 certificate used for authentication. Example: `n`$ClientCertificate = Get-Item Cert:\CurrentUser\My\" )] [System.Security.Cryptography.X509Certificates.X509Certificate2] $ClientCertificate, - # + # Client Secret + [Parameter( + ParameterSetName = 'ClientSecret', + Mandatory = $true, + HelpMessage = ` + 'The client secret used for authentication.' + )] + [ValidateNotNullOrEmpty()] + [SecureString] + $ClientSecret, + # Managed Identity [Parameter( + ParameterSetName = 'ManagedIdentity', Mandatory = $true, HelpMessage = ` - 'The Azure AD application (client) ID.' + 'Use Azure Managed Identity for authentication (only works in Azure-hosted environments).' + )] + [switch] + $UseManagedIdentity, + # Client ID + [Parameter( + Mandatory = $true, + HelpMessage = 'The Azure AD application (client) ID.' )] [ValidatePattern('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')] [string] $ClientId, - # + # Tenant ID [Parameter( Mandatory = $true, - HelpMessage = ` - 'The Azure AD tenant ID (GUID). Example: 12345678-1234-1234-1234-1234567890ab.')] + HelpMessage = 'The Azure AD tenant ID (GUID).' + )] [ValidatePattern('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')] [string] $TenantId, - # + # Scope [Parameter( - HelpMessage = ` - 'The API scope for token access. Default is Microsoft Graph.' + HelpMessage = 'The API scope for token access. Default is Microsoft Graph.' )] [ValidatePattern('^https:\/\/[a-zA-Z0-9.-]+\/[a-zA-Z0-9.-]+$')] [string] $Scope = 'https://graph.microsoft.com/.default', - # + # Authority Type [Parameter( - Mandatory = $false, - HelpMessage = ` - 'The authority type to use for authentication.' + HelpMessage = 'The authority type to use for authentication.' )] - [ValidateSet('Global', 'AzureGov', 'China')] # Removed invalid options + [ValidateSet('Global', 'AzureGov', 'China')] [string] $AuthorityType = 'Global' ) begin { if (-not $script:LogString) { - Write-AuditLog -Start + #Write-AuditLog -Start } else { - Write-AuditLog -BeginFunction + #Write-AuditLog -BeginFunction } - # Define Authority URL based on the chosen AuthorityType + # Define Authority URL based on selected cloud type switch ($AuthorityType) { 'Global' { $Authority = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token" } 'AzureGov' { $Authority = "https://login.microsoftonline.us/$TenantId/oauth2/v2.0/token" } 'China' { $Authority = "https://login.chinacloudapi.cn/$TenantId/oauth2/v2.0/token" } } - # Generate JWT Header - $JwtHeader = @{ - alg = 'RS256' - typ = 'JWT' - x5t = [Convert]::ToBase64String($ClientCertificate.GetCertHash()) -replace '\+', '-' -replace '/', '_' -replace '=' - } - # Generate Unix timestamps for `iat` and `exp` - $IatTime = [int](Get-Date (Get-Date).ToUniversalTime() -UFormat %s) - $ExpTime = $IatTime + 600 # JWT Expiration time (10 minutes from `iat`) - # Generate JWT Payload - $JwtPayload = @{ - aud = $Authority - exp = $ExpTime - iat = $IatTime - iss = $ClientId - sub = $ClientId - jti = [guid]::NewGuid().ToString() - } } process { - # Convert JWT Header & Payload to Base64URL Encoding - $Base64UrlEncode = { param ($String) [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($String)) -replace '\+', '-' -replace '/', '_' -replace '=' } - $JwtHeaderEncoded = &$Base64UrlEncode (ConvertTo-Json $JwtHeader -Compress) - $JwtPayloadEncoded = &$Base64UrlEncode (ConvertTo-Json $JwtPayload -Compress) - # Combine Header & Payload - $JwtToSign = "$JwtHeaderEncoded.$JwtPayloadEncoded" - # Sign JWT with Certificate Private Key - $Csp = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($ClientCertificate) - $Signature = [Convert]::ToBase64String( - $Csp.SignData( - [System.Text.Encoding]::UTF8.GetBytes($JwtToSign), - [System.Security.Cryptography.HashAlgorithmName]::SHA256, - [System.Security.Cryptography.RSASignaturePadding]::Pkcs1 - ) - ) -replace '\+', '-' -replace '/', '_' -replace '=' - $ClientAssertion = "$JwtToSign.$Signature" - # Prepare Token Request - $Body = @{ - client_id = $ClientId - client_assertion = $ClientAssertion - client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer' - grant_type = 'client_credentials' - scope = $Scope + if ($PSCmdlet.ParameterSetName -eq 'ManagedIdentity') { + # 🟢 Managed Identity Authentication (Only Works in Azure-hosted Environments) + try { + # 📝 Construct the URL for requesting an access token from the Azure Instance Metadata Service (IMDS) + # This URL is specific to Managed Identity authentication in Azure VMs, Azure Functions, App Services, etc. + $uri = 'http://169.254.169.254/metadata/identity/oauth2/token?resource=https://graph.microsoft.com&api-version=2019-08-01' + # 📝 Invoke-RestMethod sends a request to retrieve an OAuth2 token for the specified resource (Graph API) + # 🔹 Managed Identity requires a GET request (unlike client secret/cert auth which use POST) + $Response = Invoke-RestMethod ` + -Uri $uri ` + -Method Get ` + -Headers @{ 'Metadata' = 'true' } ` # 🔹 Mandatory header to indicate this is an IMDS request + -ErrorAction Stop # 🔹 Ensures an error is thrown if the request fails + # 📝 Return only the access token from the API response + return $Response.access_token + } + catch { + # 🛑 If the request fails, print an error message and rethrow the exception + Write-Error "Failed to obtain token via Managed Identity: $_" + throw + } + } + elseif ($PSCmdlet.ParameterSetName -eq 'ClientCertificate') { + # Validate Certificate Expiration + if ($ClientCertificate.NotAfter -lt (Get-Date)) { + Write-Error "The provided certificate has expired on $($ClientCertificate.NotAfter). Please use a valid certificate." + return $null + } + # Generate JWT for client certificate authentication + $JwtHeader = @{ + alg = 'RS256' + typ = 'JWT' + x5t = [Convert]::ToBase64String($ClientCertificate.GetCertHash()) -replace '\+', '-' -replace '/', '_' -replace '=' + } + $IatTime = [int](Get-Date -UFormat %s) + $ExpTime = $IatTime + 600 # 10 min expiration + $JwtPayload = @{ + aud = $Authority + exp = $ExpTime + iat = $IatTime + nbf = $IatTime + iss = $ClientId + sub = $ClientId + jti = [guid]::NewGuid().ToString() + } + $Base64UrlEncode = { param ($String) [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($String)) -replace '\+', '-' -replace '/', '_' -replace '=' } + $JwtHeaderEncoded = &$Base64UrlEncode (ConvertTo-Json $JwtHeader -Compress) + $JwtPayloadEncoded = &$Base64UrlEncode (ConvertTo-Json $JwtPayload -Compress) + $JwtToSign = "$JwtHeaderEncoded.$JwtPayloadEncoded" + try { + $Csp = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($ClientCertificate) + $Signature = [Convert]::ToBase64String( + $Csp.SignData( + [System.Text.Encoding]::UTF8.GetBytes($JwtToSign), + [System.Security.Cryptography.HashAlgorithmName]::SHA256, + [System.Security.Cryptography.RSASignaturePadding]::Pkcs1 + ) + ) -replace '\+', '-' -replace '/', '_' -replace '=' + } + catch { + Write-Error "Failed to sign JWT: $_" + throw + } + $ClientAssertion = "$JwtToSign.$Signature" + $Body = @{ + client_id = $ClientId + client_assertion = $ClientAssertion + client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer' + grant_type = 'client_credentials' + scope = $Scope + } + } + elseif ($PSCmdlet.ParameterSetName -eq 'ClientSecret') { + $PlainClientSecret = ConvertFrom-SecureString -SecureString $ClientSecret -AsPlainText + $Body = @{ + client_id = $ClientId + client_secret = $PlainClientSecret + grant_type = 'client_credentials' + scope = $Scope + } } } end { try { - # Get Token Using Invoke-RestMethod - $Response = Invoke-RestMethod -Method Post -Uri $Authority -ContentType 'application/x-www-form-urlencoded' -Body $Body -ErrorAction Stop - $tokenExpires = [System.TimeSpan]::FromSeconds($Response.expires_in) - $tokenExpMinutes = $tokenExpires.Minutes - $tokenExpTime = (Get-Date).AddMinutes($tokenExpMinutes) - Write-AuditLog "The token expires today $($tokenExpTime.ToShortDateString()) in $tokenExpMinutes minutes at $($tokenExpTime.ToShortTimeString())." - Write-AuditLog -EndFunction - return $Response.access_token + return (Invoke-RestMethod -Method Post -Uri $Authority -ContentType 'application/x-www-form-urlencoded' -Body $Body -ErrorAction Stop).access_token } catch { Write-Error "Failed to obtain token: $_" - return $null + throw } } } - From acc5fb7b30acff522def31835a5016c5338d036a Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sun, 16 Mar 2025 14:03:02 -0500 Subject: [PATCH 08/15] fix: utc time for jwt --- source/Private/Get-TkMsalToken.ps1 | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/source/Private/Get-TkMsalToken.ps1 b/source/Private/Get-TkMsalToken.ps1 index 6de68d5..a14478c 100644 --- a/source/Private/Get-TkMsalToken.ps1 +++ b/source/Private/Get-TkMsalToken.ps1 @@ -93,10 +93,10 @@ function Get-TkMsalToken { ) begin { if (-not $script:LogString) { - #Write-AuditLog -Start + Write-AuditLog -Start } else { - #Write-AuditLog -BeginFunction + Write-AuditLog -BeginFunction } # Define Authority URL based on selected cloud type switch ($AuthorityType) { @@ -132,7 +132,7 @@ function Get-TkMsalToken { # Validate Certificate Expiration if ($ClientCertificate.NotAfter -lt (Get-Date)) { Write-Error "The provided certificate has expired on $($ClientCertificate.NotAfter). Please use a valid certificate." - return $null + throw "Certificate has expired." } # Generate JWT for client certificate authentication $JwtHeader = @{ @@ -140,7 +140,7 @@ function Get-TkMsalToken { typ = 'JWT' x5t = [Convert]::ToBase64String($ClientCertificate.GetCertHash()) -replace '\+', '-' -replace '/', '_' -replace '=' } - $IatTime = [int](Get-Date -UFormat %s) + $IatTime = [int](Get-Date (Get-Date).ToUniversalTime() -UFormat %s) $ExpTime = $IatTime + 600 # 10 min expiration $JwtPayload = @{ aud = $Authority @@ -190,7 +190,10 @@ function Get-TkMsalToken { } end { try { - return (Invoke-RestMethod -Method Post -Uri $Authority -ContentType 'application/x-www-form-urlencoded' -Body $Body -ErrorAction Stop).access_token + Write-AuditLog "Requesting token from $Authority." + $TokenResponse = (Invoke-RestMethod -Method Post -Uri $Authority -ContentType 'application/x-www-form-urlencoded' -Body $Body -ErrorAction Stop).access_token + Write-AuditLog -EndFunction + return $TokenResponse } catch { Write-Error "Failed to obtain token: $_" From 08fbd94e02adac2e0f42de389ccc963544a6efc5 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sun, 16 Mar 2025 14:17:46 -0500 Subject: [PATCH 09/15] security: add secure string support to token grab --- source/Private/Get-TkMsalToken.ps1 | 3 ++- source/Public/Send-TkEmailAppMessage.ps1 | 11 ++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/source/Private/Get-TkMsalToken.ps1 b/source/Private/Get-TkMsalToken.ps1 index a14478c..6f60f86 100644 --- a/source/Private/Get-TkMsalToken.ps1 +++ b/source/Private/Get-TkMsalToken.ps1 @@ -31,6 +31,7 @@ #> function Get-TkMsalToken { [CmdletBinding(DefaultParameterSetName = 'ClientCertificate')] + [OutputType([System.Security.SecureString])] param ( # Client Certificate [Parameter( @@ -193,7 +194,7 @@ function Get-TkMsalToken { Write-AuditLog "Requesting token from $Authority." $TokenResponse = (Invoke-RestMethod -Method Post -Uri $Authority -ContentType 'application/x-www-form-urlencoded' -Body $Body -ErrorAction Stop).access_token Write-AuditLog -EndFunction - return $TokenResponse + return $TokenResponse | ConvertTo-SecureString -AsPlainText -Force } catch { Write-Error "Failed to obtain token: $_" diff --git a/source/Public/Send-TkEmailAppMessage.ps1 b/source/Public/Send-TkEmailAppMessage.ps1 index 3697051..796bdb3 100644 --- a/source/Public/Send-TkEmailAppMessage.ps1 +++ b/source/Public/Send-TkEmailAppMessage.ps1 @@ -255,7 +255,16 @@ function Send-TkEmailAppMessage { -ErrorAction Stop #> # Set up the request headers - $authHeader = @{Authorization = "Bearer $($MSToken)" } + # If powershell 7, use ConvertFrom-SecureString -AsPlainText + # Else use $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureString),$plaintext = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) + if ($PSVersionTable.PSVersion.Major -ge 7) { + $Token = ConvertFrom-SecureString -SecureString $MSToken -AsPlainText + } + else { + $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($MSToken) + $Token = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) + } + $authHeader = @{Authorization = "Bearer $Token" } # Set up the request URL $url = "https://graph.microsoft.com/v1.0/users/$($FromAddress)/sendMail" # Build the message body From 587938bc03246728bd8c80d473a418ae0c225e7f Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sun, 16 Mar 2025 14:18:05 -0500 Subject: [PATCH 10/15] docs: Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9562d4..3d60aea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added Get-TkMsalToken cmdlet to retrieve an MSAL token using API calls. - Added Managed Identity support for Get-TkMsalToken cmdlet (Needs to be tested). +- SecureString support for Get-TkMsalToken cmdlet. ### Fixed From db580556066b7a62fd32e4f5dd2974920a7ca672 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sun, 16 Mar 2025 15:51:55 -0500 Subject: [PATCH 11/15] fix: Msal token handling --- source/Private/Get-TkMsalToken.ps1 | 100 +++++++++++------------ source/Public/Send-TkEmailAppMessage.ps1 | 19 +---- 2 files changed, 49 insertions(+), 70 deletions(-) diff --git a/source/Private/Get-TkMsalToken.ps1 b/source/Private/Get-TkMsalToken.ps1 index 6f60f86..e30a777 100644 --- a/source/Private/Get-TkMsalToken.ps1 +++ b/source/Private/Get-TkMsalToken.ps1 @@ -1,9 +1,11 @@ <# .SYNOPSIS - Retrieves an OAuth2 token using various authentication methods. + Retrieves an OAuth2 token for accessing Microsoft Graph or other APIs using various authentication methods. .DESCRIPTION - The Get-TkMsalToken function retrieves an OAuth2 token for accessing APIs such as Microsoft Graph. - It supports multiple authentication methods including client certificate, client secret, and managed identity. + The Get-TkMsalToken function supports three authentication methods: + - Client Certificate + - Client Secret + - Managed Identity (only works in Azure-hosted environments) .PARAMETER ClientCertificate The X.509 certificate used for authentication. Example: $ClientCertificate = Get-Item Cert:\CurrentUser\My\ @@ -20,18 +22,19 @@ .PARAMETER AuthorityType The authority type to use for authentication. Valid values are 'Global', 'AzureGov', and 'China'. .EXAMPLE - $token = Get-TkMsalToken -ClientId 'your-client-id' -TenantId 'your-tenant-id' -ClientSecret $secureClientSecret + Get-TkMsalToken -ClientCertificate $ClientCert -ClientId 'your-client-id' -TenantId 'your-tenant-id' .EXAMPLE - $token = Get-TkMsalToken -ClientId 'your-client-id' -TenantId 'your-tenant-id' -ClientCertificate $cert + Get-TkMsalToken -ClientSecret $ClientSecret -ClientId 'your-client-id' -TenantId 'your-tenant-id' .EXAMPLE - $token = Get-TkMsalToken -ClientId 'your-client-id' -TenantId 'your-tenant-id' -UseManagedIdentity - + Get-TkMsalToken -UseManagedIdentity -ClientId 'your-client-id' -TenantId 'your-tenant-id' .NOTES - This function requires the MSAL.PS module for token acquisition. + Author: DrIOSx + Date: 2025-03-16 + Version: 1.0 #> function Get-TkMsalToken { [CmdletBinding(DefaultParameterSetName = 'ClientCertificate')] - [OutputType([System.Security.SecureString])] + [OutputType([string])] param ( # Client Certificate [Parameter( @@ -93,7 +96,7 @@ function Get-TkMsalToken { $AuthorityType = 'Global' ) begin { - if (-not $script:LogString) { + if (-not $script:logString) { Write-AuditLog -Start } else { @@ -101,66 +104,58 @@ function Get-TkMsalToken { } # Define Authority URL based on selected cloud type switch ($AuthorityType) { - 'Global' { $Authority = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token" } - 'AzureGov' { $Authority = "https://login.microsoftonline.us/$TenantId/oauth2/v2.0/token" } - 'China' { $Authority = "https://login.chinacloudapi.cn/$TenantId/oauth2/v2.0/token" } + 'Global' { $authority = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token" } + 'AzureGov' { $authority = "https://login.microsoftonline.us/$TenantId/oauth2/v2.0/token" } + 'China' { $authority = "https://login.chinacloudapi.cn/$TenantId/oauth2/v2.0/token" } } } process { if ($PSCmdlet.ParameterSetName -eq 'ManagedIdentity') { - # 🟢 Managed Identity Authentication (Only Works in Azure-hosted Environments) + # Managed Identity Authentication (Only Works in Azure-hosted Environments) try { - # 📝 Construct the URL for requesting an access token from the Azure Instance Metadata Service (IMDS) - # This URL is specific to Managed Identity authentication in Azure VMs, Azure Functions, App Services, etc. $uri = 'http://169.254.169.254/metadata/identity/oauth2/token?resource=https://graph.microsoft.com&api-version=2019-08-01' - # 📝 Invoke-RestMethod sends a request to retrieve an OAuth2 token for the specified resource (Graph API) - # 🔹 Managed Identity requires a GET request (unlike client secret/cert auth which use POST) - $Response = Invoke-RestMethod ` + $response = Invoke-RestMethod ` -Uri $uri ` -Method Get ` - -Headers @{ 'Metadata' = 'true' } ` # 🔹 Mandatory header to indicate this is an IMDS request - -ErrorAction Stop # 🔹 Ensures an error is thrown if the request fails - # 📝 Return only the access token from the API response - return $Response.access_token + -Headers @{ 'Metadata' = 'true' } ` + -ErrorAction Stop + return $response.access_token } catch { - # 🛑 If the request fails, print an error message and rethrow the exception Write-Error "Failed to obtain token via Managed Identity: $_" throw } } elseif ($PSCmdlet.ParameterSetName -eq 'ClientCertificate') { - # Validate Certificate Expiration if ($ClientCertificate.NotAfter -lt (Get-Date)) { Write-Error "The provided certificate has expired on $($ClientCertificate.NotAfter). Please use a valid certificate." throw "Certificate has expired." } - # Generate JWT for client certificate authentication - $JwtHeader = @{ + $jwtHeader = @{ alg = 'RS256' typ = 'JWT' x5t = [Convert]::ToBase64String($ClientCertificate.GetCertHash()) -replace '\+', '-' -replace '/', '_' -replace '=' } - $IatTime = [int](Get-Date (Get-Date).ToUniversalTime() -UFormat %s) - $ExpTime = $IatTime + 600 # 10 min expiration - $JwtPayload = @{ - aud = $Authority - exp = $ExpTime - iat = $IatTime - nbf = $IatTime + $iatTime = [int](Get-Date (Get-Date).ToUniversalTime() -UFormat %s) + $expTime = $iatTime + 600 # 10 min expiration + $jwtPayload = @{ + aud = $authority + exp = $expTime + iat = $iatTime + nbf = $iatTime iss = $ClientId sub = $ClientId jti = [guid]::NewGuid().ToString() } - $Base64UrlEncode = { param ($String) [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($String)) -replace '\+', '-' -replace '/', '_' -replace '=' } - $JwtHeaderEncoded = &$Base64UrlEncode (ConvertTo-Json $JwtHeader -Compress) - $JwtPayloadEncoded = &$Base64UrlEncode (ConvertTo-Json $JwtPayload -Compress) - $JwtToSign = "$JwtHeaderEncoded.$JwtPayloadEncoded" + $base64UrlEncode = { param ($string) [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($string)) -replace '\+', '-' -replace '/', '_' -replace '=' } + $jwtHeaderEncoded = &$base64UrlEncode (ConvertTo-Json $jwtHeader -Compress) + $jwtPayloadEncoded = &$base64UrlEncode (ConvertTo-Json $jwtPayload -Compress) + $jwtToSign = "$jwtHeaderEncoded.$jwtPayloadEncoded" try { - $Csp = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($ClientCertificate) - $Signature = [Convert]::ToBase64String( - $Csp.SignData( - [System.Text.Encoding]::UTF8.GetBytes($JwtToSign), + $csp = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($ClientCertificate) + $signature = [Convert]::ToBase64String( + $csp.SignData( + [System.Text.Encoding]::UTF8.GetBytes($jwtToSign), [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1 ) @@ -170,20 +165,20 @@ function Get-TkMsalToken { Write-Error "Failed to sign JWT: $_" throw } - $ClientAssertion = "$JwtToSign.$Signature" - $Body = @{ + $clientAssertion = "$jwtToSign.$signature" + $body = @{ client_id = $ClientId - client_assertion = $ClientAssertion + client_assertion = $clientAssertion client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer' grant_type = 'client_credentials' scope = $Scope } } elseif ($PSCmdlet.ParameterSetName -eq 'ClientSecret') { - $PlainClientSecret = ConvertFrom-SecureString -SecureString $ClientSecret -AsPlainText - $Body = @{ + $plainClientSecret = ConvertFrom-SecureString -SecureString $ClientSecret -AsPlainText + $body = @{ client_id = $ClientId - client_secret = $PlainClientSecret + client_secret = $plainClientSecret grant_type = 'client_credentials' scope = $Scope } @@ -191,13 +186,14 @@ function Get-TkMsalToken { } end { try { - Write-AuditLog "Requesting token from $Authority." - $TokenResponse = (Invoke-RestMethod -Method Post -Uri $Authority -ContentType 'application/x-www-form-urlencoded' -Body $Body -ErrorAction Stop).access_token + Write-AuditLog "Requesting token from $authority." + $tokenResponse = (Invoke-RestMethod -Method Post -Uri $authority -ContentType 'application/x-www-form-urlencoded' -Body $body -ErrorAction Stop).access_token + Write-AuditLog "Successfully obtained token from $authority." Write-AuditLog -EndFunction - return $TokenResponse | ConvertTo-SecureString -AsPlainText -Force + return $tokenResponse } catch { - Write-Error "Failed to obtain token: $_" + Write-AuditLog -Message "Failed to obtain token: $($_.Exception.Message)" -Severity "Error" throw } } diff --git a/source/Public/Send-TkEmailAppMessage.ps1 b/source/Public/Send-TkEmailAppMessage.ps1 index 796bdb3..f47e07c 100644 --- a/source/Public/Send-TkEmailAppMessage.ps1 +++ b/source/Public/Send-TkEmailAppMessage.ps1 @@ -247,24 +247,7 @@ function Send-TkEmailAppMessage { -ClientId $AppId ` -TenantId $Tenant ` -ErrorAction Stop - <# - $MSToken = Get-MsalToken ` - -ClientCertificate $Cert ` - -ClientId $AppId ` - -Authority "https://login.microsoftonline.com/$Tenant/oauth2/v2.0/token" ` - -ErrorAction Stop - #> - # Set up the request headers - # If powershell 7, use ConvertFrom-SecureString -AsPlainText - # Else use $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureString),$plaintext = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) - if ($PSVersionTable.PSVersion.Major -ge 7) { - $Token = ConvertFrom-SecureString -SecureString $MSToken -AsPlainText - } - else { - $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($MSToken) - $Token = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) - } - $authHeader = @{Authorization = "Bearer $Token" } + $authHeader = @{Authorization = "Bearer $MSToken" } # Set up the request URL $url = "https://graph.microsoft.com/v1.0/users/$($FromAddress)/sendMail" # Build the message body From 055415fa6f98555683bf05d06b65dab7a9752026 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sun, 16 Mar 2025 20:11:46 -0500 Subject: [PATCH 12/15] format: style formatting --- source/Private/Connect-TkMsService.ps1 | 18 +- source/Private/ConvertTo-ParameterSplat.ps1 | 9 +- source/Private/Get-TkExistingCert.ps1 | 8 +- source/Private/Get-TkExistingSecret.ps1 | 16 +- .../Initialize-TkAppAuthCertificate.ps1 | 15 +- source/Private/Initialize-TkAppName.ps1 | 17 +- source/Private/Initialize-TkModuleEnv.ps1 | 123 ++++- ...ize-TkRequiredResourcePermissionObject.ps1 | 31 +- source/Private/New-TkAppRegistration.ps1 | 55 +- .../Private/New-TkAppSpOauth2Registration.ps1 | 90 ++-- .../Private/New-TkExchangeEmailAppPolicy.ps1 | 8 +- source/Private/Set-TkJsonSecret.ps1 | 9 +- source/Private/Write-AuditLog.ps1 | 39 +- source/Public/New-MailEnabledSendingGroup.ps1 | 78 +-- source/Public/Publish-TkEmailApp.ps1 | 497 +++++++++--------- 15 files changed, 564 insertions(+), 449 deletions(-) diff --git a/source/Private/Connect-TkMsService.ps1 b/source/Private/Connect-TkMsService.ps1 index 42d742e..3a17827 100644 --- a/source/Private/Connect-TkMsService.ps1 +++ b/source/Private/Connect-TkMsService.ps1 @@ -30,17 +30,17 @@ function Connect-TkMsService { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param ( [Parameter( - HelpMessage = 'Connect to Microsoft Graph.' + HelpMessage = 'Switch to connect to Microsoft Graph.' )] [Switch] $MgGraph, [Parameter( - HelpMessage = 'Graph Scopes.' + HelpMessage = 'Array of scopes required for Microsoft Graph authentication.' )] [String[]] $GraphAuthScopes, [Parameter( - HelpMessage = 'Connect to Exchange Online.' + HelpMessage = 'Switch to connect to Exchange Online.' )] [Switch] $ExchangeOnline @@ -68,14 +68,6 @@ function Connect-TkMsService { Get-MgUser -Top 1 -ErrorAction Stop | Out-Null $ContextMg = Get-MgContext -ErrorAction Stop # Check required scopes - <# - $scopesNeeded = @( - 'Application.ReadWrite.All', - 'DelegatedPermissionGrant.ReadWrite.All', - 'Directory.ReadWrite.All', - 'RoleManagement.ReadWrite.Directory' - ) - #> $scopesNeeded = $GraphAuthScopes $missing = $scopesNeeded | Where-Object { $ContextMg.Scopes -notcontains $_ } if ($missing) { @@ -103,7 +95,7 @@ function Connect-TkMsService { Remove-MgContext -ErrorAction SilentlyContinue Write-AuditLog 'Creating a new Microsoft Graph session.' Connect-MgGraph -ContextScope Process -Scopes $scopesNeeded ` - -ErrorAction Stop + -ErrorAction Stop | Out-Null Write-AuditLog 'Connected to Microsoft Graph.' } } @@ -111,7 +103,7 @@ function Connect-TkMsService { # No valid session, so just connect Write-AuditLog 'No valid Microsoft Graph session found. Connecting...' Connect-MgGraph -ContextScope Process -Scopes $scopesNeeded ` - -ErrorAction Stop + -ErrorAction Stop | Out-Null Write-AuditLog 'Connected to Microsoft Graph.' } } diff --git a/source/Private/ConvertTo-ParameterSplat.ps1 b/source/Private/ConvertTo-ParameterSplat.ps1 index e18306c..e933d62 100644 --- a/source/Private/ConvertTo-ParameterSplat.ps1 +++ b/source/Private/ConvertTo-ParameterSplat.ps1 @@ -17,16 +17,19 @@ } .NOTES Author: DrIOSx - Date: YYYY-MM-DD + Last Updated: 2025-03-16 #> function ConvertTo-ParameterSplat { [CmdletBinding()] [OutputType([string])] param ( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, ValueFromPipeline = $true, HelpMessage = 'The object whose properties will be converted into a parameter splatting hashtable script.')] + [ValidateNotNullOrEmpty()] [PSObject]$InputObject ) process { + Write-AuditLog -Message "Starting ConvertTo-ParameterSplat function." -Severity "Information" + $splatScript = "`$params = @{`n" $InputObject.psobject.Properties | ForEach-Object { $value = $_.Value @@ -36,6 +39,8 @@ function ConvertTo-ParameterSplat { $splatScript += " $($_.Name) = $value`n" } $splatScript += "}" + + Write-AuditLog -Message "Completed ConvertTo-ParameterSplat function." -Severity "Information" Write-Output $splatScript } } diff --git a/source/Private/Get-TkExistingCert.ps1 b/source/Private/Get-TkExistingCert.ps1 index ad939a5..fb8f32a 100644 --- a/source/Private/Get-TkExistingCert.ps1 +++ b/source/Private/Get-TkExistingCert.ps1 @@ -18,17 +18,20 @@ function Get-TkExistingCert { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param ( - [Parameter(Mandatory = $true)] + [Parameter(Mandatory = $true, HelpMessage = 'The subject name of the certificate to search for in the current user''s certificate store.')] [string]$CertName ) + if (-not $script:LogString) { Write-AuditLog -Start } else { Write-AuditLog -BeginFunction } + $ExistingCert = Get-ChildItem -Path Cert:\CurrentUser\My -ErrorAction SilentlyContinue | Where-Object { $_.Subject -eq $CertName } -ErrorAction SilentlyContinue + if ( $ExistingCert) { $VerbosePreference = 'Continue' Write-AuditLog "Certificate with subject '$CertName' already exists in the certificate store." @@ -37,7 +40,7 @@ function Get-TkExistingCert { Write-AuditLog "Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { `$_.Subject -eq '$CertName' }" Write-AuditLog '2. If you are comfortable removing the old certificate, and any duplicates, run the following command:' Write-AuditLog "Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { `$_.Subject -eq '$CertName' } | Remove-Item" - Write-AuditLog "If you would like to remove the certificate, confirm the operation when prompted." + Write-AuditLog 'If you would like to remove the certificate, confirm the operation when prompted.' $shouldProcessOperation = 'Remove-Item' $shouldProcessTarget = "Certificate with subject '$CertName' with thumbprint $($ExistingCert.Thumbprint)" if ($PSCmdlet.ShouldProcess($shouldProcessTarget, $shouldProcessOperation)) { @@ -53,5 +56,6 @@ function Get-TkExistingCert { else { Write-AuditLog "Certificate with subject '$CertName' does not exist in the certificate store. Continuing..." } + Write-AuditLog -EndFunction } \ No newline at end of file diff --git a/source/Private/Get-TkExistingSecret.ps1 b/source/Private/Get-TkExistingSecret.ps1 index 3c0c127..971fb61 100644 --- a/source/Private/Get-TkExistingSecret.ps1 +++ b/source/Private/Get-TkExistingSecret.ps1 @@ -26,12 +26,18 @@ function Get-TkExistingSecret { [string]$AppName, [string]$VaultName = 'GraphEmailAppLocalStore' ) - $ExistingSecret = Get-Secret -Name "$AppName" -Vault $VaultName -ErrorAction SilentlyContinue - if ($ExistingSecret) { - return $true + Write-AuditLog -BeginFunction + try { + $ExistingSecret = Get-Secret -Name "$AppName" -Vault $VaultName -ErrorAction SilentlyContinue + if ($ExistingSecret) { + return $true + } + else { + return $false + } } - else { - return $false + finally { + Write-AuditLog -EndFunction } } diff --git a/source/Private/Initialize-TkAppAuthCertificate.ps1 b/source/Private/Initialize-TkAppAuthCertificate.ps1 index 2725eea..50e86a7 100644 --- a/source/Private/Initialize-TkAppAuthCertificate.ps1 +++ b/source/Private/Initialize-TkAppAuthCertificate.ps1 @@ -1,4 +1,3 @@ - <# .SYNOPSIS Initializes or retrieves an authentication certificate for the TkApp. @@ -69,8 +68,8 @@ function Initialize-TkAppAuthCertificate { try { if ($Thumbprint) { # Retrieve an existing certificate - $Cert = Get-ChildItem -Path $CertStoreLocation | Where-Object { $_.Thumbprint -eq $Thumbprint } - if (-not $Cert) { + $cert = Get-ChildItem -Path $CertStoreLocation | Where-Object { $_.Thumbprint -eq $Thumbprint } + if (-not $cert) { throw "Certificate with thumbprint $Thumbprint not found in $CertStoreLocation." } Write-AuditLog "Retrieved certificate with thumbprint $Thumbprint from $CertStoreLocation." @@ -83,7 +82,7 @@ function Initialize-TkAppAuthCertificate { Get-TkExistingCert ` -CertName $Subject ` -ErrorAction Stop - $Cert = New-SelfSignedCertificate -Subject $Subject -CertStoreLocation $CertStoreLocation ` + $cert = New-SelfSignedCertificate -Subject $Subject -CertStoreLocation $CertStoreLocation ` -KeyExportPolicy $KeyExportPolicy -KeySpec Signature -KeyLength 2048 -KeyAlgorithm RSA -HashAlgorithm SHA256 Write-AuditLog "Created new self-signed certificate with subject '$Subject' in $CertStoreLocation." } @@ -93,8 +92,8 @@ function Initialize-TkAppAuthCertificate { } } $output = [PSCustomObject]@{ - CertThumbprint = $Cert.Thumbprint - CertExpires = $Cert.NotAfter.ToString('yyyy-MM-dd HH:mm:ss') + CertThumbprint = $cert.Thumbprint + CertExpires = $cert.NotAfter.ToString('yyyy-MM-dd HH:mm:ss') } if ($AppName) { $output | Add-Member -NotePropertyName 'AppName' -NotePropertyValue $AppName @@ -102,10 +101,10 @@ function Initialize-TkAppAuthCertificate { return $output } catch { + Write-AuditLog -Message "Error occurred: $($_.Exception.Message)" -Severity "Error" throw } finally { Write-AuditLog -EndFunction } -} - +} \ No newline at end of file diff --git a/source/Private/Initialize-TkAppName.ps1 b/source/Private/Initialize-TkAppName.ps1 index 33ab18b..45212de 100644 --- a/source/Private/Initialize-TkAppName.ps1 +++ b/source/Private/Initialize-TkAppName.ps1 @@ -2,19 +2,22 @@ .SYNOPSIS Generates a new application name based on provided prefix, scenario name, and user email. .DESCRIPTION - The Initialize-TkAppName function constructs an application name using a specified prefix, an optional scenario name, - and an optional user email. The generated name includes a domain suffix derived from the environment variable USERDNSDOMAIN. + The Initialize-TkAppName function constructs an application name using a specified prefix, an optional scenario name, and an optional user email. The generated name includes a domain suffix derived from the environment variable USERDNSDOMAIN. .PARAMETER Prefix A short prefix for your app name (2-4 alphanumeric characters). This parameter is mandatory. .PARAMETER ScenarioName - An optional scenario name to include in the app name (e.g., AuditGraphEmail, MemPolicy, etc.). Defaults to "TkEmailApp". + An optional scenario name to include in the app name (for example, AuditGraphEmail, MemPolicy, etc.). Defaults to "TkEmailApp". .PARAMETER UserId - An optional user email to append an "As-[username]" suffix to the app name. The email must be in a valid format. + An optional user email to append an "As-[username]" suffix to the app name. The email must be provided in a valid format. .PARAMETER DoNotUseDomainSuffix - A switch to add session domain suffix to the app name. If not specified, the domain suffix is derived from USERDNSDOMAIN. + A switch to add a session domain suffix to the app name. If not specified, the domain suffix is derived from the USERDNSDOMAIN environment variable. + .INPUTS + System.String + .OUTPUTS + System.String .EXAMPLE PS> Initialize-TkAppName -Prefix "MSN" - Generates an app name with the prefix "MSN" and default scenario name "GraphApp". + Generates an app name with the prefix "MSN" and default scenario name "TkEmailApp". .EXAMPLE PS> Initialize-TkAppName -Prefix "MSN" -ScenarioName "AuditGraphEmail" Generates an app name with the prefix "MSN" and scenario name "AuditGraphEmail". @@ -83,7 +86,7 @@ function Initialize-TkAppName { } catch { $errorMessage = "An error occurred while building the app name: $_" - Write-AuditLog $errorMessage + Write-AuditLog -Message $errorMessage -Severity "Error" # include error severity per StyleGuide throw $errorMessage } finally { diff --git a/source/Private/Initialize-TkModuleEnv.ps1 b/source/Private/Initialize-TkModuleEnv.ps1 index 0aabf76..0c6fae7 100644 --- a/source/Private/Initialize-TkModuleEnv.ps1 +++ b/source/Private/Initialize-TkModuleEnv.ps1 @@ -4,17 +4,21 @@ .DESCRIPTION The Initialize-TkModuleEnv function installs and imports specified PowerShell modules, either public or pre-release versions, based on the provided parameters. It also ensures that the PowerShellGet module is up-to-date and handles the installation scope, requiring elevation for 'AllUsers' scope. The function logs the installation and import process using Write-AuditLog. .PARAMETER PublicModuleNames - An array of public module names to be installed. + An array of public module names to be installed and imported from the PowerShell Gallery. Each module must exist in the gallery. .PARAMETER PublicRequiredVersions - An array of required versions corresponding to the public module names. + An array of required versions corresponding to the public module names. Must match the count of PublicModuleNames. .PARAMETER PrereleaseModuleNames - An array of pre-release module names to be installed. + An array of pre-release module names to be installed from the PowerShell Gallery. Used for modules in preview/beta state. .PARAMETER PrereleaseRequiredVersions - An array of required versions corresponding to the pre-release module names. + An array of required versions corresponding to the pre-release module names. Must match the count of PrereleaseModuleNames. .PARAMETER Scope - The installation scope, either 'AllUsers' or 'CurrentUser'. + The installation scope, either 'AllUsers' (requires elevation) or 'CurrentUser' (default, no elevation needed). .PARAMETER ImportModuleNames - An optional array of module names to be imported after installation. + An optional array of module names to be imported after installation. Useful for importing specific modules from a larger package. + .INPUTS + None. This function does not accept pipeline input. + .OUTPUTS + None. This function does not generate output. .EXAMPLE $params1 = @{ PublicModuleNames = "PSnmap","Microsoft.Graph" @@ -43,47 +47,67 @@ function Initialize-TkModuleEnv { param( [Parameter( ParameterSetName = 'Public', - Mandatory + Mandatory, + HelpMessage = 'Array of public module names to be installed from the PowerShell Gallery' )] [string[]] $PublicModuleNames, + [Parameter( ParameterSetName = 'Public', - Mandatory + Mandatory, + HelpMessage = 'Array of required versions corresponding to the public module names' )] [string[]] $PublicRequiredVersions, + [Parameter( ParameterSetName = 'Prerelease', - Mandatory + Mandatory, + HelpMessage = 'Array of pre-release module names to be installed from the PowerShell Gallery' )] [string[]] $PrereleaseModuleNames, + [Parameter( ParameterSetName = 'Prerelease', - Mandatory + Mandatory, + HelpMessage = 'Array of required versions corresponding to the pre-release module names' )] [string[]] $PrereleaseRequiredVersions, + + [Parameter( + HelpMessage = 'Installation scope, either AllUsers (requires admin) or CurrentUser' + )] [ValidateSet('AllUsers', 'CurrentUser')] [string] $Scope, + + [Parameter( + HelpMessage = 'Optional array of module names to import after installation (useful for submodules)' + )] [string[]] $ImportModuleNames = $null ) + if (-not $script:LogString) { Write-AuditLog -Start - } else { + } + else { Write-AuditLog -BeginFunction } Write-AuditLog '###########################################################' + try { # If Microsoft.Graph is being installed, raise function limit if < 8192. if (($PublicModuleNames -match 'Microsoft.Graph') -or ($PrereleaseModuleNames -match 'Microsoft.Graph')) { if ($script:MaximumFunctionCount -lt 8192) { $script:MaximumFunctionCount = 8192 + Write-AuditLog "Increased maximum function count to $script:MaximumFunctionCount for Microsoft.Graph" -Severity Information } } + # Step 1: Check/Update PowerShellGet if needed $psGetModules = Get-Module -Name PowerShellGet -ListAvailable $hasNonDefaultVer = $false @@ -93,10 +117,12 @@ function Initialize-TkModuleEnv { break } } + if ($hasNonDefaultVer) { # Import the latest version $latestModule = $psGetModules | Sort-Object Version -Descending | Select-Object -First 1 Import-Module -Name $latestModule.Name -RequiredVersion $latestModule.Version -ErrorAction Stop + Write-AuditLog "Imported PowerShellGet version $($latestModule.Version)" -Severity Information } else { if (-not(Test-IsAdmin)) { @@ -104,14 +130,16 @@ function Initialize-TkModuleEnv { throw 'Elevation required to update PowerShellGet!' } else { - Write-AuditLog 'Updating PowerShellGet...' + Write-AuditLog 'Updating PowerShellGet...' -Severity Information [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 Install-Module PowerShellGet -AllowClobber -Force -ErrorAction Stop $psGetModules = Get-Module -Name PowerShellGet -ListAvailable $latestModule = $psGetModules | Sort-Object Version -Descending | Select-Object -First 1 Import-Module -Name $latestModule.Name -RequiredVersion $latestModule.Version -ErrorAction Stop + Write-AuditLog "Updated and imported PowerShellGet version $($latestModule.Version)" -Severity Information } } + # Step 2: Validate scope if ($Scope -eq 'AllUsers') { if (-not(Test-IsAdmin)) { @@ -119,9 +147,10 @@ function Initialize-TkModuleEnv { throw "Elevation required for 'AllUsers' scope." } else { - Write-AuditLog "Installing modules for 'AllUsers' scope." + Write-AuditLog "Installing modules for 'AllUsers' scope." -Severity Information } } + # Step 3: Determine module set $prerelease = $false if ($PSCmdlet.ParameterSetName -eq 'Public') { @@ -133,51 +162,91 @@ function Initialize-TkModuleEnv { $versions = $PrereleaseRequiredVersions $prerelease = $true } + # Step 4: Install/Import each module - foreach ($m in $modules) { - $requiredVersion = $versions[$modules.IndexOf($m)] - $installed = Get-Module -Name $m -ListAvailable | Where-Object { [version]$_.Version -ge [version]$requiredVersion } | Sort-Object Version -Descending | Select-Object -First 1 + for ($i = 0; $i -lt $modules.Count; $i++) { + $m = $modules[$i] + $requiredVersion = $versions[$i] # Using index instead of IndexOf for reliability + $installed = Get-Module -Name $m -ListAvailable | + Where-Object { [version]$_.Version -ge [version]$requiredVersion } | + Sort-Object Version -Descending | + Select-Object -First 1 + $SelectiveImports = $null if ($ImportModuleNames) { $SelectiveImports = $ImportModuleNames | Where-Object { $_ -match $m } } + if (-not $installed) { $msgPrefix = if ($prerelease) { 'PreRelease' }else { 'stable' } Write-AuditLog "The $msgPrefix module $m version $requiredVersion (or higher) is not installed." -Severity Warning Write-AuditLog "Installing $m version $requiredVersion -AllowPrerelease:$prerelease." - Install-Module $m -Scope $Scope -RequiredVersion $requiredVersion -AllowPrerelease:$prerelease -ErrorAction Stop - Write-AuditLog "$m module successfully installed!" + + try { + Install-Module $m -Scope $Scope -RequiredVersion $requiredVersion -AllowPrerelease:$prerelease -ErrorAction Stop + Write-AuditLog "$m module successfully installed!" -Severity Information + } + catch { + Write-AuditLog "Failed to install $m v$requiredVersion`: $(${($_.Exception.Message)})" -Severity Error + throw + } + if ($SelectiveImports) { foreach ($ModName in $SelectiveImports) { Write-AuditLog "Importing $ModName." - Import-Module $ModName -ErrorAction Stop - Write-AuditLog "Successfully imported $ModName." + try { + Import-Module $ModName -ErrorAction Stop + Write-AuditLog "Successfully imported $ModName." -Severity Information + } + catch { + Write-AuditLog "Failed to import $ModName`: $($_.Exception.Message)" -Severity Error + throw + } } } else { Write-AuditLog "Importing $m" - Import-Module $m -ErrorAction Stop - Write-AuditLog "Successfully imported $m" + try { + Import-Module $m -ErrorAction Stop + Write-AuditLog "Successfully imported $m" -Severity Information + } + catch { + Write-AuditLog "Failed to import $m`: $($_.Exception.Message)" -Severity Error + throw + } } } else { - Write-AuditLog "$m v$($installed.Version) exists." + Write-AuditLog "$m v$($installed.Version) exists." -Severity Information if ($SelectiveImports) { foreach ($ModName in $SelectiveImports) { Write-AuditLog "Importing SubModule: $ModName." - Import-Module $ModName -ErrorAction Stop - Write-AuditLog "Imported SubModule: $ModName." + try { + Import-Module $ModName -ErrorAction Stop + Write-AuditLog "Imported SubModule: $ModName." -Severity Information + } + catch { + Write-AuditLog "Failed to import submodule $ModName`: $($_.Exception.Message)" -Severity Error + throw + } } } else { Write-AuditLog "Importing $m" - Import-Module $m -ErrorAction Stop - Write-AuditLog "Imported $m" + try { + Import-Module $m -ErrorAction Stop + Write-AuditLog "Imported $m" -Severity Information + } + catch { + Write-AuditLog "Failed to import $m`: $($_.Exception.Message)" -Severity Error + throw + } } } } } catch { + Write-AuditLog "Module initialization failed: $($_.Exception.Message)" -Severity Error throw } finally { diff --git a/source/Private/Initialize-TkRequiredResourcePermissionObject.ps1 b/source/Private/Initialize-TkRequiredResourcePermissionObject.ps1 index cc72fd1..1ac6204 100644 --- a/source/Private/Initialize-TkRequiredResourcePermissionObject.ps1 +++ b/source/Private/Initialize-TkRequiredResourcePermissionObject.ps1 @@ -4,9 +4,13 @@ .DESCRIPTION The Initialize-TkRequiredResourcePermissionObject function creates a new required resource permission object for Microsoft Graph and specific scenarios. It retrieves service principals by display name, builds an array of MicrosoftGraphRequiredResourceAccess objects, and processes application permissions and scenario-specific permissions. .PARAMETER GraphPermissions - An array of application (app-only) permissions for Microsoft Graph. Default is 'Mail.Send'. + Specifies an array of application (app-only) permissions for Microsoft Graph. Defaults to 'Mail.Send'. This parameter supports multiple permissions. .PARAMETER Scenario - The scenario app version. Currently supports '365Audit'. + Specifies the scenario for which to include additional permissions. Currently supports '365Audit'. + .INPUTS + None + .OUTPUTS + [PSCustomObject] containing the RequiredResourceAccessList. .EXAMPLE PS C:\> Initialize-TkRequiredResourcePermissionObject -GraphPermissions 'User.Read', 'Mail.Send' @@ -16,6 +20,8 @@ Creates a required resource permission object for the '365Audit' scenario, including specific SharePoint and Exchange permissions. .NOTES + Author: DougRios | GraphAppToolkit Module + Last Updated: 2025-03-16 This function requires the Microsoft.Graph PowerShell module. #> function Initialize-TkRequiredResourcePermissionObject { @@ -23,13 +29,13 @@ function Initialize-TkRequiredResourcePermissionObject { param ( [Parameter( Mandatory = $false, - HelpMessage = 'Application (app-only) permissions for Microsoft Graph.' + HelpMessage = 'Specifies an array of application (app-only) permissions for Microsoft Graph. Defaults to ''Mail.Send''. This parameter supports multiple permissions.' )] [string[]] $GraphPermissions = @('Mail.Send'), [Parameter( Mandatory = $false, - HelpMessage = 'Scenario app version.', + HelpMessage = 'Specifies the scenario for which to include additional permissions. Currently supports ''365Audit''.', ParameterSetName = 'Scenario' )] [ValidateSet('365Audit')] @@ -37,6 +43,7 @@ function Initialize-TkRequiredResourcePermissionObject { $Scenario ) process { + # Start logging if (-not $script:LogString) { Write-AuditLog -Start } @@ -45,7 +52,7 @@ function Initialize-TkRequiredResourcePermissionObject { } try { Write-AuditLog '###############################################' - ## 1) Retrieve service principals by DisplayName + # 1) Retrieve service principals by DisplayName Write-AuditLog 'Looking up service principals by display name...' $spGraph = Get-MgServicePrincipal -Filter "DisplayName eq 'Microsoft Graph'" -ErrorAction Stop # 2) Build an array of [MicrosoftGraphRequiredResourceAccess] objects @@ -57,13 +64,15 @@ function Initialize-TkRequiredResourcePermissionObject { # If GraphPermissions is not null or empty, process them if ($GraphPermissions -and $GraphPermissions.Count -gt 0) { if (-not $spGraph) { - throw 'Microsoft Graph Service Principal not found (by display name).' + $errorMessage = 'Microsoft Graph Service Principal not found (by display name).' + Write-AuditLog -Message $errorMessage -Severity Error + throw $errorMessage } Write-AuditLog "Gathering permissions: $($GraphPermissions -join ', ')" $graphRra = [Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess]::new() $graphRra.ResourceAppId = $spGraph.AppId foreach ($permName in $GraphPermissions) { - $foundPerm = $permissionList | Where-Object { $_.Name -eq $permName } #Find-MgGraphPermission -PermissionType Application -All | + $foundPerm = $permissionList | Where-Object { $_.Name -eq $permName } if ($foundPerm) { # If multiple matches, pick the first $graphRra.ResourceAccess += @{ Id = $foundPerm.Id; Type = 'Role' } @@ -77,7 +86,9 @@ function Initialize-TkRequiredResourcePermissionObject { $requiredResourceAccessList += $graphRra } else { - throw "No Graph permissions found for '$($GraphPermissions -join ', ')'. Check the permission names and try again." + $errorMessage = "No Graph permissions found for '$($GraphPermissions -join ', ')'. Check the permission names and try again." + Write-AuditLog -Message $errorMessage -Severity Error + throw $errorMessage } } # endregion @@ -93,7 +104,7 @@ function Initialize-TkRequiredResourcePermissionObject { $requiredResourceAccessList += $spRra # endregion # region Exchange perms - [Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess] $spRra = $null + [Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess] $exRra = $null $exRra = [Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess]::new() $exRra.ResourceAppId = "00000002-0000-0ff1-ce00-000000000000" # Exchange Online $exRra.ResourceAccess += @{ Id = 'dc50a0fb-09a3-484d-be87-e023b12c6440'; Type = 'Role' } @@ -104,11 +115,11 @@ function Initialize-TkRequiredResourcePermissionObject { $result = [PSCustomObject]@{ RequiredResourceAccessList = $requiredResourceAccessList } - # } Write-AuditLog 'Returning context object.' return $result } catch { + Write-AuditLog -Message "An error occurred: $($_.Exception.Message)" -Severity Error throw } finally { diff --git a/source/Private/New-TkAppRegistration.ps1 b/source/Private/New-TkAppRegistration.ps1 index c7ed879..2195d06 100644 --- a/source/Private/New-TkAppRegistration.ps1 +++ b/source/Private/New-TkAppRegistration.ps1 @@ -1,24 +1,26 @@ <# .SYNOPSIS - Creates a new enterprise app registration in Azure AD. + Creates a new enterprise app registration in Azure AD. .DESCRIPTION - The New-TkAppRegistration function creates a new enterprise app registration in Azure AD using the provided display name, certificate thumbprint, and other optional parameters such as required resource access list, sign-in audience, certificate store location, and notes. + The New-TkAppRegistration function creates a new enterprise app registration in Azure AD using the provided display name, certificate thumbprint, and additional optional parameters such as required resource access list, sign-in audience, certificate store location, and descriptive notes about this app's purpose or usage. .PARAMETER DisplayName - The display name for the new app registration. This parameter is mandatory. + The display name for the new app registration, which must be clearly defined and descriptive. .PARAMETER RequiredResourceAccessList - An array of MicrosoftGraphRequiredResourceAccess objects for multi-resource mode. This parameter is optional. + An array of MicrosoftGraphRequiredResourceAccess objects to configure multi-resource access modes securely. .PARAMETER SignInAudience - The sign-in audience for the app registration. Valid values are 'AzureADMyOrg', 'AzureADMultipleOrgs', and 'AzureADandPersonalMicrosoftAccount'. The default value is 'AzureADMyOrg'. + The sign-in audience for the app registration. Valid values include 'AzureADMyOrg', 'AzureADMultipleOrgs', and 'AzureADandPersonalMicrosoftAccount'. .PARAMETER CertThumbprint - The thumbprint of the certificate used to secure this app. This parameter is mandatory. + The thumbprint of the certificate used to secure this app registration, ensuring the certificate is valid and present. .PARAMETER CertStoreLocation - The certificate store location (e.g., "Cert:\CurrentUser\My"). The default value is 'Cert:\CurrentUser\My'. This parameter is optional. + The certificate store location, for example "Cert:\CurrentUser\My", where the certificate is located. .PARAMETER Notes - A descriptive note about this app's purpose or usage. This parameter is optional. + A descriptive note about this app's purpose or usage to provide context and clarity. + .INPUTS + None. + .OUTPUTS + [Microsoft.Graph.PowerShell.Models.MicrosoftGraphApplication1] representing the newly created app registration. .EXAMPLE - $AppRegistration = New-TkAppRegistration -DisplayName "MyApp" -CertThumbprint "ABC123" -Notes "This is a sample app." - - This example creates a new app registration with the display name "MyApp" and the specified certificate thumbprint. A note is also provided. + $AppRegistration = New-TkAppRegistration -DisplayName "MyApp" -CertThumbprint "ABC123" -Notes "This is a sample app registration for enterprise use." .NOTES This function requires the Microsoft.Graph PowerShell module. Required permissions: @@ -39,13 +41,13 @@ function New-TkAppRegistration { [Parameter( Mandatory = $false, HelpMessage = ` - 'Pass an array of MicrosoftGraphRequiredResourceAccess objects for multi-resource mode.' + 'An array of MicrosoftGraphRequiredResourceAccess objects to configure multi-resource access modes securely.' )] [Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess[]] $RequiredResourceAccessList, [Parameter( HelpMessage = ` - 'The sign-in audience for the app registration.' + 'The sign-in audience for the app registration. Valid values include ''AzureADMyOrg'', ''AzureADMultipleOrgs'', and ''AzureADandPersonalMicrosoftAccount''.' )] [ValidateSet('AzureADMyOrg', 'AzureADMultipleOrgs', 'AzureADandPersonalMicrosoftAccount')] [string] @@ -53,20 +55,21 @@ function New-TkAppRegistration { [Parameter( Mandatory = $true, HelpMessage = ` - 'The thumbprint of the certificate used to secure this app.' + 'The thumbprint of the certificate used to secure this app registration, ensuring the certificate is valid and present.' )] [string] $CertThumbprint, [Parameter( Mandatory = $false, HelpMessage = ` - 'The certificate store location (e.g., "Cert:\CurrentUser\My").' + 'The certificate store location (e.g., "Cert:\CurrentUser\My") where the certificate is located.' )] [string] $CertStoreLocation = 'Cert:\CurrentUser\My', [Parameter( Mandatory = $false, - HelpMessage = "A descriptive note about this app's purpose or usage." + HelpMessage = ` + 'A descriptive note about this app''s purpose or usage to provide context and clarity.' )] [string] $Notes @@ -83,14 +86,13 @@ function New-TkAppRegistration { Write-AuditLog "Creating new enterprise app registration for '$DisplayName'." if ($CertThumbprint) { # 1) Retrieve the certificate from the CurrentUser store - $Cert = Get-ChildItem -Path $CertStoreLocation | - Where-Object { $_.Thumbprint -eq $CertThumbprint } - if (-not $Cert) { + $cert = Get-ChildItem -Path $CertStoreLocation | Where-Object { $_.Thumbprint -eq $CertThumbprint } + if (-not $cert) { throw "Certificate with thumbprint $CertThumbprint not found in $CertStoreLocation." } $shouldProcessTarget = "'$DisplayName' for sign-in audience '$SignInAudience' with certificate thumbprint $CertThumbprint." $shouldProcessOperation = 'New-MgApplication' - if ($PSCmdlet.ShouldProcess($shouldProcessTarget , $shouldProcessOperation )) { + if ($PSCmdlet.ShouldProcess($shouldProcessTarget, $shouldProcessOperation)) { $MgApplicationParams = @{ DisplayName = $DisplayName Notes = $Notes @@ -104,18 +106,15 @@ function New-TkAppRegistration { Key = $Cert.RawData } ) - Web = @{ - RedirectUris = @('https://login.microsoftonline.com/common/oauth2/nativeclient') - } + Web = @{ RedirectUris = @('https://login.microsoftonline.com/common/oauth2/nativeclient') } } - $AppRegistration = New-MgApplication @MgApplicationParams + $appRegistration = New-MgApplication @MgApplicationParams } - # 2) Create the new app registration - if (-not $AppRegistration) { + if (-not $appRegistration) { throw "The app creation failed for '$DisplayName'." } - Write-AuditLog "App registration created with app Object ID $($AppRegistration.Id)." - return $AppRegistration + Write-AuditLog "App registration created with app Object ID $($appRegistration.Id)." + return $appRegistration } else { throw 'CertThumbprint is required to create an app registration. No other methods are supported yet.' diff --git a/source/Private/New-TkAppSpOauth2Registration.ps1 b/source/Private/New-TkAppSpOauth2Registration.ps1 index 6fd3c99..296cf98 100644 --- a/source/Private/New-TkAppSpOauth2Registration.ps1 +++ b/source/Private/New-TkAppSpOauth2Registration.ps1 @@ -4,24 +4,24 @@ .DESCRIPTION This function sets up the Service Principal registration for an application in Azure AD. It supports certificate-based authentication and grants OAuth2 permissions to the Service Principal. .PARAMETER AppRegistration - The App Registration object for various properties. + The App Registration object containing various properties. .PARAMETER RequiredResourceAccessList - The Graph Service Principal Id. + The list of required resource access for the Service Principal. .PARAMETER Context The Microsoft Graph context that we are currently in. .PARAMETER Scopes One or more OAuth2 scopes to grant. Defaults to Mail.Send. .PARAMETER AuthMethod - Auth method (placeholder). Currently only "Certificate" is used. Valid values are 'Certificate', 'ClientSecret', 'ManagedIdentity', 'None'. + Authentication method to use. Valid values are 'Certificate', 'ClientSecret', 'ManagedIdentity', 'None'. .PARAMETER CertThumbprint - Certificate thumbprint if using Certificate-based auth. + Certificate thumbprint if using Certificate-based authentication. .PARAMETER CertStoreLocation The certificate store location (e.g., "Cert:\CurrentUser\My"). Defaults to 'Cert:\CurrentUser\My'. .EXAMPLE - $AppRegistration = Get-MgApplication -AppId "your-app-id" - $RequiredResourceAccessList = @() - $Context = [PSCustomObject]@{ TenantId = "your-tenant-id" } - New-TkAppSpOauth2Registration -AppRegistration $AppRegistration -RequiredResourceAccessList $RequiredResourceAccessList -Context $Context + $appRegistration = Get-MgApplication -AppId "your-app-id" + $requiredResourceAccessList = @() + $context = [PSCustomObject]@{ TenantId = "your-tenant-id" } + New-TkAppSpOauth2Registration -AppRegistration $appRegistration -RequiredResourceAccessList $requiredResourceAccessList -Context $context .NOTES This function requires the Microsoft.Graph PowerShell module. #> @@ -31,29 +31,25 @@ function New-TkAppSpOauth2Registration { param( [Parameter( Mandatory = $true, - HelpMessage = ` - 'The App Registration object.' + HelpMessage = 'The App Registration object containing various properties.' )] [Microsoft.Graph.PowerShell.Models.IMicrosoftGraphApplication] $AppRegistration, [Parameter( Mandatory = $true, - HelpMessage = ` - 'The Graph Service Principal Id.' + HelpMessage = 'The list of required resource access for the Service Principal.' )] [PSCustomObject[]] $RequiredResourceAccessList, [Parameter( Mandatory = $true, - HelpMessage = ` - 'The Azure context.' + HelpMessage = 'The Microsoft Graph context that we are currently in.' )] [PSCustomObject] $Context, [Parameter( Mandatory = $false, - HelpMessage = ` - 'One or more OAuth2 scopes to grant. Defaults to Mail.Send.' + HelpMessage = 'One or more OAuth2 scopes to grant. Defaults to Mail.Send.' )] [psobject[]] $Scopes = [PSCustomObject]@{ @@ -61,23 +57,20 @@ function New-TkAppSpOauth2Registration { }, [Parameter( Mandatory = $false, - HelpMessage = ` - 'Auth method (placeholder). Currently only "Certificate" is used.' + HelpMessage = 'Authentication method to use. Valid values are "Certificate", "ClientSecret", "ManagedIdentity", "None".' )] [ValidateSet('Certificate', 'ClientSecret', 'ManagedIdentity', 'None')] [string] $AuthMethod = 'Certificate', [Parameter( Mandatory = $false, - HelpMessage = ` - 'Certificate thumbprint if using Certificate-based auth.' + HelpMessage = 'Certificate thumbprint if using Certificate-based authentication.' )] [string] $CertThumbprint, [Parameter( Mandatory = $false, - HelpMessage = ` - 'The certificate store location (e.g., "Cert:\CurrentUser\My").' + HelpMessage = 'The certificate store location (e.g., "Cert:\CurrentUser\My"). Defaults to "Cert:\CurrentUser\My".' )] [string] $CertStoreLocation = 'Cert:\CurrentUser\My' @@ -94,15 +87,15 @@ function New-TkAppSpOauth2Registration { throw "CertThumbprint is required when AuthMethod is 'Certificate'." } - $Cert = $null + $cert = $null } process { try { # 1. If using certificate auth, retrieve the certificate if ($AuthMethod -eq 'Certificate') { Write-AuditLog "Retrieving certificate with thumbprint $CertThumbprint." - $Cert = Get-ChildItem -Path $CertStoreLocation | Where-Object { $_.Thumbprint -eq $CertThumbprint } - if (-not $Cert) { + $cert = Get-ChildItem -Path $CertStoreLocation | Where-Object { $_.Thumbprint -eq $CertThumbprint } + if (-not $cert) { throw "Certificate with thumbprint $CertThumbprint not found in $CertStoreLocation." } } @@ -114,64 +107,64 @@ function New-TkAppSpOauth2Registration { [void](New-MgServicePrincipal -AppId $AppRegistration.AppId -AdditionalProperties @{}) } # 3. Get the client Service Principal for the created app. - $ClientSp = Get-MgServicePrincipal -Filter "appId eq '$($AppRegistration.AppId)'" - if (-not $ClientSp) { + $clientSp = Get-MgServicePrincipal -Filter "appId eq '$($AppRegistration.AppId)'" + if (-not $clientSp) { Write-AuditLog "Client service principal not found for $($AppRegistration.AppId)." -Severity Error throw 'Unable to find client service principal.' } - $shouldProcessTarget = "'$($ClientSp.DisplayName)' requested scopes for tenant $($Context.TenantId)." + $shouldProcessTarget = "'$($clientSp.DisplayName)' requested scopes for tenant $($Context.TenantId)." $shouldProcessOperation = 'New-MgOauth2PermissionGrant' if ($PSCmdlet.ShouldProcess($shouldProcessTarget, $shouldProcessOperation)) { $i = 0 - foreach ($Resource in $RequiredResourceAccessList) { + foreach ($resource in $RequiredResourceAccessList) { # 4. Combine all scopes into a single space-delimited string switch ($i) { 0 { - $ScopesList = $Scopes.Graph - $ResourceId = (Get-MgServicePrincipal -Filter "DisplayName eq 'Microsoft Graph'").Id + $scopesList = $Scopes.Graph + $resourceId = (Get-MgServicePrincipal -Filter "DisplayName eq 'Microsoft Graph'").Id } 1 { - $ScopesList = $Scopes.SharePoint - $ResourceId = (Get-MgServicePrincipal -Filter "DisplayName eq 'Office 365 SharePoint Online'").Id + $scopesList = $Scopes.SharePoint + $resourceId = (Get-MgServicePrincipal -Filter "DisplayName eq 'Office 365 SharePoint Online'").Id } 2 { - $ScopesList = $Scopes.Exchange - $ResourceId = (Get-MgServicePrincipal -Filter "DisplayName eq 'Office 365 Exchange Online'").Id + $scopesList = $Scopes.Exchange + $resourceId = (Get-MgServicePrincipal -Filter "DisplayName eq 'Office 365 Exchange Online'").Id } ($i > 2) { throw 'Too many resources in RequiredResourceAccessList.' } - Default { Write-AuditLog "No scopes found for $Resource." } + Default { Write-AuditLog "No scopes found for $resource." } } - $combinedScopes = $ScopesList -join ' ' + $combinedScopes = $scopesList -join ' ' # Foreach resource id start - Write-AuditLog "Granting the following scope(s) to Service Principal for: $($ClientSp.DisplayName): $combinedScopes" - $MgOauth2PermissionGrantParams = @{ - ClientId = $ClientSp.Id + Write-AuditLog "Granting the following scope(s) to Service Principal for: $($clientSp.DisplayName): $combinedScopes" + $mgOauth2PermissionGrantParams = @{ + ClientId = $clientSp.Id ConsentType = 'AllPrincipals' - ResourceId = $ResourceId + ResourceId = $resourceId Scope = $combinedScopes } - [void](New-MgOauth2PermissionGrant -BodyParameter $MgOauth2PermissionGrantParams -Confirm:$false -ErrorAction Stop) - Write-AuditLog "Admin consent granted for $ResourceId with scopes: $combinedScopes." + [void](New-MgOauth2PermissionGrant -BodyParameter $mgOauth2PermissionGrantParams -Confirm:$false -ErrorAction Stop) + Write-AuditLog "Admin consent granted for $resourceId with scopes: $combinedScopes." Start-Sleep -Seconds 2 $i++ } } - $RedirectUri = "&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient" + $redirectUri = "&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient" # 5. Build the admin consent URL $adminConsentUrl = ` 'https://login.microsoftonline.com/' ` + $Context.TenantId ` + '/adminconsent?client_id=' ` + $AppRegistration.AppId ` - + $RedirectUri + + $redirectUri Write-Verbose 'Please go to the following URL in your browser to provide admin consent:' -Verbose - Write-AuditLog "`n`n$adminConsentUrl`n" -Severity information + Write-AuditLog "`n`n$adminConsentUrl`n" -Severity information -InformationAction Continue # For each end Write-Verbose 'After providing admin consent, you can use the following command for certificate-based auth:' -Verbose if ($AuthMethod -eq 'Certificate') { $connectGraph = 'Connect-MgGraph -ClientId "' + $AppRegistration.AppId + '" -TenantId "' + - $Context.TenantId + '" -CertificateName "' + $Cert.SubjectName.Name + '"' - Write-AuditLog "`n`n$connectGraph`n" -Severity Information + $Context.TenantId + '" -CertificateName "' + $cert.SubjectName.Name + '"' + Write-AuditLog "`n`n$connectGraph`n" -Severity Information -InformationAction Continue } else { # Placeholder for other auth methods @@ -181,6 +174,7 @@ function New-TkAppSpOauth2Registration { return $adminConsentUrl } catch { + Write-AuditLog -Message "Error occurred: $($_.Exception.Message)" -Severity "Error" throw } } diff --git a/source/Private/New-TkExchangeEmailAppPolicy.ps1 b/source/Private/New-TkExchangeEmailAppPolicy.ps1 index f34f787..9333cac 100644 --- a/source/Private/New-TkExchangeEmailAppPolicy.ps1 +++ b/source/Private/New-TkExchangeEmailAppPolicy.ps1 @@ -26,19 +26,19 @@ function New-TkExchangeEmailAppPolicy { param ( [Parameter( Mandatory = $true, - HelpMessage = 'The application registration object.' + HelpMessage = 'The application registration object. This parameter is mandatory.' )] [Microsoft.Graph.PowerShell.Models.IMicrosoftGraphApplication] $AppRegistration, [Parameter( Mandatory = $true, - HelpMessage = 'The Mail Enabled Sending Group.' + HelpMessage = 'The mail-enabled sending group. This parameter is mandatory.' )] [string] $MailEnabledSendingGroup, [Parameter( Mandatory = $false, - HelpMessage = 'Authorized Sender UserName' + HelpMessage = 'The username of the authorized sender to be added to the mail-enabled sending group. This parameter is optional.' )] [string] $AuthorizedSenderUserName @@ -72,7 +72,7 @@ function New-TkExchangeEmailAppPolicy { } } catch { - Write-AuditLog -Message "Error creating Exchange Application policy: $_" + Write-AuditLog -Message "Error creating Exchange Application policy: $_" -Severity "Error" throw } Write-AuditLog -EndFunction diff --git a/source/Private/Set-TkJsonSecret.ps1 b/source/Private/Set-TkJsonSecret.ps1 index 70cbc55..ba5bb43 100644 --- a/source/Private/Set-TkJsonSecret.ps1 +++ b/source/Private/Set-TkJsonSecret.ps1 @@ -30,14 +30,17 @@ function Set-TkJsonSecret { [OutputType([string])] param( [Parameter( - Mandatory = $true, HelpMessage = 'The name under which to store the secret.' + Mandatory = $true, + HelpMessage = 'The name under which to store the secret. Must be a non-empty string.' )] + [ValidateNotNullOrEmpty()] [string] $Name, [Parameter( Mandatory = $true, - HelpMessage = 'The object to convert to JSON and store.' + HelpMessage = 'The object to convert to JSON and store. Must be a valid PSObject.' )] + [ValidateNotNullOrEmpty()] [PSObject] $InputObject, [Parameter( @@ -59,7 +62,7 @@ function Set-TkJsonSecret { [switch] $Overwrite ) - if (!($script:LogString)) { Write-AuditLog -Start }else { Write-AuditLog -BeginFunction } + if (!($script:logString)) { Write-AuditLog -Start } else { Write-AuditLog -BeginFunction } try { Write-AuditLog '###############################################' # Auto-register vault if missing diff --git a/source/Private/Write-AuditLog.ps1 b/source/Private/Write-AuditLog.ps1 index 89ccf51..6f3c7ce 100644 --- a/source/Private/Write-AuditLog.ps1 +++ b/source/Private/Write-AuditLog.ps1 @@ -18,7 +18,7 @@ .PARAMETER Message The message string to log. .PARAMETER Severity - The severity of the log message. Accepted values are 'Information', 'Warning', and 'Error'. Defaults to 'Information'. + The severity of the log message. Accepted values are 'Information', 'Warning', 'Error'. Defaults to 'Verbose'. .PARAMETER Start Initializes the script-wide log variable and sets the message to "Begin [FunctionName] Log.", where FunctionName is the name of the calling function. .PARAMETER End @@ -30,7 +30,7 @@ .EXAMPLE Write-AuditLog -Message "This is a test message." - Writes a test message with the default severity (Information) to the console and adds it to the log variable. + Writes a test message with the default severity (Verbose) to the console and adds it to the log variable. .EXAMPLE Write-AuditLog -Message "This is a warning message." -Severity "Warning" @@ -92,6 +92,7 @@ function Write-AuditLog { ParameterSetName = 'BeginFunction' )] [switch]$BeginFunction, + ### [Parameter( Mandatory = $false, ParameterSetName = 'EndFunction' @@ -113,15 +114,15 @@ function Write-AuditLog { begin { $ErrorActionPreference = 'SilentlyContinue' # Define variables to hold information about the command that was invoked. - $ModuleName = $Script:MyInvocation.MyCommand.Name -replace '\..*' + $moduleName = $Script:MyInvocation.MyCommand.Name -replace '\..*' $callStack = Get-PSCallStack if ($callStack.Count -gt 1) { - $FuncName = $callStack[1].Command + $funcName = $callStack[1].Command } else { - $FuncName = 'DirectCall' # Or any other default name you prefer + $funcName = 'DirectCall' # Or any other default name you prefer } - $ModuleVer = $MyInvocation.MyCommand.Version.ToString() + $moduleVer = $MyInvocation.MyCommand.Version.ToString() # Set the error action preference to continue. $ErrorActionPreference = 'Continue' } @@ -130,50 +131,50 @@ function Write-AuditLog { if (-not $Start -and -not (Test-Path variable:script:LogString)) { throw "The logging variable is not initialized. Please call Write-AuditLog with the -Start switch or ensure $script:LogString is set." } - $Function = $($FuncName + '.v' + $ModuleVer) + $function = $($funcName + '.v' + $moduleVer) if ($Start) { $script:LogString = @() - $Message = '+++ Begin Log +++ | ' + $Function + ' |' + $Message = '+++ Begin Log +++ | ' + $function + ' |' } elseif ($BeginFunction) { - $Message = '>>> Begin Function Log >>> | ' + $Function + ' |' + $Message = '>>> Begin Function Log >>> | ' + $function + ' |' } $logEntry = [pscustomobject]@{ Time = ((Get-Date).ToString('yyyy-MM-dd hh:mmTss')) - Module = $ModuleName + Module = $moduleName PSVersion = ($PSVersionTable.PSVersion).ToString() PSEdition = ($PSVersionTable.PSEdition).ToString() IsAdmin = $(Test-IsAdmin) User = "$Env:USERDOMAIN\$Env:USERNAME" HostName = $Env:COMPUTERNAME - InvokedBy = $Function + InvokedBy = $function Severity = $Severity Message = $Message RunID = -1 } if ($BeginFunction) { - $maxRunID = ($script:LogString | Where-Object { $_.InvokedBy -eq $Function } | Measure-Object -Property RunID -Maximum).Maximum + $maxRunID = ($script:LogString | Where-Object { $_.InvokedBy -eq $function } | Measure-Object -Property RunID -Maximum).Maximum if ($null -eq $maxRunID) { $maxRunID = -1 } $logEntry.RunID = $maxRunID + 1 } else { - $lastRunID = ($script:LogString | Where-Object { $_.InvokedBy -eq $Function } | Select-Object -Last 1).RunID + $lastRunID = ($script:LogString | Where-Object { $_.InvokedBy -eq $function } | Select-Object -Last 1).RunID if ($null -eq $lastRunID) { $lastRunID = 0 } $logEntry.RunID = $lastRunID } if ($EndFunction) { - $FunctionStart = "$((($script:LogString | Where-Object {$_.InvokedBy -eq $Function -and $_.RunId -eq $lastRunID } | Sort-Object Time)[0]).Time)" - $startTime = ([DateTime]::ParseExact("$FunctionStart", 'yyyy-MM-dd hh:mmTss', $null)) + $functionStart = "$((($script:LogString | Where-Object {$_.InvokedBy -eq $function -and $_.RunId -eq $lastRunID } | Sort-Object Time)[0]).Time)" + $startTime = ([DateTime]::ParseExact("$functionStart", 'yyyy-MM-dd hh:mmTss', $null)) $endTime = Get-Date $timeTaken = $endTime - $startTime - $Message = '<<< End Function Log <<< | ' + $Function + ' | Runtime: ' + "$($timeTaken.Minutes) min $($timeTaken.Seconds) sec" + $Message = '<<< End Function Log <<< | ' + $function + ' | Runtime: ' + "$($timeTaken.Minutes) min $($timeTaken.Seconds) sec" $logEntry.Message = $Message } elseif ($End) { $startTime = ([DateTime]::ParseExact($($script:LogString[0].Time), 'yyyy-MM-dd hh:mmTss', $null)) $endTime = Get-Date $timeTaken = $endTime - $startTime - $Message = '--- End Log | ' + $Function + ' | Runtime: ' + "$($timeTaken.Minutes) min $($timeTaken.Seconds) sec" + $Message = '--- End Log | ' + $function + ' | Runtime: ' + "$($timeTaken.Minutes) min $($timeTaken.Seconds) sec" $logEntry.Message = $Message } $script:LogString += $logEntry @@ -181,9 +182,9 @@ function Write-AuditLog { 'Warning' { Write-Warning ('[WARNING] ! ' + $Message) } - 'Error' { Write-Error ('[ERROR] X - ' + "[$((Get-Date).ToString('yyyy.MM.dd HH:mm:ss.fff'))] " + $FuncName + ' ' + $Message) -ErrorAction Continue } + 'Error' { Write-Error ('[ERROR] X - ' + "[$((Get-Date).ToString('yyyy.MM.dd HH:mm:ss.fff'))] " + $funcName + ' ' + $Message) -ErrorAction Continue } 'Verbose' { Write-Verbose ('~ ' + "[$((Get-Date).ToString('yyyy.MM.dd HH:mm:ss.fff'))] " + $Message) } - Default { Write-Information ("[NFO] [$((Get-Date).ToString('yyyy.MM.dd HH:mm:ss.fff'))] " + $Message) -InformationAction Continue } + Default { Write-Information ("[NFO] [$((Get-Date).ToString('yyyy.MM.dd HH:mm:ss.fff'))] " + $Message) } } } catch { diff --git a/source/Public/New-MailEnabledSendingGroup.ps1 b/source/Public/New-MailEnabledSendingGroup.ps1 index 20c38cc..0f49a10 100644 --- a/source/Public/New-MailEnabledSendingGroup.ps1 +++ b/source/Public/New-MailEnabledSendingGroup.ps1 @@ -21,6 +21,8 @@ .PARAMETER DefaultDomain (DefaultDomain parameter set) The domain portion to be appended to the group alias (e.g. "Alias@DefaultDomain"). This parameter is mandatory when using the 'DefaultDomain' parameter set. + .PARAMETER LogOutputPath + An optional path to output the log file. If not provided, logs will not be written to a file. .EXAMPLE PS C:\> New-MailEnabledSendingGroup -Name "SecureSenders" -DefaultDomain "contoso.com" Creates a new mail-enabled security group named "SecureSenders" with a primary SMTP address @@ -67,47 +69,54 @@ function New-MailEnabledSendingGroup { HelpMessage = 'Specifies the default domain to construct the primary SMTP address (alias@DefaultDomain) for the group.' )] [string] - $DefaultDomain + $DefaultDomain, + [Parameter( + Mandatory = $false, + HelpMessage = 'Optional path to output the log file. If not provided, logs will not be written to a file.' + )] + [string] + $LogOutputPath ) - if (!($script:LogString)) { + if (-not $script:LogString) { Write-AuditLog -Start } else { Write-AuditLog -BeginFunction } try { - # TODO Add confirmation prompt - Connect-TkMsService -ExchangeOnline - if (-not $Alias) { - $Alias = $Name - } - if ($PSCmdlet.ParameterSetName -eq 'DefaultDomain') { - $PrimarySmtpAddress = "$Alias@$DefaultDomain" - } - # Check if the distribution group already exists - $existingGroup = Get-DistributionGroup -Identity $Name -ErrorAction SilentlyContinue - if ($existingGroup) { - # Confirm the group is security-enabled - if ($existingGroup.GroupType -notmatch 'SecurityEnabled') { - throw "Group '$Name' exists but is not SecurityEnabled. Please provide a mail-enabled security group." + if ($PSCmdlet.ShouldProcess("Creating or retrieving mail-enabled security group '$Name'")) { + Connect-TkMsService -ExchangeOnline + if (-not $Alias) { + $Alias = $Name + } + if ($PSCmdlet.ParameterSetName -eq 'DefaultDomain') { + $PrimarySmtpAddress = "$Alias@$DefaultDomain" + } + # Check if the distribution group already exists + $existingGroup = Get-DistributionGroup -Identity $Name -ErrorAction SilentlyContinue + if ($existingGroup) { + # Confirm the group is security-enabled + if ($existingGroup.GroupType -notmatch 'SecurityEnabled') { + throw "Group '$Name' exists but is not SecurityEnabled. Please provide a mail-enabled security group." + } + Write-AuditLog -Message "Distribution group '$Name' already exists. Returning existing group." + return $existingGroup + } + # Create the distribution group + $groupParams = @{ + Name = $Name + Alias = $Alias + PrimarySmtpAddress = $PrimarySmtpAddress + Type = 'security' + } + Write-AuditLog -Message "Creating distribution group with parameters: `n$($groupParams | Out-String)" + $shouldProcessOperation = 'New-DistributionGroup' + $shouldProcessTarget = "'$PrimarySmtpAddress'" + if ($PSCmdlet.ShouldProcess($shouldProcessTarget, $shouldProcessOperation)) { + $group = New-DistributionGroup @groupParams + Write-AuditLog -Message "Distribution group created:`n$($group | Out-String)" + return $group } - Write-AuditLog -Message "Distribution group '$Name' already exists. Returning existing group." - return $existingGroup - } - # Create the distribution group - $groupParams = @{ - Name = $Name - Alias = $Alias - PrimarySmtpAddress = $PrimarySmtpAddress - Type = 'security' - } - Write-AuditLog -Message "Creating distribution group with parameters: `n$($groupParams | Out-String)" - $shouldProcessOperation = 'New-DistributionGroup' - $shouldProcessTarget = "'$PrimarySmtpAddress'" - if ($PSCmdlet.ShouldProcess($shouldProcessTarget, $shouldProcessOperation)) { - $group = New-DistributionGroup @groupParams - Write-AuditLog -Message "Distribution group created:`n$($group | Out-String)" - return $group } } catch { @@ -115,5 +124,8 @@ function New-MailEnabledSendingGroup { } finally { Write-AuditLog -EndFunction + if ($LogOutputPath) { + Write-AuditLog -End -OutputPath $LogOutputPath + } } } diff --git a/source/Public/Publish-TkEmailApp.ps1 b/source/Public/Publish-TkEmailApp.ps1 index b55c10b..2e0d4a2 100644 --- a/source/Public/Publish-TkEmailApp.ps1 +++ b/source/Public/Publish-TkEmailApp.ps1 @@ -27,6 +27,8 @@ If specified, return the parameter splat for use in other functions. .PARAMETER DoNotUseDomainSuffix Switch to add session domain suffix to the app name. + .PARAMETER LogOutput + If specified, log the output to the console. .EXAMPLE # Permissions required for app registration: - 'Application.ReadWrite.All' @@ -115,7 +117,7 @@ This cmdlet requires that the user running the cmdlet have the necessary permissions to create the app and connect to Exchange Online. #> function Publish-TkEmailApp { - [CmdletBinding(ConfirmImpact = 'High', DefaultParameterSetName = 'CreateNewApp')] + [CmdletBinding(SupportsShouldProcess = $true , ConfirmImpact = 'High', DefaultParameterSetName = 'CreateNewApp')] param( # REGION: CREATE NEW APP param set [Parameter( @@ -213,7 +215,14 @@ function Publish-TkEmailApp { 'Switch to add session domain suffix to the app name.' )] [switch] - $DoNotUseDomainSuffix + $DoNotUseDomainSuffix, + [Parameter( + Mandatory = $false, + HelpMessage = ` + 'If specified, log the output to the console to the specified log file.' + )] + [string] + $LogOutput ) begin { <# @@ -256,257 +265,263 @@ function Publish-TkEmailApp { } } process { - switch ($PSCmdlet.ParameterSetName) { - # ------------------------------------------------------ - # ============== SCENARIO 1: CREATE NEW APP ============= - # ------------------------------------------------------ - 'CreateNewApp' { - # 2) Connect to both Graph and Exchange - Connect-TkMsService ` - -MgGraph ` - -ExchangeOnline ` - -GraphAuthScopes $scopesNeeded - # 3) Grab MgContext for tenant info - $Context = Get-MgContext - if (!$Context) { - throw 'Could not retrieve the context for the tenant.' - } - # 1) Validate the user (AuthorizedSenderUserName) is in tenant - $user = Get-MgUser -Filter "Mail eq '$AuthorizedSenderUserName'" - if (-not $user) { - throw "User '$AuthorizedSenderUserName' not found in the tenant." - } - # 2) Build the app context (Mail.Send permission, etc.) - $AppSettings = Initialize-TkRequiredResourcePermissionObject ` - -GraphPermissions 'Mail.Send' - $appName = Initialize-TkAppName ` - -Prefix $AppPrefix ` - -UserId $AuthorizedSenderUserName ` - -DoNotUseDomainSuffix:$DoNotUseDomainSuffix ` - -ErrorAction Stop - # Verify if the secret already exists in the vault - $existingSecret = Get-TkExistingSecret ` - -AppName $appName ` - -VaultName $VaultName ` - -ErrorAction SilentlyContinue - if ($ExistingSecret -and -not $OverwriteVaultSecret) { - throw "Secret '$AppName' already exists in vault '$VaultName'. Use the -OverwriteVaultSecret switch to overwrite it." - } - # Add relevant properties - $AppSettings | Add-Member -NotePropertyName 'User' -NotePropertyValue $user - $AppSettings | Add-Member -NotePropertyName 'AppName' -NotePropertyValue $appName - if ($CertPrefix) { - $updatedString = $appName -replace '(GraphToolKit-)[A-Za-z0-9]{2,4}(?=-)', "`$1$CertPrefix" - $CertificateSubject = "CN=$updatedString" - $ClientCertPrefix = "$certPrefix" - } - else { - $CertificateSubject = "CN=$appName" - $ClientCertPrefix = "$AppPrefix" - } - # 3) Create or retrieve the certificate - $AppAuthCertificateParams = @{ - AppName = $AppSettings.AppName - Thumbprint = $CertThumbprint - Subject = $CertificateSubject - KeyExportPolicy = $KeyExportPolicy - ErrorAction = 'Stop' - } - $CertDetails = Initialize-TkAppAuthCertificate @AppAuthCertificateParams - # 4) Show the proposed object - $proposedObject = [PSCustomObject]@{ - ProposedAppName = $AppSettings.AppName - ProposedCertificateSubject = $CertificateSubject - CertificateThumbprintUsed = $CertDetails.CertThumbprint - CertExpires = $CertDetails.CertExpires - UserPrincipalName = $user.UserPrincipalName - TenantID = $Context.TenantId - Permissions = 'Mail.Send' - PermissionType = 'Application' - ConsentType = 'AllPrincipals' - ExchangePolicyRestrictedToGroup = $MailEnabledSendingGroup - } - Write-AuditLog 'The following object will be created (or configured) in Azure AD:' - Write-AuditLog "`n$($proposedObject | Format-List)`n" - # 5) Only proceed if ShouldProcess is allowed - try { - # Build a hashtable (or PSCustomObject) of the fields you want: - $notesHash = [ordered]@{ - GraphEmailAppFor = $AuthorizedSenderUserName - RestrictedToGroup = $MailEnabledSendingGroup - AppPermissions = 'Mail.Send' - ($ClientCertPrefix + '_ClientIP') = (Invoke-RestMethod ifconfig.me/ip) - ($ClientCertPrefix + '_Host') = $env:COMPUTERNAME + $target = if ($AppPrefix) { $AppPrefix } else { $CertPrefix } + $shouldProcessTarget = "Graph Email App $target" + $shouldProcessOperation = 'Publish-TkEmailApp' + if ($PSCmdlet.ShouldProcess($shouldProcessTarget, $shouldProcessOperation)) { + switch ($PSCmdlet.ParameterSetName) { + # ------------------------------------------------------ + # ============== SCENARIO 1: CREATE NEW APP ============= + # ------------------------------------------------------ + 'CreateNewApp' { + # 2) Connect to both Graph and Exchange + Connect-TkMsService ` + -MgGraph ` + -ExchangeOnline ` + -GraphAuthScopes $scopesNeeded + # 3) Grab MgContext for tenant info + $Context = Get-MgContext + if (!$Context) { + throw 'Could not retrieve the context for the tenant.' } - # Convert that hashtable to a JSON string: - $Notes = $notesHash | ConvertTo-Json #-Compress - # 6) Register the new enterprise app for Graph - $AppRegistrationParams = @{ - DisplayName = $AppSettings.AppName - CertThumbprint = $CertDetails.CertThumbprint - RequiredResourceAccessList = $AppSettings.RequiredResourceAccessList - SignInAudience = 'AzureADMyOrg' - Notes = $Notes - ErrorAction = 'Stop' + # 1) Validate the user (AuthorizedSenderUserName) is in tenant + $user = Get-MgUser -Filter "Mail eq '$AuthorizedSenderUserName'" + if (-not $user) { + throw "User '$AuthorizedSenderUserName' not found in the tenant." } - $appRegistration = New-TkAppRegistration @AppRegistrationParams - # 7) Initialize the service principal, permissions, etc. - $AppSpRegistrationParams = @{ - AppRegistration = $appRegistration - Context = $Context - RequiredResourceAccessList = $AppSettings.RequiredResourceAccessList - Scopes = $permissionsObject - AuthMethod = 'Certificate' - CertThumbprint = $CertDetails.CertThumbprint - ErrorAction = 'Stop' + # 2) Build the app context (Mail.Send permission, etc.) + $AppSettings = Initialize-TkRequiredResourcePermissionObject ` + -GraphPermissions 'Mail.Send' + $appName = Initialize-TkAppName ` + -Prefix $AppPrefix ` + -UserId $AuthorizedSenderUserName ` + -DoNotUseDomainSuffix:$DoNotUseDomainSuffix ` + -ErrorAction Stop + # Verify if the secret already exists in the vault + $existingSecret = Get-TkExistingSecret ` + -AppName $appName ` + -VaultName $VaultName ` + -ErrorAction SilentlyContinue + if ($ExistingSecret -and -not $OverwriteVaultSecret) { + throw "Secret '$AppName' already exists in vault '$VaultName'. Use the -OverwriteVaultSecret switch to overwrite it." } - $ConsentUrl = New-TkAppSpOauth2Registration @AppSpRegistrationParams - [void](Read-Host 'Provide admin consent now, or copy the url and provide admin consent later. Press Enter to continue.') - # 8) Create the Exchange Online policy restricting send - New-TkExchangeEmailAppPolicy ` - -AppRegistration $appRegistration ` - -MailEnabledSendingGroup $MailEnabledSendingGroup ` - -AuthorizedSenderUserName $AuthorizedSenderUserName - # 9) Build final output object - $EmailAppParams = @{ - AppId = $appRegistration.AppId - Id = $appRegistration.Id - AppName = "$($AppSettings.AppName)" - CertificateSubject = $CertificateSubject - AppRestrictedSendGroup = $MailEnabledSendingGroup - CertExpires = $CertDetails.CertExpires - CertThumbprint = $CertDetails.CertThumbprint - ConsentUrl = $ConsentUrl - DefaultDomain = $MailEnabledSendingGroup.Split('@')[1] - SendAsUser = $AppSettings.User.UserPrincipalName.Split('@')[0] - SendAsUserEmail = $AppSettings.User.UserPrincipalName - TenantID = $Context.TenantId + # Add relevant properties + $AppSettings | Add-Member -NotePropertyName 'User' -NotePropertyValue $user + $AppSettings | Add-Member -NotePropertyName 'AppName' -NotePropertyValue $appName + if ($CertPrefix) { + $updatedString = $appName -replace '(GraphToolKit-)[A-Za-z0-9]{2,4}(?=-)', "`$1$CertPrefix" + $CertificateSubject = "CN=$updatedString" + $ClientCertPrefix = "$certPrefix" } - [TkEmailAppParams]$graphEmailApp = Initialize-TkEmailAppParamsObject @EmailAppParams - # 10) Store it as JSON in the vault - $JsonSecretParams = @{ - Name = "CN=$($AppSettings.AppName)" - InputObject = $graphEmailApp - VaultName = $VaultName - Overwrite = $OverwriteVaultSecret - ErrorAction = 'Stop' + else { + $CertificateSubject = "CN=$appName" + $ClientCertPrefix = "$AppPrefix" } - $savedSecretName = Set-TkJsonSecret @JsonSecretParams - Write-AuditLog "Secret '$savedSecretName' saved to vault '$VaultName'." - } - catch { - throw - } - } - # --------------------------------------------------------- - # ============ SCENARIO 2: USE EXISTING APP =============== - # --------------------------------------------------------- - 'UseExistingApp' { - # Grab MgContext for tenant info - Connect-TkMsService ` - -MgGraph ` - -GraphAuthScopes $scopesNeeded - $Context = Get-MgContext - if (!$Context) { - throw 'Could not retrieve the context for the tenant.' - } - $ClientCertPrefix = "$CertPrefix" - # Retrieve the existing app registration by AppId - Write-AuditLog "Looking up existing app with ObjectId: $ExistingAppObjectId" - # Get-MgApplication uses the application object id, not the app id - $existingApp = Get-MgApplication -ApplicationId $ExistingAppObjectId -ErrorAction Stop - if (-not $existingApp) { - throw "Could not find an existing application with AppId '$ExistingAppObjectId'." - } - if (!($existingApp | Where-Object { $_.DisplayName -like 'GraphToolKit-*' })) { - throw "The existing app with AppId '$ExistingAppObjectId' is not a GraphToolKit app." - } - $updatedString = $existingApp.DisplayName -replace '(GraphToolKit-)[A-Za-z0-9]{2,4}(?=-)', "`$1$CertPrefix" - # Retrieve or create the certificate - $certParams = @{ - AppName = $updatedString - Thumbprint = $CertThumbprint - Subject = "CN=$updatedString" - KeyExportPolicy = $KeyExportPolicy - ErrorAction = 'Stop' - } - $certDetails = Initialize-TkAppAuthCertificate @certParams - Write-AuditLog "Attaching certificate (Thumbprint: $($certDetails.CertThumbprint)) to existing app '$($existingApp.DisplayName)'." - # Merge or append the new certificate to the existing KeyCredentials - $currentKeys = $existingApp.KeyCredentials - $newCert = @{ - Type = 'AsymmetricX509Cert' - Usage = 'Verify' - Key = (Get-ChildItem -Path Cert:\CurrentUser\My | - Where-Object { $_.Thumbprint -eq $certDetails.CertThumbprint }).RawData - DisplayName = "CN=$updatedString" - } - # If you want to specify start/end date, you can do so as well: - # $newCert.StartDateTime = (Get-Date) - # $newCert.EndDateTime = (Get-Date).AddYears(1) - # Append the new cert to existing - $mergedKeys = $currentKeys + $newCert - $existingNotesRaw = $existingApp.Notes - if (-not [string]::IsNullOrEmpty($existingNotesRaw)) { + # 3) Create or retrieve the certificate + $AppAuthCertificateParams = @{ + AppName = $AppSettings.AppName + Thumbprint = $CertThumbprint + Subject = $CertificateSubject + KeyExportPolicy = $KeyExportPolicy + ErrorAction = 'Stop' + } + $CertDetails = Initialize-TkAppAuthCertificate @AppAuthCertificateParams + # 4) Show the proposed object + $proposedObject = [PSCustomObject]@{ + ProposedAppName = $AppSettings.AppName + ProposedCertificateSubject = $CertificateSubject + CertificateThumbprintUsed = $CertDetails.CertThumbprint + CertExpires = $CertDetails.CertExpires + UserPrincipalName = $user.UserPrincipalName + TenantID = $Context.TenantId + Permissions = 'Mail.Send' + PermissionType = 'Application' + ConsentType = 'AllPrincipals' + ExchangePolicyRestrictedToGroup = $MailEnabledSendingGroup + } + Write-AuditLog 'The following object will be created (or configured) in Azure AD:' + Write-AuditLog ($proposedObject | Format-List | Out-String) + # 5) Only proceed if ShouldProcess is allowed try { - $notesObject = $existingNotesRaw | ConvertFrom-Json -ErrorAction Stop + # Build a hashtable (or PSCustomObject) of the fields you want: + $notesHash = [ordered]@{ + GraphEmailAppFor = $AuthorizedSenderUserName + RestrictedToGroup = $MailEnabledSendingGroup + AppPermissions = 'Mail.Send' + ($ClientCertPrefix + '_ClientIP') = (Invoke-RestMethod ifconfig.me/ip) + ($ClientCertPrefix + '_Host') = $env:COMPUTERNAME + } + # Convert that hashtable to a JSON string: + $Notes = $notesHash | ConvertTo-Json #-Compress + # 6) Register the new enterprise app for Graph + $AppRegistrationParams = @{ + DisplayName = $AppSettings.AppName + CertThumbprint = $CertDetails.CertThumbprint + RequiredResourceAccessList = $AppSettings.RequiredResourceAccessList + SignInAudience = 'AzureADMyOrg' + Notes = $Notes + ErrorAction = 'Stop' + } + $appRegistration = New-TkAppRegistration @AppRegistrationParams + # 7) Initialize the service principal, permissions, etc. + $AppSpRegistrationParams = @{ + AppRegistration = $appRegistration + Context = $Context + RequiredResourceAccessList = $AppSettings.RequiredResourceAccessList + Scopes = $permissionsObject + AuthMethod = 'Certificate' + CertThumbprint = $CertDetails.CertThumbprint + ErrorAction = 'Stop' + } + $ConsentUrl = New-TkAppSpOauth2Registration @AppSpRegistrationParams + [void](Read-Host 'Provide admin consent now, or copy the url and provide admin consent later. Press Enter to continue.') + # 8) Create the Exchange Online policy restricting send + New-TkExchangeEmailAppPolicy ` + -AppRegistration $appRegistration ` + -MailEnabledSendingGroup $MailEnabledSendingGroup ` + -AuthorizedSenderUserName $AuthorizedSenderUserName + # 9) Build final output object + $EmailAppParams = @{ + AppId = $appRegistration.AppId + Id = $appRegistration.Id + AppName = "$($AppSettings.AppName)" + CertificateSubject = $CertificateSubject + AppRestrictedSendGroup = $MailEnabledSendingGroup + CertExpires = $CertDetails.CertExpires + CertThumbprint = $CertDetails.CertThumbprint + ConsentUrl = $ConsentUrl + DefaultDomain = $MailEnabledSendingGroup.Split('@')[1] + SendAsUser = $AppSettings.User.UserPrincipalName.Split('@')[0] + SendAsUserEmail = $AppSettings.User.UserPrincipalName + TenantID = $Context.TenantId + } + [TkEmailAppParams]$graphEmailApp = Initialize-TkEmailAppParamsObject @EmailAppParams + # 10) Store it as JSON in the vault + $JsonSecretParams = @{ + Name = "CN=$($AppSettings.AppName)" + InputObject = $graphEmailApp + VaultName = $VaultName + Overwrite = $OverwriteVaultSecret + ErrorAction = 'Stop' + } + $savedSecretName = Set-TkJsonSecret @JsonSecretParams + Write-AuditLog "Secret '$savedSecretName' saved to vault '$VaultName'." } catch { - Write-AuditLog 'Existing .Notes was not valid JSON; ignoring it.' - $notesObject = [ordered]@{} + throw } } - else { - $notesObject = [ordered]@{} - } - # Add your new properties each time the function runs - $notesObject | Add-Member -NotePropertyName ($ClientCertPrefix + '_ClientIP') -NotePropertyValue (Invoke-RestMethod ifconfig.me/ip) - $notesObject | Add-Member -NotePropertyName ($ClientCertPrefix + '_Host') -NotePropertyValue $env:COMPUTERNAME - $updatedNotes = $notesObject | ConvertTo-Json #-Compress - if (($updatedNotes.length -gt 1024)) { - throw 'The Notes object is too large. Please reduce the size of the Notes object.' - } - try { - # Update the application with the new KeyCredentials array - $updateAppParams = @{ - ApplicationId = $existingApp.Id - KeyCredentials = $mergedKeys - Notes = $updatedNotes - ErrorAction = 'Stop' + # --------------------------------------------------------- + # ============ SCENARIO 2: USE EXISTING APP =============== + # --------------------------------------------------------- + 'UseExistingApp' { + # Grab MgContext for tenant info + Connect-TkMsService ` + -MgGraph ` + -GraphAuthScopes $scopesNeeded + $Context = Get-MgContext + if (!$Context) { + throw 'Could not retrieve the context for the tenant.' } - Update-MgApplication @updateAppParams | Out-Null - # Build an output object similar to "new" scenario - $EmailAppParams = @{ - AppId = $existingApp.AppId - Id = $existingApp.Id - AppName = "$updatedString" - CertificateSubject = "CN=$updatedString" - AppRestrictedSendGroup = $notesObject.RestrictedToGroup - CertExpires = $CertDetails.CertExpires - CertThumbprint = $CertDetails.CertThumbprint - ConsentUrl = $null - DefaultDomain = ($notesObject.GraphEmailAppFor.Split('@')[1]) - SendAsUser = ($notesObject.GraphEmailAppFor.Split('@')[0]) - SendAsUserEmail = $notesObject.GraphEmailAppFor - TenantID = $Context.TenantID + $ClientCertPrefix = "$CertPrefix" + # Retrieve the existing app registration by AppId + Write-AuditLog "Looking up existing app with ObjectId: $ExistingAppObjectId" + # Get-MgApplication uses the application object id, not the app id + $existingApp = Get-MgApplication -ApplicationId $ExistingAppObjectId -ErrorAction Stop + if (-not $existingApp) { + throw "Could not find an existing application with AppId '$ExistingAppObjectId'." } - [TkEmailAppParams]$graphEmailApp = Initialize-TkEmailAppParamsObject @EmailAppParams - # Store updated info in the vault - $JsonSecretParams = @{ - Name = "CN=$updatedString" - InputObject = $graphEmailApp - VaultName = $VaultName - Overwrite = $OverwriteVaultSecret - ErrorAction = 'Stop' + if (!($existingApp | Where-Object { $_.DisplayName -like 'GraphToolKit-*' })) { + throw "The existing app with AppId '$ExistingAppObjectId' is not a GraphToolKit app." + } + $updatedString = $existingApp.DisplayName -replace '(GraphToolKit-)[A-Za-z0-9]{2,4}(?=-)', "`$1$CertPrefix" + # Retrieve or create the certificate + $certParams = @{ + AppName = $updatedString + Thumbprint = $CertThumbprint + Subject = "CN=$updatedString" + KeyExportPolicy = $KeyExportPolicy + ErrorAction = 'Stop' + } + $certDetails = Initialize-TkAppAuthCertificate @certParams + Write-AuditLog "Attaching certificate (Thumbprint: $($certDetails.CertThumbprint)) to existing app '$($existingApp.DisplayName)'." + # Merge or append the new certificate to the existing KeyCredentials + $currentKeys = $existingApp.KeyCredentials + $newCert = @{ + Type = 'AsymmetricX509Cert' + Usage = 'Verify' + Key = (Get-ChildItem -Path Cert:\CurrentUser\My | + Where-Object { $_.Thumbprint -eq $certDetails.CertThumbprint }).RawData + DisplayName = "CN=$updatedString" + } + # If you want to specify start/end date, you can do so as well: + # $newCert.StartDateTime = (Get-Date) + # $newCert.EndDateTime = (Get-Date).AddYears(1) + # Append the new cert to existing + $mergedKeys = $currentKeys + $newCert + $existingNotesRaw = $existingApp.Notes + if (-not [string]::IsNullOrEmpty($existingNotesRaw)) { + try { + $notesObject = $existingNotesRaw | ConvertFrom-Json -ErrorAction Stop + } + catch { + Write-AuditLog 'Existing .Notes was not valid JSON; ignoring it.' + $notesObject = [ordered]@{} + } + } + else { + $notesObject = [ordered]@{} + } + # Add your new properties each time the function runs + $notesObject | Add-Member -NotePropertyName ($clientCertPrefix + '_ClientIP') -NotePropertyValue (Invoke-RestMethod ifconfig.me/ip) + $notesObject | Add-Member -NotePropertyName ($clientCertPrefix + '_Host') -NotePropertyValue $env:COMPUTERNAME + $updatedNotes = $notesObject | ConvertTo-Json #-Compress + if (($updatedNotes.length -gt 1024)) { + throw 'The Notes object is too large. Please reduce the size of the Notes object.' + } + try { + # Update the application with the new KeyCredentials array + $updateAppParams = @{ + ApplicationId = $existingApp.Id + KeyCredentials = $mergedKeys + Notes = $updatedNotes + ErrorAction = 'Stop' + } + Update-MgApplication @updateAppParams | Out-Null + # Build an output object similar to "new" scenario + $emailAppParams = @{ + AppId = $existingApp.AppId + Id = $existingApp.Id + AppName = "$updatedString" + CertificateSubject = "CN=$updatedString" + AppRestrictedSendGroup = $notesObject.RestrictedToGroup + CertExpires = $certDetails.CertExpires + CertThumbprint = $certDetails.CertThumbprint + ConsentUrl = $null + DefaultDomain = ($notesObject.GraphEmailAppFor.Split('@')[1]) + SendAsUser = ($notesObject.GraphEmailAppFor.Split('@')[0]) + SendAsUserEmail = $notesObject.GraphEmailAppFor + TenantID = $context.TenantId + } + [TkEmailAppParams]$graphEmailApp = Initialize-TkEmailAppParamsObject @emailAppParams + # Store updated info in the vault + $jsonSecretParams = @{ + Name = "CN=$updatedString" + InputObject = $graphEmailApp + VaultName = $VaultName + Overwrite = $OverwriteVaultSecret + ErrorAction = 'Stop' + } + $savedSecretName = Set-TkJsonSecret @JsonSecretParams + Write-AuditLog "Secret for existing app saved as '$savedSecretName' in vault '$VaultName'." + } + catch { + throw } - $savedSecretName = Set-TkJsonSecret @JsonSecretParams - Write-AuditLog "Secret for existing app saved as '$savedSecretName' in vault '$VaultName'." - } - catch { - throw } - } - } # end switch + } # end switch + } + } end { if ($ReturnParamSplat -and $graphEmailApp) { @@ -515,7 +530,9 @@ function Publish-TkEmailApp { elseif ($graphEmailApp) { return $graphEmailApp } - Write-AuditLog -EndFunction + if ($LogOutput) { + Write-AuditLog -End -LogOutput $LogOutput + } } } From 103f7adbc6230dba84a38556ee3430ae6d1041d2 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sun, 16 Mar 2025 20:12:08 -0500 Subject: [PATCH 13/15] docs: update help --- .gitignore | 3 +- CHANGELOG.md | 1 + README2.md | 15 +- docs/index.html | 15 +- help/GraphAppToolkit.md | 1 + help/New-MailEnabledSendingGroup.md | 72 +-- help/Publish-TkEmailApp.md | 78 +-- help/Publish-TkM365AuditApp.md | 56 +-- help/Publish-TkMemPolicyManagerApp.md | 60 +-- help/Send-TkEmailAppMessage.md | 106 ++-- source/en-US/GraphAppToolkit-help.xml | 698 ++++++++++++++------------ 11 files changed, 585 insertions(+), 520 deletions(-) diff --git a/.gitignore b/.gitignore index 23fea0e..e78aadc 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,5 @@ node_modules package-lock.json ZZBuild-Help.ps1 test1.ps1 -helpdoc.ps1 \ No newline at end of file +helpdoc.ps1 +StyleGuide.md \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d60aea..648d973 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added Get-TkMsalToken cmdlet to retrieve an MSAL token using API calls. - Added Managed Identity support for Get-TkMsalToken cmdlet (Needs to be tested). - SecureString support for Get-TkMsalToken cmdlet. +- Formatting alignment for cmdlets. ### Fixed diff --git a/README2.md b/README2.md index fcf6fc1..56db3cf 100644 --- a/README2.md +++ b/README2.md @@ -5,9 +5,9 @@ Creates or retrieves a mail-enabled security group with a custom or default doma ### Syntax ```powershell -New-MailEnabledSendingGroup -Name [-Alias ] -PrimarySmtpAddress [-WhatIf] [-Confirm] [] +New-MailEnabledSendingGroup -Name [-Alias ] -PrimarySmtpAddress [-LogOutputPath ] [-WhatIf] [-Confirm] [] -New-MailEnabledSendingGroup -Name [-Alias ] -DefaultDomain [-WhatIf] [-Confirm] [] +New-MailEnabledSendingGroup -Name [-Alias ] -DefaultDomain [-LogOutputPath ] [-WhatIf] [-Confirm] [] @@ -20,6 +20,7 @@ New-MailEnabledSendingGroup -Name [-Alias ] -DefaultDomain Alias | | An optional alias for the group. If omitted, the group name is used as the alias. | false | false | | | PrimarySmtpAddress | | \(CustomDomain parameter set\\) The full SMTP address for the group \(e.g. "MyGroup@contoso.com"\\). This parameter is mandatory when using the 'CustomDomain' parameter set. | true | false | | | DefaultDomain | | \(DefaultDomain parameter set\\) The domain portion to be appended to the group alias \(e.g. "Alias@DefaultDomain"\\). This parameter is mandatory when using the 'DefaultDomain' parameter set. | true | false | | +| LogOutputPath | | An optional path to output the log file. If not provided, logs will not be written to a file. | false | false | | | WhatIf | wi | | false | false | | | Confirm | cf | | false | false | | ### Inputs @@ -270,16 +271,16 @@ Send-TkEmailAppMessage -AppId -TenantId -CertThumbprint AppName | | \[Vault Parameter Set Only\] The name of the pre-created Microsoft Graph Email App \(stored in GraphEmailAppLocalStore\\). Used only if the 'Vault' parameter set is chosen. The function retrieves the AppId, TenantId, and certificate thumbprint from the vault entry. | true | false | | -| AppId | | \[Manual Parameter Set Only\] The Azure AD application \(client\\) ID to use for sending the email. Must be used together with TenantId and CertThumbprint in the 'Manual' parameter set. | true | false | | -| TenantId | | \[Manual Parameter Set Only\] The Azure AD tenant ID \(GUID or domain name\\). Must be used together with AppId and CertThumbprint in the 'Manual' parameter set. | true | false | | -| CertThumbprint | | \[Manual Parameter Set Only\] The certificate thumbprint \(in Cert:\\CurrentUser\\My\\) used for authenticating as the Azure AD app. Must be used together with AppId and TenantId in the 'Manual' parameter set. | true | false | | +| AppName | | \[Vault Parameter Set Only\\] The name of the pre-created Microsoft Graph Email App \(stored in GraphEmailAppLocalStore\\). Used only if the 'Vault' parameter set is chosen. The function retrieves the AppId, TenantId, and certificate thumbprint from the vault entry. | true | false | | +| AppId | | \[Manual Parameter Set Only\\] The Azure AD application \(client\\) ID to use for sending the email. Must be used together with TenantId and CertThumbprint in the 'Manual' parameter set. | true | false | | +| TenantId | | \[Manual Parameter Set Only\\] The Azure AD tenant ID \(GUID or domain name\\). Must be used together with AppId and CertThumbprint in the 'Manual' parameter set. | true | false | | +| CertThumbprint | | \[Manual Parameter Set Only\\] The certificate thumbprint \(in Cert:\\CurrentUser\\My\\) used for authenticating as the Azure AD app. Must be used together with AppId and TenantId in the 'Manual' parameter set. | true | false | | | To | | The email address of the recipient. | true | false | | | FromAddress | | The email address of the sender who is authorized to send email as configured in the Graph Email App. | true | false | | | Subject | | The subject line of the email. | true | false | | | EmailBody | | The body text of the email. | true | false | | | AttachmentPath | | An array of file paths for any attachments to include in the email. Each path must exist as a leaf file. | false | false | | -| VaultName | | \[Vault Parameter Set Only\] The name of the vault to retrieve the GraphEmailApp object. Default is 'GraphEmailAppLocalStore'. | false | false | GraphEmailAppLocalStore | +| VaultName | | \[Vault Parameter Set Only\\] The name of the vault to retrieve the GraphEmailApp object. Default is 'GraphEmailAppLocalStore'. | false | false | GraphEmailAppLocalStore | | WhatIf | wi | | false | false | | | Confirm | cf | | false | false | | ### Note diff --git a/docs/index.html b/docs/index.html index 49154fe..a7f9411 100644 --- a/docs/index.html +++ b/docs/index.html @@ -2,7 +2,7 @@