Entra ID Guest Expiration Automation ⚙️

In this PowerShell script, we automate the deletion and expiration of Guest users within an Entra ID tenant.

Entra ID Guest Expiration Automation ⚙️
Photo by Jornada Produtora / Unsplash

🔗 Click here to download script.

At my workplace we use SharePoint Online a lot... it's our primary document storage system and our main source of collaboration both within the business and externally. With this, comes the ability to collaborate and share with external clients, partners, and persons.

Fast forward twelve months, SharePoint Online usage has taken off dramatically, documents and folders are being shared with such ease that we're seeing a huge increase in the number of guest users being created within our Entra ID tenant. It dawns on me that we need a way of managing the lifecycle of these guest users, but our primary struggle with this is Entra ID licensing, we just cannot justify Entra ID P2 or Entra ID Identity Governance right now.

So... how do we solve this issue within our current licensing restraints?

Microsoft Graph.

Let's dive into the PowerShell script!

Prerequisites

  • At least one Microsoft Entra ID P1 license.
    • This allows access to sign in logs, and reporting.
  • A preferred way of scheduling the script to run.
    • Task Scheduler, Azure Automation, whatever floats your boat.

The Script

So, when I was first designing the script, my main concern was security. No guest account should be around and active for longer than is necessary, but with that, we also had external collaborators that infrequently accessed documents and resources within our tenant. I needed to find a happy middle ground that purged those guests that hadn't been seen in a long while but catered for those infrequent guest users.

What does this script look for?

  • All Guest users that were created more than six months ago, but are 'PendingAcceptance' within Entra ID.
    • This means they were invited to collaborate in our tenant but haven't accepted that invitation in the six months we allow.
  • Next, we check for all Guest users that were created more than twelve months ago. Of those users, we check to see if they have a 'LastSuccessfulSignInDateTime' or 'LastSignInDateTime' that is within the last twelve months. If the user was created more than 12 months ago, but hasn't successfully authenticated in the last year, they're being deleted.

Considerations

There are a few things to consider before attempting to run this script.

  1. On line #2, we connect to Microsoft Graph with a Managed Identity, you'll need to change this dependant on how you want to connect to Graph.
  2. On line #20, we specify our date limit as one year. You can change this to suit your needs or your own security requirements. The date limit only applies to last successful sign ins. Change "-1" to how far back you want to look.
  3. On line #24, we specify our "guest invitation acceptance limit" as six months. Again, you can change this to suit your own needs or security requirements. Simply edit "-6" to however many months you want.
  4. On line #114, enter the email address you want to receive the email report.
  5. On line #137, enter the sender email address.

🔗 Click here to download script.

## Connect to Microsoft Graph
Connect-MgGraph -Identity

$GuestUsers = Get-MgUser -Filter "userType eq 'Guest'" -Property id,userPrincipalName,displayName,accountEnabled,onPremisesSyncEnabled,createdDateTime,signInActivity,externalUserState,mail -All
Write-Output "Found $($GuestUsers.Count) Guest users..."

if ($GuestUsers.Count -eq 0) {
    Write-Error "Found zero guest users... weird... exiting..."
    exit
}

$UnacceptedCSV = "$env:temp\UnacceptedUsers.csv"
$ExpiredUsersCSV = "$env:temp\ExpiredUsers.csv"

$UsersToDelete = @()
$DeletedUsers = @()
$ErroredUsers = @()
$UnacceptedUsers = @()
$DeletedUnacceptedUsers = @()
$DateLimit = (Get-Date).AddYears(-1)

## Fetch all users older than 1 year, and has a last sign in greater than one year
foreach ($user in $GuestUsers) {
    if ($user.CreatedDateTime -lt (Get-Date).AddMonths(-6) -and $user.ExternalUserState -like 'PendingAcceptance') {
        $UnacceptedUsers += $user
        continue
    }

    if ($user.CreatedDateTime -gt (Get-Date).AddYears(-1)) {
        continue
    }
    if ($user.SignInActivity.LastSuccessfulSignInDateTime) {
        if ($user.SignInActivity.LastSuccessfulSignInDateTime -gt $DateLimit) {
            continue
        }
        $UsersToDelete += $user
    } elseif ($user.SignInActivity.LastSignInDateTime) {
        if ($user.SignInActivity.LastSignInDateTime -gt $DateLimit) {
            continue
        }
        $UsersToDelete += $user
    } else {
        $UsersToDelete += $user
    }
}

