Skip to content

Commit

Permalink
Add support for the Firebase Auth Emulator when using lcobucci/jwt 5.*
Browse files Browse the repository at this point in the history
  • Loading branch information
jeromegamez committed Mar 21, 2023
1 parent e2be672 commit e03289a
Show file tree
Hide file tree
Showing 16 changed files with 453 additions and 60 deletions.
9 changes: 6 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,11 @@ jobs:
echo "::add-matcher::${{ runner.tool_cache }}/php.json"
echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: Run PHPStan
- name: Lint code
run: vendor/bin/phpstan analyse --no-progress

- name: Run PHPUnit
run: vendor/bin/phpunit --testdox
- name: Run Tests
run: vendor/bin/phpunit --exclude-group=emulator --testdox

- name: Run Emulator Tests
run: vendor/bin/phpunit --group=emulator --testdox
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Unreleased

* Added support for the Firebase Auth Emulator when using `lcobucci/jwt` 5.*

Note: The `Kreait\Firebase\JWT\Token` class has been renamed to `\Kreait\Firebase\JWT\SecureToken`. This is technically
a breaking change, but since the `*Verifier` classes type-hint `\Kreait\Firebase\JWT\Contract\Token` as return values,
I consider it unlikely that this should cause trouble for most people. If it does, I'll deal with the consequences.

## 4.1.0 - 2023-02-28

* Added support for `lcobucci/jwt` 5.*
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ try {

### Tokens

Tokens returned from the Generator and Verifier are instances of `Kreait\Firebase\JWT\Token` and
Tokens returned from the Generator and Verifier are instances of `\Kreait\Firebase\JWT\Contract\Token` and
represent a [JWT](https://jwt.io/). The displayed outputs are examples and vary depending on
the information associated with the given user in your project's auth database.

Expand Down
7 changes: 2 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,8 @@
],
"test": [
"vendor/bin/phpstan",
"vendor/bin/phpunit"
],
"test-coverage": [
"Composer\\Config::disableProcessTimeout",
"XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html=.build/coverage"
"vendor/bin/phpunit --exclude-group=emulator --testdox",
"FIREBASE_AUTH_EMULATOR_HOST=localhost:9099 vendor/bin/phpunit --group=emulator --testdox"
]
}
}
2 changes: 1 addition & 1 deletion phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
beStrictAboutOutputDuringTests="true"
cacheResultFile="tools/.phpunit.result.cache"
cacheResultFile=".build/.phpunit.result.cache"
convertDeprecationsToExceptions="true"
colors="true"
>
Expand Down
7 changes: 6 additions & 1 deletion src/JWT/Action/VerifyIdToken/WithLcobucciJWT.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@
use Kreait\Firebase\JWT\Contract\Keys;
use Kreait\Firebase\JWT\Contract\Token;
use Kreait\Firebase\JWT\Error\IdTokenVerificationFailed;
use Kreait\Firebase\JWT\InsecureToken;
use Kreait\Firebase\JWT\SecureToken;
use Kreait\Firebase\JWT\Signer\None;
use Kreait\Firebase\JWT\Token\Parser;
use Kreait\Firebase\JWT\Util;
use Lcobucci\Clock\FrozenClock;
use Lcobucci\JWT\Encoding\JoseEncoder;
use Lcobucci\JWT\Signer;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Signer\Rsa\Sha256;
use Lcobucci\JWT\Token\Parser;
use Lcobucci\JWT\UnencryptedToken;
use Lcobucci\JWT\Validation\Constraint\IssuedBy;
use Lcobucci\JWT\Validation\Constraint\LooseValidAt;
Expand Down Expand Up @@ -127,6 +128,10 @@ public function handle(VerifyIdToken $action): Token
}
unset($header);

if (Util::authEmulatorHost() !== '') {
return InsecureToken::withValues($tokenString, $headers, $claims);
}

return SecureToken::withValues($tokenString, $headers, $claims);
}

Expand Down
7 changes: 6 additions & 1 deletion src/JWT/Action/VerifySessionCookie/WithLcobucciJWT.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@
use Kreait\Firebase\JWT\Contract\Keys;
use Kreait\Firebase\JWT\Contract\Token;
use Kreait\Firebase\JWT\Error\SessionCookieVerificationFailed;
use Kreait\Firebase\JWT\InsecureToken;
use Kreait\Firebase\JWT\SecureToken;
use Kreait\Firebase\JWT\Signer\None;
use Kreait\Firebase\JWT\Token\Parser;
use Kreait\Firebase\JWT\Util;
use Lcobucci\Clock\FrozenClock;
use Lcobucci\JWT\Encoding\JoseEncoder;
use Lcobucci\JWT\Signer;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Signer\Rsa\Sha256;
use Lcobucci\JWT\Token\Parser;
use Lcobucci\JWT\UnencryptedToken;
use Lcobucci\JWT\Validation\Constraint\IssuedBy;
use Lcobucci\JWT\Validation\Constraint\LooseValidAt;
Expand Down Expand Up @@ -126,6 +127,10 @@ public function handle(VerifySessionCookie $action): Token
}
unset($header);

