Service Principal is Latest Critical Azure AD Object to Support Recovery
According to message center notification MC344406 (18 March), in early April Microsoft plans to roll-out the capability of recovering deleted service principal objects. Service principals are critical parts of registered Azure AD apps, such as the apps used to execute Microsoft Graph API queries with PowerShell. They’re also used in Azure Automation accounts, the Microsoft Graph PowerShell SDK, and managed identities. In all cases, the service principals hold the permissions needed for an app or account to operate. The worldwide roll-out to all tenants should complete by late May.
When the capability is available, any time an administrator deletes a service principal (for instance, because a registered app is no longer needed) using the Azure AD admin center, PowerShell (using Remove-AzureADServicePrincipal), or the Microsoft Graph API, Azure AD will place the service principal into a soft-deleted state. This already happens today for user, group, device, and application objects.
Deleted Azure AD objects stay in the deleted items container for 30 days. When the retention period elapses (extending to maybe a few days afterwards), Azure AD proceeds to permanently delete the object.
During the retention period, administrators can restore an object, which makes it easy to recover if someone deletes an important item by accident. For now, the list deleted items API doesn’t support service principals, but it will after the roll-out. Figure 1 shows user objects in the deleted items container as viewed through the Graph Explorer.
Figure 1: Viewing deleted Azure AD user accounts via the Graph Explorer
Using Old Azure AD Cmdlets
MC344406 features two cmdlets from the Azure AD Preview module:
Of course, apart from the licensing management cmdlets, the rest of the Azure AD cmdlets will continue to work after retirement, which makes it perfectly acceptable to specify the cmdlets now, especially if replacements in the Microsoft Graph PowerShell SDK are unavailable.
Using Microsoft Graph PowerShell SDK Cmdlets with Deleted Azure AD User Accounts
The Microsoft Graph PowerShell SDK can be difficult to navigate. It’s composed of 38 separate sub-modules. Although cmdlets are gathered logically, it can still be hard to find the right cmdlet to do a job. As you’d expect, the current version (1.9.3) doesn’t appear to include cmdlets to handle soft-deleted service principal objects. For now, we can see how to perform common administrative actions with user accounts as a guide to what should be available for service principals.
With that in mind, here are the steps to soft-delete user accounts, list the accounts in the deleted items container, and hard-delete (permanently remove) an account.
Soft-Delete an Azure AD User Account
To soft-delete an Azure AD account, run the Remove-MgUser and pass the object identifier or user principal name of the account to delete. The cmdlet does not prompt for a confirmation and deletes the account immediately:
During the 30-day retention period in the deleted items container, you can recover the account from the Azure AD admin center or by running the Restore-MgUser cmdlet. Before we can run Restore-MgUser, we need to know the object identifiers of the objects in the deleted items container. This code:
Uses the Get-MgDirectoryDeletedItem cmdlet to fetch the list of deleted user accounts (instead of passing an object identifier, we use the special microsoft.graph.user term to tell Azure AD that we want a list of user accounts). The Property parameter can be ‘*’ to return all properties of the deleted objects, but in this case, I’ve chosen to limit the set of properties to those that I want to use.
Loops through the data returned by Azure AD to extract the properties we want to use. The different behaviour of the Azure AD cmdlets and the Microsoft Graph PowerShell SDK cmdlets is an example of why tenants need to plan the upgrade and testing of scripts which use the Azure AD cmdlets.
Lists the soft-deleted user accounts.
$DeletedItems = Get-MgDirectoryDeletedItem -DirectoryObjectId microsoft.graph.user -Property 'Id','displayname', 'deletedDateTime', 'userType'
[int]$n = $DeletedItems.AdditionalProperties['value'].count
If ($n -eq 0) {
Write-Host "No deleted accounts found - exiting"; break }
$n = $n-1
[int]$i
$Report = [System.Collections.Generic.List[Object]]::new()
For ($i = 0; $i -le $n; $i++) {
$Command = "$" + "DeletedItems.AdditionalProperties" + "['value'][$i]"
$Item = Invoke-Expression $Command
$DeletedDate = Get-Date($Item['deletedDateTime'])
$ReportLine = [PSCustomObject][Ordered]@{
UserId = $Item['id']
Name = $Item['displayName']
Deleted = $DeletedDate
"Days Since Deletion" = (New-TimeSpan $DeletedDate).Days
Type = $Item['userType'] }
$Report.Add($ReportLine)
}
$Report | Sort {$_.Deleted -as [datetime]} | Format-Table UserId, Name, Deleted, "Days Since Deletion", Type -AutoSize
UserId Name Deleted Days Since Deletion Type
------ ---- ------- ------------------- ----
92cef396-1bd3-4296-b06f-786e2ee09077 The Maestro of Office 365 19/02/2022 17:36:44 31 Guest
c6133be4-71d4-47c4-b109-e37c0c93f8d3 Oisin Johnston 26/02/2022 18:13:26 24 Member
2e9f1189-d2d9-4301-be57-2d66f3df6bb1 Jessica Chen (Marketing) 04/03/2022 11:52:48 18 Member
8cd64635-bce6-4af0-8e64-3bebe354e9a4 Alex Redmond 05/03/2022 17:36:45 17 Member
0f16501c-8302-468a-99a6-78c22b0903d2 Jennifer Caroline 18/03/2022 21:33:13 3 Member
3a6116ab-0116-490e-bd60-7e0cd9f36c9d Sue Ricketts (Operations) 20/03/2022 19:53:29 2 Member
4a25ccf0-17df-42cf-beeb-4fd449531b47 Stephen Rice 22/03/2022 19:30:06 0 Guest
To restore a soft-deleted Azure AD account, run the Restore-MgDirectoryDeletedItem cmdlet and pass the account’s identifier. After restoring the account, remember to assign licenses to allow the account to access Microsoft 365 services.
To remove a soft-deleted directory object, run the Remove-MgDirectoryDeletedItem cmdlet and pass the object identifier. Like Remove-MgUser, the cmdlet doesn’t ask for confirmation and permanent deletion happens immediately.
We’re in a time of transition now as Microsoft does its best to retire the Azure AD modules and build the capabilities (and hopefully the documentation) of the Microsoft Graph PowerShell SDK. In the intervening period, any time you see an example using Azure AD cmdlets, try to convert it to use the SDK. It’s a great way to learn.
Keep up to date with developments like the Microsoft Graph PowerShell SDK by subscribing to the Office 365 for IT Pros eBook. Our monthly updates make sure that our subscribers understand the most important changes happening across Office 365.
10 Replies to “Delete and Restore Azure AD User Accounts with the Microsoft Graph PowerShell SDK”
Hello, I tried the script using the Graph Powershell SDK but it doesn’t return the deleted groups and users. The special microsoft.graph.user / microsoft.graph.group terms don’t seem to work for me. The old Azure AD cmdlets, on the contrary, return all the soft-deleted groups/users. Am I doing something wrong? I have all the required scopes so I don’t think it’s a permission problem.
Any suggestions are welcome!
What version of the SDK are you using? I used 1.9.6.
The code works for me. You could also try an Invoke-MgGraphRequest command to run the Graph query to return the soft-deleted users. I find the Graph X-Ray tool useful here because it shows the query to use. https://office365itpros.com/2022/05/23/graph-x-ray-powershell/
Hello,
I have done some searching and I found this: https://github.com/microsoftgraph/msgraph-sdk-powershell/issues/294
Apparently it’s a known issue and I was able to get the soft-deleted users/groups using “thuld” and “vandreytrindade” suggestions.
I am posting the link with the temporary workaround here for other users who might have the same issue.
I confirm that also “Invoke-GraphRequest” works ok.
Many thanks for your time.
Loading...
My apologies, your script works fine and you are also using the “AdditionalProperties[‘value’]” property. I was running only the
“Get-MgDirectoryDeletedItem -DirectoryObjectId microsoft.graph.user -Property ‘*'” expecting to see a list of the soft-deleted users/groups but always got no output.
Sorry again for the confusion!
Hello,
I would like to add that like many other Graph SDK command, the “Get-MgDirectoryDeletedItem” returns only 100 results and when you use the “-DirectoryObjectId” parameter you don’t have the option to get all the results. We have recently deleted 250 users and I get only 100 when running the report.
Is there a way to use your script without having the “100 results” limitation? Or should I still use the old cmdlets from the MSOnline/AzureAD modules?
Many thanks in advance for your time!
You’re hitting a thing called pagination, where Graph API requests return a limited amount of data to the initial request to prevent requests asking for thousands of items without good reason.
The Get-MgDirectoryDeletedItem cmdlet supports an All parameter to return all matching items. I have played with it and can’t make it work, but I will keep on trying. In the interim, you can always revert to the API call and do:
$Uri = “https://graph.microsoft.com/beta/directory/deletedItems/microsoft.graph.user”
[array]Data = Invoke-MgGraphRequest -Method Get -Uri $Uri
Microsoft acknowledges a problem in this area. They’re working to fix it. In the interim, use the Graph method I gave you and all will be well.
Loading...
You just saved my butt with that tip…thanks!
Loading...
Thank you for your input, I also tried to use the “All” parameter but to no avail. And when I try the “Search” and “Filter” parameters, I got the error message saying
“Request with $search query parameter only works through MSGraph with a special request header: ‘ConsistencyLevel: eventual'”.
When you use the “Get-MgUser” cmdlet you have the parameter “-ConsistencyLevel eventual” but you don’t have that for the “Get-MgDirectoryDeletedItem” cmdlet, which is a bit frustrating (https://github.com/microsoftgraph/msgraph-sdk-powershell/issues/513).
I will try the API call and go through all the odata.nextlink.
Many thanks again
{"id":null,"mode":"button","open_style":"in_modal","currency_code":"EUR","currency_symbol":"\u20ac","currency_type":"decimal","blank_flag_url":"https:\/\/office365itpros.com\/wp-content\/plugins\/tip-jar-wp\/\/assets\/images\/flags\/blank.gif","flag_sprite_url":"https:\/\/office365itpros.com\/wp-content\/plugins\/tip-jar-wp\/\/assets\/images\/flags\/flags.png","default_amount":100,"top_media_type":"featured_image","featured_image_url":"https:\/\/office365itpros.com\/wp-content\/uploads\/2022\/11\/cover-141x200.jpg","featured_embed":"","header_media":null,"file_download_attachment_data":null,"recurring_options_enabled":true,"recurring_options":{"never":{"selected":true,"after_output":"One time only"},"weekly":{"selected":false,"after_output":"Every week"},"monthly":{"selected":false,"after_output":"Every month"},"yearly":{"selected":false,"after_output":"Every year"}},"strings":{"current_user_email":"","current_user_name":"","link_text":"Virtual Tip Jar","complete_payment_button_error_text":"Check info and try again","payment_verb":"Pay","payment_request_label":"Office 365 for IT Pros","form_has_an_error":"Please check and fix the errors above","general_server_error":"Something isn't working right at the moment. Please try again.","form_title":"Office 365 for IT Pros","form_subtitle":null,"currency_search_text":"Country or Currency here","other_payment_option":"Other payment option","manage_payments_button_text":"Manage your payments","thank_you_message":"Thank you for supporting the work of Office 365 for IT Pros!","payment_confirmation_title":"Office 365 for IT Pros","receipt_title":"Your Receipt","print_receipt":"Print Receipt","email_receipt":"Email Receipt","email_receipt_sending":"Sending receipt...","email_receipt_success":"Email receipt successfully sent","email_receipt_failed":"Email receipt failed to send. Please try again.","receipt_payee":"Paid to","receipt_statement_descriptor":"This will show up on your statement as","receipt_date":"Date","receipt_transaction_id":"Transaction ID","receipt_transaction_amount":"Amount","refund_payer":"Refund from","login":"Log in to manage your payments","manage_payments":"Manage Payments","transactions_title":"Your Transactions","transaction_title":"Transaction Receipt","transaction_period":"Plan Period","arrangements_title":"Your Plans","arrangement_title":"Manage Plan","arrangement_details":"Plan Details","arrangement_id_title":"Plan ID","arrangement_payment_method_title":"Payment Method","arrangement_amount_title":"Plan Amount","arrangement_renewal_title":"Next renewal date","arrangement_action_cancel":"Cancel Plan","arrangement_action_cant_cancel":"Cancelling is currently not available.","arrangement_action_cancel_double":"Are you sure you'd like to cancel?","arrangement_cancelling":"Cancelling Plan...","arrangement_cancelled":"Plan Cancelled","arrangement_failed_to_cancel":"Failed to cancel plan","back_to_plans":"\u2190 Back to Plans","update_payment_method_verb":"Update","sca_auth_description":"Your have a pending renewal payment which requires authorization.","sca_auth_verb":"Authorize renewal payment","sca_authing_verb":"Authorizing payment","sca_authed_verb":"Payment successfully authorized!","sca_auth_failed":"Unable to authorize! Please try again.","login_button_text":"Log in","login_form_has_an_error":"Please check and fix the errors above","uppercase_search":"Search","lowercase_search":"search","uppercase_page":"Page","lowercase_page":"page","uppercase_items":"Items","lowercase_items":"items","uppercase_per":"Per","lowercase_per":"per","uppercase_of":"Of","lowercase_of":"of","back":"Back to plans","zip_code_placeholder":"Zip\/Postal Code","download_file_button_text":"Download File","input_field_instructions":{"tip_amount":{"placeholder_text":"How much would you like to tip?","initial":{"instruction_type":"normal","instruction_message":"How much would you like to tip? Choose any currency."},"empty":{"instruction_type":"error","instruction_message":"How much would you like to tip? Choose any currency."},"invalid_curency":{"instruction_type":"error","instruction_message":"Please choose a valid currency."}},"recurring":{"placeholder_text":"Recurring","initial":{"instruction_type":"normal","instruction_message":"How often would you like to give this?"},"success":{"instruction_type":"success","instruction_message":"How often would you like to give this?"},"empty":{"instruction_type":"error","instruction_message":"How often would you like to give this?"}},"name":{"placeholder_text":"Name on Credit Card","initial":{"instruction_type":"normal","instruction_message":"Enter the name on your card."},"success":{"instruction_type":"success","instruction_message":"Enter the name on your card."},"empty":{"instruction_type":"error","instruction_message":"Please enter the name on your card."}},"privacy_policy":{"terms_title":"Terms and conditions","terms_body":null,"terms_show_text":"View Terms","terms_hide_text":"Hide Terms","initial":{"instruction_type":"normal","instruction_message":"I agree to the terms."},"unchecked":{"instruction_type":"error","instruction_message":"Please agree to the terms."},"checked":{"instruction_type":"success","instruction_message":"I agree to the terms."}},"email":{"placeholder_text":"Your email address","initial":{"instruction_type":"normal","instruction_message":"Enter your email address"},"success":{"instruction_type":"success","instruction_message":"Enter your email address"},"blank":{"instruction_type":"error","instruction_message":"Enter your email address"},"not_an_email_address":{"instruction_type":"error","instruction_message":"Make sure you have entered a valid email address"}},"note_with_tip":{"placeholder_text":"Your note here...","initial":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"empty":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"not_empty_initial":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"saving":{"instruction_type":"normal","instruction_message":"Saving note..."},"success":{"instruction_type":"success","instruction_message":"Note successfully saved!"},"error":{"instruction_type":"error","instruction_message":"Unable to save note note at this time. Please try again."}},"email_for_login_code":{"placeholder_text":"Your email address","initial":{"instruction_type":"normal","instruction_message":"Enter your email to log in."},"success":{"instruction_type":"success","instruction_message":"Enter your email to log in."},"blank":{"instruction_type":"error","instruction_message":"Enter your email to log in."},"empty":{"instruction_type":"error","instruction_message":"Enter your email to log in."}},"login_code":{"initial":{"instruction_type":"normal","instruction_message":"Check your email and enter the login code."},"success":{"instruction_type":"success","instruction_message":"Check your email and enter the login code."},"blank":{"instruction_type":"error","instruction_message":"Check your email and enter the login code."},"empty":{"instruction_type":"error","instruction_message":"Check your email and enter the login code."}},"stripe_all_in_one":{"initial":{"instruction_type":"normal","instruction_message":"Enter your credit card details here."},"empty":{"instruction_type":"error","instruction_message":"Enter your credit card details here."},"success":{"instruction_type":"normal","instruction_message":"Enter your credit card details here."},"invalid_number":{"instruction_type":"error","instruction_message":"The card number is not a valid credit card number."},"invalid_expiry_month":{"instruction_type":"error","instruction_message":"The card's expiration month is invalid."},"invalid_expiry_year":{"instruction_type":"error","instruction_message":"The card's expiration year is invalid."},"invalid_cvc":{"instruction_type":"error","instruction_message":"The card's security code is invalid."},"incorrect_number":{"instruction_type":"error","instruction_message":"The card number is incorrect."},"incomplete_number":{"instruction_type":"error","instruction_message":"The card number is incomplete."},"incomplete_cvc":{"instruction_type":"error","instruction_message":"The card's security code is incomplete."},"incomplete_expiry":{"instruction_type":"error","instruction_message":"The card's expiration date is incomplete."},"incomplete_zip":{"instruction_type":"error","instruction_message":"The card's zip code is incomplete."},"expired_card":{"instruction_type":"error","instruction_message":"The card has expired."},"incorrect_cvc":{"instruction_type":"error","instruction_message":"The card's security code is incorrect."},"incorrect_zip":{"instruction_type":"error","instruction_message":"The card's zip code failed validation."},"invalid_expiry_year_past":{"instruction_type":"error","instruction_message":"The card's expiration year is in the past"},"card_declined":{"instruction_type":"error","instruction_message":"The card was declined."},"missing":{"instruction_type":"error","instruction_message":"There is no card on a customer that is being charged."},"processing_error":{"instruction_type":"error","instruction_message":"An error occurred while processing the card."},"invalid_request_error":{"instruction_type":"error","instruction_message":"Unable to process this payment, please try again or use alternative method."},"invalid_sofort_country":{"instruction_type":"error","instruction_message":"The billing country is not accepted by SOFORT. Please try another country."}}}},"fetched_oembed_html":false}
Hello, I tried the script using the Graph Powershell SDK but it doesn’t return the deleted groups and users. The special microsoft.graph.user / microsoft.graph.group terms don’t seem to work for me. The old Azure AD cmdlets, on the contrary, return all the soft-deleted groups/users. Am I doing something wrong? I have all the required scopes so I don’t think it’s a permission problem.
Any suggestions are welcome!
What version of the SDK are you using? I used 1.9.6.
The code works for me. You could also try an Invoke-MgGraphRequest command to run the Graph query to return the soft-deleted users. I find the Graph X-Ray tool useful here because it shows the query to use. https://office365itpros.com/2022/05/23/graph-x-ray-powershell/
Hello,
I have done some searching and I found this: https://github.com/microsoftgraph/msgraph-sdk-powershell/issues/294
Apparently it’s a known issue and I was able to get the soft-deleted users/groups using “thuld” and “vandreytrindade” suggestions.
I am posting the link with the temporary workaround here for other users who might have the same issue.
I confirm that also “Invoke-GraphRequest” works ok.
Many thanks for your time.
My apologies, your script works fine and you are also using the “AdditionalProperties[‘value’]” property. I was running only the
“Get-MgDirectoryDeletedItem -DirectoryObjectId microsoft.graph.user -Property ‘*'” expecting to see a list of the soft-deleted users/groups but always got no output.
Sorry again for the confusion!
Hello,
I would like to add that like many other Graph SDK command, the “Get-MgDirectoryDeletedItem” returns only 100 results and when you use the “-DirectoryObjectId” parameter you don’t have the option to get all the results. We have recently deleted 250 users and I get only 100 when running the report.
Is there a way to use your script without having the “100 results” limitation? Or should I still use the old cmdlets from the MSOnline/AzureAD modules?
Many thanks in advance for your time!
You’re hitting a thing called pagination, where Graph API requests return a limited amount of data to the initial request to prevent requests asking for thousands of items without good reason.
The Get-MgDirectoryDeletedItem cmdlet supports an All parameter to return all matching items. I have played with it and can’t make it work, but I will keep on trying. In the interim, you can always revert to the API call and do:
$Uri = “https://graph.microsoft.com/beta/directory/deletedItems/microsoft.graph.user”
[array]Data = Invoke-MgGraphRequest -Method Get -Uri $Uri
And check the odata.nextlink returned. If one exists, you know more data is available and you can retrieve it (see https://office365itpros.com/2020/02/17/microsoft-graph-queries-powershell/ for an explanation).
Microsoft acknowledges a problem in this area. They’re working to fix it. In the interim, use the Graph method I gave you and all will be well.
You just saved my butt with that tip…thanks!
Thank you for your input, I also tried to use the “All” parameter but to no avail. And when I try the “Search” and “Filter” parameters, I got the error message saying
“Request with $search query parameter only works through MSGraph with a special request header: ‘ConsistencyLevel: eventual'”.
When you use the “Get-MgUser” cmdlet you have the parameter “-ConsistencyLevel eventual” but you don’t have that for the “Get-MgDirectoryDeletedItem” cmdlet, which is a bit frustrating (https://github.com/microsoftgraph/msgraph-sdk-powershell/issues/513).
I will try the API call and go through all the odata.nextlink.
Many thanks again