Building an Embedded Shopify App in Symfony: Token Exchange and PHP Security
A deep dive into developing embedded Shopify applications using Symfony 7.4 and PHP 8.5, covering session tokens, token exchange, and undocumented PHP challenges.

Stock photo for illustration only, not from the actual event
- Developers using languages other than Node face unique challenges handling embedded apps inside iframes.
- Embedded Shopify apps cannot use PHP sessions, relying instead on short-lived JWT session tokens valid for one minute.
- Token exchange utilizes the OAuth 2.0 token exchange standard (RFC 8693) instead of traditional authorization-code flows.
- This guide features production-grade code from StockPilot, a live app running on Symfony 7.4 and PHP 8.5.
Shopify's official application documentation notoriously provides one primary, first-class path utilizing Node, the official CLI, and a Remix template that handles authentication automatically. Choosing any alternative programming language means stepping off the paved path immediately, primarily because all critical interactions occur before your framework's router ever processes the incoming request. The application operates inside an iframe within the Shopify admin dashboard, preventing browsers from issuing cookies in that context, while required Admin API tokens must be acquired dynamically on the fly.
None of these implementation steps are inherently difficult; rather, comprehensive documentation simply does not exist outside the JavaScript ecosystem. This publication outlines the complete authentication and security workflow for an embedded Shopify application built on Symfony 7.4 and PHP 8.5, extracted directly from StockPilot—a live application that successfully cleared Shopify's App Store review process. Every code snippet provided below represents actual production code rather than conceptual sketches.
An embedded application resides directly within admin.shopify.com inside a cross-origin iframe architecture. Because third-party cookies are entirely deprecated in this environment, traditional PHP sessions are unavailable and will remain permanently inaccessible. Shopify addresses this constraint through a short-lived JSON Web Token known as a session token. App Bridge, Shopify's JavaScript library served via their CDN, generates one token per request with a one-minute expiration window, automatically appending it to every same-origin fetch() request executed by the page.
This mechanism necessitates a structural separation that most PHP developers do not implement by default:
- The application shell and admin UI: Designed for high-speed delivery, simple caching, and zero server-side authentication overhead.
- The application API requests: Fully stateless endpoints requiring session token verification on every single incoming request.
This architectural split intentionally inverts standard Symfony instincts, which typically dictate securing controllers and rendering data on the server side. In this environment, server-side data rendering inside the shell would require authenticating a page load completely devoid of valid credentials. The resulting advantage is a trivially cacheable shell backed by a clean security surface consisting of a single firewall managing one path prefix.
Security configurations within config/packages/security.yaml follow this precise structure:
- webhooks: Authenticators verify HMAC signatures directly within controllers rather than utilizing firewall rules.
- api: Handles embedded admin API requests by validating App Bridge session tokens as stateless connections.
- main: Configured as lazy with user providers stored directly in memory.
The session token operates as an HS256-signed JWT utilizing the application's client secret. While verifying cryptographic signatures represents standard practice, subsequent claim validation steps are frequently omitted despite closing critical security vulnerabilities. Omitting audience (aud) validation accepts tokens minted for unrelated applications sharing the same algorithm, whereas ignoring issuer and destination (iss / dest) validation permits mismatched shop credentials.
Mastering Shopify's security architecture within an enterprise framework like Symfony is crucial for maintaining compliance with strict App Store guidelines. By enforcing stateless APIs and rigorous JWT validation, developers effectively eliminate common vulnerabilities such as Cross-Site Request Forgery (CSRF) and cross-origin attacks, bridging the gap between traditional PHP session management and modern token-based architectures.
Furthermore, understanding edge cases like translation engine parsing rules prevents silent failures in production environments, highlighting the importance of thorough end-to-end testing.
A session token merely establishes the identity of the requester but does not grant direct access to the Admin API. To acquire API access, the session token must be traded for an access token via OAuth 2.0 token exchange (RFC 8693), bypassing legacy authorization-code redirect flows entirely. With managed installations, this approach streamlines the installation sequence completely, eliminating dedicated /auth routes, redirection loops, and callback endpoints. The initial authenticated request from a newly installed shop triggers the exchange immediately.
Webhooks operate independently of session tokens, relying instead on cryptographic signatures. Specifically, Shopify calculates an HMAC-SHA256 signature over the raw request body, base64-encoded within the X-Shopify-Hmac-Sha256 header. Raw data processing must occur prior to any JSON decoding or middleware interaction. Automated Shopify App Store reviews explicitly test this requirement by dispatching intentionally corrupted signatures, expecting a strict HTTP 401 response.
Additional production requirements include scoped frame-ancestors Content Security Policies calculated per request using shop parameters, alongside critical App Bridge placement rules. App Bridge must load synchronously as the absolute first script inside the <head> tag directly from Shopify's CDN, prohibiting bundling, deferral, or asynchronous loading on embedded pages.
Production environments can also reveal subtle framework quirks, such as Symfony translation issues where subject lines like digest.attachment : ' Attached: your restock list (%count% items).' unexpectedly truncated their initial words. Because %count% evaluates as a numeric value, Symfony routes the string through pluralization logic containing regular expressions matching explicit-interval syntax, treating introductory words followed by colons as rule definitions without throwing errors or generating logs. The primary takeaway involves avoiding colon-prefixed phrasing directly adjacent to count variables and ensuring test suites assert strings starting from their exact initial character.
Source: Dev.to
Found something wrong in this article? Report an issue with this article
Comments
Leave a Comment