From 7704c1aa3050ae72f08928c2d07249ee6a9f0829 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Sat, 5 Jul 2025 18:27:08 +0100 Subject: [PATCH 1/6] feat: add targetOrigin parameter to outputHTML function Add support for specifying a target origin for postMessage calls to improve security by restricting which origins can receive the authentication messages. --- src/index.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/index.js b/src/index.js index 70b4dab..0f5ff48 100644 --- a/src/index.js +++ b/src/index.js @@ -18,9 +18,10 @@ const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); * @param {string} [args.error] - Error message when an OAuth token is not available. * @param {string} [args.errorCode] - Error code to be used to localize the error message in * Sveltia CMS. + * @param {string} [args.targetOrigin] - The origin to send the postMessage to. * @returns {Response} Response with HTML. */ -const outputHTML = ({ provider = 'unknown', token, error, errorCode }) => { +const outputHTML = ({ provider = 'unknown', token, error, errorCode, targetOrigin }) => { const state = error ? 'error' : 'success'; const content = error ? { provider, error, errorCode } : { provider, token }; @@ -28,15 +29,16 @@ const outputHTML = ({ provider = 'unknown', token, error, errorCode }) => { ` `, From c859e212baffa4c10f19cc1e7684b201f6374e44 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Sat, 5 Jul 2025 18:27:50 +0100 Subject: [PATCH 2/6] feat: capture referring origin from request headers Extract the referring origin from the Referer header to determine which domain initiated the authentication request. This will be used to restrict postMessage communication to the originating domain. --- src/index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 0f5ff48..1e6c194 100644 --- a/src/index.js +++ b/src/index.js @@ -59,9 +59,13 @@ const outputHTML = ({ provider = 'unknown', token, error, errorCode, targetOrigi * @returns {Promise} HTTP response. */ const handleAuth = async (request, env) => { - const { url } = request; + const { url, headers } = request; const { origin, searchParams } = new URL(url); const { provider, site_id: domain } = Object.fromEntries(searchParams); + + // Get the referring domain from the Referer header + const referer = headers.get('Referer'); + const referringOrigin = referer ? new URL(referer).origin : null; if (!provider || !supportedProviders.includes(provider)) { return outputHTML({ From a92074d9e2f233b02d050b5646b2a6cb5ce50234 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Sat, 5 Jul 2025 18:28:22 +0100 Subject: [PATCH 3/6] feat: encode origin in OAuth state parameter Encode both CSRF token and referring origin in the OAuth state parameter as base64-encoded JSON. This allows passing the original domain through the OAuth flow for secure postMessage targeting. --- src/index.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index 1e6c194..878b52d 100644 --- a/src/index.js +++ b/src/index.js @@ -102,6 +102,13 @@ const handleAuth = async (request, env) => { // Generate a random string for CSRF protection const csrfToken = globalThis.crypto.randomUUID().replaceAll('-', ''); let authURL = ''; + + // Create state parameter that includes both CSRF token and original domain + const stateData = { + csrf: csrfToken, + origin: referringOrigin || origin + }; + const state = btoa(JSON.stringify(stateData)); // GitHub if (provider === 'github') { @@ -116,7 +123,7 @@ const handleAuth = async (request, env) => { const params = new URLSearchParams({ client_id: GITHUB_CLIENT_ID, scope: 'repo,user', - state: csrfToken, + state: state, }); authURL = `https://${GITHUB_HOSTNAME}/login/oauth/authorize?${params.toString()}`; @@ -137,7 +144,7 @@ const handleAuth = async (request, env) => { redirect_uri: `${origin}/callback`, response_type: 'code', scope: 'api', - state: csrfToken, + state: state, }); authURL = `https://${GITLAB_HOSTNAME}/oauth/authorize?${params.toString()}`; From 3e551ee022d6eb579eb03ab2280e32c9c3269d14 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Sat, 5 Jul 2025 18:29:15 +0100 Subject: [PATCH 4/6] feat: pass targetOrigin in handleAuth error responses Add targetOrigin parameter to error responses in the handleAuth function to ensure error messages are only sent to the originating domain. --- src/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/index.js b/src/index.js index 878b52d..e8dcc07 100644 --- a/src/index.js +++ b/src/index.js @@ -117,6 +117,7 @@ const handleAuth = async (request, env) => { provider, error: 'OAuth app client ID or secret is not configured.', errorCode: 'MISCONFIGURED_CLIENT', + targetOrigin: referringOrigin || origin, }); } @@ -136,6 +137,7 @@ const handleAuth = async (request, env) => { provider, error: 'OAuth app client ID or secret is not configured.', errorCode: 'MISCONFIGURED_CLIENT', + targetOrigin: referringOrigin || origin, }); } From aaece55ee5d1b65f0a1a26d50c21599f5ae8d200 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Sat, 5 Jul 2025 18:30:17 +0100 Subject: [PATCH 5/6] feat: decode state and pass targetOrigin in callback handler - Decode the OAuth state parameter to extract original domain - Add backward compatibility for old state format - Pass targetOrigin to all error and success responses in handleCallback - Ensure postMessage communication is restricted to the originating domain --- src/index.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index e8dcc07..e9ae626 100644 --- a/src/index.js +++ b/src/index.js @@ -195,11 +195,24 @@ const handleCallback = async (request, env) => { }); } - if (!csrfToken || state !== csrfToken) { + // Decode the state to get CSRF token and original domain + let stateData; + let originalOrigin = origin; + + try { + stateData = JSON.parse(atob(state)); + originalOrigin = stateData.origin || origin; + } catch { + // Fallback to old behavior if state is not base64 JSON + stateData = { csrf: state }; + } + + if (!csrfToken || stateData.csrf !== csrfToken) { return outputHTML({ provider, error: 'Potential CSRF attack detected. Authentication flow aborted.', errorCode: 'CSRF_DETECTED', + targetOrigin: originalOrigin, }); } @@ -222,6 +235,7 @@ const handleCallback = async (request, env) => { provider, error: 'OAuth app client ID or secret is not configured.', errorCode: 'MISCONFIGURED_CLIENT', + targetOrigin: originalOrigin, }); } @@ -239,6 +253,7 @@ const handleCallback = async (request, env) => { provider, error: 'OAuth app client ID or secret is not configured.', errorCode: 'MISCONFIGURED_CLIENT', + targetOrigin: originalOrigin, }); } @@ -274,6 +289,7 @@ const handleCallback = async (request, env) => { provider, error: 'Failed to request an access token. Please try again later.', errorCode: 'TOKEN_REQUEST_FAILED', + targetOrigin: originalOrigin, }); } @@ -284,10 +300,11 @@ const handleCallback = async (request, env) => { provider, error: 'Server responded with malformed data. Please try again later.', errorCode: 'MALFORMED_RESPONSE', + targetOrigin: originalOrigin, }); } - return outputHTML({ provider, token, error }); + return outputHTML({ provider, token, error, targetOrigin: originalOrigin }); }; export default { From 5d6a7d65980fb48a5b844a9e46fdc904dac540d5 Mon Sep 17 00:00:00 2001 From: "exadev[bot]" <182929975+exadev[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 07:14:25 +0000 Subject: [PATCH 6/6] Create/Update dependabot.yaml --- .github/dependabot.yaml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/dependabot.yaml diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..e6f2cbd --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,20 @@ +version: 2 +updates: + - package-ecosystem: 'npm' + directory: '/' + schedule: + interval: 'weekly' + groups: + production-dependencies: + dependency-type: 'production' + development-dependencies: + dependency-type: 'development' + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'weekly' + groups: + production-dependencies: + dependency-type: 'production' + development-dependencies: + dependency-type: 'development'