Skip to content

User Bundle

The Roadiz User Bundle provides a comprehensive public user management system for your Roadiz application. It handles user registration, authentication, password reset, and account management for front-end users.

Features

  • User Registration: Sign-up functionality with email validation
  • Authentication: Traditional password-based and passwordless login options
  • Password Management: Password reset and change functionality
  • Email Validation: Token-based email verification system
  • Login Links: Passwordless authentication via email magic links
  • Rate Limiting: Built-in protection against brute-force attacks
  • User Roles: Flexible role-based access control for public users

Installation

Install the bundle using Composer:

bash
composer require roadiz/user-bundle

If you're not using Symfony Flex, you'll need to manually enable the bundle in config/bundles.php:

php
// config/bundles.php

return [
    // ...
    \RZ\Roadiz\UserBundle\RoadizUserBundle::class => ['all' => true],
];

Configuration

Step 1: Copy API Platform Resources

Copy the API Platform resource configurations to your project:

bash
cp vendor/roadiz/user-bundle/config/api_resources/user.yaml config/api_resources/
cp vendor/roadiz/user-bundle/config/api_resources/me.yaml config/api_resources/

Step 2: Configure Rate Limiters

Add rate limiter configuration to config/packages/framework.yaml:

yaml
# config/packages/framework.yaml
framework:
    rate_limiter:
        user_signup:
            policy: 'token_bucket'
            limit: 5
            rate: { interval: '1 minutes', amount: 3 }
            cache_pool: 'cache.user_signup_limiter'
        password_request:
            policy: 'token_bucket'
            limit: 3
            rate: { interval: '1 minutes', amount: 3 }
            cache_pool: 'cache.password_request_limiter'
        password_reset:
            policy: 'token_bucket'
            limit: 3
            rate: { interval: '1 minutes', amount: 3 }
            cache_pool: 'cache.password_reset_limiter'

Step 3: Configure Cache Pools

Add cache pools in config/packages/cache.yaml:

yaml
# config/packages/cache.yaml
framework:
    cache:
        pools:
            cache.user_signup_limiter: ~
            cache.password_request_limiter: ~
            cache.password_reset_limiter: ~

Step 4: Configure Security Access Control

Update config/packages/security.yaml to define public and protected routes:

yaml
# config/packages/security.yaml
security:
    access_control:
        # Prepend user routes configuration before API Platform ones
        # Public routes must be defined before protected ones
        - { path: "^/api/users/login_link_check", methods: [ POST ], roles: PUBLIC_ACCESS }
        - { path: "^/api/users/login_link", methods: [ POST ], roles: PUBLIC_ACCESS }
        - { path: "^/api/users/signup", methods: [ POST ], roles: PUBLIC_ACCESS }
        - { path: "^/api/users/password_request", methods: [ POST ], roles: PUBLIC_ACCESS }
        - { path: "^/api/users/password_reset", methods: [ PUT ], roles: PUBLIC_ACCESS }
        # Protected routes
        - { path: "^/api", roles: ROLE_BACKEND_USER, methods: [ POST, PUT, PATCH, DELETE ] }
        - { path: "^/api/users", methods: [ GET, PUT, PATCH, POST ], roles: ROLE_USER }

Step 5: Configure Environment Variables

Add the following to your .env or .env.local file:

dotenv
# User bundle configuration
USER_PASSWORD_RESET_URL=https://your-public-url.test/reset
USER_VALIDATION_URL=https://your-public-url.test/validate
USER_PASSWORD_RESET_EXPIRES_IN=600
USER_VALIDATION_EXPIRES_IN=3600

Replace the URLs with your actual front-end application URLs where users will complete password reset and email validation.

Step 6: Configure CORS

Update CORS configuration to allow required headers:

yaml
# config/packages/nelmio_cors.yaml
nelmio_cors:
    defaults:
        allow_headers: ['Content-Type', 'Authorization', 'Www-Authenticate', 'x-g-recaptcha-response']
        expose_headers: ['Link', 'Www-Authenticate']

User Roles

The bundle provides the following user roles:

  • ROLE_PUBLIC_USER: Default role for all public users
  • ROLE_PASSWORDLESS_USER: Role for users authenticated via login link
  • ROLE_EMAIL_VALIDATED: Role assigned after email validation

Passwordless Authentication

The bundle supports passwordless authentication using login links sent via email.

Configuration

  1. Add the login link check route to config/routes.yaml:
yaml
# config/routes.yaml
public_login_link_check:
    path: /api/users/login_link_check
    methods: [POST]
  1. Configure the security firewall in config/packages/security.yaml:
