From dbdac430ef8fe13942ba243f098abede36f7805b Mon Sep 17 00:00:00 2001 From: Bradley Shellnut Date: Thu, 1 Aug 2024 09:26:42 -0700 Subject: [PATCH] Adding verify login with TOTP credentials coming from the credentials table. --- .../api/controllers/login.controller.ts | 2 +- .../server/api/controllers/user.controller.ts | 42 ++++++++++++------- .../database/tables/credentials.table.ts | 2 +- .../repositories/credentials.repository.ts | 28 ++++++++++++- .../api/services/loginrequest.service.ts | 37 +++++++++++----- src/lib/server/api/services/users.service.ts | 17 ++++++++ 6 files changed, 99 insertions(+), 29 deletions(-) create mode 100644 src/lib/server/api/services/users.service.ts diff --git a/src/lib/server/api/controllers/login.controller.ts b/src/lib/server/api/controllers/login.controller.ts index fab874c..15e4b37 100644 --- a/src/lib/server/api/controllers/login.controller.ts +++ b/src/lib/server/api/controllers/login.controller.ts @@ -19,7 +19,7 @@ export class LoginController implements Controller { return this.controller .post('/', zValidator('json', signInEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => { 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' }); }) } diff --git a/src/lib/server/api/controllers/user.controller.ts b/src/lib/server/api/controllers/user.controller.ts index 372e240..dc23cde 100644 --- a/src/lib/server/api/controllers/user.controller.ts +++ b/src/lib/server/api/controllers/user.controller.ts @@ -1,22 +1,34 @@ import { Hono } from 'hono'; import { zValidator } from '@hono/zod-validator'; +import { inject, injectable } from 'tsyringe'; import { requireAuth } from "../middleware/auth.middleware"; import { registerEmailPasswordDto } from '$lib/dtos/register-emailpassword.dto'; import { limiter } from '../middleware/rate-limiter.middleware'; +import type { HonoTypes } from '../types'; +import type { Controller } from '../interfaces/controller.interface'; -const app = new Hono() - .get('/me', requireAuth, async (c) => { - const user = c.var.user; - return c.json({ user }); - }) - .get('/user', requireAuth, async (c) => { - const user = c.var.user; - return c.json({ user }); - }) - .post('/login/request', zValidator('json', registerEmailPasswordDto), limiter({ limit: 10, minutes: 60 }), async (c) => { - const { email } = c.req.valid('json'); - await this.loginRequestsService.create({ email }); - return c.json({ message: 'Verification email sent' }); - }); +@injectable() +export class UserController implements Controller { + controller = new Hono(); -export default app; + constructor( + @inject('LoginRequestsService') private readonly loginRequestsService: LoginRequestsService + ) { } + + routes() { + return this.controller + .get('/me', requireAuth, async (c) => { + const user = c.var.user; + return c.json({ user }); + }) + .get('/user', requireAuth, async (c) => { + const user = c.var.user; + return c.json({ user }); + }) + .post('/login/request', zValidator('json', registerEmailPasswordDto), limiter({ limit: 10, minutes: 60 }), async (c) => { + const { email } = c.req.valid('json'); + await this.loginRequestsService.create({ email }); + return c.json({ message: 'Verification email sent' }); + }); + } +} diff --git a/src/lib/server/api/infrastructure/database/tables/credentials.table.ts b/src/lib/server/api/infrastructure/database/tables/credentials.table.ts index 1683b6a..2025619 100644 --- a/src/lib/server/api/infrastructure/database/tables/credentials.table.ts +++ b/src/lib/server/api/infrastructure/database/tables/credentials.table.ts @@ -2,7 +2,7 @@ import { pgTable, text, uuid } from "drizzle-orm/pg-core"; import { timestamps } from '../utils'; import { usersTable } from "./users.table"; -enum CredentialsType { +export enum CredentialsType { SECRET = 'secret', PASSWORD = 'password', TOTP = 'totp', diff --git a/src/lib/server/api/repositories/credentials.repository.ts b/src/lib/server/api/repositories/credentials.repository.ts index 7bc32d3..3c9ee07 100644 --- a/src/lib/server/api/repositories/credentials.repository.ts +++ b/src/lib/server/api/repositories/credentials.repository.ts @@ -1,5 +1,5 @@ -import { eq, type InferInsertModel } from "drizzle-orm"; -import { credentialsTable } from "../infrastructure/database/tables/credentials.table"; +import { and, eq, type InferInsertModel } from "drizzle-orm"; +import { credentialsTable, CredentialsType } from "../infrastructure/database/tables/credentials.table"; import { db } from "../infrastructure/database"; import { takeFirstOrThrow } from "../infrastructure/database/utils"; @@ -8,6 +8,30 @@ export type UpdateCredentials = Partial; 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) { return db.query.credentialsTable.findFirst({ where: eq(credentialsTable.id, id) diff --git a/src/lib/server/api/services/loginrequest.service.ts b/src/lib/server/api/services/loginrequest.service.ts index 5b9c1eb..852b422 100644 --- a/src/lib/server/api/services/loginrequest.service.ts +++ b/src/lib/server/api/services/loginrequest.service.ts @@ -7,7 +7,8 @@ import { LuciaProvider } from '../providers/lucia.provider'; import { UsersRepository } from '../repositories/users.repository'; import type { SignInEmailDto } from '../../../dtos/signin-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() export class LoginRequestsService { @@ -17,13 +18,9 @@ export class LoginRequestsService { @inject(TokensService) private readonly tokensService: TokensService, @inject(MailerService) private readonly mailerService: MailerService, @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) { // generate a token, expiry date, and hash const { token, expiry, hashedToken } = await this.tokensService.generateTokenWithExpiryAndHash(15, 'm'); @@ -36,16 +33,36 @@ export class LoginRequestsService { }); } - async verify(data: SignInEmailDto) { - let existingUser = await this.usersRepository.findOneByUsername(data.username); + async verify(data: SignInEmailDto, req: HonoRequest) { + 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) { throw BadRequest('User not found'); } - + const credential = await this.credentialsRepository.findPasswordCredentialsByUserId(existingUser.id); - return this.lucia.createSession(existingUser.id, {}); + if (!credential) { + throw BadRequest('Invalid credentials'); + } + + 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 diff --git a/src/lib/server/api/services/users.service.ts b/src/lib/server/api/services/users.service.ts new file mode 100644 index 0000000..8040442 --- /dev/null +++ b/src/lib/server/api/services/users.service.ts @@ -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); + } +} \ No newline at end of file