A Proliferation of Guest User Accounts Within Office 365 Tenants
Azure Active Directory Guest User Accounts are terrifically useful in terms of allowing people outside your Office 365 tenant to access resources inside the tenant. Applications like Teams, SharePoint Online, Planner, and Outlook Groups use the Azure B2B Collaboration framework to create new guest accounts to share information, whether it’s the membership of a team or access to a shared document or folder.
All is good, except for the proliferation of guest accounts that can quickly accumulate inside a tenant. In short, a regular spring cleaning is needed to ensure that unwanted guests are ejected.
The Problem of Deciding When to Leave
As always, the problem is to decide when a guest account should be removed. Left by themselves, guest accounts will remain in the tenant directory because neither Office 365 nor Azure Active Directory include an automated method to clean up guests past their best-by date. One approach is to review guest accounts that are older than a certain age and look for evidence to indicate if they should be removed.
For example, you might decide that membership of multiple Microsoft 365 groups (aka Office 365 groups) is sufficient reason to keep guest accounts. The logic here is that these memberships give people access to Teams (conversations), Outlook Groups (conversations delivered via email), and Planner (group tasks). Therefore, if we write a script to scan for guest accounts older than x days and then check if these accounts are members of groups, we should have some evidence upon which to base a decision to remove or keep.
PowerShell Script to Highlight Stale Guest Users and their Group Membership
The script below does the following:
- Finds all guest accounts in the tenant.
- Checks each guest to discover its age based on the RefreshTokensValidFromDateTime attribute, which is updated when the guest account is created. The New-TimeSpan cmdlet calculates how many days have elapsed since Azure B2B Collaboration created the guest account in the tenant directory.
- If the guest account is older than 365 days, we look for its group membership by running the Get-Recipient cmdlet to check the account’s distinguished name (an old but still useful X.500 construct) against the membership of Microsoft 365 groups. If you want to check guest accounts older or younger than 365 days, update the $GuestAccountAge variable with the number of days to check for.
- Writes the discovered information out to an array.
- After all guest accounts are processed, writes the contents of the array to a CSV file.
Here’s the code. Remember that you might want to update the code to add error handling and do whatever testing is necessary before you run the script against your production tenant. You need to connect your PowerShell session to Exchange Online and Azure Active Directory before running the script.
# Script to find Guest User Accounts in an Office 365 Tenant that are older than 365 days and the groups they belong to # Find guest accounts $GuestAccountAge = 365 # Value used for guest age comparison. If you want this to be a different value (like 30 days), change this here. $GuestUsers = Get-AzureADUser -All $true -Filter "UserType eq 'Guest'" | Sort DisplayName $Today = (Get-Date); $StaleGuests = 0 $Report = [System.Collections.Generic.List[Object]]::new() # Check each account and find those over 365 days old ForEach ($Guest in $GuestUsers) { $AADAccountAge = ($Guest.RefreshTokensValidFromDateTime | New-TimeSpan).Days If ($AADAccountAge -gt $GuestAccountAge) { $StaleGuests++ Write-Host "Processing" $Guest.DisplayName $i = 0; $GroupNames = $Null # Find what Microsoft 365 Groups the guest belongs to... if any $DN = (Get-Recipient -Identity $Guest.UserPrincipalName).DistinguishedName $GuestGroups = (Get-Recipient -Filter "Members -eq '$Dn'" -RecipientTypeDetails GroupMailbox | Select DisplayName, ExternalDirectoryObjectId) If ($GuestGroups -ne $Null) { ForEach ($G in $GuestGroups) { If ($i -eq 0) { $GroupNames = $G.DisplayName; $i++ } Else {$GroupNames = $GroupNames + "; " + $G.DisplayName } }} $ReportLine = [PSCustomObject]@{ UPN = $Guest.UserPrincipalName Name = $Guest.DisplayName Age = $AADAccountAge Created = $Guest.RefreshTokensValidFromDateTime Groups = $GroupNames DN = $DN} $Report.Add($ReportLine) } } $Report | Sort Name | Export-CSV -NoTypeInformation c:\Temp\OldGuestAccounts.csv
The latest version of the script is available on GitHub.
The output CSV file is shown (somewhat obscured to protect the names of the guilty) in Figure 1. Any guest that isn’t a member of at least one Microsoft 365 group is a potential delete target. As you can see from the created column, it’s easy for old and stale guest accounts to linger on unless you clean them up from time to time.

