timrourke/incognito

View on GitHub
src/Token/Validation/ClaimsValidator.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php

declare(strict_types=1);

namespace Incognito\Token\Validation;

use Jose\Component\Checker\ClaimCheckerManager;
use Jose\Component\Checker\HeaderCheckerManager;
use Jose\Component\Checker\InvalidClaimException;
use Jose\Component\Checker\InvalidHeaderException;
use Jose\Component\Core\Util\JsonConverter;
use Jose\Component\Signature\JWS;
use Jose\Component\Signature\JWSTokenSupport;

class ClaimsValidator
{
    /**
     * @var string[]
     */
    private const REQUIRED_HEADER_CLAIMS = [
        'alg',
        'kid',
    ];

    /**
     * @var string[]
     */
    private const REQUIRED_PAYLOAD_CLAIMS = [
        'token_use',
    ];

    /**
     * @var \Jose\Component\Checker\ClaimCheckerManager
     */
    private ClaimCheckerManager $claimChecker;

    /**
     * @var \Jose\Component\Checker\HeaderCheckerManager
     */
    private HeaderCheckerManager $headerChecker;

    /**
     * @var \Jose\Component\Signature\JWSTokenSupport
     */
    private JWSTokenSupport $tokenSupport;

    /**
     * Constructor.
     *
     * @param \Jose\Component\Checker\ClaimCheckerManager  $claimChecker
     * @param \Jose\Component\Checker\HeaderCheckerManager $headerChecker
     * @param \Jose\Component\Signature\JWSTokenSupport    $tokenSupport
     */
    public function __construct(
        ClaimCheckerManager $claimChecker,
        HeaderCheckerManager $headerChecker,
        JWSTokenSupport $tokenSupport
    ) {
        $this->claimChecker = $claimChecker;
        $this->headerChecker = $headerChecker;
        $this->tokenSupport = $tokenSupport;
    }

    /**
     * Check a token for having valid header values and claims
     *
     * @param  \Jose\Component\Signature\JWS $token
     * @return bool
     * @throws \InvalidArgumentException
     * @throws \Jose\Component\Checker\InvalidHeaderException
     * @throws \Jose\Component\Checker\MissingMandatoryHeaderParameterException
     * @throws \Jose\Component\Checker\InvalidClaimException
     * @throws \Jose\Component\Checker\MissingMandatoryClaimException
     */
    public function validate(JWS $token): bool
    {
        $this->verifyRequiredHeaderClaims($token);
        $this->headerChecker->check($token, 0);

        $payload = (string) $token->getPayload();
        $claims = JsonConverter::decode($payload);
        $this->verifyRequiredPayloadClaims($claims);
        $this->claimChecker->check($claims);

        return true;
    }

    /**
     * Verify that all required header claims are present
     *
     * @param  \Jose\Component\Signature\JWS $token
     * @throws \Jose\Component\Checker\InvalidHeaderException
     */
    private function verifyRequiredHeaderClaims(JWS $token): void
    {
        $headers = $this->getTokenHeaders($token);

        foreach (self::REQUIRED_HEADER_CLAIMS as $requiredHeaderClaim) {
            if (!array_key_exists($requiredHeaderClaim, $headers)) {
                throw new InvalidHeaderException(
                    sprintf(
                        'The required header claim "%s" is missing from the token.',
                        $requiredHeaderClaim
                    ),
                    $requiredHeaderClaim,
                    null
                );
            }
        }
    }

    /**
     * Verify that all required payload claims are present
     *
     * @param  array<string, mixed> $claims
     * @throws \Jose\Component\Checker\InvalidClaimException
     */
    private function verifyRequiredPayloadClaims(array $claims): void
    {
        foreach (self::REQUIRED_PAYLOAD_CLAIMS as $requiredPayloadClaim) {
            if (!array_key_exists($requiredPayloadClaim, $claims)) {
                throw new InvalidClaimException(
                    sprintf(
                        'The required payload claim "%s" is missing from the token.',
                        $requiredPayloadClaim
                    ),
                    $requiredPayloadClaim,
                    null
                );
            }
        }
    }

    /**
     * Get the headers from a token.
     *
     * We only intend to validate the protected headers of a token, so we can
     * ignore the unprotected headers here.
     *
     * @param  \Jose\Component\Signature\JWS $token
     * @return array<string, mixed>
     */
    private function getTokenHeaders(JWS $token): array
    {
        $protected = [];
        $unprotected = [];
        $this->tokenSupport->retrieveTokenHeaders(
            $token,
            0,
            $protected,
            $unprotected
        );

        return $protected;
    }
}