Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

API Auth 101: Session? Cookies? Tokens?

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

How do we want our users to log into our API? There are about a million possible answers for this. To figure out your answer, don't think about your API, just ask:

What action will a user take to log into my app?

Most likely, your users will do something super traditional: they'll type an email and password into a form and submit. It doesn't matter if they're typing that into a boring HTML form on your site, a single page application built in Hipster.js or even in a mobile app. In all those situations, the user of your API will be sending an email & password. By the way, if you do not have this situation, that doesn't change much! I'll talk more about why later, but most of what we'll do will transfer to other authentication schemes.

Sessions or Tokens?

It turns out that the more important question - more important than what pieces of data your users will send to authenticate - is what happens when your API receives that data. How does it respond when you successfully authenticate?

And you might be thinking:

Hey! We're building a RESTful API... and APIs are supposed to be "stateless"... so that means don't use sessions... and so that means our API will return some sort of an API token.

Yep! That's not... super true. Session-based authentication - the type of login system you've known and loved for years - is just a token-based system in disguise! When you perform a traditional login, the server sends back a cookie. This is your "token". Then, every future request sends that token and becomes authenticated.

Here's what's really important. If the "user" of your API is you or your company - whether that be your JavaScript or a mobile app owned by you, then, on authentication, your API should return an HttpOnly cookie. This type of cookie is automatically sent with each request but is not readable in JavaScript, which makes it safe from being stolen by other JavaScript. The contents of that cookie, it turns out, are much less important. It could be a session string - like we're used to in PHP, or it could be some encrypted package of information that contains authentication details. If you've heard about JSON web tokens - JWT - that's what those are: strings that actually contain information. In all cases, your API will set an HttpOnly cookie, each future request will naturally send that back, and your API will use that to authenticate the user. Exactly what that cookie looks like is not really that important.

The great thing about session-based authentication with cookies - versus generating a JWT and storing it in a cookie - is that it's super easy to set up. And all HTTP clients - even mobile apps - support cookies.

Listen: later on, we are going to dive into some details about token-based authentication systems - the ones where you attach some token string to an Authorization header when you make the request. And we'll talk about when you need this. For example, if third parties - like someone else's mobile app - need to make requests to your API and be authenticated as users in your system, you would need OAuth.

So here's our first goal: build a super-nice, API-friendly, session-based authentication system where we POST the email and password as a JSON string to an endpoint. Then, instead of returning an API token, that endpoint will start the session and send back the session cookie.

Leave a comment!

10
Login or Register to join the conversation
Nick-F Avatar
Nick-F Avatar Nick-F | posted 8 months ago | edited

I am bulding an api solely for 3rd party applications.

  1. Get requests to pull data into excel
  2. Automated post requests from a single email provider to create entities when emails are received
  3. Automated post requests from a single phone provider to create entities when phone calls are received

I was thinking of a simple api key authorization system. Is this session/user login based authentication system applicable to me?
If not, which part of this api security course would be most relevant?

Reply

Hey Nick!

Apologies for the slow reply - holidays!

I was thinking of a simple api key authorization system. Is this session/user login based authentication system applicable to me?

The session/user login might be applicable for (1) depending on "who" is making these requests. But for the other items, no, it's not relevant. If you truly have just a single email provider and a single phone provider that need to make requests, you could build a super simple api key system. Heck, you could even hardcode those 1 or 2 tokens into your config - no need to complicate things.

If not, which part of this api security course would be most relevant?

You'll need to build an authenticator for the API token system. But other than that, pretty much the rest of this tutorial is relevant because it is not about authentication, but more about applying authorization rules (i.e. denying access) or returning data based on the user. Well, this will be relevant to you if you have these use-cases. Let me know if you have any questions about the api token authenticator that you'll need to build.

Cheers!

Reply
Pedro S. Avatar
Pedro S. Avatar Pedro S. | posted 1 year ago

Hi there! I have a tricky question that I haven't figured out how to manage.

Let's say I have two frontends hosted in different subdomains (frontend1.domain.com and frontend2.domain.com) that can login using the same api hosted on another subdomain (api.domain.com).

I want a customer to be able to use both frontends at the same time on the same browser (in 2 different browser tabs), being logged in with two different API users (each user has a different role with different permissions in each frontend), so I need each login to each frontend to be completely independent from the other one.

