Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

API Platform Part 2: Security

4:45:13

What you'll be learning

This tutorial works great for Symfony 5 and API Platform 2.5/2.6.
// composer.json
{
    "require": {
        "php": "^7.1.3, <8.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^2.1", // v2.4.5
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/annotations": "^1.0", // 1.13.2
        "doctrine/doctrine-bundle": "^1.6", // 1.11.2
        "doctrine/doctrine-migrations-bundle": "^2.0", // v2.0.0
        "doctrine/orm": "^2.4.5", // v2.7.2
        "nelmio/cors-bundle": "^1.5", // 1.5.6
        "nesbot/carbon": "^2.17", // 2.21.3
        "phpdocumentor/reflection-docblock": "^3.0 || ^4.0", // 4.3.1
        "symfony/asset": "4.3.*", // v4.3.2
        "symfony/console": "4.3.*", // v4.3.2
        "symfony/dotenv": "4.3.*", // v4.3.2
        "symfony/expression-language": "4.3.*", // v4.3.2
        "symfony/flex": "^1.1", // v1.18.7
        "symfony/framework-bundle": "4.3.*", // v4.3.2
        "symfony/http-client": "4.3.*", // v4.3.3
        "symfony/monolog-bundle": "^3.4", // v3.4.0
        "symfony/security-bundle": "4.3.*", // v4.3.2
        "symfony/twig-bundle": "4.3.*", // v4.3.2
        "symfony/validator": "4.3.*", // v4.3.2
        "symfony/webpack-encore-bundle": "^1.6", // v1.6.2
        "symfony/yaml": "4.3.*" // v4.3.2
    },
    "require-dev": {
        "hautelook/alice-bundle": "^2.5", // 2.7.3
        "symfony/browser-kit": "4.3.*", // v4.3.3
        "symfony/css-selector": "4.3.*", // v4.3.3
        "symfony/maker-bundle": "^1.11", // v1.12.0
        "symfony/phpunit-bridge": "^4.3", // v4.3.3
        "symfony/stopwatch": "4.3.*", // v4.3.2
        "symfony/web-profiler-bundle": "4.3.*" // v4.3.2
    }
}
// composer.json
{
    "require": {
        "php": "^7.1.3, <8.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^2.1", // v2.4.5
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/annotations": "^1.0", // 1.13.2
        "doctrine/doctrine-bundle": "^1.6", // 1.11.2
        "doctrine/doctrine-migrations-bundle": "^2.0", // v2.0.0
        "doctrine/orm": "^2.4.5", // v2.7.2
        "nelmio/cors-bundle": "^1.5", // 1.5.6
        "nesbot/carbon": "^2.17", // 2.21.3
        "phpdocumentor/reflection-docblock": "^3.0 || ^4.0", // 4.3.1
        "symfony/asset": "4.3.*", // v4.3.2
        "symfony/console": "4.3.*", // v4.3.2
        "symfony/dotenv": "4.3.*", // v4.3.2
        "symfony/expression-language": "4.3.*", // v4.3.2
        "symfony/flex": "^1.1", // v1.18.7
        "symfony/framework-bundle": "4.3.*", // v4.3.2
        "symfony/http-client": "4.3.*", // v4.3.3
        "symfony/monolog-bundle": "^3.4", // v3.4.0
        "symfony/security-bundle": "4.3.*", // v4.3.2
        "symfony/twig-bundle": "4.3.*", // v4.3.2
        "symfony/validator": "4.3.*", // v4.3.2
        "symfony/webpack-encore-bundle": "^1.6", // v1.6.2
        "symfony/yaml": "4.3.*" // v4.3.2
    },
    "require-dev": {
        "hautelook/alice-bundle": "^2.5", // 2.7.3
        "symfony/browser-kit": "4.3.*", // v4.3.3
        "symfony/css-selector": "4.3.*", // v4.3.3
        "symfony/maker-bundle": "^1.11", // v1.12.0
        "symfony/phpunit-bridge": "^4.3", // v4.3.3
        "symfony/stopwatch": "4.3.*", // v4.3.2
        "symfony/web-profiler-bundle": "4.3.*" // v4.3.2
    }
}

