the oidc module expect accountId and sub to be the same

in our case sub is the username exposed to the app, not the userId
internal to Cloudron

Upstream behavior change 9b89153c0e
This commit is contained in:
Johannes Zellner
2025-07-01 22:07:31 +02:00
parent b2d380afcc
commit 846986987d
3 changed files with 27 additions and 19 deletions

View File

@@ -3,7 +3,7 @@
exports = module.exports = {
start,
stop,
revokeByUserId,
revokeByUsername,
consumeAuthCode,
cleanupExpired,
@@ -96,7 +96,11 @@ class StorageAdapter {
if (this.name === 'AccessToken' && (payload.clientId === oidcClients.ID_WEBADMIN || payload.clientId === oidcClients.ID_DEVELOPMENT)) {
const expires = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS;
const [error] = await safe(tokens.add({ clientId: payload.clientId, identifier: payload.accountId, expires, accessToken: id, allowedIpRanges: '' }));
// oidc uses the username as accountId but accesstoken identifiers are userIds
const user = await users.getByUsername(payload.accountId);
if (!user) throw new Error(`user for username ${payload.accountId} not found`);
const [error] = await safe(tokens.add({ clientId: payload.clientId, identifier: user.id, expires, accessToken: id, allowedIpRanges: '' }));
if (error) {
console.log('Error adding access token', error);
throw error;
@@ -150,10 +154,14 @@ class StorageAdapter {
// dashboard AccessToken are in the db. the app tokens are in the json files
const [error, result] = await safe(tokens.getByAccessToken(id));
if (!error && result) {
return {
accountId: result.identifier,
clientId: result.clientId
};
// translate from userId in the token to username for oidc
const user = await users.get(result.identifier);
if (user) {
return {
accountId: user.username,
clientId: result.clientId
};
}
}
} else if (this.name === 'Session') {
const data = await StorageAdapter.getData(this.name);
@@ -162,7 +170,7 @@ class StorageAdapter {
if (session.payload.accountId) {
// check if the session user still exists and is active
const user = await users.get(session.payload.accountId);
const user = await users.getByUsername(session.payload.accountId);
if (!user || !user.active) return null;
}
@@ -217,14 +225,14 @@ class StorageAdapter {
}
// Session, Grant and Token management. This is based on the same storage as the below CloudronAdapter
async function revokeByUserId(userId) {
assert.strictEqual(typeof userId, 'string');
async function revokeByUsername(username) {
assert.strictEqual(typeof username, 'string');
const types = [ 'Session', 'Grant', 'AuthorizationCode', 'AccessToken' ];
for (const type of types) {
await StorageAdapter.updateData(type, (data) => {
for (const id in data) {
if (data[id].payload?.accountId === userId) delete data[id];
if (data[id].payload?.accountId === username) delete data[id];
}
});
}
@@ -321,7 +329,7 @@ async function renderInteractionPage(req, res, next) {
// check if user has access to the app if client refers to an app
if (app) {
const user = await users.get(session.accountId);
const user = await users.getByUsername(session.accountId);
options.NAME = app.label || app.fqdn;
options.ICON_URL = app.iconUrl;
@@ -356,13 +364,13 @@ async function interactionLogin(req, res, next) {
const token = await tokens.getByAccessToken(req.body.autoLoginToken);
if (!token) return next(new HttpError(401, 'No such token'));
const user = await users.get(token.identifier);
const user = await users.getByUsername(token.identifier);
if (!user) return next(new HttpError(401,'User not found'));
if (!user.active) return next(new HttpError(401,'User not active'));
const result = {
login: {
accountId: user.id,
accountId: user.username,
},
};
@@ -391,7 +399,7 @@ async function interactionLogin(req, res, next) {
// this is saved as part of interaction.lastSubmission
const result = {
login: {
accountId: user.id,
accountId: user.username,
},
ghost: !!user.ghost
};
@@ -411,7 +419,7 @@ async function interactionConfirm(req, res, next) {
const client = await oidcClients.get(params.client_id);
if (!client) return next(new Error('Client not found'));
const user = await users.get(accountId);
const user = await users.getByUsername(accountId);
if (!user) return next(new Error('User not found'));
user.ghost = !!lastSubmission?.ghost; // restore ghost flag. lastSubmission can be empty if login interaction was skipped (already logged in)
@@ -476,8 +484,8 @@ async function interactionAbort(req, res, next) {
if (error) return next(error);
}
async function getClaims(userId/*, use, scope*/) {
const [error, user] = await safe(users.get(userId));
async function getClaims(username/*, use, scope*/) {
const [error, user] = await safe(users.getByUsername(username));
if (error) return { error: 'user not found' };
const [groupsError, allGroups] = await safe(groups.listWithMembers());