---
title: "Auth Project"
description: "Derafu Auth"
type: "docs"
category: "doc"
tags: [php]
authors: [Anonymous]
date: "2026-09-09"
last_update: "2026-09-09"
time_minutes: 1
draft: false
unlisted: false
url: "https://www.derafu.dev/docs/core/auth"
---

# Derafu Auth



---

## Introduction

Authentication and Authorization

# Derafu: Auth

PSR-15 compliant authentication and authorization library for PHP applications with Keycloak integration.

## Features

- **PSR-15 Middleware**: Standard-compliant middleware.
- **Keycloak Integration**: OAuth2/OpenID Connect.
- **Session Management**: Secure session handling.
- **Token Refresh**: Automatic token refresh.
- **Route Protection**: Flexible route-based auth.
- **CSRF Protection**: State parameter validation.

## Quick Start

### Installation

Install the package:

```bash
composer require derafu/auth
```

### Environment Variables

Configure, at the very least, the following environment variables:

```env
KEYCLOAK_URL=http://localhost:8080
KEYCLOAK_CLIENT_ID=your-client-id
KEYCLOAK_CLIENT_SECRET=your-client-secret
KEYCLOAK_REDIRECT_URI=http://localhost/auth/callback
```

### Routes

Import the routes to your `routes.yaml`:

```yaml
imports:
    - { resource: &#039;../vendor/derafu/auth/resources/config/auth-routes.yaml&#039; }
```

### Services

Import the services to your `services.yaml`:

```yaml
imports:
    - { resource: &#039;../vendor/derafu/auth/resources/config/auth-services.yaml&#039; }
```

### Middleware

Add the middleware to your `services.yaml`:

```yaml
Psr\Http\Server\RequestHandlerInterface:
    class: Derafu\Http\Service\RequestHandler
    public: true
    arguments:
        $middlewares:
            - &#039;@Derafu\Auth\Middleware\AuthenticationMiddleware&#039;
```

**Note**: the `AuthenticationMiddleware` needs to be placed before the `DispatcherMiddleware`.

### Access User Information

Directly with the attribute `user` or `access_token`:

```php
$user = $request-&gt;getAttribute(&#039;user&#039;);
$accessToken = $request-&gt;getAttribute(&#039;access_token&#039;);
```

Using the `UserInterface` from the `mezzio/mezzio-authentication` package:

```php
$user = $request-&gt;getAttribute(UserInterface::class);
if ($user) {
    $identity = $user-&gt;getIdentity();
    $roles = iterator_to_array($user-&gt;getRoles());
    $email = $user-&gt;getDetail(&#039;email&#039;);
}
```




---

## Configuration

Configuration options for Derafu Auth

# Configuration

## Environment Variables

| Variable | Type | Required | Default | Description |
|----------|------|----------|---------|-------------|
| `KEYCLOAK_URL` | string | **Yes** | - | Keycloak server URL |
| `KEYCLOAK_CLIENT_ID` | string | **Yes** | - | OAuth2 client ID |
| `KEYCLOAK_CLIENT_SECRET` | string | **Yes** | - | OAuth2 client secret |
| `KEYCLOAK_REDIRECT_URI` | string | **Yes** | - | OAuth2 redirect URI |
| `KEYCLOAK_REALM` | string | No | `master` | Keycloak realm |
| `KEYCLOAK_SCOPES` | json | No | `[&quot;openid&quot;, &quot;profile&quot;, &quot;email&quot;]` | OAuth2 scopes |
| `KEYCLOAK_PROTECTED_ROUTES` | json | No | `[&quot;/dashboard&quot;, &quot;/profile&quot;, &quot;/admin&quot;]` | Protected routes |
| `KEYCLOAK_CALLBACK_ROUTE` | string | No | `/auth/callback` | Callback route |
| `KEYCLOAK_LOGOUT_ROUTE` | string | No | `/auth/logout` | Logout route |
| `KEYCLOAK_SESSION_LIFETIME` | int | No | `3600` | Session lifetime |
| `KEYCLOAK_SECURE_COOKIES` | bool | No | `false` | Secure cookies |
| `KEYCLOAK_HTTP_TIMEOUT` | int | No | `30` | HTTP timeout |
| `KEYCLOAK_HTTP_CONNECT_TIMEOUT` | int | No | `30` | HTTP connect timeout |
| `KEYCLOAK_HTTP_VERIFY` | bool | No | `false` | SSL verification |

