How To Build A Group Chat App With Vanilla JS, Twilio And Node.js

Chat is becoming an increasingly popular communication medium in both business and social contexts. Businesses use chat for customer and employee intra-company communication like with Slack, Microsoft Teams, Chanty, HubSpot Live Chat, Help Scout, etc. Most social networks and communication apps also offer chat as an option by default, like on Instagram, Facebook, Reddit, and Twitter. Other apps like Discord, Whatsapp, and Telegram are mostly chat-based, with group chats being one of their main functionalities.

While there exist numerous products to facilitate chat, you may need a custom-tailored solution for your site that fits your particular communication needs. For example, many of these products are stand-alone apps and may not be able to integrate within your own site. Having your users leave your website to chat may not be the greatest option as it can affect user experience and conversion. On the flip side, building a chat app from scratch can be a daunting and sometimes overwhelming task. However, by using APIs like Twilio Conversations you can simplify the process of creating them. These communication APIs handle group creation, adding participants, sending messages, notifications, among other important chat functions. Backend apps that use these APIs only have to handle authentication and make calls to these APIs. Front-end apps then display conversations, groups, and messages from the backend.

In this tutorial, you will learn how to create a group chat app using the Twilio Conversations API. The front end for this app will be built using HTML, CSS, and Vanilla JavaScript. It will allow users to create group chats, send invites, login, as well as send and receive messages. The backend will be a Node.js app. It will provide authentication tokens for chat invitees and manage chat creation.

Prerequisites

Before you can start this tutorial, you need to have the following:

  • Node.js installed. You’ll use it primarily for the backend app and to install dependencies in the front-end app.
    You can get it using a pre-built installer available on the Node.js downloads page.
  • A Twilio account.
    You can create one on the Twilio website at this link.
  • http-server to serve the front-end app.
    You can install it by running npm i -g http-server. You can also run it with npx http-server for one-off runs.
  • MongoDB for session storage in the backend app.
    Its installation page has a detailed guide on how to get it running.
The Backend App

To send chat messages using Twilio API, you need a conversation. Chat messages are sent and received within a conversation. The people sending the messages are called participants. A participant can only send a message within a conversation if they are added to it. Both conversations and participants are created using the Twilio API. The backend app will perform this function.

A participant needs an access token to send a message and get their subscribed conversations. The front-end portion of this project will use this access token. The backend app creates the token and sends it to the frontend. There it will be used to load conversations and messages.

Project Starter

You’ll call the backend app twilio-chat-server. A scaffolded project starter for it is available on Github. To clone the project and get the starter, run:

git clone https://github.com/zaracooper/twilio-chat-server.git
cd twilio-chat-server
git checkout starter

The backend app takes this structure:

.
├── app.js
├── config/
├── controllers/
├── package.json
├── routes/
└── utils/

To run the app, you’ll use the node index.js command.

Dependencies

The backend app needs 8 dependencies. You can install them by running:

npm i 

Here’s a list of each of the dependencies:

  • connect-mongo connects to MongoDB, which you’ll use as a session store;
  • cors handles CORS;
  • dotenv loads environment variables from the .env file that you will create in a later step;
  • express is the web framework you’ll use for the backend;
  • express-session provides middleware to handle session data;
  • http-errors helps create server errors;
  • morgan handles logging;
  • twilio creates the Twilio client, generates tokens, creates conversations, and adds participants.

Configuration

The config folder is responsible for loading configuration from environment variables. The configuration is grouped into three categories: configuration for CORS, Twilio, and the MongoDB session DB. When the environment is development, you will load config from the .env file using dotenv.

Start by creating the .env file on the terminal. This file is already added to the .gitignore file to prevent the sensitive values it contains from being checked into the repository.

touch .env

Here’s what your .env should look like:

# Session DB Config
SESSION_DB_HOST=XXXX
SESSION_DB_USER=XXXX
SESSION_DB_PASS=XXXX
SESSION_DB_PORT=XXXX
SESSION_DB_NAME=XXXX
SESSION_DB_SECRET=XXXX

# Twilio Config
TWILIO_ACCOUNT_SID=XXXX
TWILIO_AUTH_TOKEN=XXXX
TWILIO_API_KEY=XXXX
TWILIO_API_SECRET=XXXX

# CORS Client Config
CORS_CLIENT_DOMAIN=XXXX

You can learn how to create a user for your session DB from this MongoDB manual entry. Once you create a session database and a user who can write to it, you can fill the SESSION_DB_USER, SESSION_DB_PASS, and SESSION_DB_NAME values. If you’re running a local instance of MongoDB, the SESSION_DB_HOST would be localhost, and the SESSION_DB_PORT usually is 27017. The SESSION_DB_SECRET is used by express-session to sign the session ID cookie, and it can be any secret string you set.

In the next step, you will get credentials from the Twilio Console. The credentials should be assigned to the variables with the TWILIO_ prefix. During local development, the front-end client will run on http://localhost:3000. So, you can use this value for the CORS_CLIENT_DOMAIN environment variable.

Add the following code to config/index.js to load environment variables.

import dotenv from 'dotenv';

if (process.env.NODE_ENV == 'development') {
    dotenv.config();
}

const corsClient = {
    domain: process.env.CORS_CLIENT_DOMAIN
};

const sessionDB = {
    host: process.env.SESSION_DB_HOST,
    user: process.env.SESSION_DB_USER,
    pass: process.env.SESSION_DB_PASS,
    port: process.env.SESSION_DB_PORT,
    name: process.env.SESSION_DB_NAME,
    secret: process.env.SESSION_DB_SECRET
};

const twilioConfig = {
    accountSid: process.env.TWILIO_ACCOUNT_SID,
    authToken: process.env.TWILIO_AUTH_TOKEN,
    apiKey: process.env.TWILIO_API_KEY,
    apiSecret: process.env.TWILIO_API_SECRET
};

