@ynotzort/sveltekit-password-protect
Version:
Simple utility to add a layer of protection to your websites, very useful for agencies and freelancers
130 lines (129 loc) • 6.16 kB
JavaScript
import { json } from '@sveltejs/kit';
import { InMemoryCache } from './InMemoryCache.js';
import { PAGE_TEMPLATE } from './page.template.js';
import { CSRFTokenStore, SessionStore, RateLimiter, SessionJWTStore, FakeCSRFTokenStore } from './Stores.js';
import { building } from '$app/environment';
const DEFAULT_OPTIONS = {
enableCSRF: false,
sessionTTL: 60 * 60 * 1000 * 12, // 12 hours,
csrfTokenTTL: 60 * 1000 * 5, // 5 minutes
cookieName: 'password-protect',
title: 'Password Protected',
description: 'Please enter the password to access the website',
theme: 'light',
redirect: '/',
path: '/password-protect',
cacheAdapter: new InMemoryCache(60 * 1000 * 5), // 5 minutes
maxFailedCalls: 5,
rateLimitTTL: 1000 * 60 * 1, // 1 minutes
jwtSecret: '',
getPasswordFormPage: _getPageTemplate
};
export const createPasswordProtectHandler = (options) => {
if (!options.password) {
throw new Error('password is required');
}
const defaultOptions = {
...DEFAULT_OPTIONS,
...options
};
const sessionStore = defaultOptions.jwtSecret
? new SessionJWTStore(defaultOptions)
: new SessionStore(defaultOptions.cacheAdapter, defaultOptions);
const csrfTokenStore = defaultOptions.enableCSRF
? new CSRFTokenStore(defaultOptions.cacheAdapter, defaultOptions)
: new FakeCSRFTokenStore();
const rateLimiter = new RateLimiter(defaultOptions);
return async ({ event, resolve }) => {
if (building)
return resolve(event);
const { url } = event;
const { pathname } = url;
const method = event.request.method;
if (pathname.startsWith(defaultOptions.path) && method === 'POST') {
const formData = await event.request.formData();
const password = formData.get('password');
const csrfToken = formData.get('csrf_token');
const redirect = formData.get('redirect');
const ip = event.getClientAddress();
const rateLimitCookie = event.cookies.get(`${defaultOptions.cookieName}-rate-limit`);
const ratelimitKey = `${ip || 'none'}-${rateLimitCookie || 'none'}`;
const reached = rateLimiter.checkRateLimit(ratelimitKey);
if (reached) {
return new Response(_getPageTemplate({
redirect: redirect || defaultOptions.redirect,
csrfToken: await csrfTokenStore.generateCSRFToken(),
error: `Too many attempts, max ${defaultOptions.maxFailedCalls} per ${defaultOptions.rateLimitTTL / 1000 / 60} minutes`,
defaultOptions
}), { status: 401, headers: { 'Content-Type': 'text/html' } });
}
if (defaultOptions.enableCSRF &&
(!csrfToken || !(await csrfTokenStore.getCSRFToken(csrfToken)))) {
return new Response(_getPageTemplate({
redirect: redirect || defaultOptions.redirect,
csrfToken: await csrfTokenStore.generateCSRFToken(),
error: 'Invalid CSRF token',
defaultOptions
}), { status: 401, headers: { 'Content-Type': 'text/html' } });
}
if (!password || password !== defaultOptions.password) {
await csrfTokenStore.deleteCSRFToken(csrfToken);
return new Response(_getPageTemplate({
redirect: redirect || defaultOptions.redirect,
csrfToken: await csrfTokenStore.generateCSRFToken(),
error: 'Invalid password',
defaultOptions
}), { status: 401, headers: { 'Content-Type': 'text/html' } });
}
const csrfTokenTime = await csrfTokenStore.getCSRFToken(csrfToken);
if (!csrfTokenTime) {
return new Response(_getPageTemplate({
redirect: redirect || '/',
csrfToken: await csrfTokenStore.generateCSRFToken(),
error: 'CSRF token expired',
defaultOptions
}), { status: 401, headers: { 'Content-Type': 'text/html' } });
}
// delete the csrf token
await csrfTokenStore.deleteCSRFToken(csrfToken);
return new Response(null, {
status: 302,
headers: {
Location: redirect || defaultOptions.redirect,
'Set-Cookie': sessionStore.getCookieHeaderValue(await sessionStore.generateSessionToken())
}
});
}
else if (pathname.startsWith(defaultOptions.path) && method === 'DELETE') {
return json({ success: true }, {
headers: {
'Set-Cookie': `${defaultOptions.cookieName}=; Path=/; Secure; HttpOnly; SameSite=Strict; Max-Age=0`
}
});
}
const cookie = event.cookies.get(defaultOptions.cookieName);
const sessionToken = cookie ? await sessionStore.getSession(cookie) : null;
if (!sessionToken) {
const htmlPage = _getPageTemplate({
redirect: url.pathname + new URL(event.request.url).search,
csrfToken: await csrfTokenStore.generateCSRFToken(),
defaultOptions
});
return new Response(htmlPage, {
headers: { 'Content-Type': 'text/html' }
});
}
else {
return resolve(event);
}
};
};
function _getPageTemplate({ title, description, redirect, csrfToken, theme, path, error, defaultOptions }) {
return PAGE_TEMPLATE.replaceAll('%title%', title || defaultOptions.title)
.replaceAll('%description%', description || defaultOptions.description)
.replaceAll('%redirect%', redirect || defaultOptions.redirect)
.replaceAll('%csrf_token%', csrfToken || '')
.replaceAll('%theme%', theme || defaultOptions.theme)
.replaceAll('%path%', path || defaultOptions.path)
.replaceAll('%error%', error || '');
}