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

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

    
    if ($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))
        $script:CachedTenants = ConvertFrom-Json $decryptedData
        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 {
        # 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
        $tenants = @(Get-StoredTenants)
        
        # Remove existing entry for this tenant if it exists
        $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')
        $tenants += $TenantInfo
        
        # Keep only the 20 most recently used tenants
        $tenants = $tenants | Sort-Object LastUsed -Descending | Select-Object -First 20
        
        # Encrypt and save
        $jsonData = ConvertTo-Json $tenants -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 = $tenants
        
    } catch {
        Write-Warning "Failed to save tenant information: $($_.Exception.Message)"
    }
}

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:" -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 {
        # 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 to add a new tenant..." -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 and validate it
                $storedCert = Get-ChildItem -Path "Cert:\CurrentUser\My\$($selectedTenantInfo.Thumbprint)" -ErrorAction SilentlyContinue
                if ($storedCert) {
                    # Validate certificate
                    $certIsValid = $true
                    $certIssues = @()
                    $daysUntilExpiry = ($storedCert.NotAfter - (Get-Date)).Days
                    
                    # Check expiry
                    if ($daysUntilExpiry -le 0) {
                        $certIssues += "Certificate expired on $($storedCert.NotAfter.ToString('yyyy-MM-dd HH:mm'))"
                        $certIsValid = $false
                    } elseif ($daysUntilExpiry -le 30) {
                        $certIssues += "Certificate expires soon ($daysUntilExpiry days)"
                        Write-Host " ! Warning: Certificate expires in $daysUntilExpiry days" -ForegroundColor Yellow
                    }
                    
                    # Validate private key
                    try {
                        if ($storedCert.HasPrivateKey) {
                            $rsaCert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($storedCert)
                            if (-not $rsaCert) {
                                $certIssues += "Private key not accessible"
                                $certIsValid = $false
                            }
                        } else {
                            $certIssues += "No private key available"
                            $certIsValid = $false
                        }
                    } catch {
                        $certIssues += "Private key validation failed: $($_.Exception.Message)"
                        $certIsValid = $false
                    }
                    
                    if (-not $certIsValid) {
                        Write-Host " ! Stored certificate invalid: $($certIssues -join '; ')" -ForegroundColor Red
                        Write-Host " ! Removing invalid tenant from storage and falling back to interactive login" -ForegroundColor Yellow
                        
                        # Remove invalid tenant from storage
                        $remainingTenants = @(Get-StoredTenants) | Where-Object { $_.TenantId -ne $selectedTenantInfo.TenantId }
                        if ($remainingTenants.Count -eq 0) {
                            if (Test-Path $script:TenantStoragePath) {
                                Remove-Item $script:TenantStoragePath -Force
                            }
                        } else {
                            $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
                        }
                        $script:CachedTenants = $remainingTenants
                        
                        # Reset variables to force normal flow
                        $useStoredTenant = $false
                        $selectedTenantInfo = $null
                        $TenantId = $null
                    } else {
                        try {
                            Write-Host " - Using certificate thumbprint: $($selectedTenantInfo.Thumbprint)" -ForegroundColor Gray
                            Write-Host " - Certificate expires: $($storedCert.NotAfter.ToString('yyyy-MM-dd HH:mm')) ($daysUntilExpiry days)" -ForegroundColor Gray
                            Write-Host " - Using app ID: $($selectedTenantInfo.AppId)" -ForegroundColor Gray
                            Write-Host " - Using tenant ID: $($selectedTenantInfo.TenantId)" -ForegroundColor Gray
                            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 {
                            Write-Host " ! Stored certificate authentication failed, falling back to interactive login" -ForegroundColor Yellow
                            Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Red
                            # Reset variables to force normal flow
                            $useStoredTenant = $false
                            $selectedTenantInfo = $null
                            $TenantId = $null
                        }
                    }
                } else {
                    Write-Host " ! Stored certificate not found, falling back to interactive login" -ForegroundColor Yellow
                    # 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
        $app = Get-MgApplication -Filter "displayName eq '$AppName'" -ErrorAction SilentlyContinue | Select-Object -First 1
        
        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
                    } catch {
                        if ($_.Exception.Message -notlike "*Permission being assigned already exists*") {
                            Write-Host " ! Failed to assign Graph permission $roleId" -ForegroundColor Yellow
                        }
                    }
                }
                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
                        Write-Host "- Exchange permissions assigned" -ForegroundColor Green
                    } catch {
                        if ($_.Exception.Message -notlike "*Permission being assigned already exists*") {
                            Write-Host "! Failed to assign Exchange permission" -ForegroundColor Yellow
                        }
                    }
                }
            } 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
                        } catch {
                            if ($_.Exception.Message -notlike "*Permission being assigned already exists*") {
                                Write-Host " ! Failed to assign SharePoint permission $roleId" -ForegroundColor Yellow
                            }
                        }
                    }
                    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"
                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
                } else {
                    Write-Host "! Failed to assign Exchange Administrator role - manual assignment may be required" -ForegroundColor Yellow
                }
            }
            
            # 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"
                Write-Host "- Global Administrator role assigned" -ForegroundColor Green
            } catch {
                Write-Host "! Failed to assign Global Admin role - manual assignment may be required" -ForegroundColor Yellow
            }
            
            # 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
                        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 " ! Failed to assign Exchange Administrator role - manual assignment required" -ForegroundColor Yellow
                        }
                    }
                } 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"
                        Write-Host " - Added Global Administrator role" -ForegroundColor Green
                    } catch {
                        Write-Host " ! Failed to assign Global Admin role - manual assignment may be required" -ForegroundColor Yellow
                    }
                } 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
                            Write-Host " - Added Graph permission: $roleId" -ForegroundColor Green
                        } catch {
                            Write-Host " ! Failed to add Graph permission $roleId" -ForegroundColor Yellow
                        }
                    }
                }
                
                # 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
                            Write-Host " - Added Exchange permission: $exchangeAppRole" -ForegroundColor Green
                        } catch {
                            Write-Host " ! Failed to add Exchange permission $exchangeAppRole" -ForegroundColor Yellow
                        }
                    }
                }
                
                # 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
                                Write-Host " - Added SharePoint permission: $roleId" -ForegroundColor Green
                            } catch {
                                Write-Host " ! Failed to add SharePoint permission $roleId" -ForegroundColor Yellow
                            }
                        }
                    }
                }
                
                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
        }
        
        Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
        
        # Use thumbprint method like the original script - suppress all output
        Connect-MgGraph -ClientId $app.AppId -TenantId $actualTenantId -CertificateThumbprint $cert.Thumbprint -NoWelcome -ContextScope Process -ErrorAction Stop | Out-Null 2>&1
        
        $newContext = Get-MgContext
        if ($newContext.AuthType -ne "AppOnly") {
            throw "Certificate authentication failed - AuthType: $($newContext.AuthType)"
        }
        
        # Quick API test
        $null = Get-MgUser -Top 1 -ErrorAction Stop
        Write-Host "- Certificate authentication ready" -ForegroundColor Green
        
        # 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
            Write-Host "- Tenant information saved for future use" -ForegroundColor Green
        } 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 {
            Write-Host " ! Could not export certificate with private key, using public key only" -ForegroundColor Yellow
            $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 "USAGE:" -ForegroundColor Yellow
    Write-Host "`$auth = Get-CertificateForAuth" -ForegroundColor White
    Write-Host "Returns: Certificate, AppId, TenantId, Thumbprint, CertificateBase64, CertificatePassword" -ForegroundColor Gray
    Write-Host "Note: Module automatically connects to Microsoft Graph with required permissions`n" -ForegroundColor Gray
    
    Write-Host "CONNECTION EXAMPLES:" -ForegroundColor Yellow
    
    Write-Host "`nExchange Online:" -ForegroundColor Cyan
    Write-Host "Connect-ExchangeOnline -AppId `$auth.AppId -CertificateThumbprint `$auth.Thumbprint -Organization 'tenant.onmicrosoft.com'" -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://tenant.sharepoint.com' -ClientId `$auth.AppId -Tenant 'tenant.onmicrosoft.com' -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 'tenant.onmicrosoft.com'" -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 "Get-StoredTenants # List all stored tenants (returns objects)" -ForegroundColor White
    Write-Host "Show-StoredTenants # Display formatted list of stored tenants" -ForegroundColor White
    Write-Host "Remove-StoredTenant # Remove specific stored tenant" -ForegroundColor White
    Write-Host "Clear-StoredTenants # Clear all stored 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 "`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