const port = process.env.PORT || '8000';

export { corsClient, port, sessionDB, twilioConfig };

The environment variables are grouped into categories based on what they do. Each of the configuration categories has its own object variable, and they are all exported for use in other parts of the app.

Getting Twilio Credentials From the Console

To build this project, you’ll need four different Twilio credentials: an Account SID, an Auth Token, an API key, and an API secret. In the console, on the General Settings page, scroll down to the API Credentials section. This is where you will find your Account SID and Auth Token.

To get an API Key and Secret, go to the API Keys page. You can see it in the screenshot below. Click the + button to go to the New API Key page.

On this page, add a key name and leave the KEY TYPE as Standard, then click Create API Key. Copy the API key and secret. You will add all these credentials in a .env file as you shall see in subsequent steps.

Utils

The backend app needs two utility functions. One will create a token, and the other will wrap async controllers and handle errors for them.

In utils/token.js, add the following code to create a function called createToken that will generate Twilio access tokens:

import { twilioConfig } from '../config/index.js';
import twilio from 'twilio';

function createToken(username, serviceSid) {
    const AccessToken = twilio.jwt.AccessToken;
    const ChatGrant = AccessToken.ChatGrant;

    const token = new AccessToken(
        twilioConfig.accountSid,
        twilioConfig.apiKey,
        twilioConfig.apiSecret,
        { identity: username }
    );

    const chatGrant = new ChatGrant({
        serviceSid: serviceSid,
    });

    token.addGrant(chatGrant);

    return token.toJwt();
}

In this function, you generate access tokens using your Account SID, API key, and API secret. You can optionally supply a unique identity which could be a username, email, etc. After creating a token, you have to add a chat grant to it. The chat grant can take a conversation service ID among other optional values. Lastly, you’ll convert the token to a JWT and return it.

The utils/controller.js file contains an asyncWrapper function that wraps async controller functions and catches any errors they throw. Paste the following code into this file:

function asyncWrapper(controller) {
    return (req, res, next) => Promise.resolve(controller(req, res, next)).catch(next);
}

export { asyncWrapper, createToken };

Controllers

The backend app has four controllers: two for authentication and two for handling conversations. The first auth controller creates a token, and the second deletes it. One of the conversations controllers creates new conversations, while the other adds participants to existing conversations.

Conversation Controllers

In the controllers/conversations.js file, add these imports and code for the StartConversation controller:

import { twilioConfig } from '../config/index.js';
import { createToken } from '../utils/token.js';
import twilio from 'twilio';

async function StartConversation(req, res, next) {
    const client = twilio(twilioConfig.accountSid, twilioConfig.authToken);

    const { conversationTitle, username } = req.body;

    try {
        if (conversationTitle && username) {
            const conversation = await client.conversations.conversations
                .create({ friendlyName: conversationTitle });

            req.session.token = createToken(username, conversation.chatServiceSid);
            req.session.username = username;

            const participant = await client.conversations.conversations(conversation.sid)
                .participants.create({ identity: username })

            res.send({ conversation, participant });
        } else {
            next({ message: 'Missing conversation title or username' });
        }
    }
    catch (error) {
        next({ error, message: 'There was a problem creating your conversation' });
    }
}

The StartConversation controller first creates a Twilio client using your twilioConfig.accountSid and twilioConfig.authToken which you get from config/index.js.

Next, it creates a conversation. It needs a conversation title for this, which it gets from the request body. A user has to be added to a conversation before they can participate in it. A participant cannot send a message without an access token. So, it generates an access token using the username provided in the request body and the conversation.chatServiceSid. Then the user identified by the username is added to the conversation. The controller completes by responding with the newly created conversation and participant.

Next, you need to create the AddParticipant controller. To do this, add the following code below what you just added in the controllers/conversations.js file above:

async function AddParticipant(req, res, next) {
    const client = twilio(twilioConfig.accountSid, twilioConfig.authToken);

    const { username } = req.body;
    const conversationSid = req.params.id;

    try {
        const conversation = await client.conversations.conversations
            .get(conversationSid).fetch();

        if (username && conversationSid) {
            req.session.token = createToken(username, conversation.chatServiceSid);
            req.session.username = username;

            const participant = await client.conversations.conversations(conversationSid)
                .participants.create({ identity: username })

            res.send({ conversation, participant });
        } else {
            next({ message: 'Missing username or conversation Sid' });
        }
    } catch (error) {
        next({ error, message: 'There was a problem adding a participant' });
    }
}

export { AddParticipant, StartConversation };

The AddParticipant controller adds new participants to already existing conversations. Using the conversationSid provided as a route parameter, it fetches the conversation. It then creates a token for the user and adds them to the conversation using their username from the request body. Lastly, it sends the conversation and participant as a response.

Auth Controllers

The two controllers in controllers/auth.js are called GetToken and DeleteToken. Add them to the file by copying and pasting this code:

function GetToken(req, res, next) {
    if (req.session.token) {
        res.send({ token: req.session.token, username: req.session.username });
    } else {
        next({ status: 404, message: 'Token not set' });
    }
}

function DeleteToken(req, res, _next) {
    delete req.session.token;
    delete req.session.username;

    res.send({ message: 'Session destroyed' });
}

export { DeleteToken, GetToken };

The GetToken controller retrieves the token and username from the session if they exist and returns them as a response. DeleteToken deletes the session.

Routes

The routes folder has three files: index.js, conversations.js, and auth.js.

Add these auth routes to the routes/auth.js file by adding this code:

import { Router } from 'express';

import { DeleteToken, GetToken } from '../controllers/auth.js';

var router = Router();

router.get('/', GetToken);
router.delete('/', DeleteToken);

export default router;

The GET route at the / path returns a token while the DELETE route deletes a token.

Next, copy and paste the following code to the routes/conversations.js file:

import { Router } from 'express';
import { AddParticipant, StartConversation } from '../controllers/conversations.js';
import { asyncWrapper } from '../utils/controller.js';

var router = Router();

router.post('/', asyncWrapper(StartConversation));
router.post('/:id/participants', asyncWrapper(AddParticipant));

export default router;

In this file, the conversations router is created. A POST route for creating conversations with the path / and another POST route for adding participants with the path /:id/participants are added to the router.

Lastly, add the following code to your new routes/index.js file.

import { Router } from 'express';

import authRouter from './auth.js';
import conversationRouter from './conversations.js';

var router = Router();

router.use('/auth/token', authRouter);
router.use('/api/conversations', conversationRouter);

export default router;

By adding the conversation and auth routers here, you are making them available at /api/conversations and /auth/token to the main router respectively. The router is then exported.

The Backend App

Now it’s time to put the backend pieces together. Open the index.js file in your text editor and paste in the following code:

import cors from 'cors';
import createError from 'http-errors';
import express, { json, urlencoded } from 'express';
import logger from 'morgan';
import session from 'express-session';
import store from 'connect-mongo';

import { corsClient, port, sessionDB } from './config/index.js';

import router from './routes/index.js';

var app = express();

app.use(logger('dev'));
app.use(json());
app.use(urlencoded({ extended: false }));

app.use(cors({
    origin: corsClient.domain,
    credentials: true,
    methods: ['GET', 'POST', 'DELETE'],
    maxAge: 3600 * 1000,
    allowedHeaders: ['Content-Type', 'Range'],
    exposedHeaders: ['Accept-Ranges', 'Content-Encoding', 'Content-Length', 'Content-Range']
}));
app.options('*', cors());

app.use(session({
    store: store.create({
        mongoUrl: mongodb://${sessionDB.user}:${sessionDB.pass}@${sessionDB.host}:${sessionDB.port}/${sessionDB.name},
        mongoOptions: { useUnifiedTopology: true },
        collectionName: 'sessions'
    }),
    secret: sessionDB.secret,
    cookie: {
        maxAge: 3600 * 1000,
        sameSite: 'strict'
    },
    name: 'twilio.sid',
    resave: false,
    saveUninitialized: true
}));

app.use('/', router);

app.use(function (_req, _res, next) {
    next(createError(404, 'Route does not exist.'));
});

app.use(function (err, _req, res, _next) {
    res.status(err.status || 500).send(err);
});

app.listen(port);

This file starts off by creating the express app. It then sets up JSON and URL-encoded payload parsing and adds the logging middleware. Next, it sets up CORS and the session handling. As mentioned earlier, MongoDB is used as the session store.

After all that is set up, it then adds the router created in the earlier step before configuring error handling. Lastly, it makes the app listen to and accept connections at the port specified in the .env file. If you haven’t set the port, the app will listen on port 8000.

Once you’re finished creating the backend app, make sure MongoDB is running and start it by running this command on the terminal:

NODE_ENV=development npm start

You pass the NODE_ENV=development variable, so that configuration is loaded from the local .env file.

The Front-end

The front-end portion of this project serves a couple of functions. It allows users to create conversations, see the list of conversations they are a part of, invite others to conversations they created, and send messages within conversations. These roles are achieved by four pages:

  • a conversations page,
  • a chat page,
  • an error page,
  • a login page.

You’ll call the front-end app twilio-chat-app. A scaffolded starter exists for it on Github. To clone the project and get the starter, run:

git clone https://github.com/zaracooper/twilio-vanilla-js-chat-app.git
cd twilio-vanilla-js-chat-app
git checkout starter

The app takes this structure:

.
├── index.html
├── pages
│   ├── chat.html
│   ├── conversation.html
│   ├── error.html
│   └── login.html
├── scripts
│   ├── chat.js
│   ├── conversation.js
│   └── login.js
└── styles
    ├── chat.css
    ├── main.css
    └── simple-page.css

The styling and HTML markup have already been added for each of the pages in the starter. This section will only cover the scripts you have to add.

Dependencies

The app has two dependencies: axios and @twilio/conversations. You’ll use axios to make requests to the backend app and @twilio/conversations to send and fetch messages and conversations in scripts. You can install them on the terminal by running:

npm i

The Index Page

This page serves as a landing page for the app. You can find the markup for this page (index.html) here. It uses two CSS stylesheets: styles/main.css which all pages use and styles/simple-page.css which smaller, less complicated pages use.

You can find the contents of these stylesheets linked in the earlier paragraph. Here is a screenshot of what this page will look like:

The Error Page

This page is shown when an error occurs. The contents of pages/error.html can be found here. If an error occurs, a user can click the button to go to the home page. There, they can try what they were attempting again.

The Conversations Page

On this page, a user provides the title of a conversation to be created and their username to a form.

The contents of pages/conversation.html can be found here. Add the following code to the scripts/conversation.js file:

window.twilioChat = window.twilioChat || {};

function createConversation() {
    let convoForm = document.getElementById('convoForm');
    let formData = new FormData(convoForm);

    let body = Object.fromEntries(formData.entries()) || {};

    let submitBtn = document.getElementById('submitConvo');
    submitBtn.innerText = "Creating..."
    submitBtn.disabled = true;
    submitBtn.style.cursor = 'wait';

    axios.request({
        url: '/api/conversations',
        baseURL: 'http://localhost:8000',
        method: 'post',
        withCredentials: true,
        data: body
    })
        .then(() => {
            window.twilioChat.username = body.username;
            location.href = '/pages/chat.html';
        })
        .catch(() => {
            location.href = '/pages/error.html';
        });
}

When a user clicks the Submit button, the createConversation function is called. In it, the contents of the form are collected and used in the body of a POST request made to http://localhost:8000/api/conversations/ in the backend.

You will use axios to make the request. If the request is successful, a conversation is created and the user is added to it. The user will then be redirected to the chat page where they can send messages in the conversation.

Below is a screenshot of the conversations page:

The Chat Page

On this page, a user will view a list of conversations they are part of and send messages to them. You can find the markup for pages/chat.html here and the styling for styles/chat.css here.

The scripts/chat.js file starts out by defining a namespace twilioDemo.

window.twilioChat = window.twilioChat || {};

Add the initClient function below. It is responsible for initializing the Twilio client and loading conversations.

async function initClient() {
    try {
        const response = await axios.request({
            url: '/auth/token',
            baseURL: 'http://localhost:8000',
            method: 'GETget',
            withCredentials: true
        });

        window.twilioChat.username = response.data.username;
        window.twilioChat.client = await Twilio.Conversations.Client.create(response.data.token);

        let conversations = await window.twilioChat.client.getSubscribedConversations();

        let conversationCont, conversationName;

        const sideNav = document.getElementById('side-nav');
        sideNav.removeChild(document.getElementById('loading-msg'));

        for (let conv of conversations.items) {
            conversationCont = document.createElement('button');
            conversationCont.classList.add('conversation');
            conversationCont.id = conv.sid;
            conversationCont.value = conv.sid;
            conversationCont.onclick = async () => {
                await setConversation(conv.sid, conv.channelState.friendlyName);
            };

            conversationName = document.createElement('h3');
            conversationName.innerText = 💬 ${conv.channelState.friendlyName};

            conversationCont.appendChild(conversationName);
            sideNav.appendChild(conversationCont);
        }
    }
    catch {
        location.href = '/pages/error.html';
    }
};

When the page loads, initClient fetches the user’s access token from the backend, then uses it to initialise the client. Once the client is initialised, it’s used to fetch all the conversations the user is subscribed to. After that, the conversations are loaded onto the side-nav. In case any error occurs, the user is sent to the error page.

The setConversion function loads a single conversation. Copy and paste the code below in the file to add it:

async function setConversation(sid, name) {
    try {
        window.twilioChat.selectedConvSid = sid;

        document.getElementById('chat-title').innerText = '+ ' + name;

        document.getElementById('loading-chat').style.display = 'flex';
        document.getElementById('messages').style.display = 'none';

        let submitButton = document.getElementById('submitMessage')
        submitButton.disabled = true;

        let inviteButton = document.getElementById('invite-button')
        inviteButton.disabled = true;

        window.twilioChat.selectedConversation = await window.twilioChat.client.getConversationBySid(window.twilioChat.selectedConvSid);

        const messages = await window.twilioChat.selectedConversation.getMessages();

        addMessagesToChatArea(messages.items, true);

        window.twilioChat.selectedConversation.on('messageAdded', msg => addMessagesToChatArea([msg], false));

        submitButton.disabled = false;
        inviteButton.disabled = false;
    } catch {
        showError('loading the conversation you selected');
    }
};

When a user clicks on a particular conversation, setConversation is called. This function receives the conversation SID and name and uses the SID to fetch the conversation and its messages. The messages are then added to the chat area. Lastly, a listener is added to watch for new messages added to the conversation. These new messages are appended to the chat area when they are received. In case any errors occur, an error message is displayed.

This is a screenshot of the chat page:

Next, you’ll add the addMessagedToChatArea function which loads conversation messages.

function addMessagesToChatArea(messages, clearMessages) {
    let cont, msgCont, msgAuthor, timestamp;

    const chatArea = document.getElementById('messages');

    if (clearMessages) {
        document.getElementById('loading-chat').style.display = 'none';
        chatArea.style.display = 'flex';
        chatArea.replaceChildren();
    }

    for (const msg of messages) {
        cont = document.createElement('div');
        if (msg.state.author == window.twilioChat.username) {
            cont.classList.add('right-message');
        } else {
            cont.classList.add('left-message');
        }

        msgCont = document.createElement('div');
        msgCont.classList.add('message');

        msgAuthor = document.createElement('p');
        msgAuthor.classList.add('username');
        msgAuthor.innerText = msg.state.author;

        timestamp = document.createElement('p');
        timestamp.classList.add('timestamp');
        timestamp.innerText = msg.state.timestamp;

        msgCont.appendChild(msgAuthor);
        msgCont.innerText += msg.state.body;

        cont.appendChild(msgCont);
        cont.appendChild(timestamp);

        chatArea.appendChild(cont);
    }

    chatArea.scrollTop = chatArea.scrollHeight;
}

The function addMessagesToChatArea adds messages of the current conversation to the chat area when it is selected from the side nav. It is also called when new messages are added to the current conversation. A loading message is usually displayed as the messages are being fetched. Before the conversation messages are added, this loading message is removed. Messages from the current user are aligned to the right, while all other messages from group participants are aligned to the left.

This is what the loading message looks like:

Add the sendMessage function to send messages:

function sendMessage() {
    let submitBtn = document.getElementById('submitMessage');
    submitBtn.disabled = true;

    let messageForm = document.getElementById('message-input');
    let messageData = new FormData(messageForm);

    const msg = messageData.get('chat-message');

    window.twilioChat.selectedConversation.sendMessage(msg)
        .then(() => {
            document.getElementById('chat-message').value = '';
            submitBtn.disabled = false;
        })
        .catch(() => {
            showError('sending your message');
            submitBtn.disabled = false;
        });
};

When the user sends a message, the sendMessage function is called. It gets the message text from the text area and disables the submit button. Then using the currently selected conversation, the message is sent using its sendMessage method. If successful, the text area is cleared and the submit button is re-enabled. If unsuccessful, an error message is displayed instead.

The showError method displays an error message when it is called; hideError hides it.

function showError(msg) {
    document.getElementById('error-message').style.display = 'flex';
    document.getElementById('error-text').innerText = There was a problem ${msg ? msg : 'fulfilling your request'}.;
}

function hideError() {
    document.getElementById('error-message').style.display = 'none';
}

This is what this error message will look like:

The logout function logouts out the current user. It does this by making a request to the backend which clears their session. The user is then redirected to the conversation page, so they can create a new conversation if they’d like.

function logout(logoutButton) {
    logoutButton.disabled = true;
    logoutButton.style.cursor = 'wait';

    axios.request({
        url: '/auth/token',
        baseURL: 'http://localhost:8000',
        method: 'DELETEdelete',
        withCredentials: true
    })
        .then(() => {
            location.href = '/pages/conversation.html';
        })
        .catch(() => {
            location.href = '/pages/error.html';
        });
}

Add the inviteFriend function to send conversation invites:

async function inviteFriend() {
    try {
        const link = http://localhost:3000/pages/login.html?sid=${window.twilioChat.selectedConvSid};

        await navigator.clipboard.writeText(link);

        alert(The link below has been copied to your clipboard.\n\n${link}\n\nYou can invite a friend to chat by sending it to them.);
    } catch {
        showError('preparing your chat invite');
    }
}

To invite other people to participate in the conversation, the current user can send another person a link. This link is to the login page and contains the current conversation SID as a query parameter. When they click the invite button, the link is added to their clipboard. An alert is then displayed giving invite instructions.

Here is a screenshot of the invite alert:

The Login Page

On this page, a user logs in when they are invited to a conversation. You can find the markup for pages/login.html at this link.

In scripts/login.js, the login function is responsible for logging in conversation invitees. Copy its code below and add it to the aforementioned file:

function login() {
    const convParams = new URLSearchParams(window.location.search);
    const conv = Object.fromEntries(convParams.entries());

    if (conv.sid) {
        let submitBtn = document.getElementById('login-button');
        submitBtn.innerText = 'Logging in...';
        submitBtn.disabled = true;
        submitBtn.style.cursor = 'wait';

        let loginForm = document.getElementById('loginForm');
        let formData = new FormData(loginForm);
        let body = Object.fromEntries(formData.entries());

        axios.request({
            url: /api/conversations/${conv.sid}/participants,
            baseURL: 'http://localhost:8000',
            method: 'POSTpost',
            withCredentials: true,
            data: body
        })
            .then(() => {
                location.href = '/pages/chat.html';
            })
            .catch(() => {
                location.href = '/pages/error.html';
            });
    } else {
        location.href = '/pages/conversation.html';
    }
}

The login function takes the conversation sid query parameter from the URL and the username from the form. It then makes a POST request to api/conversations/{sid}/participants/ on the backend app. The backend app adds the user to the conversation and generates an access token for messaging. If successful, a session is started in the backend for the user.

The user is then redirected to the chat page, but if the request returns an error, they are redirected to the error page. If there is no conversation sid query parameter in the URL, the user is redirected to the conversation page.

Below is a screenshot of the login page:

Running the App

Before you can start the front-end app, make sure that the backend app is running. As mentioned earlier, you can start the backend app using this command on the terminal:

NODE_ENV=development npm start

To serve the front-end app, run this command in a different terminal window:

http-server -p 3000

This serves the app at http://localhost:3000. Once it’s running, head on over to http://localhost:3000/pages/conversation.html; set a name for your conversation and add your username, then create it. When you get to the chat page, click on the conversation, then click the Invite button.

In a separate incognito window, paste the invite link and put a different username. Once you’re on the chat page in the incognito window, you can begin chatting with yourself. You can send messages back and forth between the user in the first window and the second user in the incognito window in the same conversation.

Conclusion

In this tutorial, you learned how to create a chat app using Twilio Conversations and Vanilla JS. You created a Node.js app that generates user access tokens, maintains a session for them, creates conversations, and adds users to them as participants. You also created a front-end app using HTML, CSS, and Vanilla JS. This app should allow users to create conversations, send messages, and invite other people to chat. It should get access tokens from the backend app and use them to perform these functions. I hope this tutorial gave you a better understanding of how Twilio Conversations works and how to use it for chat messaging.

To find out more about Twilio Conversations and what else you could do with it, check out its documentation linked here. You can also find the source code for the backend app on Github here, and the code for the front-end app here.

In-Memory Logins in ASP.NET Core with JWT

Introduction

Some line of business (LOB) applications or internal systems have a predefined set of users and corresponding login information. In situations like those, using a database mechanism to store user information is an overhead. Combine this overhead with Microsoft’s heavy-duty implementation of the Identity framework with persisting concerns in the Entity framework, and you have a recipe for a headache. Those frameworks might be useful in certain situations, but here we have a limited scope and, in situations like this, we can, instead, take a lighter approach towards this functionality.

In this post, we will look at a way to implement authentication functionality in an ASP.NET Core Web application. We will go through all the steps and how things are put together to achieve the desired result. The source code is available in this GitHub repository.

Need Security Tokens for Java? Try JPaseto

To circumvent the issues with JSON Web Tokens, try PASETO; a new security token format, made for quick and easy accessibility, free of the complications of JWT. PASETO, or Platform Agnostic SEcurity TOkens was created by Scott Arciszewski as a draft RFC spec. PASETO can provide the needed security functions that applications need, in addition to reducing the scope of JavaScript Object Signing and Encryption, (JOSE) family of specs, including JWT. 

PASETO is everything you love about JOSE (JWT, JWE, JWS) without any of the many design deficits that plague the JOSE standards.

Understanding the Need for JSON Web Token (JWT)

  • JWT stands for JSON Web Token
  • It is pronounced as JAWT
  • It is Open Standard — RFC7519
  • JWT makes it possible to communicate securely between two bodies
  • JWT is used for Authorization

Video

This tutorial is explained in the below Youtube Video.

Okta Introduces Java Library for PASETOs

Okta, an authentication solution provider, has announced a new Java developer library to streamline use of PASETOs. PASETOs (Platform Agnostic Security Tokens) reduce the scope of JavaScript Object Signing and Encryption (JOSE), but continues to provide the functionality needed to secure applications. PASETOs are said to be easier to use than JOSE or any JWT, with all the same security.

Creating a JWT auth server in 1 second

Security is one of those things you shouldn't play around with yourself, unless you know what you're doing. This is the reason products such as Identity Server has gained such momentum and popularity. However, Identity Server is extremely difficult to configure correctly, and OIDC is also arguably a "hack" on top of OAuth2. JWT on the other hand, is dead simple to understand, and was created explicitly to authenticate and authorise users, contrary to OAuth that was originally created for an entirely different purpose. Hence, JWT is just as secure as OpenID Connect, only a gazillion times easier to understand and implement.

In the following video I demonstrate how to create your own JWT server using Magic in 1 second. Notice, Magic is a commercial product, and you need to pay a small fee to use it in a production environment - But compared to the number of hours you'd have to spend rolling your own Enterprise Single Sign On solution using JWT, I'm confident in that the license costs are small in comparison.

Authenticate Your Angular App With JWTs

No web application should ever be made without a user registration and authentication feature. Authentication gives permission for a user to access the proper resources or services. The fundamental property of HTTP is a stateless protocol that contradicts authentication, which works to keep the state of the user. 

JSON Web Tokens (JWTs) help get around the contradiction between HTTP and authentication. The backend of the Angular app authenticates the JWT, validates the user, and grants them access. To make this happen, the app talks to the backend to generate a token, which then is communicated to the Authorization header to verify the token. You can also address this issue with session-based authentication and cookies. This means that the backend will create a “session cookie,” which provides a process for the server to confirm the user’s identity. 

Origin Authentication and RBAC in Istio with Custom Identity Provider

The concept of access control can be boiled down to two factors: authentication (AuthN) and authorization (AuthZ). While authentication determines the identity of a client based on the data presented to the identity provider (e.g., Google and Microsoft AD), authorization determines whether an authenticated principal may interact with the resource.

Istio supports Token-based end-user authentication with JSON Web Tokens or JWT. In terms of Istio, the process of authentication of the end-user, which might be a person or a device, is known as origin authentication. Istio allows you to validate nearly all the fields of a JWT token presented to it. Since JWT is an industry-standard token format, the origin authentication feature of Istio is compatible with OpenID connect providers such as Auth0, Google Auth, and Key Cloak.

Deep Dive to OAuth2.0 and JWT (Part 3)

dog-shaking-owners-hand
In previous article we have introduced OAuth2.0. In this article let us have a look at JWT.

JSON Web Token (JWT), usually pronounced as “jot,” is an standard () that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. They contain information in terms of claims and are specially used in in space constrained environments such as HTTP. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSAor ECDSA.

How to Secure APIs

To gather insights on the current and future state of API management, we asked IT professionals from 18 companies to share their thoughts. We asked them, "What techniques and tools are most effective for securing APIs?" Here's what they told us:

Authentication

  • We are frequently providing API access to known B2B partners. In these kinds of situations, the ability to limit API access to partner IP addresses only (whitelisting) is very powerful. You still need to authenticate and rate limit, but you have cut traffic to only known partners. This eliminates a great deal of the nefarious traffic that is seen on APIs open to the broader Internet such as brute force attempts to gain access and denial of service attacks.

    Even with IP whitelisting in place, having an API gateway in place is still best practice. This aids in authentication and ensures the backend is only receiving properly formed API calls. 
  • The most common is OAuth and OAuth2 for communicating and securing communications between APIs. Underneath is token-based and claims-based authentication where the APIs are passing digitally signed tokens back and forth to verify the token is representative of who is making the call. 
  • Authentication is unevenly implemented by providers. Even with OAuth, the way tokens are persisted by others can be a problem. How is the lifecycle of tokens managed? Do the tokens get refreshed? Within our own infrastructure, we use one-time tokens heavily scoped to the type of operation we’re trying to do. It comes down to secure token management and certification-based authentication. 
  • Always authenticate APIs before Authorizing — There are many ways to do API authentication, but multi-factor authentication is the commonly recommended approach. For APIs, it is pretty common to get access token using an external process e.g. through OAuth protocol. Authentication keys are most sensitive and must be kept secure; however, it is recommended that a management store be used to automate the whole process.

    That said, authentication alone is not enough to grant access to an API, there should be an authorization step to determine what resources have access to the API. There various ways to check for proper authorization include content-based access control (CBAC), role-based access control (RBAC) or policy-based access control (PBAC) — these methods ensure business data remains fully protected against unapproved access. 

Rate Limit

  • Securing the API environment involves every API touchpoint — authenticating and authorizing API clients (third‑party applications and developers or microservices), rate-limiting API calls to mitigate distributed denial-of-service (DDoS) attacks and protecting the backend applications that process the API calls.

    Some techniques and tools for securing APIs are:

    1) Using JSON Web Tokens (JWTs) in authenticating and authorizing API clients — these tokens include information about the client such as administrative privilege or expiration date. When the client presents a JWT with its API request, the API gateway validates the JWT and verifies that the claims in it match the access policy you have set for the resource the client is requesting.

    2) Defining and implementing access control policies that allow only certain kinds of clients to perform write operations or access sensitive data such as pricing.

    3) Defining Role-based access control that allows only certain users (such as developers within a specific org) to publish APIs that expose sensitive information such as pricing or inventory levels.

    4) Securing the APIs themselves by applying a rate-limiting policy that sets a threshold on the number of requests the API gateway accepts each second (or other time period) from a specified source, such as a client IP address.

    5) Protecting the backend applications using HTTPS — HTTPS protocol should be used traffic between API gateway and the backend systems that process the API requests. 
  • Circuit Breakers - Throttling and Quotas — A good practice is to enforce per-app data usage quota so that the backend won’t be impacted in case of DoS, DDoS attacks or to prevent improper use of the API by unauthorized users. Throttling and quotas per resource not only act as a circuit breaker, but they can also prevent a negative impact on the downstream systems during an unwanted surge in usage. A sophisticated API management platform with policies like quotas and throttling can offer this capability. 

Broader

  • Three key areas in our approach to API security:

    1) Have a prescriptive approach

    2) Think carefully about how application identities are connected to user identities

    3) Think about API security in its broadest sense beyond authentication to mitigate intrusion attempts.

    Prescriptive model: Customers move towards OAuth 2 and overlay with Open ID Connect. There are a lot of choices to be made with OAuth 2, Open ID restricts the choice and directs people on best practices. Ultimately, it’s better to implement a clearly understood standard. Look at a balance between security, usability, and scalability similar to the world of storage.

    Roles of application and user identities: There is a number of API management vendors try to do both. It just isn’t scalable. Identity management is so robust you cannot keep up with the pace of change and new standard. We integrate with external identity management solutions and have full out of the box integration or OAuth2 and Open ID use cases. We have open interfaces for customers to swap us out for their own implementation if they want.

    For broader security concerns: We take the approach of distributing implementation of security. By default, API management is focused on providing an API gateway. We take an approach that an API gateway should be focused on authentication and authorization of traffic. We recommend taking a multi-layered approach and include a web app firewall in a separate layer with Apache Mod Security. When deploying additional security to deploy to the network edge. The last is using heuristics or machine learning to monitor traffic behavior and determine if traffic is malicious.

Other

  • API management security is still one of the areas where people are struggling. We need to recognize that APIs are inherently insecure. We need to relearn what is security for APIs and understand the internet fragility that is being attacked. Cloud providers are more mature. AWS and Azure API management tools provide an additional security layer. Think about the security of the user name, password, keys, and know who is using your APIs. Think about the data now that APIs are exposing legacy systems have the same mindset for securing the data that is there. More APIs consume more user data — how and where do you store? 
  • APIs can be a security risk in two key ways. First, as dependencies are like a set of Russian dolls. Inside your dependencies are the dependencies’ dependencies… and inside them your dependencies, dependencies’, dependencies’ and so on and so on. From a functionality perspective, this enables each development organization to focus on the code for which they add the most value.

    However, from a security perspective, as the end consumer, you inherit the entire chain of vulnerabilities. Application security professions must go beyond basic software composition analysis, which matches known CVEs to version strings because they only go one layer deep. As an example, Jackson-databind is a popular open-source library that is leveraged in many 3rd party communications SDKs. 

    The “jacksploit” deserialization vulnerability, that has plagued many version of the library, was exploitable for some of the SDK users, but the vulnerable version of Jackson-databind was obfuscated and not identifiable by traditional SCA. Hence, security professionals must look for tools that evaluate how the source code works, rather than relying on looking up CVE databases.

    Second, since APIs often pass data externally, it is important to map critical data flows to ensure critical data isn’t inadvertently leaked into them. This means identifying which variables are critical and mapping their journey from source to sink and all the transformations on the way. That way unencrypted or unredacted critical data can be caught in development before leaky routes are pushed into production. 
  • You need to have good API management platform in place. It will enforce the rules and roles you put in place to control who has access to what data. You can use a gateway or platform with options or use built-in authentication and authorization tools. Being able to control access in a friendly, easy-to-configure way. The web interface is the best way to do and control who has access without requiring a deployment or digging into the server and update complex configuration or files. Control at the gateway level and then it's easy to configure or manage. 
  • API security is important and requires a comprehensive approach to prevent data leakage. Because of the way data can be shared via API, users can inadvertently leak data in just a few clicks This occurs when employees authorize OAuth API connectivity between sanctioned apps and unsanctioned, third-party apps, granting the latter permissions to read and even write data (e.g. Email, Google Drive, Box, Office 365, and SharePoint). This functionality provides great business value but can expose data when not properly secured.

    Structured data can be synced between apps via JSON, XML, or file-based APIs (to name a few). This structured data may contain sensitive information that should be protected with encryption or tokenization. In these scenarios, it’s critical to leverage a bring-your-own-key (BYOK) approach; this is because data is not truly secure if an encryption key is stored with the app that houses the encrypted data. Fortunately, leading multi-mode cloud access security brokers (CASBs) provide visibility and real-time protections for all of these use cases, ensuring data is secure on any app, any device, anywhere. 
  • When it comes to securing APIs, serving them via HTTPS is a basic yet absolute requirement. It’s relatively straightforward to setup: the tools and processes involved are widely available and well understood. Moving away from the common username/password couple is also a practice we would recommend. Usernames are an easy target for social engineering. Passwords, while uses are evolving, are still too often created manually — constraining their size or composition leads to frustrating experiences. We prefer to go for API keys instead.

    Our API keys have the form we devised. We can assign several API keys to the same customer. A single API key can be exchanged to several entities for a given customer. We can revoke API keys. All of this can be done independently from actual customer account management. 
  • Enforce Encryption all the way — A fundamental security requirement is to encrypt the data using the latest cryptographic protocols to ensure proper protection while the data is in transit. Weak protocol version and ciphers should always be disabled. TLS1.0 and 1.1. Encryption in transit is great, but not enough. It should be enforced all the way, including while the data is at rest. 
  • Audit and Logging — Auditing is usually an afterthought, whereas it should be considered as a feature during the API development cycle. The more logging the better, since the cost of having this capability far outweighs the cost of not having them when you need this capability the most (during the attack). Logging should be systematic and independent, and resistant to log injection attacks. Auditing should be used for detecting and proactively deterring attacks.
  • Alert and Monitoring — Alert and monitoring are key to see what is going on in your world to protect against bad behaviors. Logging and auditing should be used proactively in detecting and alerting you against threats and attack.
  • Protect HTTP Verbs and Cross-Site Request Forgery (CSRF) — It is common with APIs to allow multiple methods for a URL for different operations. For example, a GET request to read data, PUT to update data, POST to create data, and DELETE to delete data. It is important for the APIs to restrict the allowed HTTP verbs so that only the allowed methods will work, while the others will result in an exception along with a proper response code sent back to the client. It is important for PUT, POST, and DELETE methods to be protected from Cross-Site Request Forgery (CSRF). Typically one would use a token-based approach to enforce this capability.
  • There's an external way and an internal way. External SSL or TLS for encryption, make sure traffic is secure and encrypted. Make sure the client and server are both SSL. Make sure you are encrypting data. Know who the person is that’s calling the application – OAuth, OpenID connect. The last one is securely based on who you are. External is a web application firewall to scan the request and make sure nothing is suspicious. Internally see other stuff like after already past the gateway two microservices talking to each other with MTLS. Limit the audience of the job. Only give permission to go to a certain place. Identity management.
  • Because we’ve taken microservices to their logical extreme, with each team producing APIs on the public internet for direct consumption, we don’t have the benefit of a gateway as a chokepoint for access.  In our world, there’s no such thing as a “backend API.”  That makes securing our APIs an especially diffuse problem, but it also means no one in our company relies on a false sense of security built on assumptions that only the “right” callers can reach their APIs.

    As a result, every team is responsible for the security of their APIs, and we achieve this by following a few key principles:

    1) Every caller (human or programmatic) authenticates using the same standard (OAuth2 bearer tokens in the form of JWTs signed by our identity vendor, Auth0)

    2) Every API validates the identity and access rights of the caller for each action

    3) Every API presents the original caller’s credentials for subsequent requests it must make to our APIs

    In this way, every API knows exactly who initiated a call chain and can accurately reason about the access rights of the caller. These techniques have served us well as every developer has the same expectations and responsibilities for securing access. We haven’t found great tooling to automate security testing.  We have found that human ingenuity and vigilance are always necessary.

    We use crowdsourced testing services for an external perspective but also empower internal teams to red flag, up to the highest levels of the organization, any security issues they identify in anyone’s APIs.  The latter works particularly well because, again, we’re all responsible for the security of APIs exposed on the public internet, so every developer has a well-honed sense for good and bad practices.
  • For RESTful APIs — OAuth2 and JWT are pretty much de-facto standard. We use both. OAuth from customer to our cloud-platform and JWT internally for performance benefits. With the growing popularity of Webhooks, it is a must to sign your payloads when customers “sign-up” for API notifications.

    We use HMAC signatures so customers can confirm data is coming from us and hasn’t been altered. I’m surprised how frequently we encounter API providers who are still not doing that. We use more advanced measures on some occasions, but those are sufficient for most cases.

    You also have to have some sort of single API entry point before API calls hit your microservices or monolith applications. Normally this is an API gateway that will block any calls very fast if they haven’t been properly authenticated.
  • Most security issues result from too much complexity and too many APIs. Simplicity is key, with all data security being handled centrally so there is one place to keep it current. Protocols like GraphQL and SPARQL allow consumers to describe their own data needs through a single API, substantially minimizing the need for so many APIs to manage to begin with.
  • Restrict the list of users that are authorized to access the token. Token-based authorization is there for a reason. Second, there’s a need to track and control the use of resources allocated for a single user, which is absolutely essential to protect from DDoS attacks.

