Sudo Mode with CakePHP Authorization Plugin
I’ve been working on content for my CakeFest workshop this year, and thought it would be interesting to see a commonly used authorization pattern implemented as an extension to CakePHP’s authorization system. The pattern I wanted to implement was ‘sudo mode’. Often this pattern is used in applications that have longer session duration. Applications like GitHub use sudo mode to allow you to use basic read/write privileges with long-lived sessions, but if you need to make a change that would impact the security of your account, you are required to re enter your password or MFA device; much like how one uses sudo
to temporarily elevate privileges to make system configuration changes in *nix systems.
What follows is a summary of the pattern implemented in CakePHP. During my workshop, I’ll give more context on this solution and some future improvements that could be made.
Functional Requirements
My goal for this solution was to provide a flexible way to require and apply sudo requirements. I also wanted to show how sudo mode can be done in server rendered applications without disrupting user flows. For API driven applications, you would need different error handling logic that ensured you returned the correct JSON payload for your front-end applications.
I wanted to have:
- Time based sudo mode activation. This would allow me to change the timeout globally or perhaps have operations that require a more recent privilege escalation.
- Policy based API. One of the goals of this was to show this pattern with the Authorization plugin so we need to use it.
- Server rendered flows. I wanted to show how sudo mode can fit into server rendered applications as well as API based ones. The HTML flow is easier to demo but more complex to implement as you have to build the client and server state with HTML.
Authorization Policies
Because we only want to selectively apply sudo, I used a trait based API:
- namespace App\Policy;
- trait RequireSudoTrait
- {
- /**
- * Requires the user to have an active sudo time window
- */
- protected function requireSudo(IdentityInterface $user): void
- {
- if (!$user->sudo_until || $user->sudo_until < DateTime::now()) {
- throw new SudoRequiredException();
- }
- }
- }
This allows us to include the trait as required and have a simple API of $this->requireSudo()
in the policy to activate our sudo mode. When a user doesn’t have sudo active and it is required, an application error will be found. I’ve not included the code for that class. It was trivial, just remember to extend the ForbiddenException
from the Authorization plugin.
We’ll catch this error in our error controller in later on.
Schema changes
As you may have noticed in the trait logic that I’m using $user->sudo_until
as a way to track when sudo was activated. You’ll need to add a datetime column with the name of your choice.
Handling Errors
Now that our schema has been updated and we have an action we want to protect, we need to handle the error. First we’ll need to add some custom logic. In our src/Controller/ErrorController.php lets add the following:
- /**
- * Enable json error views.
- */
- {
- return [JsonView::class];
- }
- /**
- * beforeRender callback.
- *
- * @param \Cake\Event\EventInterface<\Cake\Controller\Controller> $event Event.
- * @return \Cake\Http\Response|null|void
- */
- public function beforeRender(EventInterface $event)
- {
- parent::beforeRender($event);
- $builder = $this->viewBuilder();
- $builder->setTemplatePath('Error');
- $error = $builder->getVar('error');
- if ($error instanceof SudoRequiredException) {
- $builder->setTemplate('sudo_required');
- }
- }
Now when error pages are rendered, if the error is our SudoRequiredException
we can render a custom template instead of the default. Our template will look like:
- <?php
- declare(strict_types=1);
- ?>
- <div class="sudo-required form content">
- <?= $this->Form->create() ?>
- <?= $this->Form->hidden('op', ['value' => 'sudo_activate']) ?>
- <?php
- if ($key === 'op' || $key === 'password') {
- continue;
- }
- ?>
- <?= $this->Form->hidden($key, ['value' => $value]) ?>
- <?php endforeach; ?>
- <fieldset>
- <legend><?= __('Please enter your password to continue') ?></legend>
- <?= $this->Form->control('password') ?>
- </fieldset>
- <?= $this->Form->button(__('Proceed')); ?>
- </div>
This form will capture all of the request body data and store it in a form alongside the password and op
parameter that is used to signal that a sudo side-channel request is occurring.
Some ways to improve this would be to store request data in a cache. Instead of serializing the data back into a form, you could pass uuid style identifier of the request payload. When the credentials are provided the cache data can replace the request data.
Activating Sudo
Now we have a form that captures the previous request, and will allow it to be replayed again we need controller logic to handle this. I chose to implement this as middleware so that it could be self contained. This logic could also live on the AppController
if that’s more your style.
- namespace App\Middleware;
- use App\Model\Entity\User;
- use App\Policy\SudoRequiredException;
- use Cake\Http\ServerRequest;
- use Cake\ORM\Locator\LocatorAwareTrait;
- use Psr\Http\Message\ResponseInterface;
- use Psr\Http\Message\ServerRequestInterface;
- use Psr\Http\Server\MiddlewareInterface;
- use Psr\Http\Server\RequestHandlerInterface;
- class SudoRequiredMiddleware implements MiddlewareInterface
- {
- use LocatorAwareTrait;
- public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
- {
- $password = $request->getData('password');
- $identity = $request->getAttribute('identity');
- if (!$identity || $request->getData('op') !== 'sudo_activate') {
- return $handler->handle($request);
- }
- $user = $identity->getOriginalData();
- if ($user->activateSudo($password)) {
- $users = $this->fetchTable('Users');
- $users->saveOrFail($user);
- $data = $request->getData();
- $request = $request->withParsedBody($data);
- return $handler->handle($request);
- }
- $request->getFlash()->error('Sudo failed, password was incorrect.');
- throw new SudoRequiredException();
- }
- }
This logic checks each request for our op
signal parameter. You could choose any name for this parameter however, I would avoid overlapping with a field you use in your schema anywhere. Should sudo fail again, we can raise another error (which is caught and rendered by our error controller) starting the authorization process again.
Entity Changes
Finally we have to update our entity with the logic required to set a timeout for sudo. If you’re going to have multiple timeouts. I would set this to the longest of those timeouts and have constraints on how fresh the timestamp needs to be.
- public function activateSudo(string $password): bool
- {
- $hasher = new DefaultPasswordHasher();
- if ($hasher->check($password, $this->password)) {
- // The sudo activation timeout could be application configuration as well.
- $this->sudo_until = DateTime::now()->modify('+15 minutes');
- return true;
- }
- return false;
- }
That’s it
That’s all there is to building a ‘sudo mode’ with CakePHP’s authorization system. Let me know what you think of it, and if you’re interested on working on a plugin together for this.
There are no comments, be the first!