Skip to content

Free Server Side Token Generation

Jacob Steele Aug 17, 2023 4:29:14 PM

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.

 

Step-by-Step Guide

  1. Create a free Cloudflare account by visiting https://dash.cloudflare.com/.

  2. On the left-hand navigation panel, click on "Workers" and create a new free tier worker.

  3. Create a new environment and give it a suitable name.

  4. Edit the environment variables in the settings tab as follows (retrieve values from LiveSwitch Cloud Console):

    • APPLICATION_ID = <LiveSwitch Application Id>
    • SHARED_SECRET = <LiveSwitch Shared Secret>
  5. Access the Quick Edit option from the resource tab.

  6. Paste the provided code snippet into the worker editor.

  7. Modify your application to make requests to the URL: <workerurl>?channel=${channel}&userId=${userId}&deviceId=${deviceId}

  8. Voila! You have successfully generated server-side tokens.

 

In-Depth Breakdown

  1. Create a free Cloudflare account
    You got this!

  2. Create a new worker

    Begin by creating a new HTTP handler in Cloudflare Workers, as shown in the screenshot above. You can choose any service name, such as TokenAuthService.

  3. Create a new environment

    By default, Cloudflare creates a production environment. However, it's recommended to create a new environment specifically for your application, as it allows direct editing instead of having to publish changes. 

  4. Edit the environment variables
    Visit the Settings tab for your newly created environment and set the required environment variables.

    These values can be obtained from your LiveSwitch Cloud Console:

    Make sure to click the eye icon to reveal the shared secret.

  5. Edit the new worker

    Return to the Resources tab and access the Quick Edit option for the created worker. This will allow you to modify the worker.

  6. Insert the code snippet
    Copy the provided code snippet into the Quick Edit section. This code is responsible for generating the signed URL and handling the token generation process.
    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));
      }
    });
  7. Example usage
    An example of generating tokens from the server side in a JavaScript 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];

 

Security Considerations

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!