Feat: Enhance OIDC nonce handling with middleware and session support
- 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.
This commit is contained in:
parent
2682a234d4
commit
0921535ae2
24
app/Http/Middleware/CaptureOidcNonce.php
Normal file
24
app/Http/Middleware/CaptureOidcNonce.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class CaptureOidcNonce
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
if ($request->isMethod('GET') && $request->has('nonce')) {
|
||||||
|
$request->session()->put('oidc_nonce', $request->input('nonce'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -23,6 +23,6 @@ public function toResponse($request)
|
|||||||
])
|
])
|
||||||
->log('User logged in}');
|
->log('User logged in}');
|
||||||
|
|
||||||
return to_route('dashboard');
|
return redirect()->intended(route('dashboard'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,12 +17,18 @@ public function persistNewAuthCode(AuthCodeEntityInterface $authCodeEntity): voi
|
|||||||
{
|
{
|
||||||
parent::persistNewAuthCode($authCodeEntity);
|
parent::persistNewAuthCode($authCodeEntity);
|
||||||
|
|
||||||
if (request()->has('nonce')) {
|
$nonce = request()->input('nonce') ?? (request()->hasSession() ? request()->session()->get('oidc_nonce') : null);
|
||||||
|
|
||||||
|
if ($nonce) {
|
||||||
Cache::put(
|
Cache::put(
|
||||||
'oidc_nonce_'.$authCodeEntity->getIdentifier(),
|
'oidc_nonce_'.$authCodeEntity->getIdentifier(),
|
||||||
request()->input('nonce'),
|
$nonce,
|
||||||
now()->addMinutes(10)
|
now()->addMinutes(10)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (request()->hasSession()) {
|
||||||
|
request()->session()->forget('oidc_nonce');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,10 @@
|
|||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware): void {
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
|
$middleware->web(append: [
|
||||||
|
App\Http\Middleware\CaptureOidcNonce::class,
|
||||||
|
]);
|
||||||
|
|
||||||
$middleware->validateCsrfTokens(except: [
|
$middleware->validateCsrfTokens(except: [
|
||||||
'keka/callback',
|
'keka/callback',
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Http\Middleware\CaptureOidcNonce;
|
||||||
use App\OAuth\{CustomAuthCodeRepository, CustomTokenResponseType};
|
use App\OAuth\{CustomAuthCodeRepository, CustomTokenResponseType};
|
||||||
use Defuse\Crypto\Crypto;
|
use Defuse\Crypto\Crypto;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
@ -63,3 +64,43 @@
|
|||||||
// 4. Assert the resolved nonce is correct
|
// 4. Assert the resolved nonce is correct
|
||||||
expect($resolvedNonce)->toBe('secure-nonce-value-xyz');
|
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();
|
||||||
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user