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!

  • 2018-01-22 weaverryan

    Hey Zbyněk Šimek!

    Yea, good solution! I do the same type of thing: I make my JWT authentication ONLY return true for supports if it sees a JWT header. You are basically doing the same: returning false when you see the Basic header. You want your authenticator to do its work only when there is some authentication information to process!

    But, before I answer your question about logout, I have a question/comment. Because your API will only be used by your own JavaScript, my *actual* recommendation was to NOT use JWT, and instead to use a traditional form login instead (and then your AJAX requests will automatically use the session cookie). You *can* use JWT, but I think it's overkill, and will make your life more difficult (one reason is that "logout" with JWT is a bit trickier).

    Cheers!

  • 2018-01-20 Zbyněk Šimek

    Okay,
    I probably found a solution using two request and modify support methods in authenticator guard..If the first token request contains Basic header, authenticator guard is skipped..

    First request to api/login/token pick up the token from the server to client and this token is passed into api/login request, which just return HTTP_OK result if user in session..

    Is it correct?

    One question about logout action. It is possible just execute:

    $this->get('security.context')->setToken(null);

    in the web logout controller?

  • 2018-01-20 Zbyněk Šimek

    Hi weaverryan,
    thank you for your advise. I tried to make one Guard authenticator via post controler api/login, bud I have a little worry :-(

    First, a little bit my code. This is my login controller:


    public function postLoginAction(Request $request)
    {
    $usernameOrEmail = $request->getUser();
    $password = $request->getPassword();

    $user = $this->getDoctrine()
    ->getRepository(User::class)
    ->loadUserByUsername($usernameOrEmail);

    dump($user->getUsername());

    if (!$user) {
    throw $this->createNotFoundException();
    }

    $isValid = $this->get('security.password_encoder')->isPasswordValid($user, $password);

    if (!$isValid) {
    throw new BadCredentialsException();
    }

    $token = $this->get('lexik_jwt_authentication.encoder')
    ->encode([
    'roles' => $user->getRoles(),
    'username' => $user->getUsername(),
    'exp' => time() + 3600 // 1 hour expiration
    ]);

    return new JsonResponse(['token' => $token]);
    }

    And this is my security.yaml:


    security:
    # https://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers
    encoders:
    App\Entity\ORM\CORE\User:
    algorithm: bcrypt
    cost: 12

    providers:
    user_db_provider:
    entity:
    class: App\ORM\CORE\User
    property: username
    firewalls:
    dev:
    pattern: ^/(_(profiler|wdt)|css|images|js)/
    security: false
    provider: user_db_provider
    main:
    pattern: ^/
    anonymous: true
    stateless: false
    provider: user_db_provider
    guard:
    authenticators:
    - 'jwt_token_authenticator'

    logout:
    target: /{_locale}
    access_control:
    - { path: ^/admin, roles: ROLE_ADMIN }
    - { path: ^/api/testuser, roles: ROLE_USER }
    - { path: ^/api/admin, roles: ROLE_ADMIN }
    - { path: ^/register, roles: IS_AUTHENTICATED_ANONYMOUSLY }
    - { path: ^/, roles: IS_AUTHENTICATED_ANONYMOUSLY }

    And supports, getCredentials, getUser of my JwtTokenAuthenticator:


    public function supports(Request $request)
    {
    return true;
    }

    /**
    * @param Request $request
    * @return array|bool|false|string
    */
    public function getCredentials(Request $request)
    {
    $extractor = new AuthorizationHeaderTokenExtractor(
    'Bearer',
    'Authorization'
    );

    $token = $extractor->extract($request);

    if (!$token) {
    return false;
    }

    return $token;
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
    try{
    $data = $this->jwtEncoder->decode($credentials);
    }catch (JWTDecodeFailureException $e){
    throw new CustomUserMessageAuthenticationException('Invalid token for user authentication: '.$e->getMessage() );
    }

    $username = $data['username'];

    return $this->em
    ->getRepository(User::class)
    ->loadUserByUsername($username);
    }

    If I send POST request (via POSTMAN) to api/login, the response is:

    {"message":"Invalid token for user authentication: Invalid JWT Token"}

    If I disable authenticator using supports (return false;), the token is successfully returned, and then if I use the Bearer token (via POSTMAN) and put it into token filed and send it to secured api/tesuser, user is successfully authenticated and api code executed..

    How I get the Bearer token into the header request for authenticator guard? Have I the rewrite authorization response header in api/login controller like you in testPOSTProgrammerWorks (https://knpuniversity.com/s...

  • 2018-01-17 weaverryan

    Hi Zbyněk Šimek!

    Good questions :). I have some quick advice that I hope will help. Even though your admin will use React and make API requests, you can still use normal, cookie-based authentication. In fact, this is how AJAX works traditionally: the user submits a login form (e.g. twig template), they receive a cookie, and then all future AJAX calls automatically use the cookie to become authenticated. There is nothing wrong with using this approach, even if your admin will be 100% API calls

    So, my recommendation is to create 1 Guard authenticator for your login form and that's all. Have all users use this login form. Then, if they go to the admin, the AJAX requests will use the cookie. You can even make your login form submit via AJAX if you want it to be fancy - it makes no difference (a cookie will still be set on the user's browser).

    Let me know if this helps!

    Cheers!

  • 2018-01-15 Zbyněk Šimek

    Hello, very nice tutorial!
    I have a question and it is possible that the our answer follows from the below..But I am not sure..

    I have application, which si two type of pages: Public and Admin page.. Public pages will be developed in pure symfony with twig, admin pages will be developed in REACT..Admin pages wil be consumed data from symfony RETS API (FOSRestBundle)..

    Public page is for anonymous or logged users.. Private pages is just for logged-in users with - for example - role ROLE_ADMIN..

    My question is about uniform authentification..I have an idea.. First, user accesses on public page like anynomous user..User may or may not log in..If the user is not logged in, no authentication is needed and everything is alright..

    If the user logged in, authentication process start and I have several options in my mind:

    1) User can log on using rest API - through javascript POST request in menu page header
    2) User can log on using login form (twig template)

    If the user is logged in, it can access it to admin (REACT) page (if it have right admin_role, how to say above..)..

    Can I use it API token autentization for all application or I have to two separate autentization guards under the firewalls? Once autentization - cookie-based - for the public page using by login form and second autentization - api token - for API? Then, if I had two autentization, do I need to have two login form? What is recomended?

    Thank you for any advice..

  • 2018-01-10 Ahmed Bhs

    Tank you :) you made my day Diego Aguiar
    Best regadrs,

  • 2018-01-10 Diego Aguiar

    Great questions Ahmed

    1) An endpoint is basically another route that you can define in any controller, but it's usually hitted via AJAX or if it's an API, via a CURL request, e.g. an API endpoint could be something like: "/users/{id}", returning you information about user with id 3

    An entry point is where the authentication process starts whenever a user tries to access a secured area by a firewall. In this case the name is really appropiate, you can read more info about entry points here: https://symfony.com/doc/cur...

    2) As you said, the stateless option is for letting Symfony know if you want to store the authentication information in the session, if your app is an API then you don't need this behaviour (again, as you said), so I would stick with the solution proposed in this chapter - having 2 firewalls, one for the site and another for the API

    I hope it helps you a bit :)

    Cheers!

  • 2018-01-09 Ahmed Bhs

    Diego Aguiar thank you for replaying i really appreciate, 1) What is the difference between two phrases "entry point" and "end point"?
    2) why under firewall configuration we mention stateless: false or true !!?
    The stateless configuration parameter prevents Symfony from trying to store the authentication information in the session, which isn't necessary since the client will send the apikey, what do you think here, what is recommended !

  • 2018-01-08 Diego Aguiar

    Hey Ahmed Bhs

    Hmm, your proposal is interesting, but I do see more solid having 2 firewalls for this situation, maybe, in the future, you will want to apply more rules to your authentication process, so having them separated will give you more flexibility.

    Cheers!

  • 2018-01-07 Ahmed Bhs

    Hey, really enjoy the tuto, but i have a simple propose,

    To solve the confuse between logging from the REST API and from the traditional login form, via the *same* firewall, I think we could configure 2 check_path:
    The first one check_path is under login_from for a traditional app
    The second check_path is under json_login for our API, so do you think this can be a solution, of course without using the any entry_type option (useless) under the main firewall because the check_path will represente the EndPoint?? and i think i'm missing something :(

    Cheers!

  • 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?