Control access using JWTs
Using JSON Web Tokens (JWTs) provides a robust way to control access to both your assets and livestreams. The JWTs can be signed and validated to ensure that only authorized users can access the content. Below are examples for both assets and livestreams.
Adding access control to a content only takes a few lines of code.
This guide is written for developers using @livepeer/react
in a React
application.
Configuring Providers
First, we
create a new React client and wrap the app with LivepeerConfig
.
Create a Gated Content
For livestreams
Create your gated stream, with the stream key returned once we create it (styling has been removed for simplicity)
import { useCreateStream, useStream } from "@livepeer/react";
import { useMemo, useState } from "react";
export const AccessControl = () => {
const [streamName, setStreamName] = useState<string>("");
const {
mutate: createStream,
data: createdStream,
status,
} = useCreateStream(
streamName
? {
name: streamName,
playbackPolicy: { type: "jwt" },
}
: null,
);
const { data: stream } = useStream({
streamId: createdStream?.id,
refetchInterval: (stream) => (!stream?.isActive ? 5000 : false),
});
const isLoading = useMemo(() => status === "loading", [status]);
return (
<div>
<input
placeholder="Stream Name"
onChange={(e) => setStreamName(e.target.value)}
/>
<button
onClick={() => {
createStream?.();
}}
disabled={isLoading || !createStream || Boolean(stream)}
>
Create Gated Stream
</button>
</div>
);
};
For assets
Create your gated asset, with the jwt playback policy type.
```tsx accessControl.tsx
import { useCreateAsset, useAsset } from "@livepeer/react";
import { useMemo, useState } from "react";
export const AccessControlAssets = () => {
const [assetName, setAssetName] = useState<string>("");
const {
mutate: createAsset,
data: createdAsset,
status,
} = useCreateAsset(
assetName
? {
name: assetName,
playbackPolicy: { type: "jwt" },
}
: null
);
// ... Rest of the code for managing and playing assets with JWT
};
Sign a JWT (Node.JS API Route)
Next, we add an API route - since we are using Next.JS, we add a custom Next.js API route. We add a check in the API route for a special “secret” that must be passed in the POST body for the user to gain access to the stream.
Make sure to create a signing key - those
values will be used as the environment variables ACCESS_CONTROL_PRIVATE_KEY
and NEXT_PUBLIC_ACCESS_CONTROL_PUBLIC_KEY
.
// use the signAccessJwt export from `livepeer` in Node.JS
import { signAccessJwt } from "@livepeer/web/crypto";
import { NextApiRequest, NextApiResponse } from "next";
import { ApiError } from "../../lib/error";
export type CreateSignedPlaybackBody = {
playbackId: string;
secret: string;
};
export type CreateSignedPlaybackResponse = {
token: string;
};
const accessControlPrivateKey = process.env.ACCESS_CONTROL_PRIVATE_KEY;
const accessControlPublicKey =
process.env.NEXT_PUBLIC_ACCESS_CONTROL_PUBLIC_KEY;
const handler = async (
req: NextApiRequest,
res: NextApiResponse<CreateSignedPlaybackResponse | ApiError>,
) => {
try {
const method = req.method;
if (method === "POST") {
if (!accessControlPrivateKey || !accessControlPublicKey) {
return res
.status(500)
.json({ message: "No private/public key configured." });
}
const { playbackId, secret }: CreateSignedPlaybackBody = req.body;
if (!playbackId || !secret) {
return res.status(400).json({ message: "Missing data in body." });
}
// we check that the "supersecretkey" was passed in the body
// this could be a more complex check, like taking a signed payload,
// getting the address for that signature, and fetching if they own an NFT
//
// https://docs.ethers.io/v5/single-page/#/v5/api/utils/signing-key/-%23-SigningKey--other-functions
if (secret !== "supersecretkey") {
return res.status(401).json({ message: "Incorrect secret." });
}
// we sign the JWT and return it to the user
const token = await signAccessJwt({
privateKey: accessControlPrivateKey,
publicKey: accessControlPublicKey,
issuer: "https://docs.livepeer.org",
// playback ID to include in the JWT
playbackId,
// expire the JWT in 1 hour
expiration: "1h",
// custom metadata to include
custom: {
userId: "user-id-1",
},
});
return res.status(200).json({
token,
});
}
res.setHeader("Allow", ["POST"]);
return res.status(405).end(`Method ${method} Not Allowed`);
} catch (err) {
console.error(err);
return res
.status(500)
.json({ message: (err as Error)?.message ?? "Error" });
}
};
export default handler;
Configure the Player with the JWT
Lastly, when the content is created, we make a POST request to the
/api/create-signed-jwt
API route we created in the previous step (using
React Query to manage the
mutation state handling). We pass along the playbackId
for the content, which
is used to create the JWT.
Then, we pass the JWT to the Player using the
jwt
prop, which will use that JWT to prove
access to the content!
import { Player, useCreateStream, useStream } from "@livepeer/react";
import { useMutation } from "@tanstack/react-query";
import { useEffect, useMemo, useState } from "react";
import { ApiError } from "../../lib/error";
import {
CreateSignedPlaybackBody,
CreateSignedPlaybackResponse,
} from "../../pages/api/create-signed-jwt";
export const AccessControl = () => {
const [streamName, setStreamName] = useState<string>("");
const {
mutate: createStream,
data: createdStream,
status,
} = useCreateStream(
streamName
? {
name: streamName,
playbackPolicy: { type: "jwt" },
}
: null,
);
const { data: stream } = useStream({
streamId: createdStream?.id,
refetchInterval: (stream) => (!stream?.isActive ? 5000 : false),
});
const { mutate: createJwt, data: createdJwt } = useMutation({
mutationFn: async () => {
if (!stream?.playbackId) {
throw new Error("No playback ID yet.");
}
const body: CreateSignedPlaybackBody = {
playbackId: stream.playbackId,
// we pass along a "secret key" to demonstrate how gating can work
secret: "supersecretkey",
};
// we make a request to the Next.JS API route shown above
const response = await fetch("/api/create-signed-jwt", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
return response.json() as Promise<
CreateSignedPlaybackResponse | ApiError
>;
},
});
useEffect(() => {
if (stream?.playbackId) {
// when we have a playbackId for the stream, create a JWT
createJwt();
}
}, [stream?.playbackId, createJwt]);
const isLoading = useMemo(() => status === "loading", [status]);
return (
<div>
{!stream?.id ? (
<>
<input
placeholder="Stream Name"
onChange={(e) => setStreamName(e.target.value)}
/>
<button
onClick={() => {
createStream?.();
}}
disabled={isLoading || !createStream || Boolean(stream)}
>
Create Gated Stream
</button>
</>
) : (
<Player
title={stream?.name}
playbackId={stream?.playbackId}
autoPlay
muted
jwt={(createdJwt as CreateSignedPlaybackResponse)?.token}
/>
)}
</div>
);
};
If you are not using the player, you will need to manually append the jwt to the URL as a query parameter, similar to:
https://playback.livepeer.studio/asset/hls/abcd1234/index.m3u8?jwt=your-jwt
Was this page helpful?