The "Entry Point" & Multiple Firewalls
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeThe authentication system works great! Except for how it behaves when things go wrong. When an API client tries to access a protected endpoint but forgets to send an Authorization
header, they're redirected to the login page. But, why?
Here's what's going on. Whenever an anonymous user comes into a Symfony app and tries to access a protected page, Symfony triggers something called an "entry point". Basically, Symfony wants to be super hip and helpful by instructing the user that they need to login. In a traditional HTML form app, that means redirecting the user to the login page.
But in an api
, we instruct the API client that credentials are needed by returning a 401 response. So, how can we control this entry point? In Guard authentication, you control it with the start()
method.
The start() Method
Return a new JsonResponse
and we'll just say error => 'auth required'
as a start. Then, set the status code to 401:
// ... lines 1 - 17 | |
class JwtTokenAuthenticator extends AbstractGuardAuthenticator | |
{ | |
// ... lines 20 - 79 | |
public function start(Request $request, AuthenticationException $authException = null) | |
{ | |
// called when authentication info is missing from a | |
// request that requires it | |
return new JsonResponse([ | |
'error' => 'auth required' | |
], 401); | |
} | |
} |
To see if it's working, copy the testRequiresAuthentication
method name and run that test:
./vendor/bin/phpunit --filter testRequiresAuthentication
Huh, it didn't change anything: we're still redirected to the login page. I thought Symfony was supposed to call our start()
method in this situation? So what gives?
One Entry Point per Firewall
Open up security.yml
:
security: | |
// ... lines 2 - 8 | |
firewalls: | |
main: | |
pattern: ^/ | |
anonymous: true | |
form_login: | |
# The route name that the login form submits to | |
check_path: security_login_check | |
login_path: security_login_form | |
logout: | |
# The route name the user can go to in order to logout | |
path: security_logout | |
guard: | |
authenticators: | |
- 'jwt_token_authenticator' | |
// ... lines 24 - 32 |
Here's the problem: we have a single firewall. When an anonymous request accesses the site and hits a page that requires a valid user, Symfony has to figure out what one thing to do. If this were a traditional app, we should redirect the user to /login
. If this were an API, we should return a 401 response. But our app is both: we have an HTML frontend and API endpoints. Symfony doesn't really know what one thing to do.
security: | |
// ... lines 2 - 8 | |
firewalls: | |
main: | |
// ... lines 11 - 12 | |
form_login: | |
# The route name that the login form submits to | |
check_path: security_login_check | |
login_path: security_login_form | |
// ... lines 17 - 32 |
The form_login
authentication mechanism has a built-in entry point and it is taking priority. Our cute start()
entry point function is being totally ignored.
But no worries, you can control this! You could add an entry_point
key under your firewall and point to the authenticator service to say "No no no: I want to use my authenticator as the one entry point". But then, our HTML app would break: we still want users on the frontend to be redirected.
Normally, I'm a big advocate of having a single firewall. But this is a perfect use-case for splitting into two firewalls: we really do have two very different authentication systems at work.
Adding the Second Firewall
Above, the main firewall, add a new key called api
: the name is not important. And set pattern: ^/api/
:
security: | |
// ... lines 2 - 8 | |
firewalls: | |
api: | |
pattern: ^/api/ | |
// ... lines 12 - 36 |
That's a regular expression, so it'll match anything starting with /api/
. Oh, and when Symfony boots, it only matches and uses one firewall. Going to /api/something
will use the api
firewall. Everything else will match the main
firewall. And this is exactly what we want.
Add the anonymous
key: we may still want some endpoints to not require authentication:
security: | |
// ... lines 2 - 8 | |
firewalls: | |
api: | |
pattern: ^/api/ | |
anonymous: true | |
stateless: true | |
// ... lines 14 - 36 |
I'll also add stateless: true
. This is kind of cool: it tells Symfony to not store the user in the session. That's perfect: we expect the client to send a valid Authorization
header on every request.
Move the guard authenticator up into the api
firewall:
security: | |
// ... lines 2 - 8 | |
firewalls: | |
api: | |
pattern: ^/api/ | |
anonymous: true | |
stateless: true | |
guard: | |
authenticators: | |
- 'jwt_token_authenticator' | |
main: | |
pattern: ^/ | |
anonymous: true | |
form_login: | |
# The route name that the login form submits to | |
check_path: security_login_check | |
login_path: security_login_form | |
logout: | |
# The route name the user can go to in order to logout | |
path: security_logout | |
// ... lines 28 - 36 |
And that should do it! Now, it will use the start()
method from our authenticator.
Give it a try!
./vendor/bin/phpunit –filter testRequiresAuthentication
It passes! Don't rush into having multiple firewalls, but if you have two very different ways of authentication, it could be useful.
Thank you for writing this tutorial, I’m really enjoying folllowing it.
I started building my web app in a traditional way. After building some Ajax into my app I am thinking it would be cool to make the whole front end talk to the back end via JSON api end points.
Is there any reason why a traditional login form on a web app couldn’t use an api end point for authentication? That way there would be no need for multiple firewalls...