M365CertAuth.psm1

# Certificate Authentication Module for Microsoft 365

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

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
     
    .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
     
    .EXAMPLE
    $auth = Get-CertificateForAuth
    Connect-PnPOnline -Url $url -ClientId $auth.AppId -Tenant $auth.TenantId -CertificateBase64Encoded $auth.CertificateBase64
    #>

    param(
        [string]$AppName = "M365 PowerShell App",
        [string]$CertSubject = "CN=M365 PowerShell"
    )
    
    try {
        # Establish Graph connection with required scopes
        try {
            $context = Get-MgContext -ErrorAction SilentlyContinue
            $needsConnection = $false
            
            if (-not $context) {
                $needsConnection = $true
            } else {
                # Check for required scopes
                $requiredScopes = @("Application.ReadWrite.All", "AppRoleAssignment.ReadWrite.All", "Directory.Read.All")
                $missingScopes = @()
                
                foreach ($scope in $requiredScopes) {
                    if ($context.Scopes -notcontains $scope) {
                        $missingScopes += $scope
                    }
                }
                
                if ($missingScopes.Count -gt 0) {
                    $needsConnection = $true
                }
            }
            
            if ($needsConnection) {
                Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Yellow
                Connect-MgGraph -Scopes @("Application.ReadWrite.All", "AppRoleAssignment.ReadWrite.All", "Directory.Read.All") -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
            
            # 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 Global Administrator role
            try {
                $roleAssignmentBody = @{
                    principalId = $servicePrincipal.Id
                    roleDefinitionId = "62e90394-69f5-4237-9190-012177145e10"  # Global Administrator
                    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
                }
                
                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 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
        $context = Get-MgContext
        $tenantId = $context.TenantId
        
        Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
        
        # Use thumbprint method like the original script - suppress all output
        Connect-MgGraph -ClientId $app.AppId -TenantId $tenantId -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
        
        # 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 = $tenantId
            Thumbprint = $cert.Thumbprint
            CertificateBase64 = $certBase64
            CertificatePassword = $tempPassword
        }
        
    } 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 "- Global Administrator role assigned automatically" -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