How We Actually Built It:
The Technical Reality of Embedding a Legacy App
Part 1 covered the strategy: embed the legacy application inside the new platform, give users a seamless single-pane experience, and let the client migrate at their own pace — no downtime, no forced cutover. This post covers what that actually took to build — and the specific technical problems that stood between the plan and working software.
Step 1 — Getting the App into an iFrame
The first step was straightforward: modify the application’s Content Security Policy to permit embedding. In an Nginx-served application, that means setting frame-src and frame-ancestors to allow requests only from the platform domain. Once we had access to the config, this was a clean change.
We embedded the whole app in a single iFrame as a proof of concept. The OAuth login screen appeared as expected — followed immediately by a permission denied from login.microsoft.com.
Microsoft does not allow authentication requests to originate from within an iFrame — by design, for good security reasons. The client also needed the application to remain independently accessible and secure outside the platform. So we couldn’t simply bypass auth.
Step 2 — Replacing OAuth with SAML
The platform has built-in support for SAML authentication, and the client’s Power BI environment was already on SAML. The natural move was to bring the legacy web app onto the same identity flow. With everything under the same hood, SSO from the platform login would extend to the application — no re-authentication prompt, no iFrame-blocked Microsoft request.
The application was running in Docker, and the existing OAuth proxy was already isolated in its own container. That made the swap clean: replace the OAuth proxy container with a custom SAML proxy built on Node Express and saml-passport. SAML authentication now protected the web app itself.
With SAML in place, we embedded the app again. Permission denied. Having multiple applications authorized via SSO through the same identity provider does allow a single sign-on for the user — but each application still fires an auth check in the background. And that check is still blocked from within an iFrame.
An alternate authorization path. The platform exposes a REST API (and the support team was genuinely helpful here). When the legacy app detects it’s running inside an iFrame, the SAML proxy container reaches out to the platform’s API rather than Microsoft to obtain identity. The embedded app now operates under the platform’s SAML umbrella entirely — the Microsoft IDP check never fires. Pages rendered correctly, securely, inside the iFrame.
Step 3 — Handing Navigation to the Platform
With auth solved, the next step was navigation. The legacy application’s nav bar needed to disappear when embedded — replaced entirely by the platform’s menu — while still driving the correct page loads inside the iFrame.
A conditional render on the navigation bar handled the hide. For communication between the iFrame and the platform, the Windows Messaging API was the right tool: when a link inside the app is clicked, the app posts a message to its parent iFrame. JavaScript on the platform’s front end picks that up and loads the appropriate page. Despite the application’s complexity, the number of distinct page links was manageable, and this worked cleanly.
Step 4 — State Persistence Across iFrame Loads
Initially the state footprint looked small. A handful of parameters could ride the messaging system — the platform would relay them after each new iFrame loaded. It worked, with some workarounds for a quirky cascade of asynchronous store changes that fired when certain values were set.
Then we discovered the persistence ran deeper than expected.
Three variables needed to persist across all iFrame page loads for all users. Messaging alone wasn’t going to handle this reliably. And the async cascade problem — where setting one store value triggered a ripple of downstream mutations — meant a naive approach would overwrite values before they’d been read.
Redis was already in the Docker stack as part of the SAML proxy setup. We used Vue’s mutation subscription to sync the required variables to user-keyed Redis entries, and added fetch logic to retrieve and set them in the store on load. A second Axios module pointed at the SAML proxy container handled the transport, with POST and GET endpoints for state added to the proxy.
The cascade problem had a pragmatic fix: asynchronously stagger the sets by a small delay. Not the cleanest solution — a proper overhaul with completion hooks would have been better — but with the application slated for deprecation, that was juice not worth the squeeze. Staggered sets gave us reliable persistence across iFrame page loads.
The Result
Python and Flask and Vue, Nuxt and Docker and Nginx — five-plus technologies, not helped by the quality of their existing implementations. The Rower team traversed every one of these pitfalls because of combined breadth of experience across the stack.
A legacy application — 60,000+ lines of fragmented code — fully embedded inside a modern platform. Secure SAML authentication with no Microsoft iFrame conflicts. Navigation controlled by the platform. State persisted across page loads via Redis. End users saw nothing change. The client got the transition runway they needed.
If you’re facing a similar situation — a legacy system that can’t be torn down overnight, users who depend on it daily, and a modernization target that keeps moving — this is the kind of problem Rower is built for.
Technologies & Partners