gdbots/pbjx-bundle-php

View on GitHub
src/PbjxTokenSigner.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php
declare(strict_types=1);

namespace Gdbots\Bundle\PbjxBundle;

use Gdbots\Pbjx\PbjxToken;
use Gdbots\Schemas\Pbjx\Enum\Code;

final class PbjxTokenSigner
{
    /**
     * Default kid to use when creating new tokens.
     *
     * @var string|null
     */
    private ?string $defaultKid;

    /**
     * An array of secrets keyed by the kid used to
     * create and validate signatures.
     *
     * @var string[]
     */
    private array $keys = [];

    public function __construct(array $keys, ?string $defaultKid = null)
    {
        $backupKid = null;
        foreach ($keys as $key) {
            $this->keys[$key['kid']] = $key['secret'];
            $backupKid = $key['kid'];
        }

        $this->defaultKid = $defaultKid ?: $backupKid;
    }

    /**
     * Adds a signing key (will overwrite existing kid if present).
     *
     * @param string $kid
     * @param string $secret
     */
    public function addKey(string $kid, string $secret): void
    {
        $this->keys[$kid] = $secret;
    }

    /**
     * Removes a signing key
     *
     * @param string $kid
     */
    public function removeKey(string $kid): void
    {
        unset($this->keys[$kid]);
    }

    /**
     * Creates a new signed token for the provided content.
     *
     * @param string      $content Pbjx content that is being signed
     * @param string      $aud     Pbjx endpoint this token will be sent to
     * @param string|null $kid     Key ID to use to sign the token.
     *
     * @return PbjxToken
     */
    public function sign(string $content, string $aud, ?string $kid = null): PbjxToken
    {
        $kid = $kid ?: $this->defaultKid;
        return PbjxToken::create($content, $aud, $kid, $this->getSecret($kid));
    }

    /**
     * Validates that the provided token is valid and okay to use
     * and also creates a new token with the same secret and content
     * and compares our result to the provided token to determine
     * if they are an exact match.
     *
     * If no exception is thrown the token is valid.
     *
     * @param string $content Pbjx content that has been signed
     * @param string $aud     Pbjx endpoint this token was sent to
     * @param string $token   The token string (typically from header x-pbjx-token)
     *
     * @throws \Throwable
     */
    public function validate(string $content, string $aud, string $token): void
    {
        $actualToken = PbjxToken::fromString($token);
        $expectedToken = PbjxToken::create(
            $content,
            $aud,
            $actualToken->getKid(),
            $this->getSecret($actualToken->getKid()),
            [
                'exp' => $actualToken->getExp(),
                'iat' => $actualToken->getIat(),
            ]
        );

        if (!$actualToken->equals($expectedToken)) {
            throw new \InvalidArgumentException('PbjxTokens do not match.', Code::INVALID_ARGUMENT->value);
        }
    }

    /**
     * @param string $kid
     *
     * @return string
     *
     * @throws \InvalidArgumentException
     */
    private function getSecret(string $kid): string
    {
        $secret = $this->keys[$kid] ?? null;
        if (null !== $secret) {
            return $secret;
        }

        throw new \InvalidArgumentException('PbjxTokenSigner given unknown kid.', Code::INVALID_ARGUMENT->value);
    }
}