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.
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.
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:
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:
- By requesting a JWT through our REST API.
- By self-signing a JWT with your Daily API key.
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.
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.
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:
- Storing the JWT directly in a session cookie, as mentioned above
- Local or session storage via the Web Storage API
- Application memory, including in closures
- Web workers
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!