Here comes the problem: when a customer logs in using the API, the cookie generated is based on the API subdomain (api.domain.com) and not on the frontend subdomain. This causes a big mess when the customer uses both frontends at the same time, because the session cookie is shared in between both frontends.

Is there a way to generate two different session cookies (one per each frontend) to be able to achieve that scenario?

Many thanks in advance for your help!
Pedro

Reply

Hey Pedro S. !

Apologies for the slow reply!

Is there a way to generate two different session cookies (one per each frontend) to be able to achieve that scenario?

Hmm. Yes, I believe what you need to do is change the session cookie name used by each app. By default it is PHPSESSID, but that can be changed. I believe this can be set in framework.yaml via:


framework:
    session:
        name: OTHER_NAME

Let me know if that helps.

Cheers!

Reply

The make:user that is required for the next chapter is missing in the video

Reply

Hey Sridhar,

This is the 2nd tutorial, we created that User entity in the 1st one, exactly here: https://symfonycasts.com/sc... . We based the code of this tutorial on the finish code of the first tutorial, so it's like continuation of the 1st tutorial :)

If you skipped the first tutorial and started from this one - you would need to create User yourself, or we always recommend to code along with us downloading the course code and start from start/ directory - it will contain everything you need to start this course :)

Cheers!

1 Reply
Krzysztof K. Avatar
Krzysztof K. Avatar Krzysztof K. | posted 3 years ago

Hi Ryan, I am using API Platform with LexikJWTAuthenticationBundle and Symfony 4.4.
All works but I have no idea how to add some extra data to JWT.
I have configured that bundle to return user email as: user_identity_field, but I would like to add username too.

Documentation seems to be for older version of Symfony:
https://github.com/lexik/Le...

I have also checked: https://symfony.com/doc/cur...
but there are no examples..

Could you advice?

Reply

Hey Krzysztof K.!

I can definitely help with this :). The answer is to listen to the "jwt_created" event - I talk a bit about it here: https://symfonycasts.com/screencast/symfony-rest4/jwt-guard-authenticator#comment-4497986969

The docs you linked to - https://github.com/lexik/LexikJWTAuthenticationBundle/blob/master/Resources/doc/2-data-customization.md#eventsjwt_created---adding-custom-data-or-headers-to-the-jwt - I think is up to date. Well, sort of :). The docs are written on a way that would work - but there are easier ways to do things now. Here's what I would do:

1) Run bin/console make:subscriber
2) When it asks for the event name, type in lexik_jwt_authentication.on_jwt_created

That will generate the subscriber class for you - not YAML configuration will be needed. Then you can make the method in the subscriber class look like the example in their docs look: https://github.com/lexik/LexikJWTAuthenticationBundle/blob/master/Resources/doc/2-data-customization.md#example-add-client-ip-to-the-encoded-payload

Let me know if you hit any issues :).

Cheers!

Reply
Joel-L Avatar

Hi,
just a little question on auth strategy, if the client and the API (with no path prefix) are on different domain what would be the best authentification option ?

Reply

Hey Joel L.!

Sorry for the slow reply :). I've never had to implement this specifically - so I can't say for sure from a perfectly-educated place :). In theory, you could still use session-based cookie authentication as long as you whitelist your frontend's domain in CORS. However, I "think" that this will simply *not* work with a samesite cookie, which you *do* want to use.

So in this case, you probably would need to use some sort of API token to make authenticated requests to the API. I just don't love this because it means that, on some level, you're going to be needing to "keep track of" something sensitive - like an API token - in JavaScript. If you're careful and just keep it on a scoped JavaScript variable, I think you're ok (but "I think" is not a strong statement in security). However, even if that's true, unless you truly *store* it somewhere (like local storage or a cookie that JavaScript can read) then when you refresh, it will be gone and the user won't be authenticated. But as soon as you "store" something sensitive in JavaScript, you're asking for trouble. Basically, I think you're on shaky ground here, and it's why having some backend that's on the same domain as you is super handy... even if that backend itself is just making requests to some other API (but then it can securely store the authentication info, instead of JavaScript).

I hope this helps! Tough stuff :/

Reply
Cat in space

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

This tutorial works great for Symfony 5 and API Platform 2.5/2.6.

What PHP libraries does this tutorial use?

// 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
    }
}
userVoice