<# .SYNOPSIS Create a Temporary Access Pass code for new hires that have not set up any Authentication Methods so far .DESCRIPTION This script is intended to be run as an Azure Automation Runbook or as a standalone script. Before generating a Temporary Access Pass, the script checks if the user has set up any Authentication Methods and has reached its hire date. If the user has not set up any Authentication Methods, a Temporary Access Pass is created. Depending on the configuration, the pass is output or sent to the user's manager via email. The email notification can be customized using a template with variables. Automatic email branding is supported by retrieving tenant branding images and information. Based on the manager's mailbox language, the email notification is sent in the appropriate language. As a fallback, the manager's preferredLanguage attribute is used. The script can be triggered by a webhook request, which is validated using an HMAC signature. The webhook request must include a JSON object with the UserId and may optionally include StartDateTime, LifetimeInMinutes, IsUsableOnce, and EmailLanguage. Other parameters are intentionally ignored to prevent sending unexpected emails. The webhook request must be signed using the shared secret key set as Automation Variable 'AuthConfig_WebhookSignatureKey'. An example function for the client request called 'Get-HmacSignedHeaders' can be found in the FUNCTIONS region down below. **Azure Automation & Managed Identity Setup Requirements:** - Run this script in an Azure Automation account using a system-assigned managed identity. - The managed identity must have the following Microsoft Graph API permissions: • User.Read.All • UserAuthenticationMethod.ReadWrite.All • Policy.Read.All • Directory.Read.All • MailboxSettings.Read • Mail.Send - Ensure admin consent is granted for all required permissions. - Configure the 'AuthConfig_WebhookSignatureKey' Automation Variable with the shared secret used for HMAC validation. - Configure the 'TAPConfig_SenderEmailAddress' Automation Variable with the sender email address. .PARAMETER UserId User account identifier. May be an Entra Identity Object ID or User Principal Name (UPN). .PARAMETER StartDateTime The date and time when the Temporary Access Pass becomes available to use. Needs to be in Universal Time (UTC). .PARAMETER LifetimeInMinutes The lifetime of the Temporary Access Pass in minutes starting at StartDateTime. Must be between 10 and 43200 inclusive (equivalent to 30 days). .PARAMETER IsUsableOnce Determines whether the pass is limited to a one-time use. If true, the pass can be used once; if false, the pass can be used multiple times within the Temporary Access Pass lifetime. .PARAMETER SendEmailToManager Send the Temporary Access Pass to the user's manager via email. .PARAMETER SenderEmailAddress The email address of the sender. Can be a user or shared mailbox address. When run interactively, the sender address is automatically set to the signed-in user's email address. When running in Azure Automation, the sender address must either be provided directly, in the webhook configuration, or as Automation Variable TAPConfig_SenderEmailAddress. .PARAMETER EmailLanguage The language to use for the email notification. If not specified, the user's preferred language is used. The built-in template supports English (en), German (de), and French (fr). The parameter may also be set as an Automation Variable TAPConfig_EmailLanguage. .PARAMETER EmailSubject Custom email subject to use when sending notification to manager. If not specified, the default subject is used. Variables may be used, see EmailTemplate parameter for available variables. Multiple languages can be defined using the JSON format: {"en": "English subject", "de": "German subject", "fr": "French subject"} The parameter may also be set as an Automation Variable TAPConfig_EmailSubject. .PARAMETER EmailTitle Custom email title to use when sending notification to manager. If not specified, the default title is used. Variables may be used, see EmailTemplate parameter for available variables. Multiple languages can be defined using the JSON format: {"en": "English title", "de": "German title", "fr": "French title"} The parameter may also be set as an Automation Variable TAPConfig_EmailTitle. .PARAMETER EmailSalutation Custom email salutation to use when sending notification to manager. If not specified, the default salutation is used. Variables may be used, see EmailTemplate parameter for available variables. Multiple languages can be defined using the JSON format: {"en": "English salutation", "de": "German salutation", "fr": "French salutation"} The parameter may also be set as an Automation Variable TAPConfig_EmailSalutation. .PARAMETER EmailClosing Custom email closing phrase to use when sending notification to manager. If not specified, the default closing phrase is used. Variables may be used, see EmailTemplate parameter for available variables. Multiple languages can be defined using the JSON format: {"en": "English closing", "de": "German closing", "fr": "French closing"} The parameter may also be set as an Automation Variable TAPConfig_EmailClosing. .PARAMETER EmailBodyText Custom email body text to use when sending notification to manager. If not specified, the default body text is used. Variables may be used, see EmailTemplate parameter for available variables. Possible text variants are: "TAPCreated", "UserBlocked", "TAPNotEnabled", "TAPExpiredWithOtherMethodsConfigured", "TAPActiveWithOtherMethodsConfigured", "OtherMethodsConfigured". The value must be a JSON string with the format {"en": {"TAPCreated": "English text", "UserBlocked": "English text", ...}, "de": {"TAPCreated": "German text", "UserBlocked": "German text", ...}, ...} The parameter may also be set as an Automation Variable TAPConfig_EmailBodyText. .PARAMETER EmailTemplate Custom email template to use when sending notification to manager. Uses {{ variable }} syntax for variable substitution. Available variables: userGivenName, userName, userPrincipalName, managerName, temporaryAccessPass, lifetimeInMinutes, expirationTime, startDateTime, emailTitle, emailSalutation, emailClosing, emailBodyText, emailBodyAltertBanner, emailBodyFooterHint. Images can be embedded using {{ image:imageName }} placeholders. Multiple languages can be defined using the JSON format: {"en": "English template", "de": "German template", "fr": "French template"} The parameter may also be set as an Automation Variable TAPConfig_EmailTemplate. .PARAMETER EmailImagesJson A JSON string representing image references to embed in the email template. Format: {"imageName1": "base64string1", "imageName2": "base64string2"} Each image can be referenced in the template as {{ image:imageName1 }} Example: '{"logo": "data:image/png;base64,iVBORw0KG..."}' The parameter may also be set as an Automation Variable TAPConfig_EmailImagesJson. .PARAMETER UseHtmlEmail When specified, sends the email as HTML format. Default is true. Set to false to send plain text email. The parameter may also be set as an Automation Variable TAPConfig_UseHtmlEmail. .PARAMETER OutputJson Output the result in JSON format .PARAMETER OutputText Output the Temporary Access Pass only. .PARAMETER Simulate Same as -WhatIf parameter but makes it available for Azure Automation. .PARAMETER WebhookSignatureKey Shared secret used to validate incoming webhook requests using HMAC HTTP request header authorization. When running in Azure Automation, this parameter is automatically populated from the 'AuthConfig_WebhookSignatureKey' automation variable. .PARAMETER WebhookData Webhook data object for Azure Automation. The following parameters may be overridden by the webhook request: UserId, StartDateTime, LifetimeInMinutes, IsUsableOnce, EmailLanguage .NOTES Filename: New-Temporary-Access-Pass-for-Initial-MFA-Setup-V2.ps1 Author: Julian Pawlowski, Workoho GmbH Version: 2.3.1 #> #Requires -Version 7.4 #Requires -Modules @{ ModuleName='Microsoft.Graph.Authentication'; ModuleVersion='2.0' } using namespace System.Collections.Generic using namespace System.Text [CmdletBinding( SupportsShouldProcess, ConfirmImpact = 'Medium' )] param ( [string]$UserId, [datetime]$StartDateTime, [int64]$LifetimeInMinutes, [bool]$IsUsableOnce = $false, [bool]$SendEmailToManager = $false, [string]$SenderEmailAddress, [string]$EmailLanguage, [string]$EmailSubject, [string]$EmailTitle, [string]$EmailSalutation, [string]$EmailClosing, [string]$EmailBodyText, [string]$EmailTemplate, [string]$EmailImagesJson, [bool]$UseHtmlEmail = $true, [bool]$OutJson = $false, [bool]$OutText = $false, [bool]$Simulate = $false, [securestring]$WebhookSignatureKey, [object]$WebhookData ) #region FUNCTIONS function Get-HmacSignedHeaders { <# .SYNOPSIS Generates signed headers for HMAC authentication for Azure Automation webhook requests. .DESCRIPTION Generates signed headers for HMAC authentication based on a shared secret (provided as SecureString), webhook URL, and request body content. Includes timestamp, nonce, and content hash, all covered in the HMAC signature. SecureString is converted as late as possible and securely cleared after use. This function is intended to be used when calling Azure Automation webhooks or other APIs requiring signed requests for enhanced security. ------------------------------------------ 🔐 Secure Shared Secret Retrieval Options: ------------------------------------------ Always retrieve secrets securely from encrypted sources, ensuring they are stored and used as SecureStrings: 1️⃣ Azure Key Vault (Recommended for cloud/hybrid environments) Connect-AzAccount -Identity $sharedSecret = (Get-AzKeyVaultSecret -VaultName "" -Name "HmacSharedSecret").SecretValue 2️⃣ Azure Automation Encrypted Variable (inside Automation Account only) $sharedSecret = (Get-AutomationVariable -Name "SharedSecret") | ConvertTo-SecureString -AsPlainText -Force 3️⃣ Windows Credential Manager (if running on Windows, via SecretManagement module) Import-Module Microsoft.PowerShell.SecretManagement $sharedSecret = (Get-StoredCredential -Target "HmacSharedSecret").Password 4️⃣ Encrypted local file (protected by ACLs; not recommended for cloud, acceptable in controlled environments) $sharedSecret = (Get-Content -Path "C:\Secrets\HmacSecret.txt" -Raw) | ConvertTo-SecureString -AsPlainText -Force ❌ For demonstration purposes only (NEVER hardcode secrets in production): $sharedSecret = ConvertTo-SecureString -String "MySuperSecretKey" -AsPlainText -Force .EXAMPLE # Example usage: $sharedSecret = (Get-AutomationVariable -Name "SharedSecret") | ConvertTo-SecureString -AsPlainText -Force $webhookUrl = "https:///webhooks" $body = '{"param1":"value1"}' $headers = Get-HmacSignedHeaders -SharedSecret $sharedSecret ` -WebhookUrl $webhookUrl ` -Body $body [void]$sharedSecret.Dispose() Invoke-RestMethod -Method POST ` -Uri $webhookUrl ` -Headers $headers ` -Body $body ` -ContentType 'application/json; charset=utf-8' .FUNCTIONALITY Security, HMAC Authentication .NOTES Author: Julian Pawlowski Company Name: Workoho GmbH Created: 2025-03-17 Updated: 2025-03-18 #> param( # Shared secret used for HMAC signature (SecureString) [Parameter(Mandatory)][securestring]$SharedSecret, # Full webhook URL to which the request will be sent [Parameter(Mandatory)][string]$WebhookUrl, # Request body content as a string (usually JSON or similar) [Parameter(Mandatory)][string]$Body, # Algorithm to use for HMAC signature (default: HMACSHA256) [string]$Algorithm = "HMACSHA256", # Optional: Provide custom nonce (for testing/debugging); defaults to a new random GUID if omitted [string]$Nonce ) if ($Algorithm -notin @('HMACSHA256', 'HMACSHA512')) { throw "Unsupported algorithm: $Algorithm" } # Parse URL components $uri = [System.Uri]$WebhookUrl $method = "POST" $path = $uri.AbsolutePath $webHost = $uri.Host # Timestamp header (RFC1123 format) $date = (Get-Date).ToUniversalTime().ToString("R") # Compute body hash (SHA256) $bodyBytes = [Text.Encoding]::UTF8.GetBytes($Body) $contentHash = [Convert]::ToBase64String(([System.Security.Cryptography.SHA256]::Create()).ComputeHash($bodyBytes)) # Generate nonce if not provided if (-not $Nonce) { $Nonce = [Guid]::NewGuid().ToString() } # Define signed headers and order $signedHeadersList = @('x-ms-date', 'Host', 'x-ms-content-sha256', 'x-ms-nonce') $signedHeaders = ($signedHeadersList -join ';') # Build canonical message $canonicalMessage = "$method`n$path`n$date;$webHost;$contentHash;$Nonce" $unsecureSecret = $null try { $hmac = New-Object ("System.Security.Cryptography.$Algorithm") # SecureString → plaintext $unsecureSecret = [System.Net.NetworkCredential]::New("", $SharedSecret).Password # Set key and compute signature $hmac.Key = [Text.Encoding]::UTF8.GetBytes($unsecureSecret) $signature = [Convert]::ToBase64String($hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($canonicalMessage))) # Cleanup HMAC key [Array]::Clear($hmac.Key, 0, $hmac.Key.Length) $hmac = $null } finally { if ($unsecureSecret) { [Array]::Clear($unsecureSecret.ToCharArray(), 0, $unsecureSecret.Length) $unsecureSecret = $null } } # Prepare headers (Host excluded intentionally) $headers = @{ 'x-ms-date' = $date 'x-ms-content-sha256' = $contentHash 'x-ms-nonce' = $Nonce 'x-authorization' = "HMAC-$($Algorithm.Replace('HMAC','')) SignedHeaders=$signedHeaders&Signature=$signature" } return $headers } function Test-HmacAuthorization { <# .SYNOPSIS Verifies HMAC signature of incoming Azure Automation webhook requests. .DESCRIPTION Validates HMAC signature based on signed request headers (timestamp, nonce, content hash) and a shared secret. Supports both HMACSHA256 and HMACSHA512 algorithms. The shared secret is passed securely as a SecureString, converted as late as possible, and cleared from memory immediately after use. Signature verification includes: - Timestamp freshness validation (anti-replay window configurable via AllowedTimeDriftMinutes) - Host - Body content hash integrity check - Nonce inclusion for replay protection (future extension to store/check nonce is left open) .EXAMPLE # ✅ Example 1: Production (Azure Automation) # Retrieve encrypted variable securely $sharedSecret = (Get-AutomationVariable -Name "SharedSecret") | ConvertTo-SecureString -AsPlainText -Force # Verify signature if (-not (Test-HmacAuthorization -SharedSecret $sharedSecret -WebhookData $WebhookData)) { [void]$sharedSecret.Dispose() throw "Unauthorized: Signature verification failed." } # Dispose secret after use [void]$sharedSecret.Dispose() .EXAMPLE # ✅ Example 2: Local Development / Testing # Use test shared secret (DO NOT use hardcoded secrets in production) $sharedSecret = ConvertTo-SecureString -String "MyTestSecretKey" -AsPlainText -Force # Verify signature if (-not (Test-HmacAuthorization -SharedSecret $sharedSecret -WebhookData $WebhookData)) { Write-Output "❌ Signature invalid." } else { Write-Output "✅ Signature valid." } # Dispose secret [void]$sharedSecret.Dispose() .FUNCTIONALITY Security, HMAC Authentication .NOTES Author: Julian Pawlowski Company Name: Workoho GmbH Created: 2025-03-17 Updated: 2025-03-18 #> param( # The shared secret key used for HMAC calculation (SecureString) [Parameter(Mandatory)][securestring]$SharedSecret, # The full webhook request data object passed by Azure Automation (includes headers and body) [Parameter(Mandatory)][object]$WebhookData, # Allowed time difference in minutes for timestamp validation (default: 5 minutes) [int]$AllowedTimeDriftMinutes = 5, # Expected request path (used in canonical message construction) [string]$ExpectedPath = '/webhooks' ) $allowedAlgorithms = @('HMACSHA256', 'HMACSHA512') $headers = $WebhookData.RequestHeader $authHeader = $headers.'x-authorization' if (-not $authHeader) { Write-Error 'Missing x-authorization header' return $false } if ($authHeader -notmatch '^HMAC-(?[A-Z0-9]+)\s+SignedHeaders=(?[^&]+)&Signature=(?.+)$') { Write-Error 'Invalid x-authorization header format' return $false } $algorithm = "HMAC$($matches['Algorithm'])" if ($allowedAlgorithms -notcontains $algorithm) { Write-Error "Algorithm $algorithm not allowed" return $false } $signedHeaders = $matches['SignedHeaders'].Split(';') $receivedHmac = $matches['Signature'] foreach ($header in $signedHeaders) { if ([string]::IsNullOrEmpty($headers.$header)) { Write-Error "Missing signed header: $header" return $false } } # Host header check if ([string]::IsNullOrEmpty($headers.'Host')) { Write-Error 'Host header required' return $false } # Timestamp freshness check if ([string]::IsNullOrEmpty($headers.'x-ms-date')) { Write-Error 'x-ms-date header required' return $false } else { try { $requestTime = [datetime]::Parse($headers.'x-ms-date').ToUniversalTime() $currentTime = (Get-Date).ToUniversalTime() if ([math]::Abs(($currentTime - $requestTime).TotalMinutes) -gt $AllowedTimeDriftMinutes) { Write-Error 'Request timestamp expired' return $false } } catch { Write-Error 'Invalid timestamp format' return $false } } # Body hash check if ([string]::IsNullOrEmpty($headers.'x-ms-content-sha256')) { Write-Error 'x-ms-content-sha256 header required' return $false } else { $bodyBytes = [Text.Encoding]::UTF8.GetBytes($WebhookData.RequestBody) $computedBodyHash = [Convert]::ToBase64String(([System.Security.Cryptography.SHA256]::Create()).ComputeHash($bodyBytes)) if ($headers.'x-ms-content-sha256' -ne $computedBodyHash) { Write-Error 'Content hash mismatch' return $false } } # Nonce check if ([string]::IsNullOrEmpty($headers.'x-ms-nonce')) { Write-Error 'x-ms-nonce header required' return $false } # Optional future: Implement nonce storage to prevent re-use attacks # Canonical message construction $method = 'POST' $path = $ExpectedPath $headerValues = foreach ($header in $signedHeaders) { $headers.$header } $canonicalMessage = "$method`n$path`n" + ($headerValues -join ';') $unsecureSecret = $null try { $hmac = New-Object ("System.Security.Cryptography.$algorithm") # SecureString → plaintext $unsecureSecret = [System.Net.NetworkCredential]::New("", $SharedSecret).Password $hmac.Key = [Text.Encoding]::UTF8.GetBytes($unsecureSecret) $computedHmac = [Convert]::ToBase64String($hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($canonicalMessage))) # Cleanup HMAC key [Array]::Clear($hmac.Key, 0, $hmac.Key.Length) $hmac = $null } finally { if ($unsecureSecret) { [Array]::Clear($unsecureSecret.ToCharArray(), 0, $unsecureSecret.Length) $unsecureSecret = $null } } # Final comparison (constant-time) return [System.Security.Cryptography.CryptographicOperations]::FixedTimeEquals( [Text.Encoding]::UTF8.GetBytes($computedHmac), [Text.Encoding]::UTF8.GetBytes($receivedHmac) ) } function Assert-ParameterType { param ( [Parameter(Mandatory = $true)] [string]$Name, [Parameter(Mandatory = $true)] $Value, [Parameter(Mandatory = $true)] [type[]]$ExpectedTypes ) if ($null -eq $Value) { return $false } # Check if the value type matches any of the expected types $matchFound = $false foreach ($type in $ExpectedTypes) { if ($Value -is $type) { $matchFound = $true break } } if (-not $matchFound) { $typeNames = $ExpectedTypes | ForEach-Object { "[$($_.Name)]" } throw "Parameter '$Name' must be one of these types: $($typeNames -join ', '), but received [$($Value.GetType().Name)]" } return $true } function Invoke-ResilientRemoteCall { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [scriptblock]$ScriptBlock ) $maxRetries = 3 $retryCount = 0 $backoffInterval = 15 do { try { return Invoke-Command -ScriptBlock $ScriptBlock } catch { if ($retryCount -ge $maxRetries) { Write-Verbose "Operation failed after $maxRetries attempts: $ScriptBlock" throw } $retryCount++ Write-Verbose "Operation failed, retrying in $backoffInterval seconds (Attempt $retryCount of $maxRetries)" Start-Sleep -Seconds $backoffInterval } } while ($true) } function Get-OrganizationInfo { [CmdletBinding()] param() try { $organization = Invoke-ResilientRemoteCall { Invoke-MgGraphRequest -Method GET -Uri "/v1.0/organization" -ErrorAction Stop } if (-not $organization -or -not $organization.value -or $organization.value.Count -eq 0) { Write-Verbose "Could not retrieve organization information" return $null } return $organization.value[0] } catch { Write-Warning "Failed to retrieve tenant information: $_" return $null } } function Get-TenantBrandingImages { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$TenantId ) try { $brandingImages = @{} # Get the branding configuration (contains URLs, not binary data) try { $branding = Invoke-ResilientRemoteCall { Invoke-MgGraphRequest -Method GET ` -Uri "/v1.0/organization/$TenantId/branding" ` -ErrorAction Stop } if (-not $branding -or -not $branding.cdnList -or $branding.cdnList.Count -eq 0) { Write-Verbose "No branding CDN information found" return $null } # Use the first CDN domain $cdnDomain = 'https://' + $branding.cdnList[0] + '/' Write-Verbose "Using CDN domain: $cdnDomain" # Process banner logo if (-not [string]::IsNullOrEmpty($branding.bannerLogoRelativeUrl)) { try { $logoUrl = "$cdnDomain$($branding.bannerLogoRelativeUrl)" Write-Verbose "Retrieving banner logo from: $logoUrl" $response = Invoke-ResilientRemoteCall { Invoke-WebRequest -Uri $logoUrl -ErrorAction Stop } if ($response.StatusCode -eq 200) { # Get content type from response headers or determine from content $contentType = $response.Headers['Content-Type'] # Check if content type is generic or missing if ([string]::IsNullOrEmpty($contentType) -or $contentType -eq 'image/*') { $contentType = Get-ImageContentTypeFromBytes -ImageBytes $response.Content Write-Verbose "Determined banner logo content type from binary data: $contentType" } $base64 = [System.Convert]::ToBase64String($response.Content) $brandingImages["tenantBannerLogo"] = "data:$contentType;base64,$base64" Write-Verbose "Retrieved tenant banner logo ($contentType)" } } catch { Write-Verbose "Failed to retrieve banner logo: $_" } } # Process square logo (light) if (-not [string]::IsNullOrEmpty($branding.squareLogoRelativeUrl)) { try { $logoUrl = "$cdnDomain$($branding.squareLogoRelativeUrl)" Write-Verbose "Retrieving square logo from: $logoUrl" $response = Invoke-ResilientRemoteCall { Invoke-WebRequest -Uri $logoUrl -ErrorAction Stop } if ($response.StatusCode -eq 200) { # Get content type from response headers or determine from content $contentType = $response.Headers['Content-Type'] # Check if content type is generic or missing if ([string]::IsNullOrEmpty($contentType) -or $contentType -eq 'image/*') { $contentType = Get-ImageContentTypeFromBytes -ImageBytes $response.Content Write-Verbose "Determined square logo content type from binary data: $contentType" } $base64 = [System.Convert]::ToBase64String($response.Content) $brandingImages["tenantSquareLogoLight"] = "data:$contentType;base64,$base64" Write-Verbose "Retrieved tenant square logo (light) ($contentType)" } } catch { Write-Verbose "Failed to retrieve square logo: $_" } } # Process square logo dark if (-not [string]::IsNullOrEmpty($branding.squareLogoDarkRelativeUrl)) { try { $logoUrl = "$cdnDomain$($branding.squareLogoDarkRelativeUrl)" Write-Verbose "Retrieving dark square logo from: $logoUrl" $response = Invoke-ResilientRemoteCall { Invoke-WebRequest -Uri $logoUrl -ErrorAction Stop } if ($response.StatusCode -eq 200) { # Get content type from response headers or determine from content $contentType = $response.Headers['Content-Type'] # Check if content type is generic or missing if ([string]::IsNullOrEmpty($contentType) -or $contentType -eq 'image/*') { $contentType = Get-ImageContentTypeFromBytes -ImageBytes $response.Content Write-Verbose "Determined dark square logo content type from binary data: $contentType" } $base64 = [System.Convert]::ToBase64String($response.Content) $brandingImages["tenantSquareLogoDark"] = "data:$contentType;base64,$base64" Write-Verbose "Retrieved tenant square logo (dark) ($contentType)" } } catch { Write-Verbose "Failed to retrieve dark square logo: $_" } } } catch { Write-Verbose "Failed to retrieve organization branding: $_" } return $brandingImages } catch { Write-Warning "Failed to retrieve tenant branding: $_" return $null } } function Get-ImageContentTypeFromBytes { [CmdletBinding()] [OutputType([string])] param ( [Parameter(Mandatory = $true)] [byte[]]$ImageBytes ) # Check file signature (magic numbers) to determine file type if ($ImageBytes.Length -gt 4) { # JPEG: FF D8 FF if ($ImageBytes[0] -eq 0xFF -and $ImageBytes[1] -eq 0xD8 -and $ImageBytes[2] -eq 0xFF) { return "image/jpeg" } # PNG: 89 50 4E 47 elseif ($ImageBytes[0] -eq 0x89 -and $ImageBytes[1] -eq 0x50 -and $ImageBytes[2] -eq 0x4E -and $ImageBytes[3] -eq 0x47) { return "image/png" } # GIF: 47 49 46 38 elseif ($ImageBytes[0] -eq 0x47 -and $ImageBytes[1] -eq 0x49 -and $ImageBytes[2] -eq 0x46 -and $ImageBytes[3] -eq 0x38) { return "image/gif" } # BMP: 42 4D elseif ($ImageBytes[0] -eq 0x42 -and $ImageBytes[1] -eq 0x4D) { return "image/bmp" } # WebP: 52 49 46 46 xx xx xx xx 57 45 42 50 elseif ($ImageBytes.Length -gt 11 -and $ImageBytes[0] -eq 0x52 -and $ImageBytes[1] -eq 0x49 -and $ImageBytes[2] -eq 0x46 -and $ImageBytes[3] -eq 0x46 -and $ImageBytes[8] -eq 0x57 -and $ImageBytes[9] -eq 0x45 -and $ImageBytes[10] -eq 0x42 -and $ImageBytes[11] -eq 0x50) { return "image/webp" } # SVG: Check if it starts with " $templateVariables.emailTitle = $templateVariables.emailTitle -replace "`n", "
" $templateVariables.emailSalutation = $templateVariables.emailSalutation -replace "`n", "
" $templateVariables.emailClosing = $templateVariables.emailClosing -replace "`n", "
" # Process email images from JSON if provided $EmailImages = @{} if (-not [string]::IsNullOrEmpty($EmailImagesJson)) { try { $parsedImages = ConvertFrom-Json -InputObject $EmailImagesJson -AsHashtable -ErrorAction Stop if ($parsedImages -is [hashtable] -or $parsedImages -is [System.Collections.IDictionary]) { $EmailImages = $parsedImages } } catch { Write-Warning "Failed to parse EmailImagesJson parameter: $_" } } # Default email images $EmailImages.iconWarning = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFcklEQVR4nO2Ve2iVZRzHD4Gey/uendt7WmzSQovFduYfpu4+z5xzkUSE3TAyDLKQ8A8hCsp/imJmQUaRgww0RPA6NdfOzs5lOzfPNndeaARGRSAp4khkMvHsPO833ufcb9vznq25Yj/4wGBjfL7f9/c8j0q1PMuzPAs2gOohiNanIVp/gChMQLTeoYwLE4gIxyBau+S/US3FgWhtgShEIFpBieQiAOMylisYtzaplsoguEoLUehGxErypa0J6RzGBAlXhB5MWPkHKy9ankVE+INJ+koSS5oxyzWMmncurvSYagVE4WVEhEDhFZlTOp8Rsw9h00vy/17Y1YiYKyGW2xCxtEMU9kEUehERJktsO5tRcz5h8y2MmM4hbNqHUYMdIyYbguZK2UWZvPhwIHUYZz2QStsuID2SxJQmnI102Qhy2eBTEKCQNGPbY4xt54qHC4tnwh5gHisijVow5TPjltOYxdSwCVKYrW0pl1AcBQFKP5DRkBm9h9Zg2zNd6OrqQmdnJ7q6OnDxqypEg+xtS6FMDBT2APNYkftBM777uJbKb926lQbo6OjA9x89gWiAvW0pIS0F07AHmMeBjAbNOPpJdUp+y5YtNMDx7jU0AJN0KFucElAUoPQDKa/Jic8ez5LfvHkzTn3+GKKB2VdEChYSL0vBHqCUWySx2zNBE05/sTpLvr29Hee/fDQeQKG0lMSvOMBs0sWvPznAhUNVVDwpb7fb0f91JWZogOIrIhWQTqNXEGAed/ZM0Ij+b1alxGU2bdoEb88jmPGzty0lpCm+OAoCmO6xvpC5yC27D1ekxGXa2toQOlKOmJ9FuixPXIYM66fZA4yYbih5ITMPZCxgQOBIOZWWaW1tpYwfExDzKZOWkgzzIEP8X+wBwsawkhcyc6/llseOWlPiLS0tlF9OmEH8s6+IlCOdCRnmgkoC9Ch5ITN3Wg7w83ELlW5ubqY0NTXht1MGEN8c4sP54tJQHOLlDisIYNqh5IXMPJByy1dPGql0ksbGRlzrLWNuWxrKhKPAw73CHmDMZCAh4322FzJ7r+UAf541UOmGhgZKY0M9bl7kmduWkuLeOMTDReExGFVKhoSM5xQ9NhkH8voFPerr6ykbN25EW8t63LrEM7ctJcTTAXRnFMnTrxAwtpV6Z0/28di1vQb21nWUPTuq8XdfMXGuoDTFo6PArWlWHIB+hYDBz/JC5u521KunwpOXOMrtPh4zXra2JU9aXIa4dcMlydOvEDSsI/4yovTOjnl53HUmAvzIYcrBI+Zha1tK4qbyBE7thpID0K/g1/fMLZ3ebVn+xgUe+3dXwd5sg72lDh++WYUb53jE3LO3LSXEkxC35tt5ydOv4CjniJ//lfX6m3byOLB3FdauXZuirq4O3e9UYnpgbmnJraUQl/Z3+FX6eQegIQJlG4hPf6+gdM6BvPMTh13Pr6HSMjabjfL6c6tx59Ls0pIrDhnUTsOjXb8g8qkQPt1OlutPbvng3grU1tZSampqKJ/uqcC0o4C4KxsyqJXg1Ly6oPKpEEP8/rnvbA43ezl88EYlGtdXo+Gparz7WiWun+ZAXPltS5kMagCn5n3VvznEyx0sfIukdzvm1uFuvw6T5+NM9ekQcxWT1lJxGeJUH1AtxsCre4/lFinadoa0lJbvXhT5VAiP9m3i0RFm6cSKZOGUxTUxDKrfWlT5jBAvELfubrEDWaxtWZzKD2im4Fy5/YHIp0OsfJIMaiZYpeOoQQbUV+FYYVMthZEfHOLSnmQRlwao/An0LtAjtZADl/ZFMqiezJNOijvUt+FQ71Yt5cGAtoI41WeT0kmIQ31G/p3qvzLoV3eSAfWEjPzzg/ZZnuX5v84/V/4B6rP1Gg4AAAAASUVORK5CYII=' $EmailImages.iconInfo = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFCUlEQVR4nO2YSUwbVxjHh1Ztis0SttisxmYxOxjUVL300kMvPfVS9dBL21SqVKlS1ahVKwW1YicYs+9hM5iYQIMDDmCsSlVTtRV7IDDeVzCZYcapoqAoKl81PtpRB88brB74S3Md/X5v3vfm+x6GXeQiF+Elb/5yJK5YPv6ofIm6WbZILZTpj1dLF0hL8QJpLZ4nV4t1xP0iHdFaqCM+zrtHpmP/h1TpvILyZfqawkD/VrF0fFq+REH5IgVl94+hVH8MJQsklMyTUHyPgCIdAYVzBBTefQwFPz8G+ezRXwV3jr6U3yViIw5epIXXK430dcUKdaQw0FCxTEGY8CCfOYL8O0eQN+2jcrW+GxlaV3RE4BWGJ28rVug9xQoNPMBDrtYHubcPQaY5sMk0vnfPFb7SSH+jWKFf8A2fM8UIHIJs0vuPVO39EQOI4pccIKrS6G+pNPrh/OAPQDpxANlqL0jGPeqqvtXXeOOvMvpVkYP3gmTMC1kjbg1WDa/wAf9dxOFHPZA54oGMYVcDErzCSL3Ddc/LtW7IU5shb9wMeRoXB3g3ZAy5TtOGPO9zgs/Vw6VKI41zgS/S2ED9iADq5EXgGd8hIH/cEh78LTekD7khbdDlkwzbL4ctULniv8Fp5ac9UPPHAQTnpwdekKld4cJD2oALUvsdyrDgr+rJOIWBorns+fwpJ0w+IkMEJnYIkI7aOcA7QdTrPEnqcKadWUBhoL/mWrD5Wi9cW3aECHyit4N03BU2vLgvIACibkfNmQUqluktlNMmR22Dz5cccBcnA8+nDPywhTt8jwNSuhzOMx2rZYYn+bwclRoPyEZtIB2xgUztRoK/0s0I2CGx0/YW++obqC9Q4XPGrJA9sAuS/l2Q9O1AVu8OSIbMSPDJnXZIbrd+z/4FFo+HUX9SDHxwMrsfosF32CCxzTp3BgHqd9Q/LLPqIQJd20jwSe02SFBZ99gF9KQTtT2Q9D4MEcjo3EKCT2xjBCxPWQVK9SSF2ttk9YQKpHdsIcEnqqyQoLScYlp49T8FiucJP2pjltm9HSKQ1rGJBt9qhctKC2BsbXbJPOlF7SpfKtC+iQwf32I+Yd1CxTpiA7UlZgo2OKltm4jwFohrNjtZBYrmiCnUfp4p2OCIVRtI8PHNFohtNC+xChTMEd+iDiNMwYYItG4gwcc1mRmBRvYvMEteRZ2kmIINjki5jgoPggbze6wCzDEln/V5uI+B7kDBBudKyzoSvLAB92PV9jfYBTAMk8/4GrnCM0clU7DBSbm5zhk+psEEwnp8EDtrZFpvVu607zkXeOaoZAo2RKB5jTN8TB1+KqzdL8PCSc5t3y2OM2ygYIOT3LTGDb7eBMJa0ywWbqSTPlGO5pDmMsOKlKECSU3rHOHxZ5fqcVnYAgGJCd9nnGbYjr3Anme2DbPySY1rkKTcDR++zgSCGvw6hpJs9cEk6iTFaeXrTBBds7+AfE/KXHtLxjy/Rhy+Ft+Or+ZwH/SyMBdLmSPuBxFc+U1B044Y4zOpfV5BxrBrJgLwSwkN1njsXAIQlT7o/ip1wPnsHOCfC2rwH3i5kWaLqN8hFfc5Z8W9jlN+4PHFmNr9AizSSemxlYt67GMp3fan4cPjJzH1pqnoOpz9rufcRTp3YhK77B8kt9s6k9psfyaqbP7QYcTyd2yjaTW2wdQnrNv/8Pz2OU8RNR8KhSqrSKB0pDKCfL33IhfB+M2/hW3M5uDgqEwAAAAASUVORK5CYII=' # Get tenant branding images $tenantBranding = Get-TenantBrandingImages -TenantId $organizationInfo.Id if ($tenantBranding -and $tenantBranding.Count -gt 0) { Write-Verbose "Retrieved tenant branding with $($tenantBranding.Count) images" # Add tenant branding images to the EmailImages collection foreach ($key in $tenantBranding.Keys) { if (-not $EmailImages.ContainsKey($key)) { $EmailImages[$key] = $tenantBranding[$key] Write-Verbose "Added tenant branding image: $key" } } } # Add all image references to template variables if ($EmailImages.Count -gt 0) { # First add all possible image references to template variables foreach ($imageKey in $EmailImages.Keys) { $templateVariables["image:$imageKey"] = "cid:$imageKey" } } $bodyContentType = 'HTML' $EmailBodyAlertBannerTranslations = @{ en = @'
Warning Security notice: This message contains sensitive account information. Please handle with care.
'@ de = @'
Warning Sicherheitshinweis: Diese Nachricht enthält sensible Kontoinformationen. Bitte sorgfältig behandeln.
'@ fr = @'
Warning Notification de sécurité : Ce message contient des informations sensibles concernant un compte. Veuillez le manipuler avec précaution.
'@ } if ($Reason -ne 'TAPCreated') { $templateVariables.emailBodyAlertBanner = '' } else { $templateVariables.emailBodyAlertBanner = if ($EmailBodyAlertBannerTranslations.ContainsKey($languageCode)) { $EmailBodyAlertBannerTranslations[$languageCode] } else { $EmailBodyAlertBannerTranslations['en'] } } $EmailBodyFooterHintTranslations = @{ en = @'

