How to add a login with Github feature in your Symfony 5 application

How to add a login with Github feature in your Symfony 5 application

Nowadays, most applications should allow their users to log in to their application using a social network. Basically, because it allows you to read data of a user from another application, without forcing them to fill all the required data of the registration form in your application as they can simply log in to the desired social network and authorize the access. It supplies the authorization workflow for any web, desktop applications, and mobile apps as in the server-side, the web app uses an authorization code and does not interact with user credentials.

In this article, I will explain to you how to easily implement a "Login with Github" feature in your Symfony 5 application.

Requirements

You need to have a User entity in your project. This entity needs to have the 2 following extra properties with their respective getters and setters:

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
// DON'T forget the following use statement!!!
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * User
 *
 * @ORM\Table(name="user")
 * @ORM\Entity
 * @UniqueEntity("email")
 */
class User implements UserInterface
{
    /**
     * @var string|null
     *
     * @ORM\Column(name="github_id", type="string", length=255, nullable=true, options={"default"="NULL"})
     */
    private $githubId;

    /**
     * @var string|null
     *
     * @ORM\Column(name="github_access_token", type="string", length=255, nullable=true, options={"default"="NULL"})
     */
    private $githubAccessToken;
}

1. Register your OAuth App on Github

Sign in to Github and access the Developers area through this link. You can as well access this menu through your profile settings, then search for Developer settings and scroll to OAuth Apps. Click on create new App and fill the form:

Register OAuth App on Github

The information that you need to provide is simple, and the only thing you need to keep in mind is the Authorization callback URL which will be the URL endpoint that we will create later in this tutorial. In this case, the address will be /connect/check/github, however it could be different if you decide to change it as we advance in this tutorial.

After registering the application, you will obtain a Github Client ID and you can generate a new Github Client Secret. Those credentials will be required in the configuration of this implementation in later steps.

2. Install KnpUOAuth2ClientBundle

In order to make things easier to integrate any OAuth2 server, the KnpUOAuth2ClientBundle is the right thing to do the job in Symfony 5. This bundle will provide you to:

  • Allow "Social" authentication / login
  • "Connect with Facebook" type of functionality
  • Fetching access keys via OAuth2 to be used with an API
  • Doing OAuth2 authentication with Guard.

This bundle integrates with the league/oauth2-client. To install this library in your Symfony project, run the following command:

composer require knpuniversity/oauth2-client-bundle

For more information about this bundle, please visit the official repository at Github here.

3. Install the Github Client Library

The oauth2 client handles the OAuth2 authentication, however it needs to know with which client should it work. In this case, we are going to work with the Github Client library that can be installed easily with the following command:

composer require league/oauth2-github

For more information about this client, please visit the official repository at Github here.

4. Configure Github ID and Github Secret parameters

After installing both libraries, we can finally start with the implementation of our own. The first thing you need to do is to define the Github Client ID and Secret from your OAuth app created in the first step. You can define them in your .env file like this:

# project/.env
###> Social Authentication ###
OAUTH_GITHUB_CLIENT_ID="your-github-oauth-app-id"
OAUTH_GITHUB_CLIENT_SECRET="your-unique-secret-client-secret"
###< Social Authentication ###

This will be required in the knpu_oauth2_client.yaml configuration file. Then, register a new OAuth client with a custom ID that will use the Github Client library like this:

# app/config/packages/knpu_oauth2_client.yaml
knpu_oauth2_client:
    clients:
        # ID of this OAuth client will be "github_main", you can use a custom ID
        # like "my_github"
        github_main:
            # must be "github" - it activates that type!
            type: github
            # add and set these environment variables in your .env files
            client_id: '%env(OAUTH_GITHUB_CLIENT_ID)%'
            client_secret: '%env(OAUTH_GITHUB_CLIENT_SECRET)%'
            # a route name you'll create, in this case the route with id "connect_github_check"
            # that we'll create in the authentication controller
            redirect_route: connect_github_check
            redirect_params: {}
            # whether to check OAuth2 "state": defaults to true
            # use_state: true

