Obtaining, handling, and validating meeting tokens in your video application
This is part of our social gaming series, in which we walk through key features of our Daily-powered social game, Code of Daily: Modern Wordfare. This is a standalone post. You do not need to read the rest of the series to follow along. But if you’re curious about the greater context of our application, check out  part one or the rest of the series.

Introduction

Daily’s meeting tokens enable developers to determine participants’ privileges and session settings in a Daily call. Meeting tokens take the form of JSON Web Tokens (JWTs) and are issued per-user. Our social game, Code of Daily: Modern Wordfare, uses Daily's meeting tokens to enable a basic game feature: allowing a game host to mute all other players.

Cursor hovering over "Mute all" button
Button to mute all other players

In this post, I’ll go through some basic guidelines for using meeting tokens in your video applications, using our social game as one example of how they can be handled.

Following along

Check out our repository to follow along directly on GitHub. I'll link any relevant reference to specific code here. You can also check out the instructions to clone and run the game locally if you're the digging-around-the-code type, like me.

Player clicking a word during gameplay
Code of Daily: Modern Wordfare gameplay

But first... what is a JWT, anyway?


What is a JSON Web Token (JWT)?

A JWT is a standard way to communicate pieces of information (known as “claims”) that can be verified with a shared secret value. JWTs are strings made up of three base64-encoded parts separated by periods:

  • Header: contains information about algorithm and token type
  • Payload: contains token claims like expiry, issue time, and room privileges
  • Signature: produced by taking the header, payload, and algorithm type, and signing the whole thing with a secret.

An example JWT could look as follows:

A sample JWT split up into "Header", "Payload", and "Signature" sections
The parts of a JWT

The payload, which contains our Daily-specific claims, is the most interesting part:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

When decoding this base64 payload into a human-readable string, we see the following:

{"r":"roomname","d":"mydomain","exp":1660293162,"o":false,"iat":1660206762}

Daily meeting token claims can include user options and permissions that will apply to whoever joins the Daily call with that token.

A resource server (the API which actually hosts and grants access to protected functionality or data) can validate expiry and other relevant claims along with the token signature. If valid, the token can be used to grant access to privileged resources or operations.

If you’d like to know more about JSON Web Tokens, check out JWT.io.


How to obtain a meeting token

A Daily meeting token can be obtained in two different ways:

Requesting a meeting token from Daily's REST API

This is the token retrieval method we use in Code of Daily (CoD).
By making a POST request to our /meeting-tokens endpoint with your Daily API key in an Authorization header, you can have Daily generate a meeting token for you.

💡
You can find your Daily API key in the Developers section of your Daily dashboard. If you haven't already, check out my earlier post on using Daily's API keys.

As a practical example, let’s go through how CoD uses this method of retrieving meeting tokens.

Retrieving a Daily meeting token in our social game

In CoD, the creator of the game is designated to be the game host with special privileges. These privileges are:

  • being able to mute all other players
  • being able to restart a game mid-play

When creating a game, the server sets a signed HttpOnly session cookie on the client. I won’t go through this part of the code in detail since it isn’t directly related to the meeting token itself, but you might be interested to check out this implementation in the /create handler. Reach out if you have any questions!

When a user with a valid game-host cookie joins a game, I retrieve the token on the game server and send it back to the client as part of the response body. Token retrieval takes place in the src/server/daily.ts file:

// getMeetingToken() obtains a room-specific meeting token from Daily
export async function getMeetingToken(roomName: string): Promise<string> {
  const req = {
    properties: {
      room_name: roomName,
      exp: Math.floor(Date.now() / 1000) + 86400,
      is_owner: true,
    },
  };

  const data = JSON.stringify(req);
  const headers = {
    Authorization: `Bearer ${DAILY_API_KEY}`,
    "Content-Type": "application/json",
  };

  const url = `${dailyAPIURL}/meeting-tokens/`;

  const errMsg = "failed to create meeting token";
  const res = await axios.post(url, data, { headers }).catch((error) => {
    throw new Error(`${errMsg}: ${error})`);
  });
  if (res.status !== 200) {
    throw new Error(`${errMsg}: got status ${res.status})`);
  }
  return res.data?.token;
}

Let's go through the most interesting parts of the function above.

Preparing the token properties

We first prepare our request with the token properties we'll want Daily to encode into the JWT  payload. Importantly, we specify an expiry time in the exp property and a room name to be used in the generated token.

