Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add user verification for server API #37

Closed
wants to merge 14 commits into from
34 changes: 22 additions & 12 deletions client/src/components/account/admin.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,36 @@
import { getAuth } from "firebase/auth";

import { useEffect, useState } from "react";

import { getRoomsWhereUserISAdmin } from "../../lib/firestore";
import { app } from "../../lib/firestore/init";
import { getStreamKey, resetRoom } from "../../lib/server-api";

const auth = getAuth(app);
/**
* AdminView renders RoomAdminUI for each room that a user id admin for
*/
interface AdminViewProps {
uid: string;
}
const Admin: React.FC<AdminViewProps> = ({ uid }) => {
const [rooms, setRooms] = useState<undefined | RoomInfo[]>(undefined);
const [state, setState] = useState<{
rooms?: RoomInfo[];
userToken?: string;
}>({});

useEffect(() => {
async function getRooms() {
let rooms = await getRoomsWhereUserISAdmin(uid);
setRooms(rooms);
let [rooms, userToken] = await Promise.all([
getRoomsWhereUserISAdmin(uid),
auth.currentUser?.getIdToken(),
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I tried to use the user from UserStore at first, but for some reason it couldn't find the getIdToken method.

]);
setState({ rooms, userToken });
}
getRooms();
}, [uid]);
}, [auth.currentUser, uid]);

