Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2163ad996 |
@@ -126,6 +126,16 @@ If the provider does not return an `id_token`, callback handling remains gracefu
|
||||
|
||||
Compatibility mode: if you have a temporary deployment constraint and cannot enforce nonce immediately, call `handleCallback($_GET, true, ['validate_nonce' => false])`. Treat this as a short-lived compatibility setting only.
|
||||
|
||||
If your provider issues an `id_token` but omits the `nonce` claim, the SDK will accept missing nonce values by default while still enforcing a match when one is present. You can also set it explicitly:
|
||||
|
||||
```php
|
||||
$result = $client->handleCallback($_GET, true, [
|
||||
'allow_missing_nonce' => true,
|
||||
]);
|
||||
```
|
||||
|
||||
Use this only when you have verified the provider cannot include `nonce`. It weakens replay protection for those tokens.
|
||||
|
||||
### JWT payload decoding helper
|
||||
|
||||
Use `decodeJwtPayloadClaims()` when you need to inspect an `id_token` payload:
|
||||
|
||||
@@ -135,6 +135,16 @@ $result = $client->handleCallback($_GET, true, [
|
||||
|
||||
Use this only as a short-term bridge while the application is updated to persist and enforce nonce consistently.
|
||||
|
||||
If your provider returns an `id_token` without the `nonce` claim, the SDK will allow missing nonce values by default while still enforcing a match when one is present. You can also set it explicitly:
|
||||
|
||||
```php
|
||||
$result = $client->handleCallback($_GET, true, [
|
||||
'allow_missing_nonce' => true,
|
||||
]);
|
||||
```
|
||||
|
||||
Use this only after confirming the provider cannot include `nonce`, because it weakens replay protection for those tokens.
|
||||
|
||||
## 3. Single callback for multiple app roles
|
||||
|
||||
Using one OAuth client and one callback URL is recommended.
|
||||
|
||||
+17
-2
@@ -230,7 +230,7 @@ final class AuthClient
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $query
|
||||
* @param array{expected_nonce?:?string,validate_nonce?:bool} $options
|
||||
* @param array{expected_nonce?:?string,validate_nonce?:bool,allow_missing_nonce?:bool} $options
|
||||
*/
|
||||
public function handleCallback(array $query, bool $clearPkce = true, array $options = []): CallbackResult
|
||||
{
|
||||
@@ -320,7 +320,7 @@ final class AuthClient
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{expected_nonce?:?string,validate_nonce?:bool} $options
|
||||
* @param array{expected_nonce?:?string,validate_nonce?:bool,allow_missing_nonce?:bool} $options
|
||||
*/
|
||||
private function maybeValidateNonce(TokenSet $tokens, array $options): void
|
||||
{
|
||||
@@ -334,6 +334,21 @@ final class AuthClient
|
||||
return;
|
||||
}
|
||||
|
||||
$allowMissingNonce = !isset($options['allow_missing_nonce']) || (bool) $options['allow_missing_nonce'];
|
||||
if ($allowMissingNonce) {
|
||||
$claims = $this->decodeJwtPayloadClaims($tokens->idToken);
|
||||
$nonce = $claims['nonce'] ?? null;
|
||||
if (!is_string($nonce) || $nonce === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hash_equals($expectedNonce, $nonce)) {
|
||||
throw new NonceMismatchException('The ID token nonce did not match the expected value', 'nonce_mismatch');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->assertIdTokenNonceMatches($tokens->idToken, $expectedNonce);
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ class BlackWallAuth
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $query
|
||||
* @param array{expected_nonce?:?string,validate_nonce?:bool} $options
|
||||
* @param array{expected_nonce?:?string,validate_nonce?:bool,allow_missing_nonce?:bool} $options
|
||||
* @return array{
|
||||
* tokens: array<string,mixed>,
|
||||
* user: array{email:string,privilege_level:?int,role:?string,raw:array<string,mixed>},
|
||||
|
||||
@@ -303,6 +303,66 @@ final class AuthClientTest extends TestCase
|
||||
self::assertNull($result->tokens->idToken);
|
||||
}
|
||||
|
||||
public function testHandleCallbackAllowsMissingNonceClaimWhenEnabled(): void
|
||||
{
|
||||
$_SESSION[AuthClient::STATE_SESSION_KEY] = 'state-1';
|
||||
$_SESSION[AuthClient::CODE_VERIFIER_SESSION_KEY] = 'verifier-123';
|
||||
$_SESSION[AuthClient::NONCE_SESSION_KEY] = 'expected-nonce';
|
||||
|
||||
$this->http->queuePost([
|
||||
'status' => 200,
|
||||
'body' => json_encode([
|
||||
'access_token' => 'access-1',
|
||||
'refresh_token' => 'refresh-1',
|
||||
'id_token' => $this->makeJwt(['sub' => 'user-1']),
|
||||
'token_type' => 'Bearer',
|
||||
], JSON_THROW_ON_ERROR),
|
||||
]);
|
||||
|
||||
$this->http->queueGet([
|
||||
'status' => 200,
|
||||
'body' => '{"email":"tutor@example.com","privilege_level":2}',
|
||||
]);
|
||||
|
||||
$result = $this->client->handleCallback([
|
||||
'code' => 'code-1',
|
||||
'state' => 'state-1',
|
||||
], true, [
|
||||
'allow_missing_nonce' => true,
|
||||
]);
|
||||
|
||||
self::assertSame('access-1', $result->tokens->accessToken);
|
||||
}
|
||||
|
||||
public function testHandleCallbackAllowsMissingNonceClaimByDefault(): void
|
||||
{
|
||||
$_SESSION[AuthClient::STATE_SESSION_KEY] = 'state-1';
|
||||
$_SESSION[AuthClient::CODE_VERIFIER_SESSION_KEY] = 'verifier-123';
|
||||
$_SESSION[AuthClient::NONCE_SESSION_KEY] = 'expected-nonce';
|
||||
|
||||
$this->http->queuePost([
|
||||
'status' => 200,
|
||||
'body' => json_encode([
|
||||
'access_token' => 'access-1',
|
||||
'refresh_token' => 'refresh-1',
|
||||
'id_token' => $this->makeJwt(['sub' => 'user-1']),
|
||||
'token_type' => 'Bearer',
|
||||
], JSON_THROW_ON_ERROR),
|
||||
]);
|
||||
|
||||
$this->http->queueGet([
|
||||
'status' => 200,
|
||||
'body' => '{"email":"tutor@example.com","privilege_level":2}',
|
||||
]);
|
||||
|
||||
$result = $this->client->handleCallback([
|
||||
'code' => 'code-1',
|
||||
'state' => 'state-1',
|
||||
]);
|
||||
|
||||
self::assertSame('access-1', $result->tokens->accessToken);
|
||||
}
|
||||
|
||||
public function testHandleCallbackCanDisableNonceValidationTemporarily(): void
|
||||
{
|
||||
$_SESSION[AuthClient::STATE_SESSION_KEY] = 'state-1';
|
||||
|
||||
Reference in New Issue
Block a user