yaml
# config/packages/security.yaml
security:
    providers:
        # Combined provider for both backend and public users
        all_users:
            chain:
                providers: ['roadiz_user_provider', 'roadiz_backend_user_provider']
    
    firewalls:
        api:
            pattern: ^/api
            stateless: true
            provider: all_users
            jwt: ~
            login_link:
                check_route: public_login_link_check
                check_post_only: true
                success_handler: lexik_jwt_authentication.handler.authentication_success
                failure_handler: lexik_jwt_authentication.handler.authentication_failure
                signature_properties: [ 'email' ]
                lifetime: 600
                max_uses: 3

Create a controller to handle login link requests:

php
<?php
// src/Controller/SecurityController.php

declare(strict_types=1);

namespace App\Controller;

use RZ\Roadiz\CoreBundle\Repository\UserRepository;
use RZ\Roadiz\CoreBundle\Security\LoginLink\LoginLinkSenderInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface;

final readonly class SecurityController
{
    public function __construct(
        private LoginLinkSenderInterface $loginLinkSender,
    ) {
    }
    
    #[Route('/api/users/login_link', name: 'public_user_login_link_request', methods: ['POST'])]
    public function requestLoginLink(
        LoginLinkHandlerInterface $loginLinkHandler,
        UserRepository $userRepository,
        Request $request
    ): Response {
        $email = $request->getPayload()->get('email');
        $user = $userRepository->findOneBy(['email' => $email]);

        if (null === $user) {
            // Do not leak if a user exists or not
            return new JsonResponse(null, Response::HTTP_NO_CONTENT);
        }

        $loginLinkDetails = $loginLinkHandler->createLoginLink($user, $request);
        $this->loginLinkSender->sendLoginLink($user, $loginLinkDetails);
        
        return new JsonResponse(null, Response::HTTP_NO_CONTENT);
    }
}

Register the controller in config/services.yaml:

yaml
# config/services.yaml
services:
    App\Controller\SecurityController:
        tags: [ 'controller.service_arguments' ]

To use a different base URL for login links (e.g., your front-end application):

yaml
# config/services.yaml
services:
    RZ\Roadiz\UserBundle\Security\FrontendLoginLinkHandler:
        decorates: Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface
        arguments:
            $decorated: '@RZ\Roadiz\UserBundle\Security\FrontendLoginLinkHandler.inner'
            $frontendLoginCheckRoute: '%frontend_login_check_route%'
            $frontendLoginLinkRequestRoutes:
                - 'frontend_user_login_link_request'
                - 'public_user_login_link_request'
                - 'api_user_signup'
            $signatureHasher: '@security.authenticator.login_link_signature_hasher.api_login_link'

Maintenance Commands

The bundle provides useful maintenance commands:

Purge Expired Validation Tokens

Remove expired user validation tokens:

bash
bin/console users:purge-validation-tokens

Remove Inactive Users

Delete inactive users who haven't logged in for a specified period:

bash
# Delete public users inactive for 60 days without ROLE_EMAIL_VALIDATED
bin/console users:inactive -d 60 -r ROLE_PUBLIC_USER -m ROLE_EMAIL_VALIDATED -v

Options:

  • -d, --days: Number of days of inactivity (default: 60)
  • -r, --role: Filter by role (can be used multiple times)
  • -m, --missing-role: Filter by missing role
  • -v: Dry run (display users without deleting)

TIP

Run maintenance commands regularly using cron jobs or Symfony Scheduler to keep your user database clean.

API Endpoints

The bundle provides the following API endpoints:

  • POST /api/users/signup: User registration
  • POST /api/users/login_link: Request login link
  • POST /api/users/login_link_check: Verify login link
  • POST /api/users/password_request: Request password reset
  • PUT /api/users/password_reset: Reset password with token
  • GET /api/users/me: Get current user information
  • GET /api/users/{id}: Get specific user (requires authentication)
  • PUT /api/users/{id}: Update user information
  • PATCH /api/users/{id}: Partially update user

Security Considerations

WARNING

  • Always use HTTPS in production
  • Configure appropriate rate limits based on your traffic
  • Set reasonable token expiration times
  • Regularly purge expired tokens and inactive users
  • Validate email addresses to prevent spam registrations
  • Consider implementing CAPTCHA for registration and login endpoints

Troubleshooting

Users Not Receiving Emails

Check:

  1. Mailer configuration in config/packages/mailer.yaml
  2. Environment variables for SMTP settings
  3. Email logs and queue

Rate Limiting Issues

If legitimate users are being rate-limited:

  1. Adjust rate limiter settings in framework.yaml
  2. Check cache pool configuration
  3. Consider using different rate limits for different environments

Verify:

  1. Login link handler is properly configured
  2. Routes are registered correctly
  3. Token lifetime is appropriate
  4. Front-end URL configuration matches your application

More Information

For detailed API documentation, refer to the auto-generated OpenAPI documentation available in your Roadiz application at /api/docs.