In this article we will discuss and design a ledger-backed authentication model for interacting with Hedera dApps. We will be using HashPack, the Hedera community wallet, to sign authentication prompts. Amazon’s AWS constructs will be leveraged to speed up the concrete implementation details.
A word on semantics
This context uses dApps to mean any client-run website, most likely a JavaScript bundle of some sort of business logic written in a higher order expression language such as JSX.
For a dApp to do useful operations, it oftentimes requires ways to manipulate data. Current web-applications do this via REST calls to semantically crafted URLs. Even though the website logic behind these URL endpoints tends to be hosted on centralized servers, it is due to the client-side execution of these bundles along with their interaction with web3 components such as wallets and chain data that we say that, overall, these applications are (albeit not fully) decentralized. Hence the dApp acronym.
Also, in this article, we use the notion of decentralized access (dAccess) to refer to a complete authentication and authorization flow that leverage the use of web3 components. More on this, later.
Hopefully, by the end of this post, you will have a conceptual foundation for a decentralized auth model flexible enough to not only support various Hedera web3 use-cases but also other chains’ as well.
By doing so, we will strive to achieve a high degree of decentralization for both the authentication and the authorization phases providing for a robust and resilient app experience.
Whenever discussing about permissioned services, our traditional access model consists, at a very minimum, of:
a fenced service to which the user is interested in,
a place (usually a webpage, if we’re talking about the internet) where the user can authenticate and request authorization to access various aspects of the fenced service and
a source (usually a database) that stores user’s credentials which are used as part of the above authentication and authorization step
Now, depending on the degree of complexity, access sensitivity and security, there are various protocols that cover these traditional, web2, steps. While discussing these flows goes beyond the scope of this article, I will mention that web sites, in particular, have popularized the use of OAuth2.
As discussed in the semantics section, even though dApp’s are inherently run in a decentralized manner, it is due to the centralized nature of database technologies that make up point 3 which argues for the overall centralized nature of such approaches. This is not only with regards to access mechanism but anything that’s CRUD-able from the service’s perspective.
Therefore, to obey the ethos of the web3 movement and strive to provide dAccess mechanisms for services that don’t have a single point of failure, one would need to solve for the decentralization aspect of point 3.
building our authorization logic around data which is publicly accessible (eg. Hedera’s mirror network) sourced from trusted entities (eg. Hedera’s official mirror site)
If a flow is successful and dAccess is granted, the user can then acquire API access to safeguarded (coded as a lambda function in AWS’s ApiGateway Lambda Authorizer construct, for instance) REST pathways.
A successful completion of a dAccess flow leads to the issuance of a time-lived anonym session token (aST) cookie which is returned to the user and subsequent provided to all API calls.
Our dAccess service is currently comprised out of 3 endpoints:
a ping one — allows the frontend to query if the user has a valid aST. Due to browser cookie security policies, the client side JavaScript can only tell if a valid aST is present (a HTTP 200 is returned if OK, and 4xx otherwise) but can’t say anything about the token value itself
a challenge one — used to create data packet challenges (part of step 1) and discussed in the following section
a create one — if payload conditions are right, allows for the creation of the time-lived aST cookie token
Before discussing each dAccess core component, let’s talk a little about how each of this dAuth endpoints plays a role in this overall dAccess framework.
dAccess flow
dAccess cold start: generating and wallet-signing a secure-challenge and exchanging it for a new aST
Having all these endpoints available, if we want to guard a certain piece of dApp functionality, we would first do a query on the ping endpoint to see if the user is a valid aST holder. If there is no such token available, we start an aST challenge-creation sub flow, otherwise we just load the guarded service and continue execution as normal.
If we’re attempting to challenge-create an aST, the time-sensitive piece referred to by point 1 is required to give a sense of time-validity to the data. If a 3rd part would try to brute-force the creation of such a packet, it would have a limited amount of time before the challenge itself would expire and their cracking attempt would need to start from scratch. This is the packet that the user needs to sign and we call this, “the challenge”.
To make sure that only service-creator can generate a challenge, we assign it a private-public key pair in the form of a hedera account and use the private key to generate a signature of parts of the data that composes the challenge. We then embed the signature in the final response which we return to the requester.
For HeadStarter, the challenge currently looks like this:
The create endpoint then verifies that the original packet received by the user was signed by the server, the time-stamp (ts) is not too old and that the wallet accountId is the one that generated the wallet signature by fetching the wallet’s accountId public-key from a trusted hedera mirror-node.
If this basic validation passes, then we can be sure that:
the user signed something that the challenge endpoint created and, more importantly,
the user owns that wallet accountId
We then go on and check the other dapp specific preconditions (eg. is a NFT present on this account?) before concluding the dAccess attempt.
If successful, an aST is issued and returned as part of a Cookie (as mentioned previously). For HeadStarter, it looks like the following:
The aST is a dot separated list of base64 encoded strings that contain basic information regarding the session. Information such as:
when was the aST generated — a timestamp
a server signature of the aST generated timestamp to make sure that the aST was created by us, the server, and not a non-trusted entity
the wallet accountId that signed the original valid challenge
Having a valid-sourced timestamp available, the server can then invalidate (or phase out sessions) in a time-framed manner. This is also achieved on the client side by Max-Age-ing the aST Cookie.
dAccess authorization
While dAccess authentication is, at its core, more or less self-explanatory, authorization can have multiple implementation variants.
Common dAccess authorization implementation
For example, one might chose to authorize a certain account for which we know the account-id. This would query the mirror network only for the account info, making sure it exists and using its public-key to validate the challenge signature.
A more sophisticated authorization logic would imply verifying that an account has a token associated, has a minimum amount of a certain token on their account or owns a NFT token. All these transitive use-cases would require access to mirror-network data to verify and are the sole responsibility of the authorizer-verifier.
Conclusions
Decentralizing access to your dApp’s sensitive parts is not as hard as it seems. It can be easily accomplished on Hedera but is by no means limited to it.
Having designed the interaction flow and the data-exchange packet formats, the only prerequisites left to implement such a mechanic on another chain are:
the presence of a means (eg software wallet) for the principle client to sign arbitrary data-packets
public access to ledger data which the dAccess authorization logic can consume
an authorization infrastructure such as AWS’s to speed up development efforts (optional)