Pre-authenticated (tempauth) URLs are retiring: move to Microsoft Entra

That download button might be relying on more than Microsoft Graph. It might be relying on a temporary credential hidden inside the URL.
Microsoft announced that SharePoint Online and OneDrive will begin removing pre-authenticated URLs on April 1, 2027. If I fetch file metadata, take the returned download URL, and hand it to the browser, I need to revisit that code.
My preferred direction: authenticate the content request, retrieve the bytes, and give the user a local browser download. Not a remote URL that doubles as a credential.
What is tempauth?
tempauth is short for temporary authentication. SharePoint issues a short-lived token for a resource and embeds it in a pre-authenticated URL.
A typical URL looks like this:
https://contoso.sharepoint.com/sites/demo/_layouts/15/download.aspx?UniqueId=FILE-ID&tempauth=TEMPORARY-TOKEN
The important part is not the file path. It is the token in the query string.
Possession of that URL lets a client retrieve the resource while the token remains valid, without sending its own Authorization header. It is not a permanent public link, but it is still a temporary access credential.
That made several scenarios convenient:
- Put the URL into an anchor and let the browser download the file.
- Use it as an image source.
- Pass it from a backend to a frontend.
- Follow a content API’s redirect without acquiring another token.
The trade-off? URLs travel. They end up in logs, telemetry, browser history, messages, and application state. I do not want temporary credentials travelling with them.
With Microsoft Entra ID authentication, the URL identifies the resource. The request presents the access token separately:
GET https://graph.microsoft.com/beta/drives/DRIVE-ID/items/ITEM-ID/contentStream
Authorization: Bearer MICROSOFT-GRAPH-ACCESS-TOKEN
The audience matters. A Microsoft Graph token is for Microsoft Graph. It is not interchangeable with a token for https://contoso.sharepoint.com.
The common pattern I would look for first
This was a convenient browser implementation:
// Before: relies on the returned URL carrying its own authorization.
const item = await client
.api(`/drives/${driveId}/items/${itemId}`)
.get();
const downloadUrl = item["@microsoft.graph.downloadUrl"];
const link = document.createElement("a");
link.href = downloadUrl;
link.download = "report.pdf";
document.body.appendChild(link);
link.click();
link.remove();
The Graph metadata request is authenticated. The subsequent browser navigation does not attach the application’s Graph bearer token.
The retirement announcement also shows this annotation:
{
"@content.downloadUrl": "https://contoso.sharepoint.com/...?tempauth=..."
}
The pattern to stop using is taking @content.downloadUrl or @microsoft.graph.downloadUrl and treating it as an anonymously redeemable download link.
The annotation itself is not necessarily disappearing. For configured applications and supported APIs, it can point to an authenticated Graph contentStream URL instead. The old assumption about how to use it is what breaks.
Adding the URL to fetch() without authentication does not solve that:
// Still relies on pre-authentication.
const response = await fetch(downloadUrl);
const blob = await response.blob();
A Blob changes how I handle the response. It does not authenticate the request that produced it.
What changes, and what does not?
Microsoft describes two affected patterns:
| Existing pattern | New behavior |
|---|---|
| An affected content API redirects to a pre-authenticated URL. | The API returns content directly. Do not depend on a 302 or extract its Location header. |
| An API returns a SharePoint URL containing temporary authentication information. | The URL needs a SharePoint-audience Entra access token once pre-authentication is removed. |
| A supported API returns a Graph URL after the app is opted in. | Call that URL with a Microsoft Graph access token. |
Downloads are my focus here, but uploads, previews, thumbnails, conversions, versions, and copy-operation monitor URLs also deserve an audit. A download helper will not fix an upload session or an iframe preview.
A version caveat before copying the examples
As of this draft, Microsoft Learn documents driveItem/contentStream under Microsoft Graph beta. Microsoft does not support beta APIs for production applications.
The contentStream examples below are therefore migration experiments for a test environment, not a claim that the endpoint is already generally available. Check the API’s version selector again before shipping.
The stable /content endpoint remains relevant. However, its existing behavior includes a redirect to a pre-authenticated URL. Merely changing a browser call from a download annotation to /content does not prove that I have removed that dependency.
For production, I can keep using the supported content API, make sure my client accepts a direct content response, and validate it under the new tenant behavior. If I must redeem a returned SharePoint URL, I need a correctly scoped SharePoint token, not my Graph token.
The better browser pattern: authenticated bytes, then a Blob
I separate the operation into two steps:
- Retrieve file content through an authenticated request.
- Create a local object URL for those bytes and offer a download.
User clicks Download
-> authenticated Microsoft Graph request
-> file bytes in a browser Blob
-> local blob: object URL
-> browser download
I am holding the content in browser-managed storage for the current page, not persisting a remote download credential. A blob: URL is not a durable sharing link and does not work as an attachment URL for somebody else.
Microsoft Graph JavaScript Client Library
This example uses the Microsoft Graph JavaScript Client Library. It assumes client already has an authentication provider that acquires Microsoft Graph tokens.
First, the browser-only download helper:
export function saveBlob(blob: Blob, fileName: string): void {
const objectUrl = URL.createObjectURL(blob);
const link = document.createElement("a");
try {
link.href = objectUrl;
link.download = fileName;
document.body.appendChild(link);
link.click();
} finally {
link.remove();
// Defer cleanup so the browser has time to start the download.
setTimeout(() => URL.revokeObjectURL(objectUrl), 60_000);
}
}
Then retrieve the content. This is the beta/test-environment part:
import { Client, ResponseType } from "@microsoft/microsoft-graph-client";
export async function downloadGraphFile(
client: Client,
driveId: string,
itemId: string,
fileName: string
): Promise<void> {
const blob: unknown = await client
.api(
`/drives/${encodeURIComponent(driveId)}` +
`/items/${encodeURIComponent(itemId)}/contentStream`
)
.version("beta")
.responseType(ResponseType.BLOB)
.get();
if (!(blob instanceof Blob)) {
throw new Error("Microsoft Graph did not return file content as a Blob.");
}
saveBlob(blob, fileName);
}
ResponseType.BLOB makes the expected response explicit. The client’s authentication provider supplies the Graph token. I never read a download URL from the metadata response.
For a stable API test, use /content with .version("v1.0") instead, and validate the direct-content behavior in the configured tenant. Do not assume the legacy redirect path works in every browser or CORS configuration.
Both helpers belong in the same module here. The filename should come from trusted application metadata, not an arbitrary URL query parameter.
SharePoint Framework: MSGraphClientV3
In SPFx, MSGraphClientV3 wraps version 3 of the JavaScript client and handles Graph authentication. I still need to request the necessary permissions and have them approved.
Inside an async web part method, using the same saveBlob helper:
import { MSGraphClientV3 } from "@microsoft/sp-http";
import { ResponseType } from "@microsoft/microsoft-graph-client";
// Beta endpoint: validate in a test environment.
const client: MSGraphClientV3 =
await this.context.msGraphClientFactory.getClient("3");
const blob: unknown = await client
.api(
`/drives/${encodeURIComponent(driveId)}` +
`/items/${encodeURIComponent(itemId)}/contentStream`
)
.version("beta")
.responseType(ResponseType.BLOB)
.get();
if (!(blob instanceof Blob)) {
throw new Error("Microsoft Graph did not return file content as a Blob.");
}
saveBlob(blob, fileName);
Use the ResponseType export from the matching v3 Graph client dependency. Do not introduce an unrelated client major version just to copy an import.
For the documented delegated work/school scenario, Files.Read is the least-privileged permission listed for contentStream. The signed-in user must also have access to the file. An SPFx solution requests Graph permissions through webApiPermissionRequests, for example:
{
"webApiPermissionRequests": [
{
"resource": "Microsoft Graph",
"scope": "Files.Read"
}
]
}
Merge that request into the existing solution configuration in config/package-solution.json; do not replace the configuration with this fragment. Tenant approval is still required.
A few practical limits
- A Blob download buffers the file. For large files, prefer a streaming design rather than loading everything into browser memory.
- Handle rejected download promises in the UI. Show an error and reset loading state; do not silently fall back to an old download URL.
- Revoke object URLs after use. The timer above is deferred cleanup, not a signal that the download completed.
- Do not put access tokens into query strings, local storage, logs, or analytics.
- Do not forward a Graph bearer token to an arbitrary returned hostname.
- Do not render untrusted HTML or SVG from a Blob as active page content. This example offers an attachment download.
How I find the dependencies in an existing codebase
I start with data flow, not just package names. An SDK can authenticate the metadata call perfectly while my next line hands a temporary URL to an unauthenticated client.
In VS Code, enable regular-expression search and start with:
@content\.downloadUrl|@microsoft\.graph\.downloadUrl|tempauth=|access_token=
Then inspect where each value goes: fetch, HttpClient, an anchor, window.open, an image, an iframe, a queue, or a database.
If ripgrep is available, this searches application source formats while excluding common generated output:
rg -n \
-g '*.{cs,ts,tsx,js,jsx,json}' \
-g '!node_modules/**' -g '!dist/**' -g '!bin/**' -g '!obj/**' \
'@content\.downloadUrl|@microsoft\.graph\.downloadUrl|tempauth=|access_token=' .
The search includes access_token= because that is another credential-in-URL smell. Review any matches locally without pasting live tokens into tickets or reports.
.NET: Microsoft.Graph
For applications using the Microsoft.Graph NuGet package, I search for:
GraphServiceClient|AdditionalData|downloadUrl|AllowAutoRedirect|Headers\.Location|GetByteArrayAsync|GetStreamAsync|\.Content\b|ContentStream
An older implementation might look like this:
// Before: graphClient is authenticated; this separate GET is not.
var item = await graphClient.Drives[driveId].Items[itemId].GetAsync();
var downloadUrl =
(string)item!.AdditionalData["@microsoft.graph.downloadUrl"];
var bytes = await httpClient.GetByteArrayAsync(downloadUrl);
I inspect reads from AdditionalData carefully. That is where the download annotation commonly appears in the .NET model. I also look for older .Request().GetAsync() syntax and custom redirect handlers.
With the stable .NET SDK v5-style API, I can ask the authenticated client for content instead of extracting the URL:
using Microsoft.Graph;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
public static async Task CopyGraphFileAsync(
GraphServiceClient graphClient,
string driveId,
string itemId,
Stream destination,
CancellationToken cancellationToken)
{
using var content = await graphClient
.Drives[driveId]
.Items[itemId]
.Content
.GetAsync(cancellationToken: cancellationToken)
?? throw new InvalidOperationException("Graph returned no file content.");
await content.CopyToAsync(destination, cancellationToken);
}
This keeps the operation in the authenticated client and processes a stream. It still uses /content: test the installed SDK’s handling of the changed response under the tenant controls. Do not describe its legacy redirect behavior as tempauth-free.
For a beta contentStream experiment, use the separate Microsoft.Graph.Beta package and an authenticated Microsoft.Graph.Beta.GraphServiceClient:
// Beta SDK/client, not Microsoft.Graph.GraphServiceClient.
using var content = await betaGraphClient
.Drives[driveId]
.Items[itemId]
.ContentStream
.GetAsync(cancellationToken: cancellationToken)
?? throw new InvalidOperationException("Graph returned no file content.");
await content.CopyToAsync(destination, cancellationToken);
If a backend serves these bytes to a browser, authorize the caller before streaming the response. Never expose an app-only Graph download through an unauthenticated proxy.
.NET: PnP Core SDK interop with Graph
PnP Core SDK can mix SharePoint and Graph operations. Its Graph SDK interop guide is a useful starting point for finding that boundary.
Search for:
PnPContext|pnpCoreContext|AuthenticationProvider|AuthenticateRequestAsync|GetAccessTokenAsync|DelegateAuthenticationProvider|GraphServiceClient|AdditionalData|downloadUrl
Then follow any resulting download URL into the next HTTP request. The fact that a PnPContext acquired the metadata does not authenticate a separate browser navigation.
One version trap: the interop guide shows DelegateAuthenticationProvider and .Request() syntax from older Graph SDK versions. Do not paste that constructor into a v5 project.
For a direct content request, I can also reuse PnP Core’s existing authentication provider without adding a custom Graph SDK adapter. This beta/test-environment fragment assumes an active pnpCoreContext, a managed HttpClient, and a destination stream:
var contentUri = new Uri(
"https://graph.microsoft.com/beta/drives/" +
Uri.EscapeDataString(driveId) + "/items/" +
Uri.EscapeDataString(itemId) + "/contentStream");
using var request = new HttpRequestMessage(HttpMethod.Get, contentUri);
await pnpCoreContext.AuthenticationProvider.AuthenticateRequestAsync(
new Uri("https://graph.microsoft.com"),
request);
using var response = await httpClient.SendAsync(
request,
HttpCompletionOption.ResponseHeadersRead,
cancellationToken);
response.EnsureSuccessStatusCode();
await response.Content.CopyToAsync(destination, cancellationToken);
The PnP Core authentication provider authenticates for the Graph resource here, not for the SharePoint site URL. The configured provider must support Graph token acquisition and have the appropriate consent.
For existing Graph SDK interop, keep the supported authentication bridge for your installed SDK version and use its authenticated content operation. Changing SDKs is not the fix; changing the unauthenticated hand-off is.
JavaScript and SPFx search patterns
For the standalone JavaScript client:
@microsoft/microsoft-graph-client|Client\.init|Client\.initWithMiddleware|downloadUrl|ResponseType\.(BLOB|ARRAYBUFFER)|/content\b|/contentStream\b
For SPFx:
MSGraphClientV3|msGraphClientFactory|getClient\(['"]3['"]\)|downloadUrl|window\.open|createElement\(['"]a['"]\)
These are discovery patterns, not vulnerability detectors. MSGraphClientV3 is not the problem. Neither is URL.createObjectURL. I am looking for an authenticated API call followed by an unauthenticated remote URL hand-off.
Finally, search the wider API surface:
uploadUrl|getUrl|postUrl|createUploadSession|/preview\b|/thumbnails\b|monitorUrl|Location|redirect
Review matches in context. For example, an upload-session URL and a thumbnail URL may need different migration work.
I will enforce this sooner, not wait for April 2027
My plan is to make authenticated content retrieval the default for new code now, migrate existing download flows, and enforce the new behavior in a test tenant before the retirement starts.
I do not want April 1, 2027 to be the first day I discover that a customer-facing download button depended on tempauth.
That is a staged rollout decision, not a recommendation to disable pre-authentication across production without testing.
Start by reading the current tenant settings
In a supported SharePoint Online Management Shell session, connect as a SharePoint administrator and inspect the configuration:
Connect-SPOService -Url "https://contoso-admin.sharepoint.com"
Get-SPOTenantPreAuthSettings
Get-SPOTenantPreAuthSettings | ConvertTo-Json -Depth 10
Get-SPOTenantPreAuthSettings is read-only. It reports the configuration; it does not enforce the migration.
The announcement describes the Graph URL switches as forthcoming. The current cmdlet reference documents them, but the installed module still needs to expose them. Check before relying on them:
Get-Command Get-SPOTenantPreAuthSettings -Syntax
Get-Command Set-SPOTenantPreAuthSettings -Syntax
If the installed version supports the switch:
Get-SPOTenantPreAuthSettings -UseGraphUrlSettings
Opt one application into Graph URLs
In a test tenant, the documented switches let me target an application:
$appId = "11111111-2222-3333-4444-555555555555"
Set-SPOTenantPreAuthSettings `
-UseGraphUrlIsEnabled $true `
-UseGraphUrlAppsList $appId
Get-SPOTenantPreAuthSettings -UseGraphUrlSettings
Record the existing configuration first and preserve any existing app list when planning a change. The list parameter is not an append operation. An empty list means all applicable applications, so do not use an empty string for a limited pilot.
This setting applies to third-party applications making the original request through Graph. It is not a universal switch that turns every SharePoint URL into a Graph URL.
For SPFx, establish which Entra application identity actually makes the request. Do not assume the SPFx solution’s package ID is the Entra application ID, or that a shared principal isolates one web part.
Test removing pre-authentication separately
Opting into Graph URLs and denying pre-authentication are related, but different controls. After checking the current Set-SPOTenantPreAuthSettings documentation, I can pilot a targeted download restriction in a test tenant:
Set-SPOTenantPreAuthSettings `
-Add `
-Type Deny `
-IncludedApps $appId `
-IncludedFeatures "Download"
Get-SPOTenantPreAuthSettings | ConvertTo-Json -Depth 10
Capture the new rule’s ID from the settings so it can be removed after the test. This targets the Download feature; it does not validate uploads, previews, or every other affected scenario.
The broader tenant-wide control also exists:
# Broad impact: evaluate in a test tenant, not as the first production step.
Set-SPOTenantPreAuthSettings -IsDisabled $true
The documented precedence is Deny, then Allow, then IsDisabled. An allow rule can create an exception to tenant-wide disablement, while a matching deny rule wins over an allow rule.
These controls can break other experiences, including first-party scenarios. Inspect existing rules, define rollback, and test before expanding scope. Simply setting -IsDisabled $false does not remove a deny rule.
What I would verify before expanding the rollout
- The downloaded bytes and filename are correct, including binary files.
- The authenticated content path works without extracting a
302redirect. - The application no longer hands a credential-bearing remote URL to the browser.
- Permission failures produce a visible error, not a fallback to the old URL.
- Large-file behavior is acceptable without excessive browser memory use.
- Thumbnails, previews, uploads, and background jobs have their own tests.
- Diagnostics do not record tokens or complete pre-authenticated URLs.
My takeaway
The old model was: get a URL that carries access, then give it to another client.
The model I want is: authenticate the content request, then deliver the content.
For a browser download, a Blob is a practical final step. The important migration happens before that Blob exists: the request for the bytes must no longer depend on a temporary credential embedded in a URL.
References
- Microsoft’s tempauth retirement announcement
- Get file by contentStream - Microsoft Graph beta
- Download driveItem content - Microsoft Graph v1.0
- PnP Core SDK interop with Microsoft Graph
- Microsoft Graph JavaScript Client Library
- Use MSGraphClientV3 in SPFx
- Get-SPOTenantPreAuthSettings
- Set-SPOTenantPreAuthSettings