Skip to content
75 changes: 74 additions & 1 deletion apps/server/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,81 @@
app.use(sessionParser);
app.use(favicon(path.join(assetsDir, "icon.ico")));

if (openID.isOpenIDEnabled())
if (openID.isOpenIDEnabled()) {
// Always log OAuth initialization for better debugging
log.info('OAuth: Initializing OAuth authentication middleware');

// Check for potential reverse proxy configuration issues
const baseUrl = config.MultiFactorAuthentication.oauthBaseUrl;
const trustProxy = app.get('trust proxy');

log.info(`OAuth: Configuration check - baseURL=${baseUrl}, trustProxy=${trustProxy}`);

// Log potential issue if OAuth is configured with HTTPS but trust proxy is not set
if (baseUrl.startsWith('https://') && !trustProxy) {
log.info('OAuth: baseURL uses HTTPS but trustedReverseProxy is not configured.');
log.info('OAuth: If you are behind a reverse proxy, this MAY cause authentication failures.');
log.info('OAuth: The OAuth library might generate HTTP redirect_uris instead of HTTPS.');
log.info('OAuth: If authentication fails with redirect_uri errors, try setting:');
log.info('OAuth: In config.ini: trustedReverseProxy=true');
log.info('OAuth: Or environment: TRILIUM_NETWORK_TRUSTEDREVERSEPROXY=true');
log.info('OAuth: Note: This is only needed if running behind a reverse proxy.');
}

// Test OAuth connectivity on startup for non-Google providers
const issuerUrl = config.MultiFactorAuthentication.oauthIssuerBaseUrl;
const isCustomProvider = issuerUrl &&
issuerUrl !== "" &&
issuerUrl !== "https://accounts.google.com";

if (isCustomProvider) {
// For non-Google providers, verify connectivity
openID.testOAuthConnectivity().then(result => {
if (result.success) {
log.info('OAuth: Provider connectivity verified successfully');
} else {
log.error(`OAuth: Provider connectivity check failed: ${result.error}`);
log.error('OAuth: Authentication may not work. Please verify:');
log.error(' 1. The OAuth provider URL is correct');
log.error(' 2. Network connectivity between Trilium and the OAuth provider');
log.error(' 3. Any firewall or proxy settings');
}
}).catch(err => {
log.error(`OAuth: Connectivity test error: ${err.message || err}`);
});
}

// Register OAuth middleware
app.use(auth(openID.generateOAuthConfig()));

// Add OAuth error logging middleware AFTER auth middleware
app.use(openID.oauthErrorLogger);

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.

Copilot Autofix

AI about 2 months ago

To fix the issue, we should add rate-limiting middleware to the application in order to prevent abuse of the authentication/authorization chain, including the openID.oauthErrorLogger middleware. The best practice is to use the popular express-rate-limit package. You should import this package near the top of your file, configure a sensible rate limit (for example, 100 requests per 15 minutes), and use app.use(limiter) immediately before the authentication middlewares, so that all upstream authentication attempts (and error loggers) will be protected. This approach ensures that expensive operations in the authentication chain are not abused by high-frequency requests. All changes are to be done in the shown region of apps/server/src/app.ts, including the import statement, definition/configuration of the limiter, and the application of the middleware.


Suggested changeset 2
apps/server/src/app.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts
--- a/apps/server/src/app.ts
+++ b/apps/server/src/app.ts
@@ -1,3 +1,4 @@
+import RateLimit from "express-rate-limit";
 import express from "express";
 import path from "path";
 import favicon from "serve-favicon";
@@ -144,6 +145,13 @@
             });
         }
         
+        // Apply rate limiter before all authentication and error logging middleware
+        const limiter = RateLimit({
+            windowMs: 15 * 60 * 1000, // 15 minutes
+            max: 100, // limit each IP to 100 requests per windowMs
+        });
+        app.use(limiter);
+
         // Register OAuth middleware
         app.use(auth(openID.generateOAuthConfig()));
         
EOF
@@ -1,3 +1,4 @@
import RateLimit from "express-rate-limit";
import express from "express";
import path from "path";
import favicon from "serve-favicon";
@@ -144,6 +145,13 @@
});
}

// Apply rate limiter before all authentication and error logging middleware
const limiter = RateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
});
app.use(limiter);

// Register OAuth middleware
app.use(auth(openID.generateOAuthConfig()));

