- Introduced `CaptureOidcNonce` middleware to capture and store nonce in session during GET requests. - Updated `CustomAuthCodeRepository` to resolve nonce from session if not present in POST requests, ensuring seamless handling. - Registered middleware in `bootstrap/app.php` for web routes. - Adjusted `LoginResponse` to use `redirect()->intended` for improved redirection logic. - Added comprehensive feature tests to validate nonce capture, session storage, and cache resolution workflows.
107 lines
4.5 KiB
PHP
107 lines
4.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Http\Middleware\CaptureOidcNonce;
|
|
use App\OAuth\{CustomAuthCodeRepository, CustomTokenResponseType};
|
|
use Defuse\Crypto\Crypto;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Laravel\Passport\Passport;
|
|
use League\OAuth2\Server\Entities\{AuthCodeEntityInterface, ClientEntityInterface};
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
it('caches the nonce from the request during authorization and resolves it during token request', function (): void {
|
|
// --- Phase 1: Authorization ---
|
|
// 1. Create a request to authorize endpoint with a nonce
|
|
$authorizeRequest = Request::create('/oauth/authorize', 'GET', ['nonce' => 'secure-nonce-value-xyz']);
|
|
app()->instance('request', $authorizeRequest);
|
|
|
|
// 2. Create the mock AuthCodeEntity
|
|
$client = Mockery::mock(ClientEntityInterface::class);
|
|
$client->shouldReceive('getIdentifier')->andReturn('client-123');
|
|
|
|
$authCodeEntity = Mockery::mock(AuthCodeEntityInterface::class);
|
|
$authCodeEntity->shouldReceive('getIdentifier')->andReturn('code-id-abc');
|
|
$authCodeEntity->shouldReceive('getUserIdentifier')->andReturn('user-1');
|
|
$authCodeEntity->shouldReceive('getClient')->andReturn($client);
|
|
$authCodeEntity->shouldReceive('getScopes')->andReturn([]);
|
|
$authCodeEntity->shouldReceive('getExpiryDateTime')->andReturn(new DateTimeImmutable('+10 minutes'));
|
|
|
|
// 4. Run the repository persist method
|
|
$repository = new CustomAuthCodeRepository();
|
|
$repository->persistNewAuthCode($authCodeEntity);
|
|
|
|
// 5. Assert the nonce is cached
|
|
expect(Cache::get('oidc_nonce_code-id-abc'))->toBe('secure-nonce-value-xyz');
|
|
|
|
// --- Phase 2: Token Exchange ---
|
|
// 1. Prepare mock encrypted auth code payload
|
|
$key = Passport::tokenEncryptionKey(app('encrypter'));
|
|
$payload = json_encode([
|
|
'client_id' => 'client-123',
|
|
'auth_code_id' => 'code-id-abc',
|
|
'user_id' => 'user-1',
|
|
'scopes' => [],
|
|
'expire_time' => time() + 600,
|
|
]);
|
|
$encryptedAuthCode = Crypto::encryptWithPassword($payload, $key);
|
|
|
|
// 2. Set up the request to token endpoint containing the encrypted code
|
|
$tokenRequest = Request::create('/oauth/token', 'POST', ['code' => $encryptedAuthCode]);
|
|
app()->instance('request', $tokenRequest);
|
|
|
|
// 3. Resolve CustomTokenResponseType and call the protected resolveNonce method using reflection
|
|
$responseType = app(CustomTokenResponseType::class);
|
|
$reflection = new ReflectionClass(CustomTokenResponseType::class);
|
|
$method = $reflection->getMethod('resolveNonce');
|
|
$method->setAccessible(true);
|
|
|
|
$resolvedNonce = $method->invoke($responseType);
|
|
|
|
// 4. Assert the resolved nonce is correct
|
|
expect($resolvedNonce)->toBe('secure-nonce-value-xyz');
|
|
});
|
|
|
|
it('captures nonce via middleware and resolves it from session during POST approval request', function (): void {
|
|
// 1. Setup session
|
|
$session = app('session.store');
|
|
|
|
// 2. GET Request (Middleware captures nonce into session)
|
|
$getRequest = Request::create('/oauth/authorize', 'GET', ['nonce' => 'session-nonce-value-999']);
|
|
$getRequest->setLaravelSession($session);
|
|
|
|
$middleware = new CaptureOidcNonce();
|
|
$middleware->handle($getRequest, fn () => new Symfony\Component\HttpFoundation\Response());
|
|
|
|
expect($session->get('oidc_nonce'))->toBe('session-nonce-value-999');
|
|
|
|
// 3. POST Request (No nonce in parameters, but resolved from session)
|
|
$postRequest = Request::create('/oauth/authorize', 'POST');
|
|
$postRequest->setLaravelSession($session);
|
|
app()->instance('request', $postRequest);
|
|
|
|
// Mock AuthCodeEntity
|
|
$client = Mockery::mock(ClientEntityInterface::class);
|
|
$client->shouldReceive('getIdentifier')->andReturn('client-123');
|
|
|
|
$authCodeEntity = Mockery::mock(AuthCodeEntityInterface::class);
|
|
$authCodeEntity->shouldReceive('getIdentifier')->andReturn('code-id-session');
|
|
$authCodeEntity->shouldReceive('getUserIdentifier')->andReturn('user-1');
|
|
$authCodeEntity->shouldReceive('getClient')->andReturn($client);
|
|
$authCodeEntity->shouldReceive('getScopes')->andReturn([]);
|
|
$authCodeEntity->shouldReceive('getExpiryDateTime')->andReturn(new DateTimeImmutable('+10 minutes'));
|
|
|
|
// Persist
|
|
$repository = new CustomAuthCodeRepository();
|
|
$repository->persistNewAuthCode($authCodeEntity);
|
|
|
|
// Verify cache has it
|
|
expect(Cache::get('oidc_nonce_code-id-session'))->toBe('session-nonce-value-999');
|
|
|
|
// Verify session was cleared
|
|
expect($session->has('oidc_nonce'))->toBeFalse();
|
|
});
|