return rooms ? (
return state.rooms && state.userToken ? (
<div className="stack padded border-thin lightFill">
<em>Rooms you manage</em>
<p>
Expand All @@ -28,11 +39,11 @@ const Admin: React.FC<AdminViewProps> = ({ uid }) => {
rtmps://global-live.mux.com:443/app
</span>
</p>
{rooms.map((r) => (
{state.rooms.map((r) => (
<RoomAdminUI
roomID={r.roomID}
key={r.roomName + "-adminView"}
uid={uid}
userToken={state.userToken || ""}
/>
))}
</div>
Expand All @@ -41,16 +52,15 @@ const Admin: React.FC<AdminViewProps> = ({ uid }) => {
);
};

const RoomAdminUI: React.FC<{ roomID: string; uid: string }> = ({
const RoomAdminUI: React.FC<{ roomID: string; userToken: string }> = ({
roomID,
uid,
userToken,
}) => {
let [streamKey, setStreamKey] = useState<string>();

useEffect(() => {
//TODO: Send UID here to authenticate with server
async function getSK() {
let sk = await getStreamKey(roomID);
let sk = await getStreamKey(userToken, roomID);
setStreamKey(sk);
}
getSK();
Expand All @@ -66,7 +76,7 @@ const RoomAdminUI: React.FC<{ roomID: string; uid: string }> = ({
</div>
<div
onClick={() => {
resetRoom(roomID);
resetRoom(userToken, roomID);
setStreamKey("");
}}
className="button"
Expand Down
18 changes: 10 additions & 8 deletions client/src/lib/server-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ export const generateStreamLink = (playbackID: string) => {
return `https://stream.mux.com/${playbackID}.m3u8`;
};

const fetchResponse = async (endpoint: string) => {
const response = await fetch(endpoint);
const fetchResponse = async (adminToken: string, endpoint: string) => {
const response = await fetch(endpoint, {
headers: {
Authorization: `Bearer ${adminToken}`,
},
});
if (!response.ok) {
throw new Error(
`Request to ${endpoint} failed with status code ${response.status}`,
Expand All @@ -21,12 +25,10 @@ const fetchResponse = async (endpoint: string) => {
return response;
};

export const getStreamKey = async (streamName: string) => {
export const getStreamKey = async (adminToken: string, streamName: string) => {
try {
// Imagine that this getStreamKey has a secret key like
// let secretKey = hash(process.env.secretKey)
//Then fetchResponse(/streamName, )
const streamKeyResponse = await fetchResponse(
adminToken,
`${STREAMS_KEY_ENDPOINT}/${streamName}`,
);
const streamKeys = await streamKeyResponse.json();
Expand All @@ -40,6 +42,6 @@ export const getStreamKey = async (streamName: string) => {
}
};

export const resetRoom = async (roomID: string) => {
await fetch(SERVER_URL + "/reset-room/" + roomID);
export const resetRoom = async (adminToken: string, roomID: string) => {
return fetchResponse(adminToken, SERVER_URL + "/reset-room/" + roomID);
};
1 change: 0 additions & 1 deletion client/src/pages/account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { NextPage } from "next";
import { useCallback, useEffect } from "react";

import Auth from "../components/account/auth";
import Layout from "../components/room/layout";
import { useRoomStore } from "../stores/roomStore";

const Account: NextPage = () => {
Expand Down
4 changes: 4 additions & 0 deletions server/src/firebase-init.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { initializeApp, cert } from "firebase-admin/app";
import { getAuth } from "firebase-admin/auth";
import { getFirestore } from "firebase-admin/firestore";

import dotenv from "dotenv";

// Load environment variables
Expand All @@ -14,4 +16,6 @@ export const app = initializeApp({
databaseURL: "https://is-this-thing-on-320a7-default-rtdb.firebaseio.com",
storageBucket: "is-this-thing-on-320a7.appspot.com",
});

export const firestore = getFirestore(app);
export const firebaseAuth = getAuth(app);
12 changes: 8 additions & 4 deletions server/src/firestore-api.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@

import { QueryDocumentSnapshot, DocumentData,Timestamp, FieldValue} from "firebase-admin/firestore";
import { FieldValue, Timestamp } from "firebase-admin/firestore";
import { logError, logInfo, logUpdate } from './logger.js';

import { firestore } from './firebase-init.js';
import { logError, logInfo, logUpdate, logWarning } from './logger.js';


const PRESENCE_LENGTH = 5 * 1000;

Expand All @@ -30,6 +28,12 @@ const getRoom = async (roomID: string) => {
}
}

export async function isAdminForAnyRoom(userID: string) {
const q = firestore.collection("rooms").where("admins", "array-contains", userID).where("hidden", "!=", true).count();
const result = await q.get();
return result.data().count > 0;
}

/**
* gets stream key from firestore, given a room name.
* @param roomID
Expand Down
41 changes: 41 additions & 0 deletions server/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { RequestHandler } from "express";
import { firebaseAuth } from "./firebase-init.js";
import { isAdminForAnyRoom } from "./firestore-api.js";
import { logError } from "./logger.js";

/**
* Middleware to ensure that the caller has provided admin credentials.
* An admin is any user who has been assigned as an admin to any room in Firestore.
*/
export const verifyThingAdmin: RequestHandler = async (req, res, next) => {
const bearer = req.headers.authorization;

if (!bearer) {
logError("No ID token provided for request");
return res.sendStatus(403);
}

const parts = bearer.split("Bearer ");
if (parts.length <= 1) {
logError("Invalid ID token provided for request");
return res.sendStatus(403);
}

const idToken = parts[1];

try {
const decodedToken = await firebaseAuth.verifyIdToken(idToken)
const uid = decodedToken.uid;

const isAdmin = await isAdminForAnyRoom(uid);
if (isAdmin) {
return next();
} else {
logError(`User with ID ${uid} is not an admin for any rooms`);
}
} catch (err) {
logError(`Failed to verify auth token: ${err}`)
}

return res.sendStatus(403);
}
9 changes: 5 additions & 4 deletions server/src/muxAPI.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import Mux, { CreateLiveStreamParams } from "@mux/mux-node";
import auth from "basic-auth";
import { Request, RequestHandler } from "express";
import { getStreamKey, writeNewStreamToDB } from "./firestore-api.js";
import { logError, logInfo } from "./logger.js";

import { RequestHandler } from "express";
import auth from "basic-auth";

const { Video } = new Mux(process.env.MUX_TOKEN_ID, process.env.MUX_TOKEN_SECRET);
import { getStreamKey, writeNewStreamToDB } from "./firestore-api.js";

export const createAndReturnStreamKey : RequestHandler = async (req, res) => {
const roomID = req.params.id;
Expand Down Expand Up @@ -64,4 +65,4 @@ export const getPlaybackIDFromMuxData = ( data: any) => {
return playbackIDs[0].id
}
return undefined
}
}
17 changes: 9 additions & 8 deletions server/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import "dotenv/config";

import { createAndReturnStreamKey, muxAuthHelper } from "./muxAPI.js";
import express, { Application } from "express";
import { createServer } from "http";
import { managePresenceInDB, presenceProcessor, resetMuxFirestoreRelationship } from "./firestore-api.js";
import { presenceProcessor, resetMuxFirestoreRelationship } from "./firestore-api.js";

import bodyParser from "body-parser"
import { createServer } from "http";
import { logUpdate } from "./logger.js";
import { muxAuthHelper, createAndReturnStreamKey } from "./muxAPI.js";
import { muxUpdateWasReceived } from "./processUpdate.js";


import { verifyThingAdmin } from "./middleware.js";

const app: Application = express();
app.use(bodyParser.json());
Expand All @@ -17,15 +18,15 @@ const httpServer = createServer(app);

app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization");
next();
});
app.get("/", (req, res) => {
res.send("Server?");
});
app.get("/stream-key/:id", createAndReturnStreamKey);
app.get("/stream-key/:id", verifyThingAdmin, createAndReturnStreamKey);
app.post("/mux-hook", muxAuthHelper, muxUpdateWasReceived);
app.get("/reset-room/:id", async (req,res) => {
app.get("/reset-room/:id", verifyThingAdmin, async (req,res) => {
logUpdate(`Resetting room ${req.params.id}`);
try {
const roomID = req.params.id;
Expand Down