Buy

The "Entry Point" & Multiple Firewalls

The 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:

90 lines src/AppBundle/Security/JwtTokenAuthenticator.php
... 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:

32 lines app/config/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.

32 lines app/config/security.yml
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/:

36 lines app/config/security.yml
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:

36 lines app/config/security.yml
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:

36 lines app/config/security.yml
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.

Leave a comment!