diff --git a/build/nightly-E2E-test-pipelines.yml b/build/nightly-E2E-test-pipelines.yml index 1de1217a33..74c6f7b0f7 100644 --- a/build/nightly-E2E-test-pipelines.yml +++ b/build/nightly-E2E-test-pipelines.yml @@ -9,6 +9,7 @@ schedules: branches: include: - v15/dev + - v16/dev - main - v17/dev @@ -695,4 +696,4 @@ stages: --data "$PAYLOAD" \ "$SLACK_WEBHOOK_URL" env: - SLACK_WEBHOOK_URL: $(E2ESLACKWEBHOOKURL) \ No newline at end of file + SLACK_WEBHOOK_URL: $(E2ESLACKWEBHOOKURL) diff --git a/src/Umbraco.Web.UI.Client/src/external/openid/src/redirect_based_handler.ts b/src/Umbraco.Web.UI.Client/src/external/openid/src/redirect_based_handler.ts index e9bb94a8a7..eb110ba16a 100644 --- a/src/Umbraco.Web.UI.Client/src/external/openid/src/redirect_based_handler.ts +++ b/src/Umbraco.Web.UI.Client/src/external/openid/src/redirect_based_handler.ts @@ -77,6 +77,41 @@ export class RedirectRequestHandler extends AuthorizationRequestHandler { }); } + /** + * Cleanup all stale authorization requests and configurations from storage. + * This scans localStorage for any keys matching the appauth patterns and removes them, + * including the authorization request handle key. + */ + public cleanupStaleAuthorizationData(): Promise { + // Check if we're in a browser environment with localStorage + if (typeof window === 'undefined' || !window.localStorage) { + return Promise.resolve(); + } + + const keysToRemove: string[] = []; + + // Scan localStorage for all appauth-related keys + for (let i = 0; i < window.localStorage.length; i++) { + const key = window.localStorage.key(i); + if ( + key && + (key.includes('_appauth_authorization_request') || + key.includes('_appauth_authorization_service_configuration') || + key === AUTHORIZATION_REQUEST_HANDLE_KEY) + ) { + keysToRemove.push(key); + } + } + + // Remove all found stale keys + const removePromises = keysToRemove.map((key) => this.storageBackend.removeItem(key)); + return Promise.all(removePromises).then(() => { + if (keysToRemove.length > 0) { + log(`Cleaned up ${keysToRemove.length} stale authorization data entries`); + } + }); + } + /** * Attempts to introspect the contents of storage backend and completes the * request. @@ -119,12 +154,8 @@ export class RedirectRequestHandler extends AuthorizationRequestHandler { } else { authorizationResponse = new AuthorizationResponse({ code: code, state: state }); } - // cleanup state - return Promise.all([ - this.storageBackend.removeItem(AUTHORIZATION_REQUEST_HANDLE_KEY), - this.storageBackend.removeItem(authorizationRequestKey(handle)), - this.storageBackend.removeItem(authorizationServiceConfigurationKey(handle)), - ]).then(() => { + // cleanup all authorization data including current and stale entries + return this.cleanupStaleAuthorizationData().then(() => { log('Delivering authorization response'); return { request: request, @@ -134,7 +165,10 @@ export class RedirectRequestHandler extends AuthorizationRequestHandler { }); } else { log('Mismatched request (state and request_uri) dont match.'); - return Promise.resolve(null); + // cleanup all authorization data even on mismatch to prevent stale PKCE data + return this.cleanupStaleAuthorizationData().then(() => { + return null; + }); } }) ); 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 0e02546423..fc6f5d1ab2 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 @@ -247,6 +247,10 @@ export class UmbAuthFlow { // clear the internal state this.#tokenResponse.setValue(undefined); + + // Also cleanup any OAuth/PKCE artifacts that may still be in localStorage + // This is a defense-in-depth measure during logout + await this.#authorizationHandler.cleanupStaleAuthorizationData(); } /** diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index 0f160e4733..953a56cd0c 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -27,4 +27,4 @@ "dotenv": "^16.3.1", "node-fetch": "^2.6.7" } -} +} \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/Clipboard/ClipboardBlockGridBlocks.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/Clipboard/ClipboardBlockGridBlocks.spec.ts index 0cfdccd38b..6c3f51690f 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/Clipboard/ClipboardBlockGridBlocks.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/Clipboard/ClipboardBlockGridBlocks.spec.ts @@ -106,7 +106,8 @@ test('can copy and paste a single block into the same document but different gro await umbracoUi.content.doesBlockEditorBlockWithNameContainValue(elementGroupName, elementPropertyName, ConstantHelper.inputTypes.tipTap, blockPropertyValue); }); -test('can copy and paste a single block into another document', async ({umbracoApi, umbracoUi}) => { +// Remove skip after this issue is resolved: https://github.com/umbraco/Umbraco-CMS/issues/20680 +test.skip('can copy and paste a single block into another document', async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.document.ensureNameNotExists(secondContentName); await umbracoApi.document.createDefaultDocumentWithABlockGridEditorAndBlockWithValue(contentName, documentTypeName, blockGridDataTypeName, elementTypeId, AliasHelper.toAlias(elementPropertyName), blockPropertyValue, richTextDataTypeUiAlias); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/Clipboard/ClipboardBlockListBlocks.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/Clipboard/ClipboardBlockListBlocks.spec.ts index 615112ef45..40ccfe598c 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/Clipboard/ClipboardBlockListBlocks.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/Clipboard/ClipboardBlockListBlocks.spec.ts @@ -106,7 +106,8 @@ test('can copy and paste a single block into the same document but different gro await umbracoUi.content.doesBlockEditorBlockWithNameContainValue(elementGroupName, elementPropertyName, ConstantHelper.inputTypes.tipTap, blockPropertyValue); }); -test('can copy and paste a single block into another document', async ({umbracoApi, umbracoUi}) => { +// Remove skip after this issue is resolved: https://github.com/umbraco/Umbraco-CMS/issues/20680 +test.skip('can copy and paste a single block into another document', async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.document.ensureNameNotExists(secondContentName); await umbracoApi.document.createDefaultDocumentWithABlockListEditorAndBlockWithValue(contentName, documentTypeName, blockListDataTypeName, elementTypeId, AliasHelper.toAlias(elementPropertyName), blockPropertyValue, elementDataTypeUiAlias, groupName); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Webhook/Webhook.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Webhook/Webhook.spec.ts index 51544486d5..2caf826919 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Webhook/Webhook.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Webhook/Webhook.spec.ts @@ -14,7 +14,7 @@ test.afterEach(async ({umbracoApi}) => { await umbracoApi.webhook.ensureNameNotExists(webhookName); }); -test('can create a webhook', {tag: '@release'}, async ({umbracoApi, umbracoUi}) => { +test('can create a webhook', async ({umbracoApi, umbracoUi}) => { // Arrange const event = 'Content Deleted'; const webhookSiteUrl = umbracoApi.webhook.webhookSiteUrl + webhookSiteToken; @@ -122,7 +122,7 @@ test('can disable a webhook', async ({umbracoApi, umbracoUi}) => { await umbracoApi.webhook.isWebhookEnabled(webhookName, false); }); -test('cannot remove all events from a webhook', {tag: '@release'}, async ({umbracoApi, umbracoUi}) => { +test('cannot remove all events from a webhook', async ({umbracoApi, umbracoUi}) => { // Arrange const event = 'Content Deleted'; await umbracoApi.webhook.createDefaultWebhook(webhookName, webhookSiteToken, event);