2024-06-10 20:20:25 +03:00
|
|
|
import { Audit } from '$lib/server/audit/audit.js';
|
|
|
|
import { AuditAction } from '$lib/server/audit/types.js';
|
2024-05-17 17:31:14 +03:00
|
|
|
import { Challenge } from '$lib/server/challenge.js';
|
|
|
|
import { Changesets } from '$lib/server/changesets.js';
|
2024-05-16 23:17:06 +03:00
|
|
|
import { Users } from '$lib/server/users/index.js';
|
2024-05-17 17:31:14 +03:00
|
|
|
import { TimeOTP } from '$lib/server/users/totp.js';
|
2024-05-20 20:25:46 +03:00
|
|
|
import { error, fail, redirect, type Actions } from '@sveltejs/kit';
|
|
|
|
import { RateLimiter } from 'sveltekit-rate-limiter/server';
|
2024-05-16 23:17:06 +03:00
|
|
|
|
2024-05-17 17:31:14 +03:00
|
|
|
interface LoginParams {
|
|
|
|
email: string;
|
|
|
|
password: string;
|
|
|
|
challenge: string;
|
|
|
|
otpCode: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface LoginChallenge {
|
|
|
|
email: string;
|
|
|
|
}
|
|
|
|
|
2024-05-21 19:16:15 +03:00
|
|
|
const rainbowTableLimiter = new RateLimiter({
|
|
|
|
IP: [3, '10s']
|
|
|
|
});
|
|
|
|
|
2024-05-20 20:25:46 +03:00
|
|
|
const limiter = new RateLimiter({
|
2024-05-21 19:16:15 +03:00
|
|
|
IP: [6, 'm']
|
2024-05-20 20:25:46 +03:00
|
|
|
});
|
|
|
|
|
2024-05-16 23:17:06 +03:00
|
|
|
export const actions = {
|
2024-05-20 20:25:46 +03:00
|
|
|
default: async (event) => {
|
|
|
|
const { request, locals, url } = event;
|
2024-06-10 20:20:25 +03:00
|
|
|
if (await limiter.isLimited(event)) {
|
|
|
|
await Audit.insertRequest(AuditAction.THROTTLE, event, undefined, 'login');
|
|
|
|
throw error(429);
|
|
|
|
}
|
2024-05-20 20:25:46 +03:00
|
|
|
|
2024-05-17 17:31:14 +03:00
|
|
|
// Redirect
|
2024-05-16 23:17:06 +03:00
|
|
|
const redirectUrl = url.searchParams.has('redirectTo')
|
|
|
|
? (url.searchParams.get('redirectTo') as string)
|
|
|
|
: '/';
|
|
|
|
|
|
|
|
// Already logged in
|
|
|
|
if (locals.session.data?.user) {
|
|
|
|
return redirect(303, redirectUrl);
|
|
|
|
}
|
|
|
|
|
2024-05-17 17:31:14 +03:00
|
|
|
const body = await request.formData();
|
2025-02-18 20:54:09 +02:00
|
|
|
const { email, password, challenge, otpCode } = Changesets.only(
|
2024-05-17 17:31:14 +03:00
|
|
|
['email', 'password', 'challenge', 'otpCode'],
|
|
|
|
body
|
|
|
|
);
|
|
|
|
|
|
|
|
if (!email) {
|
|
|
|
return fail(400, { incorrect: true });
|
|
|
|
}
|
2024-05-16 23:17:06 +03:00
|
|
|
|
2024-05-17 17:31:14 +03:00
|
|
|
if (!password && (!challenge || !otpCode)) {
|
2024-05-16 23:17:06 +03:00
|
|
|
return fail(400, { incorrect: true });
|
|
|
|
}
|
|
|
|
|
|
|
|
// Find existing active user
|
|
|
|
const loginUser = await Users.getByLogin(email);
|
2024-05-17 17:31:14 +03:00
|
|
|
if (!loginUser) {
|
2024-06-10 20:20:25 +03:00
|
|
|
if (await rainbowTableLimiter.isLimited(event)) {
|
|
|
|
await Audit.insertRequest(
|
|
|
|
AuditAction.THROTTLE,
|
|
|
|
event,
|
|
|
|
undefined,
|
|
|
|
`rainbow table\nemail=${email}`
|
|
|
|
);
|
|
|
|
throw error(429);
|
|
|
|
}
|
|
|
|
|
2024-05-16 23:17:06 +03:00
|
|
|
return fail(400, { email, incorrect: true });
|
|
|
|
}
|
|
|
|
|
2024-05-17 17:31:14 +03:00
|
|
|
// Check OTP challenge
|
|
|
|
if (challenge && otpCode) {
|
|
|
|
const userOtp = await TimeOTP.getUserOtp(loginUser);
|
|
|
|
if (!userOtp) {
|
|
|
|
return fail(400, { email, incorrect: true });
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
const valid = await Challenge.verifyChallenge<LoginChallenge>(
|
|
|
|
challenge,
|
|
|
|
loginUser.uuid,
|
|
|
|
otpCode,
|
|
|
|
userOtp.token
|
|
|
|
);
|
|
|
|
|
|
|
|
if (email !== valid.email) {
|
|
|
|
return fail(400, { email, incorrect: true });
|
|
|
|
}
|
|
|
|
} catch {
|
|
|
|
return fail(400, { email, incorrect: true });
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Compare user password
|
2024-05-21 19:16:15 +03:00
|
|
|
if (!password || !(await Users.validatePassword(loginUser, password))) {
|
2024-06-10 20:20:25 +03:00
|
|
|
if (await rainbowTableLimiter.isLimited(event)) {
|
|
|
|
await Audit.insertRequest(
|
|
|
|
AuditAction.THROTTLE,
|
|
|
|
event,
|
|
|
|
loginUser,
|
|
|
|
`password attempts\nemail=${email}`
|
|
|
|
);
|
|
|
|
throw error(429);
|
|
|
|
}
|
|
|
|
|
2024-05-17 17:31:14 +03:00
|
|
|
return fail(400, { email, incorrect: true });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Issue two-factor challenge
|
|
|
|
if (!challenge) {
|
|
|
|
const isOtp = await TimeOTP.isUserOtp(loginUser);
|
|
|
|
if (isOtp) {
|
|
|
|
const issued = await Challenge.issueChallenge<LoginChallenge>({ email }, loginUser.uuid);
|
|
|
|
return { otpRequired: issued, email };
|
|
|
|
}
|
|
|
|
}
|
2024-05-16 23:17:06 +03:00
|
|
|
|
|
|
|
// Create session data for user
|
|
|
|
const sessionUser = await Users.toSession(loginUser);
|
|
|
|
await locals.session.set({ user: sessionUser });
|
|
|
|
|
2024-06-10 20:20:25 +03:00
|
|
|
await Audit.insertRequest(AuditAction.LOGIN, event, loginUser, sessionUser.sid);
|
|
|
|
|
2024-05-16 23:17:06 +03:00
|
|
|
return redirect(303, redirectUrl);
|
|
|
|
}
|
|
|
|
} as Actions;
|
2024-05-18 19:07:56 +03:00
|
|
|
|
2024-06-10 20:20:25 +03:00
|
|
|
export const load = async ({ locals, url, ...event }) => {
|
2024-06-08 20:22:39 +03:00
|
|
|
if (url.searchParams.has('redirectTo')) {
|
|
|
|
// Check that the redirect URL is a local path
|
|
|
|
if (!url.searchParams.get('redirectTo')?.startsWith('/')) {
|
|
|
|
return redirect(301, '/login');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Redirect if already logged in
|
|
|
|
if (locals.session.data?.user) {
|
|
|
|
return redirect(301, url.searchParams.get('redirectTo') || '/');
|
|
|
|
}
|
2024-05-18 19:07:56 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Activation routine
|
|
|
|
if (url.searchParams.has('activate')) {
|
|
|
|
const activationInfo = await Users.getActivationToken(
|
|
|
|
url.searchParams.get('activate') as string
|
|
|
|
);
|
|
|
|
|
|
|
|
if (!activationInfo?.user) {
|
|
|
|
return {
|
|
|
|
activated: false
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
await Users.activateUserBy(activationInfo.token, activationInfo.user);
|
2024-06-10 20:20:25 +03:00
|
|
|
await Audit.insertRequest(AuditAction.USER_UPDATE, event, activationInfo?.user, 'activate');
|
2024-05-18 19:07:56 +03:00
|
|
|
|
|
|
|
return {
|
|
|
|
activated: true
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
activated: null
|
|
|
|
};
|
|
|
|
};
|