External login: wait for app-entry-points before the login provider decision#23167
Conversation
…ecision The backoffice boot stopped waiting for app-entry-point extensions to settle before deciding which auth provider to use (regression introduced in #22522). On a slow connection an externally registered authProvider (e.g. Umbraco ID) is not registered yet when the login screen renders, so the user is dropped on the local login instead of being redirected to the external provider. - extension-initializer-base: `loaded` re-arms to `undefined` while a pass is in flight and resolves to `true` unconditionally (including zero extensions), so `.asPromise()` gates correctly and never hangs on a default install (which has no app-entry-points) — the reason the await was removed in the first place. - app.element: restore the awaited boot gate before routing. Tests: - Unit test for the `loaded` signal contract (zero extensions resolves; a late, slow extension is awaited). - Playwright acceptance test that deploys an app-entry-point registering an authProvider after a delay and asserts it is offered on the login screen. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @iOvergaard's task in 8m 54s —— View job PR Review Complete
Review posted as a new comment. Verdict: Approved — one narrow theoretical suggestion about concurrent async callback ordering (not a blocker for the specific fix). |
There was a problem hiding this comment.
Pull request overview
Fixes a regression in the backoffice boot sequence where the login provider decision could occur before appEntryPoint extensions (and their async onInit) finished, causing external auth providers to be missed on slow connections.
Changes:
- Adjust
UmbExtensionInitializerBase.loadedto re-arm while processing and to resolve even when zero extensions exist. - Restore an awaited boot gate in
UmbAppElementto wait for app-entry-points to settle before routing. - Add unit + acceptance coverage for late auth provider registration and wire it into Playwright projects.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Umbraco.Tests.AcceptanceTest/tests/AuthProviderLateRegistration/LateAuthProvider.spec.ts | Adds E2E regression test ensuring a late-registered auth provider appears on the login screen. |
| tests/Umbraco.Tests.AcceptanceTest/tests/AuthProviderLateRegistration/AdditionalSetup/App_Plugins/LateAuthProvider/umbraco-package.json | Adds a public package fixture that contributes an appEntryPoint for the test. |
| tests/Umbraco.Tests.AcceptanceTest/tests/AuthProviderLateRegistration/AdditionalSetup/App_Plugins/LateAuthProvider/entry-point.js | Implements delayed authProvider registration inside onInit to reproduce the race. |
| tests/Umbraco.Tests.AcceptanceTest/playwright.config.ts | Adds a new Playwright project for the unauthenticated late-registration login test. |
| src/Umbraco.Web.UI.Client/src/libs/extension-api/initializers/extension-initializer-base.ts | Updates loaded semantics to avoid hangs (zero extensions) and to re-arm while a pass is running. |
| src/Umbraco.Web.UI.Client/src/libs/extension-api/initializers/extension-initializer-base.test.ts | Adds unit tests for loaded resolving with zero extensions and waiting for late/slow instantiation. |
| src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts | Waits for app-entry-points to finish initializing before routing/login provider decision. |
PR ReviewTarget: Restores the app-entry-point boot gate removed in #22522, fixing the race where externally registered
Suggestions
ApprovedThis looks good to be merged as-is. Root cause analysis is thorough, both unit regression tests cover the exact failure modes (zero-extension hang and late-extension race), and the acceptance test provides an end-to-end guard documented as verified red-on-unfixed / green-on-fixed. Recommend a manual sanity check on the login flow before merging. |
Add a test asserting the collection initializer's `loaded` does not open the gate (`#loadedGuard` awaits it via `.asPromise()`, fronting private-extension and user-permission loading) until the initially-registered extensions have instantiated. Addresses the #22522 "user permissions resolved too late" concern in writing; user-permission condition resolution itself lives in UmbBaseExtensionInitializer (covered by base-extension-initializer.race.test.ts) and is untouched by this change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…review) Address PR review feedback: - extension-initializer-base: only the latest processing pass settles `loaded` (monotonic pass id), so a slow earlier pass can't unblock waiters early when the async observer overlaps passes; and use `Promise.allSettled` so a throwing `instantiateExtension` can't leave `loaded` stuck at `undefined` (hanging the boot gate) — failures are logged rather than swallowed. - playwright.config: narrow the project glob to `**/*.spec.ts` so Playwright doesn't try to load the App_Plugins `entry-point.js` ESM fixture as a test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed the review suggestions in c566dd0:
Still green: 175 extension-api unit tests, and the |
leekelleher
left a comment
There was a problem hiding this comment.
Tested out, works as described. 🚀
…ecision (#23167) * External login: wait for app-entry-points before the login provider decision The backoffice boot stopped waiting for app-entry-point extensions to settle before deciding which auth provider to use (regression introduced in #22522). On a slow connection an externally registered authProvider (e.g. Umbraco ID) is not registered yet when the login screen renders, so the user is dropped on the local login instead of being redirected to the external provider. - extension-initializer-base: `loaded` re-arms to `undefined` while a pass is in flight and resolves to `true` unconditionally (including zero extensions), so `.asPromise()` gates correctly and never hangs on a default install (which has no app-entry-points) — the reason the await was removed in the first place. - app.element: restore the awaited boot gate before routing. Tests: - Unit test for the `loaded` signal contract (zero extensions resolves; a late, slow extension is awaited). - Playwright acceptance test that deploys an app-entry-point registering an authProvider after a delay and asserts it is offered on the login screen. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(backoffice): guard the loaded-gate timing for permission loading Add a test asserting the collection initializer's `loaded` does not open the gate (`#loadedGuard` awaits it via `.asPromise()`, fronting private-extension and user-permission loading) until the initially-registered extensions have instantiated. Addresses the #22522 "user permissions resolved too late" concern in writing; user-permission condition resolution itself lives in UmbBaseExtensionInitializer (covered by base-extension-initializer.race.test.ts) and is untouched by this change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(backoffice): harden loaded signal + narrow acceptance test glob (review) Address PR review feedback: - extension-initializer-base: only the latest processing pass settles `loaded` (monotonic pass id), so a slow earlier pass can't unblock waiters early when the async observer overlaps passes; and use `Promise.allSettled` so a throwing `instantiateExtension` can't leave `loaded` stuck at `undefined` (hanging the boot gate) — failures are logged rather than swallowed. - playwright.config: narrow the project glob to `**/*.spec.ts` so Playwright doesn't try to load the App_Plugins `entry-point.js` ESM fixture as a test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `loaded`-signal test (added in #23167) built its host with `UmbControllerHostElementMixin(HTMLElement)`, mirroring the older `UmbBaseExtensionInitializer` tests. But `UmbExtensionInitializerBase` requires a full `UmbElement` host, so the test failed `tsc` (TS2345) under the root tsconfig. The product build excludes `*.test.ts`, so it slipped through CI but breaks `npm run compile`/the editor. Use `UmbElementMixin(HTMLElement)`, matching what production callers pass (app/backoffice/preview elements are all UmbElements). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
What
The backoffice boot stopped waiting for
appEntryPointextensions to settle before deciding which auth provider to use — a regression introduced in #22522 (the block-permissions PR, which also reworked the extension-initializer lifecycle). On a slow connection an externally registeredauthProvider(e.g. Umbraco ID) registered inside an app-entry-point's asynconInitisn't present yet when the login screen renders, so the login decision is made against a stale registry and the user is dropped on the local login instead of being redirected to the external provider.Reported from Cloud: intermittent on 3G/incognito, reproducible from v17.4.0 onward (and present on v18 RCs).
Root cause
#22522 changed
UmbExtensionInitializerBase.#loadedfrom a one-shotReplaySubjecttoUmbBooleanState(undefined)and removed theawaitthat gated the boot on the app-entry-point initializer. Two problems followed:loadedonly becametrueif (extensions.length > 0), so for a type with zero registered extensions (a default install has no app-entry-points) it never resolved — which is why the boot gate could not simply be re-added (it would hang).onInitbefore the login provider decision.Fix
extension-initializer-base.ts—loadedre-arms toundefinedwhile a processing pass is in flight and resolves totrueunconditionally (including zero extensions)..asPromise()skipsundefined, so this gates correctly and never hangs.app.element.ts— restore the awaited boot gate before routing:await this.observe(entryPointInitializer.loaded).asPromise().Tests
extension-initializer-base.test.ts):loadedmust resolve with zero extensions (the hang), and must not report loaded until a late-registered, slow extension has finished instantiating (the race). Both fail on the unfixed code, pass with the fix.AuthProviderLateRegistration/): deploys anappEntryPointthat registers anauthProvider1.5s after boot and asserts it is offered on the login screen. Verified RED on the unfixed client build and GREEN on the fixed build against a live instance.Non-regression
The tests #22522 introduced/reworked all pass with this change: block language-access controller, read-only + read-only-variant guards, and the extension-initializer condition-race tests (64 tests), plus the full extension-api suite (174).
🤖 Generated with Claude Code