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.
|
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
|
### JWT payload decoding helper
|
||||||
|
|
||||||
Use `decodeJwtPayloadClaims()` when you need to inspect an `id_token` payload:
|
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.
|
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
|
## 3. Single callback for multiple app roles
|
||||||
|
|
||||||
Using one OAuth client and one callback URL is recommended.
|
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<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
|
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
|
private function maybeValidateNonce(TokenSet $tokens, array $options): void
|
||||||
{
|
{
|
||||||
@@ -334,6 +334,21 @@ final class AuthClient
|
|||||||
return;
|
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);
|
$this->assertIdTokenNonceMatches($tokens->idToken, $expectedNonce);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ class BlackWallAuth
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string,mixed> $query
|
* @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{
|
* @return array{
|
||||||
* tokens: array<string,mixed>,
|
* tokens: array<string,mixed>,
|
||||||
* user: array{email:string,privilege_level:?int,role:?string,raw: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);
|
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
|
public function testHandleCallbackCanDisableNonceValidationTemporarily(): void
|
||||||
{
|
{
|
||||||
$_SESSION[AuthClient::STATE_SESSION_KEY] = 'state-1';
|
$_SESSION[AuthClient::STATE_SESSION_KEY] = 'state-1';
|
||||||
|
|||||||
Reference in New Issue
Block a user