Examples
A protected page
Configure the path, and the controller reads the user that the authentication put in the request:
AUTH_PROTECTED_PATHS='["/dashboard"]'
use Laminas\Diactoros\Response\JsonResponse;
use Mezzio\Authentication\UserInterface;
use Psr\Http\Message\ServerRequestInterface;
final class DashboardController
{
public function __construct(private readonly RendererInterface $renderer)
{
}
public function index(ServerRequestInterface $request): string
{
$user = $request->getAttribute(UserInterface::class);
return $this->renderer->render('dashboard', ['user' => $user]);
}
}
If the user is not logged in, the controller never runs: the user goes to Keycloak (or to the login page) and comes back to /dashboard.
Roles in a path
AUTH_PROTECTED_PATHS='{"/dashboard": [], "/admin": ["admin"], "/reports": ["analyst", "admin"]}'
A user without the role gets a 403. For a route that needs its own roles (the path does not have to be in AUTH_PROTECTED_PATHS: a route with roles is protected by itself):
export:
path: /dashboard/export
roles: [admin]
handler: App\Controller\ExportController::index
An API
A request to /api or to a path below it (/api/items; /apiary is not) that is not authenticated gets a JSON 401, never a redirect:
AUTH_PROTECTED_PATHS='["/api"]'
final class UsersApiController
{
public function index(ServerRequestInterface $request): ResponseInterface
{
$user = $request->getAttribute(UserInterface::class);
if (!$user->hasRole('admin')) {
return new JsonResponse(['status' => 403, 'title' => 'Forbidden'], 403);
}
return new JsonResponse(['users' => $this->users->all()]);
}
}
The user in every page, and the logout
The app variable of the templates (from derafu/http) has the user. The profile has the button to log out, so the layout only links to it, or to the login:
{% if app.user %}
<a href="{{ profile_path() }}">{{ app.user.name ?? app.user.identity }}</a>
{% else %}
<a href="{{ login_path() }}">Log in</a>
{% endif %}
{% include 'partials/flash-messages.html.twig' %}
app.user is null when nobody is logged in. The functions for the links are in Profile.
Keycloak for development
The realm of the tests of the package is a good start: a container with a client, its roles and a user.
docker run --rm -p 8080:8080 \
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
-v "$PWD/vendor/derafu/auth/tests/fixtures/keycloak:/opt/keycloak/data/import:ro" \
quay.io/keycloak/keycloak:26.8.0 start-dev --import-realm
AUTH_KEYCLOAK_URL=http://localhost:8080
AUTH_KEYCLOAK_REALM=test
AUTH_KEYCLOAK_CLIENT_ID=derafu-auth
AUTH_KEYCLOAK_CLIENT_SECRET=test-secret
AUTH_KEYCLOAK_WEB_REDIRECT_URI=https://app.test/auth/callback
SESSION_COOKIE_SECURE=false
The redirect URIs of that client are the ones of the tests: add the ones of your application to the realm, in the administration console (http://localhost:8080, admin / admin).
The database provider
AUTH_DATABASE_URL=sqlite:%kernel.project_dir%/var/users.db
AUTH_DATABASE_USER_TABLE=user
AUTH_DATABASE_USER_FIELD_IDENTITY=email
AUTH_DATABASE_USER_FIELD_PASSWORD=password
AUTH_PROTECTED_PATHS='["/dashboard"]'
AUTH_DATABASE_LOGIN_MAX_ATTEMPTS=5
AUTH_DATABASE_LOGIN_LOCK_SECONDS=900
// Creating a user: the hash is the one of password_hash().
$pdo->prepare('INSERT INTO user (email, password) VALUES (:email, :password)')->execute([
'email' => '[email protected]',
'password' => password_hash('a good password', PASSWORD_DEFAULT),
]);
Do not forget the role of the user (a row in user_role, or whatever the query of the roles reads): a user without roles is denied in the protected paths.
Without a container
use Derafu\Auth\Authentication\AuthenticationManager;
use Derafu\Auth\Authentication\Channel\Api\ApiChannel;
use Derafu\Auth\Authentication\Channel\Api\ApiConfiguration;
use Derafu\Auth\Authentication\Channel\Web\SessionManager;
use Derafu\Auth\Authentication\Channel\Web\WebChannel;
use Derafu\Auth\Authentication\Channel\Web\WebConfiguration;
use Derafu\Auth\Authentication\LoginThrottle;
use Derafu\Auth\Authorization\AccessRules;
use Derafu\Auth\Provider\Database\Api\DatabaseBasicScheme;
use Derafu\Auth\Provider\Database\DatabaseConfiguration;
use Derafu\Auth\Provider\Database\DatabaseUserRepository;
use Derafu\Auth\Provider\Database\Web\DatabaseWebFlow;
$config = new DatabaseConfiguration(['database_url' => 'sqlite:/path/to/users.db']);
$users = new DatabaseUserRepository($config);
$sessions = new SessionManager();
$web = new WebConfiguration();
$throttle = new LoginThrottle($cachePool, maxAttempts: 5, lockSeconds: 900);
$authentication = new AuthenticationManager(
[
// The API one is asked first, and the web one is the last.
new ApiChannel(new ApiConfiguration(), new DatabaseBasicScheme($users, $config, $throttle)),
new WebChannel(
// $formManager: Derafu\Auth\Authentication\Channel\Web\FormManager, with the form factory and the form data processor.
new DatabaseWebFlow($users, $config, $web, $sessions, $formManager, throttle: $throttle),
$web,
$sessions
),
],
new AccessRules(['protected_paths' => ['/dashboard' => []]])
);
// With the middleware of Mezzio: after the session and the flash messages.
$middleware = new Derafu\Auth\Authentication\AuthenticationMiddleware($authentication);