## Required Configuration

The following parameters are **mandatory** and must be provided:

```env
# Keycloak server configuration.
KEYCLOAK_URL=https://&lt;YOUR_KEYCLOAK_SERVER&gt;

# OAuth2 client credentials.
KEYCLOAK_CLIENT_ID=your-client-id
KEYCLOAK_CLIENT_SECRET=your-client-secret

# OAuth2 redirect URI.
KEYCLOAK_REDIRECT_URI=https://&lt;YOUR_APP_URL&gt;/auth/callback
```

## Routes Configuration

```yaml
auth_callback:
    path: /auth/callback
    handler: Derafu\Auth\Controller\CallbackController::handle

```

## Services Configuration

```yaml
services:

    _defaults:
        autowire: true
        autoconfigure: true
        public: false

    Derafu\Auth\Contract\AuthConfigurationInterface:
        class: Derafu\Auth\Configuration\AuthConfiguration
        arguments:
            $config:
                keycloak_url: &#039;%env(default::string:KEYCLOAK_URL)%&#039;
                realm: &#039;%env(default::string:KEYCLOAK_REALM)%&#039;
                client_id: &#039;%env(default::string:KEYCLOAK_CLIENT_ID)%&#039;
                client_secret: &#039;%env(default::string:KEYCLOAK_CLIENT_SECRET)%&#039;
                redirect_uri: &#039;%env(default::string:KEYCLOAK_REDIRECT_URI)%&#039;
                scopes: &#039;%env(default::json:KEYCLOAK_SCOPES)%&#039;
                protected_routes: &#039;%env(default::json:KEYCLOAK_PROTECTED_ROUTES)%&#039;
                callback_route: &#039;%env(default::string:KEYCLOAK_CALLBACK_ROUTE)%&#039;
                logout_route: &#039;%env(default::string:KEYCLOAK_LOGOUT_ROUTE)%&#039;
                session_lifetime: &#039;%env(default::int:KEYCLOAK_SESSION_LIFETIME)%&#039;
                secure_cookies: &#039;%env(default::bool:KEYCLOAK_SECURE_COOKIES)%&#039;
                http_client_options:
                    timeout: &#039;%env(default::int:KEYCLOAK_HTTP_TIMEOUT)%&#039;
                    connect_timeout: &#039;%env(default::int:KEYCLOAK_HTTP_CONNECT_TIMEOUT)%&#039;
                    verify: &#039;%env(default::bool:KEYCLOAK_HTTP_VERIFY)%&#039;

    Derafu\Auth\Contract\AuthenticationProviderInterface:
        class: Derafu\Auth\Service\KeycloakAuthenticationService

    Derafu\Auth\Contract\SessionManagerInterface:
        class: Derafu\Auth\Service\SessionService

    Derafu\Auth\Contract\RouteValidatorInterface:
        class: Derafu\Auth\Validator\RouteValidator

    Derafu\Auth\Middleware\AuthenticationMiddleware: ~

    Derafu\Auth\Controller\CallbackController:
        public: true

```




---

## Route Protection

Route protection capabilities in Derafu Auth

# Route Protection

The Derafu Auth package provides route protection through a simple array-based configuration system.

## How It Works

The `RouteValidator` class determines which routes require authentication by checking if the requested path starts with any of the configured protected routes.

```php
public function requiresAuth(string $path): bool
{
    foreach ($this-&gt;config-&gt;getProtectedRoutes() as $route) {
        if (str_starts_with($path, $route)) {
            return true;
        }
    }

    return false;
}
```

## Configuration

### Environment Variable

```env
KEYCLOAK_PROTECTED_ROUTES=&#039;[&quot;/dashboard&quot;, &quot;/profile&quot;, &quot;/admin&quot;]&#039;
```

### Default Protected Routes

If not specified, the package uses these default protected routes:

```json
[&quot;/dashboard&quot;, &quot;/profile&quot;, &quot;/admin&quot;]
```

## Route Protection Examples

### Basic Protection

Protect specific routes:

```env
KEYCLOAK_PROTECTED_ROUTES=[&quot;/dashboard&quot;, &quot;/profile&quot;, &quot;/admin&quot;]
```

This configuration will protect:

- `/dashboard` - Exact match.
- `/dashboard/settings` - Starts with `/dashboard`.
- `/profile` - Exact match.
- `/profile/edit` - Starts with `/profile`.
- `/admin` - Exact match.
- `/admin/users` - Starts with `/admin`.

### API Protection

Protect API routes:

```env
KEYCLOAK_PROTECTED_ROUTES=[&quot;/api/v1&quot;, &quot;/api/v2&quot;]
```

This will protect all routes starting with `/api/v1` or `/api/v2`:

- `/api/v1/users`
- `/api/v1/users/123`
- `/api/v2/admin`
- `/api/v2/admin/settings`

### Admin Panel Protection

Protect admin panel:

```env
KEYCLOAK_PROTECTED_ROUTES=[&quot;/admin&quot;, &quot;/moderator&quot;]
```

This protects:

- `/admin` - Admin panel
- `/admin/users` - User management
- `/admin/settings` - Settings
- `/moderator` - Moderator panel
- `/moderator/comments` - Comment moderation

## Limitations

The current implementation has the following limitations:

1. **Prefix Matching Only**: Routes are protected using `str_starts_with()`, so `/admin` will also protect `/admin-panel` and `/administer`.

2. **No Regex Support**: The package does not support regular expressions for route matching.

3. **No Wildcard Support**: There is no support for wildcard patterns like `/admin/*`.

4. **No Negative Patterns**: Cannot exclude specific routes from protection.

5. **No Role-Based Protection**: All protected routes require the same level of authentication. But you can use authorization to protect specific routes based on the user&#039;s roles.

## Special Routes

The package automatically handles these special routes:

- **Callback Route**: Default is `/auth/callback`.
- **Logout Route**: Default is `/auth/logout`.

These routes are automatically excluded from authentication requirements.

## Custom Route Validator

You can create a custom route validator by implementing the `RouteValidatorInterface`:

```php
use Derafu\Auth\Contract\RouteValidatorInterface;

class CustomRouteValidator implements RouteValidatorInterface
{
    public function requiresAuth(string $path): bool
    {
        // Your custom logic here
        return str_starts_with($path, &#039;/protected&#039;);
    }

    public function isCallbackPath(string $path): bool
    {
        return $path === &#039;/auth/callback&#039;;
    }

    public function isLogoutPath(string $path): bool
    {
        return $path === &#039;/auth/logout&#039;;
    }

    public function getProtectedRoutes(): array
    {
        return [&#039;/protected&#039;];
    }

    public function getCallbackRoute(): string
    {
        return &#039;/auth/callback&#039;;
    }

    public function getLogoutRoute(): string
    {
        return &#039;/auth/logout&#039;;
    }
}
```

Then configure it in your services:

```yaml
services:
    Derafu\Auth\Contract\RouteValidatorInterface:
        class: App\Auth\CustomRouteValidator
```




---

## Authorization

Authorization and permissions in Derafu Auth

# Authorization

Derafu Auth handles **authentication** (who you are) and helps with **authorization** (what you can do). The last one depends on the roles and permissions you set in Keycloak.

## What comes from the package?

When a user is authenticated, the package automatically provides:

- **Basic data**: `sub`, `email`, `name`, `preferred_username`, etc.
- **Roles**: Come directly from Keycloak (e.g., `user`, `admin`, `moderator`).

## What you need to implement?

### Option 1: Role-based authorization (Recommended)

**Advantages**:

- Simple to implement.
- Roles already come from Keycloak.
- No additional configuration required.

**How it works**:

1. Keycloak assigns roles to users.
2. Your application maps permissions to roles.
3. You verify if the user has the required role.

**Mapping example**:

- Permission `user:read` → Roles `user`, `admin`, `moderator`.
- Permission `user:write` → Roles `admin`, `moderator`.
- Permission `admin:access` → Only role `admin`.

### Option 2: Permission-based authorization

**Advantages**:

- More granular.
- Specific permissions per action.
- Better access control.

**Disadvantages**:

- Requires additional Keycloak configuration.
- More complex to maintain.

## Keycloak configuration for permissions

### Step 1: Create a custom scope

1. Go to **Clients** → Your Client → **Client Scopes**
2. Create a new scope called `permissions`.
3. Add it to your client.

### Step 2: Configure Token Mapper

1. In the `permissions` scope, go to **Mappers**.
2. Create a new mapper:
   - **Name**: `permissions`
   - **Mapper Type**: `User Attribute`
   - **User Attribute**: `permissions`
   - **Token Claim Name**: `permissions`
   - **Claim JSON Type**: `String`
   - **Full group path**: `false`

