dunglas / mercure

🪽 An open, easy, fast, reliable and battery-efficient solution for real-time communications
https://mercure.rocks
GNU Affero General Public License v3.0
3.98k stars 297 forks source link

Mercure with Symfony and API Platform: 401 Unauthorized Error for Real-Time Notifications #914

Open sayou opened 5 months ago

sayou commented 5 months ago

Hello everyone,

I’m working on a Symfony 6 project with API Platform where users can log in and receive JWT tokens (I use lexik_jwt_authentication) to access private pages. I’m using the Mercure protocol to send real-time updates, and I’d like to create private updates, such as notifications, for authenticated users. I’m also using MongoDB as my database.

I’ve set up my Notification entity as an API resource and configured it to use Mercure for real-time updates. Here’s the relevant configuration for my Notification entity:

// src/Document/Notification.php

namespace App\Document;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Get;
use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;
use Symfony\Component\Serializer\Annotation\Groups;

#[MongoDB\Document]
#[ApiResource(
    normalizationContext: [
        'groups' => [
            'notification:get',
        ]
    ],
    mercure: ['private' => true],
)]
#[GetCollection(
    uriTemplate: '/notifications',
    normalizationContext: [
        'groups' => [
            'notification:get',
        ]
    ],
    security: "is_granted('IS_AUTHENTICATED_FULLY')",
)]
#[Get(
    normalizationContext: [
        'groups' => [
            'notification:get',
        ]
    ],
    security: "is_granted('IS_AUTHENTICATED_FULLY') and object.getTo() === user",
)]
class Notification
{
    #[MongoDB\Id()]
    #[Groups(['notification:get'])]
    private $id;

    #[MongoDB\Field(type: 'string')]
    #[Groups(['notification:get'])]
    private $message;

    // Other fields and getter/setter methods...
}

I’ve also created a JWTCreatedListener to add Mercure claims to the JWT payload:

// src/EventListener/JWTCreatedListener.php

namespace App\EventListener;

use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent;
use Symfony\Component\Security\Core\User\UserInterface;

class JWTCreatedListener
{
    public function onJWTCreated(JWTCreatedEvent $event): void
    {
        $user = $event->getUser();
        if (!$user instanceof UserInterface) {
            return;
        }

        $payload = $event->getData();

        // Define the topics the user can subscribe to
        $topics = ["http://localhost/api/notifications/65437333e5ddba2e970a2dd2"];

        // Add Mercure claims to the payload
        $payload['mercure'] = [
            'subscribe' => $topics,
        ];

        $event->setData($payload);
    }
}

On the client side, I’m using the EventSource to include the JWT token in the request headers:

<!DOCTYPE html>
<html>
<head>
    <title>Mercure Real-Time Updates Test</title>
</head>
<body>
<h1>Real-Time Updates with JWT</h1>
<div id="update-container">
    <!-- Real-time updates will be displayed here -->
</div>

<script type="module">

    // Replace with your actual Mercure hub URL
    const mercureHubURL = 'http://localhost:8082/.well-known/mercure';

    // Replace with the actual topic URL for the resource you want to track updates for
    const topicURL = 'http://localhost/api/notifications/65437333e5ddba2e970a2dd2';
    const userJwtToken = 'your_jwt_token_here';

    const eventSource = new EventSource(`${mercureHubURL}?topic=${topicURL}`, {
        withCredentials: true,
        headers: {
            Authorization: `Bearer ${userJwtToken}`
        }
    });

    eventSource.onmessage = (event) => {
        // Handle the real-time update received from Mercure
        const updateData = JSON.parse(event.data);

        // Display the real-time update in the container
        const updateContainer = document.getElementById('update-container');
        updateContainer.innerHTML += `<p>Real-time update: ${JSON.stringify(updateData)}</p>`;

        // You can update your UI or perform any necessary actions with the updated data here
    };
</script>
</body>
</html>

Despite this setup, I’m encountering a 401 Unauthorized error when trying to receive updates:

GET http://localhost:8082/.well-known/mercure?topic=http://localhost/api/notifications/65437333e5ddba2e970a2dd2 net::ERR_ABORTED 401 (Unauthorized)

I’ve verified that the JWT token includes the correct Mercure claims and that the secret used to sign the JWT token matches the one configured in the Mercure hub.

Can anyone help me identify what might be going wrong or suggest any steps I might have missed?

Thank you in advance!