M365CertAuth.psm1

# Certificate Authentication Module for Microsoft 365

#Requires -Modules Microsoft.Graph.Authentication, Microsoft.Graph.Applications, Microsoft.Graph.Identity.DirectoryManagement

# Module-level variables for tenant storage
$script:TenantStoragePath = "$env:APPDATA\M365CertAuth\tenants.json"
$script:CachedTenants = $null

# Load required assemblies for encryption
Add-Type -AssemblyName System.Security

function Get-StoredTenants {
    <#
    .SYNOPSIS
    Retrieves stored tenant information from local storage
    
    .DESCRIPTION
    Loads tenant information from encrypted local storage file
    #>

    
    if ($null -ne $script:CachedTenants) {
        return $script:CachedTenants
    }
    
    if (-not (Test-Path $script:TenantStoragePath)) {
        return @()
    }
    
    try {
        $encryptedData = Get-Content $script:TenantStoragePath -Raw
        if ([string]::IsNullOrWhiteSpace($encryptedData)) {
            return @()
        }
        
        $decryptedData = [System.Text.Encoding]::UTF8.GetString([System.Security.Cryptography.ProtectedData]::Unprotect([Convert]::FromBase64String($encryptedData), $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser))
        $parsedData = ConvertFrom-Json $decryptedData
        
        # Store as array and return as array
        if ($parsedData -is [array]) {
            $script:CachedTenants = $parsedData
        } else {
            $script:CachedTenants = @($parsedData)
        }
        return $script:CachedTenants
    } catch {
        Write-Warning "Failed to load stored tenants: $($_.Exception.Message)"
        return @()
    }
}

function Save-TenantInfo {
    <#
    .SYNOPSIS
    Saves tenant information to encrypted local storage
    
    .DESCRIPTION
    Encrypts and stores tenant information for future use
    
    .PARAMETER TenantInfo
    Hashtable containing tenant information (TenantId, TenantName, PrimaryDomain, etc.)
    #>

    param(
        [Parameter(Mandatory = $true)]
        [hashtable]$TenantInfo
    )
    
    try {
        # Validate required fields
        if (-not $TenantInfo.TenantId -or -not $TenantInfo.PrimaryDomain) {
            Write-Warning "Invalid tenant information: TenantId and PrimaryDomain are required"
            return
        }
        
        # Ensure storage directory exists
        $storageDir = Split-Path $script:TenantStoragePath -Parent
        if (-not (Test-Path $storageDir)) {
            New-Item -Path $storageDir -ItemType Directory -Force | Out-Null
        }
        
        # Load existing tenants - force fresh read to avoid cache issues
        $script:CachedTenants = $null
        $tenants = @(Get-StoredTenants)
        
        # Remove any existing entries for this tenant (by TenantId)
        $tenants = $tenants | Where-Object { $_.TenantId -ne $TenantInfo.TenantId }
        
        # Add updated tenant info with timestamp
        $TenantInfo.LastUsed = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
        
        # Create new array with the updated tenant
        $updatedTenants = @($tenants) + @($TenantInfo)
        
        # Keep only the 20 most recently used tenants
        $finalTenants = $updatedTenants | Sort-Object LastUsed -Descending | Select-Object -First 20
        
        # Ensure we have a proper array for JSON serialization
        if ($finalTenants.Count -eq 1) {
            $jsonData = ConvertTo-Json @($finalTenants) -Depth 3
        } else {
            $jsonData = ConvertTo-Json $finalTenants -Depth 3
        }
        
        # Encrypt and save
        $encryptedData = [Convert]::ToBase64String([System.Security.Cryptography.ProtectedData]::Protect([System.Text.Encoding]::UTF8.GetBytes($jsonData), $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser))
        Set-Content -Path $script:TenantStoragePath -Value $encryptedData -Force
        
        # Update cache with final tenants
        $script:CachedTenants = $finalTenants
        
    } catch {
        Write-Warning "Failed to save tenant information: $($_.Exception.Message)"
        # Clear cache on error to force reload next time
        $script:CachedTenants = $null
    }
}

function Select-Tenant {
    <#
    .SYNOPSIS
    Prompts user to select from stored tenants or add a new one
    
    .DESCRIPTION
    Displays a menu of stored tenants and allows user to select one or add a new tenant
    
    .PARAMETER AllowNew
    Whether to allow adding a new tenant
    #>

    param(
        [switch]$AllowNew
    )
    
    # Default AllowNew to true if not specified
    if (-not $PSBoundParameters.ContainsKey('AllowNew')) {
        $AllowNew = $true
    }
    
    $storedTenants = @(Get-StoredTenants)
    
    if ($storedTenants.Count -eq 0 -and -not $AllowNew) {
        return $null
    }
    
    Write-Host "`nTenant Selection" -ForegroundColor Cyan
    Write-Host "=================" -ForegroundColor Cyan
    
    if ($storedTenants.Count -gt 0) {
        Write-Host "Stored tenants ($($storedTenants.Count) found):" -ForegroundColor Yellow
        for ($i = 0; $i -lt $storedTenants.Count; $i++) {
            $tenant = $storedTenants[$i]
            $lastUsed = if ($tenant.LastUsed) { " (last used: $($tenant.LastUsed))" } else { "" }
            Write-Host " $($i + 1). $($tenant.PrimaryDomain) - $($tenant.TenantName)$lastUsed" -ForegroundColor White
        }
        
        if ($AllowNew) {
            Write-Host " $($storedTenants.Count + 1). Add new tenant" -ForegroundColor Green
            Write-Host " 0. Cancel" -ForegroundColor Red
        } else {
            Write-Host " 0. Cancel" -ForegroundColor Red
        }
    } else {
        if ($AllowNew) {
            Write-Host "No stored tenants found. You'll need to add a new tenant." -ForegroundColor Yellow
            Write-Host " 1. Add new tenant" -ForegroundColor Green
            Write-Host " 0. Cancel" -ForegroundColor Red
        } else {
            Write-Host "No stored tenants found." -ForegroundColor Yellow
            return $null
        }
    }
    
    do {
        $selection = Read-Host "`nSelect option"
        
        if ($selection -eq "0") {
            return $null
        }
        
        $selectionNum = $null
        if ([int]::TryParse($selection, [ref]$selectionNum)) {
            if ($selectionNum -ge 1 -and $selectionNum -le $storedTenants.Count) {
                return $storedTenants[$selectionNum - 1]
            } elseif ($AllowNew -and $selectionNum -eq ($storedTenants.Count + 1)) {
                return "NEW"
            }
        }
        
        Write-Host "Invalid selection. Please try again." -ForegroundColor Red
    } while ($true)
}

function Remove-StoredTenant {
    <#
    .SYNOPSIS
    Removes a stored tenant from local storage
    
    .DESCRIPTION
    Allows user to select and remove stored tenant information
    #>

    
    $storedTenants = @(Get-StoredTenants)
    
    if ($storedTenants.Count -eq 0) {
        Write-Host "No stored tenants to remove." -ForegroundColor Yellow
        return
    }
    
    Write-Host "`nRemove Stored Tenant" -ForegroundColor Cyan
    Write-Host "====================" -ForegroundColor Cyan
    
    for ($i = 0; $i -lt $storedTenants.Count; $i++) {
        $tenant = $storedTenants[$i]
        Write-Host " $($i + 1). $($tenant.PrimaryDomain) - $($tenant.TenantName)" -ForegroundColor White
    }
    Write-Host " 0. Cancel" -ForegroundColor Red
    
    do {
        $selection = Read-Host "`nSelect tenant to remove"
        
        if ($selection -eq "0") {
            return
        }
        
        $selectionNum = $null
        if ([int]::TryParse($selection, [ref]$selectionNum) -and $selectionNum -ge 1 -and $selectionNum -le $storedTenants.Count) {
            $tenantToRemove = $storedTenants[$selectionNum - 1]
            $confirmation = Read-Host "Remove tenant '$($tenantToRemove.PrimaryDomain)' - $($tenantToRemove.TenantName)? (y/N)"
            
            if ($confirmation -eq 'y' -or $confirmation -eq 'Y') {
                $remainingTenants = $storedTenants | Where-Object { $_.TenantId -ne $tenantToRemove.TenantId }
                
                if ($remainingTenants.Count -eq 0) {
                    # Remove file if no tenants left
                    if (Test-Path $script:TenantStoragePath) {
                        Remove-Item $script:TenantStoragePath -Force
                    }
                } else {
                    # Save remaining tenants
                    $jsonData = ConvertTo-Json $remainingTenants -Depth 3
                    $encryptedData = [Convert]::ToBase64String([System.Security.Cryptography.ProtectedData]::Protect([System.Text.Encoding]::UTF8.GetBytes($jsonData), $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser))
                    Set-Content -Path $script:TenantStoragePath -Value $encryptedData -Force
                }
                
                # Update cache
                $script:CachedTenants = $remainingTenants
                
                Write-Host "Tenant removed successfully." -ForegroundColor Green
            }
            return
        } else {
            Write-Host "Invalid selection. Please try again." -ForegroundColor Red
        }
    } while ($true)
}

function Show-StoredTenants {
    <#
    .SYNOPSIS
    Displays all stored tenant information
    
    .DESCRIPTION
    Shows a formatted list of all stored tenants with their details
    #>

    
    $storedTenants = @(Get-StoredTenants)
    
    if ($storedTenants.Count -eq 0) {
        Write-Host "No stored tenants found." -ForegroundColor Yellow
        return
    }
    
    Write-Host "`nStored Tenants" -ForegroundColor Cyan
    Write-Host "==============" -ForegroundColor Cyan
    Write-Host "Found $($storedTenants.Count) stored tenant(s):`n" -ForegroundColor Yellow
    
    $sortedTenants = $storedTenants | Sort-Object LastUsed -Descending
    
    foreach ($tenant in $sortedTenants) {
        Write-Host "Tenant: $($tenant.PrimaryDomain)" -ForegroundColor White
        Write-Host " Name: $($tenant.TenantName)" -ForegroundColor Gray
        Write-Host " ID: $($tenant.TenantId)" -ForegroundColor Gray
        Write-Host " SharePoint: $($tenant.SharePointTenantName)" -ForegroundColor Gray
        Write-Host " App ID: $($tenant.AppId)" -ForegroundColor Gray
        Write-Host " Last Used: $($tenant.LastUsed)" -ForegroundColor Gray
        Write-Host ""
    }
}

function Clear-StoredTenants {
    <#
    .SYNOPSIS
    Clears all stored tenant information
    
    .DESCRIPTION
    Removes all stored tenant information from local storage after confirmation
    #>

    
    $storedTenants = @(Get-StoredTenants)
    
    if ($storedTenants.Count -eq 0) {
        Write-Host "No stored tenants to clear." -ForegroundColor Yellow
        return
    }
    
    Write-Host "`nClear All Stored Tenants" -ForegroundColor Cyan
    Write-Host "========================" -ForegroundColor Cyan
    Write-Host "This will remove $($storedTenants.Count) stored tenant(s):" -ForegroundColor Yellow
    
    foreach ($tenant in $storedTenants) {
        Write-Host " - $($tenant.PrimaryDomain) - $($tenant.TenantName)" -ForegroundColor White
    }
    
    $confirmation = Read-Host "`nAre you sure you want to clear all stored tenants? (y/N)"
    
    if ($confirmation -eq 'y' -or $confirmation -eq 'Y') {
        if (Test-Path $script:TenantStoragePath) {
            Remove-Item $script:TenantStoragePath -Force
        }
        $script:CachedTenants = @()
        Write-Host "All stored tenants cleared successfully." -ForegroundColor Green
    } else {
        Write-Host "Operation cancelled." -ForegroundColor Yellow
    }
}


function Get-CertificateForAuth {
    <#
    .SYNOPSIS
    Gets or creates a certificate for Azure authentication
    
    .DESCRIPTION
    Handles certificate creation, app registration, permission assignment, and returns certificate for connections.
    Now supports tenant selection from stored tenants for improved user experience.
    
    .NOTES
    Automatically connects to Microsoft Graph with required scopes if not already connected
    
    .PARAMETER AppName
    Name of the Azure AD app registration
    
    .PARAMETER CertSubject
    Certificate subject name
    
    .PARAMETER TenantId
    Specific tenant ID to use (bypasses tenant selection)
    
    .PARAMETER UseTenantSelection
    Whether to prompt for tenant selection from stored tenants (default: $true)
    
    .EXAMPLE
    $auth = Get-CertificateForAuth
    Connect-PnPOnline -Url $url -ClientId $auth.AppId -Tenant $auth.TenantId -CertificateBase64Encoded $auth.CertificateBase64
    
    .EXAMPLE
    $auth = Get-CertificateForAuth -TenantId "12345678-1234-1234-1234-123456789012"
    # Uses specific tenant ID, bypasses selection
    #>

    param(
        [string]$AppName = "M365 PowerShell App",
        [string]$CertSubject = "CN=M365 PowerShell",
        [string]$TenantId = $null,
        [bool]$UseTenantSelection = $true
    )
    
    try {
        # Ensure clean session - disconnect any existing Graph connection
        $existingContext = Get-MgContext -ErrorAction SilentlyContinue
        if ($existingContext) {
            Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
        }
        
        # Handle tenant selection if enabled and no specific tenant provided
        $selectedTenantInfo = $null
        $useStoredTenant = $false
        
        if ([string]::IsNullOrEmpty($TenantId) -and $UseTenantSelection) {
            $selectedTenant = Select-Tenant -AllowNew
            
            if ($null -eq $selectedTenant) {
                throw "No tenant selected. Operation cancelled."
            }
            
            if ($selectedTenant -eq "NEW") {
                Write-Host "You'll be prompted to log in twice - once for identity, once for admin consent" -ForegroundColor Yellow
                # Continue with normal flow to add new tenant
            } else {
                $selectedTenantInfo = $selectedTenant
                $TenantId = $selectedTenantInfo.TenantId
                $useStoredTenant = $true
                Write-Host "Using stored tenant: $($selectedTenantInfo.PrimaryDomain) - $($selectedTenantInfo.TenantName)" -ForegroundColor Green
            }
        }
        
        # Establish Graph connection with required scopes
        try {
            # If we have stored tenant info with certificate details, try certificate auth directly first
            if ($useStoredTenant -and $selectedTenantInfo.AppId -and $selectedTenantInfo.Thumbprint) {
                Write-Host "Connecting to Microsoft Graph using stored certificate for tenant $($selectedTenantInfo.TenantName)..." -ForegroundColor Yellow
                
                # Verify the certificate still exists
                $storedCert = Get-ChildItem -Path "Cert:\CurrentUser\My\$($selectedTenantInfo.Thumbprint)" -ErrorAction SilentlyContinue
                if ($storedCert) {
                    try {
                        Connect-MgGraph -ClientId $selectedTenantInfo.AppId -TenantId $selectedTenantInfo.TenantId -CertificateThumbprint $selectedTenantInfo.Thumbprint -NoWelcome -ErrorAction Stop | Out-Null
                        Write-Host "Connected to Microsoft Graph using stored certificate" -ForegroundColor Green
                        
                        # Skip the rest of the certificate setup since we're already connected
                        $cert = $storedCert
                        $app = @{ AppId = $selectedTenantInfo.AppId }
                        $actualTenantId = $selectedTenantInfo.TenantId
                        $primaryDomain = $selectedTenantInfo.PrimaryDomain
                        $initialDomain = $selectedTenantInfo.InitialDomain
                        $tenantName = $selectedTenantInfo.TenantName
                        $sharePointTenantName = $selectedTenantInfo.SharePointTenantName
                        
                        # Quick API test
                        $null = Get-MgUser -Top 1 -ErrorAction Stop
                        Write-Host "- Certificate authentication ready" -ForegroundColor Green
                        
                        # Update last used timestamp
                        Save-TenantInfo -TenantInfo @{
                            TenantId = $selectedTenantInfo.TenantId
                            TenantName = $selectedTenantInfo.TenantName
                            PrimaryDomain = $selectedTenantInfo.PrimaryDomain
                            InitialDomain = $selectedTenantInfo.InitialDomain
                            SharePointTenantName = $selectedTenantInfo.SharePointTenantName
                            AppId = $selectedTenantInfo.AppId
                            AppName = $selectedTenantInfo.AppName
                            CertSubject = $selectedTenantInfo.CertSubject
                            Thumbprint = $selectedTenantInfo.Thumbprint
                        }
                        
                        # Export certificate with private key as base64 for PnP PowerShell
                        $tempPassword = ConvertTo-SecureString -String "TempPassword123!" -Force -AsPlainText
                        $tempPfxPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "temp_cert_$($cert.Thumbprint).pfx")
                        
                        try {
                            Export-PfxCertificate -Cert $cert -FilePath $tempPfxPath -Password $tempPassword -Force | Out-Null
                            $pfxBytes = [System.IO.File]::ReadAllBytes($tempPfxPath)
                            $certBase64 = [Convert]::ToBase64String($pfxBytes)
                            Remove-Item $tempPfxPath -Force -ErrorAction SilentlyContinue
                        } catch {
                            Write-Host " ! Could not export certificate with private key, using public key only" -ForegroundColor Yellow
                            $certBase64 = [Convert]::ToBase64String($cert.RawData)
                            $tempPassword = $null
                        }
                        
                        # Return early with stored tenant info
                        return @{
                            Certificate = $cert
                            AppId = $selectedTenantInfo.AppId
                            TenantId = $selectedTenantInfo.TenantId
                            Thumbprint = $cert.Thumbprint
                            CertificateBase64 = $certBase64
                            CertificatePassword = $tempPassword
                            TenantName = $selectedTenantInfo.TenantName
                            SharePointTenantName = $selectedTenantInfo.SharePointTenantName
                            PrimaryDomain = $selectedTenantInfo.PrimaryDomain
                            InitialDomain = $selectedTenantInfo.InitialDomain
                        }
                        
                    } catch {
                        # Reset variables to force normal flow
                        $useStoredTenant = $false
                        $selectedTenantInfo = $null
                        $TenantId = $null
                    }
                } else {
                    # Reset variables to force normal flow
                    $useStoredTenant = $false
                    $selectedTenantInfo = $null
                    $TenantId = $null
                }
            }
            
            # Normal interactive authentication flow (only if stored cert auth failed or not available)
            $context = Get-MgContext -ErrorAction SilentlyContinue
            $needsConnection = $false
            $needsSpecificTenant = $false
            
            if (-not $context) {
                $needsConnection = $true
            } else {
                # Check if we're connected to the right tenant
                if ($TenantId -and $context.TenantId -ne $TenantId) {
                    $needsConnection = $true
                    $needsSpecificTenant = $true
                }
                
                # Check for required scopes
                $requiredScopes = @("Application.ReadWrite.All", "AppRoleAssignment.ReadWrite.All", "Directory.Read.All", "RoleManagement.ReadWrite.Directory")
                $missingScopes = @()
                
                foreach ($scope in $requiredScopes) {
                    if ($context.Scopes -notcontains $scope) {
                        $missingScopes += $scope
                    }
                }
                
                if ($missingScopes.Count -gt 0) {
                    $needsConnection = $true
                }
            }
            
            if ($needsConnection) {
                if ($needsSpecificTenant -or $TenantId) {
                    Write-Host "Connecting to Microsoft Graph for tenant $TenantId..." -ForegroundColor Yellow
                    Connect-MgGraph -TenantId $TenantId -Scopes @("Application.ReadWrite.All", "AppRoleAssignment.ReadWrite.All", "Directory.Read.All", "RoleManagement.ReadWrite.Directory") -NoWelcome -ErrorAction Stop | Out-Null
                } else {
                    Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Yellow
                    Connect-MgGraph -Scopes @("Application.ReadWrite.All", "AppRoleAssignment.ReadWrite.All", "Directory.Read.All", "RoleManagement.ReadWrite.Directory") -NoWelcome -ErrorAction Stop | Out-Null
                }
                Write-Host "Connected to Microsoft Graph" -ForegroundColor Green
            }
            
        } catch {
            throw "Failed to connect to Microsoft Graph: $($_.Exception.Message)"
        }
        
        # Validate and select best certificate
        
        $allMatchingCerts = Get-ChildItem -Path "Cert:\CurrentUser\My" | Where-Object { $_.Subject -eq $CertSubject }
        $validCerts = @()
        
        foreach ($cert in $allMatchingCerts) {
            $certStatus = @{
                Certificate = $cert
                IsValid = $true
                Issues = @()
                DaysUntilExpiry = ($cert.NotAfter - (Get-Date)).Days
                HasPrivateKey = $false
                IsRegisteredInAzure = $false
            }
            
            # Check expiry
            if ($certStatus.DaysUntilExpiry -le 0) {
                $certStatus.Issues += "Certificate expired on $($cert.NotAfter.ToString('yyyy-MM-dd HH:mm'))"
                $certStatus.IsValid = $false
            } elseif ($certStatus.DaysUntilExpiry -le 30) {
                $certStatus.Issues += "Certificate expires soon ($($certStatus.DaysUntilExpiry) days)"
            }
            
            # Validate private key
            try {
                if ($cert.HasPrivateKey) {
                    $rsaCert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
                    if ($rsaCert) {
                        $certStatus.HasPrivateKey = $true
                    } else {
                        $certStatus.Issues += "Private key not accessible"
                        $certStatus.IsValid = $false
                    }
                } else {
                    $certStatus.Issues += "No private key available"
                    $certStatus.IsValid = $false
                }
            } catch {
                $certStatus.Issues += "Private key validation failed: $($_.Exception.Message)"
                $certStatus.IsValid = $false
            }
            
            $validCerts += $certStatus
        }
        
        # Select best certificate
        $bestCert = $validCerts | Where-Object { 
            $_.IsValid -and $_.HasPrivateKey -and $_.DaysUntilExpiry -gt 0 
        } | Sort-Object @{Expression = {$_.IsRegisteredInAzure}; Descending = $true}, @{Expression = {$_.DaysUntilExpiry}; Descending = $true} | Select-Object -First 1
        
        if (-not $bestCert) {
            # Clean up expired certificates silently
            $expiredCerts = $validCerts | Where-Object { -not $_.IsValid }
            foreach ($expiredCert in $expiredCerts) {
                try {
                    Remove-Item -Path "Cert:\CurrentUser\My\$($expiredCert.Certificate.Thumbprint)" -Force -ErrorAction SilentlyContinue
                } catch {}
            }
            
            # Create new certificate
            $cert = New-SelfSignedCertificate -Subject $CertSubject -CertStoreLocation "Cert:\CurrentUser\My" -KeyExportPolicy Exportable -KeySpec Signature -KeyLength 2048 -KeyAlgorithm RSA -HashAlgorithm SHA256 -NotAfter (Get-Date).AddYears(2)
            Write-Host "- Certificate created: $($cert.Thumbprint)" -ForegroundColor Green
        } else {
            $cert = $bestCert.Certificate
            if ($bestCert.Issues.Count -gt 0) {
                Write-Host "! Certificate issues: $($bestCert.Issues -join '; ')" -ForegroundColor Yellow
            }
            Write-Host "- Certificate ready: $($cert.Thumbprint) (expires in $($bestCert.DaysUntilExpiry) days)" -ForegroundColor Green
        }
        
        # Get or create app registration
        try {
            $app = Get-MgApplication -Filter "displayName eq '$AppName'" -ErrorAction SilentlyContinue | Select-Object -First 1
        } catch {
            Write-Host "Failed to query applications: $($_.Exception.Message)" -ForegroundColor Red
            throw "Cannot proceed without ability to query/create app registrations"
        }
        
        if (-not $app) {
            Write-Host "Creating app registration and permissions..." -ForegroundColor Yellow
            $app = New-MgApplication -DisplayName $AppName
            $servicePrincipal = New-MgServicePrincipal -AppId $app.AppId
            
            # Wait for service principal propagation
            Start-Sleep -Seconds 10
            
            # Assign Microsoft Graph permissions
            try {
                $graphServicePrincipal = Get-MgServicePrincipal -Filter "displayName eq 'Microsoft Graph'" -ErrorAction Stop
                $graphAppRoles = @(
                    "62a82d76-70ea-41e2-9197-370581804d09", # Group.ReadWrite.All
                    "741f803b-c850-494e-b5df-cde7c675a1ca", # User.ReadWrite.All
                    "bf7b1a76-6e77-406b-b258-bf5c7720e98f", # Group.Create
                    "810c84a8-4a9e-49e6-bf7d-12d183f40d01", # Mail.Read
                    "75359482-378d-4052-8f01-80520e7db3cd", # Files.ReadWrite.All
                    "9492366f-7969-46a4-8d15-ed1a20078fff", # Sites.ReadWrite.All
                    "0c0bf378-bf22-4481-8f81-9e89a9b4960a", # Sites.FullControl.All
                    "b633e1c5-b582-4048-a93e-9f11b44c7e96", # Mail.Send
                    "e2a3a72e-5f79-4c64-b1b1-878b674786c9", # Mail.ReadWrite
                    "40f97065-369a-49f4-947c-6a255697ae91", # MailboxSettings.ReadWrite
                    "ef54d2bf-783f-4e0f-bca1-3210c0444d99", # Calendars.ReadWrite
                    "19dbc75e-c2e2-444c-a770-ec69d8559fc7", # Directory.ReadWrite.All
                    "230c1aed-a721-4c5d-9cb4-a90514e508ef", # Reports.Read.All
                    "06b708a9-e830-4db3-a914-8e69da51d44f", # AppRoleAssignment.ReadWrite.All
                    "9e3f62cf-ca93-4989-b6ce-bf83c28f9fe8"  # RoleManagement.ReadWrite.Directory
                )
                
                foreach ($roleId in $graphAppRoles) {
                    try {
                        New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $graphServicePrincipal.Id -PrincipalId $servicePrincipal.Id -ResourceId $graphServicePrincipal.Id -AppRoleId $roleId -ErrorAction SilentlyContinue | Out-Null
                    } catch {
                        # Ignore already exists errors
                    }
                }
                Write-Host "- Microsoft Graph permissions assigned" -ForegroundColor Green
            } catch {
                Write-Host "! Failed to assign Graph permissions: $($_.Exception.Message)" -ForegroundColor Yellow
            }
            
            # Assign Exchange Online permissions
            try {
                $exchangeServicePrincipal = Get-MgServicePrincipal -Filter "displayName eq 'Office 365 Exchange Online'" -ErrorAction Stop
                if ($exchangeServicePrincipal) {
                    $exchangeAppRole = "dc50a0fb-09a3-484d-be87-e023b12c6440" # Exchange.ManageAsApp
                    try {
                        New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $exchangeServicePrincipal.Id -PrincipalId $servicePrincipal.Id -ResourceId $exchangeServicePrincipal.Id -AppRoleId $exchangeAppRole -ErrorAction SilentlyContinue | Out-Null
                        Write-Host "- Exchange permissions assigned" -ForegroundColor Green
                    } catch {
                        # Ignore already exists errors
                    }
                }
            } catch {
                Write-Host " ! Failed to assign Exchange permissions: $($_.Exception.Message)" -ForegroundColor Yellow
            }
            
            # Assign SharePoint permissions
            try {
                $sharePointServicePrincipal = Get-MgServicePrincipal -Filter "displayName eq 'Office 365 SharePoint Online'" -ErrorAction Stop
                if ($sharePointServicePrincipal) {
                    $sharePointAppRoles = @(
                        "678536fe-1083-478a-9c59-b99265e6b0d3", # Sites.FullControl.All
                        "741f803b-c850-494e-b5df-cde7c675a1ca"  # User.ReadWrite.All
                    )
                    
                    foreach ($roleId in $sharePointAppRoles) {
                        try {
                            New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sharePointServicePrincipal.Id -PrincipalId $servicePrincipal.Id -ResourceId $sharePointServicePrincipal.Id -AppRoleId $roleId -ErrorAction SilentlyContinue | Out-Null
                        } catch {
                            # Ignore already exists errors
                        }
                    }
                    Write-Host "- SharePoint permissions assigned" -ForegroundColor Green
                }
            } catch {
                Write-Host " ! Failed to assign SharePoint permissions: $($_.Exception.Message)" -ForegroundColor Yellow
            }
            
            # Assign Exchange Administrator role (required for Exchange Online App-Only Authentication)
            try {
                $exchangeAdminRoleId = "29232cdf-9323-42fd-ade2-1d097af3e4de"  # Exchange Administrator
                $roleAssignmentBody = @{
                    principalId = $servicePrincipal.Id
                    roleDefinitionId = $exchangeAdminRoleId
                    directoryScopeId = "/"
                }
                Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments" -Body ($roleAssignmentBody | ConvertTo-Json) -ContentType "application/json" | Out-Null
                Write-Host "- Exchange Administrator role assigned" -ForegroundColor Green
            } catch {
                if ($_.Exception.Message -like "*already exists*" -or $_.Exception.Message -like "*already assigned*") {
                    Write-Host "- Exchange Administrator role already assigned" -ForegroundColor Green
                }
            }
            
            # Try to assign Global Administrator role (optional but helpful)
            try {
                $globalAdminRoleId = "62e90394-69f5-4237-9190-012177145e10"  # Global Administrator
                $roleAssignmentBody = @{
                    principalId = $servicePrincipal.Id
                    roleDefinitionId = $globalAdminRoleId
                    directoryScopeId = "/"
                }
                Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments" -Body ($roleAssignmentBody | ConvertTo-Json) -ContentType "application/json" | Out-Null
                Write-Host "- Global Administrator role assigned" -ForegroundColor Green
            } catch {
                # Optional role assignment, ignore failures
            }
            
            # Wait for role propagation
            Write-Host "Waiting for role propagation..." -ForegroundColor Yellow
            Start-Sleep -Seconds 30
                
            Write-Host "- App created: $($app.AppId)" -ForegroundColor Green
        } else {
            Write-Host "- Using existing app: $($app.AppId)" -ForegroundColor Green
            
            # Check and add missing permissions to existing app
            
            try {
                $servicePrincipal = Get-MgServicePrincipal -Filter "appId eq '$($app.AppId)'" -ErrorAction Stop
                
                # Get service principals for permission checks
                $graphServicePrincipal = Get-MgServicePrincipal -Filter "displayName eq 'Microsoft Graph'" -ErrorAction Stop
                $exchangeServicePrincipal = Get-MgServicePrincipal -Filter "displayName eq 'Office 365 Exchange Online'" -ErrorAction SilentlyContinue
                $sharePointServicePrincipal = Get-MgServicePrincipal -Filter "displayName eq 'Office 365 SharePoint Online'" -ErrorAction SilentlyContinue
                
                # Check existing directory role assignments
                $existingRoleAssignments = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments?`$filter=principalId eq '$($servicePrincipal.Id)'" -ErrorAction SilentlyContinue
                $exchangeAdminRoleId = "29232cdf-9323-42fd-ade2-1d097af3e4de"  # Exchange Administrator
                
                $hasExchangeAdminRole = $existingRoleAssignments.value | Where-Object { $_.roleDefinitionId -eq $exchangeAdminRoleId }
                
                # Assign Exchange Administrator role if missing
                if (-not $hasExchangeAdminRole) {
                    try {
                        $roleAssignmentBody = @{
                            principalId = $servicePrincipal.Id
                            roleDefinitionId = $exchangeAdminRoleId
                            directoryScopeId = "/"
                        }
                        Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments" -Body ($roleAssignmentBody | ConvertTo-Json) -ContentType "application/json" -ErrorAction Stop | Out-Null
                        Write-Host " - Added Exchange Administrator role" -ForegroundColor Green
                    } catch {
                        if ($_.Exception.Message -like "*already exists*" -or $_.Exception.Message -like "*already assigned*") {
                            Write-Host " - Exchange Administrator role already assigned" -ForegroundColor Green
                        }
                    }
                } else {
                    Write-Host " - Exchange Administrator role already assigned" -ForegroundColor Green
                }
                
                # Try to assign Global Administrator role if missing (optional but helpful)
                $globalAdminRoleId = "62e90394-69f5-4237-9190-012177145e10"  # Global Administrator
                $hasGlobalAdminRole = $existingRoleAssignments.value | Where-Object { $_.roleDefinitionId -eq $globalAdminRoleId }
                
                if (-not $hasGlobalAdminRole) {
                    try {
                        $roleAssignmentBody = @{
                            principalId = $servicePrincipal.Id
                            roleDefinitionId = $globalAdminRoleId
                            directoryScopeId = "/"
                        }
                        Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments" -Body ($roleAssignmentBody | ConvertTo-Json) -ContentType "application/json" | Out-Null
                        Write-Host " - Added Global Administrator role" -ForegroundColor Green
                    } catch {
                        # Ignore failures
                    }
                } else {
                    Write-Host " - Global Administrator role already assigned" -ForegroundColor Green
                }

                # Check Microsoft Graph permissions
                $existingGraphAssignments = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $servicePrincipal.Id | Where-Object { $_.ResourceId -eq $graphServicePrincipal.Id }
                
                $requiredGraphRoles = @(
                    "62a82d76-70ea-41e2-9197-370581804d09", # Group.ReadWrite.All
                    "741f803b-c850-494e-b5df-cde7c675a1ca", # User.ReadWrite.All
                    "bf7b1a76-6e77-406b-b258-bf5c7720e98f", # Group.Create
                    "810c84a8-4a9e-49e6-bf7d-12d183f40d01", # Mail.Read
                    "75359482-378d-4052-8f01-80520e7db3cd", # Files.ReadWrite.All
                    "9492366f-7969-46a4-8d15-ed1a20078fff", # Sites.ReadWrite.All
                    "0c0bf378-bf22-4481-8f81-9e89a9b4960a", # Sites.FullControl.All (correct ID)
                    "b633e1c5-b582-4048-a93e-9f11b44c7e96", # Mail.Send
                    "e2a3a72e-5f79-4c64-b1b1-878b674786c9", # Mail.ReadWrite
                    "40f97065-369a-49f4-947c-6a255697ae91", # MailboxSettings.ReadWrite
                    "ef54d2bf-783f-4e0f-bca1-3210c0444d99"  # Calendars.ReadWrite
                )
                
                foreach ($roleId in $requiredGraphRoles) {
                    $hasRole = $existingGraphAssignments | Where-Object { $_.AppRoleId -eq $roleId }
                    if (-not $hasRole) {
                        try {
                            New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $graphServicePrincipal.Id -PrincipalId $servicePrincipal.Id -ResourceId $graphServicePrincipal.Id -AppRoleId $roleId -ErrorAction Stop | Out-Null
                        } catch {
                            # Ignore failures
                        }
                    }
                }
                
                # Check Exchange permissions
                $exchangeServicePrincipal = Get-MgServicePrincipal -Filter "displayName eq 'Office 365 Exchange Online'" -ErrorAction SilentlyContinue
                if ($exchangeServicePrincipal) {
                    $existingExchangeAssignments = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $servicePrincipal.Id | Where-Object { $_.ResourceId -eq $exchangeServicePrincipal.Id }
                    
                    $exchangeAppRole = "dc50a0fb-09a3-484d-be87-e023b12c6440" # Exchange.ManageAsApp
                    $hasExchangeRole = $existingExchangeAssignments | Where-Object { $_.AppRoleId -eq $exchangeAppRole }
                    if (-not $hasExchangeRole) {
                        try {
                            New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $exchangeServicePrincipal.Id -PrincipalId $servicePrincipal.Id -ResourceId $exchangeServicePrincipal.Id -AppRoleId $exchangeAppRole -ErrorAction Stop | Out-Null
                        } catch {
                            # Ignore failures
                        }
                    }
                }
                
                # Check SharePoint permissions
                if ($sharePointServicePrincipal) {
                    $existingSharePointAssignments = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $servicePrincipal.Id | Where-Object { $_.ResourceId -eq $sharePointServicePrincipal.Id }
                    
                    $requiredSharePointRoles = @(
                        "678536fe-1083-478a-9c59-b99265e6b0d3", # Sites.FullControl.All
                        "741f803b-c850-494e-b5df-cde7c675a1ca"  # User.ReadWrite.All
                    )
                    
                    foreach ($roleId in $requiredSharePointRoles) {
                        $hasRole = $existingSharePointAssignments | Where-Object { $_.AppRoleId -eq $roleId }
                        if (-not $hasRole) {
                            try {
                                New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sharePointServicePrincipal.Id -PrincipalId $servicePrincipal.Id -ResourceId $sharePointServicePrincipal.Id -AppRoleId $roleId -ErrorAction Stop | Out-Null
                            } catch {
                                # Ignore failures
                            }
                        }
                    }
                }
                
                Write-Host "- Permissions configured" -ForegroundColor Green
                
            } catch {
                Write-Host " ! Could not check/update permissions: $($_.Exception.Message)" -ForegroundColor Yellow
            }
        }
        
        # Upload certificate to app with duplicate detection
        $fullApp = Get-MgApplication -ApplicationId $app.Id -Property "Id,KeyCredentials" -ErrorAction Stop
        
        $displayName = "PowerShell Certificate - $($cert.Thumbprint)"
        $existingKeys = @($fullApp.KeyCredentials)
        
        # Duplicate detection using multiple methods
        $certAlreadyRegistered = $false
        $certDerBase64 = [Convert]::ToBase64String($cert.RawData)
        
        # Compute thumbprint for comparison
        $hexThumb = ($cert.Thumbprint -replace ' ','')
        $thumbBytes = @()
        for ($i = 0; $i -lt $hexThumb.Length; $i += 2) { 
            $thumbBytes += [Convert]::ToByte($hexThumb.Substring($i,2),16) 
        }
        $thumbBase64 = [Convert]::ToBase64String($thumbBytes)
        
        foreach ($keyCred in $existingKeys) {
            $customIdBase64 = $null
            if ($keyCred.CustomKeyIdentifier) { 
                $customIdBase64 = [Convert]::ToBase64String($keyCred.CustomKeyIdentifier) 
            }
            
            $typeMatches = ($keyCred.Type -eq 'AsymmetricX509Cert') -or -not $keyCred.Type
            $keyMatches = $false
            if ($keyCred.Key) {
                try { 
                    $keyBase64 = [Convert]::ToBase64String($keyCred.Key) 
                    if ($keyBase64 -and ($keyBase64 -eq $certDerBase64)) { 
                        $keyMatches = $true 
                    }
                } catch {}
            }
            $displayMatches = ($keyCred.DisplayName -and ($keyCred.DisplayName -eq $displayName))
            
            if ($typeMatches -and (
                ($customIdBase64 -and ($customIdBase64 -eq $thumbBase64)) -or 
                $keyMatches -or 
                $displayMatches
            )) {
                if ($keyCred.EndDateTime -and $keyCred.EndDateTime -lt (Get-Date)) {
                    Write-Host "! Certificate registration expired - will re-upload" -ForegroundColor Yellow
                } else {
                    $certAlreadyRegistered = $true
                    break
                }
            }
        }
        
        if (-not $certAlreadyRegistered) {
            Write-Host "Uploading certificate to app..." -ForegroundColor Yellow
            
            $newKey = @{
                type = "AsymmetricX509Cert"
                usage = "Verify"
                key = $cert.RawData
                displayName = $displayName
            }
            
            $allKeys = @($existingKeys) + @($newKey)
            Update-MgApplication -ApplicationId $fullApp.Id -BodyParameter @{ keyCredentials = $allKeys }
            Write-Host "- Certificate uploaded, waiting for propagation..." -ForegroundColor Green
            Start-Sleep -Seconds 45
        } else {
            Write-Host "- Certificate already registered" -ForegroundColor Green
        }
        
        # Step 4: Test certificate auth and get tenant info
        $context = Get-MgContext
        $actualTenantId = $context.TenantId
        
        # Get tenant domain information (only if not using stored tenant or tenant info is incomplete)
        if (-not $useStoredTenant -or -not $selectedTenantInfo.PrimaryDomain) {
            $domains = Get-MgDomain -All -ErrorAction SilentlyContinue
            $initialDomain = ($domains | Where-Object { $_.IsInitial }).Id
            $primaryDomain = ($domains | Where-Object { $_.IsDefault -eq $true }).Id
            if (-not $primaryDomain) {
                $primaryDomain = ($domains | Where-Object { $_.Id -notlike "*.onmicrosoft.com" }).Id | Select-Object -First 1
            }
            if (-not $primaryDomain) {
                $primaryDomain = $initialDomain
            }
            
            # Find SharePoint tenant name - look for common patterns
            $sharePointTenantName = $initialDomain.Split(".")[0]
            $allOnMicrosoftDomains = $domains | Where-Object { $_.Id -like "*.onmicrosoft.com" }
            
            # Look for common SharePoint tenant patterns
            $sharePointCandidates = $allOnMicrosoftDomains | Where-Object { 
                $_.Id -like "*groupltd*" -or $_.Id -like "*group*" -or $_.Id -like "*ltd*" 
            }
            if ($sharePointCandidates) {
                $sharePointTenantName = $sharePointCandidates[0].Id.Split(".")[0]
            }
            
            $tenantName = $initialDomain.Split(".")[0]
        } else {
            # Use stored tenant information
            $primaryDomain = $selectedTenantInfo.PrimaryDomain
            $initialDomain = $selectedTenantInfo.InitialDomain
            $tenantName = $selectedTenantInfo.TenantName
            $sharePointTenantName = $selectedTenantInfo.SharePointTenantName
        }
        
        # Switch to certificate authentication if needed
        $currentContext = Get-MgContext
        $needsCertificateReconnection = $false
        
        if (-not $currentContext) {
            $needsCertificateReconnection = $true
        } elseif ($currentContext.TenantId -ne $actualTenantId) {
            $needsCertificateReconnection = $true
        } elseif ($currentContext.AuthType -eq "Delegated" -and $currentContext.TenantId -eq $actualTenantId) {
            $needsCertificateReconnection = $false
        } elseif ($currentContext.AuthType -eq "AppOnly" -and $currentContext.ClientId -ne $app.AppId) {
            $needsCertificateReconnection = $true
        } elseif ($currentContext.AuthType -eq "AppOnly" -and $currentContext.TenantId -ne $actualTenantId) {
            $needsCertificateReconnection = $true
        }
        
        if ($needsCertificateReconnection) {
            Connect-MgGraph -ClientId $app.AppId -TenantId $actualTenantId -CertificateThumbprint $cert.Thumbprint -NoWelcome -ContextScope Process -ErrorAction Stop | Out-Null 2>&1
        }
        
        # Quick API test
        $null = Get-MgUser -Top 1 -ErrorAction Stop
        
        # Save tenant information for future use (if not using stored tenant or if tenant info was updated)
        if (-not $useStoredTenant -or -not $selectedTenantInfo.PrimaryDomain) {
            $tenantInfoToSave = @{
                TenantId = $actualTenantId
                TenantName = $tenantName
                PrimaryDomain = $primaryDomain
                InitialDomain = $initialDomain
                SharePointTenantName = $sharePointTenantName
                AppId = $app.AppId
                AppName = $AppName
                CertSubject = $CertSubject
                Thumbprint = $cert.Thumbprint
            }
            
            Save-TenantInfo -TenantInfo $tenantInfoToSave
        } elseif ($useStoredTenant) {
            # Update last used timestamp for stored tenant
            $tenantInfoToSave = @{
                TenantId = $selectedTenantInfo.TenantId
                TenantName = $selectedTenantInfo.TenantName
                PrimaryDomain = $selectedTenantInfo.PrimaryDomain
                InitialDomain = $selectedTenantInfo.InitialDomain
                SharePointTenantName = $selectedTenantInfo.SharePointTenantName
                AppId = $selectedTenantInfo.AppId
                AppName = $selectedTenantInfo.AppName
                CertSubject = $selectedTenantInfo.CertSubject
                Thumbprint = $selectedTenantInfo.Thumbprint
            }
            
            Save-TenantInfo -TenantInfo $tenantInfoToSave
        }
        
        # Return certificate and connection info
        # Export certificate with private key as base64 for PnP PowerShell
        $tempPassword = ConvertTo-SecureString -String "TempPassword123!" -Force -AsPlainText
        $tempPfxPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "temp_cert_$($cert.Thumbprint).pfx")
        
        try {
            Export-PfxCertificate -Cert $cert -FilePath $tempPfxPath -Password $tempPassword -Force | Out-Null
            $pfxBytes = [System.IO.File]::ReadAllBytes($tempPfxPath)
            $certBase64 = [Convert]::ToBase64String($pfxBytes)
            Remove-Item $tempPfxPath -Force -ErrorAction SilentlyContinue
        } catch {
            $certBase64 = [Convert]::ToBase64String($cert.RawData)
            $tempPassword = $null
        }
        
        return @{
            Certificate = $cert
            AppId = $app.AppId
            TenantId = $actualTenantId
            Thumbprint = $cert.Thumbprint
            CertificateBase64 = $certBase64
            CertificatePassword = $tempPassword
            TenantName = $tenantName
            SharePointTenantName = $sharePointTenantName
            PrimaryDomain = $primaryDomain
            InitialDomain = $initialDomain
        }
        
    } catch {
        throw "Certificate setup failed: $($_.Exception.Message)"
    }
}


function Show-CertificateAuthHelp {
    <#
    .SYNOPSIS
    Shows help and examples for using certificate authentication with Microsoft 365 services
    
    .DESCRIPTION
    Displays connection examples for all major Microsoft 365 PowerShell modules using certificate authentication
    #>

    
    Write-Host "`nCertificate Authentication Help" -ForegroundColor Cyan
    Write-Host "===============================" -ForegroundColor Cyan
    Write-Host "Microsoft 365 Certificate Authentication Module`n" -ForegroundColor Gray
    
    Write-Host "BASIC USAGE:" -ForegroundColor Yellow
    Write-Host "`$auth = Get-CertificateForAuth" -ForegroundColor White
    Write-Host "Returns object with all connection details:" -ForegroundColor Gray
    Write-Host " - AppId, TenantId, Thumbprint (for certificate-based connections)" -ForegroundColor Gray
    Write-Host " - CertificateBase64, CertificatePassword (for PnP PowerShell)" -ForegroundColor Gray
    Write-Host " - TenantName, PrimaryDomain, InitialDomain (tenant information)" -ForegroundColor Gray
    Write-Host " - SharePointTenantName (for SharePoint URLs)" -ForegroundColor Gray
    Write-Host "Note: First run requires interactive login; subsequent runs show tenant selection menu" -ForegroundColor Gray
    Write-Host " Module automatically connects to Microsoft Graph with required permissions`n" -ForegroundColor Gray
    
    Write-Host "CONNECTION EXAMPLES:" -ForegroundColor Yellow
    Write-Host "The module provides all necessary values - use them directly:" -ForegroundColor Gray
    Write-Host "To see all available values: `$auth | Format-List" -ForegroundColor Gray
    
    Write-Host "`nExchange Online:" -ForegroundColor Cyan
    Write-Host "Connect-ExchangeOnline -AppId `$auth.AppId -CertificateThumbprint `$auth.Thumbprint -Organization `$auth.InitialDomain" -ForegroundColor White
    
    Write-Host "`nMicrosoft Graph:" -ForegroundColor Cyan
    Write-Host "Connect-MgGraph -ClientId `$auth.AppId -TenantId `$auth.TenantId -CertificateThumbprint `$auth.Thumbprint" -ForegroundColor White
    
    Write-Host "`nSharePoint Online (PnP):" -ForegroundColor Cyan
    Write-Host "Connect-PnPOnline -Url `"https://`$(`$auth.SharePointTenantName).sharepoint.com`" -ClientId `$auth.AppId -Tenant `$auth.InitialDomain -CertificateBase64Encoded `$auth.CertificateBase64 -CertificatePassword `$auth.CertificatePassword" -ForegroundColor White
    
    Write-Host "`nMicrosoft Teams:" -ForegroundColor Cyan
    Write-Host "Connect-MicrosoftTeams -ApplicationId `$auth.AppId -TenantId `$auth.TenantId -CertificateThumbprint `$auth.Thumbprint" -ForegroundColor White
    
    Write-Host "`nAzure PowerShell:" -ForegroundColor Cyan
    Write-Host "Connect-AzAccount -ApplicationId `$auth.AppId -TenantId `$auth.TenantId -CertificateThumbprint `$auth.Thumbprint -ServicePrincipal" -ForegroundColor White
    
    Write-Host "`nSecurity & Compliance:" -ForegroundColor Cyan
    Write-Host "Connect-IPPSSession -AppId `$auth.AppId -CertificateThumbprint `$auth.Thumbprint -Organization `$auth.InitialDomain" -ForegroundColor White
    
    Write-Host "`nPERMISSIONS INCLUDED:" -ForegroundColor Yellow
    Write-Host "- Microsoft Graph: User, Group, Mail, Files, Sites, Directory, Reports" -ForegroundColor White
    Write-Host "- Exchange Online: ManageAsApp (full mailbox access)" -ForegroundColor White  
    Write-Host "- SharePoint: FullControl and User permissions" -ForegroundColor White
    Write-Host "- Exchange Administrator role (required for Exchange Online App-Only auth)" -ForegroundColor White
    Write-Host "- Global Administrator role (optional, enhances compatibility)" -ForegroundColor White
    
    Write-Host "`nTENANT MANAGEMENT:" -ForegroundColor Yellow
    Write-Host "- Automatically stores tenant information after first login" -ForegroundColor White
    Write-Host "- Prompts for tenant selection on subsequent runs" -ForegroundColor White
    Write-Host "- Encrypted local storage for security (stores up to 20 tenants)" -ForegroundColor White
    Write-Host "- Automatic certificate validation and expiry checking" -ForegroundColor White
    Write-Host "- Automatic cleanup of invalid/expired tenant entries" -ForegroundColor White
    Write-Host "`nTenant Management Commands:" -ForegroundColor Yellow
    Write-Host "Show-StoredTenants # Display formatted list of stored tenants with details" -ForegroundColor White
    Write-Host "Get-StoredTenants # Get stored tenants as objects for scripting" -ForegroundColor White
    Write-Host "Remove-StoredTenant # Interactive removal of specific stored tenant" -ForegroundColor White
    Write-Host "Clear-StoredTenants # Clear all stored tenants (with confirmation)" -ForegroundColor White
    
    Write-Host "`nTenant Management Workflow:" -ForegroundColor Yellow
    Write-Host "1. First run: `$auth = Get-CertificateForAuth # Two logins required for new tenants" -ForegroundColor White
    Write-Host "2. Next runs: `$auth = Get-CertificateForAuth # Shows menu, select saved tenant" -ForegroundColor White
    Write-Host "3. Manage: Show-StoredTenants # View all saved tenants" -ForegroundColor White
    Write-Host "4. Cleanup: Remove-StoredTenant # Remove specific tenants" -ForegroundColor White
    
    Write-Host "`nAdvanced Usage:" -ForegroundColor Yellow
    Write-Host "`$auth = Get-CertificateForAuth -TenantId 'specific-tenant-id' # Bypass selection" -ForegroundColor White
    Write-Host "`$auth = Get-CertificateForAuth -UseTenantSelection `$false # Disable tenant selection" -ForegroundColor White
    
    Write-Host "`nREQUIREMENTS:" -ForegroundColor Yellow
    Write-Host "- Microsoft.Graph.Authentication module (for Graph connections)" -ForegroundColor White
    Write-Host "- Microsoft.Graph.Applications module (for app registration management)" -ForegroundColor White
    Write-Host "- Microsoft.Graph.Identity.DirectoryManagement module (for permissions)" -ForegroundColor White
    Write-Host "- PowerShell 5.1 or higher" -ForegroundColor White
    Write-Host "- Windows certificate store access" -ForegroundColor White
    
    Write-Host "`nCERTIFICATE MANAGEMENT:" -ForegroundColor Yellow
    Write-Host "- Automatically creates certificates if none exist" -ForegroundColor White
    Write-Host "- Validates expiry dates and private key access" -ForegroundColor White
    Write-Host "- Cleans up expired certificates automatically" -ForegroundColor White
    Write-Host "- Uploads certificates to app registration" -ForegroundColor White
    Write-Host "- Handles duplicate detection and re-registration" -ForegroundColor White
}

Export-ModuleMember -Function Get-CertificateForAuth, Show-CertificateAuthHelp, Get-StoredTenants, Show-StoredTenants, Remove-StoredTenant, Clear-StoredTenants