Modern OAuth2 Discord Authentification with Symfony
Tags: symfony, php, oauth2, discord
Published: 2023-08-29 16:38:01

---
title: Modern OAuth2 Discord Authentification with Symfony
published: true
description: Quick but complete tutorial which described step by step how to easily implement Discord authentication into modern Symfony applications.
tags: symfony, php, oauth2, discord
# cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hjfhzx3037f6y21vnt2r.jpg
# published_at: 2023-08-29 06:37 +0000
---

Are you making an app that's related to gaming ? Are you requiring users to be authenticated ? But most importantly, are you a gamer (although that's not a requirement) ? 

This tutorial has been created for you !

---

## The OAuth2 protocol

If you are already familiar with the OAuth2 protocol, you can already skip to the next part.

I'm not going to make any revolutionary speech about what is the OAuth2 protocol and how great it is, so at first, I'm going to quote the official [oauth2.net](https://oauth.net/2/) website for this one :
> *OAuth 2.0 is the industry-standard protocol for authorization. OAuth 2.0 focuses on client developer simplicity while providing specific authorization flows for web applications, desktop applications, mobile phones, and living room devices.*

In simple words, it means the users, as clients, only have to click buttons to connect into your app using external services such as Google, Facebook, Discord, etc.

Other than simplifying authentication, it also makes registration way easier, since there's none !

With this protocol, the main resource, the key, is the **access token**. The goal here is to obtain this access token using your secret API key, and use it in order to retrieve resources from the provider (such as user data).

Here I've unleashed my skills and made a drawing on Paint so you get the idea :

![OAuth2 drawing](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nlgz1vrie6379x6aoxb9.png)

*Note that authentication and resource servers can be the same.*

## Registering your app

The very first step but not the least, going onto [Discord's developer portal](https://discord.com/developers/docs/intro) in order to register your app. Once you've done that, you will obtain your API keys that will make you able to send requests to the Discord API.

⚠️ *Before you continue, make sure you have a Discord account which is still available.*

### Find the button

At first, click the "New application" button. You can find it on the top right hand corner of the portal, next to your profile :

![Create application](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1lidx0f4ac2js4nops4n.png)

### Enter the name

Once you've done that, a popup will appear so you can give a name to the app. For the purpose of the tutorial, the app will be called "OAuthTutorial" :

![Application name popup](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ewvs4gkt4vlsrbrv0f36.png)

Obviously, don't forget to read the terms of service 😉

You now have access to the dashboard of the app. 

## Create the Symfony application

In this part, we are going to run throughout basic Symfony project setup. If you are already familiar with Symfony, you can already go to the next part.

### Set the project up

Open a terminal, and the following command :

```bash
symfony new --webapp DiscordOauthTutorial
```

If you havn't installed [Symfony CLI](https://github.com/symfony-cli/symfony-cli) yet, although I highly recommend you to do so, run the following boring command :

```bash
composer create-project symfony/skeleton:"6.3.*" DiscordOauthTutorial
cd DiscordOauthTutorial
composer require webapp
```

### Run the webserver

Once you've opened the project in your favorite IDE *which must obviously be PHP Storm I mean what else*, run the following command :

```bash
symfony server:start
```

You now have launched the local web server, which makes the app available at https://127.0.0.1:8000/ by default. If you click that link, you will be greeted by the Symfony welcome page 🥳.

If you encounter problems or wish to learn more about local web servers, [click here](https://symfony.com/doc/current/setup/symfony_server.html).

## Database things

This part is for people who would like to authenticate the user using Symfony's authentication system. This requires creating a database with a `User` entity. If you don't want to do so ([which is possible](https://github.com/knpuniversity/oauth2-client-bundle#step-3-use-the-client-service)), skip to the next part.

At first, let's modify the database information in our `.env` file to match our database's settings :

```
DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
```

*Let's assume we are working on MariaDB.*

Then run :

```batch
php bin/console doctrine:database:create
```

### Create the entity

Now that our database is alive, run the following command :

```batch
php bin/console make:entity
```

For the purpose of the tutorial, we are going to call the entity `User` (how normy).

The main goal here is to add a (e.g.) `discordId` property, which will be a `string`. This will hold the user's Discord ID so you will be able to retrieve or create it while authenticating.

Our entity should look like this :

```php
#[ORM\Entity(repositoryClass: UserRepository::class)]
class User
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private ?string $discordId = null;

    // getters and setters
}

```

### Migrating to the database

Let's now use the magic of Doctrine to push the entity into our database. In order to do so, Doctrine uses [migrations](https://www.doctrine-project.org/projects/doctrine-migrations/en/3.6/reference/introduction.html). These are lines of SQL that describe what's changed in our database since last migration.

Obviously we don't have to generate migrations by ourselves, just run :

```bash
php bin/console make:migration
```

This will generate a file into the `migrations` folder of our project. The migration name is "Version" followed by the datetime.

Check it, push it.

```bash
php bin/console doctrine:migrations:migrate
```

## Let's do it

Fortunately, this planet has produced great people that make great things. I don't know if the people from KNP University are great, but from what I know, is that they make great tools to help us implementing complex things. So let's use one of these tools, the [KNP OAuth2 Client bundle](https://github.com/knpuniversity/oauth2-client-bundle).

### Install the bundle

To install the bundle, run :

```bash
composer require knpuniversity/oauth2-client-bundle
```

After Composer has downloaded the bundle, it will ask you if you wish to execute the recipes for this bundle, which you should. The recipes will generate the configuration file for the bundle, which can be found at `config/packages/knpu_oauth2_client.yaml`.

If, for some reasons, that does not happen, create the file and paste this in it :

```yaml
knpu_oauth2_client:
    clients:
        # configure your clients as described here: https://github.com/knpuniversity/oauth2-client-bundle#configuration
```

### Install the OAuth Discord client

Since the KNP package provides a flexible tool for OAuth authentication, it requires you to install clients for it to work. Another great person on Earth has made it ! So, in order to install it, run :

```bash
composer require wohali/oauth2-discord-new
```

[More information about the client here](https://github.com/wohali/oauth2-discord-new)

### Configure the client

Since we're authenticating our users using OAuth2, we need API keys. Spot on, you've just created a Discord app ! Let's gather what we need !

At first, go to the dashboard of our app, and get the application ID :

![Discord developer dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9auexczrlfwptjliulb3.png)

Go to our `.env.local` file, and paste the application ID into a brand new variable that is going to be called `DISCORD_APP_ID` :

```
DISCORD_APP_ID=app_id
```

For the secret key, it's always more complicated...

Click on the "OAuth2" section of the sidebar on the left side of the screen, and click the "General" link :

![Discord developer portal OAuth2 section](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/prnvbk06wqfjto0nhudf.png)

On this page, you should find this :

![Secret key button](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/715oxoiztuuimlaculru.png)

Click this button, get the secret key, hold it tightly, and paste it into another variable that will be called `DISCORD_SECRET_KEY` :

```
DISCORD_SECRET_KEY=app_secret
```

*Note that the app's secret key is important and private, and must not leak. If you have problems configuring the project, do not send screenshots on which people can see it.*

Now that our environment variables are set, go back to the YAML configuration file, and fill it like so :

```yaml
knpu_oauth2_client:
    clients:
        discord:
            type: discord
            client_id: '%env(DISCORD_APP_ID)%'
            client_secret: '%env(DISCORD_SECRET_KEY)%'
            redirect_route: auth_discord_login
```

As you can see, we have a `redirect_route` parameter that sets a route name that does not exist yet. This route is where the magic will happen for the user to authenticate.

### Create the login route

Let's create a [Controller](https://symfony.com/doc/current/controller.html) named `DiscordController`. I suggest you not to use the [maker command](https://symfony.com/doc/current/controller.html#generating-controllers) since it also generates a template, which we don't want for this one.

Once the Controller is created, create the route like so :

```php
namespace App\Controller;

use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;

#[AsController]
#[Route("/auth/discord", name: "auth_discord_")]
final class DiscordController
{
    #[Route("/login", name: "login")]
    public function login(Request $request, ClientRegistry $clientRegistry)
    {
    }
}
```

Here the method is empty, since the login process will be handled by the **custom authenticator**. However, you can [add logic in order to check authentication](https://github.com/knpuniversity/oauth2-client-bundle#step-3-use-the-client-service) in it if you wish to.

For the purpose of making it clean, we are separating the Discord authentication into a `/auth/discord/` prefix.

- The `/auth` part to make clear that we are in an authentication process.
- The `/discord` part to separate the Discord auth from any authentication you could add.

Now that our app knows the way, Discord must also. So let's go back to the developer portal, onto the OAuth2 > General page.

In this page, you will be able to set the **redirect URL**. As a reminder, in the OAuth2 process, it's the URL that receives the code generated after the user said "Yes" for authentication on the vendor's page (the user must say "Yes" for you to access its data).

So let's add this URL :

![Redirect URL configuration on Discord developer portal](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/twyhotn392h2tgn7cj2w.png)

### Create the authenticator

In order to manage authentication, Symfony uses [Authenticators](https://symfony.com/doc/current/security/custom_authenticator.html). These basically are services that defines how to retrieve the User and what to do once the job is done.

Let's now create the big boy :

```php
<?php

namespace App\Security;

use App\Entity\User;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
use Wohali\OAuth2\Client\Provider\DiscordResourceOwner;

final class DiscordAuthenticator extends OAuth2Authenticator implements AuthenticationEntryPointInterface
{
    public function __construct(
        private readonly ClientRegistry $clientRegistry,
        private readonly EntityManagerInterface $em,
        private readonly RouterInterface $router,
        private readonly UserRepository $userRepository
    ) {}

    public function start(Request $request, AuthenticationException $authException = null): RedirectResponse
    {
        return new RedirectResponse($this->router->generate("auth_discord_start"), Response::HTTP_TEMPORARY_REDIRECT);
    }

    public function supports(Request $request): ?bool
    {
        return $request->attributes->get("_route") === "auth_discord_login";
    }

    public function authenticate(Request $request): SelfValidatingPassport
    {
        $client = $this->clientRegistry->getClient("discord");
        $accessToken = $this->fetchAccessToken($client);

        return new SelfValidatingPassport(
            new UserBadge($accessToken->getToken(), function () use ($accessToken, $client) {
                /** @var DiscordResourceOwner $discordUser */
                $discordUser = $client->fetchUserFromToken($accessToken);

                $user = $this->userRepository->findOneBy(["discordId" => $discordUser->getId()]);

                if (null === $user) {
                    $user = new User();
                    $user->setDiscordId($discordUser->getId());

                    $this->em->persist($user);
                }

                $this->em->flush();

                return $user;
            })
        );
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
    {
        // redirect to user to your post authentication page (e.g. dashboard, home)
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
    {
        // do something
    }
}
```

Let's make a quick run through what's going on in here :

- The `start` method defines the **entrypoint** of the authentication. The goal for us here is to redirect the user to Discord's portal (in other words, start the OAuth2 process). As you can see, it references a route we haven't created yet.

- The `supports` method defined whether the current Request must be supported by our authenticator. This should happen only if the current route is the login route we've created before.

- The `authenticate` method is the heart of the authenticator. It must return a [`Passport`](https://symfony.com/doc/current/security/custom_authenticator.html#security-passport) that will contain the user to authenticate.

- The `onAuthenticationSuccess` defines our post-authentication process. We mostly want to redirect users to their homepage here.

- The `onAuthenticationFailure` allows you to interact with authentication failures if you need to.

Now that our authenticator is all set up, let's make Symfony aware that it is in charge of authentication. Let's now open the `config/packages/security.yaml` file, and make it look like this :

```yaml
security:
    providers:
        users:
            entity:
                class: 'App\Entity\User'
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            lazy: true
            provider: users
            custom_authenticators:
                - App\Security\DiscordAuthenticator
            logout:
                path: app_logout
                target: app_index
```

Symfony is now aware that you are authenticating users using the `User` entity through the `App\Security\DiscordAuthenticator` authenticator.

BUT

We are not done yet 😄

### Make the User entity authentication-friendly

Following Symfony's way to build authentication, we now have to make our `User` entity implement the `UserInterface`. It should now look like this :

```php
#[ORM\Entity(repositoryClass: UserRepository::class)]
class User implements UserInterface
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private ?string $discordId = null;

    // getters and setters

    public function getRoles(): array
    {
        // TODO: Implement getRoles() method.
    }

    public function eraseCredentials()
    {
        // TODO: Implement eraseCredentials() method.
    }

    public function getUserIdentifier(): string
    {
        // TODO: Implement getUserIdentifier() method.
    }
}
```

*This tutorial does not aim to dive into Symfony authentication. If you need more information about the User, [click here](https://symfony.com/doc/current/security.html#the-user).*

### Create the entrypoint

Going back to the authenticator, don't forget the entrypoint ! In order to set it up, let's go back to our `DiscordController`, and add the `auth_discord_start` route like so :

```php
#[Route("/start", name: "start")]
public function start(ClientRegistry $clientRegistry): RedirectResponse
{
    return $clientRegistry->getClient("discord")->redirect(["identify"]);
}
```

In this method, we are getting the client called `discord` from the registry. The name `discord` does not come out of nowhere. Indeed, this is the one we have set earlier in the YAML configuration of the KNP bundle. Don't forget to change the name if you did in the YAML.

After getting the right client, we call the `redirect` method in order to redirect the user to the Discord authentication portal. Note that we set the [list of scopes](https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes) to `[identify]`. It will allow the client to make requests to the `/users/@me` URL and gather users' information.

Now that this is all set up, you can now add a button to the UI that redirects to this URL. Once the user is on this URL, it gets redirected to the Discord authentication portal :

![Discord OAuth2 portal](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xchnxcls6nbwvuert4ic.png)

*Well yes this is French that's not the point.*

After the user clicks the "Autoriser" button, it should be redirected to the `/auth/discord/login` URL, with a little bonus in the form of a `code` query parameter. This code will be used in order to retrieve the access token, which is done automatically here.

While testing on our dev environement, after authenticating, we should be able to witness the authentication success using the Symfony Web debug toolbar :

![Symfony web debug toolbar showing successfull authentication](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3f063xv6a8xzdvye7g9v.png)

If that's the case, **good job, our full OAuth2 authentication process is done and successful !**

## Conclude with logging out

Hey ! Hold on a second ! Haven't we forgot something ? No worries, disconnecting users with Symfony is very easy. Indeed, you have already started configuring the way you want to log the user out !

In the `config/packages/security.yaml` file, we added that :

```yaml
logout:
    path: app_logout
    target: app_index
```

This part defines that our logout route name is `app_logout`, and that the user will be redirected to `app_index` once logged out.

All we need to do now is to declare the logout route :

```php
namespace App\Controller;

use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Annotation\Route;

#[AsController]
final class SecurityController
{
    #[Route('/logout', name: 'app_logout')]
    public function logout()
    {
        throw new \Exception('Logging out');
    }
}
```

---

## Conclusion

Thank you for those who followed this tutorial all along. I hope that helped you, and that everything is clear. I could have gone deeper at some points, but you know, the tutorial must stay readable !

More importantly, I hope you got the point of OAuth2 ! In this tutorial, we are using Discord. Thankfully, they have a clean OAuth2 system that can help you get introduced to the protocol. Once you have understood the protocol, it is only a matter of implementation !

If you need to, go and check out the [Github repository](https://github.com/FabienLge/symfony-discord-oauth) for this tutorial.

Thanks a lot again for reading my very first article. If you need more explaination on anything, let's meet in the comment section !

Happy coding, and don't forget that PHP is not dead !