EXT-04: Managing Guest Account Lifecycle
Overview
Guest accounts often accumulate in Microsoft 365 environments without proper lifecycle management. Stale guest accounts represent a security risk as they may retain access to sensitive resources long after the collaboration ends. This guide helps you implement automated guest expiration, regular access reviews, and cleanup processes.
Prerequisites
Required Roles
- Global Administrator - Full access to all settings
- User Administrator - Can manage user accounts
- Identity Governance Administrator - Can manage access reviews and lifecycle workflows
Required Licenses
- Microsoft Entra ID P2 (required for access reviews and lifecycle workflows)
- Microsoft Entra ID Governance (for advanced lifecycle features)
Time Estimate
- Initial Configuration: 45-60 minutes
- Access Review Setup: 20-30 minutes
- Lifecycle Workflow Configuration: 30 minutes
Step-by-Step Instructions
Step 1: Audit Current Guest Population
Before configuring lifecycle management, understand your current state:
Connect-MgGraph -Scopes "User.Read.All", "AuditLog.Read.All"
# Get all guests with activity information
$guests = Get-MgUser -Filter "userType eq 'Guest'" -All -Property DisplayName, Mail, CreatedDateTime, SignInActivity, AccountEnabled, ExternalUserState
$guestReport = $guests | ForEach-Object {
$lastSignIn = $_.SignInActivity.LastSignInDateTime
$daysSinceSignIn = if ($lastSignIn) { ((Get-Date) - $lastSignIn).Days } else { 999 }
$daysSinceCreation = ((Get-Date) - $_.CreatedDateTime).Days
[PSCustomObject]@{
DisplayName = $_.DisplayName
Email = $_.Mail
CreatedDate = $_.CreatedDateTime
DaysSinceCreation = $daysSinceCreation
LastSignIn = $lastSignIn
DaysSinceSignIn = $daysSinceSignIn
AccountEnabled = $_.AccountEnabled
State = $_.ExternalUserState
Status = if (-not $_.AccountEnabled) { "Disabled" }
elseif ($daysSinceSignIn -gt 90) { "Stale" }
elseif ($daysSinceSignIn -gt 30) { "Inactive" }
else { "Active" }
}
}
# Summary
$guestReport | Group-Object Status | Format-Table Name, Count
# Export for review
$guestReport | Export-Csv -Path "GuestAudit.csv" -NoTypeInformation
Write-Host "`nTotal Guests: $($guests.Count)"
Write-Host "Stale (>90 days): $($guestReport | Where-Object Status -eq 'Stale' | Measure-Object | Select-Object -ExpandProperty Count)"
Write-Host "Pending Acceptance: $($guestReport | Where-Object State -eq 'PendingAcceptance' | Measure-Object | Select-Object -ExpandProperty Count)"
Step 2: Configure Guest Expiration Settings
Set up automatic guest expiration via external collaboration settings:
- Navigate to Microsoft Entra admin center (https://entra.microsoft.com)
- Go to Identity → External Identities → External collaboration settings
- Scroll to Guest user expiration
- Enable guest expiration settings
Recommended Configuration
| Setting | Recommended Value | Description |
|---|---|---|
| Guest user access expires | Yes | Enable expiration |
| Number of days | 90 | Guests expire after 90 days |
| Notification days before expiration | 14 | Notify sponsors 14 days before |
Note: Guest expiration removes access but does not delete the account. Use lifecycle workflows for full cleanup.
Step 3: Set Up Access Reviews for Guests
Configure recurring reviews to validate guest access:
- Navigate to Identity Governance → Access reviews
- Click + New access review
Configure Review Settings
Step 1: Review type
- Select what to review: Users
- Review scope: Guest users only
Step 2: Reviews
- Multi-stage review: Single stage (or multi-stage for escalation)
- Select reviewers:
- Option 1: Resource owners (recommended)
- Option 2: Manager of user
- Option 3: Specific users/groups (IT Security)
Step 3: Settings
- Specify recurrence of review: Quarterly
- Duration (in days): 14
- End: Never
Step 4: Upon completion
- Auto apply results to resource: Enable
- If reviewers don't respond: Remove access
- Action to apply on denied guest users: Remove user's membership from the resource (or disable for 30 days, then delete)
- Click Create
Step 4: Configure Lifecycle Workflows
Use Entra ID Governance lifecycle workflows for automated guest management:
- Navigate to Identity Governance → Lifecycle workflows
- Click + Create workflow
Create Guest Offboarding Workflow
-
Select template: Offboard a guest user
-
Configure trigger:
- Trigger type: Time-based
- Days before/after: 90 days after account creation (or based on attribute)
-
Configure tasks:
- Disable user account: Immediately upon trigger
- Remove user from all groups: After 7 days
- Delete user account: After 30 days of being disabled
-
Configure notifications:
- Notify guest before action (if possible)
- Notify sponsor when action is taken
-
Click Create
PowerShell Lifecycle Workflow Configuration
Connect-MgGraph -Scopes "IdentityGovernance.ReadWrite.All"
# Create a custom lifecycle workflow for guest expiration
$workflow = @{
displayName = "Guest User Offboarding - 90 Days"
description = "Automatically offboard guest users 90 days after creation"
isEnabled = $true
isSchedulingEnabled = $true
executionConditions = @{
trigger = @{
"@odata.type" = "#microsoft.graph.identityGovernance.timeBasedAttributeTrigger"
timeBasedAttribute = "createdDateTime"
offsetInDays = 90
}
scope = @{
"@odata.type" = "#microsoft.graph.identityGovernance.ruleBasedSubjectSet"
rule = "userType eq 'Guest'"
}
}
tasks = @(
@{
isEnabled = $true
taskDefinitionId = "1dfdfcc7-52fa-4c2e-bf3a-e3919cc12950" # Disable user account
displayName = "Disable guest account"
arguments = @()
}
@{
isEnabled = $true
taskDefinitionId = "8d18588d-9ad3-4c0f-99d0-ec215f0e3dff" # Remove from all groups
displayName = "Remove from all groups"
arguments = @()
}
)
}
New-MgIdentityGovernanceLifecycleWorkflow -BodyParameter $workflow
Step 5: Configure Sponsor Management
Assign sponsors to guest accounts for accountability:
Set Sponsor on Guest Creation
When inviting guests, assign a sponsor:
Connect-MgGraph -Scopes "User.Invite.All"
$invitation = @{
invitedUserEmailAddress = "guest@partner.com"
inviteRedirectUrl = "https://myapps.microsoft.com"
sendInvitationMessage = $true
invitedUserType = "Guest"
}
$guest = New-MgInvitation -BodyParameter $invitation
# Set the inviter as sponsor (custom attribute or extension)
# Note: Sponsor tracking typically uses manager field or extension attributes
Update-MgUser -UserId $guest.InvitedUser.Id -Manager @{ "@odata.id" = "https://graph.microsoft.com/v1.0/users/sponsor-user-id" }
Retroactively Assign Sponsors
# Find guests without sponsors (manager)
$guestsWithoutSponsor = Get-MgUser -Filter "userType eq 'Guest'" -All | Where-Object {
$manager = Get-MgUserManager -UserId $_.Id -ErrorAction SilentlyContinue
$null -eq $manager
}
Write-Host "Guests without sponsors: $($guestsWithoutSponsor.Count)"
$guestsWithoutSponsor | Select-Object DisplayName, Mail | Format-Table
Step 6: Implement Inactive Guest Cleanup
Create an automated process to handle stale guests:
# Script to identify and process stale guests
# Run monthly via Azure Automation
Connect-MgGraph -Scopes "User.ReadWrite.All", "AuditLog.Read.All"
$staleThreshold = 90 # Days of inactivity
$cutoffDate = (Get-Date).AddDays(-$staleThreshold)
$guests = Get-MgUser -Filter "userType eq 'Guest' and accountEnabled eq true" -All -Property Id, DisplayName, Mail, SignInActivity
$staleGuests = $guests | Where-Object {
$_.SignInActivity.LastSignInDateTime -lt $cutoffDate -or
$null -eq $_.SignInActivity.LastSignInDateTime
}
foreach ($guest in $staleGuests) {
Write-Host "Processing stale guest: $($guest.DisplayName) ($($guest.Mail))"
# Option 1: Disable the account
Update-MgUser -UserId $guest.Id -AccountEnabled:$false
# Option 2: Remove from all groups
$memberships = Get-MgUserMemberOf -UserId $guest.Id
foreach ($membership in $memberships) {
if ($membership.AdditionalProperties.'@odata.type' -eq '#microsoft.graph.group') {
Remove-MgGroupMemberByRef -GroupId $membership.Id -DirectoryObjectId $guest.Id
}
}
# Log the action
Write-Host " Disabled and removed from groups" -ForegroundColor Yellow
}
# Generate report
$staleGuests | Select-Object DisplayName, Mail | Export-Csv -Path "ProcessedStaleGuests.csv" -NoTypeInformation
Step 7: Set Up Notifications
Configure Email Notifications
- Navigate to Identity Governance → Settings
- Configure notification templates for:
- Guest expiration warning (14 days before)
- Guest access removed
- Access review request
Sample Notification Template
Subject: Action Required: Your guest access to [Organization] expires in [X] days
Dear [Guest Name],
Your guest access to [Organization]'s Microsoft 365 environment will expire on [Date].
If you still need access, please contact your sponsor [Sponsor Name] at [Sponsor Email] to request an extension.
If you no longer need access, no action is required.
Thank you,
[Organization] IT Team
Step 8: Monitor Guest Lifecycle Events
Create Monitoring Dashboard
Track these metrics:
- Total guest count
- Guests created this month
- Guests expiring this month
- Guests removed via access review
- Stale guest percentage
Azure Monitor Workbook Query
// Guest lifecycle metrics
let guests = AuditLogs
| where Category == "UserManagement"
| where TargetResources[0].userPrincipalName contains "#EXT#"
| summarize
Created = countif(OperationName == "Add user"),
Deleted = countif(OperationName == "Delete user"),
Disabled = countif(OperationName == "Disable account")
by bin(TimeGenerated, 1d);
guests
| render timechart
Step 9: Handle Exceptions and Extensions
Process for Extending Guest Access
- Sponsor submits extension request via IT ticket
- IT Security reviews business justification
- If approved, extend expiration:
# Extend guest access (if using lifecycle workflows)
# This typically involves resetting the trigger attribute
$guestId = "guest-user-id"
$newExpirationDate = (Get-Date).AddDays(90)
# If using a custom extension attribute for expiration tracking
Update-MgUser -UserId $guestId -OnPremisesExtensionAttributes @{
extensionAttribute1 = $newExpirationDate.ToString("yyyy-MM-dd")
}
# Re-enable if disabled
Update-MgUser -UserId $guestId -AccountEnabled:$true
Step 10: Document Guest Lifecycle Policy
Create formal policy documentation:
Guest Account Lifecycle Policy
==============================
1. GUEST CREATION
- Sponsor assigned at time of invitation
- Business justification required
- Initial access period: 90 days
2. ACCESS REVIEWS
- Frequency: Quarterly
- Reviewers: Resource owners or sponsors
- Response period: 14 days
- Default action on no response: Remove access
3. EXPIRATION
- Automatic expiration: 90 days after creation
- Extension process: Sponsor request via IT ticket
- Maximum extension: 90 days per request
- Maximum total tenure: 2 years (then requires re-invitation)
4. STALE ACCOUNT HANDLING
- Inactive threshold: 90 days
- Action: Disable account
- Retention: 30 days disabled, then deletion
5. EXCEPTIONS
- Long-term partner accounts: Annual review required
- Exception approval: IT Security Manager
Verification Checklist
After implementing guest lifecycle management:
- Guest audit completed and baseline established
- Guest expiration settings configured (90 days recommended)
- Access reviews scheduled (quarterly recommended)
- Lifecycle workflows created for offboarding
- Sponsor assignment process documented
- Inactive guest cleanup automated
- Notification templates configured
- Monitoring dashboard created
- Exception/extension process documented
- Policy documentation complete
- Stakeholders notified of new processes
Troubleshooting
Issue: Access review not triggering
Cause: Review may not be enabled or scheduled correctly.
Solution:
- Verify the access review is enabled
- Check the start date and recurrence settings
- Verify reviewers have been assigned
- Check if there are guests in scope for the review
Issue: Guest cannot access resources after extension
Cause: Extension may not have reset all expiration controls.
Solution:
- Verify the account is enabled
- Check group memberships are restored
- Verify Conditional Access isn't blocking
- Check if resource-level permissions were removed
Issue: Lifecycle workflow not processing guests
Cause: Workflow trigger or scope may be misconfigured.
Solution:
- Verify workflow is enabled
- Check trigger conditions match guest attributes
- Verify scope filter correctly identifies guests
- Review workflow run history for errors
Issue: Sponsor field not available
Cause: Standard user attributes may not include sponsor.
Solution:
- Use the
managerfield for sponsor tracking - Create a custom extension attribute for explicit sponsor
- Use group membership for sponsor accountability
- Track sponsors in external system and sync
Issue: Too many access review notifications overwhelming reviewers
Cause: Large number of guests assigned to few reviewers.
Solution:
- Distribute reviews across more reviewers
- Use resource owners instead of central team
- Stagger review schedules (monthly batches)
- Pre-filter obviously stale guests before review
Issue: Guests still active after expiration
Cause: Expiration settings may only warn, not enforce.
Solution:
- Ensure "Auto apply results" is enabled
- Verify lifecycle workflows are running
- Check if the guest was granted an exception
- Manual cleanup may be needed for legacy guests
Issue: Cannot delete guest - dependencies exist
Cause: Guest may own content or be a group owner.
Solution:
- Check for owned objects:
Get-MgUserOwnedObject -UserId $guestId - Transfer ownership before deletion
- Remove guest from group ownership
- Delete owned content if appropriate
Related Resources
- Guest access and lifecycle
- Access reviews for guests
- Lifecycle workflows
- Entitlement management for guests
Last updated: January 2025