if (Util::authEmulatorHost() !== '') {
return InsecureToken::withValues($cookieString, $headers, $claims);
}

return SecureToken::withValues($cookieString, $headers, $claims);
}

Expand Down
53 changes: 53 additions & 0 deletions src/JWT/InsecureToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace Kreait\Firebase\JWT;

use Stringable;

final class InsecureToken implements Contract\Token, Stringable
{
/**
* @param array<string, mixed> $headers
* @param array<string, mixed> $payload
*/
private function __construct(private readonly string $encodedString, private readonly array $headers, private readonly array $payload)
{
}

public function __toString(): string
{
return $this->toString();
}

/**
* @param array<string, mixed> $headers
* @param array<string, mixed> $payload
*/
public static function withValues(string $encodedString, array $headers, array $payload): self
{
return new self($encodedString, $headers, $payload);
}

/**
* @return array<string, mixed>
*/
public function headers(): array
{
return $this->headers;
}

/**
* @return array<string, mixed>
*/
public function payload(): array
{
return $this->payload;
}

public function toString(): string
{
return $this->encodedString;
}
}
170 changes: 170 additions & 0 deletions src/JWT/Token/InsecureParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
<?php

declare(strict_types=1);

namespace Kreait\Firebase\JWT\Token;

use DateTimeImmutable;
use Lcobucci\JWT\Decoder;
use Lcobucci\JWT\Parser as ParserInterface;
use Lcobucci\JWT\Token;
use Lcobucci\JWT\Token\DataSet;
use Lcobucci\JWT\Token\InvalidTokenStructure;
use Lcobucci\JWT\Token\Plain;
use Lcobucci\JWT\Token\RegisteredClaims;
use Lcobucci\JWT\Token\Signature;
use Lcobucci\JWT\Token\UnsupportedHeaderFound;

use function array_key_exists;
use function is_array;

/**
* This is an almost 1:1 copy of the parser in `lcobucci/jwt`, with only the signature verification
* removed; hence the name `InsecureParser`.
*
* @internal
*/
final class InsecureParser implements ParserInterface
{
private const MICROSECOND_PRECISION = 6;

public function __construct(private readonly Decoder $decoder)
{
}

/**
* @param non-empty-string $jwt
*/
public function parse(string $jwt): Token
{
[$encodedHeaders, $encodedClaims] = $this->splitJwt($jwt);

if ($encodedHeaders === '') {
throw new InvalidTokenStructure('The JWT string is missing the Header part');
}

if ($encodedClaims === '') {
throw new InvalidTokenStructure('The JWT string is missing the Claim part');
}

$header = $this->parseHeader($encodedHeaders);

return new Plain(
new DataSet($header, $encodedHeaders),
new DataSet($this->parseClaims($encodedClaims), $encodedClaims),
new Signature('none', 'none'),
);
}

/**
* Splits the JWT string into an array.
*
* @param non-empty-string $jwt
*
* @throws InvalidTokenStructure when JWT doesn't have all parts
*
* @return string[]
*/
private function splitJwt(string $jwt): array
{
return explode('.', $jwt);
}

/**
* Parses the header from a string.
*
* @param non-empty-string $data
*
* @throws InvalidTokenStructure when parsed content isn't an array
* @throws UnsupportedHeaderFound when an invalid header is informed
*
* @return array<non-empty-string, mixed>
*/
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
*
* @throws InvalidTokenStructure when parsed content isn't an array or contains non-parseable dates
*
* @return array<non-empty-string, mixed>
*/
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;
}
}
30 changes: 30 additions & 0 deletions src/JWT/Token/Parser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Kreait\Firebase\JWT\Token;

use Kreait\Firebase\JWT\Util;
use Lcobucci\JWT\Decoder;
use Lcobucci\JWT\Parser as ParserInterface;
use Lcobucci\JWT\Token;
use Lcobucci\JWT\Token\Parser as SecureParser;

final class Parser implements ParserInterface
{
private ParserInterface $parser;

public function __construct(Decoder $decoder)
{
if (Util::authEmulatorHost() !== '') {
$this->parser = new InsecureParser($decoder);
} else {
$this->parser = new SecureParser($decoder);
}
}

public function parse(string $jwt): Token
{
return $this->parser->parse($jwt);
}
}
Loading

0 comments on commit e03289a

Please sign in to comment.