Yep! You ❤️your new API Platform-powered API! It's just missing... well... any type of security! This is a big & important topic, so let's take it head-on in part 2 of our API Platform tutorial:

  • API token security? Or tried-and-true session based login form security?
  • CSRF protection? SameSite Cookies? Ice Cream?
  • Security firewall setup for json_login authentication
  • Authorization & roles: restricting access to your operations!
  • Encoding user's password (during user creation/update)
  • API Platform custom data persister
  • Dynamic serialization groups: showing different fields based on the user
  • Custom normalizer for dynamic fields based on user
  • Custom validator to control what data a user can set

Woh. Let's do this!


Your Guides

Niels van der Molen Ryan Weaver

Buy Access
Login or register to track your progress!

Join the Conversation?

61
Login or Register to join the conversation
infete Avatar

Is there a chance to cover OAuth2 in this series ?

6 Reply

... we're thinking about it ... no promises yet, but we're getting this question a LOT :)

-1 Reply
Startchurch Avatar
Startchurch Avatar Startchurch | posted 3 years ago

I really hope you guys cover how to securely store JWT tokens (primarily refresh tokens). I'd like the API I am creating to work for web clients and mobile devices. Most examples I've found regarding JWT tokens don't come close to dealing with JWT in production IMO. Storing refresh tokens in browser seems to be a bad pattern. Thought that sessions was the only safe way to leverage refresh tokens without re-typing passwords for web based clients if page is reloaded. Looking forward to the course. Hope you guys can clear up my confusion in this course.

4 Reply

Hi alsbury !

We've started releasing this course (🎆) but we're still planning some of the details to cover in the second half of it. You're correct that storing tokens in a browser is not a good idea (storing any tokens in JavaScript opens you to XSS attacks). We'll talk about some of this, but the easiest way to authenticate (especially if you're own JavaScript is the consumer of the API) is with normal cookie-based session authentication. If you want the user to stay, sort of, "permanently" logged in, you'd do that with a remember me cookie (yes, the same boring remember me cookie as always)... though I think implementing that with the json_login mechanism we're using might require some extra work. I also want to mention one more important thing: using JWT (or any token) IS safe if you set it on an HttpOnly cookie on authentication success (instead of returning it so that JavaScript can store it). With an HttpOnly cookie, the token will be sent with all future requests, but won't be readable by JavaScript (so no XSS). And finally, any time you rely on a cookie being sent for authentication (whether that cookie contains the session id or a JWT) you're vulnerable to CSRF attacks unless you use SameSite cookies... *another* topic we'll talk about early in the tutorial :).

Let me know what confusions you might still have - and we'll do our best to clear them up in the course!

Cheers!

2 Reply
Startchurch Avatar

Thanks for the response Ryan. In the research I'd done up to this point, it felt reasonably safe to store the access token (short lived token) as a cookie. But if your token lives for 15 minutes, then you need a new token in 15 minutes. This would require you to login again. Or if you simply reloaded the page, you would have to login again as well. So you add a refresh token to the mix. But a refresh token has a lot of power. If you store it as a cookie too, why bother with an access token. Both access and refresh tokens will be exposed in the same way all the time. I've used sessions for years and they seem like a decent mechanism for secure and user friendly access control. But a session cannot create new sessions by itself. A refresh token on the other hand (if compromised) can create unlimited access tokens for the life of the refresh token. This scenario seems slightly more dangerous than a session. But at least with a refresh token, you can black list them unlike access tokens. So my thought was that maybe refresh tokens are stored in the session and are not transmitted. But this seems like it's getting overly complicated and maybe I am thinking about it all wrong. To sum my problem up, I want a web app to use the same api with a stateful firewall and for the mobile app, I want to use JWT tokens (access+refresh) with a stateless firewall without having to use different routes. Maybe it's a poor understanding of the Symfony firewall that is tripping me up.

I hope I'm making sense. Either way, I hope you can demonstrate a reasonably secure and user friendly access control pattern that allows both web apps and mobile apps to share the same API.

