Skip to content

JSON Web Token (JWT) auth option provider #1098

@bramcordie

Description

@bramcordie

I've been working on custom Keycloak and Shibboleth (OIDC) providers that use signed JSON web tokens to authenticate the client instead of a secret. It is based on the specifications in this RFC: https://datatracker.ietf.org/doc/html/rfc7523#section-2.2.
Just like #653, I implemented a new auth option provider that allows you to bring your own JWT generator. Your provider probably already comes with a JWT library that you can easily plug in. This prevents any extra or unnecessary dependencies, and keeps the option provider itself very basic:

class JwtAuthOptionProvider extends PostAuthOptionProvider
{
    /**
     * @var callable():string
     */
    private $jwtGenerator;

    /**
     * @param callable():string $jwtGenerator
     */
    public function __construct(callable $jwtGenerator)
    {
        $this->jwtGenerator = $jwtGenerator;
    }

    #[\Override]
    public function getAccessTokenOptions($method, array $params)
    {
        $params['client_assertion'] = ($this->jwtGenerator)();
        $params['client_assertion_type'] = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer';

        // The AbstractProvider adds client_id and client_secret to params if they are set.
        // When using a JWT, the client_secret should not be sent and the client id is already part of the JWT (iss, sub).
        unset($params['client_id'], $params['client_secret']);

        return parent::getAccessTokenOptions($method, $params);
    }
}

I'm using a custom Keycloak provider based on https://github.com/stevenmaguire/oauth2-keycloak, because it does not yet support JWTs for client authentication. I plan on upstreaming to the specific provider but I wanted to check here first for the auth option provider part. There might be other providers, that I'm not yet aware of, that could make us of it too.

I still have some testing to do with my custom providers but if there's any interest in the auth option provider part I would be happy to submit a pull request later on!

The existing Keycloak provider already depends on firebase/php-jwt so this is what the generator looks like using that same library:

use Firebase\JWT\JWT;

readonly class PrivateKeyJwtGenerator
{
    public const string ALG = 'RS256';
    public const string KID = 'rsa-sign';

    public function __construct(
        private string $clientId,
        private string $clientJwtKey,
        private string $tokenUrl,
    ) {
    }

    public function generate(): string
    {
        $timestamp = time();

        $payload = [
            'iss' => $this->clientId,
            'sub' => $this->clientId,
            'aud' => $this->tokenUrl,
            'jti' => bin2hex(random_bytes(16)),
            'iat' => $timestamp,
            'nbf' => $timestamp - 60,
            'exp' => $timestamp + 60,
        ];

        return JWT::encode($payload, $this->clientJwtKey, self::ALG, self::KID);
    }

    public function __invoke(): string
    {
        return $this->generate();
    }
}
class CustomProvider extends AbstractProvider
{
    /**
     * The RS256 (openssl) private key used to sign JWTs.
     */
    public ?string $clientJwtKey = null;

    public function __construct(array $options = [], array $collaborators = [])
    {
        parent::__construct($options, $collaborators);

        if (isset($this->clientJwtKey)) {
            $jwtGenerator = new PrivateKeyJwtGenerator(
                $this->clientId,
                $this->clientJwtKey,
                $this->getBaseAccessTokenUrl([]),
            );

            $this->setOptionProvider(new JwtAuthOptionProvider($jwtGenerator));
        }
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions