Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/dependabot.yaml
Original file line number Diff line number Diff line change
@@ -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'
48 changes: 40 additions & 8 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,27 @@ 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 };

return new Response(
`
<!doctype html><html><body><script>
(() => {
const targetOrigin = '${targetOrigin || '*'}';
window.addEventListener('message', ({ data, origin }) => {
if (data === 'authorizing:${provider}') {
window.opener?.postMessage(
'authorization:${provider}:${state}:${JSON.stringify(content)}',
origin
targetOrigin === '*' ? origin : targetOrigin
);
}
});
window.opener?.postMessage('authorizing:${provider}', '*');
window.opener?.postMessage('authorizing:${provider}', targetOrigin);
})();
</script></body></html>
`,
Expand All @@ -57,9 +59,13 @@ const outputHTML = ({ provider = 'unknown', token, error, errorCode }) => {
* @returns {Promise<Response>} 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({
Expand Down Expand Up @@ -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') {
Expand All @@ -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()}`;
Expand All @@ -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,
});
}

Expand All @@ -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()}`;
Expand Down Expand Up @@ -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,
});
}

Expand All @@ -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,
});
}

Expand All @@ -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,
});
}

Expand Down Expand Up @@ -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,
});
}

Expand All @@ -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 {
Expand Down