As a side note, most of your tutorials do a great job of addressing real world problems that go way beyond "hello world" apps, which I really appreciate. Thanks!

Reply

Hey alsbury!

Thanks for the added notes here - great stuff! This is not he only solution but to play "devil's advocate", you could have a stateful firewall that uses a session cookie that is then used by your own JavaScript front-end and a mobile app (mobile apps support cookies). If you used an OpenId authentication server, web app & mobile app could use that with the auth code grant type... the downside being that this requires more setup & complexity if you don't actually need it. If the mobile app is "owned" by you, then OAuth isn't really needed. Let me know how this "jives" with what you're thinking. There are still so many options... and, unfortunately, many possible different attack vectors with the different solutions.

P.S. You're right that having a refresh token in JavaScript is a bad idea, unfortunately :/.

Cheers!

Reply
Startchurch Avatar

I'd considered using cookies on the mobile app side. JWT (access/refresh) tokens seemed like a nice choice for the mobile app, but sessions could certainly be used instead. That may just be the most practical solution in my scenario. My app is "owned" by me, so I can avoid OAuth and some of the more complex scenarios. Is there a way that the firewall statefulness can be dynamic so that you can choose based on request context? My goal is to avoid duplicating routes and still use the auth mechanism best suited for each particular platform. Maybe I am making a mountain out of a mole hill...I don't know. Thanks for all your feedback.

Reply

Hey alsbury!

> Is there a way that the firewall statefulness can be dynamic so that you can choose based on request context

I'm pretty sure this is not possible (just checked some code). But, I'm not sure that's a problem. If you *did* want to use something like JWT for your mobile app, you can still make the request to the same firewall. Yes, this will start a session and send back a cookie. But your mobile app can ignore the cookie and use the JWT instead if you want. You could also (not sure if this would be considered a good practice, but it would certainly work) add some "flag" (e.g. POST param) to your authentication request that indicates whether or not the success response should include a JWT or not (if you want to avoid getting a JWT sent back to your JS... though again... you could just ignore that).

I don't know if I see a big benefit in doing this - though, there could be some good reason - there are so many variations with this stuff. I would use cookie-based authentication on the mobile app OR if you want to go the full OAuth route (which has the downside of complexity) I think the "most" recommended solution for mobile apps is the "authorization code" OAuth grant type with no client secret but with PKCE.

Cheers!

Reply
Startchurch Avatar

Thanks for the feedback, I really appreciate you taking the time to respond to my questions. Cookies sound like the best option. JWT tokens would probably only improve my situation if the service needed to scale really big.

Reply
Petru L. Avatar
Petru L. Avatar Petru L. | posted 1 year ago

Hey weaverryan , any updates on that oauth2 tutorial?

1 Reply

Hey Petru Lebada

Keep watching for updates, unfortunately can't provide other information.

BTW which updates are you expecting?

Cheers!

Reply
Petru L. Avatar

Hey Vladimir Sadicov ,

A symfony5 tutorial for implementing oauth2 with apiplatform

1 Reply

Installing and configuring own oauth2 server? or using 3rd party oauth2 authentication?

Reply
Stefan T. Avatar
Stefan T. Avatar Stefan T. | posted 3 years ago

Sorry for off top but will you make something about elasticsearch?

1 Reply

Hey Gdyż nbccfcc ,

Thank you for your interest in SymfonyCasts and this topic! We do really want to release a course about ElasticSearch someday, but there're a lot of good things that we're working on and would like to release sooner, so it's difficult to say when ElasticSearch course may be released. But yes, we have this topic in our idea pool :)

Thank you for your patience!

Cheers!

1 Reply
Ajie62 Avatar

I look forward watching these tutorials!

1 Reply
oscar Avatar

Great!, I would only add advanced topics such as mercure protocol and CQRS. Thanks

1 Reply
Holger N. Avatar
Holger N. Avatar Holger N. | posted 8 months ago

Hey is there any information somewhere on how to work with API Platform and API Tokens? Thanks :)

Reply

Hey @creativx!

