UNPKG

next-auth-hashpack

Version:

Authenticate with Hashpack wallet using NextAuthJs library.

375 lines (286 loc) 13.5 kB
# next-auth-hashpack NextAuth.js is an open-source library that simplifies the implementation of authentication in serverless and server-rendered web applications using various authentication providers. It is built on top of the Next.js framework and provides a simple API and pre-built authentication providers. <br> The next-auth-hashpack package is designed to enable NextAuth.js to use Hashpack Wallet as an authentication provider. This package allows users to sign in and authenticate using Hashpack Wallet & covers all the major steps. <br> ## install `npm install next-auth-hashpack` <br> ## UI View ![Authentication Flow](SignInProviders.jpg) <br> # The Flow ![Authentication Flow](AuthWithHashpack.jpg) <br> # Usage <br> ## **HashpackProvider** after configuring next-auth, call `HashpackProvider` in the providers array: ```javascript pages/api/auth/[...nextauth].ts NextAuth({ providers: [ hashpackProvider({ userReturnCallback: ({ accountId, network }, userPublicKey) => { return { id: "DavinciGraph", accountId: accountId } }, privateKey: "ServerAccountPrivateKey", mirrorNodeAccountInfoURL: { testnet: 'https://testnet.mirrornode.hedera.com/api/v1/accounts', mainnet: 'https://mainnet-public.mirrornode.hedera.com/api/v1/accounts' }, getUserPublicKey: ({accountId, network}) => { "mechanism to return user's public key" return "UserPublicKey"; }, checkOriginalData: ({accountId, originalData, network}) => { "check the originData you had sent to the user to sign with an external source like database" return true; }, debug: true }) ] }) ``` <br> ### __hashpackProvider Options__ **userReturnCallback** <br> **_`mandatory`_** this callback would be executed when user is authenticated successfully. it gets credentials inputs including user's accountId which can be used to fetch user's data from for example a database, and userPublicKey. ```javascript pages/api/auth/[...nextauth].ts hashpackProvider({ ... userReturnCallback: ({signedPayload, userSignature, accountId, network}, userPublicKey) => {} }) ``` <br> **privateKey** <br> **_`mandatory`_** the server's hedera hashgraph account private key, this key would be used to generate a publicKey and verify the signed data. after version 1.1.6 private key for different network is supported. ```javascript pages/api/auth/[...nextauth].ts hashpackProvider({ ... privateKey: "server private key", //or privateKey: (network) => { if( network === 'mainnet' ){ return 'pk for mainnet.' } return 'pk for testnet'; } // or privateKey: { testnet: "server private key for testnet", mainnet: "server private key for mainnet" } }) ``` > 💡 Attention: prior to 1.2.2 we had used publicKey directly but it has some issues with verifying on mainnet. therefore after that we generate the publicKey from this privateKey <br> <br> **mirrorNodeAccountInfoURL** <br> **_`optional for development`_** after version 1.0.6 you must define this variable as an object with two properties `tesetnet` and `mainnet` which both are urls pointing to a mirror node to get an account information. If left empty default urls are the public mirror nodes. <br> **getUserPublicKey** <br> **_`optional`_** if you want to implement your mechanism to get user's account public key, you can pass a callback to this property. you'll get signing-in user account id as the input ```javascript pages/api/auth/[...nextauth].ts hashpackProvider({ ... getUserPublicKey: ({accountId, network}) => { return "mechanism to return user's public key" } }) ``` <br> **checkOriginalData** <br> **_`optional`_** if you want to add an extra security layer to check if the data you had sent to the user matches the receiving one, you can use this function. ```javascript pages/api/auth/[...nextauth].ts hashpackProvider({ ... checkOriginalData: ({accountId, originalData, network}) => { "check the originData you had sent to the user to sign with an external source like database" return true; } }) ``` <br> **debug** <br> **_`optional`_** a flag which when true shows the process of running functionalities in the console. <br> ## **authInitializer** The usual next-auth providers have a similar flow to initiate authentication(since they implement OAuth standard), but with a cryptocurrency wallet this is not available. therefore we need another route, which must be created manually. <br> By default, a `hashpack.ts` file is supposed to exist in `pages/api/auth` containing a route that calls `authInitializer` as presented in the following snippet. ```javascript pages/api/auth/hashpack.ts export default async function hashpack(req: NextApiRequest, res: NextApiResponse) { const accountId = AccountId("server's hedera account Id"); const privateKey = PrivateKey.fromString("Server's hedera account private key"); let data = { url: 'https://davincigraph.io', data: { token: "a randomly generated token" } }; // optionally you can define a preInitializingCallback and give it to authInitializer. // after validation of user's account ID, preInitializingCallback would receive it const preInitializingCallback = (accountId) => { // logic: e.g. store the accountId and data in the database } // return whatever authInitializer return to to prevent stale requests return authInitializer(req, res, accountId, privateKey, data, "testnet", preInitializingCallback, true); // the final argument is hedera network, and it accepts either testnet or mainnet } ``` <br> as you see, `authInitializer` requires the route `request` and `response`, server's Hedera hashgraph `account Id` and `private key`, and some `data` to be signed by both parts (server & user). also a `preInitializingCallback` that would run after validation of the request. debug is also available here. > 💡 the `pages/api/auth/hashpack.ts` path is not mandatory and the route can be created anywhere needed, the client knows about it in a way which would be explained in the following. > 💡 the request contains `network` in its body object. <br> ### Sign-in page the other essential configuration for the next-auth is the sign-in page url. by default next-auth creates a page with this url pages/api/auth/signin. as you remembered crypto wallets cannot authenticate like oAuth providers by the default page treat all as that, so we cannot use it. instead we use some of next-auth-hashpack functionalities in the react. to change the sign-in page url: ```javascript pages/api/[...nextauth].ts NextAuth({ providers: [ ... ], pages: { signIn: "/auth/signin" // you can write any path you want } }) ``` <br> <br> <br> # React The previous functionalities described are responsible for handling the authentication flow related to the backend. In contrast, the following section explains how the frontend handles the authentication flow. <br> ## **hashConnect** To connect to hashpack at any time, we must instantiate HashConnect and hold some of its properties as states. next-auth-hashpack requires 3 entries to interact with hashpack. the `hashConnect instance`, `hedera network`, `hashConnect topic`, `hashConnect state`, and the state holding the paired or to-be-paired user's `account id`. > 💡 Notice that another prerequisite is to have next-auth session context set up. <br> > 💡 Import HashConnect instance from this path: ```javascript import { HashConnect } from "hashconnect/dist/esm/hashconnect"; ``` <br> ## **useHashpackAuthentication** hook returns `authenticate` function & `error` state. when `authenticate` function is called it initiate the hashpack authentication flow, for example by clicking on a button. ```javascript useHashpackAuthentication( hashConnect, // the hashConnect instance network hashConnectTopic, // hashConnect's connection topic state pairedAccountId, // paired or to be paired account id state singInOptions, // Optional - next-auth sign-in options authInitializerApiRoute ,// Optional the route that initialize the authentication, as mentioned above the default path is `pages/api/auth/hashpack.ts` onSuccess // a callback to run when the authentication succeeded ) ``` > 💡 use this hook when you want to completely create a new UI for the sign-in options, otherwise next-auth-hashpack provides enough customizable components to satisfy every need. <br> ## **HashpackButton component** default next-auth-hashpack initializer button. it accepts all the useHashpackAuthentication inputs as props and also several props for the styling purposes. ```javascript import { HashpackButton } from "next-auth-hashpack/dist/react"; <HashpackButton hashConnect={hashConnect} network={network} hashConnectTopic={hashConnectTopic} hashConnectState={hashConnectState} pairedAccountId={pairedAccountId} signInOptions={signInOptions} authInitializerApiRoute={authInitializerApiRoute} onStart={() => {console.log("authentication starts")}} onSuccess={(accountId) => {console.log("successfully signed in")}} onError={(error) => {console.log(error)}} id="whatever" // this and the two following can be used for customizing styles styles="whatever" className="whatever" /> ``` the content of the button also can be replace. ```javascript import { HashpackButton } from "next-auth-hashpack/dist/react"; <HashpackButton hashConnect={hashConnect} network={network} hashConnectTopic={hashConnectTopic} hashConnectState={hashConnectState} pairedAccountId={pairedAccountId} signInOptions={signInOptions} authInitializerApiRoute={authInitializerApiRoute} id="whatever" // this and the two following can be used for customizing styles styles="whatever" className="whatever" > Custom sign in content </HashpackButton> ``` if you want to handle the authentication error (showing below the button) yourself, give the button an onError callback, it receives the error as its input. ```javascript import { HashpackButton } from "next-auth-hashpack/dist/react"; <HashpackButton ... onError={(error: string) => console.log(error)} ... > Sign in </HashpackButton> ``` > 💡 If you need to use this button to show it alongside other providers in your custom way, you need it get the providers from next-auth manually and construct them, otherwise just use `ProvidersCard`. <br> ## **ProvidersCard component** shows the hashpack button alongside the other providers for sign-in. good for the case which sign-in through several providers is available. e.g: hashpack and google or ... ```javascript import { ProvidersCard } from "next-auth-hashpack/dist/react"; <ProvidersCard hashConnect={hashConnect} network={network} hashConnectTopic={hashConnectTopic} hashConnectState={hashConnectState} pairedAccountId={pairedAccountId} signInOptions={signInOptions} authInitializerApiRoute={authInitializerApiRoute} /> ``` > 💡 other providers button have the css class `next-auth-provider-button`, use it to customize their style. <br> ## **ProvidersCard component** use this component when you just want a boilerplate default sign-in page. ```javascript import { SignInSection } from "next-auth-hashpack/dist/react"; <SignInSection hashConnect={hashConnect!} network={network} hashConnectTopic={hashConnectTopic} hashConnectState={hashConnectState} pairedAccountId={pairingData?.accountIds[0]!} signInOptions={signInOptions} authInitializerApiRoute={authInitializerApiRoute}/> ``` # **some notes about configurations** ## changing user structure by default next-auth user object just have id, name, email and image properties, which among them a string id is mandatory. but if there is case of extending this object, do as follow: ```javascript /pages/api/auth/[...nextauth].ts ... callbacks: { async jwt({ token, user, account, profile, isNewUser }: any) { if (user?.accountId) { // add what ever properties you want to the token token.accountId = user?.accountId; } return Promise.resolve(token); }, async session({ session, token, user }: any) { if (token?.accountId) { // add the token properties to the session session.user.accountId = token?.accountId; } return Promise.resolve(session) } }, ... ``` This way, the session in the front-end contains the user properties you want ## Do not forget the jwt secret set environment variable `NEXTAUTH_SECRET` or do it in the `[...nextauth].ts`: ```javascript /pages/api/auth/[...nextauth].ts ... jwt: { secret: "a strong phrase" } ... ```