apps/server/package.json
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/apps/server/package.json b/apps/server/package.json
--- a/apps/server/package.json
+++ b/apps/server/package.json
@@ -4,7 +4,8 @@
   "description": "The server-side component of TriliumNext, which exposes the client via the web, allows for sync and provides a REST API for both internal and external use.",
   "private": true,
   "dependencies": {
-    "better-sqlite3": "12.2.0"
+    "better-sqlite3": "12.2.0",
+    "express-rate-limit": "^8.0.1"
   },
   "devDependencies": {
     "@electron/remote": "2.1.3",
EOF
@@ -4,7 +4,8 @@
"description": "The server-side component of TriliumNext, which exposes the client via the web, allows for sync and provides a REST API for both internal and external use.",
"private": true,
"dependencies": {
"better-sqlite3": "12.2.0"
"better-sqlite3": "12.2.0",
"express-rate-limit": "^8.0.1"
},
"devDependencies": {
"@electron/remote": "2.1.3",
This fix introduces these dependencies
Package Version Security advisories
express-rate-limit (npm) 8.0.1 None
Copilot is powered by AI and may make mistakes. Always verify output.

// Add diagnostic middleware for authentication initiation
app.use('/authenticate', (req, res, next) => {
log.info(`OAuth authenticate diagnostic: protocol=${req.protocol}, secure=${req.secure}, host=${req.get('host')}`);
log.info(`OAuth authenticate: baseURL from req = ${req.protocol}://${req.get('host')}`);
log.info(`OAuth authenticate: headers - x-forwarded-proto=${req.headers['x-forwarded-proto']}, x-forwarded-host=${req.headers['x-forwarded-host']}`);
// The actual redirect_uri will be logged by express-openid-connect
next();
});

// Add diagnostic middleware to log what protocol Express thinks it's using for callbacks
app.use('/callback', (req, res, next) => {
log.info(`OAuth callback diagnostic: protocol=${req.protocol}, secure=${req.secure}, originalUrl=${req.originalUrl}`);
log.info(`OAuth callback headers: x-forwarded-proto=${req.headers['x-forwarded-proto']}, x-forwarded-for=${req.headers['x-forwarded-for']}, host=${req.headers['host']}`);

// Log if there's a mismatch between expected and actual protocol
const expectedProtocol = baseUrl.startsWith('https://') ? 'https' : 'http';
if (req.protocol !== expectedProtocol) {
log.error(`OAuth callback: PROTOCOL MISMATCH DETECTED!`);
log.error(`OAuth callback: Expected ${expectedProtocol} (from baseURL) but got ${req.protocol}`);
log.error(`OAuth callback: This indicates trustedReverseProxy may need to be set.`);
}

next();
});
}

await assets.register(app);
routes.register(app);
Expand Down
20 changes: 11 additions & 9 deletions apps/server/src/services/encryption/open_id_encryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,34 +61,36 @@ function verifyOpenIDSubjectIdentifier(subjectIdentifier: string) {
throw new OpenIdError("Database not initialized!");
}

if (isUserSaved()) {
return false;
if (!isUserSaved()) {
// No user exists yet - this is a new user registration, which is allowed
return true;
}

const salt = sql.getValue("SELECT salt FROM user_data;");
const salt = sql.getValue<string>("SELECT salt FROM user_data;");
if (salt == undefined) {
console.log("Salt undefined");
console.log("OpenID verification: Salt undefined - database may be corrupted");
return undefined;
}

const givenHash = myScryptService
.getSubjectIdentifierVerificationHash(subjectIdentifier)
.getSubjectIdentifierVerificationHash(subjectIdentifier, salt)
?.toString("base64");
if (givenHash === undefined) {
console.log("Sub id hash undefined!");
console.log("OpenID verification: Failed to generate hash for subject identifier");
return undefined;
}

const savedHash = sql.getValue(
"SELECT userIDVerificationHash FROM user_data"
);
if (savedHash === undefined) {
console.log("verification hash undefined");
console.log("OpenID verification: No saved verification hash found");
return undefined;
}

console.log("Matches: " + givenHash === savedHash);
return givenHash === savedHash;
const matches = givenHash === savedHash;
console.log(`OpenID verification: Subject identifier match = ${matches}`);
return matches;
}

function setDataKey(
Expand Down
Loading