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:

  1. Navigate to Microsoft Entra admin center (https://entra.microsoft.com)
  2. Go to IdentityExternal IdentitiesExternal collaboration settings
  3. Scroll to Guest user expiration
  4. Enable guest expiration settings

Recommended Configuration

SettingRecommended ValueDescription
Guest user access expiresYesEnable expiration
Number of days90Guests expire after 90 days
Notification days before expiration14Notify 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:

  1. Navigate to Identity GovernanceAccess reviews
  2. 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)
  1. Click Create

Step 4: Configure Lifecycle Workflows

Use Entra ID Governance lifecycle workflows for automated guest management:

  1. Navigate to Identity GovernanceLifecycle workflows
  2. Click + Create workflow

Create Guest Offboarding Workflow

  1. Select template: Offboard a guest user

  2. Configure trigger:

    • Trigger type: Time-based
    • Days before/after: 90 days after account creation (or based on attribute)
  3. 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
  4. Configure notifications:

    • Notify guest before action (if possible)
    • Notify sponsor when action is taken
  5. 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

  1. Navigate to Identity GovernanceSettings
  2. 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

  1. Sponsor submits extension request via IT ticket
  2. IT Security reviews business justification
  3. 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:

  1. Verify the access review is enabled
  2. Check the start date and recurrence settings
  3. Verify reviewers have been assigned
  4. 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:

  1. Verify the account is enabled
  2. Check group memberships are restored
  3. Verify Conditional Access isn't blocking
  4. Check if resource-level permissions were removed

Issue: Lifecycle workflow not processing guests

Cause: Workflow trigger or scope may be misconfigured.

Solution:

  1. Verify workflow is enabled
  2. Check trigger conditions match guest attributes
  3. Verify scope filter correctly identifies guests
  4. Review workflow run history for errors

Issue: Sponsor field not available

Cause: Standard user attributes may not include sponsor.

Solution:

  1. Use the manager field for sponsor tracking
  2. Create a custom extension attribute for explicit sponsor
  3. Use group membership for sponsor accountability
  4. 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:

  1. Distribute reviews across more reviewers
  2. Use resource owners instead of central team
  3. Stagger review schedules (monthly batches)
  4. Pre-filter obviously stale guests before review

Issue: Guests still active after expiration

Cause: Expiration settings may only warn, not enforce.

Solution:

  1. Ensure "Auto apply results" is enabled
  2. Verify lifecycle workflows are running
  3. Check if the guest was granted an exception
  4. 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:

  1. Check for owned objects:
    Get-MgUserOwnedObject -UserId $guestId
    
  2. Transfer ownership before deletion
  3. Remove guest from group ownership
  4. Delete owned content if appropriate

Related Resources


Last updated: January 2025