Microsoft Slow to Add Expiration Dates for Guest Accounts, So Let’s See What’s Possible with PowerShell
I have long endorsed the idea of reviewing guest accounts annually to remove obsolete accounts to clean up the directory. Microsoft’s usual response is that organizations should use Azure AD account reviews, a feature that works but requires Azure AD Premium P2 licenses.
Many have pointed out that it would be good if Azure AD supported guest account expiration (the ability to assign expiration dates to guest accounts), as it is possible for on-premises Active Directory accounts. Microsoft’s community suggestions portal has a popular request going back six years. The last response from engineering (October 2022) noted that the suggestion is “something we are considering, but there is no timeline yet.” The topic is also discussed in the Microsoft Technical Community with no resolution.
Where there’s a will, there’s a way. At least, where there’s PowerShell, we can make something happen. The outline of a solution might go like this:
- The organization sets a default expiration period for guest accounts.
- A background job finds new guest accounts and stamps them with an expiration date calculated by adding the expiration period to the account creation date.
- A background job checks guest accounts weekly to find accounts due to expire within the next fortnight.
- If the guest account is active (Azure AD sign-in records exist), the expiration date is extended by six months. You could add other checks for activity here. For instance, the script could check that the account is a member of at least one Microsoft 365 group (team).
- If the guest account is inactive, it is marked for deletion, and its details are included in a report sent to administrators. If an administrator takes no action, a subsequent run of the background job removes the account automatically after it expires.
This is a simple framework for the automatic expiration of guest accounts. You could add extra features as you like. Let’s discuss how to implement the different pieces of the puzzle.
Cybersecurity Risk Management for Active Directory
Discover how to prevent and recover from AD attacks through these Cybersecurity Risk Management Solutions.
Setting the Guest Expiration Period
First, we need to set the guest expiration period. I decided to use Azure Key Vault for this purpose because I didn’t want to hard-code the value in scripts. This article explains how to use Azure Key Vault with PowerShell. After populating the value in the vault, I can retrieve it at any time with a command like:
$GuestExpirationPeriod = Get-AzKeyVaultSecret -VaultName "Office365ITPros" -Name "GuestExpirationPeriod" -AsPlainText
Stamping New Guest Accounts with an Expiration Period
One of the fifteen extension attributes available for Azure AD accounts seems like a good place to store expiration dates. In this example, I use custom attribute 15.
The goal for the first job is to ensure that every guest account has an expiration date. To do this, the job:
- Searches for guest accounts that do not have an expiration date.
- Updates the guest accounts found by the search with an expiration date.
Searching for objects that have a null value in a property is usually easy with PowerShell because the special $Null variable exists for that purpose. However, the Microsoft Graph PowerShell SDK cmdlets do not handle the $Null variable properly (one of several foibles that the SDK has). The solution is to use a complex query to look for objects where the property is greater than or equal to null (not $Null). This command works and finds guest accounts without a value in custom attribute 15:
[array]$Guests = Get-MgUser -Filter "onPremisesExtensionAttributes/extensionAttribute15 eq null and userType eq 'Guest'" -CountVariable CountVar -ConsistencyLevel eventual -All -Property id, createdDateTime, displayName
Writing an expiration date into the guest accounts is straightforward. The first time the job runs within a tenant, it will process all guest accounts. Some of the accounts might be quite old, so to make sure that the accounts receive a reasonable expiration date, the script makes sure that the calculated expiration date is at least 30 days in the future.
[datetime]$Now = Get-Date ForEach ($Guest in $Guests) { Write-Host ("Updating guest account {0}" -f $Guest.displayName) [datetime]$CreatedDate = $Guest.createdDateTime [datetime]$ExpirationDate = $CreatedDate.AddDays($GuestExpirationPeriod) If ($ExpirationDate -le $Now) { $ExpirationDate = $Now.AddDays(30) } Update-MgUser -UserId $Guest.Id -OnPremisesExtensionAttributes @{'extensionAttribute15' = (Get-Date $ExpirationDate -format s)} }
To check the assigned expiration dates, this command sorts the guest accounts in date order to report the user name and expiration date.
[array]$Guests = Get-MgUser -Filter "userType eq 'Guest'" -All $Guests | Sort-Object {$_.OnPremisesExtensionAttributes.extensionattribute15 -as [datetime]} | ft displayname,@{e={Get-Date($_.OnPremisesExtensionAttributes.extensionattribute15) -format g}; n="Expiration Date"}
Finding Expired Guests
The main guest expiration job serves two purposes:
- Find and remove guest accounts marked as expired and delete any that exceed their expiration date by seven days or more.
- Find the set of guest accounts due to expire within the next 14 days.
Essentially, the idea is to remove expired accounts and then find and mark the set of accounts that are candidates for removal the next time the job runs. To mark an account as being ready for deletion, the script writes “Expired” into custom attribute 14. To ensure administrators have time to review potential deletions, the script only deletes accounts if they are marked as expired and their expiration date occurred more than seven days ago. The script does the following:
- Run the Get-MgUser cmdlet to find all guest accounts and then loop through the set of accounts.
- Check if the account has “Expired” in custom attribute 14. If it does, the script checks the account’s expiration date to see if the account reached its expiration date more than seven days ago. If this is true, the script deletes the account. Deleted guest accounts remain in the Azure AD recycle bin for 30 days. Administrators can restore accounts as necessary using the Entra admin center or PowerShell.
- Check the remaining accounts to find the set that has reached their expiration date.
- Check each expired account for its latest sign-in activity. If the last sign-in was less than 30 days ago, assume the account is active and extend its expiration date by 183 days (six months). If the last sign-in is older than 30 days, update custom attribute 14 with “Expired” to make the account a candidate for removal the next time the job runs.
- Emails a report of the actions taken to tenant administrators.
Using Azure Automation
Azure Automation is a good choice for these kinds of background processing jobs. In previous articles, we covered the basics of using a managed identity with the Microsoft Graph PowerShell SDK (for Azure AD account operations and to send email). Both of the background jobs required to make this scheme work can use an Azure Automation schedule to make sure that they run as expected:
- Daily: background job 1 (find new guest accounts and stamp an expiration date on them). The code to find and stamp new accounts is described above.
- Weekly: background job 2 (find and mark expired guest accounts and purge expired accounts more than seven days past their expiration date). My example runbook to demonstrate the principles of these operations is available from GitHub. Remember that this code is not production quality. It works but needs enhancement to comply with your tenant’s coding standards.
Figure 1 shows an example of the email generated by the Azure Automation runbook.
Effect of Removing Guest Accounts
Remember that when Azure AD removes guest accounts, the owners of those accounts cease their membership of Microsoft 365 Groups and Teams within the tenant. This means that they immediately lose access to resources owned by those groups. In addition, any sharing links created for the accounts become invalid.
If you decide to replace mail contacts with guest accounts, as I have recently discussed, it’s unlikely that these accounts will have any sign-in activity and will therefore expire quickly. In this scenario, you could use another custom attribute to exclude these accounts from expiration processing.
Come on Microsoft
This exercise proves that there’s no real rocket science involved in the introduction of expiration dates for Azure AD guest accounts. Given Microsoft’s slowness at responding to customer requests for the functionality, it’s likely that guest expiration is low down the priority list for engineering investment. That’s a pity, but at least a DIY version can be built if you really want to expire and remove guest accounts after a period.
Hi,
Nice article.
I have one question. All your guest users are marked to expire in 365 days for the example.
For some users it could be good, for some not.
I see a problem when you have to invite some external user for a project work, it could be much more than 365 days.
So, after that date guest user will be disabled/removed. But even if we recover account it will be disabled again during the next script run as there is no procedure to prolong that expiration date, only manual intervention.
Thanks!
The code is written to illustrate a principle. After that, it’s up to you to change/develop the code to do what makes sense in your organization.
Cool workaround!
The issue that we mostly run into in terms of a Contractor is when they need to work on something specific that requires licensing. For example, PWA or Power BI Pro… Guest accounts can’t licensed from what I could see on my tenant. (Which is logical)
Guest accounts can be licensed. Have you tried assigning a license using the Microsoft 365 admin center or PowerShell?
We implemented something similar, but ran into some “interesting” problems in practice:
– Currently, there seems to be no reliable way to check for non-interactive sign-ins (MSGraph doesn’t return this info properly). So unless there’s a conditional access policy that forces guest accounts to sign-in again periodically (and in shorter time intervals than the expiration period), there’s gonna be a significant rate of false-positives.
– We tried querying the audit log instead, but that turned out to not always report all activity (pretty shocking, if you ask me).
There’s always edge cases to consider, too – especially if AzureAD guest accounts are used in M365 tenants, but not to access M365 workloads. A client at my old company had contracted an external developer, who then used an AzureAD guest account in their tenant to access Azure Devops in the same tenant. He got blocked each and every time, no matter how often he logged in.
What events were you looking for in the event log? There have been some issues with searching the log in the past (and not finding events), but I think these problems have been squashed. At least, I haven’t heard of any lately (or encountered any).
There’s no doubt that Microsoft has work to do on Azure AD B2B Collaboration if they want people to use guest accounts more broadly. My hope is that by highlighting deficiencies, Microsoft might do something to fix the gaps.
As to developers using a guest account for DevOps, that sounds like a bridge too far…
Hi Tony,
Great article (as usual). Microsoft should have account expiry for all accounts.
I was interested in having a look your script but noticed there isn’t a Github link for background job 2. Can you please provide this?
Thanks
The GitHub link is there… Can you check again? (don’t know what happened).