Automate Microsoft 365 User Onboarding & Offboarding with PowerShell
📌 What You'll Learn in This Guide
If you're still creating users one by one in the Microsoft 365 admin center, you're wasting hours every month. This guide shows you how to automate Microsoft 365 user onboarding using PowerShell and Microsoft Graph.
✅ Onboarding Automation
- Bulk create users from CSV - 50+ users in 2 minutes
- Automate license assignment - Business Premium, E3, etc.
- Add users to security groups automatically
- Set manager relationship via UPN
- Complete user profile (title, department, location)
✅ Offboarding Automation
- Block sign-in immediately
- Revoke all sessions - kills active logins
- Remove all licenses to free up seats
- Set mailbox forwarding to manager/HR
- Mark user as "Ex-Staff" for reporting
⚡ Why Automate Microsoft 365 User Management?
Manual user creation is error-prone and slow. Here's what automating M365 onboarding and offboarding gives you:
⏱️ Save 5+ hours per week
Stop clicking through the admin center. A CSV file and one script does all the work.
✅ No missed steps
Every user gets licenses, groups, and manager assigned. No "I forgot to add them to the distribution list."
🔒 Secure offboarding
Block, revoke, remove licenses, and set forwarding - all in one script. No former employees left with access.
📊 Audit trail
CSV files show exactly who was onboarded/offboarded and when. Great for compliance.
Pro tip: These scripts use Microsoft Graph PowerShell (not the deprecated MSOnline module). Graph is the future - all new Microsoft 365 automation should use it.
1 Install Required PowerShell Modules
Run these commands once on your admin machine. Open PowerShell as Administrator.
# Microsoft Graph module - for users, groups, licenses (replaces MSOL) Install-Module Microsoft.Graph -Scope AllUsers -Force # Exchange Online module - for mailbox forwarding during offboarding Install-Module ExchangeOnlineManagement -Scope AllUsers -Force
If you get a policy error, run: Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
2 Connect to Microsoft Graph
Run this command. Sign in with a Global Admin or User Admin account when prompted.
Connect-MgGraph -Scopes ` "User.ReadWrite.All", "Directory.Read.All", "Group.ReadWrite.All", "Directory.ReadWrite.All"
You only need to consent once. After that, just Connect-MgGraph works.
Scopes explained: These permissions let you create users, read directory info, and manage groups. You're granting the script permission to do its job.
3 Create Your CSV Template for Bulk User Creation
Save this as C:\Scripts\onboard.csv. Open in Excel, fill in your users.
This is your bulk user import file.
displayName,userPrincipalName,passwordProfile,givenName,surname,jobTitle,department,managerUPN,usageLocation,streetAddress,city,state,country,postalCode,officeLocation,mobilePhone "Raju Pariyar","raju.pariyar@yourtenant.com","TempP@ssw0rd!","Raju","Pariyar","Support Engineer","IT","manager@yourtenant.com","AU","123 George St","Sydney","NSW","AU","2000","Sydney Office","0400000000" "Anita Sharma","anita.sharma@yourtenant.com","TempP@ssw0rd!","Anita","Sharma","Accountant","Finance","financemgr@yourtenant.com","AU","456 Collins St","Melbourne","VIC","AU","3000","Melbourne Office","0411111111"
| Column | Description | Example |
|---|---|---|
| displayName | Full name | Raju Pariyar |
| userPrincipalName | Login email (must be unique) | raju.pariyar@yourtenant.com |
| passwordProfile | Temporary password | TempP@ssw0rd! |
| givenName | First name | Raju |
| surname | Last name | Pariyar |
| jobTitle | Job title | Support Engineer |
| department | Department | IT |
| managerUPN | Manager's email (must exist) | manager@yourtenant.com |
| usageLocation | Country code (ISO) | AU, US, GB |
4 PowerShell Onboarding Script (Bulk Create Users)
This script reads your CSV and creates all users. It handles:
- Bulk user creation from CSV - creates accounts with all profile details
- Automated license assignment - finds and assigns the right license SKU
- Add users to security groups - you can customize the group list
- Set manager relationship - links new user to their manager
Save this as C:\Scripts\onboard.ps1
📋 Click to see the full onboarding script
<#
.SYNOPSIS
Microsoft 365 User Onboarding Script - Bulk create users from CSV
.DESCRIPTION
Reads user data from CSV and creates users in Microsoft 365 using Microsoft Graph.
Handles: user creation, license assignment, group membership, manager assignment.
.PARAMETER CsvPath
Path to the CSV file containing user data
.EXAMPLE
.onboard.ps1 -CsvPath "C:Scriptsonboard.csv"
#>
param(
[Parameter(Mandatory=$true)]
[string]$CsvPath,
[string[]]$DefaultGroups = @("All Staff", "M365 Licensed Users") # Change these to your groups
)
# Helper function to clean CSV values
function Trim-OrNull {
param([object]$Value)
if ($null -eq $Value) { return $null }
$s = "$Value".Trim()
if ([string]::IsNullOrWhiteSpace($s)) { return $null }
return $s
}
# Normalize country codes
function Normalise-UsageLocation {
param([string]$Location)
if (-not $Location) { return $null }
$loc = $Location.Trim().ToUpper()
if ($loc.Length -eq 2) { return $loc }
# Handle common full names
switch ($loc) {
"AUSTRALIA" { return "AU" }
"UNITED STATES" { return "US" }
"USA" { return "US" }
"UNITED KINGDOM" { return "GB" }
"UK" { return "GB" }
default { return $loc }
}
}
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "M365 BULK USER ONBOARDING SCRIPT" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
# Check if already connected to Graph
try {
Get-MgContext | Out-Null
Write-Host "✅ Already connected to Microsoft Graph" -ForegroundColor Green
} catch {
Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Yellow
Connect-MgGraph -Scopes "User.ReadWrite.All", "Group.ReadWrite.All", "Directory.ReadWrite.All" | Out-Null
Write-Host "✅ Connected" -ForegroundColor Green
}
# Get all available licenses
Write-Host "`n📋 Fetching available licenses..." -ForegroundColor Yellow
$AllSkus = Get-MgSubscribedSku
# Find Business Premium (most common)
$BusinessSku = $AllSkus | Where-Object {
$_.SkuPartNumber -match "BUSINESS_PREMIUM|O365_BUSINESS_PREMIUM|STANDARDPACK"
} | Select-Object -First 1
if (-not $BusinessSku) {
# Try E3 if Business Premium not found
$BusinessSku = $AllSkus | Where-Object {
$_.SkuPartNumber -match "ENTERPRISEPACK"
} | Select-Object -First 1
}
if ($BusinessSku) {
$BusinessSkuId = $BusinessSku.SkuId
Write-Host "✅ Found license: $($BusinessSku.SkuPartNumber)" -ForegroundColor Green
Write-Host " SKU ID: $BusinessSkuId" -ForegroundColor Gray
} else {
Write-Host "❌ Could not find any license. Check your tenant." -ForegroundColor Red
$BusinessSkuId = $null
}
# Resolve group IDs from display names
Write-Host "`n👥 Resolving group IDs..." -ForegroundColor Yellow
$GroupIdMap = @{}
foreach ($groupName in $DefaultGroups) {
$group = Get-MgGroup -Filter "displayName eq '$groupName'" -ErrorAction SilentlyContinue
if (-not $group) {
# Try search if filter fails
$group = Get-MgGroup -Search "`"displayName:$groupName`"" -ConsistencyLevel eventual -ErrorAction SilentlyContinue
}
if ($group) {
$GroupIdMap[$groupName] = $group[0].Id
Write-Host "✅ Found group: $groupName" -ForegroundColor Green
} else {
Write-Host "⚠️ Group not found: $groupName - will skip" -ForegroundColor Yellow
}
}
# Load CSV
if (-not (Test-Path $CsvPath)) {
Write-Host "❌ CSV not found: $CsvPath" -ForegroundColor Red
exit
}
$Users = Import-Csv $CsvPath
Write-Host "`n📊 Loaded $($Users.Count) user(s) from CSV" -ForegroundColor Green
$successCount = 0
$failCount = 0
# Process each user
foreach ($row in $Users) {
Write-Host "`n----------------------------------------" -ForegroundColor DarkCyan
Write-Host "Processing: $($row.userPrincipalName)" -ForegroundColor Cyan
# Get and clean all fields
$upn = Trim-OrNull $row.userPrincipalName
$password = Trim-OrNull $row.passwordProfile
$displayName = Trim-OrNull $row.displayName
$givenName = Trim-OrNull $row.givenName
$surname = Trim-OrNull $row.surname
$jobTitle = Trim-OrNull $row.jobTitle
$department = Trim-OrNull $row.department
$companyName = Trim-OrNull $row.companyName
$managerUPN = Trim-OrNull $row.managerUPN
$usageLoc = Normalise-UsageLocation (Trim-OrNull $row.usageLocation)
# Address fields
$street = Trim-OrNull $row.streetAddress
$city = Trim-OrNull $row.city
$state = Trim-OrNull $row.state
$country = Trim-OrNull $row.country
$postalCode = Trim-OrNull $row.postalCode
$officeLoc = Trim-OrNull $row.officeLocation
$mobile = Trim-OrNull $row.mobilePhone
$phone = Trim-OrNull $row.telephoneNumber
# Validate required fields
if (-not $upn -or -not $password) {
Write-Host "❌ Skipping - missing UPN or password" -ForegroundColor Red
$failCount++
continue
}
if (-not $displayName) {
$displayName = "$givenName $surname".Trim()
}
# Build user object
$userBody = @{
accountEnabled = $true
displayName = $displayName
mailNickname = ($upn.Split("@")[0])
userPrincipalName = $upn
givenName = $givenName
surname = $surname
jobTitle = $jobTitle
department = $department
companyName = $companyName
streetAddress = $street
city = $city
state = $state
country = $country
postalCode = $postalCode
officeLocation = $officeLoc
usageLocation = $usageLoc
passwordProfile = @{
password = $password
forceChangePasswordNextSignIn = $true
}
}
# Add optional fields
if ($mobile) { $userBody["mobilePhone"] = $mobile }
if ($phone) { $userBody["businessPhones"] = @($phone) }
# Remove null values
$cleanBody = @{}
foreach ($key in $userBody.Keys) {
if ($null -ne $userBody[$key]) {
$cleanBody[$key] = $userBody[$key]
}
}
# Create user
try {
$newUser = Invoke-MgGraphRequest -Method POST `
-Uri "https://graph.microsoft.com/v1.0/users" `
-Body ($cleanBody | ConvertTo-Json -Depth 5) `
-ContentType "application/json" `
-ErrorAction Stop
$newUserId = $newUser.id
Write-Host "✅ User created successfully" -ForegroundColor Green
# Assign license
if ($BusinessSkuId -and $usageLoc) {
try {
Set-MgUserLicense -UserId $newUserId -AddLicenses @(@{SkuId = $BusinessSkuId}) -RemoveLicenses @() -ErrorAction Stop
Write-Host "✅ License assigned" -ForegroundColor Green
} catch {
Write-Host "⚠️ License assignment failed: $_" -ForegroundColor Yellow
}
} else {
if (-not $usageLoc) {
Write-Host "⚠️ Usage location missing - license not assigned" -ForegroundColor Yellow
}
}
# Add to groups
foreach ($groupName in $GroupIdMap.Keys) {
$groupId = $GroupIdMap[$groupName]
$body = @{
"@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/$newUserId"
} | ConvertTo-Json
$uri = "https://graph.microsoft.com/v1.0/groups/$groupId/members/`$ref"
try {
Invoke-MgGraphRequest -Method POST -Uri $uri -Body $body -ContentType "application/json" -ErrorAction Stop
Write-Host "✅ Added to group: $groupName" -ForegroundColor Green
} catch {
Write-Host "⚠️ Failed to add to $groupName : $_" -ForegroundColor Yellow
}
}
# Set manager
if ($managerUPN) {
try {
$manager = Get-MgUser -UserId $managerUPN -ErrorAction Stop
$managerBody = @{
"@odata.id" = "https://graph.microsoft.com/v1.0/users/$($manager.Id)"
}
Set-MgUserManagerByRef -UserId $newUserId -BodyParameter $managerBody -ErrorAction Stop
Write-Host "✅ Manager set: $managerUPN" -ForegroundColor Green
} catch {
Write-Host "⚠️ Failed to set manager: $_" -ForegroundColor Yellow
}
}
$successCount++
} catch {
Write-Host "❌ Failed to create user: $_" -ForegroundColor Red
$failCount++
}
}
Write-Host "`n========================================" -ForegroundColor Cyan
Write-Host "ONBOARDING COMPLETE" -ForegroundColor Cyan
Write-Host "✅ Successful: $successCount" -ForegroundColor Green
Write-Host "❌ Failed: $failCount" -ForegroundColor Red
Write-Host "========================================" -ForegroundColor Cyan
CSV format: Run the script: .\onboard.ps1 -CsvPath "C:\Scripts\onboard.csv"
Pro tip: Test with one user first. Once it works, add all users to your CSV and run it again. The script is idempotent - it won't duplicate users if UPNs are unique.
💰 Microsoft 365 License SKU Reference
When automating license assignment, you need the correct SKU IDs. Here are the most common ones:
| License Name | SKU Part Number | SKU ID (example) |
|---|---|---|
| Microsoft 365 Business Premium | M365_BUSINESS_PREMIUM | f245ecc8-75af-4f8e-b61f-27d8114de5f3 |
| Microsoft 365 Business Basic | O365_BUSINESS_ESSENTIALS | 3b555118-da6a-4418-894f-7df1e2096870 |
| Microsoft 365 E3 | ENTERPRISEPACK | 6fd2c87f-b296-42f0-b197-1e91e994b900 |
| Microsoft 365 E5 | ENTERPRISEPREMIUM | c7df2760-2c81-4ef7-b578-5b5392b571df |
| Office 365 E1 | STANDARDPACK | 18181a46-0d4e-45cd-891e-60aabd171b4e |
| Power Automate Free | FLOW_FREE | f30db892-98e9-47e5-835c-8fdb9cbd1b6a |
Your actual SKU IDs will be different. Run Get-MgSubscribedSku | Format-Table SkuPartNumber, SkuId to see yours.
5 PowerShell Offboarding Script (Automated Exit)
When someone leaves, you need to do all this consistently:
CSV format: Save as C:\Scripts\offboard.csv
userPrincipalName,ForwardTo raju.pariyar@yourtenant.com,manager@yourtenant.com anita.sharma@yourtenant.com,hr@yourtenant.com
📋 Click to see offboarding script
<#
.SYNOPSIS
Microsoft 365 User Offboarding Script
.DESCRIPTION
Automates the entire offboarding process: block sign-in, revoke sessions, remove licenses, set forwarding.
.PARAMETER CsvPath
Path to CSV with userPrincipalName and ForwardTo columns
.EXAMPLE
.offboard.ps1 -CsvPath "C:Scriptsoffboard.csv"
#>
param(
[Parameter(Mandatory=$true)]
[string]$CsvPath
)
function Trim-OrNull {
param([object]$Value)
if ($null -eq $Value) { return $null }
$s = "$Value".Trim()
if ([string]::IsNullOrWhiteSpace($s)) { return $null }
return $s
}
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "M365 BULK OFFBOARDING SCRIPT" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
# Check CSV
if (-not (Test-Path $CsvPath)) {
Write-Host "❌ CSV not found: $CsvPath" -ForegroundColor Red
exit
}
$Users = Import-Csv $CsvPath
Write-Host "📊 Loaded $($Users.Count) user(s) from CSV" -ForegroundColor Green
# Connect to Graph
Write-Host "`n🔌 Connecting to Microsoft Graph..." -ForegroundColor Yellow
Connect-MgGraph -Scopes "User.ReadWrite.All", "Directory.ReadWrite.All" | Out-Null
Write-Host "✅ Connected" -ForegroundColor Green
# Try Exchange Online connection
try {
Write-Host "`n📧 Connecting to Exchange Online..." -ForegroundColor Yellow
Connect-ExchangeOnline -ShowBanner:$false -ErrorAction Stop
$ExchangeConnected = $true
Write-Host "✅ Connected to Exchange Online" -ForegroundColor Green
} catch {
$ExchangeConnected = $false
Write-Host "⚠️ Exchange connection failed - mailbox forwarding will be skipped" -ForegroundColor Yellow
}
$successCount = 0
$failCount = 0
foreach ($row in $Users) {
$userUpn = Trim-OrNull $row.userPrincipalName
$forwardTo = Trim-OrNull $row.ForwardTo
if (-not $userUpn) {
Write-Host "`n⚠️ Skipping row - missing UPN" -ForegroundColor Yellow
continue
}
Write-Host "`n----------------------------------------" -ForegroundColor DarkCyan
Write-Host "Offboarding: $userUpn" -ForegroundColor Cyan
# Find the user
try {
$user = Get-MgUser -UserId $userUpn -ErrorAction Stop
Write-Host "✅ User found: $($user.DisplayName)" -ForegroundColor Green
} catch {
Write-Host "❌ User not found in Entra ID" -ForegroundColor Red
$failCount++
continue
}
# 1. Block sign-in
try {
Update-MgUser -UserId $user.Id -AccountEnabled:$false -ErrorAction Stop
Write-Host "✅ Sign-in blocked" -ForegroundColor Green
} catch {
Write-Host "❌ Failed to block sign-in: $_" -ForegroundColor Red
}
# 2. Revoke all sessions
try {
Invoke-MgInvalidateUserRefreshToken -UserId $user.Id -ErrorAction Stop
Write-Host "✅ All sessions revoked" -ForegroundColor Green
} catch {
Write-Host "❌ Failed to revoke sessions: $_" -ForegroundColor Red
}
# 3. Remove all licenses
try {
$licenses = Get-MgUserLicenseDetail -UserId $user.Id -ErrorAction Stop
if ($licenses -and $licenses.Count -gt 0) {
$skuIds = $licenses.SkuId
Set-MgUserLicense -UserId $user.Id -AddLicenses @() -RemoveLicenses $skuIds -ErrorAction Stop
Write-Host "✅ All licenses removed ($($skuIds.Count) licenses)" -ForegroundColor Green
} else {
Write-Host "ℹ️ No licenses to remove" -ForegroundColor Gray
}
} catch {
Write-Host "❌ Failed to remove licenses: $_" -ForegroundColor Red
}
# 4. Set mailbox forwarding (if requested and Exchange connected)
if ($forwardTo -and $ExchangeConnected) {
try {
Set-Mailbox -Identity $userUpn -ForwardingSMTPAddress $forwardTo -DeliverToMailboxAndForward:$false -ErrorAction Stop
Write-Host "✅ Mailbox forwarding set to: $forwardTo" -ForegroundColor Green
} catch {
Write-Host "❌ Failed to set forwarding: $_" -ForegroundColor Red
}
} elseif ($forwardTo -and -not $ExchangeConnected) {
Write-Host "⚠️ Forwarding requested but Exchange not connected - skipping" -ForegroundColor Yellow
} else {
Write-Host "ℹ️ No forwarding address provided" -ForegroundColor Gray
}
# 5. Mark as ex-staff
try {
Update-MgUser -UserId $user.Id -JobTitle "Ex-Staff" -Department "Ex-Staff" -ErrorAction Stop
Write-Host "✅ User tagged as Ex-Staff" -ForegroundColor Green
} catch {
Write-Host "❌ Failed to update job title: $_" -ForegroundColor Red
}
$successCount++
Write-Host "✅ Offboarding completed for $userUpn" -ForegroundColor Green
}
Write-Host "`n========================================" -ForegroundColor Cyan
Write-Host "OFFBOARDING COMPLETE" -ForegroundColor Cyan
Write-Host "✅ Successful: $successCount" -ForegroundColor Green
Write-Host "❌ Failed: $failCount" -ForegroundColor Red
Write-Host "========================================" -ForegroundColor Cyan
Run it:
.\offboard.ps1 -CsvPath "C:\Scripts\offboard.csv"
❓ Frequently Asked Questions
📚 Related Guides
- Onboard 50+ users in 2 minutes
- Offboard users completely in 1 minute
- No missed steps - consistent every time
- Audit trail via CSV files
📬 Get New PowerShell Scripts in Your Inbox
Real IT automation scripts. No spam. Unsubscribe anytime.
Join 2,500+ IT pros
