mikuso / ocpp-rpc

A Node.js client & server implementation of the WAMP-like RPC-over-websocket system defined in the OCPP-J protocols.
MIT License
98 stars 29 forks source link

Need to know how to update the authorization header and also return using Promise #41

Closed ahsanaasim closed 1 year ago

ahsanaasim commented 1 year ago

I have just started playing with this awesome library. I have started from scratch. Here, I am trying to implement the authorization. I want to follow the guideline from the OCPP guidelines. Can you guide me or send a sample code to implement this?

I have explained the problem I am facing in this video. https://www.loom.com/share/9c39b4204b824458bd01ba1b27fb42b9

In short, first, I need to know if there is a way to return using the promise from the central system and secondly, how can I change the authorization key of the client object? Please watch the video above to understand clearly..

This is the flow that I am trying to implement.

Screenshot 2023-05-17 at 6 03 16 PM
mikuso commented 1 year ago

Hi Ahsan, thanks for taking the time to put together this video.

I think I understand what you are trying to do.

Firstly, it appears that you are trying to implement HTTP Basic authentication (as per OCPP1.6-J and OCPP1.6 security whitepaper (security profile 1 & 2)). You'll be happy to know that this module supports this feature natively!

Note: The OCPP "auth key" is considered to be a "password" by the HTTP Basic Auth spec, so you'll see this library often referring to a "password" instead of "authorization key" or "authentication key" - but just know that these terms are all synonymous.

To take advantage of the module's support for this authentication scheme - don't set the Authorization header directly; Use the password option in the RPCClient constructor instead. The password option will ensure that the authorization header is set correctly (- even allowing you to use binary Buffer's as auth keys for maximum entropy if you want to; which seems to be encouraged by the OCPP spec, although discouraged by HTTP Basic Auth spec).

There are a few other quirks about how OCPP uses HTTP auth which you can read at some length at the bottom of the readme, but if you stick to using the password field, then you should not need to worry about any of them. (I recommend reading the linked section on OCPP Security Profile 1 if you haven't already.)

So, setting the auth key should be as simple as adapting your code as follows:

// in ChargerPoint.ts (line 19)
this.cli = new RPCClient({
  endpoint: endpoint,
  identity: identity,
  protocols: ['ocpp1.6'],
  password: this.authKey
});

Then, when you are handling the authentication on the server side, you should compare your auth key against the client.handshake.password. Note: The password will be received by your server as a Buffer - so if you want to perform a string comparison, you should do so like this:

// in server.ts (line 48)
if (client.handshake.password.toString('utf8') == 'golalu') {

Also keep in mind that the password may be undefined if the client does not specify a password, so you may want to use optional chaining for safety, like so:

if (client.handshake.password?.toString('utf8') == 'golalu') {

Finally, to change the RPCClient's password for the next time it connects, you can use the reconfigure() method like so:

// in ChargerPoint.ts (line 54)
this.cli.reconfigure({password: config.value});

I hope this helps.

ahsanaasim commented 1 year ago

Hi. Thanks for your detailed reply. I followed your steps and it worked. Really appreciate your detailed and fast response.

I had another question in the video.

OCPP specification states that the central system should return the boot notification status as "pending" for an unauthorized password and then initiate a configuration change request.

However, in the current implementation, I am required to return the boot notification message. Unfortunately, after the return line, no further execution takes place, making it impossible to trigger the config change message as per the documentation.

I would like to explore if there is any alternative approach or solution available that aligns with the specifications outlined in the documentation.

Here is my current code

const response: IBootNotificationResponse = {
            status: BootStatus.Pending,
            interval: 300,
            currentTime: new Date().toISOString(),
          };

          this.makeChangeConfigurationRequest(client, { key: 'AuthorizationKey', value: 'golalu' });
          return response;
ahsanaasim commented 1 year ago

So that means I am calling the makeChangeConfigurationRequest before returning the boot notification response. and that is not aligned with the ocpp guidelines.

Screenshot 2023-05-17 at 9 51 43 PM
mikuso commented 1 year ago

Ah yes, I admit this is a bit clunky to handle. I think this is something which may see a change in a future major revision.

You can work around this by using the alternative reply() callback which is provided as part of the object passed to the handle() callback. This behaves just the same as returning the response. Once the reply callback has been called, you no longer need to worry about your return value, and are free to do whatever else you need to do in your handler.

The general example is as follows:

client.handle('BootNotification', async ({reply}) => {
    reply({
        status: 'Pending',
        interval: 300,
        currentTime: new Date().toISOString(),
    });

    // The BootNotification response has now been handled.
    // It no longer matters what we return from this handler, as the response has already been queued.

    await client.call('ChangeConfiguration', { /* params */ });
});
ahsanaasim commented 1 year ago

Okay, it worked. But I had to add a delay to make sure that the boot notification is sent first.

here is my updated code.

const response: IBootNotificationResponse = {
            status: BootStatus.Pending,
            interval: 300,
            currentTime: new Date().toISOString(),
          };
          body.reply(response);
          setTimeout(() => {
            this.makeChangeConfigurationRequest(client, { key: 'AuthorizationKey', value: 'golalu' });
          }, 4000);

I will continue to explore and experiment with this further. If any specific issues arise, I will create separate issue. Thanks for your help.