if ($UsersToDelete.Count -gt 0 -or $UnacceptedUsers.Count -gt 0) {
    Write-Warning "Found $($UsersToDelete.Count + $UnacceptedUsers.Count) guest users eligble for deletion..."

    foreach ($user in $UsersToDelete) {
        Write-Warning "Deleting guest user $($user.UserPrincipalName) with ID $($user.Id)."
        try {
            Remove-MgUser -UserId $user.Id -ErrorAction Stop
            Write-Output "Deleted guest user $($user.UserPrincipalName) with ID $($user.Id)."
            $DeletedUsers += $user
        } catch {
            Write-Error "Could not delete guest user $($user.UserPrincipalName) with ID $($user.Id)"
            $ErroredUsers += $user
        }
    }

    foreach ($user in $UnacceptedUsers) {
        Write-Warning "Deleting guest user $($user.UserPrincipalName) with ID $($user.Id)."
        try {
            Remove-MgUser -UserId $user.Id -ErrorAction Stop
            Write-Output "Deleted guest user $($user.UserPrincipalName) with ID $($user.Id)."
            $DeletedUnacceptedUsers += $user
        } catch {
            Write-Error "Could not delete guest user $($user.UserPrincipalName) with ID $($user.Id)"
            $ErroredUsers += $user
        }
    }

$UnacceptedUsers | select DisplayName,Mail -ExpandProperty SignInActivity | select DisplayName,Mail | Export-Csv $UnacceptedCSV -NoTypeInformation
$UsersToDelete | select DisplayName,Mail -ExpandProperty SignInActivity | select DisplayName,Mail,LastSignInDateTime,LastSuccessfulSignInDateTime | Export-Csv $ExpiredUsersCSV -NoTypeInformation

    ## Send email to IT
    $emailTemplate = "Hi there,

This is just a quick email to let you know that Entra Guest User Purge has been completed.

Total Number of Guest Users: $($GuestUsers.Count)
Total Number of Expired Guest Users: $($UsersToDelete.Count)
Total Number of Guests Users in a Pending State: $($UnacceptedUsers.Count)
Successfully Deleted Expired Guest Users: $($DeletedUsers.Count)
Successfully Deleted Pending Guest Users: $($DeletedUnacceptedUsers.Count)
Failed Users: $($ErroredUsers.Count)

You have 30 days to restore a Guest User from the Entra Admin Center before they're permanently deleted.

The below guest users have been deleted because they haven't authenticated in a year:
$($DeletedUsers.Mail | Out-String)

The below guest users have been deleted because they haven't accepted their guest invitation within 6 months:
$($DeletedUnacceptedUsers.Mail | Out-String)

The below guest users failed to be deleted (intervene if needed):
$($ErroredUsers.Mail | Out-String)

Kind regards,
IT Automation
"

    $params = @{
        Message = @{
            Subject = "Monthly Entra Guest User Purge Report"
            Body = @{
                ContentType = "Text"
                Content = $emailTemplate
            }
            ToRecipients = @(
                @{
                    EmailAddress = @{
                        Address = "{REPLACE WITH RECIPIENT EMAIL ADDRESS}"
                    }
                }
            )
            Attachments = @(
                @{
                    "@odata.type" = "#microsoft.graph.fileAttachment"
                    Name = "UnacceptedUsers.csv"
                    ContentType = "text/csv"
                    ContentBytes = [Convert]::ToBase64String([IO.File]::ReadAllBytes($UnacceptedCSV))
                }
                @{
                    "@odata.type" = "#microsoft.graph.fileAttachment"
                    Name = "ExpiredUsers.csv"
                    ContentType = "text/csv"
                    ContentBytes = [Convert]::ToBase64String([IO.File]::ReadAllBytes($ExpiredUsersCSV))
                }
            )
        }
        SaveToSentItems = "true"
    }

    # A UPN can also be used as -UserId.
    Send-MgUserMail -UserId "{REPLACE WITH SENDER ADDRESS}" -BodyParameter $params
}

With this script, we schedule it within Azure Automation to run on the first of every month at 1am. That way, guest users are purged and cleared every month.

And with that, every month guest users are deleted from our tenant. Standard Entra ID deletion policies apply, and if a mistake has been made, you have thirty days to restore the guest user(s) from the 'Deleted Users' blade of the Entra ID portal.

Until next time 👋