Retirement of SharePoint OTP: Identify, report, and migrate affected guest users

Microsoft is retiring SharePoint One‑Time Passcode (SharePoint OTP) authentication for external sharing in OneDrive and SharePoint beginning in July 2026.
This retirement should streamline external collaboration, unify identity under Microsoft Entra, and bring consistent guest lifecycle management and Conditional Access coverage across Microsoft 365.

In my post, I walk you through what SharePoint OTP looks like compared to Entra Email OTP, how to identify affected users, how to export a SharePoint OTP user report, and how to migrate them to Entra External ID.


Timeline

  • Starting May 2026
    New external sharing invitations and authentication begin using Microsoft Entra B2B Collaboration. External users who previously authenticated via SharePoint OTP continue to access existing specific people links until retirement begins.

  • Starting July 2026
    SharePoint OTP retirement begins. External users without an Entra B2B guest account receive an “access denied” error on previously shared specific people links until a guest account is created.

  • By 31 August 2026
    Retirement should be completed. All external users without an Entra B2B guest account receive an “access denied” error on previously shared specific people links.

How does this affect your organization?

This retirement affects the SharePoint tenant setting EnableAzureADB2BIntegration. This setting will no longer control external sharing behavior.

EnableAzureADB2BIntegration setting will no longer control external sharing behavior
EnableAzureADB2BIntegration setting will no longer control external sharing behavior

I simulated two cases to demonstrate the differences between SharePoint OTP, Entra Email OTP, and the guest users.
I used two temporary email addresses for the simulation, created via SimpleLogin (which I can highly recommend if you want to use separate or temporary email addresses per service).

No guest user was created in Entra
No guest user was created in Entra

When a file or folder is shared with such a user, they see the SharePoint OTP prompt.

SharePoint OTP
SharePoint OTP

Checking the SharePoint User Hidden List with SharePoint Online PowerShell. You can identify these users by the login name format “urn:spo:guest#<Emailaddress>“.

Guest users with SharePoint OTP will lose access starting in July 2026
Guest users with SharePoint OTP will lose access starting in July 2026

Microsoft recommends two options to check:

  1. The email address in site sharing reports. This requires checking each site collection individually. Running a PowerShell script is faster; see my sample below.
  2. Using Purview Audit Logs. The Purview Audit Log can help identify recent logins for these accounts. You can report on the last 180 days, for example, to determine whether an account is still actively used.

Search for the operation EmailAuthOTPAuthenticationSucceeded in Purview Audit Logs to find actual logins from SharePoint OTP guests.

Search for EmailAuthOTPAuthenticationSucceeded in Purview Audit Logs
Search for EmailAuthOTPAuthenticationSucceeded in Purview Audit Logs


2) Email OTP simulation
Instead of SharePoint OTP, you should use Email OTP in Entra External ID.
I enabled the EnableAzureADB2BIntegration setting to activate Email OTP for my second test user. You should also verify that Email OTP is enabled in your tenant. My second test user is [email protected].

Again, I shared a file with the user. Because EnableAzureADB2BIntegration was enabled, SharePoint created a guest user in Entra.

B2B guest user has been created in Entra
B2B guest user has been created in Entra

When a file or folder is shared with such a user, they see the Email OTP prompt provided via Entra External ID.

Email OTP prompt
Email OTP prompt

Checking the SharePoint User Hidden List reveals a clear difference. Entra External ID users appear in the format <Emailaddress>#ext#@<Tenant>.onmicrosoft.com.
In my simulation, I can find two Entra External ID users and two SharePoint OTP users.

Guest users in the SharePoint User Hidden List
Guest users in the SharePoint User Hidden List

There is an additional property to filter on.
Switching to PnP PowerShell to query the SharePoint API with the same filter exposes two properties: IsEmailAuthenticationGuestUser and IsShareByEmailGuestUser.

PowerShell
$Site = Get-PnPSite
$SiteUsers = Invoke-PnPSPRestMethod -Method Get -Url "$($Site.Url)/_api/web/siteusers"
$SiteUsers = $SiteUsers.value | Where-Object { $_.LoginName -like "*urn%3aspo%3aguest*" }
$SiteUsers | select LoginName,IsEmailAuthenticationGuestUser,IsShareByEmailGuestUser | sort LoginName | ft -AutoSize

Check the property IsEmailAuthenticationGuestUser
Check the property IsEmailAuthenticationGuestUser

The different values for IsEmailAuthenticationGuestUser are explained:

IsShareByEmailGuestUser
This property identifies whether a SharePoint user account was provisioned through the ad-hoc share-by-email flow. When someone shares a file or folder with an external email address that has no existing Microsoft or Entra ID identity, SharePoint creates a lightweight guest account and sets this flag to true. It is useful for governance and auditing scenarios where you need to identify accounts that bypassed the formal Entra ID B2B invitation process.

IsEmailAuthenticationGuestUser
This property indicates whether a user authenticated using the one-time passcode (OTP) email verification method rather than signing in with a Microsoft account or Entra ID identity. Users with this flag set to true have no persistent identity in your directory, making them particularly relevant from a security perspective. There is no MFA enforcement, no Conditional Access coverage in the traditional sense, and no account lifecycle to manage through standard Entra ID governance tooling.

Claude AI

You should now be able to identify SharePoint OTP users. I have prepared a PowerShell script to export these guest accounts.


Reporting SharePoint OTP users

You can generate a report of all SharePoint OTP users.
In my setup, I combine this with a Purview Audit Log query.

  1. Run a Purview Audit Log query to retrieve all EmailAuthOTPAuthenticationSucceeded activities from the last 180 days.
  2. Run a report across all site collections (including OneDrive) to find users with the property IsEmailAuthenticationGuestUser. Depending on the number of site collections in your tenant, this can take a while.
  3. Combine the login data from step 1 with the report from step 2, so you can identify which SharePoint OTP guests are still actively signing in.


Run a Purview Auditlog query

The audit log query collects logs from the last 180 days. Skip this step if you don’t need the information in your report.
Note, the sample waits for the query to complete, with a maximum wait time of 60 minutes.

PowerShell
# Find the sample in my GitHub Repo: 
# https://github.com/TobiasAT/PowerShell/blob/main/Samples/Purview/Sample18-PurviewAuditLog-SharePointOTP.ps1

Import-Module Microsoft.Graph.Authentication
Connect-MgGraph -Scopes AuditLogsQuery-SharePoint.Read.All, AuditLogsQuery-OneDrive.Read.All


# Create a new Audit Log Query for SharePoint OTP authentication events in the last 180 days
$StartDate = (Get-Date).AddDays(-180).ToString("yyyy-MM-ddT00:00:00Z")
$EndDate = (Get-Date).ToString("yyyy-MM-ddTHH:mm:ssZ")

$Body = 
@"
{
    "displayName": "MSGraphAuditlogQuery-SPOOTP",
    "filterStartDateTime": "$StartDate",
    "filterEndDateTime": "$EndDate",
    "serviceFilters": ["SharePoint"],    
    "operationFilters": [
        "EmailAuthOTPAuthenticationSucceeded"
    ],
  }
"@

$Url = "https://graph.microsoft.com/beta/security/auditLog/queries"
$AuditLogNewQuery = Invoke-MgGraphRequest -Method POST -Uri $Url -Body $Body -ContentType "application/json"


# Poll the query status until it is completed, wait up to 60 minutes
$AuditLogNewQueryID = $AuditLogNewQuery.id
$QueryUrl = "https://graph.microsoft.com/beta/security/auditLog/queries/$AuditLogNewQueryID"

$QueryTimeoutMinutes = 60
$QueryPollIntervalSeconds = 60
$QueryStartTime = Get-Date

do {
    $Result = Invoke-MgGraphRequest -Method Get -Uri $QueryUrl -ContentType "application/json"

    Write-Host ("[{0}] Status: {1}" -f (Get-Date -Format "HH:mm:ss"), $Result.status)

    if ($Result.status -eq "succeeded") {
        Write-Host "Audit log query completed successfully."
        break
    }

    if ($Result.status -eq "failed") {
        throw "Audit log query failed. QueryId: $AuditLogNewQueryID"
    }

    Start-Sleep -Seconds $QueryPollIntervalSeconds

} while ((Get-Date) -lt $QueryStartTime.AddMinutes($QueryTimeoutMinutes))

if ($Result.status -ne "succeeded") {
    throw "Timeout reached. Audit log query did not complete within $QueryTimeoutMinutes minutes."
}


# Get the results of the search job (paging included)
$Url = "https://graph.microsoft.com/beta/security/auditLog/queries/$AuditLogNewQueryID/records?`$top=1000"
$AuditLogNewQueryResultRecords = @()

While ( $null -ne $Url ) {
    $data = Invoke-MgGraphRequest -Method GET -Uri $Url -ContentType "application/json" 
    $AuditLogNewQueryResultRecords += $data.Value         
    $Url = $data.'@Odata.NextLink'
}