Not currently in this tutorial. I do talk about API tokens in our Symfony 4 security tutorial - https://symfonycasts.com/sc... - though that would need to be tweaked a bit to work with the new authenticator system. I would like to put out a mini API authentication tutorial, but it may be a couple of months yet.

Sorry I can't offer a better solution! I personally like the simplicity of the ApiToken entity solution we show in the Symfony 4 tutorial, especially as most people don't need JWT. But, JWT also works great and there is LexikJwtAuthenticationBundle for that. Alternatively, it's possible you need OAuth, but only if you need other apps to be able to "make API requests on behalf of your users".

Cheers!

Reply
Holger N. Avatar

Thanx for this information :)

Reply
Gabrielius Avatar
Gabrielius Avatar Gabrielius | posted 1 year ago

Hi, just started using Api platform with one of my projects where I have two user providers (two different entities: Admin and User). And I struggle to get a list of users when I am logged in as Admin :(. I am getting 401 Unauthorized response. Cannot find any information in both Symfony & Api platform documentations.

The User entity GET method looks like this:
#[ApiResource(
collectionOperations: [
'GET' => [
'security' => 'is_granted("ROLE_USER")',
], ......

Role hierarchy is set in security.yml. Looks like Symfony cannot find other providers token in TokenStorage I guess?

Reply
Gabrielius Avatar

Aaah, I found the problem. Under admin firewall I left "pattern: "/admin"".

Reply

Hey Symfony Student!

Good job identify the users! The multiple firewall setup is not that common (which is why you probably couldn't find too much information) but it doesn't mean it's wrong. If you have two very different parts of your application (e.g. an admin area and a frontend area) and those 2 different areas have 2 totally different users that log into them (and there is no cross-over - e.g. you cannot log in as an admin and expect to go to the frontend and STILL be logged in), then it is totally the right way to go. And setting the pattern, as you discovered, is what tells Symfony which ONE firewall is active when a request comes into your system.

Cheers!

1 Reply
Ewald V. Avatar
Ewald V. Avatar Ewald V. | posted 1 year ago

I'm using the new experimental Security component. With the anonymous users removed any API call using the access_control expressions throw this error instead of the more generic "Access Denied" error:

The current token must be set to use the "security" attribute (is the URL behind a firewall?).

So I was wondering how I can allow certain actions only for users that are not logged in, and how I can just get an "Access Denied" error instead of the one above on calls where you need to be authenticated :-).

Reply

Hey Ewald Vanderveken!

I'm happy to see you're trying the new security component - it's awesome :). But... it looks like API Platform needs to be updated to work with it, specifically for anonymous users. The problem is here - https://github.com/api-plat...

I've created an issue about this - https://github.com/api-plat.... Unfortunately, until that's fixed, I don't think you'll be able to use the new system with API Platform :/.

Cheers!

1 Reply
Ewald V. Avatar

Yeah I saw your talk about it some weeks ago on SymfonyWorld and afterwards really regretted not following your workshop :-D.

Thanks for the info! (And for this amazing website!)

Reply
Fernando A. Avatar
Fernando A. Avatar Fernando A. | posted 2 years ago

Any idea of when the part 3 is going to get out?
I kinda want to play with DTOs... else I will have to find someone else to learn that from xD

PS: if it was not asking too much, a tutorial on how to build an OAuth2 server would be nice :P
yeah I dont ask for just how to use it I go for building the entire thing xD
dont beat me guys :D

Reply

Hey Fernando,

That's a top-secret for now, so don't tell anyone ;) Most probably the course will be started releasing next month. But that's a very rough estimations for now, sorry. Thank you for your patience!

About OAuth, have you watched https://symfonycasts.com/sc... tutorial already? Also, take a look at this comment from Ryan: https://symfonycasts.com/sc... - I think it might be interesting for you.

Cheers!

1 Reply
Nathan D. Avatar
Nathan D. Avatar Nathan D. | posted 2 years ago

That's an amazing tutorial ! Thanks a lot !

I Implemented an JWT based authentication with Lexik and now I'm struggling to find a way to connect my frontend (whatever the framework Flutter / Symfony / Angular, etc ... ). So a user should be able to connect from anywhere by calling /authentication_token and put the token as Bearer token inside the next request.

How should I store user email and password on my frontend ? How should I manage JWT expiration and refreshing ? Basically, how can I build a frontend without asking for login/password any time I send a request to my API ? Just want the user to connect on my mobile app and after that he can use it without login every time. And I feel like storing user email and password is not that good for security.

Do you have some ideas for me ?

Thanks in advance

Reply
Nathan D. Avatar

Just read the discussion below with Alsbury ... I'm going to check how to handle cookie / session in a Flutter App. Seems to be the easiest way to connect multiple frontend to my API.

Reply

Hey Nathan,

Great catch on that discussion! Yep, please, give it a try and let us know if that won't help you.

Cheers!

Reply
Nathan D. Avatar

For the Flutter App everything works with the dio package combine to dio_cookie_manager. Just need to use a CookieJar and you're good to go. In case someone is searching a solution ;)

I'm now struggling to keep cookies into my httpclient on my Symfony Client side. When I'm doing $httpclient->request(/login) and then 4 lines below doing $htttpclient->request(/api/recourses) I got an 401 like I'm not connected ... I'm autowiring HttpClientInterface $httpClient inside the constructor of my service responsible for API calls. How to pass a CookieJar to that instance ? How can I take the cookie from my HttpClient and push it to the user current browser ? I'm not use to Javascript so I'm going this way, and even if I can easily switch to JS, I want to understand why I get this behavior when going through PHP calls=D

One Last stuff to implement is the remember_me cookie in order to keep user logged in when closing browser or mobile app. I don't have a clue on that ! Maybe only send the right remember_me cookie ... don't know yet !

Reply

Hey Nathan,

Thank you for confirming it helped you!

About cookies: HTTP client is stateless but handling cookies requires a stateful storage. You can either handle cookies yourself using the Cookie HTTP header or use the BrowserKit component. Actually, see this reference to the docs:
https://symfony.com/doc/cur...

I hope this helps!

Cheers!

Reply
Nathan D. Avatar

I'm good with GuzzleHttp ;)

Everything is set up and working like a charm !

Thanks for your time and your help ! =D

Reply

Hey Nathan,

Perfect, glad it works for you!

Cheers!

Reply
Nathan D. Avatar

Guzzle on the way to rocketlaunch my Symfony client !

Reply
Steven L. Avatar
Steven L. Avatar Steven L. | posted 2 years ago

This tutorial appears to require MySQL 5.7, using a JSON data type for user.roles in src/Migrations/Version20190509185722.php. I am stuck on 5.6 for compatibility reasons.

If this is changed to TEXT, will the code work?

Reply

Hey Steve,

Oh, I see. Well, first of all you can check config/packages/doctrine.yaml file where we set server_version to 5.7. Thanks to this all the migrations are generated for MySQL 5.7+, that's why it may not work for your 5.6 as I see. In this case, you need to regenerate migrations with that server_version set to 5.6. But for dev/learning purposes, you can ignore migrations and just run the next command, but set server_version to 5.6 first so that Doctrine perfor specific queries:

$ bin/console doctrine:schema:update --force

But it's not recommended to run this on production when you have some data, it may truncate them. If you wonder what queries will be executed - you can check it with this command first:

$ bin/console doctrine:schema:update --dump-sql

I hope this helps!

Cheers!

Reply

Awesome tutorial!

Where can I find the package.json file?

Reply

Hey Maboa Garaboa

It's always located in the root of your project and if you are talking about course code, than you can find it in the start/ or finish/ folder depending on what are you looking.

Cheers!

Reply

Thanks for the information! Looks like I do not have access to download the code

Reply

Yeah sorry about that, but you should have a subscription or buy this course to have access.

Cheers!

Reply
Simon D. Avatar
Simon D. Avatar Simon D. | posted 3 years ago

Don't suppose there's a shortcut way to expose Blameable / Timestampable fields to the API - i.e. expose the trait itself??
I'm guessing not and that you have to add the fields to your entities manually.

Reply

Hey Simon Denman

I think there is a way but it might be ugly. What you can do is to create your own Serializer and add those fields into your response. Give it a check to the docs: https://api-platform.com/do...

But, I think it would be easier if you just add those properties manually to your entity instead of using a trait

Cheers!

Reply

Hi SymfonyCasts team,

Thank you very much for the great work you do with these API Platform tutorials.

As I'm not a frontend developer, I even don't want to install the frontend part of this tutorial.
Is there a way for me (as a backend developer) to login via Swagger so I can test the whole authentication part?

Actually I can login with Postman but I think it's better to do it with Swagger UI as my endpoints are already in place.

And I wish maybe in a next serie, you will continue with something like "how to get better performance with your API Platform" with filters, text search...

Reply

Hey Ramazan!

> Thank you very much for the great work you do with these API Platform tutorials

❤️

> Is there a way for me (as a backend developer) to login via Swagger so I can test the whole authentication part?

You somehow just need to "login" so that the session cookie gets set. You could probably do this via your browser's JavaScript console. Try running this in your browser's debug console:


fetch('https://localhost:8000/login', {
method: 'POST',
body: JSON.stringify({
email: 'cheeseplease@example.com',
password: 'foo'
}),
headers: {
'Content-type': 'application/json; charset=UTF-8'
}
})

In theory... though I've never tried it, that will successfully authenticate and set the authenticated session cookie in your browser. Let me know if that works!

> And I wish maybe in a next serie, you will continue with something like "how to get better performance with your API Platform" with filters, text search

I'd love to hear more about this. Filters & text search are a nice feature to have to make your API more usable. How do you see that this relates to performance?

Cheers!

1 Reply

Thank you very much weaverryan for your quick response for the login tip, it works :)