This is an automated email containing a secure access code. For security reasons, the code will expire after use or after the time indicated above.

'@ de = @'

Diese E-Mail wurde automatisch erstellt und enthält einen vorübergehenden Zugriffscode. Aus Sicherheitsgründen läuft der Code nach der Verwendung oder nach der oben angegebenen Zeit ab.

'@ fr = @"

Ceci est un e-mail automatique contenant un code d’accès temporaire. Pour des raisons de sécurité, le code expirera après son utilisation ou après l’heure d’expiration indiquée ci-dessus.

"@ } if ($Reason -ne 'TAPCreated') { $templateVariables.emailBodyFooterHint = '' } else { $templateVariables.emailBodyFooterHint = if ($EmailBodyFooterHintTranslations.ContainsKey($languageCode)) { $EmailBodyFooterHintTranslations[$languageCode] } else { $EmailBodyFooterHintTranslations['en'] } } $EmailBodyTextTranslations = @{ en = @{ TAPCreated = @'

{{ emailSalutation }}

A Temporary Access Pass code for onboarding has been created for the following person:

{{ userDisplayName }}

{{ userPrincipalName }}

Please provide the following code to {{ userGivenName }}:

{{ temporaryAccessPass }}

The code will expire in {{ lifeTimeInHoursMinutes }}, at {{ expirationTime }} UTC.

