sso-core/src/routes/login/+page.server.ts

147 lines
3.5 KiB
TypeScript
Raw Normal View History

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;
if (await limiter.isLimited(event)) throw error(429);
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
// TODO: Audit log failed attempts
const body = await request.formData();
const { email, password, challenge, otpCode } = Changesets.take<LoginParams>(
['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-05-21 19:16:15 +03:00
if (await rainbowTableLimiter.isLimited(event)) 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))) {
if (await rainbowTableLimiter.isLimited(event)) 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 });
return redirect(303, redirectUrl);
}
} as Actions;
2024-05-18 19:07:56 +03:00
export const load = async ({ locals, url }) => {
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);
return {
activated: true
};
}
return {
activated: null
};
};