Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
47b133d
dev(backend): incomplete oauth2 OIDC impl
KernelDeimos Feb 4, 2026
7c8f0d5
dev(backend): OIDC continued [1]
KernelDeimos Feb 10, 2026
4374281
dev: add re-authentication flow for protect actions
KernelDeimos Feb 10, 2026
5d22ee0
tweak: re-enable re-auth popup closing
KernelDeimos Feb 11, 2026
d532b3d
fix(oidc): session token vs gui token issues
KernelDeimos Feb 11, 2026
3a9a345
tweak: make monthly username changes configurable
KernelDeimos Feb 11, 2026
142d745
fix: "Popup Closed" message, + excess logs
KernelDeimos Feb 12, 2026
0b8eafa
dev(oidc): re-auth remaining protected endpoints
KernelDeimos Feb 12, 2026
df1f5c4
refactor(oidc): extract common (email + username)
KernelDeimos Feb 12, 2026
8923bda
refactor(oidc): update UIWindowChangePassword
KernelDeimos Feb 13, 2026
e2068e7
fix(oidc): fix QR code login issues caused by OIDC
KernelDeimos Feb 13, 2026
21e959b
dev(oidc): remove button to manually invoke re-auth
KernelDeimos Feb 13, 2026
8ecd6cd
dev(oidc): confirm email by default for OIDC
KernelDeimos Feb 13, 2026
4d49f5d
fix: allow `html` property in UIComponentWindow
KernelDeimos Feb 13, 2026
e145f5d
dev(oidc): rewrite "Disable 2FA" window
KernelDeimos Feb 14, 2026
298f1cd
fix: incorrect accessor reference in OIDCService
KernelDeimos Feb 18, 2026
2cdc211
fix: incorrect parameters in UIWindowChangePassword
KernelDeimos Feb 18, 2026
b5a3323
fix: incorrect parameters in UIWindowChangeEmail
KernelDeimos Feb 18, 2026
42d3f9e
fix(oidc): http-only cookie sync for switch user
KernelDeimos Feb 18, 2026
2b80214
fix(oidc): add missing awaits
KernelDeimos Feb 18, 2026
d0c2e9b
fix(oidc): rate-limit identity for username
KernelDeimos Feb 18, 2026
a2b9193
clean(oidc): remove temporary debugging logs
KernelDeimos Feb 18, 2026
7858f5b
fix(oidc): add error log for QR login flow
KernelDeimos Feb 18, 2026
8d18ee5
refactor(oidc): address review comment
KernelDeimos Feb 18, 2026
ccecf0a
fix(oidc): remove generated source file
KernelDeimos Feb 18, 2026
7ca0fe2
clean: remove commented code
KernelDeimos Feb 18, 2026
720277c
style(oidc): migrate cjs to esm
KernelDeimos Feb 19, 2026
16f2f5b
style(oidc): address PR rev on oidcCallbackPreamble_
KernelDeimos Feb 19, 2026
c97a499
style(oidc): private members in OIDCService
KernelDeimos Feb 19, 2026
b5d719e
lint: [+] no-useless-computed-key:error
KernelDeimos Feb 19, 2026
4c51883
clean(oidc): remove "ghost files"
KernelDeimos Feb 19, 2026
4de6d38
style(oidc): make this a private method
KernelDeimos Feb 19, 2026
f3bf40a
style(oidc): rename `httpPowers` to `hasHttpOnlyCookie`
KernelDeimos Feb 19, 2026
b3b3298
clean(oidc): remove jwt remap in ESM code
KernelDeimos Feb 19, 2026
4e01608
dev: remove guarded debug log
KernelDeimos Feb 19, 2026
0112f09
style(oidc): if instead of return with ternary expression
KernelDeimos Feb 19, 2026
7493573
Merge remote-tracking branch 'origin/main' into eric/262A0_PUT-453
Salazareo Feb 19, 2026
1be3eca
fix: rate limits for oidc too extreme
KernelDeimos Feb 20, 2026
0cca9d5
fix : import
Salazareo Feb 20, 2026
b39bbe0
Merge remote-tracking branch 'origin/main' into eric/262A0_PUT-453
Salazareo Feb 20, 2026
ef0a665
fix(oidc): add code lost due to editing a `.js`
KernelDeimos Feb 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export const rules = {
}],
'@stylistic/array-bracket-spacing': ['error', 'never'],
'@stylistic/linebreak-style': ['error', 'unix'],
'no-useless-computed-key': 'error',
'no-sequences': [
'error', {
allowInParentheses: false,
Expand Down
20 changes: 20 additions & 0 deletions extensions/whoami/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ extension.get('/whoami', { subdomain: 'api' }, async (req, res, next) => {
}
}

const oidc_only = req.user.password === null;
const details = {
username: req.user.username,
uuid: req.user.uuid,
Expand All @@ -88,6 +89,23 @@ extension.get('/whoami', { subdomain: 'api' }, async (req, res, next) => {
desktop_bg_color: req.user.desktop_bg_color,
desktop_bg_fit: req.user.desktop_bg_fit,
is_temp: (req.user.password === null && req.user.email === null),
oidc_only,
...(oidc_only ? await (async () => {
try {
const svc_oidc = req.services.get('oidc');
const providers = await svc_oidc.getEnabledProviderIds();
const origin = (svc_oidc.global_config?.origin || '').replace(/\/$/, '');
const provider = providers && providers[0];
if ( provider ) {
return {
oidc_revalidate_url: `${origin}/auth/oidc/${provider}/start?flow=revalidate&user_id=${req.user.id}`,
};
}
return {};
} catch ( _e ) {
return {};
}
})() : {}),
taskbar_items: await get_taskbar_items(req.user, {
...(req.query.icon_size
? { icon_size: req.query.icon_size }
Expand Down Expand Up @@ -216,6 +234,7 @@ extension.post('/whoami', { subdomain: 'api' }, async (req, res) => {
}
}

const oidc_only = req.user.password === null;
// send user object
res.send(Object.assign({
username: req.user.username,
Expand All @@ -228,6 +247,7 @@ extension.post('/whoami', { subdomain: 'api' }, async (req, res) => {
desktop_bg_color: req.user.desktop_bg_color,
desktop_bg_fit: req.user.desktop_bg_fit,
is_temp: (req.user.password === null && req.user.email === null),
oidc_only,
taskbar_items: await get_taskbar_items(req.user),
desktop_items: desktop_items,
referral_code: req.user.referral_code,
Expand Down
6 changes: 6 additions & 0 deletions src/backend/src/CoreModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,12 @@ const install = async ({ context, services, app, useapi, modapi }) => {
const { OTPService } = require('./services/auth/OTPService');
services.registerService('otp', OTPService);

const { OIDCService } = require('./services/auth/OIDCService');
services.registerService('oidc', OIDCService);

const { SignupService } = require('./services/auth/SignupService');
services.registerService('signup', SignupService);

const { UserProtectedEndpointsService } = require('./services/web/UserProtectedEndpointsService');
services.registerService('__user-protected-endpoints', UserProtectedEndpointsService);

Expand Down
8 changes: 4 additions & 4 deletions src/backend/src/ExtensionService.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,23 +153,23 @@ class ExtensionService extends BaseService {
this.state.extension.emit('preinit');
}

async ['__on_boot.consolidation'] (...a) {
async '__on_boot.consolidation' (...a) {
const svc_su = this.services.get('su');
await svc_su.sudo(async () => {
await this.state.extension.emit('init', {}, {
from_outside_of_extension: true,
});
});
}
async ['__on_boot.activation'] (...a) {
async '__on_boot.activation' (...a) {
const svc_su = this.services.get('su');
await svc_su.sudo(async () => {
await this.state.extension.emit('activate', {}, {
from_outside_of_extension: true,
});
});
}
async ['__on_boot.ready'] (...a) {
async '__on_boot.ready' (...a) {
const svc_su = this.services.get('su');
await svc_su.sudo(async () => {
await this.state.extension.emit('ready', {}, {
Expand All @@ -178,7 +178,7 @@ class ExtensionService extends BaseService {
});
}

['__on_install.routes'] (_, { app }) {
'__on_install.routes' (_, { app }) {
if ( ! this.state ) debugger;
for ( const thing of this.state.expressThings_ ) {
if ( thing.type === 'endpoint' ) {
Expand Down
6 changes: 3 additions & 3 deletions src/backend/src/Kernel.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ class Kernel extends AdvancedBase {
extensionInfo: this.extensionInfo,
registry: this.registry,
args,
['runtime-modules']: this.runtimeModuleRegistry,
'runtime-modules': this.runtimeModuleRegistry,
}, 'app');
globalThis.root_context = root_context;

Expand All @@ -148,7 +148,7 @@ class Kernel extends AdvancedBase {
services.registerModule(module_.constructor.name, module_);
const mod_context = this._create_mod_context(Context.get(), {
name: module_.constructor.name,
['module']: module_,
'module': module_,
external: false,
});
await module_.install(mod_context);
Expand Down Expand Up @@ -435,7 +435,7 @@ class Kernel extends AdvancedBase {

const mod_context = this._create_mod_context(mod_install_root_context, {
name: mod_name,
['module']: mod,
'module': mod,
external: true,
mod_path,
});
Expand Down
8 changes: 8 additions & 0 deletions src/backend/src/api/APIError.js
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,10 @@ class APIError {
status: 403,
message: 'This endpoint must be requested with a user session',
},
'session_required': {
status: 403,
message: 'This endpoint requires a full session (e.g. change password cannot be done with a GUI token).',
},
'temporary_accounts_not_allowed': {
status: 403,
message: 'Temporary accounts cannot perform this action',
Expand All @@ -465,6 +469,10 @@ class APIError {
status: 403,
message: 'Password does not match.',
},
'oidc_revalidation_required': {
Comment thread
KernelDeimos marked this conversation as resolved.
status: 403,
message: 'Re-validate by signing in with your linked account (e.g. Google).',
},

// Object Mapping
'field_not_allowed_for_create': {
Expand Down
6 changes: 6 additions & 0 deletions src/backend/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ config.captcha = {
difficulty: 'medium', // Default difficulty level
};

// OIDC/OAuth2 providers (e.g. Google). Keys in config only, not env vars.
// Example: config.oidc.providers.google = { client_id, client_secret }
config.oidc = {
providers: {},
};

config.monitor = {
metricsInterval: 60000,
windowSize: 30,
Expand Down
2 changes: 1 addition & 1 deletion src/backend/src/filesystem/hl_operations/hl_stat.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const { NodeUIDSelector } = require('../node/selectors');

class HLStat extends HLFilesystemOperation {
static MODULES = {
['mime-types']: require('mime-types'),
'mime-types': require('mime-types'),
};

async _run () {
Expand Down
9 changes: 9 additions & 0 deletions src/backend/src/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { Context } from './util/context.js';
import { ManagedError } from './util/errorutil.js';
import { kv } from './util/kvSingleton.js';
import { spanify } from './util/otelutil.js';
import { generate_identifier } from './util/identifier.js';

export * from './validation.js';

Expand Down Expand Up @@ -1487,6 +1488,14 @@ export async function username_exists (username) {
}
}

export async function generate_random_username () {
let username;
do {
username = generate_identifier();
} while ( await username_exists(username) );
return username;
}

export async function app_name_exists (name) {
/** @type BaseDatabaseAccessService */
const db = servicesContainer.services.get('database').get(DB_READ, 'filesystem');
Expand Down
12 changes: 10 additions & 2 deletions src/backend/src/middleware/configurable_auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const APIError = require('../api/APIError');
const config = require('../config');
const { LegacyTokenError } = require('../services/auth/AuthService');
const { Context } = require('../util/context');
const jwt = require('jsonwebtoken');

// The "/whoami" endpoint is a special case where we want to allow
// a legacy token to be used for authentication. The "/whoami"
Expand Down Expand Up @@ -47,23 +48,26 @@ const configurable_auth = options => async (req, res, next) => {
const optional = options?.optional;

// Request might already have been authed (PreAuthService)
if ( req.actor ) next();
if ( req.actor ) return next();
Comment thread
KernelDeimos marked this conversation as resolved.

// === Getting the Token ===
// This step came from jwt_auth in src/helpers.js
// However, since request-response handling is a concern of the
// auth middleware, it makes more sense to put it here.

let token;
let tokenSource;
// Auth token in body
if ( req.body && req.body.auth_token )
{
token = req.body.auth_token;
tokenSource = 'body';
}
// HTTML Auth header
else if ( req.header && req.header('Authorization') && !req.header('Authorization').startsWith('Basic ') && req.header('Authorization') !== 'Bearer' ) { // Bearer with no space is something office does
token = req.header('Authorization');
token = token.replace('Bearer ', '').trim();
tokenSource = 'header';
if ( token === 'undefined' ) {
APIError.create('unexpected_undefined', null, {
msg: 'The Authorization token cannot be the string "undefined"',
Expand All @@ -74,16 +78,19 @@ const configurable_auth = options => async (req, res, next) => {
else if ( req.cookies && req.cookies[config.cookie_name] )
{
token = req.cookies[config.cookie_name];
tokenSource = 'cookie';
}
// Auth token in URL
else if ( req.query && req.query.auth_token )
{
token = req.query.auth_token;
tokenSource = 'query';
}
// Socket
else if ( req.handshake && req.handshake.query && req.handshake.query.auth_token )
{
token = req.handshake.query.auth_token;
tokenSource = 'socket';
}

if ( !token || token.startsWith('Basic ') ) {
Expand Down Expand Up @@ -134,7 +141,8 @@ const configurable_auth = options => async (req, res, next) => {
throw APIError.create('forbidden');
}

res.cookie(config.cookie_name, new_info.token, {
// Use session token in cookie so cookie-based requests have hasHttpOnlyCookie; client gets GUI token in response
res.cookie(config.cookie_name, new_info.session_token ?? new_info.token, {
sameSite: 'none',
secure: true,
httpOnly: true,
Expand Down
8 changes: 4 additions & 4 deletions src/backend/src/modules/apps/AppIconService.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

import config from '../../config.js';
import { createRequire } from 'node:module';
import config from '../../config.js';
import { APP_ICONS_SUBDOMAIN } from '../../consts/app-icons.js';
import { HLWrite } from '../../filesystem/hl_operations/hl_write.js';
import { LLMkdir } from '../../filesystem/ll_operations/ll_mkdir.js';
import { LLRead } from '../../filesystem/ll_operations/ll_read.js';
import { NodePathSelector } from '../../filesystem/node/selectors.js';
import { APP_ICONS_SUBDOMAIN } from '../../consts/app-icons.js';
import { get_app, get_user } from '../../helpers.js';
import BaseService from '../../services/BaseService.js';
import { DB_WRITE } from '../../services/database/consts.js';
Expand Down Expand Up @@ -69,7 +69,7 @@ export class AppIconService extends BaseService {
* endpoints /app-icon/:app_uid and /app-icon/:app_uid/:size
* which serve the app icon at the requested size.
*/
async ['__on_install.routes'] (_, { app }) {
async '__on_install.routes' (_, { app }) {
const handler = async (req, res) => {
// Validate parameters
let { app_uid: appUid, size } = req.params;
Expand Down Expand Up @@ -686,7 +686,7 @@ export class AppIconService extends BaseService {
* `/system/app_icons` directory if it does not exist,
* and then to register the event listener for `app.new-icon`.
*/
async ['__on_user.system-user-ready'] () {
async '__on_user.system-user-ready' () {
const svcSu = this.services.get('su');
const svcUser = this.services.get('user');

Expand Down
2 changes: 1 addition & 1 deletion src/backend/src/modules/apps/OldAppNameService.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class OldAppNameService extends BaseService {
this.db = this.services.get('database').get(DB_READ, 'old-app-name');
}

async ['__on_boot.consolidation'] () {
async '__on_boot.consolidation' () {
const svc_event = this.services.get('event');
svc_event.on('app.rename', async (_, { app_uid, old_name }) => {
this.log.info('GOT EVENT', { app_uid, old_name });
Expand Down
2 changes: 1 addition & 1 deletion src/backend/src/modules/apps/RecommendedAppsService.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export default class RecommendedAppsService extends BaseService {
this.app_names = new Set(RecommendedAppsService.APP_NAMES);
}

['__on_boot.consolidation'] () {
'__on_boot.consolidation' () {
const svc_appIcon = this.services.get('app-icon');
const svc_event = this.services.get('event');
svc_event.on('apps.invalidate', async (_, { app }) => {
Expand Down
4 changes: 2 additions & 2 deletions src/backend/src/modules/broadcast/BroadcastService.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ class BroadcastService extends BaseService {
}
}

async ['__on_install.routes'] (_, { app }) {
async '__on_install.routes' (_, { app }) {
const svc_web = this.services.get('web-server');
svc_web.allow_undefined_origin('/broadcast/webhook');

Expand Down Expand Up @@ -253,7 +253,7 @@ class BroadcastService extends BaseService {
}
}

async ['__on_install.websockets'] () {
async '__on_install.websockets' () {
const svc_event = this.services.get('event');
const svc_webServer = this.services.get('web-server');

Expand Down
2 changes: 1 addition & 1 deletion src/backend/src/modules/captcha/services/CaptchaService.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class CaptchaService extends BaseService {
this.endpointsRegistered = false;
}

async ['__on_install.middlewares.context-aware'] (_, { app }) {
async '__on_install.middlewares.context-aware' (_, { app }) {
// Add express middleware
app.use(checkCaptcha({ svc_captcha: this }));
}
Expand Down
2 changes: 1 addition & 1 deletion src/backend/src/modules/core/AlarmService.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class AlarmService extends BaseService {
* AlarmService registers its commands at the consolidation phase because
* the '_init' method of CommandService may not have been called yet.
*/
['__on_boot.consolidation'] () {
'__on_boot.consolidation' () {
this._register_commands(this.services.get('commands'));
}

Expand Down
2 changes: 1 addition & 1 deletion src/backend/src/modules/core/ExpectationService.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class ExpectationService extends BaseService {
* ExpectationService registers its commands at the consolidation phase because
* the '_init' method of CommandService may not have been called yet.
*/
['__on_boot.consolidation'] () {
'__on_boot.consolidation' () {
const commands = this.services.get('commands');
commands.registerCommands('expectations', [
{
Expand Down
2 changes: 1 addition & 1 deletion src/backend/src/modules/core/LogService.js
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ class LogService extends BaseService {
/**
* Registers logging commands with the command service.
*/
['__on_boot.consolidation'] () {
'__on_boot.consolidation' () {
const commands = this.services.get('commands');
commands.registerCommands('logs', [
{
Expand Down
2 changes: 1 addition & 1 deletion src/backend/src/modules/core/PagerService.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class PagerService extends BaseService {
* PagerService registers its commands at the consolidation phase because
* the '_init' method of CommandService may not have been called yet.
*/
['__on_boot.consolidation'] () {
'__on_boot.consolidation' () {
this._register_commands(this.services.get('commands'));
}

Expand Down
2 changes: 1 addition & 1 deletion src/backend/src/modules/core/ParameterService.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ class ParameterService extends BaseService {
* for parameter management.
* @private
*/
['__on_boot.consolidation'] () {
'__on_boot.consolidation' () {
this._registerCommands(this.services.get('commands'));
}

Expand Down
2 changes: 1 addition & 1 deletion src/backend/src/modules/data-access/AppService.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ export default class AppService extends BaseService {
static WRITE_ALL_OWNER_PERMISSION = 'system:es:write-all-owners';

static IMPLEMENTS = {
['crud-q']: {
'crud-q': {
async create ({ object, options }) {
return await this.#create({ object, options });
},
Expand Down
Loading
Loading