Next steps for {{ userGivenName }}:

  1. Use the code to sign in when prompted.
  2. Set up the Microsoft Authenticator app or another authentication method.
  3. After setup is complete, the Temporary Access Pass can be deleted at https://aka.ms/MySecurityInfo .
  4. From now on, use the new authentication methods you have set up.
'@ UserBlocked = @'

{{ emailSalutation }}

A Temporary Access Pass code could not be created for the following person:

{{ userDisplayName }}

{{ userPrincipalName }}

The creation was blocked because this type of user account is not allowed to use this process to create a Temporary Access Pass code.

Next steps if {{ userGivenName }} is not able to sign in:

  1. Ask {{ userGivenName }} to contact IT support and request a reset of their authentication methods.
  2. The support team will validate the request and provide guidance on how to proceed.
'@ TAPNotEnabled = @'

{{ emailSalutation }}

A Temporary Access Pass code could not be created for the following person:

{{ userDisplayName }}

{{ userPrincipalName }}

The creation was aborted because the Temporary Access Pass feature is not enabled for this user account.

Next steps if {{ userGivenName }} is not able to sign in:

  1. Ask {{ userGivenName }} to contact IT support and request a reset of their authentication methods.
  2. The support team will validate the request and provide guidance on how to proceed.
'@ TAPExpiredWithOtherMethodsConfigured = @'

