[OIDC] allow user deletion for accounts without a password (#2567)

* fix: user deletion for OIDC accounts

* clean(backend): update copied license header

* clean(backend): replace previously removed comments

* fix: double-encoding
This commit is contained in:
Eric Dubé
2026-02-27 18:55:12 -05:00
committed by GitHub
parent 9d4e990b92
commit 2cc8cb22f8
6 changed files with 179 additions and 106 deletions
@@ -106,6 +106,8 @@ const hardcoded_user_group_permissions = {
'local-terminal:access': {},
},
'b7220104-7905-4985-b996-649fdcdb3c8f': {
'driver': {},
'service': {},
'service:hello-world:ii:hello-world': policy_perm('temp.es'),
'service:puter-kvstore:ii:puter-kvstore': policy_perm('temp.kv'),
'driver:puter-kvstore': policy_perm('temp.kv'),
@@ -119,6 +121,8 @@ const hardcoded_user_group_permissions = {
'service:es\\Csubdomain:ii:crud-q': policy_perm('user.es'),
},
'78b1b1dd-c959-44d2-b02c-8735671f9997': {
'driver': {},
'service': {},
'service:hello-world:ii:hello-world': policy_perm('user.es'),
'service:puter-kvstore:ii:puter-kvstore': policy_perm('user.kv'),
'driver:puter-kvstore': policy_perm('user.kv'),
@@ -1,61 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
const eggspress = require('../../api/eggspress');
const { deleteUser, invalidate_cached_user } = require('../../helpers');
const config = require('../../config');
module.exports = eggspress('/delete-own-user', {
subdomain: 'api',
auth: true,
allowedMethods: ['POST'],
}, async (req, res) => {
const bcrypt = require('bcrypt');
const validate_request = async () => {
const user = req.user;
// `user` should always have a value, but this is checked
// any way in case the auth middleware is broken.
if ( ! user ) return false;
// temporary users don't require password verification
if ( !user.email && !user.password ) {
return true;
}
if ( ! req.body.password ) return false;
if ( !user || !user.password ) return false;
if ( ! await bcrypt.compare(req.body.password, req.user.password) ) {
return false;
}
return true;
};
if ( ! await validate_request() ) {
return res.status(400).send({ success: false });
}
res.clearCookie(config.cookie_name);
await deleteUser(req.user.id);
invalidate_cached_user(req.user);
return res.send({ success: true });
});
@@ -0,0 +1,36 @@
/*
* Copyright (C) 2026-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 <https://www.gnu.org/licenses/>.
*/
const config = require('../../config');
const { deleteUser, invalidate_cached_user } = require('../../helpers');
const REVALIDATION_COOKIE_NAME = 'puter_revalidation';
module.exports = {
route: '/delete-own-user',
methods: ['POST'],
handler: async (req, res) => {
res.clearCookie(config.cookie_name);
res.clearCookie(REVALIDATION_COOKIE_NAME);
await deleteUser(req.user.id);
invalidate_cached_user(req.user);
return res.send({ success: true });
},
};
@@ -53,7 +53,6 @@ class PuterAPIService extends BaseService {
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'));
app.use(require('../routers/drivers/list-interfaces'));
@@ -109,9 +109,10 @@ class UserProtectedEndpointsService extends BaseService {
next();
});
// Do not allow temporary users
// Do not allow temporary users (except for delete-own-user, which allows them)
router.use(async (req, res, next) => {
if ( req.method === 'OPTIONS' ) return next();
if ( req.path === '/delete-own-user' ) return next();
if ( req.user.password === null && req.user.email === null ) {
return APIError.create('temporary_account').write(res);
@@ -122,6 +123,7 @@ class UserProtectedEndpointsService extends BaseService {
/**
* Middleware to validate identity: either password (bcrypt) or a valid OIDC revalidation cookie.
* OIDC-only accounts (user.password === null) must use revalidation; password accounts may use either.
* Temporary users (no password, no email) are allowed only for delete-own-user.
*/
router.use(async (req, res, next) => {
if ( req.method === 'OPTIONS' ) return next();
@@ -129,6 +131,10 @@ class UserProtectedEndpointsService extends BaseService {
const user = await get_user({ id: req.user.id, force: true });
const revalidationCookie = req.cookies && req.cookies[REVALIDATION_COOKIE_NAME];
if ( user.password === null && user.email === null ) {
return next();
}
if ( req.body.password ) {
if ( user.password === null ) {
return (APIError.create('oidc_revalidation_required', null, await this.#revalidateUrlFields(req, user))).write(res);
@@ -168,6 +174,8 @@ class UserProtectedEndpointsService extends BaseService {
Endpoint(require('../../routers/user-protected/change-username.js')).attach(router);
Endpoint(require('../../routers/user-protected/disable-2fa.js')).attach(router);
Endpoint(require('../../routers/user-protected/delete-own-user.js')).attach(router);
}
}