The important properties of the client in this case are:

  • github_main: this is the name of the client that you want to declare, it can be whatever you want to identify it. This will be needed later in the Authenticator class that we'll create in the next steps.
  • type: specify the type of OAuth client that will be used, in this case, it will be GitHub as is the id of the client that we installed on step 3.
  • redirect_route: this will be the Symfony route ID that you will be redirected back to after going to Github.

5. Creating Github Authenticator

Now you need to create the Authenticator class, you can use the following code as the base for the authenticator. There are some things to keep in mind and that you need to modify in this class:

  1. In the supports method: this is the id of the CHECK route that we will create in the next step. If you decide to change it later, be sure to update it here so the authenticator will react when it's needed.
  2. In the getGithubClient method: this is the id of the client defined previously in the knpu_oauth2_client.yaml file. If you decide to change it, don't forget to update it in the authenticator as well.
  3. In the onAuthenticationSuccess method: This needs to be changed as this will be the route where the user will be redirected to after the authentication succeeds. As the routes in a project are personal, you need to define the ID of the route where the user should be redirected according to the routes that exist in your project.
  4. In the getUser method: you need to review this method as this will handle what happens when the OAuth client succeeds and fetches the information of the user in Github. You can use the information to find an already existing user or create a new one according to the fields in your user entity.

The class will look like this:

<?php
// app/src/Security/GithubAuthenticator.php
namespace App\Security;

// Your user entity
use App\Entity\User;

use Doctrine\ORM\EntityManagerInterface;
use KnpU\OAuth2ClientBundle\Security\Authenticator\SocialAuthenticator;
use KnpU\OAuth2ClientBundle\Client\Provider\FacebookClient;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
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\Core\User\UserProviderInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;

class GithubAuthenticator extends SocialAuthenticator
{
    private $clientRegistry;
    private $em;
    private $router;

    public function __construct(ClientRegistry $clientRegistry, EntityManagerInterface $em, RouterInterface $router)
    {
        $this->clientRegistry = $clientRegistry;
        $this->em = $em;
	    $this->router = $router;
    }

    public function supports(Request $request)
    {
        // continue ONLY if the current ROUTE matches the check ROUTE
        return $request->attributes->get('_route') === 'connect_github_check';
    }

    public function getCredentials(Request $request)
    {
        // this method is only called if supports() returns true

        // For Symfony lower than 3.4 the supports method need to be called manually here:
        // if (!$this->supports($request)) {
        //     return null;
        // }

        return $this->fetchAccessToken($this->getGithubClient());
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        /** @var League\OAuth2\Client\Provider\ResourceOwnerInterface $githubUser */
        $githubUser = $this->getGithubClient()->fetchUserFromToken($credentials);
        
        // Note: normally, email is always null if the user has no public email address configured on Github
        // https://stackoverflow.com/questions/35373995/github-user-email-is-null-despite-useremail-scope
        $email = $githubUser->getEmail();

        // 1. have they logged in with Github before? Easy!
        $existingUser = $this->em->getRepository(User::class)->findOneBy(['githubId' => $githubUser->getId()]);
        
        // This array contains the API information of the Authenticated Github user
        $githubData = $githubUser->toArray();
        
        if ($existingUser) {
            return $existingUser;
        }
        
        // If your application requires an email to persist an User entity, you need to figure out one in case that the Github user doesn't provide one
        if(!$email){
            $email = "{$githubUser->getId()}@githuboauth.com";
        }

        // If the user exists, use it
        if ($existingUser) {
            $user = $existingUser;
        
        // Otherwise, create a new one (?)
        } else {
            // 2) do we have a matching user by email? If so, we shouldn't create a new user, we may use the same entity and set the github id
            $user = $this->em->getRepository(User::class)->findOneBy(['email' => $email]);

            // If it still doesn't exist, you need to create a new one
            // Here comes the custom logic of the creation of your user
            if (!$user) {

                // e.g. This is just an example, it depends of your user entity, so be sure to modify this
                /** @var User $user */
                $user = new User();
                $now = new \DateTime();
                $user->setLastLogin($now);
                $user->setRegisteredAt($now);
                $user->setName($githubData["name"]);
                $user->setPassword(null);
                $user->setEmail($email);
            }
        }

        // Finally, there should always exist an $user object
        // So update the GithubId and persist it if it doesn't exist
        $user->setGithubId($githubUser->getId());
        $this->em->persist($user);
        $this->em->flush();

        return $user;
    }