### Step 3: Assign permissions to users

1. Go to **Users** → Select a user → **Attributes**
2. Add the attribute:
   - **Key**: `permissions`
   - **Value**: `user:read,user:write,admin:access`

### Step 4: Configure the scope in your application

```env
KEYCLOAK_SCOPES=[&quot;openid&quot;, &quot;profile&quot;, &quot;email&quot;, &quot;permissions&quot;]
```

## How do permissions work?

### Authorization flow:

1. **User authenticates** → Keycloak generates token with permissions.
2. **Token arrives at your app** → Derafu Auth extracts the data.
3. **Your code verifies** → If the user has the required permission.
4. **Access granted/denied** → Based on the verification.

### Recommended permission structure:

```
resource:action
```

**Examples**:

- `user:read` - Read users.
- `user:write` - Create/edit users.
- `user:delete` - Delete users.
- `admin:access` - Access admin panel.
- `report:generate` - Generate reports.

## Key differences

| Aspect            | Roles         | Permissions       |
|-------------------|---------------|-------------------|
| **Configuration** | Automatic     | Requires Keycloak |
| **Granularity**   | Access groups | Specific actions  |
| **Maintenance**   | Easy          | More complex      |
| **Flexibility**   | Limited       | High              |

## Recommendation

**Start with role-based authorization** because:

- It&#039;s simpler to implement.
- No additional Keycloak configuration required.
- Sufficient for most applications.
- You can migrate to permissions later if needed.

## Summary

- **Authentication**: Handled automatically by Derafu Auth.
- **Authorization**: Implementation based on roles or permissions.
- **Roles**: Come automatically from Keycloak.
- **Permissions**: Require additional Keycloak configuration.
- **Recommendation**: Start with roles, migrate to permissions if you need more granularity.




---

## Security

Security best practices for Derafu Auth

# Security

## Environment Variables

Always use environment variables for sensitive configuration:

```env
# ✅ Good
KEYCLOAK_CLIENT_SECRET=your-secret-here

# ❌ Bad
&#039;client_secret&#039; =&gt; &#039;your-secret-here&#039;
```

## HTTPS Configuration

Enable HTTPS in production:

```env
# Development.
KEYCLOAK_SECURE_COOKIES=false
KEYCLOAK_HTTP_VERIFY=false

# Production.
KEYCLOAK_SECURE_COOKIES=true
KEYCLOAK_HTTP_VERIFY=true
```

## Session Security

```php
// Secure session configuration.
$config = new AuthConfiguration([
    &#039;session_lifetime&#039; =&gt; 3600,
    &#039;secure_cookies&#039; =&gt; true,
]);

// Additional session security that can be set.
ini_set(&#039;session.cookie_httponly&#039;, true);
ini_set(&#039;session.cookie_samesite&#039;, &#039;Lax&#039;);
ini_set(&#039;session.use_strict_mode&#039;, true);
```

## CSRF Protection

The package automatically validates the `state` parameter to prevent CSRF attacks.

## Token Security

Never store tokens in client-side storage:

```php
// ✅ Good - Server-side session.
$session-&gt;set(&#039;access_token&#039;, $accessToken);

// ❌ Bad - Never expose to client.
return new JsonResponse([&#039;token&#039; =&gt; $accessToken]);
```

## Authorization

```php
// Validate role format.
private function isValidRole(string $role): bool
{
    return preg_match(&#039;/^[a-zA-Z0-9_]+$/&#039;, $role) === 1;
}

// Check role hierarchy.
private function hasRole(array $userRoles, string $requiredRole): bool
{
    $roleHierarchy = [
        &#039;super_admin&#039; =&gt; [&#039;admin&#039;, &#039;user&#039;],
        &#039;admin&#039; =&gt; [&#039;user&#039;],
    ];

    if (in_array($requiredRole, $userRoles)) {
        return true;
    }

    foreach ($userRoles as $userRole) {
        if (isset($roleHierarchy[$userRole]) &amp;&amp;
            in_array($requiredRole, $roleHierarchy[$userRole])) {
            return true;
        }
    }

    return false;
}
```

## Error Handling

Don&#039;t expose sensitive information in production:

```php
public function handle(ServerRequestInterface $request, Throwable $exception): ResponseInterface
{
    $isProduction = getenv(&#039;APP_ENV&#039;) === &#039;production&#039;;

    if ($exception instanceof AuthenticationException) {
        return new JsonResponse([
            &#039;error&#039; =&gt; &#039;Authentication failed&#039;,
            &#039;code&#039; =&gt; &#039;UNAUTHORIZED&#039;
        ], 401);
    }

    return new JsonResponse([
        &#039;error&#039; =&gt; $isProduction ? &#039;Internal server error&#039; : $exception-&gt;getMessage(),
        &#039;code&#039; =&gt; &#039;INTERNAL_ERROR&#039;
    ], 500);
}
```

## Security Headers

```php
class SecurityHeadersMiddleware
{
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $response = $handler-&gt;handle($request);

        return $response
            -&gt;withHeader(&#039;X-Content-Type-Options&#039;, &#039;nosniff&#039;)
            -&gt;withHeader(&#039;X-Frame-Options&#039;, &#039;DENY&#039;)
            -&gt;withHeader(&#039;X-XSS-Protection&#039;, &#039;1; mode=block&#039;)
            -&gt;withHeader(&#039;Referrer-Policy&#039;, &#039;strict-origin-when-cross-origin&#039;)
            -&gt;withHeader(&#039;Content-Security-Policy&#039;, &quot;default-src &#039;self&#039;&quot;)
            -&gt;withHeader(&#039;Strict-Transport-Security&#039;, &#039;max-age=31536000; includeSubDomains&#039;);
    }
}
```

## Keycloak Security

### Client Configuration

1. **Access Type**: `confidential`.
2. **Valid Redirect URIs**: Only your application URLs.
3. **Web Origins**: Your application domain.
4. **Client Authentication**: Enabled.

### Realm Settings

1. **Password Policy**: Strong password requirements.
2. **Brute Force Detection**: Enabled.
3. **Session Timeout**: Configured appropriately.
4. **SSL Required**: `external` or `all`.




---

## Examples

Practical examples for Derafu Auth

# Examples

## Minimal Setup

```php
&lt;?php

require_once &#039;vendor/autoload.php&#039;;

use Derafu\Auth\Configuration\AuthConfiguration;
use Derafu\Auth\Service\KeycloakAuthenticationService;
use Derafu\Auth\Service\SessionService;
use Derafu\Auth\Validator\RouteValidator;
use Derafu\Auth\Middleware\AuthenticationMiddleware;

$config = new AuthConfiguration([
    &#039;keycloak_url&#039; =&gt; &#039;http://localhost:8080&#039;,
    &#039;client_id&#039; =&gt; &#039;your-client-id&#039;,
    &#039;client_secret&#039; =&gt; &#039;your-client-secret&#039;,
    &#039;redirect_uri&#039; =&gt; &#039;http://localhost/auth/callback&#039;,
]);

$authService = new KeycloakAuthenticationService($config);
$sessionService = new SessionService($config);
$routeValidator = new RouteValidator($config);

$authMiddleware = new AuthenticationMiddleware(
    $authService,
    $sessionService,
    $routeValidator
);
```

## API Examples

### REST API with Authentication

```php
class UserApiController
{
    public function getUsers(ServerRequestInterface $request): ResponseInterface
    {
        $user = $request-&gt;getAttribute(&#039;user&#039;);

        if (!$user) {
            return new JsonResponse([&#039;error&#039; =&gt; &#039;Not authenticated&#039;], 401);
        }

        if (!$this-&gt;isAuthorized($user, &#039;user:read&#039;)) {
            return new JsonResponse([&#039;error&#039; =&gt; &#039;Access denied&#039;], 403);
        }

        $users = $this-&gt;getUserList();

        return new JsonResponse([&#039;users&#039; =&gt; $users]);
    }

    private function isAuthorized(array $user, string $permission): bool
    {
        $userPermissions = $user[&#039;permissions&#039;] ?? [];

        return in_array($permission, $userPermissions);
    }
}
```

### GraphQL with Authentication

