IV - Connecting .net Identity + OIDC to Twilio

IV - Connecting .net Identity + OIDC to Twilio

This article is part of a series that explains how to Set up an Identity Server and connect it to Twilio.

Article I: Understanding Identity Server

Article II: Creating a token server, STS, API using .net 6, MySql, and Duende Identity Server

Article III: Connecting .net Identity Server with React OIDC Client

Now that you've set up an Identity Server with Data Stores in MySql and you've connected OIDC react client. The next step is to integrate react-oidc into Twilio. Twilio provides a start-up app called Twilio-react-app which is a collaboration between Twilio and React.js. This is a great starting point.

Understanding Twilio Video Rooms

Twilio offers 3 types of video rooms that are exposed through different APIs which have different capabilities and a range of pricing.

  • WebRTC Go Room

  • Peer-to-peer (P2P) Room

  • Group Room

There is a fourth small group room which is a legacy API. Twilio recommends using a Group Room instead of a Small Group Room.

WebRTC Go Room

A WebRTC Go Room (aka "Go Room") can be used for one-on-one video calls. Participant minutes and TURN server usage is FREE. Go Rooms use a peer-to-peer topology and are similar to P2P Rooms. However, the maximum number of participants in a Go Room is two. There can be a maximum of 500 concurrent participants at a time per account; for example, 250 rooms with 2 participants.

Video P2P Room

In a Peer-to-Peer Room (aka "P2P Room"), Participants exchange media directly so that:

  • Media is encrypted end-to-end (E2E) using WebRTC security protocols.

  • Twilio does not mediate the media exchange, which takes place through direct communication among Participants. The only exception is when media exchange requires TURN. In that case, a TURN server will blindly relay the encrypted media bits to guarantee connectivity. The TURN server cannot decrypt or manipulate the media.

  • As Twilio does not intercept the media in P2P Rooms, it is not possible to record or transcode the media or make it interoperate with other RTC services.

  • Despite not being in the media path, Twilio manages the signaling path, making it possible for Participants to discover each other and negotiate the communications in agreement with the application and SDK requirements. Hence, signaling connectivity to Twilio’s cloud is still necessary.

Video Group Room

  • Participants publish media to a Twilio Selective Forwarding Unit (SFU). An SFU is a Media Server that decrypts the media, processes, re-encrypts, and routes the media tracks to the correct destinations.

  • Media is not E2E encrypted, as the SFU keeps media unencrypted in memory to process it.

  • Services such as recordings and public switched telephone network (PSTN) interoperability can be added, as Twilio acts as media middleware.

Which Room Should I Use?

  • In general, Group Rooms provide the most functionality and flexibility. They support multi-party calls of more than 2 participants, recordings, PSTN dial-in/dial-out, and additional quality controls.

  • If your use case is 2-3 participants, you do not need recordings or PSTN support, and you need end-to-end encryption of the media for compliance reasons, then P2P Rooms will work well for you.

  • WebRTC Go Rooms are designed for developers looking to launch their applications as quickly as possible with minimal cost. These Rooms are functionally similar to P2P Rooms. However, there is a maximum of 2 participants in a single Room at one time and a scaling limit of 500 participants in WebRTC Go Rooms at any instant in time.

For this project, we will be using the Group Video Rooms as this is the one that satisfies the project prerequisites.

Setting Up Twilio Video App

Create a Twilio Account

  • Create an account in the Twilio Console.

  • Click on 'Settings' and take note of your Account SID.

  • Create a new API Key in the API Keys Section under Programmable Video Tools in the Twilio Console. Take note of the SID and Secret of the new API key.

  • Create a new Conversations service in the Services section under the Conversations tab in the Twilio Console. Take note of the SID generated.

  • Store your Account SID, API Key SID, API Key Secret, and Conversations Service SID somewhere safe. Later we will add these IDs to a .env in the root level of the application (example below).

TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_API_KEY_SID=SKxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_API_KEY_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_CONVERSATIONS_SERVICE_SID=ISxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Download the Twilio-react-app

Prerequisites

Ensure that you have the minimum required prerequisites. Ensure that the installed version of node is:

  • Node.js v14+

  • NPM v6+ (comes installed with newer Node versions)

Type the commands above in terminal to ensure that you have satisfied the minimum requirements

$ node --version
$ npm --version

Clone the repository and install Dependencies

$ git clone https://github.com/twilio/twilio-video-app-react.git
$ cd twilio-video-app-react
$ npm install

If you would like to use YARN or PNPM follow this article: NPM vs YARN vs PNPM and more on dependency resolution

Install Twilio CLI and RTC Plugin

Run the following command to install the twilio-cli and the Twilio RTC plugin

$ npm install -g twilio-cli

Login to the Twilio CLI. You will be prompted for your Account SID and Auth Token, both of which you can find on the dashboard of your Twilio console (saved in a file in the first step "Create Twilio Account"). Use the following command to login

$ twilio login

Once you provide the required credentials you will be logged in. You can finally install the Twilio RTC plugin which is required by this app. To install run the following command in terminal

$ twilio plugins:install @twilio-labs/plugin-rtc

Some Handy Twilio CLI commands

$ twilio plugins:update 
// Updates all Twilio Plugins

$ twilio profiles:list
// Lists Twilio Profiles and it can be used to confirm you are using the right account

$ twilio rtc:apps:video:view
// Lists all severless deployments of Twilio apps

$ twilio rtc:apps:video:delete
// Deletes serverless deployment of a Twilio app

Running and Testing the app

You can deploy the code onto the Twilio servers using the following command. The --override option overwrites any existing deployment.

$ npm run deploy:twilio-cli -- --override

When you run the command the terminal output will list some important information. Let's take a look and check what is of value here.

Web App URL: https://twilio-video-app-react-3483-dev.twil.io?passcode=3339983483
Passcode: 3339983483
Expires: Wed Jan 25 2023 09:11:59 GMT-0500
Room Type: group
Edit your token server at: https://www.twilio.com/console/functions/editor/ZSafaa84bfb1c44aef3f74a8f856fe6502/environment/ZE9c35fb55768ab8803eb4490599cd2bde/function/ZHfa227df04a69857c6d27b543dd57f70d

As you can see the deployment process generates

  • Web APP URL: this is the URL where you can see the deployed app.

  • Passcode: Generated passcode to access the App

  • Room Type: Group

  • Edit your token server at: This will take you to the Twilio editor where you can edit files that have been deployed to their server.

If you've set up everything correctly you should see the following when you visit the deployment URL generated. You can test by using two different browsers to connect to each other. Use the passcode generated in terminal above.

On the next screen, you will be prompted for a UserName (must be unique) and a room name (must be the same to test two instances connecting together). I will use Chrome as the username and room as the room name.

You will be asked to give permission to the app to access the camera and microphone. Allow the app to access them.

Repeat the steps above in another Browser. Make sure that you use a different username. Make sure that you have the same room name. I used Firefox for a user and room for the room name. You will see two different clients connecting through video for a video conversation.

Running the app Locally

To test and debug your code you will need to run the code locally. At this point, you need to create a .env file with the Account Settings. Get the info retrieved in Create a Twilio Account and add it into a .env file at the root of the project.

TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_API_KEY_SID=SKxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_API_KEY_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_CONVERSATIONS_SERVICE_SID=ISxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Now the local token server (see server/index.ts) can dispense Access Tokens to connect to a Room and a Conversation. See .env.example for information on additional environment variables that can be used (downloaded as part of git).

You can now run the app locally using the command

npm start

If you use the npm start command you should see the app load at localhost:3000 and see the same behavior as we did before when we deployed to Twilio.

Securing the Twilio app with OIDC

Now that we have got the app run locally we have to secure it with OIDC. This will require a few steps and some explanation. Let's go through this step by step. This section assumes that you have already gone through the steps in the article Connecting .net Identity Server with React OIDC Client. If you haven't, you might find this section difficult to follow.

Securing the client-side code:

You need to secure the client with SSL to be allowed to communicate with Identity Server. Follow these steps:

1. Install mkcert using brew

$ brew install mkcert

2. Create a local certificate authority (CA) by running the following command.

$ mkcert -install

3. Run the following to generate the certificate and store it in the root folder of the project.

$ mkcert -key-file ./reactcert/key.pem -cert-file ./reactcert/cert.pem "localhost"

4. Configure React to Use SSL
In package.json, add a path that points to the SSL certificates.

"scripts": {
    "start":
        "HTTPS=true SSL_CRT_FILE=./reactcert/cert.pem SSL_KEY_FILE=./reactcert/key.pem react-scripts start"
}

