SharePoint External Users From Guest Members and Sharing
A SharePoint external user is someone who doesn’t have an account in your tenant. Because of the influence of Teams, most SharePoint Online external users are guest accounts, created when external people join the membership of Microsoft 365 Groups (teams). If the organization uses the SharePoint Online integration with Azure AD B2B collaboration, SharePoint also creates guest accounts when people share files or folders with external people.
As discussed in this article, it’s reasonably easy to generate a report of the membership of all Microsoft 365 groups in a tenant. The report includes guest accounts and can be used to figure out if guests from the wrong places (like competitors) have access to information in your tenant. However, the script that creates the report relies on cmdlets like Get-UnifiedGroupLinks or Graph API requests to return details of group members, and these exclude any mention of guest accounts in a SharePoint site that aren’t members of the group which owns the site.
PnP Samples Repository
This brings me neatly to a script to report external users posted in the PnP Samples repository (a useful place to go for SharePoint-centric code examples). Reflecting that there are usually multiple ways to solve a problem, three versions are available (CLI for Microsoft 365, SharePoint Online PowerShell module, and PnP PowerShell).
Unhappily, there doesn’t appear to be a good way to retrieve the external users for a site using a Graph API request. You can certainly find the set of all guest accounts in a tenant, or the guest accounts for a team/group, but these methods exclude the guest accounts added for sharing purposes.
The Oddness of Get-SPOExternalUser
The lack of a better method is why the scripts found on the internet use the Get-SPOExternalUser cmdlet. It’s an odd cmdlet in some ways.
For example, Get-SPOExternalUser has a PageSize parameter to limit the number of external users returned. The maximum is 50, which means that if more than this number of external users exist for a site, you must continue fetching until all are retrieved (the Position parameter controls the start of the page of users to fetch). You end up with commands like:
And after fetching a page of user data, you must combine it with the other pages to get a complete set. Although pagination is common with Graph API requests, it’s unusual to see it used like this with a cmdlet that could surely benefit from a parameter to fetch all matching items, like:
Get-SPOExternalUser -Limit All
Moving onto the output, here’s an example of the data returned for an external user (guest account):
As far as I can tell, the InvitedBy and LoginName properties are not used. Across all the sites in my tenant, I found one instance of the InvitedName property being populated. In that case, the property held the user principal name of the guest account, and I couldn’t figure out how this happened.
The AcceptedBy property holds the name of the account that accepted the invitation to the site (to share a document or as a guest member). This property is not populated for sites belonging to shared Teams channels. Instead, a LoginName property captures the account used to connect to the channel site.
The WhenCreated property also deserves some comment. It seems like Microsoft reset this value for many accounts at around 18:46 UTC on 5 November 2018. Many accounts across multiple sites have this creation date. It’s an unnatural concentration of external users created at a specific time on that date. I can’t explain it.
Creating a SharePoint External Users Report
Your account needs to hold the Global tenant administrator or SharePoint administrator role to run this script and generate a SharePoint external users report. The steps are straightforward, which is probably why so many versions are available online. This version captures some extra information about the channel-connected sites used by Teams.
Find all sites.
For each site, get its external members.
Create a report file.
Here’s the script:
$Sites = Get-SPOSite -Limit All | Sort-Object Title
$ExternalSPOUsers = [System.Collections.Generic.List[Object]]::new()
#Iterate through each site and retrieve external users
$Counter = 0
ForEach ($Site in $Sites) {
$Counter++
Write-Host ("Checking Site {0}/{1}: {2}" -f $Counter, $Sites.Count, $Site.Title)
[array]$SiteUsers = $Null
$i = 0; $Done = $False
Do {
[array]$SUsers = Get-SPOExternalUser -SiteUrl $Site.Url -PageSize 50 -Position $i
If ($SUsers) {
$i = $i + 50
$SiteUsers = $SiteUsers + $SUsers }
If ($SUsers.Count -lt 50) {$Done = $True}
} While ($Done -eq $False)
ForEach ($User in $SiteUsers) {
$ReportLine = [PSCustomObject] @{
Email = $User.Email
Name = $User.DisplayName
Accepted = $User.AcceptedAs
Created = $User.WhenCreated
SPOUrl = $Site.Url
TeamsChannel = $Site.IsTeamsChannelConnected
ChannelType = $Site.TeamsChannelType
CrossTenant = $User.IsCrossTenant
LoginName = $User.LoginName }
$ExternalSPOUsers.Add($ReportLine) }
} #End ForEach Site
Playing with PSWriteHTML
Now that we have some data to report, I’ll reveal that the real reason for this article is to mention the PSWriteHTML module. The module is maintained by Przemyslaw Klys and its job is to make HTML output easier to generate for PowerShell scripts. The ImportExcel module is another example of a community-created module to help people generate nicer output.
In any case, to create a HTML report, I used these commands:
Figure 1 shows the output, which at first glance looks like a nicer version of the output generated by the Out-GridView cmdlet. The important difference is that you can export the HTML report in different formats, including a nice PDF file.
Figure 1: SharePoint Online external users report
Having different options to share information is a nice thing. If you create reports from PowerShell, consider having a look at the PSWriteHTML module. It might solve some problems for you. After all, it created a prettier SharePoint External Users report for me!
Learn more about how the Office 365 applications really work on an ongoing basis by subscribing to the Office 365 for IT Pros eBook. Our monthly updates keep subscribers informed about what’s important across the Office 365 ecosystem.
You can put the report anywhere you like. By default, I tend to put reports in c:\temp\, but as it’s PowerShell code, you can change the script to put the report where you want it to be.
Dave P – no offense but seems like you don’t know even the basics of Powershell and you are just copying and pasting the code without any knowledge what you are doing
{"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}
Where should be the location of the report file?
You can put the report anywhere you like. By default, I tend to put reports in c:\temp\, but as it’s PowerShell code, you can change the script to put the report where you want it to be.
The script doesn’t output any data. What do we need to add to the script so that it outputs the report to a file?
The post describes how to create HTML output. If you want a CSV file, do this:
$ExternalSPOUsers | Export-CSV -NoTypeInformation SPOUsers.csv
Dave P – no offense but seems like you don’t know even the basics of Powershell and you are just copying and pasting the code without any knowledge what you are doing