    /**
     * @return GithubClient
     */
    private function getGithubClient()
    {
        // "github_main" is the key used in config/packages/knpu_oauth2_client.yaml
        return $this->clientRegistry->getClient('github_main');
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        // change "app_homepage" to some route in your app
        $targetUrl = $this->router->generate('pages_homepage');

        return new RedirectResponse($targetUrl);
    
        // or, on success, let the request continue to be handled by the controller
        //return null;
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
        $message = strtr($exception->getMessageKey(), $exception->getMessageData());

        return new Response($message, Response::HTTP_FORBIDDEN);
    }

    /**
     * Called when authentication is needed, but it's not sent.
     * This redirects to the 'login'.
     */
    public function start(Request $request, AuthenticationException $authException = null)
    {
        return new RedirectResponse(
            '/connect/', // might be the site, where users choose their oauth provider
            Response::HTTP_TEMPORARY_REDIRECT
        );
    }
}

After creating the authenticator class, you need to register it. This can be done in the security.yaml file like this:

Note: as you probably have the default authenticator of your application, be sure to specify it as the entry_point of your application to prevent an exception from appearing.

# app/config/packages/security.yaml
security:
    firewalls:
        main:
            guard:
                # Define the default entry point
                entry_point: App\Security\LoginFormAuthenticator
                authenticators:
                    - App\Security\LoginFormAuthenticator
                    # Add the new Authenticator of Github
                    - App\Security\GithubAuthenticator

6. Create Authentication Controller

We are almost there! You need to create now the 2 routes that we need to send the user to authenticate with Github and the one they will be redirected to when the authentication succeeds. This step is personal and you may handle it in the way you want, in my case, as I may need to implement other social networks to login with, I'll handle those routes in a single controller. If that's not your case, you may create a controller like GithubController to handle these routes. Create the controller with the 2 mentioned methods like this:

<?php
// app/src/Controller/SocialAuthenticationController.php
namespace App\Controller;

use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;

class SocialAuthenticationController extends AbstractController
{
    /**
     * Link to this controller to start the "connect" process
     * @param ClientRegistry $clientRegistry
     *
     * @Route("/connect/github", name="connect_github_start")
     *
     * @return \Symfony\Component\HttpFoundation\RedirectResponse
     */
    public function connectGithubAction(ClientRegistry $clientRegistry)
    {
        return $clientRegistry
            // ID used in config/packages/knpu_oauth2_client.yaml
            ->getClient('github_main')
            // Request access to scopes
            // https://github.com/thephpleague/oauth2-github
            ->redirect([
                'user:email'
            ])
        ;
    }

    /**
     * After going to Github, you're redirected back here
     * because this is the "redirect_route" you configured
     * in config/packages/knpu_oauth2_client.yaml
     *
     * @param Request $request
     * @param ClientRegistry $clientRegistry
     *
     * @Route("/connect/github/check", name="connect_github_check")
     * @return \Symfony\Component\HttpFoundation\RedirectResponse
     */
    public function connectGithubCheckAction(Request $request, ClientRegistry $clientRegistry)
    {
        return $this->redirectToRoute('pages_homepage');
    }
}

In this controller, we created the 2 routes that are required in the authenticator class and the configuration file of the knpu_oauth2_client. In the connect_github_start route is where everything will start, as the user will be redirected to the Github website to login and authorize the scopes that we'll use for our app, in this case, the personal information of the user and the email. When the user accepts, he will be redirected to the connect_github_check route and the workflow of your app will continue as usual with a new user authenticated from Github.

7. Testing

All that remains is to create a button or link that redirects the user to Github to authenticate like this in some of your views:

<a href="{{ path('connect_github_start') }}">
    Login with Github
</a>

Happy coding ❤️!

This could interest you

Become a more social person