# Output the results
$AuditLogNewQueryResultRecords | select createdDateTime,userPrincipalName,operation,service | fl


Run a SharePoint and OneDrive report

This report collects all guest users with the property IsEmailAuthenticationGuestUser across all SharePoint and OneDrive site collections. Depending on the number of site collections in your tenant, this can take a while.

Note:
You may also find internal users flagged as guests in SharePoint. These can be ignored if the account exists in Entra.
To make filtering easier, I added the columns UserEmailDomain and IsEntraAccount. I also included the SiteUserCreated information to return when the guest was first invited to the SharePoint site. In most cases, these users were created a long time ago.

I am using PnP PowerShell with an Azure app registration and the application permission AllSites.FullControl, as the script connects to site collections through the SharePoint API.

PowerShell
# Find the sample in my GitHub Repo: 
# https://github.com/TobiasAT/PowerShell/blob/main/Samples/SharePoint/Sample12-SharePointOTPUsers.ps1

# Connect with an Azure App and Application Permissions AllSites.FullControl to get all SharePoint and OneDrive site collections
Connect-PnPOnline -Url "https://<Tenant>-admin.sharepoint.com" -ClientId <AzureAppID> -Thumbprint <Thumbprint> -Tenant <TenantID> 

$AllSites = Get-PnPTenantSite -IncludeOneDriveSites

$AllSPOOTPUsers = @()
$Count = 1
foreach ($Site in $AllSites ) {
    Write-Host "$Count of $($AllSites.Count) - $($Site.Url)"
    
    # Get all site users with email authentication guest users (OTP) 
    $AllOTPUsers = @()
    $NextUrl = "$($Site.Url)/_api/web/siteusers?`$filter=IsEmailAuthenticationGuestUser eq true&`$select=Id,Title,Email,LoginName,IsShareByEmailGuestUser,IsEmailAuthenticationGuestUser&`$top=1000"

    # In some cases, sites are no longer accessible (e.g. legacy sites such as SharePoint Public Site) and the REST call can fail, so I  wrap it in a try-catch to avoid the script breaking.
    try {
        do {
            $SiteUsers = Invoke-PnPSPRestMethod -Method Get -Url $NextUrl
            $AllOTPUsers += $SiteUsers.value
            $NextUrl = $SiteUsers.'@odata.nextLink'
        } while ($NextUrl)

    }
    catch {
        Write-Warning "Skipping $($Site.Url) - failed to get site users: $_"
        continue
    }

    if($AllOTPUsers.Count -gt 0) {    

        # Get the User Information List for the site to retrieve additional user details (like created date) since the siteusers endpoint does not provide that information.
        $Url = "$($Site.Url)/_api/web/lists?`$filter=ListItemEntityTypeFullName eq 'SP.Data.UserInfoItem'&`$select=Id,Title"
        $UserInfoList = Invoke-PnPSPRestMethod -Method Get -Url $Url 

        # Get all users from the User Information List with a non-null and non-empty EMail field (to exclude system groups or other objects) and retrieve their created date.
        $NextUrl = "$($Site.Url)/_api/web/lists(guid'$($UserInfoList.value.id)')/items?`$filter=EMail ne null and EMail ne ''&`$select=Id,EMail,Created&`$top=1000"
        $UserInfoListUsers = @()
        do {
            $data = Invoke-PnPSPRestMethod -Method Get -Url $NextUrl
            $UserInfoListUsers += $data.value
            $NextUrl = $data.'@odata.nextLink'
        } while ($NextUrl)

        foreach ($User in $AllOTPUsers) {        
            $SPOOTPUser = New-Object -TypeName PSObject
            $SPOOTPUser | Add-Member -MemberType NoteProperty -Name SiteURL -Value  $Site.Url   
            $SPOOTPUser | Add-Member -MemberType NoteProperty -Name SiteID -Value $Site.SiteId.Guid
            $SPOOTPUser | Add-Member -MemberType NoteProperty -Name SiteTitle -Value $Site.Title

            if($Site.Url -like "*my.sharepoint.com/personal*") {
                $SPOOTPUser | Add-Member -MemberType NoteProperty -Name SiteType -Value "OneDrive"
            } else {
                $SPOOTPUser | Add-Member -MemberType NoteProperty -Name SiteType -Value "SharePoint"
            }

            $SPOOTPUser | Add-Member -MemberType NoteProperty -Name SiteUserID -Value $User.Id 


            $UserCreatedinUIL = $UserInfoListUsers | ?{ $_.Id -eq $User.Id }
            $SPOOTPUser | Add-Member -MemberType NoteProperty -Name SiteUserCreated -Value $UserCreatedinUIL.Created
            $UserLoginName = [System.Uri]::UnescapeDataString($User.LoginName)
            $SPOOTPUser | Add-Member -MemberType NoteProperty -Name UserLoginName -Value $UserLoginName
            $SPOOTPUser | Add-Member -MemberType NoteProperty -Name UserEmail -Value $User.Email
            $SPOOTPUser | Add-Member -MemberType NoteProperty -Name UserEmailDomain -Value ($User.Email -split '@')[1]
            $SPOOTPUser | Add-Member -MemberType NoteProperty -Name UserDisplayName -Value $User.Title        
            $SPOOTPUser | Add-Member -MemberType NoteProperty -Name UserIsShareByEmailGuestUser -Value $User.IsShareByEmailGuestUser
            $SPOOTPUser | Add-Member -MemberType NoteProperty -Name UserIsEmailAuthenticationGuestUser -Value $User.IsEmailAuthenticationGuestUser        
            
            # Check if the user has an Entra ID account (if the user profile can be retrieved via PnP PowerShell)
            try{  Get-PnPUserProfileProperty -Account $User.Email | Out-Null
                $SPOOTPUser | Add-Member -MemberType NoteProperty -Name IsEntraAccount -Value  $true
            }
            catch { $SPOOTPUser | Add-Member -MemberType NoteProperty -Name IsEntraAccount -Value  $false }

            # Get the last OTP login time for the user in the last 180 days from the Audit Log Query results
            $LastOTPTime = ($AuditLogNewQueryResultRecords | ? { $_.userPrincipalName -eq ($UserLoginName -split '\|')[-1] } | sort createdDateTime -Descending | select -First 1 ).createdDateTime   
            if($LastOTPTime -ne $null) {
                $SPOOTPUser | Add-Member -MemberType NoteProperty -Name LastSPOOTPLogin180Days -Value (Get-Date $LastOTPTime).ToString("yyyy-MM-ddTHH:mm:ssZ")
            } else {
                $SPOOTPUser | Add-Member -MemberType NoteProperty -Name LastSPOOTPLogin180Days -Value $null
            }
            $AllSPOOTPUsers += $SPOOTPUser
        }
    } 
    
    $Count++

}

