I have Symfony2 application separated into 2 bundles: BackendBundle
for API and FrontendBundle
for AngularJS "client". Everything works under firewall.
BackendBundle
has entities, handles API routes; FrontendBundle
has Angular views, routing etc. and has only one controller with wildcard:
class AngularController extends Controller {
/**
* @Route("/{route}", name="angular_index_all_unmatched_routes", requirements={"route" = ".*"})
* @Template("FrontendBundle::index.html.twig")
*/
public function angularIndexAction($route) {
return ['route' => $route];
}
}
FrontendBundle
routing is defined as last resource in app/config/routing.yml
, to be invoked only if any other route was not matched. Thanks to that, it can handle Angular HTML5-mode routes if they're accessed directly (for example copy-paste) - and it works ok.
What I want to do, is define firewall and/or access control in way that all those unmatched routes (handled by AngularController::angularIndexAction()
) could be accessible by anonymous user.
Why? I want to open some API routes (via frontend proxy) to be accessible by non-users (for example confirmation URLs sent by email, with some message to user).
I don't want to hardcode access control list for every anonymous "Angular" route, I would like to do it only for API routes. At the end, those unmatched routes should open Angular's index which should know if user is logged in (for displaying full or simplified layout) and should handle Angular routes and display some kind of "Access denied" message if request failed (there is Symfony listener and Angular's $provide
interceptor for that).
Any suggestions?
Edit: @Security
annotation on AngularController::angularIndexAction()
does not work, it still redirects to firewall entry point.
Edit2: Here is fragment of security.yml
firewalls:
unsecured:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
anonymous: true
secured:
pattern: '^.*$'
form_login:
login_path: /our-provider/login
check_path: /our-provider/callback/
anonymous: true
entry_point: our_provider.entry_point
access_control:
- { path: '^/our-provider/(login(/[a-zA-Z]+)?|logout|redirect|callback)', roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: '^/', roles: ROLE_USER }
I know that { path: '^/', roles: ROLE_USER }
will redirect all routes to login page if user is not logged in. I assumed it's obvious and did not mentioned it. What I want is force ROLE_USER
for matched routes and let IS_AUTHENTICATED_ANONYMOUSLY
for those unmatched, without explicitely defining each frontend "proxy-route". In my case there is not 404 Symfony page, because everything goes to angular_index_all_unmatched_routes
route and there Angular routing definition decides if there is something to handle or not.
I haven't tried this, and I cannot begin to guess your existing security/route setup in security.yml
but I guess you could whitelist the method with IS_AUTHENTICATED_ANONYMOUSLY
. From the Symfony docs:
All users (even anonymous ones) have this - this is useful when whitelisting URLs to guarantee access - some details are in How Does the Security access_control Work?.
So, for example, if you were using the @Security
annotation you could do something like (not tested):
class AngularController extends Controller {
/**
* @Route("/{route}", name="route", requirements={"route" = ".*"})
* @Template("FrontendBundle::index.html.twig")
* @Security("has_role('IS_AUTHENTICATED_ANONYMOUSLY')")
*/
public function angularIndexAction($route) {
return ['route' => $route];
}
}
More on the @Security
annotation here.
Hope this helps :)
Edit
All that said, when you define/restrict your routes under access_control
in security.yml
, the matching process stops on the first match. I assume that you have some role-restricted paths, which you should define explicitly - and put them first, so if they match the process stops.
Otherwise, you should be able to add a catch-all route, enforced by role IS_AUTHENTICATED_ANONYMOUSLY
. Since the path definition of a route is a regex, something like ^/
should catch anything that is not explicitly defined. Just make sure and place it after your restricted route definitions.
You would not need for the @Security
annotation in this case.
Edit 2
I tried mocking this out using a clean instance and HTTP BasicAuth but what I was trying to achieve was the following, which I understand as similar to your use case:
/
and /api/
and trigger a HTTP BasicAuth authentication popup/{route}
that would match everything else and authenticate anonymously.My firewall
and access_control
configuration looks like this:
security:
encoders:
# encoder config here
providers:
# provider config here
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
secured:
anonymous: ~
http_basic: ~
access_control:
- { path: ^/$, roles: ROLE_USER }
- { path: ^/api/, roles: ROLE_USER }
- { path: ^/, roles: IS_AUTHENTICATED_ANONYMOUSLY }
Access control paths are regexes, so ^/$
and ^/
are not the same. The former will only match exactly to route /
. The latter will match any route that begins with /
; e.g: /home
, /products
, /contact
etc.
Indeed, the latter will match and anonymously authenticate /api
, but it will not match /api/
, or /api/1
etc. as these are explicitly defined and restricted to ROLE_USER
.
So the general idea is to explicitly and (if possible) exactly match the routes you want to restrict, and declare those first. The last declaration ^/
should openly catch any other route that falls through.