User authentication
This following section is aimed at Software Engineers.
Overview
Understand what you need to do to secure your web chat integration and authenticate your users.
All messages that are sent from the web chat are encrypted. The web chat client uses an RSA signature with SHA-256 (RS256) to encrypt communication. RS256 signatures use a sophisticated type of RSA encryption. An RSA key pair includes a private and a public key. The RSA private key is used to generate digital signatures, and the RSA public key is used to verify digital signatures. The complexity of the RSA algorithm that is used to scramble the message makes it nearly impossible to unscramble the message without the key.
In this article you will learn how to configure Moveo.AI to:
- Authenticate your users and ensure that messages sent from the web chat to your assistant come from your customers only.
- Pass sensitive information for your users.
The following diagram illustrates the requests that are sent back and forth to authenticate a request.
Before you begin
The process you follow to add the web chat to your website is simple. But this simplicity also means that it can be misused, therefore depending on your use-case it is important to verify the identity of the users messaging your Moveo.AI Assistant.
Before you can verify the origin of messages you need to do the following:
- Create a RS256 private/public key pair.
- Private Key:
openssl genrsa -out key.pem 2048
- Public Key:
openssl rsa -in private.pem -outform PEM -pubout -out public.pem
- Private Key:
- Add your public key in your web chat definition within the Moveo platform.
Enable Authentication
To enable user authentication complete the following steps:
- Log in to your Moveo.AI account.
- Go to Integrations > Web > Advanced security settings > Activate end-to-end encryption and paste your public key in the designated area.
- To prove that a message is coming from your website, each message that is submitted from your web chat implementation must include the JSON Web Token (JWT) that you created earlier.
After your backend creates and signs the JWT token you can pass it as an argument to the web chat initialization function. For example:
<script src="https://cdn.jsdelivr.net/npm/@moveo-ai/web-client@latest/dist/web-client.min.js"></script>
<script>
MoveoAI.init({
integrationId: "YOUR_INTEGRATION_ID",
identityToken: "YOUR_JWT",
})
.then((instance) => console.log("connected"))
.catch((error) => console.error(error));
</script>
Authenticating Users
After you generate your public/private RSA keys, use your private key to sign a JSON Web Token (JWT). You will pass the token with the messages that are sent from your website as proof of their origin.
The JWT payload must specify values for the following claims:
iss
(required): Represents the issuer of the JWT and it could be something like the name of your company.sub
(required): The value you specify for sub is used as theexternal_id
within Moveo that allows you to authenticate a user. Therefore, this value must either be scoped to be locally unique in the context of the issuer or be globally unique. Furthermore, theexternal_id
can be used to make requests to delete user data.exp
(required): Represents the expiration time on or after which the JWT cannot be accepted for processing. Many libraries set this value for you automatically. Set a short-lived exp claim with whatever library you use. Moveo.AI will reject JWT tokens which have anexp
claim more than 5 minutes into the future.
Most programming languages offer JWT libraries that you can use to generate a token. The following code samples illustrate how to generate a JWT token.
- NodeJS
- Go
// Sample NodeJS code on your server.
const jwt = require("jsonwebtoken");
/**
* Returns a signed JWT generated by RS256 algorithm.
*/
function signJWT() {
const payload = {
sub: "some-user-id", // Required
iss: "yourdomain.com", // Required
};
// The "expiresIn" option adds an "exp" claim to the payload.
return jwt.sign(payload, process.env.YOUR_PRIVATE_RSA_KEY, {
algorithm: "RS256",
expiresIn: "10000ms",
});
}
import (
"crypto/x509"
"encoding/pem"
"os"
"time"
"github.com/golang-jwt/jwt"
)
func signJWT() (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
"sub": "some-user-id", // Required
"iss": "yourdomain.com" // Required
"exp": time.Now().Add(time.Second * 3).Unix(), // Required
})
privateRSAKey, _ ok := os.LookupEnv("YOUR_PRIVATE_RSA_KEY")
block, _ := pem.Decode([]byte(privateRSAKey))
if block == nil {
return "", errors.New("error creating JWT block is nil")
}
k, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return "", err
}
signed, err := token.SignedString(k)
if err != nil {
return "", err
}
return signed, nil
}
Passing Sensitive Data
Once you create a web integration, under "Advanced Security Settings" you will notice that Moveo.AI automatically generates a public RSA key for you. You can optionally copy the generated public key that is provided by Moveo.AI, and use it to add an additional level of encryption to support passing sensitive data from the web chat.
For example, you would need to do this if you wanted to pass additional information to your virtual assistant such as a contract id, credit card number, internal url, loyalty level, security tokens etc. that can be leveraged from your virtual assistant to offer a more personalized service.
More precisely, imagine you might start a business process for a VIP customer that is different from the process you start for less important customers. You likely do not want non-VIPs to know that they are categorized as such. But you must pass this information to your dialog because it changes the route of the conversation. Finally, you would want to make this information available to your live chat agents who in turn can assist your client better.
You can pass the customer VIP status as an encrypted variable. This private context variable will be available for use by the dialog and webhooks, but not by anything else.
Information that is passed to your virtual assistant in this way is stored in the session context and is never sent back to the web chat.
Encryption Overview
We encrypt sensitive context information using AES-256/CBC/PKCS5Padding encryption.
AES (Advanced Encryption Standard) is a symmetric encryption algorithm and is one of the most advanced and secure ways of encrypting data of arbitrary size.
We use AES instead of RSA to encrypt sensitive information to overcome the limitation of RSA algorithms around the size of the encrypted data. More precisely, RSA is only able to encrypt data to a maximum amount equal to your key size (2048 bits = 256 bytes, 4096 bits = 512 bytes), minus any padding and header data (11 bytes for PKCS#1 v1.5 padding).
As a result, it is often not possible to encrypt relatively large messages with RSA directly. Since we need to allow to encode sensitive information larger than 512 bytes, we follow this strategy:
- Generate a 256-bit random keystring K and 128-bit random initialization vector IV
- Encrypt your sensitive information with AES-CBC-256 with K and IV
- Encrypt K, IV with RSA.
- Send both to Moveo.AI
AES-256/CBC/PKCS5Padding
To pass private information you need to create a "context"
claim (AES encrypted) along with additional "encryption_key"
and "init_vector"
claims (RSA encrypted).
More precisely, you can follow these steps:
- Create a context object with sensitive information that you want your assistant to store
- Sign the context object using AES-256/CBC/PKCS5Padding encryption with random Key and Initialization Vector:
- Key length can be either 16 bytes or 32 bytes (preferred)
- Initialization vector length has to be 16 bytes
- Resulting cipher context has to be padded with PKCS5 Padding (this step is typically done automatically from your libary)
- Finally convert the padded cipher to base64 and add it as a
"context"
claim to your JWT
- Sign your Key and Initialization Vector you used in step (2) using RSA PKCS1 OAEP method
- Copy the generated RSA PKCS1 public key from your web integration (as depicted in the image above)
- Using the generated RSA key, sign the AES keystring and add it to the JWT claim as
"encryption_key"
- Using the generated RSA key, sign the AES initialization vector and add it to the JWT claim as
"init_vector"
Here is an example of how to encrypt any arbitraty "context" using this encryption method:
- NodeJS
- Go
// Sample NodeJS code on your server.
import crypto from 'crypto';
import jwt from 'jsonwebtoken';
import RSA from 'node-rsa';
function getAlgorithm(keyBase64) {
const key = Buffer.from(keyBase64, 'base64');
switch (key.length) {
case 16:
return 'aes-128-cbc';
case 32:
return 'aes-256-cbc';
}
throw new Error('Invalid key length: ' + key.length);
}
function encryptAES(plainText, keyBase64, ivBase64) {
const key = Buffer.from(keyBase64, 'base64');
const iv = Buffer.from(ivBase64, 'base64');
const cipher = crypto.createCipheriv(getAlgorithm(keyBase64), key, iv);
let encrypted = cipher.update(plainText, 'utf8', 'base64');
encrypted += cipher.final('base64');
return encrypted;
}
/**
* Returns a signed JWT generated by RS256 algorithm
* encrypts the context claim using AES
* encrypts the Key, IV claims using RSA
*/
function signJWT(context) {
let context = {
user: {
gender: 'male',
credit_card: "123456789",
password: "102324",
email: 'panoskaragiannis@moveo.ai',
display_name: 'Panagiotis Karagiannis',
avatar: 'https://upload.wikimedia.org/wikipedia/commons/5/5c/Flag_of_Greece.svg',
address: '50 Pastor St, Paramus, NJ',
language: 'en',
phone: '+123456789',
is_vip: true,
external_details: {
from: '02-01-2022',
agent: "C25",
},
},
contract_id: '1234959595',
has_requested_refund: true,
}
// Encryption Key: 32 bytes key (256 bits)
const key = '(H+MbQeThWmZq4t7w!z%C&F)J@NcRfUj';
const keyBase64 = 'KEgrTWJRZVRoV21acTR0N3cheiVDJkYpSkBOY1JmVWo=';
// Initialization Vector: 16 bytes (128 bits)
const iv = '*F-JaNdRgUkXp2s5';
const ivBase64 = 'KkYtSmFOZFJnVWtYcDJzNQ==';
const rsaKey = new RSA();
rsaKey.setOptions({
environment: 'browser',
encryptionScheme: {
'hash': 'sha256',
'scheme': 'pkcs1_oaep',
},
});
rsaKey.importKey(process.env.MOVEO_GENERATED_PRIVATE_KEY);
// Encrypt Key and Init Vector using RSA
// Moveo.AI will use these to decrypt the context
encryption_key = rsaKey.encrypt(key, 'base64');
init_vector = rsaKey.encrypt(iv, 'base64');
// Encrypt the context object using AES
const ctx_encrypted = encryptAES(
JSON.stringify(payload.context),
keyBase64,
ivBase64
);
const payload = {
sub: 'some-user-id', // Required
iss: 'yourdomain.com' // Required
context: ctx_encrypted, // AES Encrypted context
encryption_key: encryption_key // RSA encrypted key
init_vector: init_vector // RSA encrypted initialization vector
};
// The "expiresIn" option adds an "exp" claim to the payload.
return jwt.sign(payload, process.env.YOUR_PRIVATE_RSA_KEY, {
algorithm: 'RS256',
expiresIn: '10000ms'
});
}
import (
"crypto/x509"
"encoding/pem"
"os"
"time"
"github.com/golang-jwt/jwt"
)
func encryptAES(plainText []byte, key []byte, iv []byte) []byte {
block, _ := aes.NewCipher(key)
encrypter := cipher.NewCBCEncrypter(block, iv)
content := padPKCS5(plainText, block.BlockSize())
crypted := make([]byte, len(content))
encrypter.CryptBlocks(crypted, content)
return crypted
}
func encryptOAEP(plainText []byte, publicRSAkey []byte) []byte {
block, _ := pem.Decode(publicRSAkey)
pubKey, _ := x509.ParsePKCS1PublicKey(block.Bytes)
cipherText, _ := rsa.EncryptOAEP(sha256.New(), rand.Reader, pubKey, plainText, nil)
return cipherText
}
func signJWT() (string, error) {
ctxJSON := []byte(`{
user: {
gender: 'male',
email: 'panoskaragiannis@moveo.ai',
display_name: 'Panagiotis Karagiannis',
avatar: 'https://upload.wikimedia.org/wikipedia/commons/5/5c/Flag_of_Greece.svg',
address: '50 Pastor St, Paramus, NJ',
language: 'en',
phone: '+123456789',
},
credit_card: "123456789",
password: "102324",
is_vip: true,
contract_id: '1234959595',
has_requested_refund: true,
external_details: {
from: '02-01-2022',
agent: "C25",
},
}`)
generatedPublicKey, _ ok := os.LookupEnv("MOVEO_GENERATED_PRIVATE_KEY")
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
"sub": sub, // Required
"iss": iss, // Required
"exp": exp, // Required
"context": encryptAES(ctxJSON, cbcKey, cbcIV), // AES Encrypted context
"encryption_key": encryptOAEP(cbcKey, generatedPublicKey), // RSA encrypted key
"init_vector": encryptOAEP(cbcIV, generatedPublicKey), // RSA encrypted initialization vector
})
privateRSAKey, _ ok := os.LookupEnv("YOUR_PRIVATE_RSA_KEY")
block, _ := pem.Decode([]byte(privateRSAKey))
if block == nil {
return "", errors.New("error creating JWT block is nil")
}
k, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return "", err
}
signed, err := token.SignedString(k)
if err != nil {
return "", err
}
return signed, nil
}
Note that after you set the value of the sub claim along with the context claim you cannot change those values programmatically for the duration of the session. The userID that you specify with this method is used for billing purposes and can be used to delete customer data upon request.