💡
Always specify an expiry time and room name for your tokens! If you do not set an expiry time or a room name, the resulting token will live forever and be valid for your entire domain. Whoever has such a token will be able to access any room with the token's permitted privileges, indefinitely. If you’re using a self-signed token, which I’ll cover below, you can deactivate it by regenerating your API key. But Daily-signed tokens without an expiry currently cannot be easily deactivated.

The is_owner claim specifies the game host as the Daily room owner, meaning they will get meeting-owner privileges.

Adding the Authorization header

After preparing the token payload above, we append relevant headers to the HTTP request. The Authorization header contains our Daily API key (which is defined as an environment variable on the CoD game server). This is how Daily will authenticate the request before issuing a token.

Making the request

Finally, we make the request to Daily's /meeting-tokens endpoint. If the return code is anything other than 200, the request failed and a token was not retrieved. Otherwise, we return the retrieved token back to the caller: our /join endpoint handler.

Getting a meeting token by self-signing a JWT

Daily’s REST API works great for requesting signed tokens, but sometimes you might want to self-sign a token, such as when you want to minimize HTTP requests. You can obtain a Daily meeting token without making a call to Daily's REST API by self-signing one using your Daily API key. There are libraries to aid developers in doing this. One example of a JavaScript library that can generate self-signed JWTs is jsonwebtoken. Let's look at a small example:

import * as jwt from "jsonwebtoken";

function generateMeetingToken() {
  const payload = {
    r: "daily-room-name",
    d: "daily-domain-uuid",
  };
  try {
    const token = jwt.sign(payload, "[DAILY_API_KEY]", { expiresIn: "1h" });
    return token
  } catch (e) {
    throw new Error(`failed to create self-signed JWT: ${e.toString()}`);
  }
}

Now that we have a token, let’s take a look at how to actually use it.


You have a token… Now what?

The most common use for a Daily meeting token is passing it to Daily’s API when joining a video call. That’s all you need to configure the local participant with whatever permissions or options the token you created specifies.

Let’s take a look at what joining a Daily call with a token can look like in practice by using our social game.

When the client gets a response from the /join endpoint, it puts all the information the game will need into an instance of BoardData:

// tryJoinGame() tries to join a game using the given
// game ID and player name.
function tryJoinGame(gameID: string, playerName: string) {
  joinGame(gameID)
    .then((res: JoinGameResponse) => {
      const boardData = <BoardData>{
        roomURL: res.roomURL,
        gameID,
        gameName: res.gameName,
        playerName,
        wordSet: res.wordSet,
        meetingToken: res.meetingToken,
      };
      initGame(boardData);
    })
    .catch((e) => {
      console.error(e);
    });
}

As we can see above, one of these pieces of data is the Daily meeting token. This is the first contact the client has with the token.

All of our Daily operations happen in our Call class, and we instantiate Call when starting the game. So, that’s where our meeting token needs to go:

  // setupCall() joins the game's Daily video call
  // and creates an instance of the Board class
  private setupCall(bd: BoardData) {
    // Create Daily call
    this.call = new Call(bd.roomURL, bd.playerName, bd.meetingToken);
    // …The rest of the function…
  }

Above, we pass the meeting token in our board data to our new Call instance. The Call constructor stores this as a member variable on the instance, until the game instructs the instance to join the Daily call. At that point, we provide the meeting token to the call object’s join() method and delete the API key, as we no longer have any use for it:

  // join() joins a Daily video call
  join() {
    const params: { [k: string]: string } = {};
    if (this.meetingToken) {
      params.token = this.meetingToken;
    }
    this.callObject.join(params);
    // We no longer need the meeting token for anything
    // after passing it to Daily.
    delete this.meetingToken;
  }

The game-host cookie we set on game creation triggers our game server to obtain a new meeting token for the host when they join the game. In this way, we avoid having to persistently store it anywhere on the client. The game host can leave the game and rejoin, and still retain their host permissions.


Alternative approaches and more persistent client-side storage of tokens

A meeting token should be handled with care: if it falls into the wrong hands, a malicious user could perform privileged operations  (such as using our “Mute all” feature when they shouldn’t be able to, or performing other participant updates).

Another approach of implementing the above might be to store the meeting token itself as our cookie value, and making it accessible to the client by omitting the HttpOnly cookie property. This would result in fewer calls to Daily’s REST API, but having a somewhat more exposed meeting token on the client-side.

In the end, which implementation you go with is all about deciding what your primary risk factors are and making informed decisions with your specific use case in mind.

If you do find that you need to store a meeting token persistently on the client side, there are a few ways you can look into:

