From f828ca4491259967967a7607887a67a3109279bf Mon Sep 17 00:00:00 2001 From: Arno Kaimbacher Date: Fri, 16 Feb 2024 15:32:47 +0100 Subject: [PATCH] - added 2fa authentication during login. see resources/js/Pages/Auth/login.vue - added validate() method inside app/Srvices/TwoFactorProvider.ts - added twoFactorChallenge() method inside app/Controllers/Http/Auth/AuthController.ts for logging in via 2fa-code --- app/Controllers/Http/Auth/AuthController.ts | 64 ++++-- app/Services/TwoFactorAuthProvider.ts | 10 +- resources/js/Dataset.ts | 6 + resources/js/Pages/Admin/User/Index.vue | 2 +- resources/js/Pages/Auth/Login.vue | 229 ++++++++++++++------ start/inertia.ts | 4 + start/routes.ts | 2 + 7 files changed, 233 insertions(+), 84 deletions(-) diff --git a/app/Controllers/Http/Auth/AuthController.ts b/app/Controllers/Http/Auth/AuthController.ts index a768169..207684f 100644 --- a/app/Controllers/Http/Auth/AuthController.ts +++ b/app/Controllers/Http/Auth/AuthController.ts @@ -1,9 +1,13 @@ import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; -// import User from 'App/Models/User'; +import User from 'App/Models/User'; // import Hash from '@ioc:Adonis/Core/Hash'; // import InvalidCredentialException from 'App/Exceptions/InvalidCredentialException'; import AuthValidator from 'App/Validators/AuthValidator'; +import TwoFactorAuthProvider from 'App/Services/TwoFactorAuthProvider'; +// import { LoginState } from 'Contracts/enums'; +// import { StatusCodes } from 'http-status-codes'; + export default class AuthController { // login function public async login({ request, response, auth, session }: HttpContextContract) { @@ -12,24 +16,30 @@ export default class AuthController { // }); await request.validate(AuthValidator); - const plainPassword = await request.input('password'); - const email = await request.input('email'); + // const plainPassword = await request.input('password'); + // const email = await request.input('email'); // grab uid and password values off request body - // const { email, password } = request.only(['email', 'password']) + const { email, password } = request.only(['email', 'password']); try { - // attempt to verify credential and login user - await auth.use('web').attempt(email, plainPassword); + // // attempt to verify credential and login user + // await auth.use('web').attempt(email, plainPassword); - // const user = await auth.use('web').verifyCredentials(email, plainPassword); - // if (user.isTwoFactorEnabled) { - // // session.put("login.id", user.id); - // // return view.render("pages/two-factor-challenge"); - // } + const user = await auth.use('web').verifyCredentials(email, password); + if (user.isTwoFactorEnabled) { + // session.put("login.id", user.id); + // return view.render("pages/two-factor-challenge"); - // session.forget('login.id'); - // session.regenerate(); - // await auth.login(user); + session.flash('user_id', user.id); + return response.redirect().back(); + + // let state = LoginState.STATE_VALIDATED; + // return response.status(StatusCodes.OK).json({ + // state: state, + // new_user_id: user.id, + // }); + } + await auth.login(user); } catch (error) { // if login fails, return vague form message and redirect back session.flash('message', 'Your username, email, or password is incorrect'); @@ -40,6 +50,32 @@ export default class AuthController { response.redirect('/apps/dashboard'); } + public async twoFactorChallenge({ request, session, auth, response }) { + const { code, recoveryCode, login_id } = request.only(['code', 'recoveryCode', 'login_id']); + // const user = await User.query().where('id', session.get('login.id')).firstOrFail(); + const user = await User.query().where('id', login_id).firstOrFail(); + + if (code) { + const isValid = await TwoFactorAuthProvider.validate(user, code); + if (isValid) { + // login user and redirect to dashboard + await auth.login(user); + response.redirect('/apps/dashboard'); + } else { + session.flash('message', 'Your tow factor code is incorrect'); + return response.redirect().back(); + } + } else if (recoveryCode) { + const codes = user?.twoFactorRecoveryCodes ?? []; + if (codes.includes(recoveryCode)) { + user.twoFactorRecoveryCodes = codes.filter((c) => c !== recoveryCode); + await user.save(); + await auth.login(user); + response.redirect('/apps/dashboard'); + } + } + } + // logout function public async logout({ auth, response }: HttpContextContract) { // await auth.logout(); diff --git a/app/Services/TwoFactorAuthProvider.ts b/app/Services/TwoFactorAuthProvider.ts index 991d65f..7a2c4cd 100644 --- a/app/Services/TwoFactorAuthProvider.ts +++ b/app/Services/TwoFactorAuthProvider.ts @@ -105,7 +105,7 @@ class TwoFactorAuthProvider { public async enable(user: User, token: string): Promise { const isValid = verifyToken(user.twoFactorSecret as string, token, 1); if (!isValid) { - return false; + return false; } user.state = TotpState.STATE_ENABLED; if (await user.save()) { @@ -113,6 +113,14 @@ class TwoFactorAuthProvider { } return false; } + + public async validate(user: User, token: string): Promise { + const isValid = verifyToken(user.twoFactorSecret as string, token, 1); + if (isValid) { + return true; + } + return false; + } } export default new TwoFactorAuthProvider(); diff --git a/resources/js/Dataset.ts b/resources/js/Dataset.ts index 5dc6799..37b0492 100644 --- a/resources/js/Dataset.ts +++ b/resources/js/Dataset.ts @@ -186,3 +186,9 @@ export interface Identifier { updated_at: string; //'2023-03-09T09:48:28.000Z' value: string; //'10.24341/tethys.209' } + +// export enum LoginState { +// STATE_DISABLED = 0, +// STATE_VALIDATED = 1, +// STATE_2FA_AUTHENTICATED = 1, +// } \ No newline at end of file diff --git a/resources/js/Pages/Admin/User/Index.vue b/resources/js/Pages/Admin/User/Index.vue index c9c1736..d2d54cc 100644 --- a/resources/js/Pages/Admin/User/Index.vue +++ b/resources/js/Pages/Admin/User/Index.vue @@ -2,7 +2,7 @@ // import { Head, Link, useForm, usePage } from '@inertiajs/inertia-vue3'; import { Head, Link, useForm, usePage } from '@inertiajs/vue3'; import { ComputedRef } from 'vue'; -import { mdiAccountKey, mdiPlus, mdiSquareEditOutline, mdiTrashCan, mdiAlertBoxOutline } from '@mdi/js'; +import { mdiAccountKey, mdiPlus, mdiSquareEditOutline, mdiAlertBoxOutline } from '@mdi/js'; import { computed } from 'vue'; import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue'; import SectionMain from '@/Components/SectionMain.vue'; diff --git a/resources/js/Pages/Auth/Login.vue b/resources/js/Pages/Auth/Login.vue index fcc8594..7fa6f19 100644 --- a/resources/js/Pages/Auth/Login.vue +++ b/resources/js/Pages/Auth/Login.vue @@ -1,37 +1,7 @@ - diff --git a/start/inertia.ts b/start/inertia.ts index 4a631d7..566f39a 100644 --- a/start/inertia.ts +++ b/start/inertia.ts @@ -16,6 +16,10 @@ Inertia.share({ return ctx.session.flashMessages.get('errors'); }, + user_id: (ctx) => { + return ctx.session.flashMessages.get('user_id'); + }, + flash: (ctx) => { return { message: ctx.session.flashMessages.get('message'), diff --git a/start/routes.ts b/start/routes.ts index 6b2b691..e4ef58b 100644 --- a/start/routes.ts +++ b/start/routes.ts @@ -98,6 +98,8 @@ Route.get('/app/login', async ({ inertia }) => { // Route.post("/login", "Users/AuthController.login"); Route.post('/app/login', 'Auth/AuthController.login').as('login.store'); +Route.post('/app/twoFactorChallenge', 'Auth/AuthController.twoFactorChallenge').as('login.twoFactorChallenge'); + // Route.on("/signup").render("signup"); // Route.post("/signup", "AuthController.signup"); Route.post('/signout', 'Auth/AuthController.logout').as('logout');