From 3ffe8eaf30fa347322563b3940d026d4cdf720bf Mon Sep 17 00:00:00 2001 From: KernelDeimos <7225168+KernelDeimos@users.noreply.github.com> Date: Tue, 3 Feb 2026 15:11:33 -0500 Subject: [PATCH] dev(backend): add `revoke_access_token` endpoint --- .../src/routers/auth/revoke-access-token.js | 64 +++++++++++++++++++ src/backend/src/services/PuterAPIService.js | 1 + src/backend/src/services/auth/AuthService.js | 30 +++++++++ 3 files changed, 95 insertions(+) create mode 100644 src/backend/src/routers/auth/revoke-access-token.js diff --git a/src/backend/src/routers/auth/revoke-access-token.js b/src/backend/src/routers/auth/revoke-access-token.js new file mode 100644 index 000000000..4081cc8af --- /dev/null +++ b/src/backend/src/routers/auth/revoke-access-token.js @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2024-present Puter Technologies Inc. + * + * This file is part of Puter. + * + * Puter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +const APIError = require('../../api/APIError'); +const eggspress = require('../../api/eggspress'); +const { Context } = require('../../util/context'); + +/** + * Coerces a read-URL string to the token (JWT) from its query. + * Works for absolute or relative URLs (e.g. .../token-read?uid=...&token=...). + * Returns the given value unchanged if it does not look like a read URL. + */ +function tokenOrUuidFromInput (value) { + if ( typeof value !== 'string' || !value.trim() ) { + return value; + } + const s = value.trim(); + console.log('s?', s); + if ( s.includes('/token-read') ) { + try { + const url = new URL(s); + const token = url.searchParams.get('token'); + console.log('token?', token); + return token ?? s; + } catch (_) { + return s; + } + } + return s; +} + +module.exports = eggspress('/auth/revoke-access-token', { + subdomain: 'api', + auth2: true, + allowedMethods: ['POST'], +}, async (req, res, next) => { + const x = Context.get(); + const svc_auth = x.get('services').get('auth'); + + const raw = req.body.tokenOrUuid; + if ( raw === undefined || raw === null ) { + throw APIError.create('field_missing', null, { key: 'tokenOrUuid' }); + } + const tokenOrUuid = tokenOrUuidFromInput(raw); + + await svc_auth.revoke_access_token(tokenOrUuid); + + res.json({ ok: true }); +}); diff --git a/src/backend/src/services/PuterAPIService.js b/src/backend/src/services/PuterAPIService.js index 32bee8bd5..96b168f6d 100644 --- a/src/backend/src/services/PuterAPIService.js +++ b/src/backend/src/services/PuterAPIService.js @@ -50,6 +50,7 @@ class PuterAPIService extends BaseService { app.use(require('../routers/auth/check-app')); app.use(require('../routers/auth/app-uid-from-origin')); app.use(require('../routers/auth/create-access-token')); + app.use(require('../routers/auth/revoke-access-token')); app.use(require('../routers/auth/delete-own-user')); app.use(require('../routers/auth/configure-2fa')); app.use(require('../routers/drivers/call')); diff --git a/src/backend/src/services/auth/AuthService.js b/src/backend/src/services/auth/AuthService.js index 7df1bedcd..e7b1241f2 100644 --- a/src/backend/src/services/auth/AuthService.js +++ b/src/backend/src/services/auth/AuthService.js @@ -444,9 +444,39 @@ class AuthService extends BaseService { Object.values(insert_object)); } + console.log('token uuid?', uuid); + return jwt; } + /** + * Revokes an access token by removing it from the database. + * Accepts either the access token JWT or the token UUID. + * + * @param {string} tokenOrUuid - The access token JWT or the token UUID. + * @returns {Promise} + */ + async revoke_access_token (tokenOrUuid) { + let token_uid; + const isJwt = typeof tokenOrUuid === 'string' && + /^[\w-]*\.[\w-]*\.[\w-]*$/.test(tokenOrUuid.trim()); + if ( isJwt ) { + const decoded = this.modules.jwt.verify(tokenOrUuid, this.global_config.jwt_secret); + if ( decoded.type !== 'access-token' || !decoded.token_uid ) { + throw APIError.create('token_auth_failed'); + } + token_uid = decoded.token_uid; + } else { + token_uid = tokenOrUuid; + } + /* eslint-disable */ + await this.db.write( + 'DELETE FROM `access_token_permissions` WHERE `token_uid` = ?', + [token_uid], + ); + /* eslint-enable */ + } + /** * Get the session list for the specified actor. *