Files
shd.build/src/lib/server/auth/primary-authentication.ts
T

121 lines
3.8 KiB
TypeScript

import type { Database } from '@tursodatabase/database';
import type { Cookies } from '@sveltejs/kit';
import { hasActiveTotpFactor, isFullAdministrator } from './bootstrap';
import {
createAuthenticationTransaction,
type AuthenticationTransaction,
} from './authentication-transactions';
import { createSession, recordLogin, type SessionAuthLevel } from './session';
export type PrimaryAuthenticationProvider =
'password' | 'discord' | 'magic_link' | 'passkey';
export class PrimaryAuthenticationError extends Error {
readonly code: 'account_restricted' | 'account_missing';
constructor(code: 'account_restricted' | 'account_missing') {
super('This account is not permitted to sign in.');
this.name = 'PrimaryAuthenticationError';
this.code = code;
}
}
export type PrimaryAuthenticationResult =
| {
kind: 'session';
sessionId: string;
aal: SessionAuthLevel;
redirectTo: string;
}
| {
kind: 'mfa';
transaction: AuthenticationTransaction;
redirectTo: '/auth/two-factor';
};
export type CompletePrimaryAuthenticationInput = {
database: Database;
cookies: Cookies;
userId: string;
provider: PrimaryAuthenticationProvider;
request?: Request;
getClientAddress?: () => string;
credentialId?: string | null;
returnPath?: string;
};
/**
* Completes a verified password, email, Discord, or passkey identity. This is
* the only primary-auth path allowed to create a session. Accounts with an
* active TOTP factor receive an opaque MFA transaction instead.
*/
export async function completePrimaryAuthentication(
input: CompletePrimaryAuthenticationInput,
): Promise<PrimaryAuthenticationResult> {
const status = await (
await input.database.prepare(`
SELECT users.id,user_status.blocked_at,user_status.banned_until,
deleted_accounts.deleted_at
FROM users
LEFT JOIN user_status ON user_status.user_id=users.id
LEFT JOIN deleted_accounts ON deleted_accounts.user_id=users.id
WHERE users.id=?
`)
).get(input.userId);
const banned =
typeof status?.banned_until === 'string' &&
new Date(status.banned_until) > new Date();
if (!status) throw new PrimaryAuthenticationError('account_missing');
if (status.deleted_at || status.blocked_at || banned) {
await recordLogin(input.userId, input.provider, false, {
request: input.request,
getClientAddress: input.getClientAddress,
credentialId: input.credentialId,
failureReason: 'account_restricted',
});
throw new PrimaryAuthenticationError('account_restricted');
}
const activeFactor = await hasActiveTotpFactor(input.database, input.userId);
const fullAdministrator = await isFullAdministrator(
input.database,
input.userId,
);
if (!activeFactor) {
const sessionId = await createSession(input.cookies, input.userId, {
aal: 1,
});
const redirectTo = fullAdministrator
? '/account/security?required=admin'
: (input.returnPath ?? '/account');
await recordLogin(input.userId, input.provider, true, {
request: input.request,
getClientAddress: input.getClientAddress,
credentialId: input.credentialId,
sessionId,
aal: 1,
});
return { kind: 'session', sessionId, aal: 1, redirectTo };
}
const transaction = await createAuthenticationTransaction(
input.cookies,
input.database,
{
userId: input.userId,
kind: 'login',
provider: input.provider,
returnPath: input.returnPath ?? '/account',
},
);
await recordLogin(input.userId, input.provider, false, {
request: input.request,
getClientAddress: input.getClientAddress,
credentialId: input.credentialId,
failureReason: 'mfa_required',
riskFlags: ['primary_authenticated_mfa_required'],
aal: 1,
});
return { kind: 'mfa', transaction, redirectTo: '/auth/two-factor' };
}