Adding verify login with TOTP credentials coming from the credentials table.

This commit is contained in:
Bradley Shellnut 2024-08-01 09:26:42 -07:00
parent bf55b04de6
commit dbdac430ef
6 changed files with 99 additions and 29 deletions

View file

@ -19,7 +19,7 @@ export class LoginController implements Controller {
return this.controller return this.controller
.post('/', zValidator('json', signInEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => { .post('/', zValidator('json', signInEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
const { username, password } = c.req.valid('json'); const { username, password } = c.req.valid('json');
await this.loginRequestsService.verify({ username, password }); await this.loginRequestsService.verify({ username, password }, c.req);
return c.json({ message: 'Verification email sent' }); return c.json({ message: 'Verification email sent' });
}) })
} }

View file

@ -1,10 +1,22 @@
import { Hono } from 'hono'; import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator'; import { zValidator } from '@hono/zod-validator';
import { inject, injectable } from 'tsyringe';
import { requireAuth } from "../middleware/auth.middleware"; import { requireAuth } from "../middleware/auth.middleware";
import { registerEmailPasswordDto } from '$lib/dtos/register-emailpassword.dto'; import { registerEmailPasswordDto } from '$lib/dtos/register-emailpassword.dto';
import { limiter } from '../middleware/rate-limiter.middleware'; import { limiter } from '../middleware/rate-limiter.middleware';
import type { HonoTypes } from '../types';
import type { Controller } from '../interfaces/controller.interface';
const app = new Hono() @injectable()
export class UserController implements Controller {
controller = new Hono<HonoTypes>();
constructor(
@inject('LoginRequestsService') private readonly loginRequestsService: LoginRequestsService
) { }
routes() {
return this.controller
.get('/me', requireAuth, async (c) => { .get('/me', requireAuth, async (c) => {
const user = c.var.user; const user = c.var.user;
return c.json({ user }); return c.json({ user });
@ -18,5 +30,5 @@ const app = new Hono()
await this.loginRequestsService.create({ email }); await this.loginRequestsService.create({ email });
return c.json({ message: 'Verification email sent' }); return c.json({ message: 'Verification email sent' });
}); });
}
export default app; }

View file