{{ emailSalutation }}

A Temporary Access Pass code could not be created for the following person:

{{ userDisplayName }}

{{ userPrincipalName }}

The creation was aborted because {{ userGivenName }} has already completed the onboarding process and has other authentication methods configured.

The previously issued Temporary Access Pass is no longer valid.

Please ask {{ userGivenName }} to sign in using their existing authentication methods.

Next steps if {{ userGivenName }} is not able to sign in:

  1. Ask {{ userGivenName }} to contact IT support and request a reset of their authentication methods.
  2. The support team will validate the request and provide guidance on how to proceed.
'@ TAPActiveWithOtherMethodsConfigured = @'

{{ emailSalutation }}

A Temporary Access Pass code could not be created for the following person:

{{ userDisplayName }}

{{ userPrincipalName }}

The creation was aborted because {{ userGivenName }} has already completed the onboarding process and has other authentication methods configured.

{{ userGivenName }} must use the existing Temporary Access Pass to sign in before being able to use the other authentication methods.

If {{ userGivenName }} is able to sign in using the active Temporary Access Pass, they can remove it themselves at https://aka.ms/MySecurityInfo to start using their other authentication methods.

Next steps if {{ userGivenName }} is not able to sign in:

  1. Ask {{ userGivenName }} to contact IT support and request removal of the active Temporary Access Pass, or wait until the code expires.
  2. {{ userGivenName }} may then try to sign in using one of their existing authentication methods.
  3. The support team will provide further guidance to restore access to the account if needed.
'@ OtherMethodsConfigured = @'

{{ emailSalutation }}

A Temporary Access Pass code could not be created for the following person:

{{ userDisplayName }}

{{ userPrincipalName }}

The creation was aborted because {{ userGivenName }} has already completed the onboarding process and already has authentication methods configured.

Temporary Access Pass codes must no longer be issued via the self-service process and require handling by IT support.

Please ask {{ userGivenName }} to sign in using their existing authentication methods.

Next steps if {{ userGivenName }} is not able to sign in:

  1. Ask {{ userGivenName }} to contact IT support and request a reset of their authentication methods.
  2. The support team will validate the request and provide guidance on how to proceed.
'@ } de = @{ TAPCreated = @'

{{ emailSalutation }}

Ein befristeter Zugriffscode für den Onboarding-Prozess wurde für die folgende Person erstellt:

{{ userDisplayName }}

{{ userPrincipalName }}

Bitte geben Sie den folgenden Code an {{ userGivenName }} weiter:

{{ temporaryAccessPass }}

Der Code läuft in {{ lifeTimeInHoursMinutes }} um {{ expirationTime }} (GMT) ab.

Nächste Schritte für {{ userGivenName }}:

  1. Verwenden Sie den Code, um sich anzumelden, wenn Sie dazu aufgefordert werden.
  2. Richten Sie die Microsoft Authenticator-App oder eine andere Authentifizierungsmethode ein.
  3. Nach Abschluss der Einrichtung kann der befristete Zugriffscode unter https://aka.ms/MySecurityInfo gelöscht werden.
  4. Nutzen Sie von nun an die neuen Authentifizierungsmethoden, die Sie eingerichtet haben.
'@ UserBlocked = @'

{{ emailSalutation }}

Ein befristeter Zugriffscode konnte für die folgende Person nicht erstellt werden:

{{ userDisplayName }}

{{ userPrincipalName }}

Die Erstellung wurde blockiert, da dieser Kontotyp nicht berechtigt ist, über diesen Prozess einen befristeten Zugriffscode zu erhalten.

Nächste Schritte, falls {{ userGivenName }} sich nicht anmelden kann:

  1. Bitten Sie {{ userGivenName }}, den IT-Support zu kontaktieren und eine Zurücksetzung der Authentifizierungsmethoden anzufordern.
  2. Der Support prüft die Anfrage und hilft bei den nächsten Schritten.
'@ TAPNotEnabled = @'

{{ emailSalutation }}

Ein befristeter Zugriffscode konnte für die folgende Person nicht erstellt werden:

{{ userDisplayName }}

{{ userPrincipalName }}

Die Erstellung wurde abgebrochen, da die Funktion für befristete Zugriffscodes für dieses Konto nicht aktiviert ist.

Nächste Schritte, falls {{ userGivenName }} sich nicht anmelden kann:

  1. Bitten Sie {{ userGivenName }}, den IT-Support zu kontaktieren und eine Zurücksetzung der Authentifizierungsmethoden anzufordern.
  2. Der Support prüft die Anfrage und hilft bei den nächsten Schritten.
'@ TAPExpiredWithOtherMethodsConfigured = @'

{{ emailSalutation }}

Ein befristeter Zugriffscode konnte für die folgende Person nicht erstellt werden:

{{ userDisplayName }}

{{ userPrincipalName }}

Die Erstellung wurde abgebrochen, da der Onboarding-Prozess bereits abgeschlossen wurde und andere Authentifizierungsmethoden für dieses Konto eingerichtet sind.

Der zuvor ausgestellte Zugriffscode ist nicht mehr gültig.

Bitten Sie {{ userGivenName }}, sich mit den bestehenden Authentifizierungsmethoden anzumelden.

Nächste Schritte, falls {{ userGivenName }} sich nicht anmelden kann:

  1. Bitten Sie {{ userGivenName }}, den IT-Support zu kontaktieren und eine Zurücksetzung der Authentifizierungsmethoden anzufordern.
  2. Der Support prüft die Anfrage und hilft bei den nächsten Schritten.
'@ TAPActiveWithOtherMethodsConfigured = @'

{{ emailSalutation }}

Ein befristeter Zugriffscode konnte für die folgende Person nicht erstellt werden:

{{ userDisplayName }}

{{ userPrincipalName }}

Die Erstellung wurde abgebrochen, da der Onboarding-Prozess bereits abgeschlossen wurde und andere Authentifizierungsmethoden für dieses Konto eingerichtet sind.

{{ userGivenName }} muss den bestehenden befristeten Zugriffscode verwenden, um sich anzumelden, bevor andere Authentifizierungsmethoden genutzt werden können.

Kann sich {{ userGivenName }} erfolgreich mit dem aktiven Zugriffscode anmelden, kann dieser unter https://aka.ms/MySecurityInfo eigenständig gelöscht werden, um anschließend die anderen Authentifizierungsmethoden zu verwenden.

Nächste Schritte, falls {{ userGivenName }} sich nicht anmelden kann:

  1. Bitten Sie {{ userGivenName }}, den IT-Support zu kontaktieren und die Entfernung des aktiven Zugriffscodes anzufordern, oder warten Sie die Ablaufzeit ab.
  2. Anschließend kann {{ userGivenName }} versuchen, sich mit einer bestehenden Authentifizierungsmethode anzumelden.
  3. Falls nötig, unterstützt der Support bei der Wiederherstellung des Kontozugriffs.
'@ OtherMethodsConfigured = @'

{{ emailSalutation }}

Ein befristeter Zugriffscode konnte für die folgende Person nicht erstellt werden:

{{ userDisplayName }}

{{ userPrincipalName }}

Die Erstellung wurde abgebrochen, da der Onboarding-Prozess bereits abgeschlossen wurde und andere Authentifizierungsmethoden für dieses Konto eingerichtet sind.

Befristete Zugriffscodes dürfen nach Abschluss des Onboardings nicht mehr über den Self-Service-Prozess erstellt werden und erfordern die Unterstützung durch den IT-Support.

Bitten Sie {{ userGivenName }}, sich mit den bestehenden Authentifizierungsmethoden anzumelden.

Nächste Schritte, falls {{ userGivenName }} sich nicht anmelden kann:

  1. Bitten Sie {{ userGivenName }}, den IT-Support zu kontaktieren und eine Zurücksetzung der Authentifizierungsmethoden anzufordern.
  2. Der Support prüft die Anfrage und hilft bei den nächsten Schritten.
'@ } fr = @{ TAPCreated = @'

{{ emailSalutation }}

Un code d’accès temporaire pour l’intégration a été créé pour la personne suivant :

{{ userDisplayName }}

{{ userPrincipalName }}

Veuillez transmettre le code suivant à {{ userGivenName }} :

{{ temporaryAccessPass }}

Le code expirera dans {{ lifeTimeInHoursMinutes }}, à {{ expirationTime }} UTC.

Prochaines étapes pour {{ userGivenName }} :

  1. Utilisez le code pour vous connecter lorsque cela vous est demandé.
  2. Configurez l’application Microsoft Authenticator ou une autre méthode d’authentification.
  3. Une fois la configuration terminée, le code d’accès temporaire peut être supprimé via https://aka.ms/MySecurityInfo .
  4. Utilisez ensuite les nouvelles méthodes d’authentification que vous avez configurées.
'@ UserBlocked = @'

{{ emailSalutation }}

Un code d’accès temporaire n’a pas pu être créé pour la personne suivante :

{{ userDisplayName }}

{{ userPrincipalName }}

La création a été bloquée car ce type de compte n’est pas autorisé à utiliser ce processus pour obtenir un code d’accès temporaire.

Prochaines étapes si {{ userGivenName }} ne parvient pas à se connecter :

  1. Demandez à {{ userGivenName }} de contacter l’assistance informatique et de demander la réinitialisation de ses méthodes d'authentification.
  2. L’équipe d’assistance validera la demande et fournira des instructions pour la suite.
'@ TAPNotEnabled = @'

{{ emailSalutation }}

Un code d’accès temporaire n’a pas pu être créé pour la personne suivante :

{{ userDisplayName }}

{{ userPrincipalName }}

La création a été annulée car la fonctionnalité de code d’accès temporaire n’est pas activée pour ce compte.

Prochaines étapes si {{ userGivenName }} ne parvient pas à se connecter :

  1. Demandez à {{ userGivenName }} de contacter l’assistance informatique et de demander la réinitialisation de ses méthodes d'authentification.
  2. L’équipe d’assistance validera la demande et fournira des instructions pour la suite.
'@ TAPExpiredWithOtherMethodsConfigured = @'

{{ emailSalutation }}

Un code d’accès temporaire n’a pas pu être créé pour la personne suivante :

{{ userDisplayName }}

{{ userPrincipalName }}

La création a été annulée car l’intégration de l’utilisateur est terminée et d’autres méthodes d’authentification ont été configurées.

Le code d’accès temporaire précédemment émis n’est plus valide.

Veuillez demander à {{ userGivenName }} de se connecter en utilisant ses méthodes d’authentification existantes.

Prochaines étapes si {{ userGivenName }} ne parvient pas à se connecter :

  1. Demandez à {{ userGivenName }} de contacter l’assistance informatique et de demander la réinitialisation de ses méthodes d’authentification.
  2. L’équipe d’assistance validera la demande et fournira des instructions pour la suite.
'@ TAPActiveWithOtherMethodsConfigured = @'

{{ emailSalutation }}

Un code d’accès temporaire n’a pas pu être créé pour la personne suivante :

{{ userDisplayName }}

{{ userPrincipalName }}

La création a été annulée car l’intégration de l’utilisateur est terminée et d’autres méthodes d’authentification ont été configurées.

{{ userGivenName }} doit utiliser le code d’accès temporaire existant pour se connecter avant de pouvoir utiliser d’autres méthodes d’authentification.

Si {{ userGivenName }} parvient à se connecter avec le code d’accès temporaire actif, il peut le supprimer lui-même sur https://aka.ms/MySecurityInfo afin d’utiliser ses autres méthodes d’authentification.

Next steps if {{ userGivenName }} is not able to sign in:

  1. Demandez à {{ userGivenName }} de contacter l’assistance informatique et de demander la suppression du code d’accès temporaire actif, ou d’attendre son expiration.
  2. Ensuite, {{ userGivenName }} peut essayer de se connecter avec une autre méthode d’authentification existante.
  3. Si nécessaire, l’assistance informatique aidera à restaurer l’accès au compte.
'@ OtherMethodsConfigured = @'

{{ emailSalutation }}

Un code d’accès temporaire n’a pas pu être créé pour la personne suivante :

{{ userDisplayName }}

{{ userPrincipalName }}

La création a été annulée car l’intégration de l’utilisateur est terminée et d’autres méthodes d’authentification sont déjà configurées.

Après l’intégration, les codes d’accès temporaires ne doivent plus être délivrés via le processus en libre-service et nécessitent l’intervention de l’assistance informatique.

Veuillez demander à {{ userGivenName }} de se connecter en utilisant ses méthodes d’authentification existantes.

Prochaines étapes si {{ userGivenName }} ne parvient pas à se connecter :

  1. Demandez à {{ userGivenName }} de contacter l’assistance informatique et de demander la réinitialisation de ses méthodes d’authentification.
  2. L’équipe d’assistance validera la demande et fournira des instructions pour la suite.
