Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

API Tokens? Session Cookies?

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.

Join me, while we tell a tale as old as... the modern Internet: API authentication. A topic of hype, complexity and unlikely heroes. Characters include sessions, API tokens, OAuth, JSON web tokens! But what do we need for our situation?

The first thing I want you to ask is:

Who will be using my API?

Is it your own JavaScript, or do you need to allow programmatic access? Like someone will write a script that will use your API?

We're going to go through both of these use-cases... and each has some extra complexities that we'll discuss along the way.

Everything is a Token!

By the way, when you think of API authentication, you typically think of an API token. And that's true! But it turns out that... pretty much all authentication is done by some sort of a token. Even session-based authentication is done by sending a cookie... which contains a unique, you guessed it, "token". It's a random string that PHP uses to find and load the related session data on the server.

So the trick is figuring out which type of token you need in each situation and how the end-user will get that token.

Use-Case 1: Building for your Own JavaScript

So let's talk about that first use-case: the user of your API is your own JavaScript.

Well, before we even dive into security, make sure your frontend and your API live on the same domain... like the exact same domain, not just a subdomain. Why? Because if they live on two different domains or subdomains, you have to deal with CORS: Cross-Origin Resource Sharing.

CORS not only adds complexity to your setup, it also hurts performance. Kévin Dunglas - the lead developer of API Platform - has a blog post about this. He even shows a strategy where your frontend and backend can live in totally different directories or repositories, but still live on the same domain thanks to some web server tricks.

If you do, for some reason, decide to put your API and frontend on different sub-domains, then you will need to worry about CORS headers and you can solve that with NelmioCorsBundle. But, I don't recommend it.

The case for Sessions

Anyways, back to security. If you're calling your API from your own JavaScript, the user is probably logging in via a login form with an email and password. It doesn't matter if that's a traditional login form or one that's built with a fancy JavaScript framework that submits via AJAX.

And, honestly, a really simple way to handle this use-case is not with API tokens, but with good ol' fashioned HTTP Basic authentication. Yea, where you literally pass the email & password to each endpoint. For example, the user enters their email and password, you make an API request to some endpoint just to make sure it's valid, then you store that email and password in JavaScript and send it on every single API request going forward. Your email & password works basically like an API token.

However, this has some practical challenges, like the question of where you securely store the email and password in JavaScript so you can continually use it. This is actually a problem in general with JavaScript and "credentials", including API tokens: you need to be very careful where you store those so that other JavaScript on your page can't read them. There are solutions: https://bit.ly/auth0-token-storage - but it adds complexity that you very likely don't need.

So instead, for your own JavaScript, you can use a session. When you start a session in Symfony, it returns an "HTTP only" cookie... and that cookie contains the session id. Though, the contents of the cookie aren't really important: it could be the session id or some sort of token you invented and are reading in Symfony. The really important thing is that because the cookie is "HTTP only", it can't be read by JavaScript: your JavaScript or anyone else's JavaScript. But whenever you make an API request to your domain, that cookie's will come with it... and your app will use it to log in the user.

So the API token in this situation is simply the "session id", which is stored securely in an HTTP-only cookie. Mmm. We will code through this use case.

Oh, and by the way, one edge-case with this situation is if you have a Single Sign On situation - an SSO. In that case, you'll authenticate with your SSO like a normal web app. When you finish, you'll have a token, which you can then use to either authenticate the user with a session like normal... or you can use that token directly from your JavaScript. That's a more advanced use case that we won't go through in this tutorial... though, we will talk about how to read & validate API tokens regardless of where those tokens came from.

Use-Case 2: Programmatic Access & API Tokens

The second big use-case for authentication is programmatic access. Some code will talk to your API... besides JavaScript from inside the browser.

In this case, the API clients absolutely will send some sort of an API token string. And so, you need to make your API able to read a token that's sent on each request, usually sent on an Authorization header:

$response = $httpClient->request(
    'GET',
    '/api/treasures',
    [
        'Authorization' => 'Bearer '.$apiToken,
    ],
);

How the user gets this token depends: there are kind of two main cases. The first one is the "GitHub personal access token" case. This is where a user can browse to a page on your site and click to create a new access token. Then they can copy that and go use it in some code.

The second big case is OAuth, which is just a fancy & secure way to get an access token. It's especially important when the "code" that's making the API requests is making those requests on "behalf" of some user on your system.

