diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 3d8a98f97038..ef9c805df9f6 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -16168,7 +16168,8 @@ } } } - } + }, + "security": [] } }, "/umbraco/management/api/v1/install/setup": { @@ -16231,7 +16232,8 @@ } } } - } + }, + "security": [] } }, "/umbraco/management/api/v1/install/validate-database": { @@ -16294,7 +16296,8 @@ } } } - } + }, + "security": [] } }, "/umbraco/management/api/v1/item/language": { @@ -17555,7 +17558,8 @@ } } } - } + }, + "security": [] } }, "/umbraco/management/api/v1/collection/media": { @@ -28356,7 +28360,8 @@ } } } - } + }, + "security": [] } }, "/umbraco/management/api/v1/profiling/status": { @@ -30679,7 +30684,8 @@ } } } - } + }, + "security": [] } }, "/umbraco/management/api/v1/segment": { @@ -30761,7 +30767,8 @@ } } } - } + }, + "security": [] } }, "/umbraco/management/api/v1/server/information": { @@ -30823,7 +30830,8 @@ } } } - } + }, + "security": [] } }, "/umbraco/management/api/v1/server/troubleshooting": { @@ -36862,7 +36870,8 @@ } } } - } + }, + "security": [] } }, "/umbraco/management/api/v1/user/invite/resend": { @@ -37068,7 +37077,8 @@ } } } - } + }, + "security": [] } }, "/umbraco/management/api/v1/user/set-user-groups": { diff --git a/src/Umbraco.Cms.Api.Management/OpenApi/Transformers/BackOfficeSecurityRequirementsTransformer.cs b/src/Umbraco.Cms.Api.Management/OpenApi/Transformers/BackOfficeSecurityRequirementsTransformer.cs index 58944f4ca1b8..e6036edab4c6 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi/Transformers/BackOfficeSecurityRequirementsTransformer.cs +++ b/src/Umbraco.Cms.Api.Management/OpenApi/Transformers/BackOfficeSecurityRequirementsTransformer.cs @@ -26,11 +26,20 @@ public Task TransformAsync( OpenApiOperationTransformerContext context, CancellationToken cancellationToken) { - if (context.Description.ActionDescriptor is not ControllerActionDescriptor description || - description.MethodInfo.GetCustomAttributes(true).Any(x => x is AllowAnonymousAttribute) || + if (context.Description.ActionDescriptor is not ControllerActionDescriptor description) + { + return Task.CompletedTask; + } + + if (description.MethodInfo.GetCustomAttributes(true).Any(x => x is AllowAnonymousAttribute) || description.MethodInfo.DeclaringType?.GetCustomAttributes(true).Any(x => x is AllowAnonymousAttribute) == true) { + // Explicitly clear security on anonymous operations so they override the document-level + // security requirement added below. Without this, OpenAPI consumers (including the + // generated backoffice SDK) treat these endpoints as authenticated and attach a Bearer + // token, which triggers a /token refresh before the user has logged in. + operation.Security = []; return Task.CompletedTask; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/sdk.gen.ts b/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/sdk.gen.ts index d2c66aebbf18..510d013e5889 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/sdk.gen.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/sdk.gen.ts @@ -3756,12 +3756,6 @@ export class InstallService { */ public static getInstallSettings(options?: Options) { return (options?.client ?? client).get({ - security: [ - { - scheme: 'bearer', - type: 'http' - } - ], url: '/umbraco/management/api/v1/install/settings', ...options }); @@ -3774,12 +3768,6 @@ export class InstallService { */ public static postInstallSetup(options: Options) { return (options.client ?? client).post({ - security: [ - { - scheme: 'bearer', - type: 'http' - } - ], url: '/umbraco/management/api/v1/install/setup', ...options, headers: { @@ -3796,12 +3784,6 @@ export class InstallService { */ public static postInstallValidateDatabase(options: Options) { return (options.client ?? client).post({ - security: [ - { - scheme: 'bearer', - type: 'http' - } - ], url: '/umbraco/management/api/v1/install/validate-database', ...options, headers: { @@ -4160,12 +4142,6 @@ export class ManifestService { */ public static getManifestManifestPublic(options?: Options) { return (options?.client ?? client).get({ - security: [ - { - scheme: 'bearer', - type: 'http' - } - ], url: '/umbraco/management/api/v1/manifest/manifest/public', ...options }); @@ -6824,12 +6800,6 @@ export class PreviewService { */ public static deletePreview(options?: Options) { return (options?.client ?? client).delete({ - security: [ - { - scheme: 'bearer', - type: 'http' - } - ], url: '/umbraco/management/api/v1/preview', ...options }); @@ -7482,12 +7452,6 @@ export class SecurityService { */ public static postSecurityForgotPasswordVerify(options: Options) { return (options.client ?? client).post({ - security: [ - { - scheme: 'bearer', - type: 'http' - } - ], url: '/umbraco/management/api/v1/security/forgot-password/verify', ...options, headers: { @@ -7526,12 +7490,6 @@ export class ServerService { */ public static getServerConfiguration(options?: Options) { return (options?.client ?? client).get({ - security: [ - { - scheme: 'bearer', - type: 'http' - } - ], url: '/umbraco/management/api/v1/server/configuration', ...options }); @@ -7562,12 +7520,6 @@ export class ServerService { */ public static getServerStatus(options?: Options) { return (options?.client ?? client).get({ - security: [ - { - scheme: 'bearer', - type: 'http' - } - ], url: '/umbraco/management/api/v1/server/status', ...options }); @@ -9120,12 +9072,6 @@ export class UserService { */ public static postUserInviteCreatePassword(options: Options) { return (options.client ?? client).post({ - security: [ - { - scheme: 'bearer', - type: 'http' - } - ], url: '/umbraco/management/api/v1/user/invite/create-password', ...options, headers: { @@ -9164,12 +9110,6 @@ export class UserService { */ public static postUserInviteVerify(options: Options) { return (options.client ?? client).post({ - security: [ - { - scheme: 'bearer', - type: 'http' - } - ], url: '/umbraco/management/api/v1/user/invite/verify', ...options, headers: { diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/OpenApi/BackOfficeSecurityRequirementsTransformerTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/OpenApi/BackOfficeSecurityRequirementsTransformerTests.cs index 72a5a09cbb56..e430e0c18479 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/OpenApi/BackOfficeSecurityRequirementsTransformerTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/OpenApi/BackOfficeSecurityRequirementsTransformerTests.cs @@ -109,7 +109,7 @@ public async Task TransformAsync_Document_Preserves_Existing_Components() #region Operation Transformer Tests [Test] - public async Task TransformAsync_Operation_Skips_AllowAnonymous_Methods() + public async Task TransformAsync_Operation_Overrides_Security_For_AllowAnonymous_Methods() { // Arrange var operation = new OpenApiOperation(); @@ -134,13 +134,14 @@ public async Task TransformAsync_Operation_Skips_AllowAnonymous_Methods() // Act await _transformer.TransformAsync(operation, context, CancellationToken.None); - // Assert - Should not add 401 response or security + // Assert - Should not add 401 response, and security must be an empty list to override document-level security Assert.IsFalse(operation.Responses?.ContainsKey(StatusCodes.Status401Unauthorized.ToString()) ?? false); - Assert.IsNull(operation.Security); + Assert.IsNotNull(operation.Security); + Assert.IsEmpty(operation.Security); } [Test] - public async Task TransformAsync_Operation_Skips_AllowAnonymous_Controllers() + public async Task TransformAsync_Operation_Overrides_Security_For_AllowAnonymous_Controllers() { // Arrange var operation = new OpenApiOperation(); @@ -165,9 +166,10 @@ public async Task TransformAsync_Operation_Skips_AllowAnonymous_Controllers() // Act await _transformer.TransformAsync(operation, context, CancellationToken.None); - // Assert - Should not add 401 response or security + // Assert - Should not add 401 response, and security must be an empty list to override document-level security Assert.IsFalse(operation.Responses?.ContainsKey(StatusCodes.Status401Unauthorized.ToString()) ?? false); - Assert.IsNull(operation.Security); + Assert.IsNotNull(operation.Security); + Assert.IsEmpty(operation.Security); } [Test]