League OAuth 2.0 server with Symfony – User, Client and Scope

Today we are going to start with the actual implementations of the league OAuth 2 server in Symfony 4.

If you haven’t done already you should first read the introduction article. This will introduce you to basic of OAuth 2. We will be starting from there.

The idea for this and the upcoming articles is to show you an example of how I would implement an OAuth 2 server in Symfony. This with maximum decoupling.

Does this mean that I’m providing the best solution for your situation? Probably not. But I hope to give you a headstart, and idea and example how you could do it. You are then free to tailor this to your needs.

Setup

We will be starting from a basic setup including the Symfony Security bundle with a custom User entity, repository and User provider. You can find this here.

Now we have the basic setup required to focus on implementing the League OAuth 2.0 server. The first step is to install the package trough composer.

composer require league/oauth2-server

Before we can start authentication we have to set up an Authorization server.

In the league OAuth 2.0 server example you will find this:

$server = new \League\OAuth2\Server\AuthorizationServer(
    $clientRepository,
    $accessTokenRepository,
    $scopeRepository,
    $privateKey,
    $encryptionKey
);

As you can see. We need to create a client, access token and scope repository. The first grant that we will be going to enable is the password grant. This grant also requires a UserRepository.

In this weeks article, we are going to create the bridge between our User entity and the OAuth 2.0 user. We then will be creating a Client entity and we will also be bridging that one to the server. Then last we are going to set up the ScopeRepository.

User

League OAuth2 Server requires us to implement their UserRepositoryInterface. We could create a Doctrine repository and them implement that interface to it.

In some situations, this might be ok. But that’s not the goal of this series of articles. I want to show you how to implement this with the least amount of coupling.

What we will be doing is to create a new Repository that we will only be using for the OAuth2 server and which is deep inside the infrastructure layer.

namespace App\Infrastructure\oAuth2Server\Bridge;

use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\UserEntityInterface;
use League\OAuth2\Server\Repositories\UserRepositoryInterface;

final class UserRepository implements UserRepositoryInterface
{
    /**
     * {@inheritdoc}
     */
    public function getUserEntityByUserCredentials(
        $username,
        $password,
        $grantType,
        ClientEntityInterface $clientEntity
    ): ?UserEntityInterface {
        // Todo
    }
}

We also need a User entity that implements the UserEntityInterface. Again, we could use our domain entity. But again, we would be coupling our domain to the infrastructure. We should not implement an interface defined in the infrastructure.

The other issue is that the UserEntityInterface requires us to have a method getIdentifier. Well, we don’t want that in our domain.

Again the solution is to create an entity-specific for oAuth2 server inside the infrastructure layer.

namespace App\Infrastructure\oAuth2Server\Bridge;

use League\OAuth2\Server\Entities\Traits\EntityTrait;
use League\OAuth2\Server\Entities\UserEntityInterface;

final class User implements UserEntityInterface
{
    use EntityTrait;

    /**
     * User constructor.
     * @param $identifier
     */
    public function __construct($identifier)
    {
        $this->setIdentifier($identifier);
    }
}

As you can see. This is infrastructure. League OAuth2 has provided us with an EntityTrait that we can and thus will use to make our life easier. There is nothing wrong using this here in our infrastructure layer.

As you can see. Both this entity and repository are not connected to our domain or database. What we going to do now is to inject our domain defined UserRepository inside the OAuth2 UserRepository to get the User from the database and convert it to an OAuth 2 User entity.

First, we need to create a new name for our ‘app’ Repositories when injecting them inside the OAuth2 server.

use App\Domain\Repository\UserRepositoryInterface as AppUserRepositoryInterface;

You could name this whatever you like. This is something you should discuss with your team.

We can then, after injecting our repository, create this implementation for retrieving the correct user.

public function getUserEntityByUserCredentials(
    $username,
    $password,
    $grantType,
    ClientEntityInterface $clientEntity
): ?UserEntityInterface {
    $appUser = $this->appUserRepository->findOneByEmail($username);
    if ($appUser === null) {
        return null;
    }

    $oAuthUser = new User($appUser->getId()->toString());
    return $oAuthUser;
}

But we forgot about one thing. Shouldn’t we validate if the correct password was provided?

public function getUserEntityByUserCredentials(
    $username,
    $password,
    $grantType,
    ClientEntityInterface $clientEntity
): ?UserEntityInterface {
    $appUser = $this->appUserRepository->findOneByEmail($username);
    if ($appUser === null) {
        return null;
    }

    $isPasswordValid = $this->userPasswordEncoder->isPasswordValid($appUser, $password);
    if (!$isPasswordValid) {
        return null;
    }

    $oAuthUser = new User($appUser->getId()->toString());
    return $oAuthUser;
}

Now, this is better. We use the password encoder to encode the password and then check if it’s actually valid.

Client

Next up is creating the Client. First, we have to create a Client entity and Repository inside our domain. These are part of our domain because in the context of authentication and authorization we need to be able to define the possible clients.

namespace App\Domain\Model;

use Ramsey\Uuid\Uuid;

class Client
{
    /**
     * @var string
     */
    private $id;

    /**
     * @var string
     */
    private $name;

    /**
     * @var string
     */
    private $secret;

    /**
     * @var string
     */
    private $redirect;

    /**
     * @var bool
     */
    private $active;

    /**
     * Client constructor.
     * @param ClientId $clientId
     * @param string $name
     */
    private function __construct(ClientId $clientId, string $name)
    {
        $this->id = $clientId->toString();
        $this->name = $name;
    }

