0

I'm building a web application in CakePHP 5 using the Authentication and Authorization plugins.
My users can be members of multiple clubs through a many-to-many relationship (using a join table). The complete app including authorization checks is working fine so far.

My Goal

I want to enforce (with middleware) that after logging in, a user must always be a member of at least one club before they can use the rest of the app (onboarding requirement).
If they are not a member, they should be redirected to a /clubs/join "join club" page, except for certain whitelisted actions like login, register, and logout.


What I've Done

I've created a custom RequireMembershipMiddleware in src/Middleware/RequireMembershipMiddleware.php:

<?php
declare(strict_types=1);

namespace App\Middleware;

use Authentication\IdentityInterface;
use Cake\Http\Response;
use Cake\Routing\Router;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class RequireMembershipMiddleware implements MiddlewareInterface
{
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $identity = $request->getAttribute('identity');

        if (!$identity) {
            // Not logged in? Not our concern, pass along.
            return $handler->handle($request);
        }

        // Assume 'clubs' relation is loaded on User
        $user = $identity instanceof IdentityInterface ? $identity->getOriginalData() : $identity;
        if (!empty($user->clubs) && count($user->clubs) > 0) {
            // User has membership(s), proceed.
            return $handler->handle($request);
        }

        // Whitelist some paths for unaffiliated users
        $allowed = [
            '/clubs/join',
            '/logout',
            '/users/logout',
            '/users/login',
            '/users/register',
        ];
        $current = $request->getPath();

        foreach ($allowed as $allowedPath) {
            if (stripos($current, $allowedPath) === 0) {
                return $handler->handle($request);
            }
        }

        // Redirect to join club page
        $response = new Response();
        return $response
            ->withHeader('Location', Router::url('/clubs/join'))
            ->withStatus(302);
    }
}

And in src/Application.php I have:

public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
{
    $middlewareQueue
        // ...
        ->add(new AuthenticationMiddleware($this))
        ->add(new AuthorizationMiddleware($this, [
            'identityDecorator' => function ($auth, $user) {
                return $user->setAuthorization($auth);
            },
        ]))
        ->add(new RequireMembershipMiddleware());

    return $middlewareQueue;
}

The Problem

If I visit any URL (as a logged-in user) that is not in the $allowed list, my middleware triggers as expected but is not redirecting to the onboarding page.

I keep getting this error/warning:

The request to /CONTROLLER/ACTION did not apply any authorization checks.

  • I'm using CakePHP 5's Authentication and Authorization plugins.

What I've Tried/Considered

  • Moving the middleware higher/lower in the stack doesn't fix the warning.

Question

How can I properly enforce this onboarding/membership requirement via middleware without running into the "did not apply any authorization checks" error?

  • Is there a CakePHP-idiomatic way to handle this?

  • Should I combine this logic with Authorization policies somehow?

  • Is there a best practice for this onboarding/guard type workflow?

1 Answer 1

0

When you need your custom middleware to run after authentication but before any authorization checks, you can use MiddlewareQueue::insertAfter(). And if you're using DebugKit you'll also want to skip your "require membership" logic on any DebugKit routes.

1) Inserting your middleware in the right spot

In src/Application.php:

use Authentication\Middleware\AuthenticationMiddleware;
use Authorization\Middleware\AuthorizationMiddleware;
use App\Middleware\RequireMembershipMiddleware;

public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
{
    $middlewareQueue
        // ... other middleware ...
        ->add(new AuthenticationMiddleware($this))

        // make sure RequireMembershipMiddleware runs after authN but before authZ
        ->insertAfter(
            'Authentication\Middleware\AuthenticationMiddleware',
            new RequireMembershipMiddleware()
        )

        ->add(new AuthorizationMiddleware($this, [
            'identityDecorator' => function ($auth, $user) {
                return $user->setAuthorization($auth);
            },
        ]));

    return $middlewareQueue;
}

2) Skipping DebugKit

Inside your RequireMembershipMiddleware::process() you can detect the DebugKit plugin and simply pass through:

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
    // Don't interfere with DebugKit requests
    $plugin = strtolower((string)$request->getAttribute('params')['plugin'] ?? '');
    if ($plugin === 'debugkit') {
        return $handler->handle($request);
    }

    // ... the rest of your membership check ...
}

3) Flash‑and‑redirect for non‑members

Here's a minimal example of your full process() method:

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
    // 1) Skip if not logged in
    $identity = $request->getAttribute('identity');
    if (!$identity) {
        return $handler->handle($request);
    }

    // 2) Skip DebugKit
    $plugin = strtolower((string)$request->getAttribute('params')['plugin'] ?? '');
    if ($plugin === 'debugkit') {
        return $handler->handle($request);
    }

    // 3) Check club membership
    $user = $identity instanceof IdentityInterface
        ? $identity->getOriginalData()
        : $identity;

    if (!empty($user->clubs)) {
        return $handler->handle($request);
    }

    // 4) Show a flash message and redirect to /clubs/index
    $response = new Response();
    if ($request instanceof ServerRequest) {
        $request->getFlash()->info(
            __('You need to activate a membership at a club before you can continue using the system.')
        );
    }

    return $response
        ->withHeader(
            'Location',
            Router::url(['controller' => 'Clubs', 'action' => 'index'])
        )
        ->withStatus(302);
}

Why this works:

  1. insertAfter() ensures your middleware runs immediately after authentication, so you can still bail out with a redirect before any authorization policy gets invoked (avoiding the "did not apply any authorization checks" error).

  2. DebugKit bypass stops your redirect flash from breaking the toolbar (since DebugKit loads its own controllers).

  3. You get a consistent flash+redirect for any user without a club membership---and all other routes (login, register, logout, DebugKit) continue to work as expected.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.