Alethio / explorer-core-plugins

Alethio Explorer core plugins that provide basic functionality

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Suggestion for eth-lite: add support for authentication on JSON-RPC

adetante opened this issue · comments

Hyperledger Besu supports user-password authentication on JSON-RPC endpoints, which is useful in some cases of private chains.

Is it interesting to add support for this authentication mode in the eth-lite plugin?

I've started a first implementation draft here: https://github.com/adetante/explorer-core-plugins.

This version of the plugin requires configuration changes in config.json:

  • Add the withauthentication: true plugin configuration
    "plugins": [
      ...,
      {
        "uri": "plugin://aleth.io/eth-lite",
        "config": {
            "nodeUrl": "http://my_besu_node:8545",
            "withAuthentication": true
        }
    }],
  • Add the auth module in dashboard page (this module displays the login form)
"pages": [
  ...,
  {
    "def": "page://aleth.io/dashboard",
    "children": {
        "content": [
            { "def": "module://aleth.io/lite/auth" },
            ...
        ]
    }
  }
}

Yes, we should support custom authentication methods per node, but I think most of the functionality should be implemented in a separate plugin. From what I saw in the Besu docs and your implementation, the authentication method used is specific to Besu and will most likely not be of any use to other types of nodes.

While most of the code to support this authentication can be included in a new plugin, we do need a method of hooking to the web3 initialization that happens in eth-lite, in order to add the authentication headers. This being said, I propose the following changes in the eth-lite plugin:

  1. Add a configuration key authStoreUri that points to a custom auth adapter responsible for applying any needed modifications to the web3 provider

Example:

    "plugins": [
      ...,
      {
        "uri": "plugin://aleth.io/eth-lite",
        "config": {
            "nodeUrl": "http://my_besu_node:8545",
            "authStoreUri": "adapter://adetante/besu/auth-store"
        }
    }],

The AuthStore will have to implement the following interface:

interface IAuthStore {
    // This needs to be an @observable (will work b/c mobx instance is shared between plugins)
    // Alternatively we could use a more complicated observer pattern implementation
    isAuthenticated: boolean;
    // Alternatively we could pass just the provider, instead of the entire web3eth object
    applyLoginInfo(web3Eth: Web3Eth): void;
}
  1. The web3 initialization code would be changed along the lines of this pseudo-implementation:
const authStore = await adapters.get("adapter://adetante/besu/auth-store").load();
when(() => authStore.isAuthenticated, () => {
    this.lastBlockWatcher.watch();
    authStore.applyLoginInfo(web3Eth);
})

There are a few gotchas though:

  • We need to fetch the adapter by its URI (this also means the besu plugin needs to be included BEFORE the eth-lite plugin, but that's ok). That means a data dependency between plugins, which I'm not sure is possible right now. I'll have to do some digging, we might have to make a few modifications to the plugin engine. I'll get back to you on that.
  • The when part needs to block/pause the initialization of the eth-lite data source, otherwise we'll get errors everywhere.

The besu plugin implementation could look like this:

import { IDataAdapter } from "plugin-api";
class AuthAdapter implements IDataAdapter {
    constructor(private authStore: AuthStore) {}
    async load() {
        return this.authStore;
    }
}

class AuthStore {
    @observable
    isAuthenticated: boolean;
    applyLoginInfo(web3Eth: any) {
        web3Eth.currentProvider.headers = [{
            name: "Authorization",
            value: `Bearer ${this.jwtToken}`
        }];
    }
    //...
}

interface IAuthProps {
    authStore: AuthStore;
}
const authModule: (authStore: Web3AuthStore) => IModuleDef<IAuthProps, {}, void> =
    (authStore) =>
        ({
            contextType: {},
            slotNames: [],
            dataAdapters: [],

            getContentComponent: () => import("./Auth").then(({ Auth }) => Auth),

            getContentProps() {
                // You can pass the store here, you don't need to wrap your component with a factory function
                return {
                    authStore
                };
            }
        });

// in plugin init
const authStore = new AuthStore();
api.addAdapter("adapter://adetante/besu/auth-store", new AuthAdapter(authStore));
api.addModule("module://adetante/besu/auth", authModule(authStore));
// NOTE: we pass the authStore directly to the module, but if we needed to do some async init, we could instead add it as a lazy dependency in the module's implementation, under `dataAdapters: [...]`

Please let me know if this approach sounds OK to you.

See the eth-lite modifications in #59

You will have to checkout the latest ethereum-lite-explorer master branch, which supports this functionality (added with Alethio/cms#v1.0.0-beta.12)

After digging deeper into the plugin code, it becomes obvious that we have to block initialization until authentication is performed, otherwise everything will start throwing errors. On the other hand because the plugin engine expects all data sources to be loaded before actually rendering anything on the page, this means you can't define a module that renders the login form, but have to either:

a) Render the form manually in the blank page:
b) Redirect to a 3rd-party login URL - I'm not sure how this would work with Besu

For the first case, your custom plugin init code would look something like this:

let authStore = new AuthStore();
api.addDataAdapter("adapter://adetante/besu/auth-store", authStore);

if (!authStore.isAuthenticated) {
    const formEl = document.createElement("div");
    document.body.appendChild(formEl);

    ReactDOM.render(<Auth authStore={authStore} onLoginSuccess={async () => {
        ReactDOM.unmountComponentAtNode(formEl);
        document.body.removeChild(formEl);

        // Notify eth-lite plugin
        runInAction(() => {
            authStore.isAuthenticated = true;
            //...
        });
    }} />, formEl);
} else {
    // Notify eth-lite plugin
    runInAction(() => {
        authStore.isAuthenticated = true;
        // ....
    });
}

Thank you for your detailed answer! I had initially tried to implement it as a standalone plugin, I agree it's better than including Besu related features in eth-lite. But without the hook at web3 initialization, it was impossible without duplicating a large part of the plugin.

I'll start an implementation as you presented it, and I'll update this ticket with the info to use it.

Thanks also for highlighting how to use getContentProps instead of the factory function in the module definition, I hadn't noticed that!

Sure, let me know when you have something working, so I can publish the updated eth-lite plugin. You can also check the docs at https://github.com/Alethio/cms and if you get stuck creating the new plugin, you can also ask questions on our Discord server.

I released the first version of the plugin, it works well with your changes on eth-lite (branch custom-auth-store) and explorer-lite (master).

Here is the github repo https://github.com/adetante/explorer-besu-plugin and the npm package https://www.npmjs.com/package/@adetante/explorer-besu-plugin.

Thanks for your patch & support

Published eth-lite @ 4.2.0
ethereum-lite-explorer @ 1.0.0-beta.10 Docker image is pending

Also a suggestion: if you render the form manually and don't depend on the global @alethio/ui theme provided by the host explorer app, you can use the next tag instead for easier form handling: https://alethio.github.io/ui/?path=/story/form--default