LiveSwitch recommends server-side token generation for enhanced security in real-time communication applications. But what if you don't have a server from which to generate tokens? Cloudflare Workers offers a solution to your problem. With 100,000 free requests per day, most startups can leverage this service without worrying about costs. In this article, we'll guide you through the process of setting up server-side token generation using Cloudflare Workers and LiveSwitch.
Create a free Cloudflare account by visiting https://dash.cloudflare.com/.
On the left-hand navigation panel, click on "Workers" and create a new free tier worker.
Create a new environment and give it a suitable name.
Edit the environment variables in the settings tab as follows (retrieve values from LiveSwitch Cloud Console):
Access the Quick Edit option from the resource tab.
Paste the provided code snippet into the worker editor.
Modify your application to make requests to the URL: <workerurl>?channel=${channel}&userId=${userId}&deviceId=${deviceId}
Voila! You have successfully generated server-side tokens.
class Base64URL {
static parse(s) {
return new Uint8Array(Array.prototype.map.call(atob(s.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, '')), c => c.charCodeAt(0)))
}
static stringify(a) {
return btoa(String.fromCharCode.apply(0, a)).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
}
}
class JWT {
constructor() {
if (typeof crypto === 'undefined' || !crypto.subtle)
throw new Error('Crypto not supported!')
this.algorithms = {
ES256: { name: 'ECDSA', namedCurve: 'P-256', hash: { name: 'SHA-256' } },
ES384: { name: 'ECDSA', namedCurve: 'P-384', hash: { name: 'SHA-384' } },
ES512: { name: 'ECDSA', namedCurve: 'P-512', hash: { name: 'SHA-512' } },
HS256: { name: 'HMAC', hash: { name: 'SHA-256' } },
HS384: { name: 'HMAC', hash: { name: 'SHA-384' } },
HS512: { name: 'HMAC', hash: { name: 'SHA-512' } },
RS256: { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-256' } },
RS384: { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-384' } },
RS512: { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-512' } },
}
}
_utf8ToUint8Array(str) {
return Base64URL.parse(btoa(unescape(encodeURIComponent(str))))
}
_str2ab(str) {
const buf = new ArrayBuffer(str.length);
const bufView = new Uint8Array(buf);
for (let i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
_decodePayload(raw) {
switch (raw.length % 4) {
case 0:
break
case 2:
raw += '=='
break
case 3:
raw += '='
break
default:
throw new Error('Illegal base64url string!')
}
try {
return JSON.parse(decodeURIComponent(escape(atob(raw))))
} catch {
return null
}
}
async sign(payload, secret, options = { algorithm: 'HS256' }) {
if (typeof options === 'string')
options = { algorithm: options }
if (payload === null || typeof payload !== 'object')
throw new Error('payload must be an object')
if (typeof secret !== 'string')
throw new Error('secret must be a string')
if (typeof options.algorithm !== 'string')
throw new Error('options.algorithm must be a string')
const importAlgorithm = this.algorithms[options.algorithm]
if (!importAlgorithm)
throw new Error('algorithm not found')
payload.iat = Math.floor(Date.now() / 1000)
const payloadAsJSON = JSON.stringify(payload)
const partialToken = `${Base64URL.stringify(this._utf8ToUint8Array(JSON.stringify({ alg: options.algorithm, kid: options.keyid, typ: "JWT" })))}.${Base64URL.stringify(this._utf8ToUint8Array(payloadAsJSON))}`
let keyFormat = 'raw'
let keyData
if (secret.startsWith('-----BEGIN')) {
keyFormat = 'pkcs8'
keyData = this._str2ab(atob(secret.replace(/-----BEGIN.*?-----/g, '').replace(/-----END.*?-----/g, '').replace(/\s/g, '')))
} else
keyData = this._utf8ToUint8Array(secret)
const key = await crypto.subtle.importKey(keyFormat, keyData, importAlgorithm, false, ['sign'])
const signature = await crypto.subtle.sign(importAlgorithm, key, this._utf8ToUint8Array(partialToken))
return `${partialToken}.${Base64URL.stringify(new Uint8Array(signature))}`
}
async verify(token, secret, options = { algorithm: 'HS256' }) {
if (typeof options === 'string')
options = { algorithm: options }
if (typeof token !== 'string')
throw new Error('token must be a string')
if (typeof secret !== 'string')
throw new Error('secret must be a string')
if (typeof options.algorithm !== 'string')
throw new Error('options.algorithm must be a string')
const tokenParts = token.split('.')
if (tokenParts.length !== 3)
throw new Error('token must consist of 3 parts')
const importAlgorithm = this.algorithms[options.algorithm]
if (!importAlgorithm)
throw new Error('algorithm not found')
const payload = this.decode(token)
if (payload.nbf && payload.nbf > Math.floor(Date.now() / 1000))
return false
if (payload.exp && payload.exp <= Math.floor(Date.now() / 1000))
return false
let keyFormat = 'raw'
let keyData
if (secret.startsWith('-----BEGIN')) {
keyFormat = 'pkcs8'
keyData = this._str2ab(atob(secret.replace(/-----BEGIN.*?-----/g, '').replace(/-----END.*?-----/g, '').replace(/\s/g, '')))
} else
keyData = this._utf8ToUint8Array(secret)
const key = await crypto.subtle.importKey(keyFormat, keyData, importAlgorithm, false, ['sign'])
const res = await crypto.subtle.sign(importAlgorithm, key, this._utf8ToUint8Array(tokenParts.slice(0, 2).join('.')))
return Base64URL.stringify(new Uint8Array(res)) === tokenParts[2]
}
decode(token) {
return this._decodePayload(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'))
}
}
async function generateSignedUrl(url) {
const payload = {
type: 'register',
applicationId: APPLICATION_ID,
userId: url.searchParams.get('userId'),
deviceId: url.searchParams.get('deviceId'),
channels: [{
// MODIFY THIS IF YOU WANT TO CHANGE USERS PERMISSIONS
id: url.searchParams.get('channel')
}],
exp: Math.floor(Date.now() / 1000) + (2 * (60 * 60)) // EDIT EXPIRY TIME
};
const jwt = new JWT();
const token = await jwt.sign(payload, SHARED_SECRET)
let response = new Response(token);
response.headers.append('Access-Control-Allow-Origin', '*');
return response;
}
addEventListener('fetch', event => {
const url = new URL(event.request.url);
const prefix = '/token/';
if (url.pathname.startsWith(prefix)) {
event.respondWith(generateSignedUrl(url));
} else {
event.respondWith(fetch(event.request));
}
});
Session
class is provided:class Session {
applicationId = "101f0282-be1c-4f21-9726-82b3ecde58ed";
gatewayURL = "https://cloud.liveswitch.io/";
authURL = "https://lowcode.tokenauthservice.jacobjamessteele.workers.dev/token/";
channel = "";
constructor() {
fm.liveswitch.Log.setDefaultLogLevel(fm.liveswitch.LogLevel.Debug);
fm.liveswitch.Log.registerProvider(new fm.liveswitch.ConsoleLogProvider());
this.client = new fm.liveswitch.Client(this.gatewayURL, this.applicationId);
this.channel = window.location.href.replace(/[^a-zA-Z0-9 ]/g, '');
}
get Client() {
return this.client;
}
fetchResource = async (pathToResource) => {
try {
const response = await fetch(pathToResource);
if (!response.ok) {
throw Error(`${response.status} ${response.statusText}`);
}
return response;
} catch (error) {
console.log('Looks like there was a problem: ', error);
}
}
generateToken = async () => {
let url = this.authURL;
let id = this.client.getUserId();
let deviceId = this.client.getDeviceId();
let channel = this.channel;
url += `?channel=${channel}&userId=${id}&deviceId=${deviceId}`;
try {
const response = await this.fetchResource(url);
return await response.text()
} catch(ex) {
fm.liveswitch.Log.error("Error generating token, service down?", ex);
}
return null
}
}
Utilize the code snippet to initialize a session, generate a token, and register the client:
this.session = new Session();
let token = await this.session.generateToken();
let client = this.session.Client;
let channels = await client.register(token);
let channel = channels[0];
It's important to note that this implementation has a potential security vulnerability. The current setup allows anyone to generate tokens for any room by calling the service. Be sure to implement additional security measures to control and restrict token generation according to your specific requirements.
Need assistance in architecting the perfect WebRTC application? Let our team help out! Get in touch with us today!