diff --git a/docs/architecture.md b/docs/architecture.md index 16ab2e6c6..f9b84a9bc 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -58,3 +58,27 @@ The pipeline caches metadata in two places: Redis and the database. Redis is use Video metadata is cached in the `CachedVideos` table. Search results are cached in Redis. The cache keys are the same as the query. Cached videos are kept for 30 days. Search results are kept for 24 hours. After these periods, the cache is considered stale, and the pipeline will attempt to refresh the cache when the video is next requested. Direct videos are not cached. + + + +# Authentication + +Upon page load, the client will immediately send a request to `/api/auth/grant` to obtain a JWT regardless of whether or not the client has one already. This is to ensure that the client has a valid JWT, and to refresh the JWT if it has expired. + +```mermaid +sequenceDiagram + participant C as Client + participant S as Server + participant R as Room + C->>+S: GET /api/auth/grant + S->>-C: 200 OK + C->>+S: Connect to websocket with JWT in cookie + S->>S: Verify JWT + alt JWT is valid + S->>S: Set client ID for room on session + S->>-C: 101 Switching Protocols + S->>R: Join Room + else JWT is invalid + S-xC: 403 Forbidden + end +``` diff --git a/server/auth/index.ts b/server/auth/index.ts index 37e61323b..f4e314613 100644 --- a/server/auth/index.ts +++ b/server/auth/index.ts @@ -8,12 +8,14 @@ import nocache from "nocache"; import usermanager from "../usermanager"; import { OttException } from "ott-common/exceptions"; import { requireApiKey } from "../admin"; +import jwt from "jsonwebtoken"; +import { conf } from "../ott-config"; const router = express.Router(); router.use(nocache()); const log = getLogger("api/auth"); -function createSession(): SessionInfo { +export function createSession(): SessionInfo { return { isLoggedIn: false, username: uniqueNamesGenerator(), @@ -53,6 +55,8 @@ export async function authTokenMiddleware( if (req.headers.authorization && req.headers.authorization.startsWith("Bearer")) { const token: AuthToken = req.headers.authorization.split(" ")[1]; req.token = token; + } else if (req.cookies && req.cookies.token) { + req.token = req.cookies.token; } if (!req.token || !(await tokens.validate(req.token))) { @@ -78,6 +82,16 @@ export async function authTokenMiddleware( } } next(); + + // TODO: set cookie with updated token if it has changed + + // FIXME: can't set cookie here because it's too late in the request lifecycle + // const token = jwt.sign(req.ottsession, conf.get("session_secret")); // FIXME: no expiration + // res.cookie("token", token, { + // httpOnly: true, + // }); + + // TODO: also apply session info update to any connected clients associated with this token } router.get("/grant", async (req, res) => { diff --git a/server/auth/tokens.ts b/server/auth/tokens.ts index ed517c274..3068cd43c 100644 --- a/server/auth/tokens.ts +++ b/server/auth/tokens.ts @@ -1,9 +1,15 @@ import crypto from "crypto"; import { redisClient } from "../redisclient"; import { AuthToken } from "ott-common/models/types"; +import jwt from "jsonwebtoken"; +import { conf } from "../ott-config"; +import { createSession } from "."; +import { getLogger } from "../logger"; + +const log = getLogger("auth/tokens"); const PREFIX = "auth"; -const EXPIRATION_TIME = 14 * 24 * 60 * 60; // 14 days in seconds +const EXPIRATION_TIME = "14d"; const EXPIRATION_TIME_LOGGED_IN = 120 * 24 * 60 * 60 * 2; // 120 days in seconds export type SessionInfo = @@ -11,30 +17,49 @@ export type SessionInfo = | { isLoggedIn: true; user_id: number }; export async function validate(token: AuthToken): Promise { - return (await redisClient.exists(`${PREFIX}:${token}`)) > 0; + // return (await redisClient.exists(`${PREFIX}:${token}`)) > 0; + try { + jwt.verify(token, conf.get("session_secret")); + return true; + } catch (err) { + log.error("Failed to validate token", err); + return false; + } } /** * Mint a new crypto-random auth token so it can be assigned session information. */ export async function mint(): Promise { - const buffer = crypto.randomBytes(512); - const token: AuthToken = buffer.toString("base64"); + // const buffer = crypto.randomBytes(512); + // const token: AuthToken = buffer.toString("base64"); + const token: AuthToken = jwt.sign(createSession(), conf.get("session_secret"), { + expiresIn: EXPIRATION_TIME, + }); return token; } export async function getSessionInfo(token: AuthToken): Promise { - const text = await redisClient.get(`${PREFIX}:${token}`); - if (!text) { + const decoded = jwt.verify(token, conf.get("session_secret")); + if (!decoded) { throw new Error(`No session info found`); } - const info = JSON.parse(text); - return info; + return decoded as SessionInfo; + // const text = await redisClient.get(`${PREFIX}:${token}`); + // if (!text) { + // throw new Error(`No session info found`); + // } + // const info = JSON.parse(text); + // return info; } +/** @deprecated This will no longer work because auth tokens are JWTs now. */ export async function setSessionInfo(token: AuthToken, session: SessionInfo): Promise { - const expiration = session.isLoggedIn ? EXPIRATION_TIME_LOGGED_IN : EXPIRATION_TIME; - await redisClient.setEx(`${PREFIX}:${token}`, expiration, JSON.stringify(session)); + // const expiration = session.isLoggedIn ? EXPIRATION_TIME_LOGGED_IN : EXPIRATION_TIME; + // await redisClient.setEx(`${PREFIX}:${token}`, expiration, JSON.stringify(session)); + log.warn( + "setSessionInfo is deprecated and will no longer work because auth tokens are JWTs now." + ); } export default { diff --git a/server/package.json b/server/package.json index 44060bcc5..f375c32a5 100644 --- a/server/package.json +++ b/server/package.json @@ -24,6 +24,7 @@ "dayjs": "^1.10.4", "express": "^4.17.1", "express-session": "^1.17.0", + "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", "m3u8-parser": "^6.2.0", "nocache": "^3.0.0", @@ -60,6 +61,7 @@ "@types/convict": "^6.1.1", "@types/express": "^4.17.11", "@types/express-session": "^1.17.3", + "@types/jsonwebtoken": "^9.0.6", "@types/lodash": "^4.14.170", "@types/node": "^18.13.0", "@types/passport": "1.0.12", diff --git a/yarn.lock b/yarn.lock index 02ebb06c1..e4799a42b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3609,6 +3609,13 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== +"@types/jsonwebtoken@^9.0.6": + version "9.0.6" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz#d1af3544d99ad992fb6681bbe60676e06b032bd3" + integrity sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw== + dependencies: + "@types/node" "*" + "@types/keygrip@*": version "1.0.2" resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72" @@ -5572,6 +5579,11 @@ buffer-crc32@~0.2.3: resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -7047,6 +7059,13 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + editorconfig@^0.15.3: version "0.15.3" resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.15.3.tgz#bef84c4e75fb8dcb0ce5cee8efd51c15999befc5" @@ -10181,6 +10200,22 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +jsonwebtoken@^9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" + integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^7.5.4" + jsprim@^1.2.2: version "1.4.2" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" @@ -10211,6 +10246,23 @@ jsprim@^2.0.2: object.assign "^4.1.4" object.values "^1.1.6" +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + keycode@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.1.tgz#09c23b2be0611d26117ea2501c2c391a01f39eff" @@ -10368,6 +10420,36 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== + lodash.memoize@^4.1.1: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -10378,7 +10460,7 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash.once@^4.1.1: +lodash.once@^4.0.0, lodash.once@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==