From b5648a8a7f4a989e8a0c9fc73a69568af527c7ee Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:13:42 +0200 Subject: [PATCH 01/17] fix user creation --- src/routes/data.js | 8 ++-- src/routes/index.js | 2 - src/routes/oauth-application.js | 5 ++- src/routes/oauth-mail.js | 11 ++--- src/services/mailboxes.js | 11 +++-- src/services/users.js | 25 +++++++++++ src/utils/db.js | 77 ++++++++++----------------------- 7 files changed, 67 insertions(+), 72 deletions(-) diff --git a/src/routes/data.js b/src/routes/data.js index 9b4b34f..ae0f641 100644 --- a/src/routes/data.js +++ b/src/routes/data.js @@ -2,9 +2,7 @@ import { Router } from "express" import { HttpError } from "#types/errors" import micromatch from "micromatch" -import logger from "#utils/logger" import config from "#utils/config" -import { FindBy, UpdateBy, DeleteFromArrayBy, AddToArray } from "#utils/db" import services from "#services" @@ -57,7 +55,7 @@ async function ValidateMailboxAsync(email, name) { throw new HttpError(400, "Invalid name") } - const existing = await FindBy({ "mailboxes.email": email }) + const existing = await services.mailboxes.GetMailboxByEmail(email) if (existing) { throw new HttpError(409, "Mailbox already claimed") @@ -67,7 +65,7 @@ async function ValidateMailboxAsync(email, name) { async function EnsureNotMailboxAsync(email) { ValidateEmail(email) - const mailbox = await FindBy({ "mailboxes.email": email }) + const existing = await services.mailboxes.GetMailboxByEmail(email) if (mailbox) { throw new HttpError(409, "Mailbox already claimed") @@ -77,7 +75,7 @@ async function EnsureNotMailboxAsync(email) { async function EnsureMailboxAsync(email) { ValidateEmail(email) - const mailbox = await FindBy({ "mailboxes.email": email }) + const existing = await services.mailboxes.GetMailboxByEmail(email) if (!mailbox) { throw new HttpError(404, "Mailbox does not exist") diff --git a/src/routes/index.js b/src/routes/index.js index bb99434..1de7f78 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -1,8 +1,6 @@ import { Router } from "express" -import { HttpError } from "#types/errors" import { RequireMailAuth } from "#router" -import logger from "#utils/logger" const router = Router() diff --git a/src/routes/oauth-application.js b/src/routes/oauth-application.js index 8b51c47..958ef5f 100644 --- a/src/routes/oauth-application.js +++ b/src/routes/oauth-application.js @@ -5,6 +5,7 @@ import logger from "#utils/logger" import passport from "passport" import { Strategy as OpenIDConnectStrategy } from "passport-openidconnect" +import services from "#services" const router = Router() @@ -26,6 +27,8 @@ passport.use( return done(new Error("OIDC profile missing id")) } + services.users.FindOrCreateUser() + logger.dev("Profile: ", profile) return done(null, profile) @@ -36,8 +39,6 @@ passport.use( passport.serializeUser((user, done) => { done(null, { id: user.id, - displayName: user.displayName, - emails: user.emails, }) }) diff --git a/src/routes/oauth-mail.js b/src/routes/oauth-mail.js index 0b705af..24fa6e6 100644 --- a/src/routes/oauth-mail.js +++ b/src/routes/oauth-mail.js @@ -6,12 +6,9 @@ import { DecodeToken, SignToken } from "#utils/token" import logger from "#utils/logger" import config from "#utils/config" -import { - GetUserByID, - WriteToCache, - GetFromCache, - DeleteFromCache, -} from "#utils/db" +import { WriteToCache, GetFromCache, DeleteFromCache } from "#utils/db" + +import services from "#services" import tldts from "tldts" @@ -58,7 +55,7 @@ async function GetUserInfo(endpoint, token) { async function IsOwnedByUser(id, email) { if (!id || !email) return false - const user = await GetUserByID(id) + const user = await services.users.GetUserByID(id) if (!user) return false diff --git a/src/services/mailboxes.js b/src/services/mailboxes.js index 4679012..fe9851e 100644 --- a/src/services/mailboxes.js +++ b/src/services/mailboxes.js @@ -1,16 +1,21 @@ -import { AddToArray, DeleteFromArrayBy, UpdateBy } from "#utils/db" +import { AddToArray, DeleteFromArrayBy, UpdateBy, FindBy } from "#utils/db" export async function EditMailbox(id, email, { name }) { await UpdateBy( + "users", { id: id, "mailboxes.email": email }, { "mailboxes.$.name": name }, ) } export async function CreateMailbox(id, { email, name }) { - await AddToArray({ id: id }, { mailboxes: { email, name } }) + await AddToArray("users", { id: id }, { mailboxes: { email, name } }) } export async function DeleteMailbox(id, email) { - await DeleteFromArrayBy({ id: id }, { mailboxes: { email: email } }) + await DeleteFromArrayBy("users", { id: id }, { mailboxes: { email: email } }) +} + +export async function GetMailboxByEmail(email) { + return await FindBy("users", { "mailboxes.email": email }) } diff --git a/src/services/users.js b/src/services/users.js index e69de29..2a854e1 100644 --- a/src/services/users.js +++ b/src/services/users.js @@ -0,0 +1,25 @@ +import { FindOrCreate, FindBy, DeleteFromArrayBy } from "#utils/db" +import logger from "#utils/logger" + +export async function FindOrCreateUser(id, name) { + return await FindOrCreate( + "users", + { id: id }, + { + id: id, + name: profile.name, + mailboxes: [], + }, + ) +} + +export async function GetUserByID(id) { + return await FindBy("users", { id: id }) +} + +export async function DeleteUserByID(id) { + const res = await DeleteFromArrayBy("users", { id: id }) + + logger.warn("Deleted a User") + return res +} diff --git a/src/utils/db.js b/src/utils/db.js index 66fd52b..570cc22 100644 --- a/src/utils/db.js +++ b/src/utils/db.js @@ -38,82 +38,53 @@ export function Connect() { // Mongo -export async function InsertUser(user) { +export async function FindBy(collectionName, query) { const db = await Connect() - const collection = db.collection("users") + const collection = db.collection(collectionName) - const result = await collection.insertOne(user) - - return result -} - -export async function GetUserByID(id) { - const db = await Connect() - - const collection = db.collection("users") - - return await collection.findOne({ id: id }) + return collection.findOne(query) } -export async function DeleteUserByID(id) { +export async function FindOrCreate(collectionName, query, data = {}) { const db = await Connect() - const collection = db.collection("users") - - const result = await collection.deleteOne({ id: id }) - - logger.warn("Deleted a User") - - return result -} - -export async function FindBy(query) { - const db = await Connect() + const collection = db.collection(collectionName) - const collection = db.collection("users") - - const result = await collection.findOne(query) - - return result + return collection.findOneAndUpdate( + query, + { + $setOnInsert: data, + }, + { + upsert: true, + returnDocument: "after", + }, + ) } -export async function AddToArray(query, update) { +export async function AddToArray(collectionName, query, update) { const db = await Connect() - const collection = db.collection("users") + const collection = db.collection(collectionName) - const result = await collection.updateOne( - query, - { $addToSet: update }, - { upsert: true }, - ) - - return result + return collection.updateOne(query, { $addToSet: update }) } -export async function DeleteFromArrayBy(query, update) { +export async function DeleteFromArrayBy(collectionName, query, update) { const db = await Connect() - const collection = db.collection("users") - - const result = await collection.updateOne(query, { $pull: update }) + const collection = db.collection(collectionName) - return result + return collection.updateOne(query, { $pull: update }) } -export async function UpdateBy(query, update) { +export async function UpdateBy(collectionName, query, update) { const db = await Connect() - const collection = db.collection("users") - - const result = await collection.updateOne( - query, - { $set: update }, - { upsert: true }, - ) + const collection = db.collection(collectionName) - return result + return collection.updateOne(query, { $set: update }) } // REDIS From 0b33fce1e8d2d5a352be9159d34df0a9d82f3902 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:16:29 +0200 Subject: [PATCH 02/17] fix missing import --- src/router.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/router.js b/src/router.js index b1092f9..07878f0 100644 --- a/src/router.js +++ b/src/router.js @@ -1,7 +1,7 @@ import { Router } from "express" import { HttpError } from "#types/errors" -import { GetUserByID } from "#utils/db" +import services from "#services" const router = Router() @@ -66,7 +66,7 @@ router.use(async (req, res, next) => { if (id) { res.locals.id = id - res.locals.user = await GetUserByID(id) + res.locals.user = await services.users.GetUserByID(id) } next() From 532c751b2ed76e2d4df1725e3a318dbc88bee50b Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:22:16 +0200 Subject: [PATCH 03/17] fix missing param --- src/routes/oauth-application.js | 2 +- src/services/users.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/oauth-application.js b/src/routes/oauth-application.js index 958ef5f..282eb9a 100644 --- a/src/routes/oauth-application.js +++ b/src/routes/oauth-application.js @@ -27,7 +27,7 @@ passport.use( return done(new Error("OIDC profile missing id")) } - services.users.FindOrCreateUser() + services.users.FindOrCreateUser(profile.id, profile.name) logger.dev("Profile: ", profile) diff --git a/src/services/users.js b/src/services/users.js index 2a854e1..b9b8bbb 100644 --- a/src/services/users.js +++ b/src/services/users.js @@ -7,7 +7,7 @@ export async function FindOrCreateUser(id, name) { { id: id }, { id: id, - name: profile.name, + name: name, mailboxes: [], }, ) From 11203855727144a65a0253186ea0534cf8539a5d Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:25:15 +0200 Subject: [PATCH 04/17] fix --- src/routes/data.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/routes/data.js b/src/routes/data.js index ae0f641..208bc77 100644 --- a/src/routes/data.js +++ b/src/routes/data.js @@ -55,9 +55,9 @@ async function ValidateMailboxAsync(email, name) { throw new HttpError(400, "Invalid name") } - const existing = await services.mailboxes.GetMailboxByEmail(email) + const mailbox = await services.mailboxes.GetMailboxByEmail(email) - if (existing) { + if (mailbox) { throw new HttpError(409, "Mailbox already claimed") } } @@ -65,7 +65,7 @@ async function ValidateMailboxAsync(email, name) { async function EnsureNotMailboxAsync(email) { ValidateEmail(email) - const existing = await services.mailboxes.GetMailboxByEmail(email) + const mailbox = await services.mailboxes.GetMailboxByEmail(email) if (mailbox) { throw new HttpError(409, "Mailbox already claimed") @@ -75,7 +75,7 @@ async function EnsureNotMailboxAsync(email) { async function EnsureMailboxAsync(email) { ValidateEmail(email) - const existing = await services.mailboxes.GetMailboxByEmail(email) + const mailbox = await services.mailboxes.GetMailboxByEmail(email) if (!mailbox) { throw new HttpError(404, "Mailbox does not exist") From 39fb42adbd07c8cadb9efcdf6067a180a4a66860 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:30:03 +0200 Subject: [PATCH 05/17] debug --- src/routes/oauth-mail.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/routes/oauth-mail.js b/src/routes/oauth-mail.js index 24fa6e6..e5010f6 100644 --- a/src/routes/oauth-mail.js +++ b/src/routes/oauth-mail.js @@ -244,6 +244,9 @@ router.get("/mailbox", async (req, res, next) => { originalHost, ) + logger.info("URIS: ", config.MAIL_CALLBACK_URIS) + logger.info("Host: ", originalHost) + if (!redirectUri) { return res.status(400).send("No matching callback URI") } From 851e229ceebb422819920a9f47c66588177d1f43 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:33:15 +0200 Subject: [PATCH 06/17] more debugging --- src/routes/oauth-mail.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/routes/oauth-mail.js b/src/routes/oauth-mail.js index e5010f6..ce7596c 100644 --- a/src/routes/oauth-mail.js +++ b/src/routes/oauth-mail.js @@ -72,8 +72,12 @@ function GetBaseUrl(req, overwriteHost = null) { function GetMatchingRedirectUri(req, redirectUris, host = null) { const baseUrl = GetBaseUrl(req, host) + logger.info("Base URL: ", baseUrl) + const rootDomain = tldts.parse(baseUrl).domain + logger.info("Domain: ", rootDomain) + let candidates = redirectUris.filter( (uri) => tldts.parse(uri).domain === rootDomain, ) From 047c8b03b1062fab0f53a07d0d63707826590dbb Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:37:28 +0200 Subject: [PATCH 07/17] moooore debug --- src/routes/oauth-mail.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/routes/oauth-mail.js b/src/routes/oauth-mail.js index ce7596c..1e924ce 100644 --- a/src/routes/oauth-mail.js +++ b/src/routes/oauth-mail.js @@ -66,17 +66,19 @@ function GetBaseUrl(req, overwriteHost = null) { const prot = req.protocol const host = overwriteHost || req.get("host") + logger.info(`Protocol: ${prot}`) + return `${prot}://${host}` } function GetMatchingRedirectUri(req, redirectUris, host = null) { const baseUrl = GetBaseUrl(req, host) - logger.info("Base URL: ", baseUrl) + logger.info(`Base URL: ${baseUrl}`) const rootDomain = tldts.parse(baseUrl).domain - logger.info("Domain: ", rootDomain) + logger.info(`Domain: ${rootDomain}`) let candidates = redirectUris.filter( (uri) => tldts.parse(uri).domain === rootDomain, @@ -242,6 +244,8 @@ router.get("/mailbox", async (req, res, next) => { req.session.mail = {} + logger.info("Checking Redirect URIS...") + const redirectUri = GetMatchingRedirectUri( req, config.MAIL_CALLBACK_URIS, From 3ce8e804f5f3db56467aa00a2e00f02a307d7b65 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:44:22 +0200 Subject: [PATCH 08/17] debug + fix using .host instead of obj --- src/routes/oauth-mail.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/routes/oauth-mail.js b/src/routes/oauth-mail.js index 1e924ce..56545a8 100644 --- a/src/routes/oauth-mail.js +++ b/src/routes/oauth-mail.js @@ -116,6 +116,7 @@ router.get("/authorize", async (req, res, next) => { let originalHost = req.get("host") if (referer) { + logger.info("Using Referer...") try { originalHost = new URL(referer).host } catch { @@ -129,6 +130,8 @@ router.get("/authorize", async (req, res, next) => { originalHost, ) + logger.info("Host: ", originalHost) + // state is attacker-influenced (comes from mailcow's query string), // so it must never be used directly as a cache key. // Generate our own random nonce instead @@ -230,7 +233,7 @@ router.get("/mailbox", async (req, res, next) => { return res.status(400).send("No pending mail session") } - const originalHost = await GetFromCache(`state:${mailData.state}`) + const originalHost = await GetFromCache(`state:${mailData.state}`)?.host const tokenRes = await GetFromCache(`code:${mailData.code}`) From 21294f3918c17c450d57db40e0d288b543684f5f Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:47:16 +0200 Subject: [PATCH 09/17] . --- src/routes/oauth-mail.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/oauth-mail.js b/src/routes/oauth-mail.js index 56545a8..88d9b4d 100644 --- a/src/routes/oauth-mail.js +++ b/src/routes/oauth-mail.js @@ -116,9 +116,9 @@ router.get("/authorize", async (req, res, next) => { let originalHost = req.get("host") if (referer) { - logger.info("Using Referer...") try { originalHost = new URL(referer).host + logger.info("Using Referer...") } catch { // malformed Referer, fall back to req.get("host") } From 78d3192a9660c87669880bffbb0b2770b1bd3dd6 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:47:38 +0200 Subject: [PATCH 10/17] . --- src/routes/oauth-mail.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/oauth-mail.js b/src/routes/oauth-mail.js index 88d9b4d..5b3fa04 100644 --- a/src/routes/oauth-mail.js +++ b/src/routes/oauth-mail.js @@ -130,7 +130,7 @@ router.get("/authorize", async (req, res, next) => { originalHost, ) - logger.info("Host: ", originalHost) + logger.info(`Host: ${originalHost}`) // state is attacker-influenced (comes from mailcow's query string), // so it must never be used directly as a cache key. From 12ee601816935512f293be025ce48eb354a1317e Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sun, 23 Aug 2026 15:02:07 +0200 Subject: [PATCH 11/17] changed ttl to 15mins instead of 60mins --- src/routes/oauth-mail.js | 4 ++++ src/utils/db.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/routes/oauth-mail.js b/src/routes/oauth-mail.js index 5b3fa04..5a5dffd 100644 --- a/src/routes/oauth-mail.js +++ b/src/routes/oauth-mail.js @@ -137,6 +137,8 @@ router.get("/authorize", async (req, res, next) => { // Generate our own random nonce instead const nonce = crypto.randomBytes(24).toString("hex") + logger.info("state: ", stateData.origState) + await WriteToCache(`state:${nonce}`, { host: originalHost, origState: req.query.state, @@ -212,6 +214,8 @@ router.get("/callback", async (req, res, next) => { await WriteToCache(`access:${tokenRes.access_token}`, idToken.sub) + logger.info("state: ", stateData.origState) + req.session.mail = { code: codeHandle, state: stateData.origState, diff --git a/src/utils/db.js b/src/utils/db.js index 570cc22..9e8c6d5 100644 --- a/src/utils/db.js +++ b/src/utils/db.js @@ -103,7 +103,7 @@ export async function GetFromCache(key) { } } -export async function WriteToCache(key, value, ttl = 3600) { +export async function WriteToCache(key, value, ttl = 900) { if (typeof value !== "string") { value = JSON.stringify(value) } From e001ef71f70c73e732d7935f2049261e03b4a6af Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sun, 23 Aug 2026 15:06:38 +0200 Subject: [PATCH 12/17] fix --- src/routes/oauth-mail.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/oauth-mail.js b/src/routes/oauth-mail.js index 5a5dffd..87e239c 100644 --- a/src/routes/oauth-mail.js +++ b/src/routes/oauth-mail.js @@ -137,7 +137,7 @@ router.get("/authorize", async (req, res, next) => { // Generate our own random nonce instead const nonce = crypto.randomBytes(24).toString("hex") - logger.info("state: ", stateData.origState) + logger.info("state: ", req.query.state) await WriteToCache(`state:${nonce}`, { host: originalHost, From 8f4533163b7386cd6bb2a3f0a69279d7c367fe91 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sun, 23 Aug 2026 15:10:01 +0200 Subject: [PATCH 13/17] . --- src/routes/oauth-mail.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/routes/oauth-mail.js b/src/routes/oauth-mail.js index 87e239c..757f3b2 100644 --- a/src/routes/oauth-mail.js +++ b/src/routes/oauth-mail.js @@ -150,12 +150,14 @@ router.get("/authorize", async (req, res, next) => { ) { const authorizeUrl = matchedUri.replace("callback", "authorize") + // replace state with our nonce key const forwardedQuery = new URLSearchParams(req.query) forwardedQuery.set("state", nonce) return res.redirect(`${authorizeUrl}?${forwardedQuery.toString()}`) } + // replace state with our nonce key const forwardedQuery = new URLSearchParams(req.query) forwardedQuery.set("state", nonce) @@ -170,6 +172,7 @@ router.get("/authorize", async (req, res, next) => { router.get("/callback", async (req, res, next) => { try { + // get our generated nonce key from the "state" const nonce = req.query.state if (!nonce) { @@ -214,7 +217,7 @@ router.get("/callback", async (req, res, next) => { await WriteToCache(`access:${tokenRes.access_token}`, idToken.sub) - logger.info("state: ", stateData.origState) + logger.info(`state: ${stateData.origState}`) req.session.mail = { code: codeHandle, From e790fec49794eed22741bcaeb9bac0c0738167a1 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sun, 23 Aug 2026 15:11:59 +0200 Subject: [PATCH 14/17] . --- src/routes/oauth-mail.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/oauth-mail.js b/src/routes/oauth-mail.js index 757f3b2..0b11403 100644 --- a/src/routes/oauth-mail.js +++ b/src/routes/oauth-mail.js @@ -137,7 +137,7 @@ router.get("/authorize", async (req, res, next) => { // Generate our own random nonce instead const nonce = crypto.randomBytes(24).toString("hex") - logger.info("state: ", req.query.state) + logger.info(`state: ${req.query.state}`) await WriteToCache(`state:${nonce}`, { host: originalHost, From b60d8c0a84e4a9a81eae395dfe0016a6f86ca1d5 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sun, 23 Aug 2026 15:32:43 +0200 Subject: [PATCH 15/17] fix nonce being sent to repeat /authorize instead of origState for domain redirects --- src/routes/oauth-mail.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/routes/oauth-mail.js b/src/routes/oauth-mail.js index 0b11403..95f48e4 100644 --- a/src/routes/oauth-mail.js +++ b/src/routes/oauth-mail.js @@ -144,15 +144,19 @@ router.get("/authorize", async (req, res, next) => { origState: req.query.state, }) - if ( - matchedUri && - tldts.parse(GetBaseUrl(req)).domain !== tldts.parse(matchedUri).domain - ) { - const authorizeUrl = matchedUri.replace("callback", "authorize") + const matchedDomain = tldts.parse(matchedUri).domain + + // check if matchedUri's domain is equal to the current request domain + // if not repeat /authorize under the correct domain + if (matchedUri && tldts.parse(GetBaseUrl(req)).domain !== matchedDomain) { + const authorizeUrl = GetBaseUrl(req, matchedDomain) // replace state with our nonce key const forwardedQuery = new URLSearchParams(req.query) - forwardedQuery.set("state", nonce) + + // use req.query.state instead of nonce since we are just repeating the /authorize flow, + // just this time on the correct domain + forwardedQuery.set("state", req.query.state) return res.redirect(`${authorizeUrl}?${forwardedQuery.toString()}`) } From 2115d41e0c84273b95768910c8a6f3762b11b4cd Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sun, 23 Aug 2026 15:39:38 +0200 Subject: [PATCH 16/17] fix --- src/routes/oauth-mail.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/routes/oauth-mail.js b/src/routes/oauth-mail.js index 95f48e4..59d65e4 100644 --- a/src/routes/oauth-mail.js +++ b/src/routes/oauth-mail.js @@ -149,7 +149,9 @@ router.get("/authorize", async (req, res, next) => { // check if matchedUri's domain is equal to the current request domain // if not repeat /authorize under the correct domain if (matchedUri && tldts.parse(GetBaseUrl(req)).domain !== matchedDomain) { - const authorizeUrl = GetBaseUrl(req, matchedDomain) + // HACK: this replaces callback with authorize so that instances that have their /oauth/mail paths potentially redirected by a proxy + // still continue on that redirected path instead of leading back to the default /oauth/mail/authorize + const authorizeUrl = matchedUri.replace("callback", "authorize") // replace state with our nonce key const forwardedQuery = new URLSearchParams(req.query) From 9287f4c8821e4ea022643a9bd55142651fba57a5 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sun, 23 Aug 2026 15:41:22 +0200 Subject: [PATCH 17/17] remove debugs --- src/routes/oauth-mail.js | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/routes/oauth-mail.js b/src/routes/oauth-mail.js index 59d65e4..5af6571 100644 --- a/src/routes/oauth-mail.js +++ b/src/routes/oauth-mail.js @@ -66,20 +66,14 @@ function GetBaseUrl(req, overwriteHost = null) { const prot = req.protocol const host = overwriteHost || req.get("host") - logger.info(`Protocol: ${prot}`) - return `${prot}://${host}` } function GetMatchingRedirectUri(req, redirectUris, host = null) { const baseUrl = GetBaseUrl(req, host) - logger.info(`Base URL: ${baseUrl}`) - const rootDomain = tldts.parse(baseUrl).domain - logger.info(`Domain: ${rootDomain}`) - let candidates = redirectUris.filter( (uri) => tldts.parse(uri).domain === rootDomain, ) @@ -118,7 +112,6 @@ router.get("/authorize", async (req, res, next) => { if (referer) { try { originalHost = new URL(referer).host - logger.info("Using Referer...") } catch { // malformed Referer, fall back to req.get("host") } @@ -130,15 +123,11 @@ router.get("/authorize", async (req, res, next) => { originalHost, ) - logger.info(`Host: ${originalHost}`) - // state is attacker-influenced (comes from mailcow's query string), // so it must never be used directly as a cache key. // Generate our own random nonce instead const nonce = crypto.randomBytes(24).toString("hex") - logger.info(`state: ${req.query.state}`) - await WriteToCache(`state:${nonce}`, { host: originalHost, origState: req.query.state, @@ -223,8 +212,6 @@ router.get("/callback", async (req, res, next) => { await WriteToCache(`access:${tokenRes.access_token}`, idToken.sub) - logger.info(`state: ${stateData.origState}`) - req.session.mail = { code: codeHandle, state: stateData.origState, @@ -260,17 +247,12 @@ router.get("/mailbox", async (req, res, next) => { req.session.mail = {} - logger.info("Checking Redirect URIS...") - const redirectUri = GetMatchingRedirectUri( req, config.MAIL_CALLBACK_URIS, originalHost, ) - logger.info("URIS: ", config.MAIL_CALLBACK_URIS) - logger.info("Host: ", originalHost) - if (!redirectUri) { return res.status(400).send("No matching callback URI") }