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.
*