Now when the code is run using npm start it will set HTTPS to true and use the generated certificate to secure the client.

Test this by running the code and making sure that the lock in the URL address bar is there and locked.

Integrate the react-oidc client

I've covered a lot on why @axa-fr/react-oidc and how to set a client in the article: Connecting .net Identity Server with React OIDC Client. In this section, we will do a very similar setup with some added code to accommodate the Twilio structure

Getting Started React using create-react-app

Use NPM YARN or PNPM to install the @AXA-fr/react-oidc package

$ npm install @axa-fr/react-oidc --save

If you have already used PNPM or YARN, it is best to use the same package manager. If you face problems with dependency resolution read this article: NPM vs YARN vs PNPM and more on dependency resolution

If you need a very secure mode where refresh_token and access_token will be hidden behind a service worker that will proxy requests. The only file you should edit is "OidcTrustedDomains.js".

// OidcTrustedDomains.js

// Add bellow trusted domains, access tokens will automatically injected to be send to
// trusted domain can also be a path like https://www.myapi.com/users, 
// then all subroute like https://www.myapi.com/useers/1 will be authorized to send access_token to.

// Domains used by OIDC server must be also declared here
const trustedDomains = {

// This line is from the git repository 
default:["https://demo.duendesoftware.com", "https://www.myapi.com/users", new RegExp('^(https://[a-zA-Z0-9-]+.domain.com/api/)')]

// This is the line that I use in my code 
 default: ['https://localhost:44310', 'https://localhost:44303']
};

The "OidcSecure" component triggers authentication if the user is not authenticated. The children of that component can only be accessed once you are signed in.

Example

import React from 'react';
import { OidcSecure } from '@axa-fr/react-oidc';
const AdminSecure = () => (
  <OidcSecure>
    <h1>My sub component</h1>}
  </OidcSecure>
);
export default AdminSecure;

The OidcProvider module contains the configuration definition, Token_renew_mode is an enumerated list

Example

import React from 'react';
import { render } from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import { OidcProvider } from '@axa-fr/react-oidc';
import { TokenRenewMode } from '@axa-fr/react-oidc';
import Header from './Layout/Header';
import Routes from './Router';

// This configuration use the ServiceWorker mode only
// "access_token" will be provided automaticaly to the urls and domains configured inside "OidcTrustedDomains.js"
const configuration = {
  client_id: 'interactive.public.short',
  redirect_uri: window.location.origin + '/authentication/callback',
  silent_redirect_uri: window.location.origin + '/authentication/silent-callback', // Optional activate silent-signin that use cookies between OIDC server and client javascript to restore the session
  scope: 'openid profile email api offline_access',
  authority: 'https://demo.duendesoftware.com',
  service_worker_relative_url:'/OidcServiceWorker.js',
  token_renew_mode: TokenRenewMode.access_token_invalid,
  service_worker_only:true,
};

const App = () => (
    <OidcProvider configuration={configuration} >
      <Router>
        <Header />
        <Routes />
      </Router>
    </OidcProvider>
);

render(<App />, document.getElementById('root'));

Now that we understand what each of those modules does, let's see how we use them in our project. Here is the finalized App.tsx

import React from 'react';
import { styled, Theme } from '@material-ui/core/styles';
/********************************************************/
// Custom import added by HS to allow OIDC Authentication
import { OidcProvider } from '@axa-fr/react-oidc';
import { OidcSecure } from '@axa-fr/react-oidc';
import { TokenRenewMode } from '@axa-fr/react-oidc';
/********************************************************/
import MenuBar from './components/MenuBar/MenuBar';
import MobileTopMenuBar from './components/MobileTopMenuBar/MobileTopMenuBar';
import PreJoinScreens from './components/PreJoinScreens/PreJoinScreens';
import ReconnectingNotification from './components/ReconnectingNotification/ReconnectingNotification';
import RecordingNotifications from './components/RecordingNotifications/RecordingNotifications';
import Room from './components/Room/Room';
import useHeight from './hooks/useHeight/useHeight';
import useRoomState from './hooks/useRoomState/useRoomState';

