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.