'@ } } if ([string]::IsNullOrEmpty($EmailBodyText)) { if ($EmailBodyTextTranslations.ContainsKey($languageCode)) { $EmailBodyText = $EmailBodyTextTranslations[$languageCode][$Reason] } else { $EmailBodyText = $EmailBodyTextTranslations['en'][$Reason] } } else { try { $EmailBodyTexts = ConvertFrom-Json -InputObject $EmailBodyText -AsHashtable -ErrorAction Stop if ($EmailBodyTexts -is [hashtable]) { if ($EmailBodyTexts.ContainsKey($languageCode)) { if ($EmailBodyTexts[$languageCode] -is [hashtable]) { if ($EmailBodyTexts[$languageCode].ContainsKey($Reason)) { $EmailBodyText = $EmailBodyTexts[$languageCode][$Reason] } else { $EmailBodyText = $EmailBodyTextTranslations['en'][$Reason] } } else { $EmailBodyText = $EmailBodyTexts[$languageCode] } } elseif ($EmailBodyTexts.ContainsKey('en')) { if ($EmailBodyTexts['en'] -is [hashtable]) { if ($EmailBodyTexts['en'].ContainsKey($Reason)) { $EmailBodyText = $EmailBodyTexts['en'][$Reason] } else { $EmailBodyText = $EmailBodyTextTranslations['en'][$Reason] } } else { $EmailBodyText = $EmailBodyTexts['en'] } } else { $EmailBodyText = $EmailBodyTextTranslations['en'][$Reason] } } } catch { Write-Verbose 'EmailBodyText: Not a valid JSON object found, interpreting as raw text' } } $templateVariables.emailBodyText = Expand-Template -Template $EmailBodyText -Variables $templateVariables #region HTML email template $EmailTemplateTranslations = @{ #region EN template en = @'
{{ emailBodyAlertBanner }}
Company Logo

{{ emailTitle }}

{{ emailBodyText }}

Thank you for your assistance.

{{ emailClosing }}

Note: If you have not made the request, you can safely ignore this e-mail.

{{ emailBodyFooterHint }}
Company Logo

{{ orgStreet }}, {{ orgPostalCode }} {{ orgCity }}, {{ orgCountry }}

Privacy Statement | Privacy Contact

Tenant ID: {{ orgTenantId }}

'@ #endregion #region DE template de = @'
{{ emailBodyAlertBanner }}
Company Logo

{{ emailTitle }}

{{ emailBodyText }}

Vielen Dank für Ihre Unterstützung.

{{ emailClosing }}

Hinweis: Wenn Sie die Anfrage nicht gestellt haben, können Sie diese E-Mail ohne Bedenken ignorieren.

{{ emailBodyFooterHint }}
Company Logo

{{ orgStreet }}, {{ orgPostalCode }} {{ orgCity }}, {{ orgCountry }}

Datenschutzerklärung | Datenschutzkontakt

Mandanten-ID: {{ orgTenantId }}

'@ #endregion #region FR template fr = @'
{{ emailBodyAlertBanner }}
Company Logo

{{ emailTitle }}

{{ emailBodyText }}

Merci pour votre collaboration.

{{ emailClosing }}

Remarque : si vous n’êtes pas à l’origine de cette demande, vous pouvez ignorer cet e-mail en toute sécurité.

{{ emailBodyFooterHint }}
Company Logo

{{ orgStreet }}, {{ orgPostalCode }} {{ orgCity }}, {{ orgCountry }}

Déclaration de confidentialité | Contact confidentialité

ID du client (Tenant ID) : {{ orgTenantId }}

'@ #endregion } #endregion if ([string]::IsNullOrEmpty($EmailTemplate)) { if ($EmailTemplateTranslations.ContainsKey($languageCode)) { Write-Verbose "Using custom HTML email template for $languageCode" $bodyContentTemplate = $EmailTemplateTranslations[$languageCode] } else { Write-Verbose 'Using default English HTML email template' $bodyContentTemplate = $EmailTemplateTranslations['en'] } } else { try { $EmailTemplates = ConvertFrom-Json -InputObject $EmailTemplate -AsHashtable -ErrorAction Stop if ($EmailTemplates -is [hashtable]) { if ($EmailTemplates.ContainsKey($languageCode)) { Write-Verbose "Using custom language-specific HTML email template for $languageCode" $bodyContentTemplate = $EmailTemplates[$languageCode] } elseif ($EmailClosings.ContainsKey('en')) { Write-Verbose 'Using custom default English HTML email template' $bodyContentTemplate = $EmailTemplates['en'] } else { Write-Verbose 'No default English template found, using default HTML template' $bodyContentTemplate = $EmailTemplateTranslations['en'] } } } catch { Write-Verbose 'EmailTemplate: Not a valid JSON object found, interpreting as raw HTML' $bodyContentTemplate = $EmailTemplate } } } #endregion #region Text Email else { $bodyContentType = 'Text' $EmailBodyFooterHintTranslations = @{ en = "This is an automated email containing a temporary access pass. For security reasons, the code will expire after use or after the time indicated above.`n`n" de = "Diese E-Mail wurde automatisch erstellt und enthält einen vorübergehenden Zugriffscode. Aus Sicherheitsgründen läuft der Code nach der Verwendung oder nach der oben angegebenen Zeit ab.`n`n" fr = "Ceci est un e-mail automatique contenant un code d’accès temporaire. Pour des raisons de sécurité, le code expirera après son utilisation ou après l’heure d’expiration indiquée ci-dessus.`n`n" } if ($Reason -ne 'TAPCreated') { $templateVariables.emailBodyFooterHint = "`n" } else { $templateVariables.emailBodyFooterHint = if ($EmailBodyFooterHintTranslations.ContainsKey($languageCode)) { $EmailBodyFooterHintTranslations[$languageCode] } else { $EmailBodyFooterHintTranslations['en'] } } $EmailBodyTextTranslations = @{ en = { TAPCreated = @' A Temporary Access Pass code for onboarding has been created for the following person: {{ userDisplayName }} {{ userPrincipalName }} Please provide the following code to {{ userGivenName }}: {{ temporaryAccessPass }} The code will expire in {{ lifeTimeInHoursMinutes }}, at {{ expirationTime }} UTC. Next steps for {{ userGivenName }}: 1. Use the code to sign in when prompted. 2. Set up the Microsoft Authenticator app or another authentication method. 3. After setup is complete, the Temporary Access Pass can be deleted at https://aka.ms/MySecurityInfo . 4. From now on, use the new authentication methods you have set up. '@ UserBlocked = @' A Temporary Access Pass code could not be created for the following person: {{ userDisplayName }} {{ userPrincipalName }} The creation was blocked because this type of user account is not allowed to use this process to create a Temporary Access Pass code. Next steps if {{ userGivenName }} is not able to sign in: 1. Ask {{ userGivenName }} to contact IT support and request a reset of their authentication methods. 2. The support team will validate the request and provide guidance on how to proceed. '@ TAPNotEnabled = @' A Temporary Access Pass code could not be created for the following person: {{ userDisplayName }} {{ userPrincipalName }} The creation was aborted because the Temporary Access Pass feature is not enabled for this user account. Next steps if {{ userGivenName }} is not able to sign in: 1. Ask {{ userGivenName }} to contact IT support and request a reset of their authentication methods. 2. The support team will validate the request and provide guidance on how to proceed. '@ TAPExpiredWithOtherMethodsConfigured = @' A Temporary Access Pass code could not be created for the following person: {{ userDisplayName }} {{ userPrincipalName }} The creation was aborted because {{ userGivenName }} has already completed the onboarding process and has other authentication methods configured. The previously issued Temporary Access Pass is no longer valid. Please ask {{ userGivenName }} to sign in using their existing authentication methods. Next steps if {{ userGivenName }} is not able to sign in: 1. Ask {{ userGivenName }} to contact IT support and request a reset of their authentication methods. 2. The support team will validate the request and provide guidance on how to proceed. '@ TAPActiveWithOtherMethodsConfigured = @' A Temporary Access Pass code could not be created for the following person: {{ userDisplayName }} {{ userPrincipalName }} The creation was aborted because {{ userGivenName }} has already completed the onboarding process and has other authentication methods configured. {{ userGivenName }} must use the existing Temporary Access Pass to sign in before being able to use the other authentication methods. If {{ userGivenName }} is able to sign in using the active Temporary Access Pass, they can remove it themselves at https://aka.ms/MySecurityInfo to start using their other authentication methods. Next steps if {{ userGivenName }} is not able to sign in: 1. Ask {{ userGivenName }} to contact IT support and request removal of the active Temporary Access Pass, or wait until the code expires. 2. {{ userGivenName }} may then try to sign in using one of their existing authentication methods. 3. The support team will provide further guidance to restore access to the account if needed. '@ OtherMethodsConfigured = @' A Temporary Access Pass code could not be created for the following person: {{ userDisplayName }} {{ userPrincipalName }} The creation was aborted because {{ userGivenName }} has already completed the onboarding process and already has authentication methods configured. Temporary Access Pass codes must no longer be issued via the self-service process and require handling by IT support. Please ask {{ userGivenName }} to sign in using their existing authentication methods. Next steps if {{ userGivenName }} is not able to sign in: 1. Ask {{ userGivenName }} to contact IT support and request a reset of their authentication methods. 2. The support team will validate the request and provide guidance on how to proceed. '@ } de = { TAPCreated = @' Ein befristeter Zugriffscode für den Onboarding-Prozess wurde für die folgende Person erstellt: {{ userDisplayName }} {{ userPrincipalName }} Bitte geben Sie den folgenden Code an {{ userGivenName }} weiter: {{ temporaryAccessPass }} Der Code läuft in {{ lifeTimeInHoursMinutes }} um {{ expirationTime }} (GMT) ab. Nächste Schritte für {{ userGivenName }}: 1. Verwenden Sie den Code, um sich anzumelden, wenn Sie dazu aufgefordert werden. 2. Richten Sie die Microsoft Authenticator-App oder eine andere Authentifizierungsmethode ein. 3. Nach Abschluss der Einrichtung kann der befristete Zugriffscode unter https://aka.ms/MySecurityInfo gelöscht werden. 4. Nutzen Sie von nun an die neuen Authentifizierungsmethoden, die Sie eingerichtet haben. '@ UserBlocked = @' Ein befristeter Zugriffscode konnte für die folgende Person nicht erstellt werden: {{ userDisplayName }} {{ userPrincipalName }} Die Erstellung wurde blockiert, da dieser Kontotyp nicht berechtigt ist, über diesen Prozess einen befristeten Zugriffscode zu erhalten. Nächste Schritte, falls {{ userGivenName }} sich nicht anmelden kann: 1. Bitten Sie {{ userGivenName }}, den IT-Support zu kontaktieren und eine Zurücksetzung der Authentifizierungsmethoden anzufordern. 2. Der Support prüft die Anfrage und hilft bei den nächsten Schritten. '@ TAPNotEnabled = @' Ein befristeter Zugriffscode konnte für die folgende Person nicht erstellt werden: {{ userDisplayName }} {{ userPrincipalName }} Die Erstellung wurde abgebrochen, da die Funktion für befristete Zugriffscodes für dieses Konto nicht aktiviert ist. Nächste Schritte, falls {{ userGivenName }} sich nicht anmelden kann: 1. Bitten Sie {{ userGivenName }}, den IT-Support zu kontaktieren und eine Zurücksetzung der Authentifizierungsmethoden anzufordern. 2. Der Support prüft die Anfrage und hilft bei den nächsten Schritten. '@ TAPExpiredWithOtherMethodsConfigured = @' Ein befristeter Zugriffscode konnte für die folgende Person nicht erstellt werden: {{ userDisplayName }} {{ userPrincipalName }} Die Erstellung wurde abgebrochen, da der Onboarding-Prozess bereits abgeschlossen wurde und andere Authentifizierungsmethoden für dieses Konto eingerichtet sind. Der zuvor ausgestellte Zugriffscode ist nicht mehr gültig. Bitten Sie {{ userGivenName }}, sich mit den bestehenden Authentifizierungsmethoden anzumelden. Nächste Schritte, falls {{ userGivenName }} sich nicht anmelden kann: 1. Bitten Sie {{ userGivenName }}, den IT-Support zu kontaktieren und eine Zurücksetzung der Authentifizierungsmethoden anzufordern. 2. Der Support prüft die Anfrage und hilft bei den nächsten Schritten. '@ TAPActiveWithOtherMethodsConfigured = @' Ein befristeter Zugriffscode konnte für die folgende Person nicht erstellt werden: {{ userDisplayName }} {{ userPrincipalName }} Die Erstellung wurde abgebrochen, da der Onboarding-Prozess bereits abgeschlossen wurde und andere Authentifizierungsmethoden für dieses Konto eingerichtet sind. {{ userGivenName }} muss den bestehenden befristeten Zugriffscode verwenden, um sich anzumelden, bevor andere Authentifizierungsmethoden genutzt werden können. Kann sich {{ userGivenName }} erfolgreich mit dem aktiven Zugriffscode anmelden, kann dieser unter https://aka.ms/MySecurityInfo eigenständig entfernt werden, um anschließend die anderen Authentifizierungsmethoden zu verwenden. Nächste Schritte, falls {{ userGivenName }} sich nicht anmelden kann: 1. Bitten Sie {{ userGivenName }}, den IT-Support zu kontaktieren und die Entfernung des aktiven Zugriffscodes anzufordern, oder warten Sie die Ablaufzeit ab. 2. Anschließend kann {{ userGivenName }} versuchen, sich mit einer bestehenden Authentifizierungsmethode anzumelden. 3. Falls nötig, unterstützt der Support bei der Wiederherstellung des Kontozugriffs. '@ OtherMethodsConfigured = @' Ein befristeter Zugriffscode konnte für die folgende Person nicht erstellt werden: {{ userDisplayName }} {{ userPrincipalName }} Die Erstellung wurde abgebrochen, da der Onboarding-Prozess bereits abgeschlossen wurde und andere Authentifizierungsmethoden für dieses Konto eingerichtet sind. Befristete Zugriffscodes dürfen nach Abschluss des Onboardings nicht mehr über den Self-Service-Prozess erstellt werden und erfordern die Unterstützung durch den IT-Support. Bitten Sie {{ userGivenName }}, sich mit den bestehenden Authentifizierungsmethoden anzumelden. Nächste Schritte, falls {{ userGivenName }} sich nicht anmelden kann: 1. Bitten Sie {{ userGivenName }}, den IT-Support zu kontaktieren und eine Zurücksetzung der Authentifizierungsmethoden anzufordern. 2. Der Support prüft die Anfrage und hilft bei den nächsten Schritten. '@ } fr = { TAPCreated = @' Un code d’accès temporaire pour l’intégration a été créé pour la personne suivant : {{ userDisplayName }} {{ userPrincipalName }} Veuillez transmettre le code suivant à {{ userGivenName }} : {{ temporaryAccessPass }} Le code expirera dans {{ lifeTimeInHoursMinutes }}, à {{ expirationTime }} UTC. Prochaines étapes pour {{ userGivenName }} : 1. Utilisez le code pour vous connecter lorsque cela vous est demandé. 2. Configurez l’application Microsoft Authenticator ou une autre méthode d’authentification. 3. Une fois la configuration terminée, le code d’accès temporaire peut être supprimé via https://aka.ms/MySecurityInfo. 4. Utilisez ensuite les nouvelles méthodes d’authentification que vous avez configurées. '@ UserBlocked = @' Un code d’accès temporaire n’a pas pu être créé pour la personne suivante : {{ userDisplayName }} {{ userPrincipalName }} La création a été bloquée car ce type de compte n’est pas autorisé à utiliser ce processus pour obtenir un code d’accès temporaire. Prochaines étapes si {{ userGivenName }} ne parvient pas à se connecter : 1. Demandez à {{ userGivenName }} de contacter l’assistance informatique et de demander la réinitialisation de ses méthodes d'authentification. 2. L’équipe d’assistance validera la demande et fournira des instructions pour la suite. '@ TAPNotEnabled = @' Un code d’accès temporaire n’a pas pu être créé pour la personne suivante : {{ userDisplayName }} {{ userPrincipalName }} La création a été annulée car la fonctionnalité de code d’accès temporaire n’est pas activée pour ce compte. Prochaines étapes si {{ userGivenName }} ne parvient pas à se connecter : 1. Demandez à {{ userGivenName }} de contacter l’assistance informatique et de demander la réinitialisation de ses méthodes d'authentification. 2. L’équipe d’assistance validera la demande et fournira des instructions pour la suite. '@ TAPExpiredWithOtherMethodsConfigured = @' Un code d’accès temporaire n’a pas pu être créé pour la personne suivante : {{ userDisplayName }} {{ userPrincipalName }} La création a été annulée car l’intégration de l’utilisateur est terminée et d’autres méthodes d’authentification ont été configurées. Le code d’accès temporaire précédemment émis n’est plus valide. Veuillez demander à {{ userGivenName }} de se connecter en utilisant ses méthodes d’authentification existantes. Prochaines étapes si {{ userGivenName }} ne parvient pas à se connecter : 1. Demandez à {{ userGivenName }} de contacter l’assistance informatique et de demander la réinitialisation de ses méthodes d’authentification. 2. L’équipe d’assistance validera la demande et fournira des instructions pour la suite. '@ TAPActiveWithOtherMethodsConfigured = @' Un code d’accès temporaire n’a pas pu être créé pour la personne suivante : {{ userDisplayName }} {{ userPrincipalName }} La création a été annulée car l’intégration de l’utilisateur est terminée et d’autres méthodes d’authentification ont été configurées. {{ userGivenName }} doit utiliser le code d’accès temporaire existant pour se connecter avant de pouvoir utiliser d’autres méthodes d’authentification. Si {{ userGivenName }} parvient à se connecter avec le code d’accès temporaire actif, il peut le supprimer lui-même sur https://aka.ms/MySecurityInfo afin d’utiliser ses autres méthodes d’authentification. Prochaines étapes si {{ userGivenName }} ne parvient pas à se connecter : 1. Demandez à {{ userGivenName }} de contacter l’assistance informatique et de demander la suppression du code d’accès temporaire actif, ou d’attendre son expiration. 2. Ensuite, {{ userGivenName }} peut essayer de se connecter avec une autre méthode d’authentification existante. 3. Si nécessaire, l’assistance informatique aidera à restaurer l’accès au compte. '@ OtherMethodsConfigured = @' Un code d’accès temporaire n’a pas pu être créé pour la personne suivante : {{ userDisplayName }} {{ userPrincipalName }} La création a été annulée car l’intégration de l’utilisateur est terminée et d’autres méthodes d’authentification sont déjà configurées. Après l’intégration, les codes d’accès temporaires ne doivent plus être délivrés via le processus en libre-service et nécessitent l’intervention de l’assistance informatique. Veuillez demander à {{ userGivenName }} de se connecter en utilisant ses méthodes d’authentification existantes. Prochaines étapes si {{ userGivenName }} ne parvient pas à se connecter : 1. Demandez à {{ userGivenName }} de contacter l’assistance informatique et de demander la réinitialisation de ses méthodes d’authentification. 2. L’équipe d’assistance validera la demande et fournira des instructions pour la suite. '@ } } #region Text email template $EmailTemplateTranslations = @{ #region EN template en = @' {{ emailSalutation }} {{ bodyText }} Thank you for your assistance. {{ emailClosing }} Note: If you have not made the request, you can safely ignore this e-mail. -- {{ emailBodyFooterHint }}{{ orgDisplayName }}, {{ orgStreet }}, {{ orgPostalCode }} {{ orgCity }}, {{ orgCountry }} Tenant ID: {{ orgTenantId }} '@ #endregion #region DE template de = @' {{ emailSalutation }} {{ bodyText }} Vielen Dank für Ihre Unterstützung. {{ emailClosing }} Hinweis: Wenn Sie die Anfrage nicht gestellt haben, können Sie diese E-Mail ohne Bedenken ignorieren. -- {{ emailBodyFooterHint }}{{ orgDisplayName }}, {{ orgStreet }}, {{ orgPostalCode }} {{ orgCity }}, {{ orgCountry }} Mandanten-ID: {{ orgTenantId }} '@ #endregion #region FR template fr = @' {{ emailSalutation }} {{ bodyText }} Merci pour votre collaboration. {{ emailClosing }} Remarque : si vous n’êtes pas à l’origine de cette demande, vous pouvez ignorer cet e-mail en toute sécurité. -- {{ emailBodyFooterHint }}{{ orgDisplayName }}, {{ orgStreet }}, {{ orgPostalCode }} {{ orgCity }}, {{ orgCountry }} ID du client (Tenant ID) : {{ orgTenantId }} '@ #endregion } #endregion if ([string]::IsNullOrEmpty($EmailTemplate)) { if ($EmailTemplateTranslations.ContainsKey($languageCode)) { Write-Verbose "Using custom text email template for $languageCode" $bodyContentTemplate = $EmailTemplateTranslations[$languageCode] } else { Write-Verbose 'Using default English text email template' $bodyContentTemplate = $EmailTemplateTranslations['en'] } } else { try { $EmailTemplates = ConvertFrom-Json -InputObject $EmailTemplate -AsHashtable -ErrorAction Stop if ($EmailTemplates -is [hashtable]) { if ($EmailTemplates.ContainsKey($languageCode)) { $bodyContentTemplate = $EmailTemplates[$languageCode] } elseif ($EmailClosings.ContainsKey('en')) { $bodyContentTemplate = $EmailTemplates['en'] } else { throw 'No default English template found' } } } catch { Write-Verbose 'EmailTemplate: Not a valid JSON object found, interpreting as raw text' $bodyContentTemplate = $EmailTemplate } } } #endregion # Expand template with variables $bodyContent = Expand-Template -Template $bodyContentTemplate -Variables $templateVariables # Extract all image references using regex to find actually used images if ($EmailImages.Count -gt 0 -and $UseHtmlEmail) { $usedImageKeys = @() # Find all occurrences of "cid:imageName" in the expanded template $cidMatches = [regex]::Matches($bodyContent, 'cid:([a-zA-Z0-9_\-]+)') foreach ($match in $cidMatches) { if ($match.Groups.Count -gt 1) { $usedImageKeys += $match.Groups[1].Value } } Write-Verbose "Found $($usedImageKeys.Count) image references in template" # Only process images that are actually used in the template $emailAttachments = @() foreach ($imageKey in $EmailImages.Keys) { if ($imageKey -in $usedImageKeys) { $imageBase64 = $EmailImages[$imageKey] # Validate the base64 data try { # Handle both clean base64 and data URLs if ($imageBase64 -match '^data:(.*?);base64,(.+)$') { $contentType = $Matches[1] $cleanBase64 = $Matches[2] } else { # Assume it's plain base64 $contentType = 'image/png' # Default content type $cleanBase64 = $imageBase64 } # Validate base64 string $null = [Convert]::FromBase64String($cleanBase64) $emailAttachments += @{ '@odata.type' = '#microsoft.graph.fileAttachment' name = "$imageKey" contentType = $contentType contentBytes = $cleanBase64 contentId = $imageKey isInline = $true } Write-Verbose "Added image attachment: $imageKey ($contentType)" } catch { Write-Warning "Invalid base64 image data for key '$imageKey': $_" } } } } # Construct email payload for Graph API $emailContent = @{ message = @{ subject = $templateVariables.emailSubject body = @{ contentType = $bodyContentType content = $bodyContent } toRecipients = @( @{ emailAddress = @{ address = $templateVariables.managerMail } } ) internetMessageHeaders = @( @{ name = 'X-Classification' value = if ($Reason -eq 'TAPCreated') { 'Confidential' } else { 'InternalOnly' } } @{ name = 'X-Exchange-Restrict' value = 'InternalOnly' } @{ name = 'X-No-Archive' value = 'Yes' } @{ name = 'X-Protect-Delivery' value = 'Secure' } @{ name = 'X-Sensitivity' value = if ($Reason -eq 'TAPCreated') { 'Company-Confidential' } else { 'Company-Internal' } } ) importance = if ($Reason -eq 'TAPCreated') { 'high' } else { 'normal' } } saveToSentItems = 'false' } # Add attachments only if we have any if ($emailAttachments.Count -gt 0) { $emailContent.message.attachments = $emailAttachments Write-Verbose "Added $($emailAttachments.Count) attachments to email" } if ($PSCmdlet.ShouldProcess( "Send Temporary Access Pass email to $($templateVariables.managerMail) from $SenderEmailAddress", "Do you want to send the TAP code for $($return.Data.DisplayName) to their manager at $($templateVariables.managerMail)?", 'Send Email with Temporary Access Pass' )) { try { $null = Invoke-ResilientRemoteCall { if (-not $WhatIfPreference) { Invoke-MgGraphRequest -Method POST ` -Uri "/v1.0/users/$SenderEmailAddress/sendMail" ` -Body ($emailContent | ConvertTo-Json -Depth 4) ` -ErrorAction Stop Write-Output "Temporary Access Pass code sent to manager $($templateVariables.managerMail) from $SenderEmailAddress" } else { Write-Verbose "What If: Would send email from $SenderEmailAddress to $($templateVariables.managerMail) with subject 'Temporary Access Pass for $($return.Data.DisplayName)'" } } } catch { # delete the TAP if email sending fails try { $null = Invoke-ResilientRemoteCall { if (-not $WhatIfPreference) { Invoke-MgGraphRequest -Method DELETE ` -Uri "/v1.0/users/$($userObj.id)/authentication/temporaryAccessPassMethods/$($return.Data.TemporaryAccessPass.Id)" ` -ErrorAction Stop } } } catch { Write-Error "Failed to delete TAP after email sending failed: $_" } throw "Failed to send Temporary Access Pass code to manager $($templateVariables.managerMail): $_" } } else { Write-Verbose "Email sending was canceled by user" } } #endregion #region Azure Automation Webhook data if ($WebhookData) { # Convert $WebhookData from JSON string to object if ($WebhookData -is [string]) { try { $WebhookData = ConvertFrom-Json -InputObject $WebhookData -ErrorAction Stop Write-Verbose 'Converted WebhookData JSON to object' } catch { throw "Invalid WebhookData JSON: $_" } } Write-Verbose "WEBHOOK-START" Write-Verbose ("object type: {0}" -f $WebhookData.gettype()) Write-Verbose $WebhookData Write-Verbose "`n`n" Write-Verbose $WebhookData.WebhookName Write-Verbose $WebhookData.RequestBody Write-Verbose $WebhookData.RequestHeader Write-Verbose "WEBHOOK-END" if ([string]::IsNullOrEmpty($WebhookSignatureKey)) { try { $WebhookSignatureKey = Get-AutomationVariable -Name 'AuthConfig_WebhookSignatureKey' -ErrorAction Stop | ConvertTo-SecureString -AsPlainText -Force } catch { throw 'WebhookSignatureKey is required when using a webhook' } } if (-not (Test-HmacAuthorization -SharedSecret $WebhookSignatureKey -WebhookData $WebhookData)) { [void] $WebhookSignatureKey.Dispose() throw 'Unauthorized webhook request' } [void] $WebhookSignatureKey.Dispose() Write-Verbose 'Webhook request authorized' try { $request = ConvertFrom-Json -InputObject $WebhookData.RequestBody -ErrorAction Stop Write-Verbose 'Request JSON parsed' } catch { throw 'Invalid Webhook RequestBody JSON' } # Always send email to manager when using a webhook $SendEmailToManager = $true try { # Validate and assign parameters if ($null -ne $request.UserId) { if (Assert-ParameterType -Name 'UserId' -Value $request.UserId -ExpectedTypes ([string])) { $UserId = $request.UserId } } if ($null -ne $request.StartDateTime) { if (Assert-ParameterType -Name 'StartDateTime' -Value $request.StartDateTime -ExpectedTypes ([datetime])) { $StartDateTime = $request.StartDateTime } } if ($null -ne $request.LifetimeInMinutes) { if (Assert-ParameterType -Name 'LifetimeInMinutes' -Value $request.LifetimeInMinutes -ExpectedTypes ([Int32], [Int64])) { try { $LifetimeInMinutes = [Int32] $request.LifetimeInMinutes } catch { throw "Parameter 'LifetimeInMinutes' must be a valid integer" } } } if ($null -ne $request.IsUsableOnce) { if (Assert-ParameterType -Name 'IsUsableOnce' -Value $request.IsUsableOnce -ExpectedTypes ([bool])) { $IsUsableOnce = $request.IsUsableOnce } } if ($null -ne $request.EmailLanguage) { if (Assert-ParameterType -Name 'EmailLanguage' -Value $request.EmailLanguage -ExpectedTypes ([string])) { $EmailLanguage = $request.EmailLanguage } } # Check for other unexpected parameters $validParameters = @( 'UserId', 'StartDateTime', 'LifetimeInMinutes', 'IsUsableOnce', 'EmailLanguage' ) $unexpectedParams = $request.PSObject.Properties.Name | Where-Object { $_ -notin $validParameters } if ($unexpectedParams) { Write-Warning "Ignoring unexpected parameters: $($unexpectedParams -join ', ')" } } catch { throw "Invalid Webhook RequestBody: $_" } } #endregion if (-not $UserId) { throw 'UserId is required.' } if (-not $WhatIfPreference -and $Simulate) { $WhatIfPreference = $true } if ('AzureAutomation/' -eq $env:AZUREPS_HOST_ENVIRONMENT -or $PSPrivateMetadata.JobId) { $OutJson = $true $ProgressPreference = 'SilentlyContinue' } # Initialize return object as ordered for consistent output $return = [ordered]@{} # Define required Microsoft Graph scopes $mgScopes = [string[]]@( 'User.Read.All' # To read user information, inlcuding EmployeeHireDate 'UserAuthenticationMethod.ReadWrite.All' # To update authentication methods (TAP) of the user 'Policy.Read.All' # To read and validate current policy settings 'Directory.Read.All' # To read directory data and settings ) # Make sure Mail.Send permission is included if SendEmailToManager is requested if ($SendEmailToManager) { $mgScopes += 'MailboxSettings.Read' # To read mailbox language $mgScopes += 'Mail.Send' # To send email on behalf of a shared mailbox } # User validation - reject specific account types $invalidUserRegexPatterns = @( '^A[0-9][A-Z][-_].+@.+$', # Tiered admin accounts '^ADM[CL]?[-_].+@.+$', # Non-Tiered admin accounts '^.+#EXT#@.+\.onmicrosoft\.com$', # External Accounts '^(?:SVCC?_.+|SVC[A-Z0-9]+)@.+$', # Service Accounts '^(?:Sync_.+|[A-Z]+SyncServiceAccount.*)@.+$' # Entra Sync Accounts ) if ($invalidUserRegexPatterns | Where-Object { $UserId -match $_ }) { Write-Error 'This type of user can not have a Temporary Access Pass created using this process.' exit 1 } # Handle Graph authentication if (-not (Get-MgContext)) { try { if ('AzureAutomation/' -eq $env:AZUREPS_HOST_ENVIRONMENT -or $PSPrivateMetadata.JobId) { $null = Invoke-ResilientRemoteCall { Connect-MgGraph -Identity -ContextScope Process } Write-Verbose "Connected with managed identity: $((Get-MgContext).Account)" } else { $null = Invoke-ResilientRemoteCall { Connect-MgGraph -Scopes $mgScopes -ContextScope Process } } } catch { throw "Failed to connect to Microsoft Graph: $_" } } # Check if current scopes are sufficient $missingScopes = [List[string]]::new() $currentScopes = (Get-MgContext).Scopes foreach ($scope in $mgScopes) { # Skip write scopes if in WhatIf mode if ($WhatIfPreference -and $scope -like '*Write*') { Write-Verbose "What If: Removed $scope from required Microsoft Graph scopes" continue } if ($scope -notin $currentScopes) { $missingScopes.Add($scope) } } if ($missingScopes.Count -gt 0) { if ('AzureAutomation/' -eq $env:AZUREPS_HOST_ENVIRONMENT -or $PSPrivateMetadata.JobId) { throw "Missing Microsoft Graph authorization scopes:`n$($missingScopes -join "`n")" } else { # Reconnect with required scopes try { $null = Invoke-ResilientRemoteCall { Connect-MgGraph -Scopes $mgScopes -ContextScope Process } } catch { throw "Failed to connect with required scopes: $_" } } } # Get Temporary Access Pass configuration try { $tapConfig = Invoke-ResilientRemoteCall { (Invoke-MgGraphRequest -Method GET -Uri '/v1.0/policies/authenticationMethodsPolicy' -ErrorAction Stop).authenticationMethodConfigurations | Where-Object { $_.id -eq 'temporaryAccessPass' } } if ($tapConfig.state -ne 'enabled') { throw "Temporary Access Pass authentication method is disabled for tenant $((Get-MgContext).TenantId)" } } catch { throw "Failed to retrieve TAP configuration: $_" } # Validate parameters if ($StartDateTime -and ($StartDateTime.ToUniversalTime() -lt (Get-Date).ToUniversalTime().AddMinutes(1))) { throw 'StartDateTime: Time cannot be in the past.' } if ($LifetimeInMinutes) { if ($LifetimeInMinutes -gt $tapConfig.maximumLifetimeInMinutes) { $LifetimeInMinutes = $tapConfig.maximumLifetimeInMinutes Write-Warning "LifetimeInMinutes: Maximum lifetime capped at $LifetimeInMinutes minutes." } if ($LifetimeInMinutes -lt $tapConfig.minimumLifetimeInMinutes) { $LifetimeInMinutes = $tapConfig.minimumLifetimeInMinutes Write-Warning "LifetimeInMinutes: Minimum lifetime capped at $LifetimeInMinutes minutes." } } if ($SendEmailToManager) { if ([string]::IsNullOrEmpty($SenderEmailAddress)) { if ('AzureAutomation/' -eq $env:AZUREPS_HOST_ENVIRONMENT -or $PSPrivateMetadata.JobId) { try { $SenderEmailAddress = Get-AutomationVariable -Name 'TAPConfig_SenderEmailAddress' -ErrorAction Stop } catch { throw 'SenderEmailAddress is required when sending email to manager.' } } else { $SenderEmailAddress = (Get-MgContext).Account Write-Verbose "Using authenticated user $SenderEmailAddress as sender email address" } } if ('AzureAutomation/' -eq $env:AZUREPS_HOST_ENVIRONMENT -or $PSPrivateMetadata.JobId) { if ([string]::IsNullOrEmpty($EmailLanguage)) { try { $EmailLanguage = Get-AutomationVariable -Name 'TAPConfig_EmailLanguage' -ErrorAction Stop } catch { # Use default language } } if ([string]::IsNullOrEmpty($EmailSubject)) { try { $EmailSubject = Get-AutomationVariable -Name 'TAPConfig_EmailSubject' -ErrorAction Stop } catch { # Use default subject } } if ([string]::IsNullOrEmpty($EmailTitle)) { try { $EmailTitle = Get-AutomationVariable -Name 'TAPConfig_EmailTitle' -ErrorAction Stop } catch { # Use default title } } if ([string]::IsNullOrEmpty($EmailSalutation)) { try { $EmailSalutation = Get-AutomationVariable -Name 'TAPConfig_EmailSalutation' -ErrorAction Stop } catch { # Use default salutation } } if ([string]::IsNullOrEmpty($EmailClosing)) { try { $EmailClosing = Get-AutomationVariable -Name 'TAPConfig_EmailClosing' -ErrorAction Stop } catch { # Use default closing } } if ([string]::IsNullOrEmpty($EmailTemplate)) { try { $EmailTemplate = Get-AutomationVariable -Name 'TAPConfig_EmailTemplate' -ErrorAction Stop } catch { # Use default template } } if ([string]::IsNullOrEmpty($EmailBodyText)) { try { $EmailBodyText = Get-AutomationVariable -Name 'TAPConfig_EmailBodyText' -ErrorAction Stop } catch { # Use default template } } if ([string]::IsNullOrEmpty($EmailImagesJson)) { try { $EmailImagesJson = Get-AutomationVariable -Name 'TAPConfig_EmailImagesJson' -ErrorAction Stop } catch { # Use default images } } if ([string]::IsNullOrEmpty($UseHtmlEmail)) { try { $rawValue = Get-AutomationVariable -Name 'TAPConfig_UseHtmlEmail' -ErrorAction Stop if ($rawValue -is [bool]) { $UseHtmlEmail = $rawValue } else { try { $UseHtmlEmail = [System.Convert]::ToBoolean($rawValue) } catch { throw "Failed to convert TAPConfig_UseHtmlEmail value '$rawValue' to a boolean." } } } catch { # Use default HTML email setting } } } } # Get user information try { $select = 'id,userPrincipalName,mail,displayName,givenName,surname,employeeHireDate,userType,accountEnabled,usageLocation,preferredLanguage' $expand = 'manager($select=id,userPrincipalName,mail,displayName,givenName,surname,preferredLanguage)' $userObj = Invoke-ResilientRemoteCall { Invoke-MgGraphRequest -Method GET ` -Uri "/v1.0/users/$UserId`?`$select=$select&`$expand=$expand" ` -ErrorAction Stop } if ($SendEmailToManager) { try { $userObj.mailboxLanguage = Invoke-ResilientRemoteCall { Invoke-MgGraphRequest -Method GET ` -Uri "/v1.0/users/$UserId/mailboxSettings/language" ` -ErrorAction Stop } Write-Verbose "Retrieved mailbox language: $($userObj.mailboxLanguage.locale)" } catch { Write-Warning "Failed to retrieve mailbox language: $_" } if ($null -ne $userObj.manager -and $null -ne $userObj.manager.id) { try { $userObj.manager.mailboxLanguage = Invoke-ResilientRemoteCall { Invoke-MgGraphRequest -Method GET ` -Uri "/v1.0/users/$($userObj.manager.id)/mailboxSettings/language" ` -ErrorAction Stop } Write-Verbose "Retrieved manager mailbox language: $($userObj.manager.mailboxLanguage.locale)" } catch { Write-Warning "Failed to retrieve manager mailbox language: $_" } } } } catch { throw "Failed to retrieve user information: $_" } if ($null -eq $userObj.manager -or -not $userObj.manager.id) { Write-Error 'User ID does not have a manager.' exit 1 } if ($SendEmailToManager) { if ([string]::IsNullOrEmpty($userObj.manager.mail)) { Write-Error "Cannot send email: Manager for $($userObj.displayName) does not have an email address." exit 1 } } # Build return data structure $return.Data = [PSCustomObject]@{ '@odata.context' = $userObj.'@odata.context' Id = $userObj.id UserPrincipalName = $userObj.userPrincipalName GivenName = $userObj.givenName Surname = $userObj.surname Mail = $userObj.mail DisplayName = $userObj.displayName EmployeeHireDate = $userObj.employeeHireDate UserType = $userObj.userType AccountEnabled = $userObj.accountEnabled UsageLocation = $userObj.usageLocation PreferredLanguage = $userObj.mailboxLanguage.locale ?? $userObj.preferredLanguage Manager = [PSCustomObject]@{ Id = $userObj.manager.id UserPrincipalName = $userObj.manager.userPrincipalName Mail = $userObj.manager.mail DisplayName = $userObj.manager.displayName GivenName = $userObj.manager.givenName Surname = $userObj.manager.surname PreferredLanguage = $userObj.manager.mailboxLanguage.locale ?? $userObj.manager.preferredLanguage } AuthenticationMethods = [List[string]]::new() } # Validate user account state if (-not $userObj.accountEnabled) { Write-Error 'User ID is disabled.' if ($SendEmailToManager) { Send-EmailNotification -Reason UserBlocked ` -SenderEmailAddress $SenderEmailAddress ` -EmailLanguage $EmailLanguage ` -EmailSubject $EmailSubject ` -EmailTitle $EmailTitle ` -EmailSalutation $EmailSalutation ` -EmailClosing $EmailClosing ` -EmailBodyText $EmailBodyText ` -EmailTemplate $EmailTemplate ` -EmailImagesJson $EmailImagesJson ` -UseHtmlEmail $UseHtmlEmail ` -Data $return.Data } exit 1 } if ($userObj.userType -ne 'Member') { Write-Error 'User ID needs to be of type Member.' if ($SendEmailToManager) { Send-EmailNotification -Reason UserBlocked ` -SenderEmailAddress $SenderEmailAddress ` -EmailLanguage $EmailLanguage ` -EmailSubject $EmailSubject ` -EmailTitle $EmailTitle ` -EmailSalutation $EmailSalutation ` -EmailClosing $EmailClosing ` -EmailBodyText $EmailBodyText ` -EmailTemplate $EmailTemplate ` -EmailImagesJson $EmailImagesJson ` -UseHtmlEmail $UseHtmlEmail ` -Data $return.Data } exit 1 } if ($null -ne $userObj.employeeHireDate) { if ($userObj.employeeHireDate -ge (Get-Date).Date) { Write-Error 'User ID has a future hire date.' if ($SendEmailToManager) { Send-EmailNotification -Reason UserBlocked ` -SenderEmailAddress $SenderEmailAddress ` -EmailLanguage $EmailLanguage ` -EmailSubject $EmailSubject ` -EmailTitle $EmailTitle ` -EmailSalutation $EmailSalutation ` -EmailClosing $EmailClosing ` -EmailBodyText $EmailBodyText ` -EmailTemplate $EmailTemplate ` -EmailImagesJson $EmailImagesJson ` -UseHtmlEmail $UseHtmlEmail ` -Data $return.Data } exit 1 } } # Get user security groups try { $userGroupsResponse = Invoke-ResilientRemoteCall { Invoke-MgGraphRequest -Method POST ` -Uri "/v1.0/users/$($userObj.id)/getMemberGroups" ` -Body @{ securityEnabledOnly = $true } | ConvertTo-Json -ErrorAction Stop } $userGroupIds = $userGroupsResponse.value } catch { throw "Failed to retrieve user groups: $_" } # Check if user can use TAP based on group memberships $isExcluded = $false $isIncluded = $false # Check exclusion rules if ($null -ne $tapConfig.excludeTargets) { $isExcluded = $tapConfig.excludeTargets | Where-Object { $_.targetType -eq 'group' -and $_.id -in $userGroupIds } | Select-Object -First 1 } # Check inclusion rules if ($null -ne $tapConfig.includeTargets) { $isIncluded = $tapConfig.includeTargets | Where-Object { $_.targetType -eq 'group' -and ($_.id -eq 'all_users' -or $_.id -in $userGroupIds) } | Select-Object -First 1 } if ($isExcluded -or -not $isIncluded) { Write-Error "Authentication method 'Temporary Access Pass' is not enabled for this user ID." if ($SendEmailToManager) { Send-EmailNotification -Reason TAPNotEnabled ` -SenderEmailAddress $SenderEmailAddress ` -EmailLanguage $EmailLanguage ` -EmailSubject $EmailSubject ` -EmailTitle $EmailTitle ` -EmailSalutation $EmailSalutation ` -EmailClosing $EmailClosing ` -EmailBodyText $EmailBodyText ` -EmailTemplate $EmailTemplate ` -EmailImagesJson $EmailImagesJson ` -UseHtmlEmail $UseHtmlEmail ` -Data $return.Data } exit 1 } # Get user's authentication methods try { $authMethods = Invoke-ResilientRemoteCall { Invoke-MgGraphRequest -Method GET ` -Uri "/v1.0/users/$($userObj.id)/authentication/methods" ` -ErrorAction Stop } } catch { throw "Failed to retrieve authentication methods: $_" } # Process authentication methods foreach ($authMethod in $authMethods.value) { if ($authMethod.'@odata.type' -match '^#microsoft\.graph\.(.+)AuthenticationMethod$') { $methodType = $Matches[1] $return.Data.AuthenticationMethods.Add($methodType) if ($methodType -eq 'temporaryAccessPass') { Write-Verbose "Found existing TAP Id $($authMethod.id)" $return.Data | Add-Member -NotePropertyName 'TemporaryAccessPass' -NotePropertyValue $authMethod $return.Data.TemporaryAccessPass | Add-Member -NotePropertyName 'Id' -NotePropertyValue $authMethod.id -Force } } } # Handle existing TAP and authentication methods if ($return.Data.AuthenticationMethods.Count -gt 0) { if ('temporaryAccessPass' -in $return.Data.AuthenticationMethods) { # Check if TAP is the only authentication method besides password $canDeleteTap = 'password' -in $return.Data.AuthenticationMethods -and $return.Data.AuthenticationMethods.Count -le 2 if ($canDeleteTap) { if ($return.Data.TemporaryAccessPass.methodUsabilityReason -ne 'Expired') { Write-Warning 'A Temporary Access Pass code was already set before.' } if ($PSCmdlet.ShouldProcess( "Delete existing Temporary Access Pass for $($userObj.userPrincipalName)", "Do you confirm to remove the existing TAP for $($userObj.userPrincipalName) ?", 'Delete existing Temporary Access Pass' )) { try { $null = Invoke-ResilientRemoteCall { if (-not $WhatIfPreference) { Invoke-MgGraphRequest -Method DELETE ` -Uri "/v1.0/users/$($userObj.id)/authentication/temporaryAccessPassMethods/$($return.Data.TemporaryAccessPass.Id)" ` -ErrorAction Stop } } $return.Data.PSObject.Properties.Remove('TemporaryAccessPass') } catch { throw "Failed to delete existing TAP: $_" } } elseif ($WhatIfPreference) { Write-Verbose 'What If: An existing Temporary Access Pass would have been deleted.' } else { Write-Error 'Deletion of existing Temporary Access Pass was aborted.' exit 1 } } else { # User has other authentication methods besides password and TAP if ($return.Data.TemporaryAccessPass.methodUsabilityReason -eq 'Expired') { $errorMsg = [StringBuilder]::new() [void] $errorMsg.Append("An expired Temporary Access Pass code was found. `n") [void] $errorMsg.Append('However, this process cannot be used to renew the Temporary Access Pass code because you have already configured other multi-factor authentication methods. ') [void] $errorMsg.Append('Note that a Temporary Access Pass is only required during the initial onboarding process. ') [void] $errorMsg.Append('You can then use your existing access to register additional methods, for example a security key. ') [void] $errorMsg.Append('However, if you later lose access to all your multi-factor authentication methods, this self-service process cannot be used to recover. ') [void] $errorMsg.Append('In this case, please contact the Global Service Desk who will help you reset your MFA methods.') if ($SendEmailToManager) { Send-EmailNotification -Reason TAPExpiredWithOtherMethodsConfigured ` -SenderEmailAddress $SenderEmailAddress ` -EmailLanguage $EmailLanguage ` -EmailSubject $EmailSubject ` -EmailTitle $EmailTitle ` -EmailSalutation $EmailSalutation ` -EmailClosing $EmailClosing ` -EmailBodyText $EmailBodyText ` -EmailTemplate $EmailTemplate ` -EmailImagesJson $EmailImagesJson ` -UseHtmlEmail $UseHtmlEmail ` -Data $return.Data } } else { $errorMsg = [StringBuilder]::new() [void] $errorMsg.Append("An active Temporary Access Pass code has already been found. `n") [void] $errorMsg.Append('It can only be displayed once after it has been created. ') [void] $errorMsg.Append('As you have already configured other methods of multi-factor authentication, a new Temporary Access Pass can no longer be created via this self-service process. ') [void] $errorMsg.Append('In this case, please contact the Global Service Desk who will help you reset your MFA methods.') if ($SendEmailToManager) { Send-EmailNotification -Reason TAPActiveWithOtherMethodsConfigured ` -SenderEmailAddress $SenderEmailAddress ` -EmailLanguage $EmailLanguage ` -EmailSubject $EmailSubject ` -EmailTitle $EmailTitle ` -EmailSalutation $EmailSalutation ` -EmailClosing $EmailClosing ` -EmailBodyText $EmailBodyText ` -EmailTemplate $EmailTemplate ` -EmailImagesJson $EmailImagesJson ` -UseHtmlEmail $UseHtmlEmail ` -Data $return.Data } } Write-Error $errorMsg.ToString() exit 1 } } # Check if user has other authentication methods besides password elseif ($return.Data.AuthenticationMethods.Count -gt 1 -or 'password' -notin $return.Data.AuthenticationMethods) { $errorMsg = [StringBuilder]::new() [void] $errorMsg.Append('This process cannot be used to request a Temporary Access Pass code as you have already configured other multi-factor authentication methods. ') [void] $errorMsg.Append('Note that a Temporary Access Pass is only required during the initial onboarding process. ') [void] $errorMsg.Append('You can then use your existing access to register additional methods, for example a security key. ') [void] $errorMsg.Append('However, if you later lose access to all your multi-factor authentication methods, this self-service process cannot be used to recover. ') [void] $errorMsg.Append('In this case, please contact the Global Service Desk who will help you reset your MFA methods.') Write-Error $errorMsg.ToString() if ($SendEmailToManager) { Send-EmailNotification -Reason OtherMethodsConfigured ` -SenderEmailAddress $SenderEmailAddress ` -EmailLanguage $EmailLanguage ` -EmailSubject $EmailSubject ` -EmailTitle $EmailTitle ` -EmailSalutation $EmailSalutation ` -EmailClosing $EmailClosing ` -EmailBodyText $EmailBodyText ` -EmailTemplate $EmailTemplate ` -EmailImagesJson $EmailImagesJson ` -UseHtmlEmail $UseHtmlEmail ` -Data $return.Data } exit 1 } } # Create new TAP if no existing one or it was deleted if ($WhatIfPreference -or -not ($return.Data.PSObject.Properties['TemporaryAccessPass'])) { $tapParams = @{} if ($StartDateTime) { $tapParams.startDateTime = $StartDateTime.ToUniversalTime().ToString("o") } if ($IsUsableOnce) { $tapParams.isUsableOnce = $true } if ($LifetimeInMinutes) { $tapParams.lifetimeInMinutes = $LifetimeInMinutes } if ($PSCmdlet.ShouldProcess( "Create new Temporary Access Pass for $($userObj.userPrincipalName)", "Do you confirm to create a new TAP for $($userObj.userPrincipalName) ?", 'New Temporary Access Pass' )) { try { $tap = Invoke-ResilientRemoteCall { if ($WhatIfPreference) { Write-Verbose "What If: Would create a new TAP with parameters: $($tapParams | ConvertTo-Json -Compress)" return $null } else { Invoke-MgGraphRequest -Method POST ` -Uri "/v1.0/users/$($userObj.id)/authentication/temporaryAccessPassMethods" ` -Body ($tapParams | ConvertTo-Json) ` -ErrorAction Stop } } if ($tap) { $return.Data | Add-Member -NotePropertyName 'TemporaryAccessPass' -NotePropertyValue $tap -Force if ('temporaryAccessPass' -notin $return.Data.AuthenticationMethods) { $return.Data.AuthenticationMethods.Add('temporaryAccessPass') } Write-Verbose 'A new Temporary Access Pass code was created.' } elseif ($WhatIfPreference) { # Expected behavior in WhatIf mode } else { throw "Failed to create TAP but no exception was thrown" } } catch { Write-Error "Failed to create new Temporary Access Pass: $_" exit 1 } } elseif ($WhatIfPreference) { Write-Verbose "What If: A new Temporary Access Pass code would have been created with the following parameters:`n$(($tapParams | Out-String).TrimEnd())" $return.WhatIf = @{ returnCode = 0 message = 'A Temporary Access Pass code may be created for this user ID.' } } else { Write-Error 'Creation of new Temporary Access Pass code was aborted.' exit 1 } } # Send email to manager if ($SendEmailToManager) { return Send-EmailNotification -Reason TAPCreated ` -SenderEmailAddress $SenderEmailAddress ` -EmailLanguage $EmailLanguage ` -EmailSubject $EmailSubject ` -EmailTitle $EmailTitle ` -EmailSalutation $EmailSalutation ` -EmailClosing $EmailClosing ` -EmailBodyText $EmailBodyText ` -EmailTemplate $EmailTemplate ` -EmailImagesJson $EmailImagesJson ` -UseHtmlEmail $UseHtmlEmail ` -Data $return.Data } # Return results if ($return.Data.PSObject.Properties.Count -eq 0) { $return.Remove('Data') } if ($OutText) { return $return.Data?.TemporaryAccessPass?.temporaryAccessPass ?? $null } if ($OutJson) { return $return | ConvertTo-Json -Depth 4 } return $return