Chapters
-
Course Code
Subscribe to download the code!
Subscribe to download the code!
-
This Video
Subscribe to download the video!
Subscribe to download the video!
-
Subtitles
Subscribe to download the subtitles!
Subscribe to download the subtitles!
-
Course Script
Subscribe to download the script!
Subscribe to download the script!
Mercure Hub's JWT Authorization
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot in the video!
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeCan anyone publish a message to any topic on a Mercure hub? Definitely not. So how does the Mercure Hub know that we are allowed to publish this message? It's entirely thanks to this long string that we're passing to the Authorization
header.
Where does this come from? It turns out, it's a JSON web token. Copy that huge string... then head over to jwt.io: a lovely site for working with JSON web tokens - or JWT's. If you're familiar with how JWT's work, awesome. If not, here's a little primer.
A JWT + Mercure Primer
Scroll down a bit to find a JWT editor. Paste in the encoded token. So this weird string here can actually be decoded to this JSON. And... you don't need a secret key to do it: the long string is basically just a base64 encoded version of this JSON. Anyone can turn this string into this JSON.
So when we send this long string to the server, what we're really sending is this JSON data. For us, the subscribe
part isn't important... and neither is the payload
. But the publish
part is important. This basically says:
Hi Mercure Hub! Guess what! I have permission to publish to any topic. Cool, huh?
Ok... but why does the Mercure server trust this? Can't anyone create a JSON web token that claims that they can publish to all topics? Yea! But those wouldn't be signed correctly unless they have the "secret".
When you run a Mercure Hub, you give it a "secret" value... which, by default - and for our Mercure Hub - is !ChangeMe!
. This is the value that you see in our .env
file.
Back over on jwt.io
, look at the bottom. It says "invalid signature". When a JWT is created, it's signed by a secret key. When someone uses a JWT, after decoding it - which anyone can do - they are then supposed to verify the signature of the token. Right now, it's trying to verify the signature of our JWT... but using the wrong secret. If we paste in our real secret instead... it's verified!
This can... be a bit technical. The point is this: in order to generate a JWT that will have a valid signature, you need the secret. And while anyone can read a JWT, if you mess around with its contents, the signature will fail. That's why the Mercure Hub trusts us when we send a JWT that says we can publish to any topic: the signature of our message is valid. That means it was generated by someone who has the secret key.
Check this out: let's regenerate this JWT using the same "payload" but signed using the wrong secret... something a bad user might try to do. Copy the new JWT... update the curl
command in our scratchpad... copy the whole command... and paste it into the terminal. Hit enter. Unauthorized! The Mercure Hub can totally read the JSON in this message, but it sees that the signature failed and does not publish the message.
Change back to our old key in the scratch pad. And at the browser, use the correct secret: !ChangeMe!
.
Simplifying the Payload
To simplify things, change the payload to just the part we need. So remove the subscribe
part - we're not trying to get access to subscribe to anything - and also remove payload
. This is all we really need: some JSON that claims that we can publish to any topic signed with the correct secret. If you ever need to create a JWT by hand, this is how you do it: create the JSON you want and have something - like this site - sign it with your secret.
Copy the new, shorter JWT... and paste it in our scratchpad. Copy the entire command, paste it at your terminal and... yes! It works! In our browser, the listening tab shows a second message.
Publishing a Turbo Stream
Enough about authorization & JWT. In the real world, as long as we have the correct MERCURE_SECRET
configured in our app, all of this will be handled automatically thanks to the Mercure PHP library. Internally, it will use the secret to generate the signed JWT for us.
But before we start publishing messages from our code, let's look closer at the data
POST parameter. So far, we've been sending JSON. And, in theory, we could write some JavaScript that listens to this topic and does something with that JSON. But remember: the turbo_stream_listen()
function activates a Stimulus controller that is already listening to this topic. It's listening and waiting for a message whose data isn't JSON, but <turbo-stream>
HTML.
Check it out: over in our scratch pad, instead of setting the data
to JSON, I'll paste in a turbo stream. It's a little ugly because it's all on one line, but it's valid: action="update
, target="product-quick-stats"
with some dummy content inside.
Let's first see if this message shows up inside our browser tab. Oh! It actually stopped listening. It probably hit a listening timeout - that's something you can configure or disable in Mercure. I'll refresh.
Now, go copy the command... find your terminal, paste, hit enter... and head back to the browser. No surprise: here's our message with the Turbo Stream HTML. But the really cool thing is back on our site. Scroll up. Yes! It updated the quick stats area! As soon as we published the message, the JavaScript from the Stimulus controller saw the message and passed the turbo-stream HTML to the stream-processing system. That's so cool.
Of course, we aren't normally going to publish via the command line & curl: we're going to publish messages via PHP... which is way easier. Let's do that next.
4 Comments
Hey @Sucram!
It's been awhile since I've looked a this, but, indeed, this little bit might be missing. https://github.com/symfony/ux/issues/291 looks related.
Cheers and sorry I can't give you a better answer!
Ryan
first of all, thanks for these great videos Ryan!
for me doesn't work as only simple DIV
<div {{ turbo_stream_listen('product-reviews') }}></div>
I had to create an internal child div to replace it
<div {{ turbo_stream_listen('product-reviews') }}>
<div id="FOO"></div>
</div>
with the following mercure message
// topic product-reviews
<turbo-stream action="update" target="FOO"><template>asdddddddddddddddasdasdasd</template></turbo-stream>
Hey @MGDSoft!
Hmm. Ok, so this DOES make sense. This line can be a bit misleading:
<div {{ turbo_stream_listen('product-reviews') }}>
This line basically says:
I want some JavaScript that is listening to the "product-reviews" Mercure topic for "stream" updates.
The weird thing about this line is that it doesn't affect the div it's attached to at all. You could put this anywhere on the page: it's just a directive to "globally listen" to that topic.
So, now you are listening to the product-reviews
Mercure topic for updates. So, in your code, you can now post an "update" to Mercure containing Turbo Stream directions to update any elements on your page (at this point, it doesn't matter at all what original element you added the {{ turbo_stream_listen('product-reviews') }}
to. So, when you return this:
// topic product-reviews
<turbo-stream action="update" target="FOO"><template>asdddddddddddddddasdasdasd</template></turbo-stream>
That will tell the JavaScript on your page that is listening to the product-reviews
channel to find the id="foo"
element and update the contents. The fact that this JavaScript is running thanks to being attached to its parent <div>
makes no differences at this point.
Does that help? Or does it confuse more :p?
Cheers!
"Houston: no signs of life"
Start the conversation!
What PHP libraries does this tutorial use?
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"composer/package-versions-deprecated": "1.11.99.1", // 1.11.99.1
"doctrine/annotations": "^1.0", // 1.13.1
"doctrine/doctrine-bundle": "^2.2", // 2.3.2
"doctrine/orm": "^2.8", // 2.9.1
"phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
"sensio/framework-extra-bundle": "^6.1", // v6.1.4
"symfony/asset": "5.3.*", // v5.3.0-RC1
"symfony/console": "5.3.*", // v5.3.0-RC1
"symfony/dotenv": "5.3.*", // v5.3.0-RC1
"symfony/flex": "^1.3.1", // v1.21.6
"symfony/form": "5.3.*", // v5.3.0-RC1
"symfony/framework-bundle": "5.3.*", // v5.3.0-RC1
"symfony/property-access": "5.3.*", // v5.3.0-RC1
"symfony/property-info": "5.3.*", // v5.3.0-RC1
"symfony/proxy-manager-bridge": "5.3.*", // v5.3.0-RC1
"symfony/runtime": "5.3.*", // v5.3.0-RC1
"symfony/security-bundle": "5.3.*", // v5.3.0-RC1
"symfony/serializer": "5.3.*", // v5.3.0-RC1
"symfony/twig-bundle": "5.3.*", // v5.3.0-RC1
"symfony/ux-chartjs": "^1.1", // v1.3.0
"symfony/ux-turbo": "^1.3", // v1.3.0
"symfony/ux-turbo-mercure": "^1.3", // v1.3.0
"symfony/validator": "5.3.*", // v5.3.0-RC1
"symfony/webpack-encore-bundle": "^1.9", // v1.11.2
"symfony/yaml": "5.3.*", // v5.3.0-RC1
"twig/extra-bundle": "^2.12|^3.0", // v3.3.1
"twig/intl-extra": "^3.2", // v3.3.0
"twig/string-extra": "^3.3", // v3.3.1
"twig/twig": "^2.12|^3.0" // v3.3.2
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.0
"symfony/debug-bundle": "^5.2", // v5.3.0-RC1
"symfony/maker-bundle": "^1.27", // v1.31.1
"symfony/monolog-bundle": "^3.0", // v3.7.0
"symfony/stopwatch": "^5.2", // v5.3.0-RC1
"symfony/var-dumper": "^5.2", // v5.3.0-RC1
"symfony/web-profiler-bundle": "^5.2", // v5.3.0-RC1
"zenstruck/foundry": "^1.10" // v1.10.0
}
}
What JavaScript libraries does this tutorial use?
// package.json
{
"devDependencies": {
"@babel/preset-react": "^7.0.0", // 7.13.13
"@fortawesome/fontawesome-free": "^5.15.3", // 5.15.3
"@hotwired/turbo": "^7.0.0-beta.5", // 1.2.6
"@popperjs/core": "^2.9.1", // 2.9.2
"@symfony/stimulus-bridge": "^2.0.0", // 2.1.0
"@symfony/ux-chartjs": "file:vendor/symfony/ux-chartjs/Resources/assets", // 1.1.0
"@symfony/ux-turbo": "file:vendor/symfony/ux-turbo/Resources/assets", // 0.1.0
"@symfony/ux-turbo-mercure": "file:vendor/symfony/ux-turbo-mercure/Resources/assets", // 0.1.0
"@symfony/webpack-encore": "^1.0.0", // 1.3.0
"bootstrap": "^5.0.0-beta2", // 5.0.1
"chart.js": "^2.9.4",
"core-js": "^3.0.0", // 3.13.0
"jquery": "^3.6.0", // 3.6.0
"react": "^17.0.1", // 17.0.2
"react-dom": "^17.0.1", // 17.0.2
"regenerator-runtime": "^0.13.2", // 0.13.7
"stimulus": "^2.0.0", // 2.0.0
"stimulus-autocomplete": "https://github.com/weaverryan/stimulus-autocomplete#toggle-event-always-dist", // 2.0.0
"stimulus-use": "^0.24.0-1", // 0.24.0-2
"sweetalert2": "^11.0.8", // 11.0.12
"webpack-bundle-analyzer": "^4.4.0", // 4.4.2
"webpack-notifier": "^1.6.0" // 1.13.0
}
}
Hi, I'm wondering about the template function
turbo_stream_listen
and authorization.I have not set the anonymous directive within the mercure config.
The php publish method is using the configured jwt and the message is published. Whereas on the listening part I always get a 401 Unauthorized as soon as the EventSource wants to connect.
I have checked the stimulus controller within the ux-turbo package but I found only this connect method:
and I'm wondering where the jwt stuff is handled for subscribing to topics?