|
| 1 | + |
| 2 | +# Export Microsoft 365 Active Users to CSV Using Microsoft Graph (Cross-Platform) |
| 3 | + |
| 4 | +## Summary |
| 5 | + |
| 6 | +This PowerShell script exports Microsoft 365 users — including properties such as Display Name, UPN, Department, Job Title, Account Status, and Manager Email — into a CSV file using the Microsoft Graph API. |
| 7 | + |
| 8 | +It automatically ensures that the Microsoft.Graph module is installed, connects securely using interactive or device-code authentication, and retrieves enabled users by default (with optional switches for guests, disabled accounts, and manager lookups). |
| 9 | + |
| 10 | +The script is fully optimized for macOS and Linux, performing a safe write-access check on the Graph cache directory (~/.mg) to prevent permission issues caused by previous sudo runs. |
| 11 | +If you are running on Windows, that permission check can be safely skipped or removed since Windows manages Graph credentials differently. |
| 12 | + |
| 13 | + |
| 14 | + |
| 15 | +# [Microsoft Graph PowerShell](#tab/graphps) |
| 16 | + |
| 17 | +```powershell |
| 18 | +
|
| 19 | +<# |
| 20 | +.SYNOPSIS |
| 21 | +Export active Microsoft 365 users to CSV using Microsoft Graph. |
| 22 | +
|
| 23 | +.DESCRIPTION |
| 24 | +Wraps the script in an advanced function that ensures the Microsoft.Graph module is available, |
| 25 | +connects using device code (with a macOS-friendly context scope), queries enabled users by default, |
| 26 | +optionally looks up manager email, and writes a CSV. Includes a fallback to interactive login if |
| 27 | +device code fails on macOS environments. |
| 28 | +
|
| 29 | +.EXAMPLE |
| 30 | +Export-M365ActiveUsers |
| 31 | +Exports enabled Member users to Desktop/M365_ActiveUsers.csv using device-code authentication. |
| 32 | +
|
| 33 | +.EXAMPLE |
| 34 | +Export-M365ActiveUsers -IncludeGuests -Path "/tmp/users.csv" |
| 35 | +Includes Guests in addition to Members and writes to /tmp/users.csv |
| 36 | +#> |
| 37 | +function Export-M365ActiveUsers { |
| 38 | + [CmdletBinding(SupportsShouldProcess)] |
| 39 | + param( |
| 40 | + [Parameter()] |
| 41 | + [string]$Path = 'M365_ActiveUsers.csv', |
| 42 | +
|
| 43 | + [Parameter()] |
| 44 | + [string]$TenantId = $(Read-Host "Enter your Azure AD Tenant ID"), |
| 45 | +
|
| 46 | + [Parameter()] |
| 47 | + [string[]]$Scopes = @('User.Read.All','Directory.Read.All'), |
| 48 | +
|
| 49 | + [Parameter()] |
| 50 | + [switch]$IncludeGuests, |
| 51 | +
|
| 52 | + [Parameter()] |
| 53 | + [switch]$IncludeDisabled, |
| 54 | +
|
| 55 | + [Parameter()] |
| 56 | + [switch]$IncludeManager = $true, |
| 57 | +
|
| 58 | + [Parameter()] |
| 59 | + [switch]$DeviceCode = $true, |
| 60 | +
|
| 61 | + [Parameter()] |
| 62 | + [switch]$Force |
| 63 | + ) |
| 64 | +
|
| 65 | + # 1) Ensure Graph module exists |
| 66 | + $moduleName = 'Microsoft.Graph' |
| 67 | + if (-not (Get-Module -ListAvailable -Name $moduleName)) { |
| 68 | + Write-Host "Microsoft Graph module not found. Installing to CurrentUser scope..." -ForegroundColor Yellow |
| 69 | + try { |
| 70 | + Install-Module -Name $moduleName -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop | Out-Null |
| 71 | + } catch { |
| 72 | + throw "Failed to install module '$moduleName': $($_.Exception.Message)" |
| 73 | + } |
| 74 | + } |
| 75 | + Import-Module -Name $moduleName -ErrorAction Stop |
| 76 | +
|
| 77 | + # Preflight check: warn if ~/.mg is owned by root (from a previous sudo run) |
| 78 | + $platform = [System.Environment]::OSVersion.Platform |
| 79 | +if ($platform -eq 'Unix' -or $platform -eq 'MacOSX') { |
| 80 | + $mgDir = Join-Path $HOME '.mg' |
| 81 | + if (Test-Path $mgDir) { |
| 82 | + try { |
| 83 | + $testFile = Join-Path $mgDir "test_write_$(Get-Random).tmp" |
| 84 | + [void][System.IO.File]::WriteAllText($testFile, 'test') |
| 85 | + Remove-Item $testFile -Force -ErrorAction SilentlyContinue |
| 86 | + } catch { |
| 87 | + Write-Warning "Cannot write to $mgDir — likely owned by root from a previous sudo run." |
| 88 | + Write-Host "Fix with: sudo chown -R `$USER:staff ~/.mg && sudo chmod 700 ~/.mg" -ForegroundColor Yellow |
| 89 | + Write-Host "Or remove it: sudo rm -rf ~/.mg`n" -ForegroundColor Yellow |
| 90 | + throw "Access denied to Microsoft Graph cache directory. See instructions above." |
| 91 | + } |
| 92 | + } |
| 93 | +} else { |
| 94 | + Write-Verbose "Windows detected — skipping ~/.mg permission check." |
| 95 | +} |
| 96 | +
|
| 97 | + # 2) Connect to Graph |
| 98 | + Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null |
| 99 | +
|
| 100 | + Write-Host "`n🔐 Connecting to Microsoft Graph..." -ForegroundColor Cyan |
| 101 | + Write-Host "A browser window will open for authentication.`n" -ForegroundColor Yellow |
| 102 | +
|
| 103 | + try { |
| 104 | + # Use interactive browser login - more reliable on macOS than device code |
| 105 | + # ContextScope Process keeps tokens in memory only (not persisted to disk) |
| 106 | + Connect-MgGraph -Scopes $Scopes -TenantId $TenantId -ContextScope Process -ErrorAction Stop |
| 107 | + } catch { |
| 108 | + $msg = $_.Exception.Message |
| 109 | + Write-Error "Failed to connect to Microsoft Graph: $msg" |
| 110 | + Write-Host "`n💡 Troubleshooting:" -ForegroundColor Yellow |
| 111 | + Write-Host " 1. Make sure you complete the browser sign-in" -ForegroundColor Yellow |
| 112 | + Write-Host " 2. If the browser doesn't open, check for popup blockers" -ForegroundColor Yellow |
| 113 | + Write-Host " 3. Close all browser windows and try again`n" -ForegroundColor Yellow |
| 114 | + throw |
| 115 | + } |
| 116 | +
|
| 117 | + $context = Get-MgContext |
| 118 | + if (-not $context) { throw "Authentication context not available after login." } |
| 119 | + Write-Host "✅ Connected as: $($context.Account)" -ForegroundColor Green |
| 120 | +
|
| 121 | + # 3) Query users |
| 122 | + $filters = @() |
| 123 | + if (-not $IncludeDisabled) { $filters += "accountEnabled eq true" } |
| 124 | + if (-not $IncludeGuests) { $filters += "userType eq 'Member'" } |
| 125 | + $filterStr = $filters -join ' and ' |
| 126 | +
|
| 127 | + $props = @('Id','DisplayName','UserPrincipalName','Department','JobTitle','AccountEnabled') |
| 128 | + if ([string]::IsNullOrWhiteSpace($filterStr)) { |
| 129 | + $users = Get-MgUser -All -Property $props |
| 130 | + } else { |
| 131 | + $users = Get-MgUser -All -Filter $filterStr -Property $props |
| 132 | + } |
| 133 | +
|
| 134 | + # 4) Project output |
| 135 | + $export = @() |
| 136 | + foreach ($u in $users) { |
| 137 | + $mgr = $null |
| 138 | + if ($IncludeManager) { |
| 139 | + try { |
| 140 | + $mgrObj = Get-MgUserManager -UserId $u.Id -ErrorAction SilentlyContinue |
| 141 | + if ($mgrObj) { $mgr = $mgrObj.AdditionalProperties.mail } |
| 142 | + } catch {} |
| 143 | + } |
| 144 | + $export += [PSCustomObject]@{ |
| 145 | + EmployeeId = $u.Id |
| 146 | + EmployeeName = $u.DisplayName |
| 147 | + Email = $u.UserPrincipalName |
| 148 | + Department = $u.Department |
| 149 | + JobTitle = $u.JobTitle |
| 150 | + ManagerEmail = $mgr |
| 151 | + AccountEnabled = $u.AccountEnabled |
| 152 | + } |
| 153 | + } |
| 154 | +
|
| 155 | + # 5) Export |
| 156 | + if ($PSCmdlet.ShouldProcess($Path, 'Export users to CSV')) { |
| 157 | + $export | Export-Csv -Path $Path -NoTypeInformation -Encoding UTF8 -Force:$Force |
| 158 | + Write-Host "📄 Export complete. File saved to $Path" -ForegroundColor Cyan |
| 159 | + } |
| 160 | +
|
| 161 | + return $export |
| 162 | +} |
| 163 | +
|
| 164 | +# Run with defaults if executed directly |
| 165 | +Export-M365ActiveUsers | Out-Null |
| 166 | +``` |
| 167 | + |
| 168 | +[!INCLUDE [More about Microsoft Graph PowerShell SDK](../../docfx/includes/MORE-GRAPHSDK.md)] |
| 169 | +*** |
| 170 | + |
| 171 | +## Contributors |
| 172 | + |
| 173 | +| Author(s) | |
| 174 | +|-----------| |
| 175 | +| [Divya Akula](https://github.com/divya-akula)| |
| 176 | + |
| 177 | +[!INCLUDE [DISCLAIMER](../../docfx/includes/DISCLAIMER.md)] |
| 178 | + |
| 179 | +<img src="https://m365-visitor-stats.azurewebsites.net/script-samples/scripts/graph-get-teams-tabs-export-to-csv" aria-hidden="true" /> |
0 commit comments