Here's who shared their insights:

Build a Simple Spring Boot App With Spring Data JPA and Postgres

Just about every application needs a way to save and update data, typically a resource server that is accessible via HTTP. Generally, this data must be secured. Within the Java ecosystem, Spring makes building secure resource servers for your data simple. When coupled with Okta for secure user management, you get professionally maintained OAuth 2.0 and JWT technologies easily integrated into Spring Boot via Spring Security.

In this tutorial, you’re going to build a resource server using Spring Boot and Spring Data JPA. On top of that, you’re going to implement a group-based authentication and authorization layer using OAuth 2.0.

Integrating Spring Boot and React With Spring Security: Basic and JWT Authentication

This guide helps you setup Spring Security with Basic and JWT authentication with a full stack application using React as a frontend framework and Spring Boot as the backend REST API. We will be using JavaScript as the frontend language and Java as the backend language.

You Will Learn

  • How to use React as a Frontend Framework
  • How to use Spring to create Backend REST Service API
  • How to setup Basic Authentication with Spring Boot
  • How to use Spring Security with Spring Boot
  • How to use JWT Security with Spring Boot
  • How to verify Basic Authentication at user login calling a REST API with React
  • How to make secured REST API calls from React frontend
  • How to use sessionStorage to track tokens in the React frontend application

10-Step Reference Courses

Step 0: Get an Overview of the Full Stack Application

Understanding Basic Features of the Application

The following screenshot shows the application we would like to build:

OAuth2 Tips: Token Validation

Bearer Token Types

There are two types of OAuth2 bearer tokens:

  • General Token that represents a string that has no meaning for the client (e.g., 2YotnFZFEjr1zCsicMWpAA). That type of bearer token cannot be validated by the Resource Server without direct communication with an Authorization Server.
  • JWT Token represents the JSON object with statements (claims) about the user and token. The JWT token contains three separate parts: header, payload, and signature — each of them are base64-encoded for transferring. JWT tokens are not a part of core OAuth2 specification but mandatory for use with OpenID Connect. JWT token is the most popular way to exchange information about current authentication between microservices. More details can be found here.

Token Validation Methods

OAuth2 tokens can be validated using the following methods: