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' diff --git a/src/index.js b/src/index.js index 70b4dab..e9ae626 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 }) => { ` `, @@ -57,9 +59,13 @@ const outputHTML = ({ provider = 'unknown', token, error, errorCode }) => { * @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({ @@ -96,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') { @@ -104,13 +117,14 @@ const handleAuth = async (request, env) => { provider, error: 'OAuth app client ID or secret is not configured.', errorCode: 'MISCONFIGURED_CLIENT', + targetOrigin: referringOrigin || origin, }); } 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()}`; @@ -123,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, }); } @@ -131,7 +146,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()}`; @@ -180,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, }); } @@ -207,6 +235,7 @@ const handleCallback = async (request, env) => { provider, error: 'OAuth app client ID or secret is not configured.', errorCode: 'MISCONFIGURED_CLIENT', + targetOrigin: originalOrigin, }); } @@ -224,6 +253,7 @@ const handleCallback = async (request, env) => { provider, error: 'OAuth app client ID or secret is not configured.', errorCode: 'MISCONFIGURED_CLIENT', + targetOrigin: originalOrigin, }); } @@ -259,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, }); } @@ -269,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 {