vagabit / fos-js-routing-security-bundle
Role-filtered and security.yaml access-control-aware route exposure for FOSJsRoutingBundle
Package info
github.com/vagabit-dev/fos-js-routing-security-bundle
Type:symfony-bundle
pkg:composer/vagabit/fos-js-routing-security-bundle
Requires
- php: >=8.1
- friendsofsymfony/jsrouting-bundle: ^3.0
- symfony/cache: ^6.4 || ^7.4 || ^8.1
- symfony/framework-bundle: ^6.4 || ^7.4 || ^8.1
- symfony/http-foundation: ^6.4 || ^7.4 || ^8.1
- symfony/routing: ^6.4 || ^7.4 || ^8.1
- symfony/security-bundle: ^6.4 || ^7.4 || ^8.1
- symfony/security-core: ^6.4 || ^7.4 || ^8.1
- symfony/security-http: ^6.4 || ^7.4 || ^8.1
- symfony/yaml: ^6.4 || ^7.4 || ^8.1
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- phpunit/phpunit: ^9.6 || ^10.0
- symfony/browser-kit: ^6.4 || ^7.4 || ^8.1
- symfony/phpunit-bridge: ^6.4 || ^7.4 || ^8.1
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Extends FOSJsRoutingBundle to dynamically filter and serve exposed routes based on user roles and permissions.
The fundamental purpose of FOSJsRoutingBundle is to maximize developer productivity by
eliminating manual URL hardcoding in JavaScript.
It brings Symfony’s route generation mechanism (Routing.generate('route_name', { id: 1 }))
directly to frontend applications.
Standard FOSJsRouting operates on a public, all-or-nothing model. Any route marked expose: true is exposed to every visitor. While this is not a security issue "per se", it presents a clear security and efficiency drawback:
internal and administrative endpoints (path structures, parameter schemas, and route names) are leaked
to users who cannot use them, increasing the attack surface without providing any functional benefit.
This extension optimizes the system by aligning route delivery with actual user utility and access permissions:
- Automated Route Filtering: for every exposed route, it evaluates Symfony’s security.yaml access_control rules and custom route-level role / roles options against the current user's security token.
- Information Minimization: Users only receive route definitions for endpoints they are actually authorized to access. Anonymous users receive only public routes; administrative routes are only transmitted to users holding ROLE_ADMIN.
- Security and Payload Reduction: Removing inaccessible routes shrinks payload size across the wire and prevents reconnaissance on sensitive backend structures.
The only drawback of this bundle is that routes can not be dumped altogether in a single static cacheable file bust must be served dynamically, a different caching mechanism is provided to avoid performance overhead.
Features
- Zero-Configuration Activation: Automatically registers and decorates
fos_js_routing.extractoras soon as the bundle is enabled inbundles.php—no manual service wiring inservices.yamlneeded. - Centralized Security Access Control: Intercepts route extraction and matches route paths against
security.yamlaccess_controlrules viaSymfony\Component\Security\Http\AccessMapInterface. - Single & Multiple Role Route Options: Supports declaring single (
role) or multiple (roles) required roles directly on the route definition (e.g.options: ['expose' => true, 'role' => 'ROLE_MANAGER']oroptions: ['expose' => true, 'roles' => ['ROLE_ADMIN', 'ROLE_MANAGER']]). - Role-Based Server-Side Caching: Controller endpoint (
/vagabit-secure-js-routes.json) caches route definitions keyed by the user's role combination (js_routes_<hash>), isolating cache entries per role configuration without repetitive path matching on every request. - Role-Based Route Debugger Command: Interactive console command (
vagabit:js-routing:debug) to inspect route exposure, separating exposed from non-exposed routes and debugging role accessibility rules. - Configurable HTTP Cache & Security Headers: Secure by default with
private, no-storeheaders, with configurable browser caching (max_age), revalidation (must_revalidate), public/private visibility, and conditional ETag handling (304 Not Modified).
Installation & Setup
1. Install via Composer
composer require vagabit/fos-js-routing-security-bundle:^1.0.0-beta1
2. Enable the Bundle
If you use Symfony Flex, the bundle is enabled automatically upon installation. Otherwise, add it to config/bundles.php:
// config/bundles.php return [ // ... FOS\JsRoutingBundle\FOSJsRoutingBundle::class => ['all' => true], Vagabit\FOSJsRoutingSecurityBundle\FOSJsRoutingSecurityBundle::class => ['all' => true], ];
3. Configure
3.1 Register Controller Routing
Because Symfony bundles do not automatically inject route definitions into host applications, register the bundle's controller routes in your application's routing configuration (e.g. config/routes/fos_js_routing_security.yaml):
# config/routes/fos_js_routing_security.yaml vagabit_fos_js_routing_security: resource: '@FOSJsRoutingSecurityBundle/src/Controller/' type: attribute
Important: Do not import the old FOSJsRouting route file (
@FOSJsRoutingBundle/Resources/config/routing/routing-sf4.xmlorfos_js_routing_js). The legacy endpoint bypasses this bundle's role-based response cache and HTTP security headers. Always use the secured endpoint (/vagabit-secure-js-routes.jsonor route namevagabit_secure_js_routes).
3.2 Configure Security Access Rules (security.yaml)
If you haven't already done so, configure route role access in security.yaml. The bundle uses these rules to determine which routes to serve based on the user's role:
# config/packages/security.yaml security: access_control: - { path: ^/admin, roles: ROLE_ADMIN } - { path: ^/account, roles: ROLE_USER }
3.3 Exposing Routes with Options
Alternatively (or additionally) routes can be mae available to roles with the custom role option.
Via security.yaml Matching
#[Route('/admin/dashboard', name: 'admin_dashboard', options: ['expose' => true])] public function dashboard(): Response { // ... }
Via Custom Single role Option
#[Route('/manager/reports', name: 'manager_reports', options: ['expose' => true, 'role' => 'ROLE_MANAGER'])] public function reports(): Response { // ... }
Via Custom Multiple roles Option
You can supply an array of roles using either the roles or role option. If the authenticated user has at least one of the specified roles, the route is exposed:
#[Route(
'/staff/reports',
name: 'staff_reports',
options: [
'expose' => true,
'roles' => ['ROLE_ADMIN', 'ROLE_MANAGER'], // Accessible to either ROLE_ADMIN or ROLE_MANAGER
]
)]
public function staffReports(): Response
{
// ...
}
4. Usage
To maximize developer productivity and application responsiveness, client-side route generation should integrate cleanly with your frontend architecture while preserving role-based endpoint isolation.
Note: Given the dynamic way of serving the js routes by role:
- routes can not be dumped with
fos:js-routing:dumpcommand- routes can not be used with Webpack plugin (
FosRouting)
Option A: Modern Frontend Bundlers (Vite, Webpack, AssetMapper, npm/yarn)
Install the client-side routing library:
npm install fos-routing --save
# or
yarn add fos-routing
Initializing Routes Before Application Bootstrap
Because route generation (Routing.generate()) is synchronous, route definitions must be loaded and registered into the router instance before the frontend application initializes or mounts its root view/components. If components or lifecycle hooks attempt to generate URLs before route data is registered, route resolution will fail.
A standard pattern is to wrap application initialization in an asynchronous bootstrap routine that fetches /vagabit-secure-js-routes.json, registers the payload via Routing.setData(), and then mounts/boots the application:
import Routing from 'fos-routing'; async function bootstrapApp() { // 1. Fetch role-filtered route definitions const response = await fetch('/vagabit-secure-js-routes.json', { headers: { 'Accept': 'application/json' } }); const routesData = await response.json(); // 2. Register route data in the router Routing.setData(routesData); // 3. Initialize / mount your application once routes are ready // initApp(); } bootstrapApp().catch((error) => { console.error('Failed to initialize routes and bootstrap application:', error); });
Once initialized during bootstrap, Routing can be imported and called synchronously across any component, utility, or module:
// In any application module or component: import Routing from 'fos-routing'; const url = Routing.generate('admin_dashboard');
Option B: Server-Rendered Twig Layouts
For traditional multi-page applications, publish bundle assets, include the router library, and initialize route data by fetching the secured endpoint in your base template (templates/base.html.twig):
php bin/console assets:install --symlink
<!-- 1. Load Router Library --> <script src="{{ asset('bundles/fosjsrouting/js/router.min.js') }}"></script> <!-- 2. Fetch and register dynamic role-filtered routes --> <script> fetch('{{ path('vagabit_secure_js_routes') }}') .then(response => response.json()) .then(data => fos.Router.setData(data)); </script>
Route Generation Examples
Once initialized, use Routing.generate() across frontend modules:
// 1. Basic route generation const homeUrl = Routing.generate('public_home'); // 2. Route generation with parameters const userUrl = Routing.generate('account_profile', { id: 42 }); // 3. Absolute URL generation (pass true as the third argument) const absoluteUrl = Routing.generate('public_home', {}, true);
5. Configure Cache Lifetime, Prefix, Tag & HTTP Cache (Optional)
Generating js routes json on each request can be a heavy task especially for a complex application, for this reason the bundle provides a specific cache system.
You can configure server-side and HTTP response caching behavior in your Symfony configuration (config/packages/fos_js_routing_security.yaml):
# config/packages/fos_js_routing_security.yaml fos_js_routing_security: # Server-side Cache TTL in seconds (integer or null). # - Positive integer (> 0): Cache expires after N seconds (e.g. 3600 for 1 hour). # - null (or ~ or "never"): Indefinite cache lifetime (entries never expire automatically). # - 0: Immediate expiration (disables caching / fresh extraction on every request). cache_ttl: 3600 # Custom cache key prefix prepended to role hashes (default: 'js_routes_') cache_prefix: 'js_routes_' # Cache tag applied to route items for selective tag-based invalidation (default: 'fos_js_routing_security') # Set to null or '' to disable cache tagging. cache_tag: 'fos_js_routing_security' # Role combinations to pre-warm in cache during cache:warmup or via console command warmup_roles: - ROLE_ANONYMOUS - ROLE_USER - [ROLE_ADMIN, ROLE_USER] # HTTP Client / Browser Cache-Control settings for /vagabit-secure-js-routes.json http_cache: # Mark response as private (recommended for user-specific routes, default: true) private: true # Disallow storage completely with no-store (default: true) no_store: true # Max-age in seconds for browser caching when no_store is false (default: 0) max_age: 0 # Require cache revalidation before using cached entry (default: false) must_revalidate: false # Enable ETag header generation and 304 Not Modified conditional responses (default: false) etag: false
HTTP Cache Configurations
Depending on your application's architecture and performance requirements, you can adjust http_cache:
- Maximum Security & Session Isolation (Default):
private: true, no_store: trueprevents storing routes in browser or intermediate caches, ensuring role changes or logouts take effect instantly. - Short-Lived Browser Cache:
private: true, no_store: false, max_age: 300caches route definitions privately in the user's browser for 5 minutes, eliminating repeat network requests during active navigation. - Conditional Validation via ETag (
304 Not Modified):private: true, no_store: false, etag: true, must_revalidate: trueallows the browser to revalidate viaIf-None-Match. On match, the server returns an empty304 Not Modifiedpayload without transferring route data.
Cache Warmer
The bundle includes Vagabit\FOSJsRoutingSecurityBundle\Service\JsRoutesCacheWarmer, which pre-populates role-filtered routes in your application cache so end users always hit a warm cache on first request:
1. Automatic Warmup on cache:warmup
The cache warmer implements Symfony's CacheWarmerInterface (kernel.cache_warmer) and is executed automatically whenever cache:warmup or cache:clear runs:
php bin/console cache:warmup
2. On-Demand Warmup Command
You can also warm up routes on-demand (e.g. during CI/CD deployment pipelines):
# Warm up all configured role combinations php bin/console vagabit:js-routing:warmup # Warm up specific role combinations on the fly php bin/console vagabit:js-routing:warmup --role=ROLE_ADMIN --role=ROLE_USER
Debugging Routes by Role (vagabit:js-routing:debug)
The bundle provides a dedicated debugging command to inspect route exposure and verify which security roles are granted access to each route in the JavaScript router.
1. Default Route Dump (Exposed vs Non-Exposed)
By default, the command inspects all registered application routes, separating exposed routes from non-exposed routes, and lists the authorized roles for each exposed route (evaluating route role/roles options and security.yaml access_control rules):
php bin/console vagabit:js-routing:debug
Example output:
------------------ ------------------ ----------- --------------------------------------
Route Name Path Method(s) Accessible Roles in JS
------------------ ------------------ ----------- --------------------------------------
public_home /public/home ANY PUBLIC (All roles / Anonymous)
admin_dashboard /admin/dashboard GET access_control: [ROLE_ADMIN]
account_profile /account/profile ANY access_control: [ROLE_USER]
manager_reports /manager/reports ANY Route Option: [ROLE_MANAGER]
------------------ ------------------ ----------- --------------------------------------
------------------- -------------------------- -----------
Route Name Path Method(s)
------------------- -------------------------- -----------
fos_js_routing_js /js/routing.{_format} GET
fos_js_routing_data /js_routes.{_format} GET
------------------- -------------------------- -----------
2. Filter by Role(s)
To see only the exposed routes that can be accessed by specific roles, use the --role (or -r) option with one or more roles (comma-separated or multiple flags):
# Filter routes accessible to ROLE_ADMIN php bin/console vagabit:js-routing:debug --role=ROLE_ADMIN # Filter routes accessible to users holding either ROLE_ADMIN or ROLE_USER php bin/console vagabit:js-routing:debug --role=ROLE_ADMIN,ROLE_USER # Alternative syntax using multiple flags php bin/console vagabit:js-routing:debug -r ROLE_ADMIN -r ROLE_USER
3. Inspect a Specific Route by Name or Path
To inspect a single route's full configuration, JavaScript exposure state, and security access rules, use the --route option (or --name, --path, or as a positional argument):
# Inspect by route name php bin/console vagabit:js-routing:debug --route=admin_dashboard # Inspect by route path php bin/console vagabit:js-routing:debug --route=/admin/dashboard # Positional argument syntax php bin/console vagabit:js-routing:debug admin_dashboard # Combine with --role to check if a specific route is accessible for given role(s) php bin/console vagabit:js-routing:debug --route=admin_dashboard --role=ROLE_ADMIN
Example inspection output:
Route Details: admin_dashboard
+------------------------+------------------------------------+
| Property | Value |
+------------------------+------------------------------------+
| Route Name | admin_dashboard |
| Path | /admin/dashboard |
| Method(s) | GET |
| Scheme(s) | ANY |
| Host | ANY |
| Exposed to JavaScript | Yes |
| Route Option Role(s) | None |
| access_control Rule(s) | ROLE_ADMIN |
| Accessible Roles in JS | access_control: [ROLE_ADMIN] |
+------------------------+------------------------------------+
Selective Cache Invalidation
When using a Symfony tag-aware cache pool (such as TagAwareAdapter, Redis, or Memcached), all cached role combinations can be invalidated simultaneously when routing or security rules change without clearing the entire application cache:
Via PHP Code:
use Symfony\Contracts\Cache\TagAwareCacheInterface; // Invalidate all role route caches at once $cache->invalidateTags(['fos_js_routing_security']);
Via Symfony Console:
php bin/console cache:pool:invalidate-tags fos_js_routing_security
Contributing
Please see if you want to contribute to the library CONTRIBUTING.md.
License
This project is licensed under the MIT License - see the LICENSE file for details.