From 18b3e06fe897aec69876f22c66cdb895fc821a38 Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Wed, 10 Apr 2024 23:00:37 -0400 Subject: [PATCH] Add session listing and revocation --- .../backend/src/routers/auth/list-sessions.js | 23 ++++++++++ .../src/routers/auth/revoke-session.js | 33 ++++++++++++++ packages/backend/src/routers/login.js | 3 +- packages/backend/src/routers/signup.js | 16 ++++--- .../backend/src/services/PuterAPIService.js | 2 + .../backend/src/services/auth/AuthService.js | 45 +++++++++++++++++-- 6 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 packages/backend/src/routers/auth/revoke-session.js diff --git a/packages/backend/src/routers/auth/list-sessions.js b/packages/backend/src/routers/auth/list-sessions.js index e69de29b..a163c78c 100644 --- a/packages/backend/src/routers/auth/list-sessions.js +++ b/packages/backend/src/routers/auth/list-sessions.js @@ -0,0 +1,23 @@ +const eggspress = require("../../api/eggspress"); +const { UserActorType } = require("../../services/auth/Actor"); +const { Context } = require("../../util/context"); + +module.exports = eggspress('/auth/list-sessions', { + subdomain: 'api', + auth2: true, + allowedMethods: ['GET'], +}, async (req, res, next) => { + const x = Context.get(); + const svc_auth = x.get('services').get('auth'); + + // Only users can list their own sessions + // apps, access tokens, etc should NEVER access this + const actor = x.get('actor'); + if ( ! (actor.type instanceof UserActorType) ) { + throw APIError.create('forbidden'); + } + + const sessions = await svc_auth.list_sessions(actor); + + res.json(sessions); +}); diff --git a/packages/backend/src/routers/auth/revoke-session.js b/packages/backend/src/routers/auth/revoke-session.js new file mode 100644 index 00000000..5604066c --- /dev/null +++ b/packages/backend/src/routers/auth/revoke-session.js @@ -0,0 +1,33 @@ +const APIError = require("../../api/APIError"); +const eggspress = require("../../api/eggspress"); +const { UserActorType } = require("../../services/auth/Actor"); +const { Context } = require("../../util/context"); + +module.exports = eggspress('/auth/revoke-session', { + subdomain: 'api', + auth2: true, + allowedMethods: ['POST'], +}, async (req, res, next) => { + const x = Context.get(); + const svc_auth = x.get('services').get('auth'); + + // Only users can list their own sessions + // apps, access tokens, etc should NEVER access this + const actor = x.get('actor'); + if ( ! (actor.type instanceof UserActorType) ) { + throw APIError.create('forbidden'); + } + + // Ensure valid UUID + if ( ! req.body.uuid || typeof req.body.uuid !== 'string' ) { + throw APIError.create('field_invalid', null, { + key: 'uuid', + expected: 'string' + }); + } + + const sessions = await svc_auth.revoke_session( + actor, req.body.uuid); + + res.json({ sessions }); +}); diff --git a/packages/backend/src/routers/login.js b/packages/backend/src/routers/login.js index 79549790..1f5c6c0c 100644 --- a/packages/backend/src/routers/login.js +++ b/packages/backend/src/routers/login.js @@ -89,7 +89,8 @@ router.post('/login', express.json(), body_parser_error_handler, async (req, res return res.status(400).send('Incorrect password.') // check password if(await bcrypt.compare(req.body.password, user.password)){ - const token = await jwt.sign({uuid: user.uuid}, config.jwt_secret) + const svc_auth = req.services.get('auth'); + const token = await svc_auth.create_session_token(user); //set cookie // res.cookie(config.cookie_name, token); res.cookie(config.cookie_name, token, { diff --git a/packages/backend/src/routers/signup.js b/packages/backend/src/routers/signup.js index ca45d9d7..012fdfb2 100644 --- a/packages/backend/src/routers/signup.js +++ b/packages/backend/src/routers/signup.js @@ -52,6 +52,7 @@ module.exports = eggspress(['/signup'], { const validator = require('validator') let uuid_user; + const svc_auth = Context.get('services').get('auth'); const svc_authAudit = Context.get('services').get('auth-audit'); svc_authAudit.record({ requester: Context.get('requester'), @@ -67,9 +68,11 @@ module.exports = eggspress(['/signup'], { // check if user is already logged in if ( req.body.is_temp && req.cookies[config.cookie_name] ) { - const token = req.cookies[config.cookie_name]; - const decoded = await jwt.verify(token, config.jwt_secret); - const user = await get_user({ uuid: decoded.uuid }); + const { user, token } = await svc_auth.check_session( + req.cookies[config.cookie_name] + ); + // const decoded = await jwt.verify(token, config.jwt_secret); + // const user = await get_user({ uuid: decoded.uuid }); if ( user ) { return res.send({ token: token, @@ -233,17 +236,20 @@ module.exports = eggspress(['/signup'], { db.write('UPDATE `user` SET `last_activity_ts` = now() WHERE id=? LIMIT 1', [pseudo_user.id]); invalidate_cached_user_by_id(pseudo_user.id); } - // create token for login - const token = await jwt.sign({uuid: user_uuid}, config.jwt_secret); // user id // todo if pseudo user, assign directly no need to do another DB lookup const user_id = (pseudo_user === undefined) ? insert_res.insertId : pseudo_user.id; + const [user] = await db.read( 'SELECT * FROM `user` WHERE `id` = ? LIMIT 1', [user_id] ); + // create token for login + const token = await svc_auth.create_session_token(user); + // jwt.sign({uuid: user_uuid}, config.jwt_secret); + //------------------------------------------------------------- // email confirmation //------------------------------------------------------------- diff --git a/packages/backend/src/services/PuterAPIService.js b/packages/backend/src/services/PuterAPIService.js index a285ef94..27d29b39 100644 --- a/packages/backend/src/services/PuterAPIService.js +++ b/packages/backend/src/services/PuterAPIService.js @@ -33,6 +33,8 @@ class PuterAPIService extends BaseService { app.use(require('../routers/auth/grant-user-user')); app.use(require('../routers/auth/revoke-user-user')); app.use(require('../routers/auth/list-permissions')) + app.use(require('../routers/auth/list-sessions')) + app.use(require('../routers/auth/revoke-session')) app.use(require('../routers/auth/check-app')) app.use(require('../routers/auth/app-uid-from-origin')) app.use(require('../routers/auth/create-access-token')) diff --git a/packages/backend/src/services/auth/AuthService.js b/packages/backend/src/services/auth/AuthService.js index 539caf02..0f5380d7 100644 --- a/packages/backend/src/services/auth/AuthService.js +++ b/packages/backend/src/services/auth/AuthService.js @@ -70,7 +70,7 @@ class AuthService extends BaseService { } if ( decoded.type === 'session' ) { - const session = this.get_session_(decoded.uuid); + const session = await this.get_session_(decoded.uuid); if ( ! session ) { throw APIError.create('token_auth_failed'); @@ -80,6 +80,7 @@ class AuthService extends BaseService { const actor_type = new UserActorType({ user, + session: session.uuid, }); return new Actor({ @@ -218,8 +219,9 @@ class AuthService extends BaseService { cur_token, this.global_config.jwt_secret ); + console.log('\x1B[36;1mDECODED SESSION', decoded); + if ( decoded.type && decoded.type !== 'session' ) { - // throw APIError.create('token_auth_failed'); return {}; } @@ -228,12 +230,22 @@ class AuthService extends BaseService { return {}; } - if ( decoded.type ) return { user, token: cur_token }; + if ( decoded.type ) { + // Ensure session exists + const session = await this.get_session_(decoded.uuid); + if ( ! session ) { + return {}; + } + + // Return the session + return { user, token: cur_token }; + } this.log.info(`UPGRADING SESSION`); // Upgrade legacy token - const token = await this.create_session_token(user); + // TODO: phase this out + const { token } = await this.create_session_token(user); return { user, token }; } @@ -294,6 +306,31 @@ class AuthService extends BaseService { return jwt; } + async list_sessions (actor) { + // We won't take the cached sessions here because it's + // possible the user has sessions on other servers + const sessions = await this.db.read( + 'SELECT uuid, meta FROM `sessions` WHERE `user_id` = ?', + [actor.type.user.id], + ); + + sessions.forEach(session => { + if ( session.uuid === actor.type.session ) { + session.current = true; + } + }); + + return sessions; + } + + async revoke_session (actor, uuid) { + delete this.sessions[uuid]; + await this.db.write( + `DELETE FROM sessions WHERE uuid = ? AND user_id = ?`, + [uuid, actor.type.user.id] + ); + } + async get_user_app_token_from_origin (origin) { origin = this._origin_from_url(origin); const app_uid = await this._app_uid_from_origin(origin);