Like imagine a site - ReplyToAllCommentsWithHearts.com - that allows you to connect with GitHub. Once you do, that site can then make API requests to GitHub for your account, like making comments as your user. Or imagine an iPhone app where, to log in, you show the user the login form on your site. Then, via an OAuth flow, that mobile app will receive an access token it can use to talk to your API on behalf of that user.

We're going to talk about the personal access token method in this tutorial, including how to read and validate API tokens, no matter where they come from. We won't talk about the OAuth flow... and it's partially because it's a separate beast. Yes, if you have the use-case where you need to allow third parties to get API tokens for different users on your site, you will need some sort of OAuth server, whether you build it yourself or use some other solution. But once the OAuth server has done its work, the client that will talk to your API receives... a token! And then they'll use that token to talk to your API. So your API will need to read, validate, and understand that token, but it doesn't care how the API client got it.

Ok, let's put all this theory behind us and start going through the first use-case next: allowing our JavaScript to log in by sending an AJAX request.

Leave a comment!

7
Login or Register to join the conversation
Pierre-A Avatar
Pierre-A Avatar Pierre-A | posted 4 months ago

Hello Ryan, another little typo error in your code here :

$response = $thhpClient->request( // This should be httpClient !!!
    'GET',
    '/api/treasures',
    [
        'Authorization' => 'Bearer '.$apiToken,
    ],
);

or someone invented a new protocole: thhp i didn't know ? ;) ... many thanks for tutos !

Reply

Lol - thank you @Pierre-A

Reply
Romain-L Avatar
Romain-L Avatar Romain-L | posted 4 months ago

Hello Ryan,
Thanks for this tutorial.
Like Ugo, I am also a bit confused but by another another point.
When you say "Or imagine an iPhone app where, to log in, you show the user the login form on your site. Then, via an OAuth flow, that mobile app will receive an access token it can use to talk to your API on behalf of that user."
Why do we need OAuth here ?
They state the same thing on the link you gave (auth0.net) : "If the Application is a native app, then use the Authorization Code Flow with Proof Key for Code Exchange (PKCE)."

I was thinking it was possible to just use JWT to authenticate for a native app. For example, let's take Flutter, we can use the secure storage (https://pub.dev/packages/flutter_secure_storage) to store the JWT, right ? So is there something wrong with sending my email+password to my api endpoint (like in normal spa), then read the answer, store the token in the safe place and then re-use for latter requests by adding this token to the Authorization header (i saw this implementation in few places also). I can also use this token, to display connected / not connected page depending if token is valid or not (by checking from time to time).

But since it's seem mandatory to use Oauth with PKCE, i think i'm missing something about the security but i don't see what :-)
If you can help on this subject, it would be very nice.
Thanks a lot.

Reply

Hey @Romain-L!

Sorry for the slow reply - just got back from vacation :).

Yea.. this stuff is SO tricky - and I'm not an expert on every aspect for sure!

I was thinking it was possible to just use JWT to authenticate for a native app. For example, let's take Flutter, we can use the secure storage (https://pub.dev/packages/flutter_secure_storage) to store the JWT, right ?

Yes, I'm 99% sure that this will "work". And, about storage, I don't know what the normal solution is for this, but even with OAuth, after you complete the process, you'll need to store the access token somewhere. So storing an access token or JWT is a legitimate thing to do.

So is there something wrong with sending my email+password to my api endpoint (like in normal spa), then read the answer, store the token in the safe place and then re-use for latter requests by adding this token to the Authorization header (i saw this implementation in few places also).

I'm not an expert here, but yes, I think this is "fine". What you're seeing is that OAuth is recommended because it's a standard, so there's less risk that you mess something up. Additionally, with OAuth, the user ultimately enters their email/password into a browser with your site's URL in the address bar. With your solution, you enter it directly into the app... which can be a risk (assuming your users are smart enough to notice) because a "bad user" could create an app that looks like your app with a login form... which then sends their credentials somewhere else. So by using OAuth, you can increase the user's trust. But in reality, I'm not sure how many users will actually do this.

Here's another document from Auth0 that talks in more depth about this topic - https://auth0.com/docs/get-started/authentication-and-authorization-flow/mobile-device-login-flow-best-practices

Cheers!

1 Reply
Romain-L Avatar

