Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"nikic/fast-route": "^1.3",
"nikic/php-parser": "^5.0",
"psr/cache": "^3.0",
"psr/clock": "^1.0",
"psr/container": "^2.0",
"psr/event-dispatcher": "^1.0",
"psr/http-factory": "^1.1",
Expand Down
65 changes: 0 additions & 65 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -140,66 +140,6 @@ parameters:
count: 1
path: src/Altair/Http/Collection/HttpStatusCollection.php

-
message: "#^Class Lcobucci\\\\Jose\\\\Parsing\\\\Decoder not found\\.$#"
count: 1
path: src/Altair/Http/Configuration/LcobucciTokenConfiguration.php

-
message: "#^Class Lcobucci\\\\Jose\\\\Parsing\\\\Encoder not found\\.$#"
count: 1
path: src/Altair/Http/Configuration/LcobucciTokenConfiguration.php

-
message: "#^Class Lcobucci\\\\Jose\\\\Parsing\\\\Parser not found\\.$#"
count: 2
path: src/Altair/Http/Configuration/LcobucciTokenConfiguration.php

-
message: "#^Cannot cast Lcobucci\\\\JWT\\\\UnencryptedToken to string\\.$#"
count: 1
path: src/Altair/Http/Jwt/LcobucciTokenGenerator.php

-
message: "#^Cannot instantiate interface Lcobucci\\\\JWT\\\\Signer\\\\Key\\.$#"
count: 1
path: src/Altair/Http/Jwt/LcobucciTokenGenerator.php

-
message: "#^Call to method getMessage\\(\\) on an unknown class Lcobucci\\\\JWT\\\\Validation\\\\InvalidTokenException\\.$#"
count: 1
path: src/Altair/Http/Jwt/LcobucciTokenParser.php

-
message: "#^Cannot instantiate interface Lcobucci\\\\JWT\\\\Signer\\\\Key\\.$#"
count: 1
path: src/Altair/Http/Jwt/LcobucciTokenParser.php

-
message: "#^Caught class Lcobucci\\\\JWT\\\\Validation\\\\InvalidTokenException not found\\.$#"
count: 1
path: src/Altair/Http/Jwt/LcobucciTokenParser.php

-
message: "#^Instantiated class Lcobucci\\\\Clock\\\\SystemClock not found\\.$#"
count: 1
path: src/Altair/Http/Jwt/LcobucciTokenParser.php

-
message: "#^Instantiated class Lcobucci\\\\JWT\\\\Validation\\\\Constraint\\\\ValidAt not found\\.$#"
count: 1
path: src/Altair/Http/Jwt/LcobucciTokenParser.php

-
message: "#^Parameter \\#1 \\.\\.\\.\\$issuers of class Lcobucci\\\\JWT\\\\Validation\\\\Constraint\\\\IssuedBy constructor expects non\\-empty\\-string, Psr\\\\Http\\\\Message\\\\UriInterface given\\.$#"
count: 1
path: src/Altair/Http/Jwt/LcobucciTokenParser.php

-
message: "#^Parameter \\#3 \\.\\.\\.\\$constraints of method Lcobucci\\\\JWT\\\\Validator\\:\\:assert\\(\\) expects Lcobucci\\\\JWT\\\\Validation\\\\Constraint, Lcobucci\\\\JWT\\\\Validation\\\\Constraint\\\\ValidAt given\\.$#"
count: 1
path: src/Altair/Http/Jwt/LcobucciTokenParser.php

-
message: "#^Offset 'domain' on array\\{lifetime\\: int\\<0, max\\>, path\\: non\\-falsy\\-string, domain\\: string, secure\\: bool, httponly\\: bool, samesite\\: string\\} on left side of \\?\\? always exists and is not nullable\\.$#"
count: 1
Expand Down Expand Up @@ -230,11 +170,6 @@ parameters:
count: 1
path: src/Altair/Http/Support/DefaultErrorHandler.php

-
message: "#^Call to an undefined method Negotiation\\\\AcceptHeader\\:\\:getValue\\(\\)\\.$#"
count: 1
path: src/Altair/Http/Support/FormatNegotiator.php

-
message: "#^Unable to resolve the template type TEntity in call to method Cycle\\\\ORM\\\\ORMInterface\\:\\:getRepository\\(\\)$#"
count: 1
Expand Down
62 changes: 44 additions & 18 deletions src/Altair/Http/Configuration/LcobucciTokenConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,40 +15,66 @@
use Altair\Configuration\Traits\EnvAwareTrait;
use Altair\Container\Container;
use Altair\Http\Contracts\TokenConfigurationInterface;
use Altair\Http\Exception\RuntimeException;
use Altair\Http\Support\TokenConfiguration;
use Lcobucci\Jose\Parsing\Decoder;
use Lcobucci\Jose\Parsing\Encoder;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Parser;
use Lcobucci\JWT\Signer\Rsa\Sha256;
use Lcobucci\JWT\Validator;
use Override;

/**
* Wires the framework {@see TokenConfigurationInterface} from environment variables.
*
* The lcobucci/jwt v5 builder, parser and validator are derived on demand from this
* configuration inside {@see \Altair\Http\Jwt\LcobucciTokenGenerator} and
* {@see \Altair\Http\Jwt\LcobucciTokenParser}, so no library primitives are bound here.
*/
class LcobucciTokenConfiguration implements ConfigurationInterface
{
use EnvAwareTrait;

/**
* Legacy placeholder default; rejected at runtime so a misconfigured deployment fails fast
* instead of silently issuing tokens that can never be verified.
*/
private const string UNCONFIGURED_KEY = 'YOU_SHOULD_CHANGE_THIS';

/**
* @inheritDoc
*/
#[Override]
public function apply(Container $container): void
{
$tokenGeneratorConfigurationFactory = fn(): TokenConfiguration => new TokenConfiguration(
$this->env->get('TOKEN_PUBLIC_KEY', 'YOU_SHOULD_CHANGE_THIS'),
(int) $this->env->get('TOKEN_TTL', \ini_get('session.gc_maxlifetime')),
new Sha256(),
null,
$this->env->get('TOKEN_PRIVATE_KEY')
);
$tokenConfigurationFactory = function (): TokenConfiguration {
$publicKey = (string) $this->env->get('TOKEN_PUBLIC_KEY', '');

if ($publicKey === '' || $publicKey === self::UNCONFIGURED_KEY) {
throw new RuntimeException(
'TOKEN_PUBLIC_KEY must be set to a valid token verification key.'
);
}

$issuer = (string) $this->env->get('TOKEN_ISSUER', '');

if ($issuer === '') {
throw new RuntimeException(
'TOKEN_ISSUER must be set to a stable issuer identifier for the tokens this service mints.'
);
}

$audience = $this->env->get('TOKEN_AUDIENCE');

return new TokenConfiguration(
$publicKey,
(int) $this->env->get('TOKEN_TTL', \ini_get('session.gc_maxlifetime')),
new Sha256(),
$issuer,
null,
$this->env->get('TOKEN_PRIVATE_KEY'),
$audience === null ? null : (string) $audience
);
};

$container
->alias(TokenConfigurationInterface::class, TokenConfiguration::class)
->alias(Validator::class, \Lcobucci\JWT\Validation\Validator::class)
->alias(Encoder::class, \Lcobucci\Jose\Parsing\Parser::class)
->alias(Decoder::class, \Lcobucci\Jose\Parsing\Parser::class)
->alias(Builder::class, \Lcobucci\JWT\Token\Builder::class)
->alias(Parser::class, \Lcobucci\JWT\Token\Parser::class)
->delegate(TokenConfiguration::class, $tokenGeneratorConfigurationFactory);
->delegate(TokenConfiguration::class, $tokenConfigurationFactory);
}
}
10 changes: 10 additions & 0 deletions src/Altair/Http/Contracts/TokenConfigurationInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ public function getTtl(): int;

public function getSigner(): Signer;

/**
* The stable issuer identifier (`iss` claim) for tokens minted and validated by this service.
*/
public function getIssuer(): string;

/**
* The intended audience (`aud` claim), or null when no audience restriction applies.
*/
public function getAudience(): ?string;

/**
* Returns the timestamp for token issuance and expiration in UNIX_TIMESTAMP format.
*/
Expand Down
64 changes: 43 additions & 21 deletions src/Altair/Http/Jwt/LcobucciTokenGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,42 +13,64 @@

use Altair\Http\Contracts\TokenConfigurationInterface;
use Altair\Http\Contracts\TokenGeneratorInterface;
use Altair\Http\Exception\InvalidTokenException;
use DateTimeImmutable;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Key\InMemory;
use Override;
use Psr\Http\Message\ServerRequestInterface;

/**
* Generates signed JWTs using lcobucci/jwt v5.
*
* Each call mints a fresh, immutable builder from a {@see Configuration} derived from the
* framework {@see TokenConfigurationInterface}. Asymmetric signing is assumed (RSA/ECDSA),
* so a private key must be configured. The `iss` claim is the configured stable issuer and
* the `aud` claim is added when an audience is configured.
*/
class LcobucciTokenGenerator implements TokenGeneratorInterface
{
/**
* LcobucciTokenGenerator constructor.
*/
public function __construct(protected ServerRequestInterface $request, protected Builder $builder, protected TokenConfigurationInterface $config) {}
public function __construct(protected TokenConfigurationInterface $config) {}

/**
* @inheritDoc
*
* @param array<string, mixed> $claims
*
* @throws InvalidTokenException when no private key is configured for signing
*/
#[Override]
public function generate(array $claims = []): string
{
$issuer = (string) $this->request->getUri();
$issued_at = (new DateTimeImmutable())->setTimestamp($this->config->getTimestamp());
$expiration = (new DateTimeImmutable())->setTimestamp($this->config->getExpirationTimestamp());
// Assumed RSA or ECDSA signatures (highly recommended)
// Signatures are based on public and private keys so you have to generate using the private key and verify
// using the public key
$key = new Key($this->config->getPrivateKey());
$privateKey = $this->config->getPrivateKey();

if ($privateKey === null || $privateKey === '') {
throw new InvalidTokenException('A private key is required to generate a token.');
}

$configuration = Configuration::forAsymmetricSigner(
$this->config->getSigner(),
InMemory::plainText($privateKey),
InMemory::plainText($this->config->getPublicKey()),
);

$builder = $configuration->builder()
->issuedBy($this->config->getIssuer())
->issuedAt((new DateTimeImmutable())->setTimestamp($this->config->getTimestamp()))
->expiresAt((new DateTimeImmutable())->setTimestamp($this->config->getExpirationTimestamp()));

$audience = $this->config->getAudience();

if ($audience !== null && $audience !== '') {
$builder = $builder->permittedFor($audience);
}

foreach ($claims as $name => $value) {
$this->builder->withClaim($name, $value);
// Builder is immutable in v5: each withClaim() returns a new instance.
$builder = $builder->withClaim($name, $value);
}

return (string) $this
->builder
->issuedBy($issuer)
->issuedAt($issued_at)
->expiresAt($expiration)
->getToken($this->config->getSigner(), $key);
return $builder
->getToken($configuration->signer(), $configuration->signingKey())
->toString();
}
}
Loading
Loading