Skip to content

Commit 7e160b8

Browse files
authored
Merge pull request #18 from portier/feat/php84
PHP 8.4 support
2 parents 13f5625 + ae69778 commit 7e160b8

File tree

22 files changed

+393
-74
lines changed

22 files changed

+393
-74
lines changed

.github/workflows/check.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ jobs:
1414
strategy:
1515
fail-fast: false
1616
matrix:
17-
php-versions: ['7.4', '8.0', '8.1', '8.2', '8.3']
17+
php-versions: ['8.0', '8.1', '8.2', '8.3', '8.4']
1818
steps:
1919

2020
- name: Checkout
21-
uses: actions/checkout@v3
21+
uses: actions/checkout@v4
2222

2323
- name: Setup PHP
2424
uses: shivammathur/setup-php@v2
@@ -33,7 +33,7 @@ jobs:
3333
echo "::set-output name=dir::$(composer config cache-files-dir)"
3434
3535
- name: Composer cache
36-
uses: actions/cache@v3
36+
uses: actions/cache@v4
3737
with:
3838
path: ${{ steps.composer-cache.outputs.dir }}
3939
key: ${{ runner.os }}-composer
@@ -56,12 +56,12 @@ jobs:
5656
run: composer run phpunit -- --teamcity
5757

5858
- name: Set up Go
59-
uses: actions/setup-go@v3
59+
uses: actions/setup-go@v5
6060
with:
6161
go-version: ^1.16
6262

6363
- name: Go cache
64-
uses: actions/cache@v3
64+
uses: actions/cache@v4
6565
with:
6666
path: ~/go/pkg/mod
6767
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Copyright (C) 2021 Angry Bytes
1+
Copyright (C) 2025 Angry Bytes
22

33
Permission is hereby granted, free of charge, to any person obtaining a copy of
44
this software and associated documentation files (the "Software"), to deal in

composer.json

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
"authors": [
77
{
88
"name": "Stéphan Kochen",
9-
"email": "stephan@kochen.nl"
9+
"email": "mail@stephank.nl"
1010
}
1111
],
1212
"scripts": {
1313
"php-cs-fixer": "php-cs-fixer fix",
14-
"phpstan": "phpstan analyse -l max -c phpstan.neon src/",
14+
"phpstan": "phpstan analyse -l max src/",
1515
"phpunit": "phpunit"
1616
},
1717
"autoload": {
@@ -20,14 +20,13 @@
2020
}
2121
},
2222
"require": {
23-
"fgrosse/phpasn1": "^2.3.1",
2423
"lcobucci/clock": "^2.0.0 || ^3.0.0",
2524
"lcobucci/jwt": "^4.1.0 || ^5.0.0",
2625
"guzzlehttp/guzzle": "^7.2.0"
2726
},
2827
"require-dev": {
2928
"phpunit/phpunit": "^9.5 || ^10.4.2",
30-
"phpstan/phpstan": "^1.2.0",
29+
"phpstan/phpstan": "^2.1.5",
3130
"friendsofphp/php-cs-fixer": "^3.14"
3231
}
3332
}

phpstan.neon

Lines changed: 0 additions & 12 deletions
This file was deleted.

src/AbstractStore.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@ abstract class AbstractStore implements StoreInterface
1919
/**
2020
* Lifespan of a nonce.
2121
*
22-
* @var float
22+
* @var int
2323
*/
2424
public $nonceTtl = 15 * 60;
2525

2626
/**
2727
* Minimum time to cache a HTTP response.
2828
*
29-
* @var float
29+
* @var int
3030
*/
3131
public $cacheMinTtl = 60 * 60;
3232