# Export the results to a CSV file
$Date = Get-Date -Format "dd-MM-yyyy"
$ExportPath = ([Environment]::GetFolderPath('MyDocuments') + "\SPOOTPGuestUsers-$Date.csv")    
$AllSPOOTPUsers | Export-Csv -Path $ExportPath -NoTypeInformation -Force
Write-Host "ExportPath: $ExportPath"
Disconnect-PnPOnline


The result is an export similar to this sample:

Sample export with SharePoint OTP guest users
Sample export with SharePoint OTP guest users


Converting SharePoint OTP users to Entra B2B guest accounts

As Microsoft describes in the FAQ documentation:

For external collaborators without a Microsoft Entra B2B guest account, you can proactively create the account in your directory to retain access to all previously shared files.

The transition is straightforward. If the guest still needs access, create a guest user account in Entra with the same email address. The guest must accept the invitation. The guest user syncs to the SharePoint User Profile service, which links the two accounts at the site collection level.

Once the guest has accepted the invitation, the SharePoint OTP prompt is replaced by the Entra Email OTP prompt.

SharePoint OTP account has been converted to Entra Email OTP
SharePoint OTP account has been converted to Entra Email OTP

Summary

All SharePoint OTP users without an Entra B2B guest account will lose access to shared content starting in July 2026.
The fix is simple: Create an Entra guest account for anyone who still needs access. Use my report to find affected users, optionally combined with an audit log query. Existing Entra B2B accounts are not affected.

More information from Microsoft:

Share
Avatar photo

Tobias Asböck

Tobias is a Senior System Engineer with more than 10 years of professional experience with Microsoft 365 products such as SharePoint Online, SharePoint Premium, OneDrive for Business, Teams Collaboration, Entra ID, Information Protection, Universal Print, and Microsoft 365 Licensing. He also has 15+ years of experience planning, administering, and operating SharePoint Server environments. Tobias is a PowerShell Scripter with certifications for Microsoft 365 products. In his spare time, Tobias is busy with updates in the Microsoft 365 world or on the road with his road bike and other sports activities. If you have additional questions, please contact me via LinkedIn or [email protected].

Leave a Reply

Your email address will not be published. Required fields are marked *