Skip to content

Instantly share code, notes, and snippets.

@marcguyer
Created September 8, 2023 17:07
Show Gist options
  • Save marcguyer/afc9edffc0ac264e28f1e5be4e712d7f to your computer and use it in GitHub Desktop.
Save marcguyer/afc9edffc0ac264e28f1e5be4e712d7f to your computer and use it in GitHub Desktop.
PSR7 Middleware supporting OAuth2 redirect flows
<?php
declare(strict_types=1);
namespace MyAuth\Middleware;
use \RuntimeException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
use SplStack;
use Mezzio\Authentication\UserInterface;
use Mezzio\Session\SessionMiddleware;
use Laminas\Uri\UriFactory;
/**
* Middleware to manage various redirect locations and deep links
*/
class RedirectMiddleware implements MiddlewareInterface
{
private LoggerInterface $logger;
private SplStack $stack;
/**
* Value used to indicate that location redirect management is deferred
* to this middleware.
*/
public const LOCATION_PLACEHOLDER = 'location_placeholder';
private array $config;
public function __construct(array $config, LoggerInterface $logger)
{
$this->config = $config;
$this->logger = $logger;
// start with a fresh stack
$this->stack = new SplStack();
}
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface {
// session is required to store redirect values across oauth2 request flows
if (
null === $session = $request
->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE)
) {
throw new \RuntimeException('Session is required by this middleware');
}
// merge the stack from session
if ($session->has(self::class)) {
$sessionStack = unserialize(
$session->get(self::class),
['allowed_classes' => [SplStack::class]]
);
$this->logger->debug(
'Redirect stack found in session',
[
'session_count' => count($sessionStack),
'current_count' => count($this->stack)
]
);
$this->stack = $sessionStack;
$this->logger->debug('Redirect stack', $this->toArray());
}
// add this object as a request attribute so later
// middlewares/handlers can use it
$request = $request->withAttribute(self::class, $this);
// if the request has a 'fwd' param, remember it for later (deep link)
// and remove it from the request
$request = $this->detectDeepLink($request);
// run the next handler and get the response
// additional redirect locations could be added to the stack
// anywhere in the rest of the pipeline
$response = $handler->handle($request);
// remember the current stack
$session->set(self::class, serialize($this->stack));
// if user is not authenticated, we do nothing
if (null === $request->getAttribute(UserInterface::class)) {
$this->logger->debug('No authenticated user. Nothing to do.');
return $response;
}
// no location header so nothing to manage
if (!$response->hasHeader($this->config['header_name'])) {
$this->logger->debug('No location header. Nothing to do.');
return $response;
}
// if the location header does not contain the special value
// we will not touch it
if (self::LOCATION_PLACEHOLDER !== $response->getHeader($this->config['header_name'])[0]) {
$this->logger->debug(
'Location header is not a placeholder. Nothing to do.',
['location_header' => $response->getHeader($this->config['header_name'])[0]]
);
return $response;
}
return $response->withHeader($this->config['header_name'], $this->getRedirect($request));
}
public function push(string $key, string $redirect, bool $replace = true): void
{
$uri = $this->getValidUri($redirect);
$this->logger->debug('Redirect stack', $this->toArray());
$this->logger->debug('Pushing location onto the stack', [$key => $uri]);
if ($replace) {
$this->stack->rewind();
$this->logger->debug('Starting with', ['key' => $this->stack->key()]);
foreach ($this->stack as $idx => $element) {
$this->logger->debug('Element', [$idx => $element]);
if (isset($element[$key])) {
$this->logger->info('Removing element', [$idx => $element]);
$this->stack->offsetUnset($idx);
break;
}
}
}
$this->stack->push([$key => $uri]);
$this->logger->debug('Redirect stack', $this->toArray());
}
public function toArray(): array
{
$arr = [];
$this->stack->rewind();
foreach ($this->stack as $e) {
$arr[] = $e;
}
return $arr;
}
public function getStack(): SplStack
{
return $this->stack;
}
private function getRedirect(ServerRequestInterface $request): string
{
// no redirect info in the session, so redirect to default loc
if ($this->stack->isEmpty()) {
$redirect = $request->getAttribute(UserDefaultRedirectMiddleware::REQUEST_ATTRIBUTE);
$this->logger->debug(
'Nothing in the stack. Using the default.',
['default_redirect' => $redirect]
);
return $redirect;
}
$redirect = $this->stack->pop();
// after modifying the stack, we save the new version to the session
$session = $request
->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE);
if ($this->stack->isEmpty()) {
// nothing more to manage
$this->logger->debug('Stack is now empty');
$session->unset(self::class);
} else {
// set the new value
// A test is needed for this: theoretically this only happens when
// there are multiple redirects set in the same flow. But I'm not
// certain this is even possible.
$this->logger->notice('Redirect stack (expected empty)', $this->toArray());
$session->set(self::class, serialize($this->stack));
}
$redirectStr = current($redirect);
$this->logger->debug('Redirect was popped', $redirect);
return $redirectStr;
}
/**
* Detect a valid deep link. If found, add it to the stack and
* remove it from the request.
*/
private function detectDeepLink(ServerRequestInterface $request): ServerRequestInterface
{
$params = $request->getQueryParams();
$fwd = $params['fwd'] ?? null;
if (null === $fwd) {
return $request;
}
unset($params['fwd']);
$this->logger->info('Found fwd in GET param', ['fwd' => $fwd]);
$this->push('deep-link', $fwd);
return $request->withQueryParams($params);
}
private function getValidUri(string $uri): string
{
try {
$uriObj = UriFactory::factory($uri);
} catch (\Throwable $t) {
$this->logger->error('Uri for redirect is invalid', ['invalid_uri' => $uri]);
throw RuntimeException::uriInvalid('We will not redirect to invalid URI', $t);
}
if (!$uriObj->isAbsolute()) {
$this->logger->error('Uri for redirect is not absolute', ['invalid_uri' => $uri]);
throw RuntimeException::uriNotAbsolute('We will not redirect to relative URI');
}
return (string)$uriObj;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment