Chapters
-
Course Code
Subscribe to download the code!
Subscribe to download the code!
-
This Video
Subscribe to download the video!
Subscribe to download the video!
-
Subtitles
Subscribe to download the subtitles!
Subscribe to download the subtitles!
-
Course Script
Subscribe to download the script!
Subscribe to download the script!
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot in the video!
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.
9 Comments
Hey @Sebastian-K!
Hmmm, very interesting situation! Ok, so typically (as you know), we read the roles from the User object. This means that, once the user logs in, the roles are static throughout the rest of that user's session. Your situation is a bit different, if I understand it, because the user could switch the project, which would change the roles for that user. There are two approaches I can think of, and I'm not positive which will be better :)
A) Try to "change" the roles on the user when the project is selected/changed. In reality, when the user authenticates, the "roles" are stored on the "token". The roles for the token are populated from User::getRoles()
(https://github.com/symfony/symfony/blob/6.4/src/Symfony/Component/Security/Http/Authenticator/AbstractAuthenticator.php#L31) but ultimately, the final roles live on the token - and that's what matters for authorization.
Specifically, the RoleVoter
calls $token->getRoleNames()
during authorization to figure out what roles the currently-authenticated user has - https://github.com/symfony/symfony/blob/6.4/src/Symfony/Component/Security/Core/Authorization/Voter/RoleVoter.php#L66
So, in theory, you could create your own, custom token class (that likely extends AbstractToken
) and give this a method like setRoles()
where you can override the current roles. To allow this to be used, you would need to extend / hook into whatever your authentication "authenticator" is and create your custom token object instead of the default one: https://github.com/symfony/symfony/blob/6.4/src/Symfony/Component/Security/Http/Authenticator/AbstractAuthenticator.php#L29-L32
Then, when the user changes projects, you could set the new roles onto your token using that ->setRoles()
method. Since it sounds like you're using session-based authentication, the updated token with the updated roles would get serialized into the session at the end of that request and used on the future requests.
B) The other approach would be to do nothing special with the token or trying to make the roles dynamic. In this case, instead of protecting your app with roles like ROLE_EDITOR
, you would create some new prefix for project-related roles and use things like PROJECT_EDITOR
. You would then create a custom voter for this, which would look at the current project and see if the user has this "role". This solution is a bit simpler.
Cheers!
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 !
Lol - thank you @Pierre-A
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.
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!
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
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
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!
"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
}
}
Hi, it's me again. Asking questions here and there 😅. I am not sure if this is the correct topic for my question, but its about authorization, so:
We are building an app where the user can work on different projects, so depending on the selected project, we automatically inject the right project IRI to every request as a filter. There is also a automatic condition in my backend repository like
project in (:allValid)
for item and collectionThis works fine, but we need the user to have different roles depending on the project. We have a
ProjectUser
entity with project, user and roles and I plan to somehow inject the roles on authentication into my user and then everything afterwards works like before (iE Security voters)But this implies that I need exactly one project (so no more filter for the current project) because I need to find exactly one
ProjectUser
so I am thinking about saving the current project in the session (will be set on login and by custom route to switch).I know how to save it into the session but not how to inject the roles into my user depending on the selected project.
Is this the right approach?