HIPAA Compliant Chat: Build a Chat App!

Nick P.
Nick P.
Published November 1, 2019 Updated August 25, 2021

As healthcare technology advances, more patients expect healthcare applications to provide secure real-time communication. This often requires patients to exchange their protected health information (PHI) over in-app chat messaging.

To maintain your users’ trust—and to ensure their privacy and data are protected—you must consider HIPAA compliance when building a chat experience.

Note: In addition to offering HIPAA-compliant chat, it’s also strongly recommended (but not required) to secure your chat experience with end-to-end encryption.

This tutorial will implement end-to-end encryption, but again, it’s not a requirement.

What Does it Mean to be HIPAA Compliant?

The Health Insurance and Accountability Act (HIPAA) is a law that regulates how healthcare professionals (like healthcare providers, agencies, or healthcare apps) handle your PHI or any information that can identify you as a patient.

For your chat app to be HIPAA compliant, you must sign a Business Associate Agreement (BAA) with Stream. That's all that's required of you to be HIPPA compliant. But, to sign a BAA with Stream, you'll need a Chat Elevate Plan. This way, you can integrate Stream's Chat and/or Feeds functionality into your app.

Building a HIPAA Compliant Chat

In this tutorial, you'll learn how to create a HIPAA-compliant telemedicine chat solution using Stream Chat.

To make your app more secure, you'll also learn how to build end-to-end encryption (E2EE) into your app by embedding Virgil Security's eThree Kit within Stream Chat's React components (you can find all source code for this app on GitHub).

Both Stream Chat and Virgil make it easy to build a chat solution with excellent security that provides all the features you expect. You can also learn how to build a HIPAA-compliant chatbot solution using Stream Chat SDKs combined with Dialogflow using Virgil Security for encryption.

Follow along to build a HIPAA-compliant chat app, whether it be for web chat, team chat, group chat, or live chat, for your chat clients.

Note: Providing E2EE ensures that PHI is transferred between chat clients securely.

Although E2EE is not a requirement to be HIPAA compliant, it provides an additional layer of security for patients; any real-world solution should incorporate E2EE.

How to Build a HIPAA Compliant Chat React App

To build this application, you'll use three libraries:

The final product will encrypt text in the browser before sending a message. Decryption and verification will both happen in the receiver's browser.

To do this, the app performs the following steps:

  1. A user authenticates with your backend.
  2. The user's app requests a Stream auth token and API key from the backend. The browser creates a Stream Chat Client for that user.
  3. The user's app requests a Virgil auth token from the backend and registers with Virgil. This generates their private and public key. The private key is stored locally, and the public key is stored in Virgil.
  4. Once the user decides who they want to chat with the app creates and joins a Stream Chat Channel.
  5. The app asks Virgil for the receiver's public key.
  6. The user types a message and sends it to stream. Before sending, the app passes the receiver's public key to Virgil to encrypt the message.
  7. The message is relayed through Stream Chat to the receiver. Stream receives ciphertext, meaning no one can see the original message.
  8. The receiving user decrypts the sent message using Virgil. When the message is received, the app decrypts the message using the Virgil and passes it along to Stream's React components.
  9. Virgil verifies the message is authentic by using the sender's public key.

This looks intimidating, but luckily Stream and Virgil do the heavy lifting for you. As developers using these services, our responsibility is to wire them together correctly.

The code is split between the React frontend contained in the frontend folder and the Express (Node.js) backend is found in the backend folder. See the README.md in each folder to see installation and running instructions.

If you'd like to follow along while running the code, make sure you get both the backend and frontend running before continuing.

What You'll Need to Build a HIPAA Compliant React App

Before you start, you'll need:

Once you've created your accounts, place your credentials in backend/.env. You can use backend/.env.example as a reference for what credentials are required.

This tutorial uses the following package versions:

  • Node 11.14.0
  • Yarn 1.17.0
  • Stream Chat 0.13.3
  • Stream Chat React 0.6.26
  • Virgil Crypto 3.2.0
  • Virgil SDK 5.3.0
  • Virgil e3Kit 0.5.3
  • Express 4.17.1

Except for node and yarn, all of these dependencies are declared in backend/package.json and frontend/package.json.

Step 1. Setup the Backend

For our React frontend to interact with Stream and Virgil, the
application provides three endpoints:

  • POST /v1/authenticate: This endpoint generates an auth token that allows the React frontend to communicate with /v1/stream-credentials and /v1/virgil-credentials.
  • /v1/virgil-credentials: This endpoint allows the client to be any user. The frontend tells the backend who it wants to authenticate as. In your application, this should be replaced with your API's authentication endpoint.
  • POST /v1/stream-credentials: This returns the data required for your React app to establish a session with Stream.