💡
It’s important to think about the application holistically and decide which functionality vs security tradeoffs you’re willing to make. Anything sent to the client can't really be guaranteed to stay safe. From Cross-Site Scripting Attacks (XSS) to Cross-Site Request Forgery (CSRF), there are plenty of ways for malicious actors to extract data from the client. Perform due diligence to minimize the chances of various known exploits affecting your application and users. At the end of the day, everything is a balance. If your website is vulnerable to certain attacks, a vulnerable token will be just one of many problems you’ll need to deal with.

This is why aside from taking preventative measures to protect your token in your application through methods like input validation and output encoding, it's also a good idea to have tokens be relatively short-lived: if an attacker grabs hold of a user's token, at least they won't be able to exploit it for too long.

How short is reasonable depends entirely on your use case. If you've got a highly security-sensitive use case, like HIPAA compliance, you might want much shorter lived meeting tokens than if you have a small social game that doesn't grant any super critical privileges or access to personal data.

Now that we have some options to handle our Daily meeting tokens, let’s take a look at one more advanced use case that some applications may have a need for: token validation.


Validating Daily meeting tokens

If you're just passing your meeting token to Daily itself to manage for various room operations, you can leave it to Daily to validate the token as needed. You often won’t need to store it in any persistent way in this case, since you only need to pass it to Daily on room join.

But for some use cases, you might want to confirm that a token is valid before asking Daily to use a token. For example, maybe you want to send some additional presence information to a user, but only when the user has a valid token. Or you might use a token for some other minor feature in your application and need to validate it for that purpose.

Daily meeting token validation can be done in two ways:

  • Using Daily's meeting-tokens/:meeting-token endpoint: avoids having to write your own validation logic, but requires an HTTP request.
  • Verifying your own for self-signed tokens: doesn't require an HTTP request, but requires you to have generated your own token earlier.

Validating a Daily-signed token

Let’s look at what a request to have Daily validate a token may look like in practice. In the code snippet below, we’ll make a call to Daily’s meeting-tokens/:meeting-token endpoint with our Daily API key and the token we’re validating:

export async function tokenIsValid(
  token: string,
  roomName: string
): Promise<boolean> {
  // Call out to Daily's meeting token REST endpoint to verify validity
  const url = `https://api.daily.co/v1/meeting-tokens/${token}`;
  const headers = {
    Authorization: `Bearer ${DAILY_API_KEY}`,
    "Content-Type": "application/json",
  };

  const errMsg = "failed to validate token";
  const res = await axios.get(url, { headers }).catch((error) => {
    throw new Error(`${errMsg}: ${error}`);
  });
  return (res.status === 200);
}

Above, I make a GET request to Daily's /meeting-tokens/:meeting-token endpoint, providing my Daily API key in the Authorization header to authenticate the request. If Daily responds with a 200 (“OK”) status code, the token is good to go!

Validating self-signed meeting tokens on your own

If you'd rather not do a round-trip to Daily's REST API, you can sign your own token and perform verification entirely on your own.

There are lots of libraries in various languages to help you do this. For example, with "jsonwebtoken", you can verify if a token is valid via the verify() method.

Validating token claims

Because meeting tokens are JWTs, you can perform preliminary checks before validating the signature or making an HTTP request to Daily’s validation endpoint. You do this by decoding the token's base64-encoded payload and examining the claims therein.

For example, two important payload claims to check at this stage are exp (token expiry) and nbf (earliest validity time). If the token has already expired or is not yet valid, there's no point trying to use it or even checking it against Daily’s validation API. In addition to checking these standard JWT claims, you could also choose to verify that a Daily token contains a r claim (signifying a Daily room). Check out all standard Daily claims in our documentation.

If you’re using self-signed tokens, many JWT libraries will handle validation of standard JWT claims (like ”exp” and ”nbf”) for you at the same time that they validate the signature. But any validation of Daily specific claims you might like to perform (like the r claim we mentioned above) will need to be done separately.


Conclusion

In this post, we've covered:

  • What Daily meeting tokens are and what data they contain in the form of a JWT
  • How to obtain Daily meeting tokens, either from Daily's REST API or by self-signing your own
  • Practical ways to use (and store, if needed) a Daily token on the client-side
  • How to validate meeting tokens by using Daily's REST API or checking your own signatures

We hope this post was useful to help you get a handle on meeting tokens and what you can do with them.

Please reach out if you have any questions!

Never miss a story

Get the latest direct to your inbox.