@@ -57,9 +57,9 @@ public function generateNonce(string $email): string
5757
*
5858
* @param string $url the URL to fetch
5959
*
60-
* @return \stdClass an object with `ttl` and `data` properties
60+
* @return object{data: \stdClass, ttl: int}
6161
*/
62-
public function fetch(string $url): \stdClass
62+
public function fetch(string $url): object
6363
{
6464
$res = $this->guzzle->get($url);
6565

src/Client.php

Lines changed: 12 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public static function normalize(string $email): string
6868
assert(defined('MB_CASE_FOLD') && function_exists('idn_to_ascii'));
6969

7070
$localEnd = strrpos($email, '@');
71-
if (false === $localEnd) {
71+
if (false === $localEnd || $localEnd + 1 === strlen($email)) {
7272
return '';
7373
}
7474

@@ -101,7 +101,7 @@ public static function normalize(string $email): string
101101
*
102102
* @return string URL to redirect the browser to
103103
*/
104-
public function authenticate(string $email, string $state = null): string
104+
public function authenticate(string $email, ?string $state = null): string
105105
{
106106
$authEndpoint = $this->fetchDiscovery()->authorization_endpoint ?? null;
107107
if (!is_string($authEndpoint)) {
@@ -161,19 +161,23 @@ public function verify(string $token): string
161161
}
162162

163163
// Find the matching public key, and verify the signature.
164-
$publicKey = null;
164+
$publicKey = '';
165165
foreach ($keysDoc->keys as $key) {
166166
if ($key instanceof \stdClass
167-
&& isset($key->alg) && 'RS256' === $key->alg
168-
&& isset($key->kid) && $key->kid === $kid
169-
&& isset($key->n) && isset($key->e)) {
170-
$publicKey = self::parseJwk($key);
167+
&& isset($key->alg) && 'RS256' === $key->alg
168+
&& isset($key->kid) && $key->kid === $kid
169+
) {
170+
try {
171+
$publicKey = JWK::toPem($key);
172+
} catch (\Exception) {
173+
}
171174
break;
172175
}
173176
}
174-
if (null === $publicKey) {
177+
if ('' === $publicKey) {
175178
throw new \Exception('Cannot find the public key used to sign the token');
176179
}
180+
$publicKey = JwtSigner\Key\InMemory::plainText($publicKey);
177181

178182
// Validate the token claims.
179183
$clock = \Lcobucci\Clock\SystemClock::fromUTC();
@@ -228,28 +232,6 @@ private function fetchDiscovery(): \stdClass
228232
return $this->store->fetchCached('discovery', $discoveryUrl);
229233
}
230234

231-
/**
232-
* Parse a JWK into a PEM public key.
233-
*/
234-
private static function parseJwk(\stdClass $jwk): JwtSigner\Key
235-
{
236-
$n = gmp_init(bin2hex(self::decodeBase64Url($jwk->n)), 16);
237-
$e = gmp_init(bin2hex(self::decodeBase64Url($jwk->e)), 16);
238-
239-
$seq = new \FG\ASN1\Universal\Sequence();
240-
$seq->addChild(new \FG\ASN1\Universal\Integer(gmp_strval($n)));
241-
$seq->addChild(new \FG\ASN1\Universal\Integer(gmp_strval($e)));
242-
$pkey = new \FG\X509\PublicKey(bin2hex($seq->getBinary()));
243-
244-
$encoded = base64_encode($pkey->getBinary());
245-
246-
return JwtSigner\Key\InMemory::plainText(
247-
"-----BEGIN PUBLIC KEY-----\n".
248-
chunk_split($encoded, 64, "\n").
249-
"-----END PUBLIC KEY-----\n"
250-
);
251-
}
252-
253235
/**
254236
* Get the origin for a URL.
255237
*/
@@ -281,14 +263,4 @@ private static function getOrigin(string $url): string
281263

282264
return $res;
283265
}
284-
285-
private static function decodeBase64Url(string $input): string
286-
{
287-
$output = base64_decode(strtr($input, '-_', '+/'), true);
288-
if (false === $output) {
289-
throw new \Exception('Invalid base64');
290-
}
291-
292-
return $output;
293-
}
294266
}

src/DER.php

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
namespace Portier\Client;
4+
5+
/**
6+
* Limited DER encoding functions.
7+
*/
8+
final class DER
9+
{
10+
private const BIT_ID_CONSTRUCTED = 0x1 << 5;
11+
12+
public const ID_INTEGER = 2;
13+
public const ID_BIT_STRING = 3;
14+
public const ID_OCTET_STRING = 4;
15+
public const ID_OBJECT_ID = 6;
16+
public const ID_SEQUENCE = 16 | self::BIT_ID_CONSTRUCTED;
17+
18+
public const NULL = "\x05\0";
19+
20+
private function __construct()
21+
{
22+
}
23+
24+
/**
25+
* Encodes a value in DER identifier-length-contents format.
26+
*
27+
* @param self::ID_* $id
28+
*/
29+
public static function encodeValue(int $id, string $content): string
30+
{
31+
// Assumption: we don't need long form.
32+
$prefix = chr($id);
33+
34+
$len = strlen($content);
35+
if ($len < 128) {
36+
$prefix .= chr($len);
37+
} else {
38+
// Assumption: we never encode anything larger than 2^31
39+
$enc = ltrim(pack('N', $len), "\0");
40+
$prefix .= chr(0x80 | strlen($enc));
41+
$prefix .= $enc;
42+
}
43+
44+
return $prefix.$content;
45+
}
46+
47+
/**
48+
* Encode an integer to base128.
49+
*/
50+
public static function encodeBase128(int $num): string
51+
{
52+
$result = chr($num & 0x7F);
53+
$num >>= 7;
54+
while ($num > 0) {
55+
$result .= chr(($num & 0x7F) | 0x80);
56+
$num >>= 7;
57+
}
58+
59+
return strrev($result);
60+
}
61+
62+
/**
63+
* Encode a sequence of values.
64+
*/
65+
public static function encodeSequence(string ...$values): string
66+
{
67+
return self::encodeValue(self::ID_SEQUENCE, implode('', $values));
68+
}
69+
70+
/**
71+
* Encode an object identifier.
72+
*/
73+
public static function encodeOid(int ...$values): string
74+
{
75+
$bin = '';
76+
foreach ($values as $value) {
77+
$bin .= self::encodeBase128($value);
78+
}
79+
80+
return self::encodeValue(self::ID_OBJECT_ID, $bin);
81+
}
82+
83+
/**
84+
* Encode some data as a bit string.
85+
*/
86+
public static function encodeBitString(string $data): string
87+
{
88+
return self::encodeValue(self::ID_BIT_STRING, "\0".$data);
89+
}
90+
}

0 commit comments

Comments
 (0)