Hello Ryan,
Thank you very much for your time and your answer. It's more clear to me know. So i will go with the "basic" solution since it's seems to be ok. I checked a few mobile apps and they all allow to enter email/pwd in the app without oauth for there own users, so let's go for this!
Thanks and have a nice day

Reply
Ugo Avatar

Hi Ryan,
I am getting a bit confused..
I am developing a backoffice+api in Symfony and a front end app in vuejs/nuxt. The front end app will be used only by external users that will have to login to get their own data (authentication + authorisation). These two apps have separate repo and I was planning to use two different subdomains (ex: backoffice.myurl.com and myurl.com) and sessions. I am now getting confused with what you explained about CORS. Is anything wrong with my setup ?
thx for your help

Reply

Hi Ugo!

Excellent question! Because you're using sessions, there are 2 things to think about:

1) Can your frontend and backend share a session? The answer is yes. And it will likely happen automatically. When you send a POST request to myurl.com (your API) to authenticate, and then your API creates a session, it will probably create the SameSite session cookie under the domain myurl.com. Because backoffice.myurl.com is a subdomain of that, it can use that cookie. So, all good here ✅

2) Can your frontend JavaScript make Ajax requests to your API? The answer is "yes", "but". You WILL be able to do this. However, your frontend will first make a CORS preflight request basically asking your API if it's ok if backoffice.myurl.com makes Ajax calls to it. So, to get this to work, you'll need to configure some CORS config on your API to say that requests coming from backoffice.myurl.com are "allowed". But, you'll STILL have s flight performance impact because your frontend will first need to make that extra "preflight" request before it makes the real AJAX request. I'm not an expert on CORS, but it looks like you can set a header - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age - so that this preflight request is not made often.

So, CORS is the problem. Your setup will work, but you'll need some CORS config and you'll suffer (slightly) from that preflight request. The alternative, which Kévin Dunglas talks about - https://dunglas.dev/2022/01/preventing-cors-preflight-requests-using-content-negotiation/ - is to just use something like myurl.com/backoffice and then configure your webserver to serve the /backoffice URL from the totally different frontend app code :).

Cheers!

Reply
Cat in space

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

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^3.0", // v3.1.2
        "doctrine/annotations": "^2.0", // 2.0.1
        "doctrine/doctrine-bundle": "^2.8", // 2.8.3
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.14", // 2.14.1
        "nelmio/cors-bundle": "^2.2", // 2.2.0
        "nesbot/carbon": "^2.64", // 2.66.0
        "phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
        "phpstan/phpdoc-parser": "^1.15", // 1.16.1
        "symfony/asset": "6.2.*", // v6.2.5
        "symfony/console": "6.2.*", // v6.2.5
        "symfony/dotenv": "6.2.*", // v6.2.5
        "symfony/expression-language": "6.2.*", // v6.2.5
        "symfony/flex": "^2", // v2.2.4
        "symfony/framework-bundle": "6.2.*", // v6.2.5
        "symfony/property-access": "6.2.*", // v6.2.5
        "symfony/property-info": "6.2.*", // v6.2.5
        "symfony/runtime": "6.2.*", // v6.2.5
        "symfony/security-bundle": "6.2.*", // v6.2.6
        "symfony/serializer": "6.2.*", // v6.2.5
        "symfony/twig-bundle": "6.2.*", // v6.2.5
        "symfony/ux-react": "^2.6", // v2.7.1
        "symfony/ux-vue": "^2.7", // v2.7.1
        "symfony/validator": "6.2.*", // v6.2.5
        "symfony/webpack-encore-bundle": "^1.16", // v1.16.1
        "symfony/yaml": "6.2.*" // v6.2.5
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "mtdowling/jmespath.php": "^2.6", // 2.6.1
        "phpunit/phpunit": "^9.5", // 9.6.3
        "symfony/browser-kit": "6.2.*", // v6.2.5
        "symfony/css-selector": "6.2.*", // v6.2.5
        "symfony/debug-bundle": "6.2.*", // v6.2.5
        "symfony/maker-bundle": "^1.48", // v1.48.0
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/phpunit-bridge": "^6.2", // v6.2.5
        "symfony/stopwatch": "6.2.*", // v6.2.5
        "symfony/web-profiler-bundle": "6.2.*", // v6.2.5
        "zenstruck/browser": "^1.2", // v1.2.0
        "zenstruck/foundry": "^1.26" // v1.28.0
    }
}
userVoice