From e2068e7b9c2f381653bb2794a3c52f7279ee34aa Mon Sep 17 00:00:00 2001 From: KernelDeimos <7225168+KernelDeimos@users.noreply.github.com> Date: Fri, 13 Feb 2026 13:46:58 -0500 Subject: [PATCH] fix(oidc): fix QR code login issues caused by OIDC In implementing OIDC it became necessary to introduce the separation of "GUI Tokens" and "Session Tokens". This breaks QR login because Puter does not set the HTTP-only session cookie when logging in with this flow. Add a middelware to WebServerService to detect QR Code logins and set the appropriate HTTP-only session cookie. --- .../src/modules/web/WebServerService.js | 28 +++++++++++++++++++ .../routers/user-protected/change-username.js | 2 +- src/backend/src/services/auth/AuthService.js | 19 +++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/backend/src/modules/web/WebServerService.js b/src/backend/src/modules/web/WebServerService.js index 1814b4412..7db1ad6a4 100644 --- a/src/backend/src/modules/web/WebServerService.js +++ b/src/backend/src/modules/web/WebServerService.js @@ -337,6 +337,34 @@ class WebServerService extends BaseService { next(); }); + // When the user visits the main origin (not api/dav subdomain) with ?auth_token= + // (e.g. QR login), set the HTTP-only session cookie so user-protected endpoints work. + app.use(async (req, res, next) => { + const has_subdomain = req.hostname.slice(0, -1 * (config.domain.length + 1)) !== ''; + if ( has_subdomain ) return next(); + + const token = req.query?.auth_token; + if ( !token || typeof token !== 'string' ) return next(); + + try { + const svc_auth = req.services.get('auth'); + const cleanToken = token.replace('Bearer ', '').trim(); + const actor = await svc_auth.authenticate_from_token(cleanToken); + const session_token = svc_auth.create_session_token_for_session( + actor.type.user, + actor.type.session, + ); + res.cookie(config.cookie_name, session_token, { + sameSite: 'none', + secure: true, + httpOnly: true, + }); + } catch ( _e ) { + // Invalid or expired token; do not set cookie + } + next(); + }); + // Measure data transfer amounts app.use(measure()); diff --git a/src/backend/src/routers/user-protected/change-username.js b/src/backend/src/routers/user-protected/change-username.js index 822b39ba4..ece0c5fc7 100644 --- a/src/backend/src/routers/user-protected/change-username.js +++ b/src/backend/src/routers/user-protected/change-username.js @@ -25,7 +25,7 @@ const { Context } = require('../../util/context'); module.exports = { route: '/change-username', methods: ['POST'], - handler: async (req, res, next) => { + handler: async (req, res, _next) => { const user = req.user; const new_username = req.body.new_username; diff --git a/src/backend/src/services/auth/AuthService.js b/src/backend/src/services/auth/AuthService.js index 91b6ad0a4..1881c3ed1 100644 --- a/src/backend/src/services/auth/AuthService.js +++ b/src/backend/src/services/auth/AuthService.js @@ -355,6 +355,25 @@ class AuthService extends BaseService { }, this.global_config.jwt_secret); } + /** + * Creates a session token (hasHttpPowers) for an existing session. + * Used when the client authenticated with a GUI token (e.g. QR login via + * ?auth_token=) so we can set the HTTP-only cookie and allow user-protected + * endpoints (change password, email, username, etc.) to work. + * + * @param {*} user - User object (must have .uuid). + * @param {string} session_uuid - Existing session UUID. + * @returns {string} JWT session token. + */ + create_session_token_for_session (user, session_uuid) { + return this.modules.jwt.sign({ + type: 'session', + version: '0.0.0', + uuid: session_uuid, + user_uid: user.uuid, + }, this.global_config.jwt_secret); + } + /** * This method checks if the provided session token is valid and returns the associated user and token. * If the token is not a valid session token or it does not exist in the database, it returns an empty object.