welcome back to dyb-tech
This commit is contained in:
+125
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Lcobucci\JWT\Token;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use Lcobucci\JWT\Builder as BuilderInterface;
|
||||
use Lcobucci\JWT\ClaimsFormatter;
|
||||
use Lcobucci\JWT\Encoder;
|
||||
use Lcobucci\JWT\Encoding\CannotEncodeContent;
|
||||
use Lcobucci\JWT\Signer;
|
||||
use Lcobucci\JWT\Signer\Key;
|
||||
use Lcobucci\JWT\UnencryptedToken;
|
||||
|
||||
use function array_diff;
|
||||
use function array_merge;
|
||||
use function in_array;
|
||||
|
||||
/** @immutable */
|
||||
final class Builder implements BuilderInterface
|
||||
{
|
||||
/** @var array<non-empty-string, mixed> */
|
||||
private array $headers = ['typ' => 'JWT', 'alg' => null];
|
||||
|
||||
/** @var array<non-empty-string, mixed> */
|
||||
private array $claims = [];
|
||||
|
||||
public function __construct(private readonly Encoder $encoder, private readonly ClaimsFormatter $claimFormatter)
|
||||
{
|
||||
}
|
||||
|
||||
public function permittedFor(string ...$audiences): BuilderInterface
|
||||
{
|
||||
$configured = $this->claims[RegisteredClaims::AUDIENCE] ?? [];
|
||||
$toAppend = array_diff($audiences, $configured);
|
||||
|
||||
return $this->setClaim(RegisteredClaims::AUDIENCE, array_merge($configured, $toAppend));
|
||||
}
|
||||
|
||||
public function expiresAt(DateTimeImmutable $expiration): BuilderInterface
|
||||
{
|
||||
return $this->setClaim(RegisteredClaims::EXPIRATION_TIME, $expiration);
|
||||
}
|
||||
|
||||
public function identifiedBy(string $id): BuilderInterface
|
||||
{
|
||||
return $this->setClaim(RegisteredClaims::ID, $id);
|
||||
}
|
||||
|
||||
public function issuedAt(DateTimeImmutable $issuedAt): BuilderInterface
|
||||
{
|
||||
return $this->setClaim(RegisteredClaims::ISSUED_AT, $issuedAt);
|
||||
}
|
||||
|
||||
public function issuedBy(string $issuer): BuilderInterface
|
||||
{
|
||||
return $this->setClaim(RegisteredClaims::ISSUER, $issuer);
|
||||
}
|
||||
|
||||
public function canOnlyBeUsedAfter(DateTimeImmutable $notBefore): BuilderInterface
|
||||
{
|
||||
return $this->setClaim(RegisteredClaims::NOT_BEFORE, $notBefore);
|
||||
}
|
||||
|
||||
public function relatedTo(string $subject): BuilderInterface
|
||||
{
|
||||
return $this->setClaim(RegisteredClaims::SUBJECT, $subject);
|
||||
}
|
||||
|
||||
public function withHeader(string $name, mixed $value): BuilderInterface
|
||||
{
|
||||
$new = clone $this;
|
||||
$new->headers[$name] = $value;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
public function withClaim(string $name, mixed $value): BuilderInterface
|
||||
{
|
||||
if (in_array($name, RegisteredClaims::ALL, true)) {
|
||||
throw RegisteredClaimGiven::forClaim($name);
|
||||
}
|
||||
|
||||
return $this->setClaim($name, $value);
|
||||
}
|
||||
|
||||
/** @param non-empty-string $name */
|
||||
private function setClaim(string $name, mixed $value): BuilderInterface
|
||||
{
|
||||
$new = clone $this;
|
||||
$new->claims[$name] = $value;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<non-empty-string, mixed> $items
|
||||
*
|
||||
* @throws CannotEncodeContent When data cannot be converted to JSON.
|
||||
*/
|
||||
private function encode(array $items): string
|
||||
{
|
||||
return $this->encoder->base64UrlEncode(
|
||||
$this->encoder->jsonEncode($items),
|
||||
);
|
||||
}
|
||||
|
||||
public function getToken(Signer $signer, Key $key): UnencryptedToken
|
||||
{
|
||||
$headers = $this->headers;
|
||||
$headers['alg'] = $signer->algorithmId();
|
||||
|
||||
$encodedHeaders = $this->encode($headers);
|
||||
$encodedClaims = $this->encode($this->claimFormatter->formatClaims($this->claims));
|
||||
|
||||
$signature = $signer->sign($encodedHeaders . '.' . $encodedClaims, $key);
|
||||
$encodedSignature = $this->encoder->base64UrlEncode($signature);
|
||||
|
||||
return new Plain(
|
||||
new DataSet($headers, $encodedHeaders),
|
||||
new DataSet($this->claims, $encodedClaims),
|
||||
new Signature($signature, $encodedSignature),
|
||||
);
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Lcobucci\JWT\Token;
|
||||
|
||||
use function array_key_exists;
|
||||
|
||||
final class DataSet
|
||||
{
|
||||
/** @param array<non-empty-string, mixed> $data */
|
||||
public function __construct(private readonly array $data, private readonly string $encoded)
|
||||
{
|
||||
}
|
||||
|
||||
/** @param non-empty-string $name */
|
||||
public function get(string $name, mixed $default = null): mixed
|
||||
{
|
||||
return $this->data[$name] ?? $default;
|
||||
}
|
||||
|
||||
/** @param non-empty-string $name */
|
||||
public function has(string $name): bool
|
||||
{
|
||||
return array_key_exists($name, $this->data);
|
||||
}
|
||||
|
||||
/** @return array<non-empty-string, mixed> */
|
||||
public function all(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
return $this->encoded;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Lcobucci\JWT\Token;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Lcobucci\JWT\Exception;
|
||||
|
||||
final class InvalidTokenStructure extends InvalidArgumentException implements Exception
|
||||
{
|
||||
public static function missingOrNotEnoughSeparators(): self
|
||||
{
|
||||
return new self('The JWT string must have two dots');
|
||||
}
|
||||
|
||||
public static function missingHeaderPart(): self
|
||||
{
|
||||
return new self('The JWT string is missing the Header part');
|
||||
}
|
||||
|
||||
public static function missingClaimsPart(): self
|
||||
{
|
||||
return new self('The JWT string is missing the Claim part');
|
||||
}
|
||||
|
||||
public static function missingSignaturePart(): self
|
||||
{
|
||||
return new self('The JWT string is missing the Signature part');
|
||||
}
|
||||
|
||||
/** @param non-empty-string $part */
|
||||
public static function arrayExpected(string $part): self
|
||||
{
|
||||
return new self($part . ' must be an array with non-empty-string keys');
|
||||
}
|
||||
|
||||
public static function dateIsNotParseable(string $value): self
|
||||
{
|
||||
return new self('Value is not in the allowed date format: ' . $value);
|
||||
}
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Lcobucci\JWT\Token;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use Lcobucci\JWT\Decoder;
|
||||
use Lcobucci\JWT\Parser as ParserInterface;
|
||||
use Lcobucci\JWT\Token as TokenInterface;
|
||||
|
||||
use function array_key_exists;
|
||||
use function count;
|
||||
use function explode;
|
||||
use function is_array;
|
||||
use function is_numeric;
|
||||
use function number_format;
|
||||
|
||||
final class Parser implements ParserInterface
|
||||
{
|
||||
private const MICROSECOND_PRECISION = 6;
|
||||
|
||||
public function __construct(private readonly Decoder $decoder)
|
||||
{
|
||||
}
|
||||
|
||||
public function parse(string $jwt): TokenInterface
|
||||
{
|
||||
[$encodedHeaders, $encodedClaims, $encodedSignature] = $this->splitJwt($jwt);
|
||||
|
||||
if ($encodedHeaders === '') {
|
||||
throw InvalidTokenStructure::missingHeaderPart();
|
||||
}
|
||||
|
||||
if ($encodedClaims === '') {
|
||||
throw InvalidTokenStructure::missingClaimsPart();
|
||||
}
|
||||
|
||||
if ($encodedSignature === '') {
|
||||
throw InvalidTokenStructure::missingSignaturePart();
|
||||
}
|
||||
|
||||
$header = $this->parseHeader($encodedHeaders);
|
||||
|
||||
return new Plain(
|
||||
new DataSet($header, $encodedHeaders),
|
||||
new DataSet($this->parseClaims($encodedClaims), $encodedClaims),
|
||||
$this->parseSignature($encodedSignature),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits the JWT string into an array
|
||||
*
|
||||
* @param non-empty-string $jwt
|
||||
*
|
||||
* @return string[]
|
||||
*
|
||||
* @throws InvalidTokenStructure When JWT doesn't have all parts.
|
||||
*/
|
||||
private function splitJwt(string $jwt): array
|
||||
{
|
||||
$data = explode('.', $jwt);
|
||||
|
||||
if (count($data) !== 3) {
|
||||
throw InvalidTokenStructure::missingOrNotEnoughSeparators();
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the header from a string
|
||||
*
|
||||
* @param non-empty-string $data
|
||||
*
|
||||
* @return array<non-empty-string, mixed>
|
||||
*
|
||||
* @throws UnsupportedHeaderFound When an invalid header is informed.
|
||||
* @throws InvalidTokenStructure When parsed content isn't an array.
|
||||
*/
|
||||
private function parseHeader(string $data): array
|
||||
{
|
||||
$header = $this->decoder->jsonDecode($this->decoder->base64UrlDecode($data));
|
||||
|
||||
if (! is_array($header)) {
|
||||
throw InvalidTokenStructure::arrayExpected('headers');
|
||||
}
|
||||
|
||||
$this->guardAgainstEmptyStringKeys($header, 'headers');
|
||||
|
||||
if (array_key_exists('enc', $header)) {
|
||||
throw UnsupportedHeaderFound::encryption();
|
||||
}
|
||||
|
||||
if (! array_key_exists('typ', $header)) {
|
||||
$header['typ'] = 'JWT';
|
||||
}
|
||||
|
||||
return $header;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the claim set from a string
|
||||
*
|
||||
* @param non-empty-string $data
|
||||
*
|
||||
* @return array<non-empty-string, mixed>
|
||||
*
|
||||
* @throws InvalidTokenStructure When parsed content isn't an array or contains non-parseable dates.
|
||||
*/
|
||||
private function parseClaims(string $data): array
|
||||
{
|
||||
$claims = $this->decoder->jsonDecode($this->decoder->base64UrlDecode($data));
|
||||
|
||||
if (! is_array($claims)) {
|
||||
throw InvalidTokenStructure::arrayExpected('claims');
|
||||
}
|
||||
|
||||
$this->guardAgainstEmptyStringKeys($claims, 'claims');
|
||||
|
||||
if (array_key_exists(RegisteredClaims::AUDIENCE, $claims)) {
|
||||
$claims[RegisteredClaims::AUDIENCE] = (array) $claims[RegisteredClaims::AUDIENCE];
|
||||
}
|
||||
|
||||
foreach (RegisteredClaims::DATE_CLAIMS as $claim) {
|
||||
if (! array_key_exists($claim, $claims)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$claims[$claim] = $this->convertDate($claims[$claim]);
|
||||
}
|
||||
|
||||
return $claims;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $array
|
||||
* @param non-empty-string $part
|
||||
*
|
||||
* @phpstan-assert array<non-empty-string, mixed> $array
|
||||
*/
|
||||
private function guardAgainstEmptyStringKeys(array $array, string $part): void
|
||||
{
|
||||
foreach ($array as $key => $value) {
|
||||
if ($key === '') {
|
||||
throw InvalidTokenStructure::arrayExpected($part);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @throws InvalidTokenStructure */
|
||||
private function convertDate(int|float|string $timestamp): DateTimeImmutable
|
||||
{
|
||||
if (! is_numeric($timestamp)) {
|
||||
throw InvalidTokenStructure::dateIsNotParseable($timestamp);
|
||||
}
|
||||
|
||||
$normalizedTimestamp = number_format((float) $timestamp, self::MICROSECOND_PRECISION, '.', '');
|
||||
|
||||
$date = DateTimeImmutable::createFromFormat('U.u', $normalizedTimestamp);
|
||||
|
||||
if ($date === false) {
|
||||
throw InvalidTokenStructure::dateIsNotParseable($normalizedTimestamp);
|
||||
}
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the signature from given data
|
||||
*
|
||||
* @param non-empty-string $data
|
||||
*/
|
||||
private function parseSignature(string $data): Signature
|
||||
{
|
||||
$hash = $this->decoder->base64UrlDecode($data);
|
||||
|
||||
return new Signature($hash, $data);
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Lcobucci\JWT\Token;
|
||||
|
||||
use DateTimeInterface;
|
||||
use Lcobucci\JWT\UnencryptedToken;
|
||||
|
||||
use function in_array;
|
||||
|
||||
final class Plain implements UnencryptedToken
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DataSet $headers,
|
||||
private readonly DataSet $claims,
|
||||
private readonly Signature $signature,
|
||||
) {
|
||||
}
|
||||
|
||||
public function headers(): DataSet
|
||||
{
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
public function claims(): DataSet
|
||||
{
|
||||
return $this->claims;
|
||||
}
|
||||
|
||||
public function signature(): Signature
|
||||
{
|
||||
return $this->signature;
|
||||
}
|
||||
|
||||
public function payload(): string
|
||||
{
|
||||
return $this->headers->toString() . '.' . $this->claims->toString();
|
||||
}
|
||||
|
||||
public function isPermittedFor(string $audience): bool
|
||||
{
|
||||
return in_array($audience, $this->claims->get(RegisteredClaims::AUDIENCE, []), true);
|
||||
}
|
||||
|
||||
public function isIdentifiedBy(string $id): bool
|
||||
{
|
||||
return $this->claims->get(RegisteredClaims::ID) === $id;
|
||||
}
|
||||
|
||||
public function isRelatedTo(string $subject): bool
|
||||
{
|
||||
return $this->claims->get(RegisteredClaims::SUBJECT) === $subject;
|
||||
}
|
||||
|
||||
public function hasBeenIssuedBy(string ...$issuers): bool
|
||||
{
|
||||
return in_array($this->claims->get(RegisteredClaims::ISSUER), $issuers, true);
|
||||
}
|
||||
|
||||
public function hasBeenIssuedBefore(DateTimeInterface $now): bool
|
||||
{
|
||||
return $now >= $this->claims->get(RegisteredClaims::ISSUED_AT);
|
||||
}
|
||||
|
||||
public function isMinimumTimeBefore(DateTimeInterface $now): bool
|
||||
{
|
||||
return $now >= $this->claims->get(RegisteredClaims::NOT_BEFORE);
|
||||
}
|
||||
|
||||
public function isExpired(DateTimeInterface $now): bool
|
||||
{
|
||||
if (! $this->claims->has(RegisteredClaims::EXPIRATION_TIME)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $now >= $this->claims->get(RegisteredClaims::EXPIRATION_TIME);
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
return $this->headers->toString() . '.'
|
||||
. $this->claims->toString() . '.'
|
||||
. $this->signature->toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Lcobucci\JWT\Token;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Lcobucci\JWT\Exception;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class RegisteredClaimGiven extends InvalidArgumentException implements Exception
|
||||
{
|
||||
private const DEFAULT_MESSAGE = 'Builder#withClaim() is meant to be used for non-registered claims, '
|
||||
. 'check the documentation on how to set claim "%s"';
|
||||
|
||||
/** @param non-empty-string $name */
|
||||
public static function forClaim(string $name): self
|
||||
{
|
||||
return new self(sprintf(self::DEFAULT_MESSAGE, $name));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Lcobucci\JWT\Token;
|
||||
|
||||
/**
|
||||
* Defines the list of claims that are registered in the IANA "JSON Web Token Claims" registry
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc7519#section-4.1
|
||||
*/
|
||||
interface RegisteredClaims
|
||||
{
|
||||
public const ALL = [
|
||||
self::AUDIENCE,
|
||||
self::EXPIRATION_TIME,
|
||||
self::ID,
|
||||
self::ISSUED_AT,
|
||||
self::ISSUER,
|
||||
self::NOT_BEFORE,
|
||||
self::SUBJECT,
|
||||
];
|
||||
|
||||
public const DATE_CLAIMS = [
|
||||
self::ISSUED_AT,
|
||||
self::NOT_BEFORE,
|
||||
self::EXPIRATION_TIME,
|
||||
];
|
||||
|
||||
/**
|
||||
* Identifies the recipients that the JWT is intended for
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc7519#section-4.1.3
|
||||
*/
|
||||
public const AUDIENCE = 'aud';
|
||||
|
||||
/**
|
||||
* Identifies the expiration time on or after which the JWT MUST NOT be accepted for processing
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc7519#section-4.1.4
|
||||
*/
|
||||
public const EXPIRATION_TIME = 'exp';
|
||||
|
||||
/**
|
||||
* Provides a unique identifier for the JWT
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc7519#section-4.1.7
|
||||
*/
|
||||
public const ID = 'jti';
|
||||
|
||||
/**
|
||||
* Identifies the time at which the JWT was issued
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc7519#section-4.1.6
|
||||
*/
|
||||
public const ISSUED_AT = 'iat';
|
||||
|
||||
/**
|
||||
* Identifies the principal that issued the JWT
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc7519#section-4.1.1
|
||||
*/
|
||||
public const ISSUER = 'iss';
|
||||
|
||||
/**
|
||||
* Identifies the time before which the JWT MUST NOT be accepted for processing
|
||||
*
|
||||
* https://tools.ietf.org/html/rfc7519#section-4.1.5
|
||||
*/
|
||||
public const NOT_BEFORE = 'nbf';
|
||||
|
||||
/**
|
||||
* Identifies the principal that is the subject of the JWT.
|
||||
*
|
||||
* https://tools.ietf.org/html/rfc7519#section-4.1.2
|
||||
*/
|
||||
public const SUBJECT = 'sub';
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Lcobucci\JWT\Token;
|
||||
|
||||
final class Signature
|
||||
{
|
||||
/**
|
||||
* @param non-empty-string $hash
|
||||
* @param non-empty-string $encoded
|
||||
*/
|
||||
public function __construct(private readonly string $hash, private readonly string $encoded)
|
||||
{
|
||||
}
|
||||
|
||||
/** @return non-empty-string */
|
||||
public function hash(): string
|
||||
{
|
||||
return $this->hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the encoded version of the signature
|
||||
*
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
return $this->encoded;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Lcobucci\JWT\Token;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Lcobucci\JWT\Exception;
|
||||
|
||||
final class UnsupportedHeaderFound extends InvalidArgumentException implements Exception
|
||||
{
|
||||
public static function encryption(): self
|
||||
{
|
||||
return new self('Encryption is not supported yet');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user