/********************************************************/
// Custom Config OIDC object initialization added by HS to allow OIDC Authentication
const configurationOIDC = {
  client_id: 'react-oidc-twilio-client',
  redirect_uri: window.location.origin + '/#authentication-callback',
  silent_redirect_uri: window.location.origin + '/#silent-callback',
  silent_login_uri: window.location.origin + '/#silent-login',
  scope: 'openid profile email skoruba_identity_admin_api offline_access',
  authority: 'https://localhost:44310',
  refresh_time_before_tokens_expiration_in_second: 60,
  // storage: sessionStorage,
  service_worker_relative_url: '/OidcServiceWorker.js',
  service_worker_only: true,
  // monitor_session: true,
  token_renew_mode: TokenRenewMode.access_token_invalid,
  response_type: 'code id_token token',
};
const Container = styled('div')({
  display: 'grid',
  gridTemplateRows: '1fr auto',
});

const Main = styled('main')(({ theme }: { theme: Theme }) => ({
  overflow: 'hidden',
  paddingBottom: `${theme.footerHeight}px`, // Leave some space for the footer
  background: 'black',
  [theme.breakpoints.down('sm')]: {
    paddingBottom: `${theme.mobileFooterHeight + theme.mobileTopBarHeight}px`, // Leave some space for the mobile header and footer
  },
}));

export default function App() {
  const roomState = useRoomState();
  // Here we would like the height of the main container to be the height of the viewport.
  // On some mobile browsers, 'height: 100vh' sets the height equal to that of the screen,
  // not the viewport. This looks bad when the mobile browsers location bar is open.
  // We will dynamically set the height with 'window.innerHeight', which means that this
  // will look good on mobile browsers even after the location bar opens or closes.
  const height = useHeight();
  return (
    <Container style={{ height }}>
      <OidcProvider configuration={configurationOIDC}>
        <OidcSecure>
          {roomState === 'disconnected' ? (
            <PreJoinScreens />
          ) : (
            <Main>
              <ReconnectingNotification />
              <RecordingNotifications />
              <MobileTopMenuBar />
              <Room />
              <MenuBar />
            </Main>
          )}
        </OidcSecure>
      </OidcProvider>
    </Container>
  );
}

I'll go over the different sections of customized code

Imports

/********************************************************/
// Custom import added by HS to allow OIDC Authentication
import { OidcProvider } from '@axa-fr/react-oidc';
import { OidcSecure } from '@axa-fr/react-oidc';
import { TokenRenewMode } from '@axa-fr/react-oidc';

This code imports the modules from the @AXA-fr/react-oidc package. In Brief, OidcProvider holds the OIDC client configuration. OidcSecure, secures a component and TokenRenewalMode contains an enumeration of modes.

Configuration

/********************************************************/
// Custom Config OIDC object initialization added by HS to allow OIDC Authentication
const configurationOIDC = {
  client_id: 'react-oidc-twilio-client',
  redirect_uri: window.location.origin + '/#authentication-callback',
  silent_redirect_uri: window.location.origin + '/#silent-callback',
  silent_login_uri: window.location.origin + '/#silent-login',
  scope: 'openid profile email skoruba_identity_admin_api offline_access',
  authority: 'https://localhost:44310',
  refresh_time_before_tokens_expiration_in_second: 60,
  // storage: sessionStorage,
  service_worker_relative_url: '/OidcServiceWorker.js',
  service_worker_only: true,
  // monitor_session: true,
  token_renew_mode: TokenRenewMode.access_token_invalid,
  response_type: 'code id_token token',
};

Note that service worker is enabled and the config is set to service_worker_only:TRUE

Securing the content

 <Container style={{ height }}>
      <OidcProvider configuration={configurationOIDC}>
        <OidcSecure>
          {roomState === 'disconnected' ? (
            <PreJoinScreens />
          ) : (
            <Main>
              <ReconnectingNotification />
              <RecordingNotifications />
              <MobileTopMenuBar />
              <Room />
              <MenuBar />
            </Main>
          )}
        </OidcSecure>
      </OidcProvider>
    </Container>

As you can see that I secure all the code inside the container with <OidcSecure>. <OidcSecure> needs to be placed inside a <OidcProvider configuration= { %CONFIGURATION DEFINITION% }>

Now when you run the code you should see the following

As you can see the Oidc Secure Module checks for the user Access token and if it doesn't exist, the user will be redirected to enter their username and password to sign in. After you sign you will receive the following screen

This is the same room select component modified to include the company logo and an Emergency Call Button. You can now proceed the same way we did earlier to test the work.