From c2952717577b450ed5391e909684609d37045b7d Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Thu, 13 Nov 2025 06:39:10 +0100 Subject: [PATCH 1/2] Bumped version to 16.4.0-rc2. --- src/Umbraco.Web.UI.Client/package.json | 2 +- version.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index afeacbe636..b076052b37 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -1,7 +1,7 @@ { "name": "@umbraco-cms/backoffice", "license": "MIT", - "version": "16.4.0-rc", + "version": "16.4.0-rc2", "type": "module", "exports": { ".": null, diff --git a/version.json b/version.json index d8d415334b..cf14b3a81a 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "16.4.0-rc", + "version": "16.4.0-rc2", "assemblyVersion": { "precision": "build" }, From 49ba89c22a7d51784cff43a13a3684a47dc47932 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 13 Nov 2025 08:19:42 +0100 Subject: [PATCH 2/2] Move access/refresh tokens to secure cookies (#20779) * feat: adds the `credentials: include` header to all manual requests * feat: adds `credentials: include` as a configurable option to xhr requests (and sets it by default to true) * feat: configures the auto-generated fetch client from hey-api to include credentials by default * Add OpenIddict handler to hide tokens from the back-office client * Make back-office token redaction optional (default false) * Clear back-office token cookies on logout * Add configuration for backoffice cookie settings * Make cookies forcefully secure + move cookie handler enabling to the BackOfficeTokenCookieSettings * Use the "__Host-" prefix for cookie names * docs: adds documentation on cookie settings * build: sets up launch profile for vscode with new cookie recommended settings * docs: adds extra note around SameSite settings * docs: adds extra note around SameSite settings * Respect sites that do not use HTTPS * Explicitly invalidate potentially valid, old refresh tokens that should no longer be used * Removed obsolete const --------- Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> --- .github/BUILD.md | 13 +- .github/copilot-instructions.md | 6 +- .vscode/launch.json | 5 +- .../HideBackOfficeTokensHandler.cs | 187 ++++++++++++++++++ .../UmbracoBuilderAuthExtensions.cs | 38 ++++ .../Models/BackOfficeTokenCookieSettings.cs | 31 +++ src/Umbraco.Core/Constants-Configuration.cs | 1 + .../UmbracoBuilder.Configuration.cs | 3 +- src/Umbraco.Web.UI.Client/.github/README.md | 12 +- .../src/external/openid/src/xhr.ts | 1 + .../src/packages/core/auth/auth-flow.ts | 2 + .../src/packages/core/http-client/index.ts | 15 +- .../try-execute/tryXhrRequest.function.ts | 1 + .../src/packages/core/resources/types.ts | 1 + .../document-permission.server.data.ts | 1 + 15 files changed, 309 insertions(+), 8 deletions(-) create mode 100644 src/Umbraco.Cms.Api.Common/DependencyInjection/HideBackOfficeTokensHandler.cs create mode 100644 src/Umbraco.Core/Configuration/Models/BackOfficeTokenCookieSettings.cs diff --git a/.github/BUILD.md b/.github/BUILD.md index 2e081548fe..424a491cce 100644 --- a/.github/BUILD.md +++ b/.github/BUILD.md @@ -37,7 +37,7 @@ In order to work with the Umbraco source code locally, first make sure you have ### Familiarizing yourself with the code -Umbraco is a .NET application using C#. The solution is broken down into multiple projects. There are several class libraries. The `Umbraco.Web.UI` project is the main project that hosts the back office and login screen. This is the project you will want to run to see your changes. +Umbraco is a .NET application using C#. The solution is broken down into multiple projects. There are several class libraries. The `Umbraco.Web.UI` project is the main project that hosts the back office and login screen. This is the project you will want to run to see your changes. There are two web projects in the solution with client-side assets based on TypeScript, `Umbraco.Web.UI.Client` and `Umbraco.Web.UI.Login`. @@ -73,13 +73,20 @@ Just be careful not to include this change in your PR. Conversely, if you are working on front-end only, you want to build the back-end once and then run it. Before you do so, update the configuration in `appSettings.json` to add the following under `Umbraco:Cms:Security`: -``` +```json "BackOfficeHost": "http://localhost:5173", "AuthorizeCallbackPathName": "/oauth_complete", "AuthorizeCallbackLogoutPathName": "/logout", -"AuthorizeCallbackErrorPathName": "/error" +"AuthorizeCallbackErrorPathName": "/error", +"BackOfficeTokenCookie": { + "Enabled": true, + "SameSite": "None" +} ``` +> [!NOTE] +> If you get stuck in a login loop, try clearing your browser cookies for localhost, and make sure that the `BackOfficeTokenCookie` settings are correct. Namely, that `SameSite` should be set to `None` when running the front-end server separately. + Then run Umbraco from the command line. ``` diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index ed34279ab9..858984a9d3 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -110,7 +110,11 @@ Use this for frontend-only development with hot module reloading: "BackOfficeHost": "http://localhost:5173", "AuthorizeCallbackPathName": "/oauth_complete", "AuthorizeCallbackLogoutPathName": "/logout", - "AuthorizeCallbackErrorPathName": "/error" + "AuthorizeCallbackErrorPathName": "/error", + "BackOfficeTokenCookie": { + "Enabled": true, + "SameSite": "None" + } ``` 2. Run backend: `cd src/Umbraco.Web.UI && dotnet run --no-build` 3. Run frontend dev server: `cd src/Umbraco.Web.UI.Client && npm run dev:server` diff --git a/.vscode/launch.json b/.vscode/launch.json index f4d47c3dab..c56f06dc2f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -105,7 +105,10 @@ "UMBRACO__CMS__SECURITY__BACKOFFICEHOST": "http://localhost:5173", "UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKPATHNAME": "/oauth_complete", "UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKLOGOUTPATHNAME": "/logout", - "UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKERRORPATHNAME": "/error" + "UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKERRORPATHNAME": "/error", + "UMBRACO__CMS__SECURITY__KEEPUSERLOGGEDIN": "true", + "UMBRACO__CMS__SECURITY__BACKOFFICETOKENCOOKIE__ENABLED": "true", + "UMBRACO__CMS__SECURITY__BACKOFFICETOKENCOOKIE__SAMESITE": "None" }, "sourceFileMap": { "/Views": "${workspaceFolder}/Umbraco.Web.UI/Views" diff --git a/src/Umbraco.Cms.Api.Common/DependencyInjection/HideBackOfficeTokensHandler.cs b/src/Umbraco.Cms.Api.Common/DependencyInjection/HideBackOfficeTokensHandler.cs new file mode 100644 index 0000000000..8d1dbd040e --- /dev/null +++ b/src/Umbraco.Cms.Api.Common/DependencyInjection/HideBackOfficeTokensHandler.cs @@ -0,0 +1,187 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using OpenIddict.Server; +using OpenIddict.Validation; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Web.Common.Security; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Common.DependencyInjection; + +internal sealed class HideBackOfficeTokensHandler + : IOpenIddictServerHandler, + IOpenIddictServerHandler, + IOpenIddictValidationHandler, + INotificationHandler +{ + private const string RedactedTokenValue = "[redacted]"; + private const string AccessTokenCookieKey = "__Host-umbAccessToken"; + private const string RefreshTokenCookieKey = "__Host-umbRefreshToken"; + + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IDataProtectionProvider _dataProtectionProvider; + private readonly BackOfficeTokenCookieSettings _backOfficeTokenCookieSettings; + private readonly GlobalSettings _globalSettings; + + public HideBackOfficeTokensHandler( + IHttpContextAccessor httpContextAccessor, + IDataProtectionProvider dataProtectionProvider, + IOptions backOfficeTokenCookieSettings, + IOptions globalSettings) + { + _httpContextAccessor = httpContextAccessor; + _dataProtectionProvider = dataProtectionProvider; + _backOfficeTokenCookieSettings = backOfficeTokenCookieSettings.Value; + _globalSettings = globalSettings.Value; + } + + /// + /// This is invoked when tokens (access and refresh tokens) are issued to a client. For the back-office client, + /// we will intercept the response, write the tokens from the response into HTTP-only cookies, and redact the + /// tokens from the response, so they are not exposed to the client. + /// + public ValueTask HandleAsync(OpenIddictServerEvents.ApplyTokenResponseContext context) + { + if (context.Request?.ClientId is not Constants.OAuthClientIds.BackOffice) + { + // Only ever handle the back-office client. + return ValueTask.CompletedTask; + } + + HttpContext httpContext = GetHttpContext(); + + if (context.Response.AccessToken is not null) + { + SetCookie(httpContext, AccessTokenCookieKey, context.Response.AccessToken); + context.Response.AccessToken = RedactedTokenValue; + } + + if (context.Response.RefreshToken is not null) + { + SetCookie(httpContext, RefreshTokenCookieKey, context.Response.RefreshToken); + context.Response.RefreshToken = RedactedTokenValue; + } + + return ValueTask.CompletedTask; + } + + /// + /// This is invoked when requesting new tokens. + /// + public ValueTask HandleAsync(OpenIddictServerEvents.ExtractTokenRequestContext context) + { + if (context.Request?.ClientId != Constants.OAuthClientIds.BackOffice) + { + // Only ever handle the back-office client. + return ValueTask.CompletedTask; + } + + // For the back-office client, this only happens when a refresh token is being exchanged for a new access token. + if (context.Request.RefreshToken == RedactedTokenValue + && TryGetCookie(RefreshTokenCookieKey, out var refreshToken)) + { + context.Request.RefreshToken = refreshToken; + } + else + { + // If we got here, either the refresh token was not redacted, or nothing was found in the refresh token cookie. + // If OpenIddict found a refresh token, it could be an old token that is potentially still valid. For security + // reasons, we cannot accept that; at this point, we expect the refresh tokens to be explicitly redacted. + context.Request.RefreshToken = null; + } + + + return ValueTask.CompletedTask; + } + + /// + /// This is invoked when extracting the auth context for a client request. + /// + public ValueTask HandleAsync(OpenIddictValidationEvents.ProcessAuthenticationContext context) + { + // For the back-office client, this only happens when an access token is sent to the API. + if (context.AccessToken != RedactedTokenValue) + { + return ValueTask.CompletedTask; + } + + if (TryGetCookie(AccessTokenCookieKey, out var accessToken)) + { + context.AccessToken = accessToken; + } + + return ValueTask.CompletedTask; + } + + public void Handle(UserLogoutSuccessNotification notification) + { + HttpContext? context = _httpContextAccessor.HttpContext; + if (context is null) + { + // For some reason there is no ambient HTTP context, so we can't clean up the cookies. + // This is OK, because the tokens in the cookies have already been revoked at user sign-out, + // so the cookie clean-up is mostly cosmetic. + return; + } + + context.Response.Cookies.Delete(AccessTokenCookieKey); + context.Response.Cookies.Delete(RefreshTokenCookieKey); + } + + private HttpContext GetHttpContext() + => _httpContextAccessor.GetRequiredHttpContext(); + + private void SetCookie(HttpContext httpContext, string key, string value) + { + var cookieValue = EncryptionHelper.Encrypt(value, _dataProtectionProvider); + + var cookieOptions = new CookieOptions + { + // Prevent the client-side scripts from accessing the cookie. + HttpOnly = true, + + // Mark the cookie as essential to the application, to enforce it despite any + // data collection consent options. This aligns with how ASP.NET Core Identity + // does when writing cookies for cookie authentication. + IsEssential = true, + + // Cookie path must be root for optimal security. + Path = "/", + + // For optimal security, the cooke must be secure. However, Umbraco allows for running development + // environments over HTTP, so we need to take that into account here. + // Thus, we will make the cookie secure if: + // - HTTPS is explicitly enabled by config (default for production environments), or + // - The current request is over HTTPS (meaning the environment supports it regardless of config). + Secure = _globalSettings.UseHttps || httpContext.Request.IsHttps, + + // SameSite is configurable (see BackOfficeTokenCookieSettings for defaults): + SameSite = ParseSameSiteMode(_backOfficeTokenCookieSettings.SameSite), + }; + + httpContext.Response.Cookies.Delete(key, cookieOptions); + httpContext.Response.Cookies.Append(key, cookieValue, cookieOptions); + } + + private bool TryGetCookie(string key, [NotNullWhen(true)] out string? value) + { + if (GetHttpContext().Request.Cookies.TryGetValue(key, out var cookieValue)) + { + value = EncryptionHelper.Decrypt(cookieValue, _dataProtectionProvider); + return true; + } + + value = null; + return false; + } + + private static SameSiteMode ParseSameSiteMode(string sameSiteMode) => + Enum.TryParse(sameSiteMode, ignoreCase: true, out SameSiteMode result) + ? result + : throw new ArgumentException($"The provided {nameof(sameSiteMode)} value could not be parsed into as SameSiteMode value.", nameof(sameSiteMode)); +} diff --git a/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs b/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs index 0139bb61ce..92a24046b2 100644 --- a/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs +++ b/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs @@ -9,6 +9,7 @@ using Umbraco.Cms.Api.Common.Security; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs; using Umbraco.Extensions; @@ -28,6 +29,11 @@ public static class UmbracoBuilderAuthExtensions private static void ConfigureOpenIddict(IUmbracoBuilder builder) { + // Optionally hide tokens from the back-office. + var hideBackOfficeTokens = (builder.Config + .GetSection(Constants.Configuration.ConfigBackOfficeTokenCookie) + .Get() ?? new BackOfficeTokenCookieSettings()).Enabled; + builder.Services.AddOpenIddict() // Register the OpenIddict server components. .AddServer(options => @@ -113,6 +119,22 @@ public static class UmbracoBuilderAuthExtensions { configuration.UseSingletonHandler().SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ResolveRequestUri.Descriptor.Order - 1); }); + + if (hideBackOfficeTokens) + { + options.AddEventHandler(configuration => + { + configuration + .UseSingletonHandler() + .SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ProcessJsonResponse.Descriptor.Order - 1); + }); + options.AddEventHandler(configuration => + { + configuration + .UseSingletonHandler() + .SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ExtractPostRequest.Descriptor.Order + 1); + }); + } }) // Register the OpenIddict validation components. @@ -137,9 +159,25 @@ public static class UmbracoBuilderAuthExtensions { configuration.UseSingletonHandler().SetOrder(OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlers.ResolveRequestUri.Descriptor.Order - 1); }); + + if (hideBackOfficeTokens) + { + options.AddEventHandler(configuration => + { + configuration + .UseSingletonHandler() + // IMPORTANT: the handler must be AFTER the built-in query string handler, because the client-side SignalR library sometimes appends access tokens to the query string. + .SetOrder(OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlers.ExtractAccessTokenFromQueryString.Descriptor.Order + 1); + }); + } }); builder.Services.AddRecurringBackgroundJob(); builder.Services.ConfigureOptions(); + + if (hideBackOfficeTokens) + { + builder.AddNotificationHandler(); + } } } diff --git a/src/Umbraco.Core/Configuration/Models/BackOfficeTokenCookieSettings.cs b/src/Umbraco.Core/Configuration/Models/BackOfficeTokenCookieSettings.cs new file mode 100644 index 0000000000..4019c42547 --- /dev/null +++ b/src/Umbraco.Core/Configuration/Models/BackOfficeTokenCookieSettings.cs @@ -0,0 +1,31 @@ +using System.ComponentModel; + +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for back-office token cookie settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigBackOfficeTokenCookie)] +[Obsolete("This will be replaced with a different authentication scheme. Scheduled for removal in Umbraco 18.")] +public class BackOfficeTokenCookieSettings +{ + private const bool StaticEnabled = false; + + private const string StaticSameSite = "Strict"; + + /// + /// Gets or sets a value indicating whether to enable access and refresh tokens in cookies. + /// + [DefaultValue(StaticEnabled)] + [Obsolete("This is only configurable in Umbraco 16. Scheduled for removal in Umbraco 17.")] + public bool Enabled { get; set; } = StaticEnabled; + + /// + /// Gets or sets a value indicating whether the cookie SameSite configuration. + /// + /// + /// Valid values are "Unspecified", "None", "Lax" and "Strict" (default). + /// + [DefaultValue(StaticSameSite)] + public string SameSite { get; set; } = StaticSameSite; +} diff --git a/src/Umbraco.Core/Constants-Configuration.cs b/src/Umbraco.Core/Constants-Configuration.cs index 8504210504..7e506a978f 100644 --- a/src/Umbraco.Core/Constants-Configuration.cs +++ b/src/Umbraco.Core/Constants-Configuration.cs @@ -64,6 +64,7 @@ public static partial class Constants public const string ConfigWebhook = ConfigPrefix + "Webhook"; public const string ConfigWebhookPayloadType = ConfigWebhook + ":PayloadType"; public const string ConfigCache = ConfigPrefix + "Cache"; + public const string ConfigBackOfficeTokenCookie = ConfigSecurity + ":BackOfficeTokenCookie"; public static class NamedOptions { diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs index cc9b03e65b..81b387a74b 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs @@ -86,7 +86,8 @@ public static partial class UmbracoBuilderExtensions .AddUmbracoOptions() .AddUmbracoOptions() .AddUmbracoOptions() - .AddUmbracoOptions(); + .AddUmbracoOptions() + .AddUmbracoOptions(); // Configure connection string and ensure it's updated when the configuration changes builder.Services.AddSingleton, ConfigureConnectionStrings>(); diff --git a/src/Umbraco.Web.UI.Client/.github/README.md b/src/Umbraco.Web.UI.Client/.github/README.md index cf76fd68ef..d677ad9e0d 100644 --- a/src/Umbraco.Web.UI.Client/.github/README.md +++ b/src/Umbraco.Web.UI.Client/.github/README.md @@ -26,6 +26,7 @@ If you have an existing Vite server running, you can run the task **Backoffice A ### Run a Front-end server against a local Umbraco instance #### 1. Configure Umbraco instance + Enable the front-end server communicating with the Backend server(Umbraco instance) you need need to correct the `appsettings.json` of your project. For code contributions use the backend project of `/src/Umbraco.Web.UI`. @@ -38,7 +39,11 @@ Open this file in an editor: `/src/Umbraco.Web.UI/appsettings.Development.json` "BackOfficeHost": "http://localhost:5173", "AuthorizeCallbackPathName": "/oauth_complete", "AuthorizeCallbackLogoutPathName": "/logout", - "AuthorizeCallbackErrorPathName": "/error", + "AuthorizeCallbackErrorPathName": "/error",, + "BackOfficeTokenCookie": { + "Enabled": true, + "SameSite": "None" + } }, }, } @@ -46,10 +51,15 @@ Open this file in an editor: `/src/Umbraco.Web.UI/appsettings.Development.json` This will override the backoffice host URL, enabling the Client to run from a different origin. +> [!NOTE] +> If you get stuck in a login loop, try clearing your browser cookies for localhost, and make sure that the `BackOfficeTokenCookie` settings are correct. Namely, that `SameSite` should be set to `None` when running the front-end server separately. + #### 2. Start Umbraco + Then start the backend server by running the command: `dotnet run` in the `/src/Umbraco.Web.UI` folder. #### 3. Start Frontend server + Now start the frontend server by running the command: `npm run dev:server` in the `/src/Umbraco.Web.UI.Client` folder. Finally open `http://localhost:5173` in your browser. diff --git a/src/Umbraco.Web.UI.Client/src/external/openid/src/xhr.ts b/src/Umbraco.Web.UI.Client/src/external/openid/src/xhr.ts index fc32dbf8cb..0ef5981a5b 100644 --- a/src/Umbraco.Web.UI.Client/src/external/openid/src/xhr.ts +++ b/src/Umbraco.Web.UI.Client/src/external/openid/src/xhr.ts @@ -35,6 +35,7 @@ export class FetchRequestor extends Requestor { const requestInit: RequestInit = {}; requestInit.method = settings.method; requestInit.mode = 'cors'; + requestInit.credentials = settings.credentials ?? 'include'; if (settings.data) { if (settings.method && settings.method.toUpperCase() === 'POST') { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts index fc6f5d1ab2..7a9d0bbf2e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts @@ -365,6 +365,7 @@ export class UmbAuthFlow { const token = await this.performWithFreshTokens(); const request = new Request(this.#unlink_endpoint, { method: 'POST', + credentials: 'include', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify({ loginProvider, providerKey }), }); @@ -458,6 +459,7 @@ export class UmbAuthFlow { const token = await this.performWithFreshTokens(); const request = await fetch(`${this.#link_key_endpoint}?provider=${provider}`, { + credentials: 'include', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/http-client/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/http-client/index.ts index 450649ecb2..26ee2b570c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/http-client/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/http-client/index.ts @@ -1 +1,14 @@ -export { client as umbHttpClient } from '@umbraco-cms/backoffice/external/backend-api'; +import { client } from '@umbraco-cms/backoffice/external/backend-api'; + +/** + * Pre-configure the client with default credentials for cookie-based authentication. + * This ensures all requests include cookies by default, which is required for + * cookie-based authentication in Umbraco 17.0+. + * + * Extensions using this client will automatically get credentials: 'include'. + */ +client.setConfig({ + credentials: 'include', +}); + +export { client as umbHttpClient }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute/tryXhrRequest.function.ts b/src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute/tryXhrRequest.function.ts index 2be9e84fa7..dce562bc3a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute/tryXhrRequest.function.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute/tryXhrRequest.function.ts @@ -44,6 +44,7 @@ function createXhrRequest(options: XhrRequestOptions): UmbCancelablePromise(async (resolve, reject, onCancel) => { const xhr = new XMLHttpRequest(); xhr.open(options.method, `${baseUrl}${options.url}`, true); + xhr.withCredentials = options.withCredentials ?? true; // Set default headers if (options.token) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/resources/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/resources/types.ts index 60e415e0d5..887f09c180 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/resources/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/resources/types.ts @@ -7,6 +7,7 @@ export interface XhrRequestOptions extends UmbTryExecuteOptions { baseUrl?: string; method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS'; url: string; + withCredentials?: boolean; body?: unknown; token?: string | (() => undefined | string | Promise); headers?: Record; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/repository/document-permission.server.data.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/repository/document-permission.server.data.ts index 4d2cde0ffc..ae14cd16a9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/repository/document-permission.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/repository/document-permission.server.data.ts @@ -22,6 +22,7 @@ export class UmbDocumentPermissionServerDataSource { this.#host, fetch(`/umbraco/management/api/v1/document/${id}/permissions`, { method: 'GET', + credentials: 'include', headers: { 'Content-Type': 'application/json', },