```php
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema;

class AuthenticatedGraphQLSchema
{
    public function createSchema(): Schema
    {
        $userType = new ObjectType([
            &#039;name&#039; =&gt; &#039;User&#039;,
            &#039;fields&#039; =&gt; [
                &#039;id&#039; =&gt; Type::string(),
                &#039;email&#039; =&gt; Type::string(),
                &#039;name&#039; =&gt; Type::string(),
                &#039;roles&#039; =&gt; Type::listOf(Type::string()),
            ],
        ]);

        $queryType = new ObjectType([
            &#039;name&#039; =&gt; &#039;Query&#039;,
            &#039;fields&#039; =&gt; [
                &#039;me&#039; =&gt; [
                    &#039;type&#039; =&gt; $userType,
                    &#039;resolve&#039; =&gt; function ($root, $args, $context) {
                        $user = $context[&#039;user&#039;] ?? null;

                        if (!$user) {
                            throw new \Exception(&#039;Not authenticated&#039;);
                        }

                        return $user;
                    },
                ],
            ],
        ]);

        return new Schema([
            &#039;query&#039; =&gt; $queryType,
        ]);
    }
}
```

## Testing Example

```php
use PHPUnit\Framework\TestCase;
use Derafu\Auth\Middleware\AuthenticationMiddleware;

class AuthenticationMiddlewareTest extends TestCase
{
    public function testProtectedRouteRequiresAuthentication()
    {
        $config = new AuthConfiguration([
            &#039;keycloak_url&#039; =&gt; &#039;http://localhost:8080&#039;,
            &#039;client_id&#039; =&gt; &#039;test-client&#039;,
            &#039;client_secret&#039; =&gt; &#039;test-secret&#039;,
            &#039;redirect_uri&#039; =&gt; &#039;http://localhost/auth/callback&#039;,
            &#039;protected_routes&#039; =&gt; [&#039;/dashboard&#039;],
        ]);

        $authService = $this-&gt;createMock(KeycloakAuthenticationService::class);
        $sessionService = $this-&gt;createMock(SessionService::class);
        $routeValidator = new RouteValidator($config);

        $middleware = new AuthenticationMiddleware(
            $authService,
            $sessionService,
            $routeValidator
        );

        $request = $this-&gt;createMock(ServerRequestInterface::class);
        $request-&gt;method(&#039;getUri&#039;)-&gt;willReturn(new Uri(&#039;/dashboard&#039;));

        $handler = $this-&gt;createMock(RequestHandlerInterface::class);

        $response = $middleware-&gt;process($request, $handler);

        $this-&gt;assertEquals(302, $response-&gt;getStatusCode()); // Redirect to login.
    }
}
```

## Real-World Examples

### E-commerce Application

```php
class EcommerceAuthController
{
    public function handleLogin(ServerRequestInterface $request): ResponseInterface
    {
        $user = $request-&gt;getAttribute(&#039;user&#039;);

        if (!$user) {
            return new JsonResponse([&#039;error&#039; =&gt; &#039;Not authenticated&#039;], 401);
        }

        if (!$this-&gt;hasPermission($user, &#039;shop:access&#039;)) {
            return new JsonResponse([&#039;error&#039; =&gt; &#039;Access denied&#039;], 403);
        }

        $cart = $this-&gt;getUserCart($user[&#039;sub&#039;]);

        return new JsonResponse([
            &#039;user&#039; =&gt; $user,
            &#039;cart&#039; =&gt; $cart,
            &#039;permissions&#039; =&gt; $user[&#039;permissions&#039;] ?? [],
        ]);
    }

    private function hasPermission(array $user, string $permission): bool
    {
        $permissions = $user[&#039;permissions&#039;] ?? [];

        return in_array($permission, $permissions);
    }
}
```

### Admin Panel

```php
class AdminPanelController
{
    public function handleDashboard(ServerRequestInterface $request): ResponseInterface
    {
        $user = $request-&gt;getAttribute(&#039;user&#039;);

        if (!$user) {
            return new JsonResponse([&#039;error&#039; =&gt; &#039;Not authenticated&#039;], 401);
        }

        if (!$this-&gt;isAdmin($user)) {
            return new JsonResponse([&#039;error&#039; =&gt; &#039;Access denied&#039;], 403);
        }

        $stats = $this-&gt;getAdminStats();

        return new JsonResponse([
            &#039;user&#039; =&gt; $user,
            &#039;stats&#039; =&gt; $stats,
        ]);
    }

    private function isAdmin(array $user): bool
    {
        $roles = $user[&#039;roles&#039;] ?? [];

        return in_array(&#039;admin&#039;, $roles) || in_array(&#039;super_admin&#039;, $roles);
    }
}
```





---
Last updated on 09/09/2026
#php
