What recommendations can you give to implement Web Push Notifications in Lucee?

I have been working this month to implement several features to modernize an old website. Implementing WebSockets, for administrator dashboard notifications, was my first feature, and now I am working to integrate Web Push Notifications for sending important messages to administrators and customers who are no longer on the website. I have the basics complete, but there isn’t a CFML Component that I can find to help with the server side post to the browser specific endpoint.

Useful resources I am reading on the topic:

I am struggling most with Encryption (aes128gcm), implementing VAPID and the Javascript Web Token (JWT).

For the encryption, you’ll have to go straight Java because the GCM cipher is not yet implemented in Lucee (https://luceeserver.atlassian.net/browse/LDEV-904) or ACF (https://tracker.adobe.com/#/view/CF-4168837). Unfortunately, I don’t remember where I put the code I was playing with for doing GCM directly in Java from 2016, else I’d give that to you to work with.

A Google search (e.g. encrypt decrypt gcm cipher java) should easily return many examples of GCM in Java, however, which is how I figured out how to do it. That’s going to be one part of your problem.

JWT you have one of the best resources on right there from Ben Nadel. Aside from the encryption used in his example, the remainder should be pretty much straight forward. You didn’t really elaborate on which parts you aren’t grasping, so I can’t really give you much more of an answer.

As for VAPID, if my reading is correct then its simply creating a JWT with some specific data requirements. https://blog.mozilla.org/services/2016/04/04/using-vapid-with-webpush/
But, again, you didn’t elaborate on what part of VAPID you are having problems understanding, so can’t be much help to you there either.

But no, there probably isn’t a magic bullet (component) for this one. I’ve not heard of many CFers venturing out into web push directly from CF yet. Most people nowadays just use an API that does the push for them (OneSignal, for example). But, if you manage to cobble something together that provides the functionality you need into a component (or set of components), please do feel free to share it on GitHub should anyone else run into a situation where your component(s) would be handy.

Sorry I can’t be of more help, but I hope this helps address some of your questions.

– Denny

It was my wishful thinking showing through, that the GCM Cipher was a hidden feature of CFML Lucee Script :wink:

As for the specifics, I am stuck at the point Authentication where I am requesting the endpoint URL with the payload:

Such as:
https://updates.push.services.mozilla.com/wpush/v2/gAAAAABeJhSXGL97QYUMh

When I make the cfhttp request I get an error stating that states:

code = 401
errno = 109
error = Unauthorized
more_info = http://autopush.readthedocs.io/en/latest/http.html#error-codes
message = Request did not validate missing authorization header

Every which way I format the Authentication JWT I just end up with the same error.

I started out by using nodejs to create a VAPID public key and secret key and I think that was the wrong method, or, it is done using the gcm encryption and I don’t have that so I fail due to that issue.

Hey Jonathan,

Sorry for the late reply. If GCM is a requirement for the process you’re trying to undertake, then yes it can be the root stumbling block for your situation. Based on the response you’re getting, you’re either not sending the http authorization header as they’ve requested, or it fails to validate as valid authorization…

If GCM is the requirement however, CFML simply runs in an engine that sits on top of the JVM and lets you drop down into Java as needed, so putting together a GCM implementation is not impossible, its simply a little more challenging than just writing an encrypt() CFML statement.

You should be adding a header to your http request for bearer authentication, so something along the lines of:

http.addParam( 
    type = 'header', 
    name = 'Authorization', 
    value = 'Bearer ' & [signedValueOrKey] 
);

The VAPID adds an additional header (as documented in the link I sent you) to consider - I’m unclear at what point the self-identification piece comes into play, but I would assume at the same point when you’re trying to produce the web push.

HTH

– Denny

I was able to dump a NodeJS request to use as a formatting example. It has the wrong vapid key for theso it is not a truly valid request but it at the very least helps me know what a properly formatted request would look like.

After installing NodeJS and implementing webpush library from github they suggest this example code:

web-push send-notification --endpoint=<url> [--key=<browser key>] [--auth=<auth secret>] [--payload=<message>] [--encoding=<aesgcm | aes128gcm>] [--ttl=<seconds>] [--vapid-subject=<vapid subject>] [--vapid-pubkey=<public key url base64>] [--vapid-pvtkey=<private key url base64>] [--gcm-api-key=<api key>]

Using and end point like this:

{
  "keys": {
    "auth": "onlL99713-BCAoL_DPhvJQ",
    "p256dh": "BL-i_oiFNrnPKF-YOS5nYVnbuk6rr08N5dw3jt6Y54wnNYzNBh-ZAVq2irQO-MevL98UA_Hs38dvCXiCuqiO7_4"
  },
  "endpoint": "https://dm3p.notify.windows.com/w/?token=BQYAAAD3J7bC6KD6jAlhEl2QLnmI8ErHyxNWInjTTebaLQOFAjXiYe%2fy57v2vulXaThEdb8CV2TIB12FPLzBgY9P5WdbujcjRbjMNUwd5WWBDb%2bb8iVF0Omc7odUOgG8v%2fPsTPQxXjLMB7KSjETyA5tbefP2%2bDo5Dz0xxqHD7JOzjibgRbLFaFIhWxVPyy4hQ3KOzXfpudIPS2xc4XK1CC9tYKX%2fLQgXjAeY7lndGFPlpE%2fnPX9IXcf6pCW%2fCqGEEjA3wVP9AK1XOxrIbeG544%2fB3sYstT8SMAhq94Fdi5FFZyMFMN%2f0n5l9fFy6kh9XjRgLp6piCxIZ4sqzrUFF%2b3uJh1tq",
  "expirationTime": null
}

I tried this test (substituting my server as the target endpoint address “www.example.com/jonathan/test4” … no, I do not own example.com)

web-push send-notification --endpoint=https://www.example.com/jonathan/test4 --key=QkZNOWpIMlctWW1CUlBZenFROUMyZTh0eFM3MzQ4YklBWWpRcFY4WkN0YWNUNXl2Zk1BMUgwMEQyS1dfaXJUbUMydjE2eUpDSTZiU3BTbjhyNWR4S0hj --auth=NnBMQlVHQzRUQlZTb19tVTc4cFJGQQ --payload=aGVsbG8gc3RyaW5nIGlzIGhlcmU --encoding=aes128gcm --ttl=500 --vapid-subject=https://www.example.com --vapid-pubkey=QkZTanVvNlpScEFyUExfR1pmRW5vVERkcUJEaERsQkZGcVI2ZWVYQ29ObVdfcHRjU1Y4RTBMN2MxT3ExWE16eHZyLUhhcGJGdHA1X1BmQkNhenhLSzc0 --vapid-pvtkey=LW9UdHZja2xsdDdmTFN1M09wY3ZGVFlHQ01naUlpdGNqc2ZsZzUzbFJmcw > result.json

NodeJS made a request like this:

{
"method": "POST",
"content": "OEUM2wiIWk8pGwlnkkJOpQAAEABBBM3YebNcTwW2e4xVoXD0xdR4hn5O7mPZUQWwdvT38oiLsnhKB95Noqd4HCOBM8wh76ML9rP3DrpgeJfTQMELUjsD3nWBbP3DeEcsvEKfoAoAwDITC3tqSMptINKbl1XFNRrkrOtT",
    "protocol": "HTTP/1.1",
    "headers": {
        "x-tomcat-docroot": "/var/www/example.com/webroot",
        "x-forwarded-host": "www.example.com",
        "connection": "close",
        "host": "www.example.com",
        "ttl": "500",
        "content-type": "application/octet-stream",
        "content-encoding": "aes128gcm",
        "authorization": "vapid t=eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL3d3dy5leHBlcnRhdXRvY2hlY2suY29tIiwiZXhwIjoxNTgwMTIwODQ3LCJzdWIiOiJodHRwczovL3d3dy5leHBlcnRhdXRvY2hlY2suY29tIn0.g1yENA-RpPVDeWhg3xe5VBQflTErz8aSXTOBMb2txGyBp5-S2pODFBARLVdlzuqrvHQfCSfd_Xj-_5w3ZfNkTg, k=BFSjuo6ZRpArPL_GZfEnoTDdqBDhDlBFFqR6eeXCoNmW_ptcSV8E0L7c1Oq1XMzxvr-HapbFtp5_PfBCazxKK74",
        "content-length": "123"
    }
}