We have lots of other PowerShell examples of how to manage Azure Active Directory guest users and other Office 365 objects in the Office 365 for IT Pros eBook.
Another approach is to use the Azure AD Access Review process which delegates this responsibility to the Group Owners, see https://docs.microsoft.com/en-us/azure/active-directory/governance/access-reviews-overview
Access reviews are indeed a good way to push responsibility down to group owners. However, they require Azure AD Premium licenses and don’t give tenants an overview of what’s happening with guests across all groups.
I must be missing some simple step. Powershell is dumping this error message for each user found. Can someone help?
Get-Recipient : The term ‘Get-Recipient’ is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included,
verify that the path is correct and try again.
At line:13 char:14
+ $DN = (Get-Recipient -Identity $Guest.UserPrincipalName).Distin …
+ ~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (Get-Recipient:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
PowerShell is telling you that it can’t find the Get-Recipient cmdlet. This is most likely because you haven’t connected to Exchange Online.
Has the RefreshTokensValidFromDateTime always been the creation date I have the feeling that this changed? https://www.undocumented-features.com/2018/06/22/how-to-find-staleish-azure-b2b-guest-accounts I was also interested to see that there is a creation date in the ExtensionProperty (Get-AzureADUser -ObjectId josh@domain.com).ExtensionProperty.createdDateTime. Might be in the previewonly would need to check. The onPremisesDistinguishedName is also great add we can do alot with this value in a multi forest environment.
For guest accounts, I think the two values will be the same, but if you want to use createdDateTime in the script replace the line with:
$AADAccountAge = ((Get-AzureADUser -ObjectId $Guest.UserPrincipalName).ExtensionProperty.createdDateTime | New-TimeSpan).Days
Thanks Tony. I took inspiration from your blog post and script and created a end to end work flow based on the new signin logs data in Graph. I have found that this has broader scope, faster and more reliable than Search-UnifiedAuditLog. I gave you a little credit in the script as you started the conversation 🙂 I hope that’s ok? Let me know. JB https://github.com/JBines/Remove-StaleGuests
Very cool! I love the way people take ideas and develop them in PowerShell…
BTW, one thing that’s good about Search-UnifiedAuditLog is that it delivers real activity data. The sign in information is interesting, but a client can sign in and stay signed in and active for quite a while without their sign in date being updated (because they haven’t been forced to sign in again). So the sign in data is accurate but misleading as an indicator of activity.
Where the session timeout is I’m not sure for the auditlogs is it 8 hours or 6 days ( Keep me signed in) it would be good to test and confirm the results. Sounds like a good topic for a new blog post 🙂
Audit log data should be available within 15 minutes of a SharePoint or Exchange event. It can take longer for events from other workloads…
This works a treat and I’ve now a report of several 100 guests and only two of them are members of groups. It could be the groups they used to be members of no longer exist. We’re not using guest access at the moment – as in inviting new guests to Groups/Teams. We disabled it for a lengthy period of time and during that time guests have been added to the directory… erm? Is this a quirk of Onedrive external sharing when used with ‘share with specific people’ and it adds them to the directory? Thanks for any pointers.
Guest accounts are also created when people share documents from SharePoint Online and OneDrive for Business with external users. You could try using this report https://office365itpros.com/2019/10/22/onedrive-for-business-external-sharing-report/ to check on OneDrive sharing or use the script in our repository https://office365itpros.com/office-365-github-repository/ to track guests added through sharing.
Very helpful thanks. I’ll go check them out