Web3 Parse Server Authentication

The instructions in this manual will demonstrate how to utilize Moralis V2 with a self-hosted Parse server.

To achieve this, we will need to:

  1. Set up parse-server
  2. Set up parse server dashboard to access db
  3. Integrate Moralis Auth with parse-server

Let's get started.

For this tutorial, we'll be building with express, which parse makes use of.

Set up parse-server

First, we will install the parse-server package.

yarn add parse-server

All that is left to do now is initialize the parsing server.

Let us create a file called parseServer.ts and initialize parse server.

import { ParseServer } from 'parse-server';

export const parseServer = new ParseServer({
  databaseURI: config.DATABASE_URI, // Connection string for your MongoDB database
  cloud: config.CLOUD_PATH, // Absolute path to your Cloud Code
  appId: config.APPLICATION_ID, // Application ID
  masterKey: config.MASTER_KEY, // Master key
  serverURL: config.SERVER_URL, // Server URL
});

Learn more about parse server

Set up parse server dashboard

We will need to set up a dashboard to access the database and other features provided by parse-server.

yarn add parse-dashboard

We also have to initialize the dashboard. Let's do this is parseDashboard.ts

import ParseDashboard from 'parse-dashboard';

export const parseDashboard = new ParseDashboard({
  apps: [
    {
      appId: config.APPLICATION_ID, // Server Application ID
      masterKey: config.MASTER_KEY, // Server Master Key
      serverURL: config.SERVER_URL, // Server URL
      appName: config.APP_NAME, // Application Name
    },
  ],
});

Learn more about parse dashboard

Integrate Moralis Auth with parse-server

Now we will integrate Web3 authentication with parse authentication in this guide. The Web3 authentication flow will be managed using the Moralis Auth API.
We will leverage parse server 0Auth support to create our custom authentication.

To do this we need to install the Moralis package.

yarn add moralis

We must not forget to initialize Moralis.

import Moralis from 'moralis';

Moralis.start({
  apiKey: 'your_api_key_here',
});

Now let's make our own authentication adapter. To verify authentication requests, we will use the Moralis Auth API in this adapter. Let us create a file called MoralisAuthAdapter.ts and code.

function validateAuthData(authData: any) {
  const { message, signature, network, id, authId } = authData;

  return Moralis.Auth.verify({
    message,
    signature,
    network,
  })
    .then((result) => {
      const data = result.toJSON();

      if (id === data.profileId && authId === data.id) {
        authData.chainId = result.result.chain.decimal;
        authData.nonce = data.nonce;
        authData.address = result.result.address.checksum;
        authData.version = data.version;
        authData.domain = data.domain;
        authData.expirationTime = data.expirationTime;
        authData.notBefore = data.notBefore;
        authData.resources = data.resources;
        authData.statement = data.statement;
        authData.uri = data.uri;
        return;
      }

      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Moralis auth failed, invalid data');
    })
    .catch(() => {
      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Moralis auth failed, invalid data');
    });
}

function validateAppId() {
  return Promise.resolve();
}

export default {
  validateAuthData,
  validateAppId,
};

The last thing we need to do to setup the adapter is to add it to our ParseServer initialization.
Parse server initialization should look like this.

import { ParseServer } from 'parse-server';
import MoralisAuthAdapter from './auth/MoralisAuthAdapter';

export const parseServer = new ParseServer({
  databaseURI: config.DATABASE_URI,
  cloud: config.CLOUD_PATH,
  appId: config.APPLICATION_ID,
  masterKey: config.MASTER_KEY,
  serverURL: config.SERVER_URL,
  auth: {
    moralis: {
      module: MoralisAuthAdapter,
    },
  },
});

We called this auth adapter moralis.

Learn more about parse custom authentication

Lastly we need to create the authentication service, this is where we will request the message and also authenticate with parse server.
Let us create a file and call it authService.ts and code.

import Moralis from 'moralis';
import { authRequests } from '../store';
import { ParseServerRequest } from '../utils/ParseServerRequest';

export async function requestMessage({ address, chain, network }: { address: string; chain: string; network: 'evm' }) {
  const result = await Moralis.Auth.requestMessage({
    address,
    chain,
    network,
    domain: 'defi.finance',
    statement: 'Please sign this message to confirm your identity.',
    uri: 'https://defi.finance',
    expirationTime: '2023-01-01T00:00:00.000Z',
    timeout: 15,
  });

  const { message, id, profileId } = result.toJSON();
  authRequests.set(message, { id, profileId });

  return message;
}