In order return this info, you must tell Stream the user exists in order to create a valid auth token:

  exports.streamCredentials = async (req, res) => {
    const data = req.body;
    const apiKey = process.env.STREAM_API_KEY;
    const apiSecret = process.env.STREAM_API_SECRET;

    const client = new StreamChat(apiKey, apiSecret);

    const user = Object.assign({}, data, {
      id: `${req.user.sender}`,
      role: "admin",
      image: `https://robohash.org/${req.user.sender}`
    });
    const token = client.createToken(user.id);
    await client.updateUsers([user]);
    res.status(200).json({ user, token, apiKey });
  };

The response payload has this shape:

  {
    "apiKey": "<string>",
    "token": "<string>",
    "user": {
      "id": "<string>",
      "role": "<string>",
      "image": "<string>"
    }
  }
  • apiKey: The stream account identifier for your Stream instance (required to identify what account your frontend is trying to connect to).
  • token: The JWT token that authorizes the frontend with Stream.
  • user: An object that contains the data that the frontend needs to connect and
    render the user's view.

    • POST /v1/virgil-credentials: The endpoint that returns the authentication token used to connect the frontend to Virgil. You'll use the Virgil Crypto SDK to generate a valid auth token:
const virgilCrypto = new VirgilCrypto();

const generator = new JwtGenerator({
  appId: process.env.VIRGIL_APP_ID,
  apiKeyId: process.env.VIRGIL_KEY_ID,
  apiKey: virgilCrypto.importPrivateKey(process.env.VIRGIL_PRIVATE_KEY),
  accessTokenSigner: new VirgilAccessTokenSigner(virgilCrypto)
});

exports.virgilCredentials = async (req, res) => {
  const virgilJwtToken = generator.generateToken(req.user.sender);

  res.json({ token: virgilJwtToken.toString() });
};

In this case, the frontend only needs the auth token.

Step 2. Authenticate the User With the Backend

Now that you've set up your backend, you can authenticate with the backend. If you're running the application, you'll see the following screen:

Registration

This is a simple React form that takes the provided input, stores it in the state as sender, and uses that information to authenticate against the backend:

post("http://localhost:8080/v1/authenticate", { sender: this.state.sender })
  .then(res => res.authToken)
  .then(this._connect);

Once you've created a sender identity with an auth token, you can connect to Stream and Virgil.

Step 3. Connect the User to Stream

Using the credentials from Step 2, you can request Stream credentials from the backend and connect the frontend client to Stream:

const response = await post(
  "http://localhost:8080/v1/stream-credentials",
  {},
  backendAuthToken
);

const client = new StreamChat(response.apiKey);
client.setUser(response.user, response.token);
Building your own app? Get early access to our Livestream or Video Calling API and launch in days!

This initializes the StreamChat object from the Stream Chat React library and authenticates a user using the token generated in the backend.

Step 4. Connect the User to Virgil

Once again, using the credentials acquired in
Step 2, you can ask the backend to generate a Virgil auth token. Using this token will initialize the EThree object from Virgil's e3kit library:

const response = await post(
  "http://localhost:8080/v1/virgil-credentials",
  {},
  backendAuthToken
);
const eThree = await EThree.initialize(() => response.token);
await eThree.register();

Step 5. Create a Stream Chat Channel

Once you've connected to both Stream and Virgil, you can start chatting with someone.

Select Register in the tutorial app. You'll see the following screen:

This form asks for the identity of the user you want to chat with. If they've registered in another browser window, you can create a Stream Chat Channel that's private to those two members:

let members = [this.state.sender, this.state.receiver];
members.sort();

const channel = this.state.stream.client.channel("messaging", {
  image: `https://getstream.io/random_svg/?id=rapid-recipe-0&name=${members.join(
    "+"
  )}`,
  name: members.join(", "),
  members: members
});

The client you're accessing in the state is the one created in
Step 3.

Calling .channel will create or join a unique channel based on the identities of the members. Only those two members will be allowed in.

However, this isn't enough to protect Stream or others from viewing those users' messages.

Step 6. Find Virgil Public Keys

In order to encrypt a message before sending it through a Stream channel, you must find the receiver's public key:

const publicKeys = await this.state.virgil.eThree.lookupPublicKeys([
  this.state.sender,
  this.state.receiver
]);

The eThree instance in your state is from Step 4. Assuming that the sender's identity is will and the receiver's identity is sara, this returns an object that looks like:

{
  will: {/* Public Key Info */},
  sara: {/* Public Key Info */}
}

Step 7. Encrypt the Sender's Message and Send it via Stream

You have everything you need to send a secure, end-to-end encrypted message via Stream. Time to chat!

First, show the user the chat room:

<Chat client={this.state.stream.client} theme={"messaging light"}>
  <Channel channel={this.state.stream.channel}>
    <Window>
      <ChannelHeader />
      <MessageList Message={this._buildMessageEncrypted} />
      <MessageInputEncrypted
        virgil={this.state.virgil}
        channel={this.state.stream.channel}
      />
    </Window>
    <Thread />
  </Channel>
</Chat>

This renders the Stream React Chat component that creates a great out-of-the box experience for your users.

If you're following along, you'll see this:

Notice the line where with the custom class MessageInputEncrypted.

This component uses the sender's public key from Virgil to encrypt, then wrap, a Stream React MessageInput component before sending the message over the Stream channel:

export class MessageInputEncrypted extends PureComponent {
  sendMessageEncrypted = async data => {
    const encryptedText = await this.props.virgil.eThree.encrypt(
      data.text,
      this.props.virgil.publicKeys
    );
    await this.props.channel.sendMessage({
      ...data,
      text: encryptedText
    });
  };

  render = () => {
    const newProps = {
      ...this.props,
      sendMessage: this.sendMessageEncrypted
    };

    return <MessageInput {...newProps} />;
  };
}

Now all Stream will see is the ciphertext!

Step 8. Decrypt the Message for the Reveiver

The last thing to do is decrypt the sender's message on the receiver's side. Assuming you've gone through the chat room setup, you'll see:

Chat

To decrypt the message, follow a similar pattern to
Step 7.

If you look at how you created the MessageList, you'll see a custom Message component called MessageEncrypted:

<MessageList Message={this._buildMessageEncrypted} />

Since you need to provide decryption props to add props for decryption to your custom Message component, you can add them to the props passed by the Stream React:

_buildMessageEncrypted = props => {
  const newProps = {
    ...props,
    sender: this.state.sender,
    receiver: this.state.receiver,
    virgil: this.state.virgil
  };
  return <MessageEncrypted {...newProps} />;
};

Once you have the props you need, you can decrypt each message:

export class MessageEncrypted extends PureComponent {
  _isMounted = false;

  constructor(props) {
    super(props);
    this.state = { decryptedText: null };
  }

  componentDidMount = () => {
    this._isMounted = true;
    this._decryptText().then(decryptedText => {
      if (this._isMounted) {
        this.setState({ decryptedText });
      }
    });
  };

  componentWillUnmount = () => {
    this._isMounted = false;
  };

  _decryptText = async () => {
    const messageCreator = this.props.isMyMessage(this.props.message)
      ? this.props.sender
      : this.props.receiver;
    return this.props.virgil.eThree.decrypt(
      this.props.message.text,
      this.props.virgil.publicKeys[messageCreator]
    );
  };

  render = () => {
    const newProps = {
      ...this.props,
      message: {
        ...this.props.message,
        text: this.state.decryptedText || ""
      }
    };

    return <MessageSimple {...newProps} />;
  };
}

This class decrypts the message before rendering the MessageSimple component from Stream Chat React.

To do this:

  1. Determine if the message is actually your message with Stream's .isMyMessage.
  2. Find the correct public key and ask Virgil to decrypt it.
  3. Pass the key along with the rest of the props to Stream's MessageSimple component.

The _isMounted flag won't allow the component to update after the message has been decrypted.

This may happen if you're scrolling quickly, or upon page load if there's a lot of messages.

Where to Go from Here

This tutorial is intended to help you build a HIPPA chat application as quickly as possible, so some critical functionality may be missing from your application.

Here are some tips for what to do next with your app:

  • Build real user registration and protect identity registration. This tutorial simplified registration and retrieving valid tokens to interact with Stream and Virgil.
  • Backup users' private keys to restore sessions for multiple devices. Using Virgil's eThree.backupPrivateKey(pwd) will securely store the private key for restoration on any device.
  • Integrate user image and file uploads. This functionality is hidden in this app via CSS. You can look at hooking into Stream React Chat's MessageInput or use as a jumping-off point to build your own chat widget.
  • Review Stream's list of the top HIPAA compliant chat apps for design and development inspiration for your app.

Happy coding! ✌️

Integrating Video With Your App?
We've built a Video and Audio solution just for you. Check out our APIs and SDKs.
Learn more ->