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!

  • 2017-04-29 Murilo Lobato

    Thanks for the reply! I made what you, and it works like a charm!

    In the future, when I want to expose this same API to other clients, I think I'll put that API behind another firewall and create a OAuth2 auth. This way my HTML app will be able to generate tokens and pass it to twig, and my clients will alse be able to create tokens in an specific controller in that API. I got these conclusions with the OAuth2 tutorial, which is also great!

    Thank you!

  • 2017-04-19 weaverryan

    Hey Murilo Lobato!

    Great question! And this is something that people overlook often when building an API. In short, if your site has a normal, cookie-based login form, then your AJAX calls to your API will automatically use that session cookie and will be authenticated. So, if you're building an API only for your own JavaScript to consume, you can likely skip any special authentication entirely. Just make sure you have only *one* firewall (with your login form) so that the HTML pages and API calls all use the same security system.

    Cheers!

  • 2017-04-18 Murilo Lobato

    Hello! Nice tutorial!

    I have a question... What if I want that the users authenticated in my HTML form app could be able to make requests to my API? I mean, how can I make the API authenticate a request made with normal cookies?