To use API Token Authentication or Not?

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Here's the million-dollar question when it comes to security and APIs: does my site need some sort of API token authentication? There's a pretty good chance that the answer is no. Even if your app has some API endpoints - like ours - if you're creating these endpoints solely so that your own JavaScript for your own site can use them, then you do not need an API token authentication system. Nope, your life will be much simpler if you use a normal login form and session-based authentication.

Session-based authentication is precisely why we have access to this endpoint: we previously logged in... and our session cookie is used to authenticate us. This works just as well on a real page as on an API endpoint.

To prove it, before I started the tutorial, I created a Stimulus controller called user-api_controller.js:

import { Controller } from 'stimulus';
import axios from 'axios';
export default class extends Controller {
static values = {
url: String
}
async connect() {
const response = await axios.get(this.urlValue);
console.log(response.data);
}
}

It's dead simple: it makes an API request... and logs the result. We're going to use it to make an API request to /api/me to prove that Ajax calls can access the authenticated endpoints.

To activate the Stimulus controller, open templates/base.html.twig... and find the body element: that's an easy place to attach it: if is_granted('IS_AUTHENTICATED_REMEMBERED'), then {{ stimulus_controller() }} and the name: user-api:

... line 1
<html>
... lines 3 - 14
<body
{% if is_granted('IS_AUTHENTICATED_REMEMBERED') %}
{{ stimulus_controller('user-api', {
... line 18
}) }}
{% endif %}
>
... lines 22 - 85
</body>
</html>

So, our JavaScript will be called only if we're logged in. To pass the URL to the endpoint, add a 2nd arg with url set to path('app_user_api_me'):

... line 1
<html>
... lines 3 - 14
<body
{% if is_granted('IS_AUTHENTICATED_REMEMBERED') %}
{{ stimulus_controller('user-api', {
url: path('app_user_api_me')
}) }}
{% endif %}
>
... lines 22 - 85
</body>
</html>

And I'm realizing that I haven't given our API endpoint a route name yet... so let's do that:

... lines 1 - 7
class UserController extends BaseController
{
/**
* @Route("/api/me", name="app_user_api_me")
... line 12
*/
public function apiMe()
{
... lines 16 - 18
}
}

Back in base.html.twig, yup! My editor looks happy now.

Ok, head back to the homepage, inspect element, go to the console and... there's my user data! The Ajax request sends the session cookie and so... authentication just works.

So if the only thing that needs to use your API is your own JavaScript, save yourself a lot of trouble and just use a login form. And if you do want to get fancy and submit your login for via Ajax, you can totally do that. In fact, if you use Turbo, that happens automatically. But if you wanted to write some custom JavaScript, it's still no problem. Just use Ajax to submit the login form and the session cookie will be automatically set like normal. If you do decide to do this, the only tweak you'll need is to make your login form authenticator return JSON instead of redirecting. I would probably go back to using my custom LoginFormAuthenticator because it would be super easy to return JSON from onAuthenticationSuccess():

... lines 1 - 26
class LoginFormAuthenticator extends AbstractLoginFormAuthenticator
{
... lines 29 - 66
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
... lines 69 - 75
}
... lines 77 - 81
}

When You Do Need API Tokens

So then, when do we need an API token authentication system? The answer is pretty simple: if someone other than your own site's JavaScript needs to access your API... including if your JavaScript lives on a completely different domain. If you have this situation, you're probably going to need some sort of API token system. Whether you need OAuth or a simpler system... depends. We won't cover API tokens in this tutorial, but we create a pretty nice system in our Symfony 4 Security tutorial, which you can check out.

Next: let's add a registration form!

Leave a comment!

This tutorial also works great for Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.4.1 || ^8.0.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.3", // v3.3.0
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "doctrine/annotations": "^1.0", // 1.13.2
        "doctrine/doctrine-bundle": "^2.1", // 2.4.3
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
        "doctrine/orm": "^2.7", // 2.10.1
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "knplabs/knp-time-bundle": "^1.11", // v1.16.1
        "pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
        "pagerfanta/twig": "^3.3", // v3.3.0
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "scheb/2fa-bundle": "^5.12", // v5.12.1
        "scheb/2fa-qr-code": "^5.12", // v5.12.1
        "scheb/2fa-totp": "^5.12", // v5.12.1
        "sensio/framework-extra-bundle": "^6.0", // v6.2.0
        "stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
        "symfony/asset": "5.3.*", // v5.3.4
        "symfony/console": "5.3.*", // v5.3.7
        "symfony/dotenv": "5.3.*", // v5.3.8
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/form": "5.3.*", // v5.3.8
        "symfony/framework-bundle": "5.3.*", // v5.3.8
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/property-access": "5.3.*", // v5.3.8
        "symfony/property-info": "5.3.*", // v5.3.8
        "symfony/rate-limiter": "5.3.*", // v5.3.4
        "symfony/runtime": "5.3.*", // v5.3.4
        "symfony/security-bundle": "5.3.*", // v5.3.8
        "symfony/serializer": "5.3.*", // v5.3.8
        "symfony/stopwatch": "5.3.*", // v5.3.4
        "symfony/twig-bundle": "5.3.*", // v5.3.4
        "symfony/ux-chartjs": "^1.3", // v1.3.0
        "symfony/validator": "5.3.*", // v5.3.8
        "symfony/webpack-encore-bundle": "^1.7", // v1.12.0
        "symfony/yaml": "5.3.*", // v5.3.6
        "symfonycasts/verify-email-bundle": "^1.5", // v1.5.0
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.3
        "twig/string-extra": "^3.3", // v3.3.3
        "twig/twig": "^2.12|^3.0" // v3.3.3
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
        "symfony/debug-bundle": "5.3.*", // v5.3.4
        "symfony/maker-bundle": "^1.15", // v1.34.0
        "symfony/var-dumper": "5.3.*", // v5.3.8
        "symfony/web-profiler-bundle": "5.3.*", // v5.3.8
        "zenstruck/foundry": "^1.1" // v1.13.3
    }
}