What I mean by performance here is about the response time, if you have a lot of data in your database, the filters that we have in the get cheeses could impact the response time with MySql. So I wonder what's the best to do to handle a cheeses search if I have a lot in my DB.
Maybe I should create a custom controller and handle the situation from there and do the search on a different data source...

Reply

Hey Ramazan!

Ah, yep, that makes sense. Some of the filters - like the boolean filter - are fast... as it ultimately create something like WHERE is_published=1... which is fast (and even faster if you decide to add an index on that field). But there is indeed a performance problem in large datasets with the "search" filter... because this creates the WHERE name LIKE %foo% type of query. The search filter sometimes isn't powerful enough - it's still a pretty "simple matching" search. If you need a more robust search or a more performant search, the proper solution is to use ElasticSearch and the ElasticSearch integration with API Platform. That's something we're thinking about covering sometime in the future (we're getting quite a lot of requests), but won't be part of this tutorial :).

I hope that helps!

Cheers!

Reply

Hey weaverryan ,

Yep it's exactly what I meant with the search text filter. It will be great to touch this topic at least with something like, how to search faster with elasticsearch... on the same project. Maybe on the 3rd tutorial :)

Good luck.

Reply
Default user avatar

Loving the API Platform specific SymfonyCasts. Nice clear and easy to understand. Wish these were out a year ago when I started building my API Platform based API haha. Would have saved me a lot of time researching how to do X.

Just wondering if you'll be covering more advanced access control concepts E.G. How to restrict a collection of related entities either by using Filters, Extensions, Voters or some other means? All examples I've seen are simple and require a user relationship on the entities which are checked against the current logged in user etc. When access control rules are more complex this gets difficult.

For item specific endpoints I'm using voters to test for for various conditions however this doesn't work for collection endpoints as looping over all results and applying voters to strict data would mess with the pager as well as add large processing overheads.

More complex example.
- User 1 has a relationship with Product 1 = Can view Product 1
- User 1 has a relationship with Group 1 = Should only be able to see their own Products so can view Product 1
- User 2 has a relationship with Group 1 = Should only be able to see their own Products so can't view Product 1
- User 3 is an admin of Group 1 = Should be able to view all products for all Users in Group 1 so can view Product 1
- User 4 is a "super" admin = Should be able to view all products from all users

Maybe it'd be better suited for a more advanced course.. Or is there already another SymfonyCast I have somehow missed that addresses this?

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!