Skip to content

Commit

Permalink
access-control: admin public key access (#1991)
Browse files Browse the repository at this point in the history
* access-control: admin public key access

* address comments

* generate jwt & dashboard get

* address comments

* fix tests

* fix

* admin check

* log
  • Loading branch information
gioelecerati committed Dec 8, 2023
1 parent fbb3179 commit d631b84
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 2 deletions.
10 changes: 10 additions & 0 deletions packages/api/src/controllers/access-control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,16 @@ app.post(
}
}

if (
playbackPolicyType !== "public" &&
req.body.pub === req.config.accessControlAdminPubkey &&
req.body.pub !== "" &&
req.body.pub
) {
res.status(204);
return res.end();
}

switch (playbackPolicyType) {
case "public":
res.status(204);
Expand Down
45 changes: 45 additions & 0 deletions packages/api/src/controllers/signing-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
SigningKeyResponsePayload,
} from "../schema/types";
import { WithID } from "../store/types";
import { SignOptions, sign } from "jsonwebtoken";

const fieldsMap: FieldsMap = {
id: `signing_key.ID`,
Expand Down Expand Up @@ -241,4 +242,48 @@ signingKeyApp.patch(
}
);

signingKeyApp.get(
"/jwt/:playbackId",
authorizer({ admin: true }),
async (req, res) => {
let { playbackId } = req.params;

const adminKey = Buffer.from(
req.config.accessControlAdminPrivkey,
"base64"
);

const pubkey = req.config.accessControlAdminPubkey;

if (!adminKey || !pubkey) {
throw new Error("jwt: error importing signing keys");
}

if (!playbackId) {
throw new Error("jwt: playback ID was not provided");
}

const issuedAtSec = Date.now() / 1000;
const expirationSec = issuedAtSec + 600;

const payload = {
action: "pull",
iss: "Livepeer",
pub: pubkey,
sub: playbackId,
video: "none",
exp: Math.floor(expirationSec),
iat: Math.floor(issuedAtSec),
};

const options: SignOptions = {
algorithm: "ES256",
};

const token = sign(payload, adminKey, options);

return res.send({ token });
}
);

export default signingKeyApp;
8 changes: 8 additions & 0 deletions packages/api/src/parse-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,14 @@ export default function parseCli(argv?: string | readonly string[]) {
describe: "Stripe webhook secret",
type: "string",
},
"access-control-admin-pubkey": {
describe: "Access Control Admin signing public key",
type: "string",
},
"access-control-admin-privkey": {
describe: "Access Control Admin signing private key",
type: "string",
},
"verification-frequency": {
describe: "verificationFreq field to return from stream/hook",
default: 0,
Expand Down
15 changes: 15 additions & 0 deletions packages/www/hooks/use-api/endpoints/stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,21 @@ export const getAdminStreams = async ({
return [streams, nextCursor, res];
};

export const generateJwt = async (playbackId: string): Promise<string> => {
const [res] = await context.fetch(
`/access-control/signing-key/jwt/${playbackId}`
);
if (res.status !== 200) {
throw new Error(JSON.stringify(res.body));
}

// Get json and get jsonRes.token
let resJson = await res.json();
let token = resJson.token;

return token;
};

export const createStream = async (params): Promise<Stream> => {
const [res, stream] = await context.fetch(`/stream`, {
method: "POST",
Expand Down
28 changes: 26 additions & 2 deletions packages/www/pages/app/stream/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ const ID = () => {
getIngest,
patchStream,
getAdminStreams,
generateJwt,
terminateStream,
} = useApi();
const userIsAdmin = user && user.admin;
Expand All @@ -191,6 +192,7 @@ const ID = () => {
}, [query.id]);
const [stream, setStream] = useState<Stream>(null);
const [streamOwner, setStreamOwner] = useState<User>(null);
const [jwt, setJwt] = useState<string>(null);
const [ingest, setIngest] = useState([]);
const [deleteModal, setDeleteModal] = useState(false);
const [terminateModal, setTerminateModal] = useState(false);
Expand Down Expand Up @@ -262,6 +264,17 @@ const ID = () => {
console.error(err); // todo: surface this
}
}, [id]);
const fetchJwt = useCallback(async () => {
if (
stream?.playbackPolicy?.type != "public" &&
stream?.playbackId &&
user.admin &&
!jwt
) {
const streamJwt = await generateJwt(stream.playbackId);
setJwt(streamJwt);
}
}, [stream]);
useEffect(() => {
fetchStream();
}, [fetchStream]);
Expand All @@ -270,9 +283,13 @@ const ID = () => {
if (!isVisible || notFound) {
return;
}
const interval = setInterval(fetchStream, 5000);
const interval = setInterval(function () {
fetchJwt();
fetchStream();
}, 5000);

return () => clearInterval(interval);
}, [fetchStream, isVisible, notFound]);
}, [fetchStream, fetchJwt, isVisible, notFound]);
const userField = useMemo(() => {
let value = streamOwner?.email;
if (streamOwner?.admin) {
Expand All @@ -289,6 +306,9 @@ const ID = () => {
}
const autoplay = query.autoplay?.toString() ?? "0";
let url = `https://lvpr.tv/?v=${stream?.playbackId}&autoplay=${autoplay}`;
if (jwt) {
url += `&jwt=${jwt}`;
}
if (isStaging() || isDevelopment()) {
url += "&monster";
}
Expand Down Expand Up @@ -919,6 +939,10 @@ const ID = () => {
{stream.id}
</Box>
</Cell>
<Cell>JWT for gated stream</Cell>
<Cell>
<Box>{jwt}</Box>
</Cell>
<Cell>Region/Broadcaster</Cell>
<Cell>
{region}{" "}
Expand Down

1 comment on commit d631b84

@vercel
Copy link

@vercel vercel bot commented on d631b84 Dec 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.