export async function verifyMessage({ network, signature, message }) {
  const storedData = authRequests.get(message);

  if (!storedData) {
    throw new Error('Invalid message');
  }

  const { id: storedId, profileId: storedProfileId } = storedData;

  const authData = {
    id: storedProfileId,
    authId: storedId,
    message,
    signature,
    network,
  };

  // Authenticate
  const user = await serverRequest.post({
    endpoint: `/users`,
    params: {
      authData: {
        moralis: authData,
      },
    },
    useMasterKey: true,
  });

  // Update user moralisProfile column
  await serverRequest.put({
    endpoint: `/users/${user.objectId}`,
    params: {
      moralisProfileId: storedProfileId,
    },
    useMasterKey: true,
  });

  // Get authenticated user
  const updatedUser = await serverRequest.get({
    endpoint: `/users/${user.objectId}`,
    useMasterKey: true,
  });

  return updatedUser;
}

We export two functions here

  • requestMessage; here, we utilize the Moralis Auth API to make a request for an authentication message, which we can then send to users to sign with their wallet. We also store the id and profileId in a store (any store of choice), we will need this information when verifying the message.
  • verifyMessage; the stored id and profileId are retrieved in this method, and an authData is prepared to be used for authentication using the moralis auth adapter we previously created.

And we are set.

Now let's put everything together using express. We will create the express router for our authentication functions.

We'll make two files. authController.ts

import { requestMessage, verifyMessage } from './authService';
import { NextFunction, Request, Response } from 'express';

export async function request(req: Request, res: Response, next: NextFunction) {
  try {
    const { address, chain, network } = req.body;

    const message = await requestMessage({
      address,
      chain,
      network,
    });

    res.status(200).json({ message });
  } catch (err) {
    next(err);
  }
}

export async function verify(req: Request, res: Response, next: NextFunction) {
  try {
    const { network, message, signature } = req.body;

    const user = await verifyMessage({
      network,
      message,
      signature,
    });

    res.status(200).json({ user });
  } catch (err) {
    next(err);
  }
}

and authRouter.ts

import express from 'express';
import { verify, request } from './authController';

export const authRouter = express.Router();

authRouter.route('/request-message').post(request);
authRouter.route('/sign-message').post(verify);

Finally our express index.ts fill will look like this

import Moralis from 'moralis';
import express from 'express';
import cors from 'cors';
import { parseDashboard } from './parseDashboard';
import { parseServer } from './parseServer';
import { errorHandler } from './middlewares/errorHandler';
import config from './config';
import { authRouter } from './auth/authRouter';

const app = express();

Moralis.start({
  apiKey: config.MORALIS_API_KEY,
});

app.use(express.urlencoded({ extended: true }));
app.use(express.json());

app.use(cors());

app.use('/server', parseServer);
app.use('/dashboard', parseDashboard);
app.use('/auth', authRouter);
app.use(errorHandler);

app.use(express.static('public'));

app.listen(config.PORT, () => {
  console.log(`${config.APP_NAME} is running on port ${config.PORT}`);
});

Client side

Making sure that this functions on the client side is the last thing we need to accomplish.

We need to add the following dependencies using CDN:

  • Etherjs so we can sign messages with our wallet (metamask in the case of this demo)
  • Axios (for making HTTP requests)
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdn.ethers.io/lib/ethers-5.2.umd.min.js" type="application/javascript"></script>

We now need some code to retrieve basic information about a user's wallet. The following code only works with the MetaMask provider.

const connectToMetamask = async () => {
  const provider = new ethers.providers.Web3Provider(window.ethereum, 'any');

  const [accounts, chainId] = await Promise.all([
    provider.send('eth_requestAccounts', []),
    provider.send('eth_chainId', []),
  ]);

  const signer = provider.getSigner();
  return { signer, chain: chainId, account: accounts[0] };
};

Now that everything is in place, we can authenticate a user. The authentication flow follows a very straightforward pattern:

  • Read a basic information about a user's wallet.
  • Generate a message by the back-end function to sign it by the user.
  • Sign the message by the user's wallet.
  • Send the signed message to the back-end function which verifies and authenticates with parse server.

The flow is in the function below

const handleAuth = async () => {
  // Connect to Metamask
  const { signer, chain, account } = await connectToMetamask();

  if (!account) {
    throw new Error('No account found');
  }
  if (!chain) {
    throw new Error('No chain found');
  }

  const { message } = await requestMessage(account, chain);

  const signature = await signer.signMessage(message);

  const { user } = await verifyMessage(message, signature);

  renderUser(user);
};

Demo project

You can find the repository with the final code here: parse-server.


Did this page help you?