UNPKG

@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
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 || ''); }