    public static function create(string $name): Client
    {
        $clientId = ClientId::fromString(Uuid::uuid4()->toString());
        return new self($clientId, $name);
    }

    public function getId(): UserId
    {
        return UserId::fromString($this->id);
    }

    // Getters and setters

We have an id and name as is to be expected for a Client. We also have secret which is used as a ‘password’.  We also require redirect later on for some specific grants. Then, at last, we want to have the ability to deactivate a client.

Then you should also create a repository interface and implementation for this entity.

Now we implement the OAuth 2 Client entity and repository. And then injecting our domain defined ClientRepositoryInterface in it. The same idea as used for User.

You could just use your domain specified entity and repository, implement the interface on those and then just as easily make use of the OAuth 2 server.

But as I have said before. We are then leaking infrastructure inside the domain. Your domain is then getting coupled to League OAuth 2, which is exactly what we don’t want to do in this series of articles.

namespace App\Infrastructure\oAuth2Server\Bridge;

use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\Traits\ClientTrait;
use League\OAuth2\Server\Entities\Traits\EntityTrait;

final class Client implements ClientEntityInterface
{
    use ClientTrait, EntityTrait;

    /**
     * Client constructor.
     * @param string $identifier
     * @param string $name
     * @param string $redirectUri
     */
    public function __construct(string $identifier, string $name, string $redirectUri)
    {
        $this->setIdentifier($identifier);
        $this->name = $name;
        $this->redirectUri = explode(',', $redirectUri);
    }
}

And then in the repository, we need one method.

namespace App\Infrastructure\oAuth2Server\Bridge;

use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Repositories\ClientRepositoryInterface;

final class ClientRepository implements ClientRepositoryInterface
{
    /**
     * {@inheritdoc}
     */
    public function getClientEntity(
        $clientIdentifier,
        $grantType = null,
        $clientSecret = null,
        $mustValidateSecret = true
    ): ?ClientEntityInterface {
        // Todo 
    }
}

The ability to get a client entity based on the identifier and secret. We also want to check if the given grant type is actually valid for this client.

public function getClientEntity(
    $clientIdentifier,
    $grantType = null,
    $clientSecret = null,
    $mustValidateSecret = true
): ?ClientEntityInterface {
    $appClient = $this->appClientRepository->findActive($clientIdentifier);
    if ($appClient === null) {
        return null;
    }

    if ($mustValidateSecret && !hash_equals($appClient->getSecret(), (string)$clientSecret)) {
        return null;
    }

    $oauthClient = new Client($clientIdentifier, $appClient->getName(), $appClient->getRedirect());
    return $oauthClient;
}

In this implementation, we look for active clients. Then if we found one we check if the secret is valid. If everything checks out. We return the OAuth2 client.

Note that we haven’t checked if this client can use this grant. We will be fixing this in a later article.

Scope

The last one we cover for this week is Scope. We don’t have to persist scope so we only have to create the entity and repository inside our OAuth2 server implementation, and we are done.

namespace App\Infrastructure\oAuth2Server\Bridge;

use League\OAuth2\Server\Entities\ScopeEntityInterface;
use League\OAuth2\Server\Entities\Traits\EntityTrait;

final class Scope implements ScopeEntityInterface
{
    use EntityTrait;

    public static $scopes = [];

    /**
     * Scope constructor.
     * @param $name
     */
    public function __construct($name)
    {
        $this->setIdentifier($name);
    }

    /**
     * @param $id
     * @return bool
     */
    public static function hasScope($id): bool
    {
        return $id === '*' || array_key_exists($id, static::$scopes);
    }

    /**
     * Get the data that should be serialized to JSON.
     *
     * @return mixed
     */
    public function jsonSerialize()
    {
        return $this->getIdentifier();
    }
}

You will notice the empty scopes static. Later on, we will define here all the possible scopes that our OAuth 2 server can provide.

namespace App\Infrastructure\oAuth2Server\Bridge;

use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\ScopeEntityInterface;
use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;

final class ScopeRepository implements ScopeRepositoryInterface
{
    /**
     * {@inheritdoc}
     */
    public function getScopeEntityByIdentifier($identifier): ScopeEntityInterface
    {
        if (Scope::hasScope($identifier)) {
            return new Scope($identifier);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function finalizeScopes(array $scopes, $grantType, ClientEntityInterface $clientEntity, $userIdentifier = null): array
    {
        $filteredScopes = [];
        /** @var Scope $scope */
        foreach ($scopes as $scope) {
            $hasScope = Scope::hasScope($scope->getIdentifier());
            if ($hasScope) {
                $filteredScopes[] = $scope;
            }
        }
        return $filteredScopes;
    }
}

The scope repository will check if the scopes asked by the client, do actually exist and are available in our server. We can then also check if the client itself actually has access to these scopes. We will be completing the scope repository in a later article.

Here you can find the solution that we have made up to now.

What’s next?

Today we created the User, Client and Scope repositories for our OAuth2 server implementation. We have used the solution that provided us with the least possible coupling.

This does not mean that the solution provided here is the best or most correct one. In some uses case,s it might be ok to actually use your Doctrine entities and repositories directly. It all depends on how much coupling you want, and how long the project is going to live.

As you can imagine, any long living or project that is going to become big. Should, in my opinion, have the least amount of possible coupling to the framework or any package that you use. This might mean you have to write more upfront code. But again this will save you pain and time later when you need to upgrade or switch packages.

Next week we will be creating the Access Token and Refresh token repositories.