@ -2,7 +2,7 @@ import { pgTable, text, uuid } from "drizzle-orm/pg-core";
import { timestamps } from '../utils'; import { timestamps } from '../utils';
import { usersTable } from "./users.table"; import { usersTable } from "./users.table";
enum CredentialsType { export enum CredentialsType {
SECRET = 'secret', SECRET = 'secret',
PASSWORD = 'password', PASSWORD = 'password',
TOTP = 'totp', TOTP = 'totp',

View file

@ -1,5 +1,5 @@
import { eq, type InferInsertModel } from "drizzle-orm"; import { and, eq, type InferInsertModel } from "drizzle-orm";
import { credentialsTable } from "../infrastructure/database/tables/credentials.table"; import { credentialsTable, CredentialsType } from "../infrastructure/database/tables/credentials.table";
import { db } from "../infrastructure/database"; import { db } from "../infrastructure/database";
import { takeFirstOrThrow } from "../infrastructure/database/utils"; import { takeFirstOrThrow } from "../infrastructure/database/utils";
@ -8,6 +8,30 @@ export type UpdateCredentials = Partial<CreateCredentials>;
export class CredentialsRepository { export class CredentialsRepository {
async findOneByUserId(userId: string) {
return db.query.credentialsTable.findFirst({
where: eq(credentialsTable.user_id, userId)
});
}
async findPasswordCredentialsByUserId(userId: string) {
return db.query.credentialsTable.findFirst({
where: and(
eq(credentialsTable.user_id, userId),
eq(credentialsTable.type, CredentialsType.PASSWORD)
)
});
}
async findTOTPCredentialsByUserId(userId: string) {
return db.query.credentialsTable.findFirst({
where: and(
eq(credentialsTable.user_id, userId),
eq(credentialsTable.type, CredentialsType.TOTP)
)
});
}
async findOneById(id: string) { async findOneById(id: string) {
return db.query.credentialsTable.findFirst({ return db.query.credentialsTable.findFirst({
where: eq(credentialsTable.id, id) where: eq(credentialsTable.id, id)

View file

@ -7,7 +7,8 @@ import { LuciaProvider } from '../providers/lucia.provider';
import { UsersRepository } from '../repositories/users.repository'; import { UsersRepository } from '../repositories/users.repository';
import type { SignInEmailDto } from '../../../dtos/signin-email.dto'; import type { SignInEmailDto } from '../../../dtos/signin-email.dto';
import type { RegisterEmailDto } from '../../../dtos/register-email.dto'; import type { RegisterEmailDto } from '../../../dtos/register-email.dto';
import { LoginRequestsRepository } from '../repositories/login-requests.repository'; import { CredentialsRepository } from '../repositories/credentials.repository';
import type { HonoRequest } from 'hono';
@injectable() @injectable()
export class LoginRequestsService { export class LoginRequestsService {
@ -17,13 +18,9 @@ export class LoginRequestsService {
@inject(TokensService) private readonly tokensService: TokensService, @inject(TokensService) private readonly tokensService: TokensService,
@inject(MailerService) private readonly mailerService: MailerService, @inject(MailerService) private readonly mailerService: MailerService,
@inject(UsersRepository) private readonly usersRepository: UsersRepository, @inject(UsersRepository) private readonly usersRepository: UsersRepository,
@inject(LoginRequestsRepository) private readonly loginRequestsRepository: LoginRequestsRepository, @inject(CredentialsRepository) private readonly credentialsRepository: CredentialsRepository,
) { } ) { }
async validate(data: SignInEmailDto) {
}
async create(data: RegisterEmailDto) { async create(data: RegisterEmailDto) {
// generate a token, expiry date, and hash // generate a token, expiry date, and hash
const { token, expiry, hashedToken } = await this.tokensService.generateTokenWithExpiryAndHash(15, 'm'); const { token, expiry, hashedToken } = await this.tokensService.generateTokenWithExpiryAndHash(15, 'm');
@ -36,16 +33,36 @@ export class LoginRequestsService {
}); });
} }
async verify(data: SignInEmailDto) { async verify(data: SignInEmailDto, req: HonoRequest) {
let existingUser = await this.usersRepository.findOneByUsername(data.username); const requestIpAddress = req.header('x-real-ip');
const requestIpCountry = req.header('x-vercel-ip-country');
const existingUser = await this.usersRepository.findOneByUsername(data.username);
if (!existingUser) { if (!existingUser) {
throw BadRequest('User not found'); throw BadRequest('User not found');
} }
const credential = await this.credentialsRepository.findPasswordCredentialsByUserId(existingUser.id);
if (!credential) {
throw BadRequest('Invalid credentials');
}
return this.lucia.createSession(existingUser.id, {}); if (!await this.tokensService.verifyHashedToken(credential.hashedPassword, data.password)) {
throw BadRequest('Invalid credentials');
}
const totpCredentials = await this.credentialsRepository.findTOTPCredentialsByUserId(existingUser.id);
return this.lucia.createSession(existingUser.id, {
ip_country: requestIpCountry || 'unknown',
ip_address: requestIpAddress || 'unknown',
twoFactorAuthEnabled:
!!totpCredentials &&
totpCredentials?.secret !== null &&
totpCredentials?.secret !== '',
isTwoFactorAuthenticated: false,
});
} }
// Create a new user and send a welcome email - or other onboarding process // Create a new user and send a welcome email - or other onboarding process

View file

@ -0,0 +1,17 @@
import { inject, injectable } from 'tsyringe';
import type { UsersRepository } from '../repositories/users.repository';
@injectable()
export class UsersService {
constructor(
@inject('UsersRepository') private readonly usersRepository: UsersRepository
) { }
async findOneByUsername(username: string) {
return this.usersRepository.findOneByUsername(username);
}
async findOneById(id: string) {
return this.usersRepository.findOneById(id);
}
}