- added 2fa authentication during login. see resources/js/Pages/Auth/login.vue
All checks were successful
CI Pipeline / japa-tests (push) Successful in 1m2s
All checks were successful
CI Pipeline / japa-tests (push) Successful in 1m2s
- added validate() method inside app/Srvices/TwoFactorProvider.ts - added twoFactorChallenge() method inside app/Controllers/Http/Auth/AuthController.ts for logging in via 2fa-code
This commit is contained in:
parent
b2dce0259a
commit
f828ca4491
|
@ -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();
|
||||
|
|
|
@ -105,7 +105,7 @@ class TwoFactorAuthProvider {
|
|||
public async enable(user: User, token: string): Promise<boolean> {
|
||||
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<boolean> {
|
||||
const isValid = verifyToken(user.twoFactorSecret as string, token, 1);
|
||||
if (isValid) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default new TwoFactorAuthProvider();
|
||||
|
|
|
@ -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,
|
||||
// }
|
|
@ -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';
|
||||
|
|
|
@ -1,37 +1,7 @@
|
|||
<!-- <template>
|
||||
<div>
|
||||
<Link href="/app">Home</Link>
|
||||
<br />
|
||||
<h1>Login</h1>
|
||||
<Head>
|
||||
<title>About - My app</title>
|
||||
<meta
|
||||
head-key="description"
|
||||
name="description"
|
||||
content="This is a page specific description"
|
||||
/>
|
||||
</Head>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AuthLayout from '@/Layouts/Auth.vue';
|
||||
import LayoutGuest from '@/Layouts/LayoutGuest.vue';
|
||||
export default {
|
||||
layout: AuthLayout,
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup>
|
||||
// import { Head, Link } from '@inertiajs/vue3';
|
||||
import { Head, Link } from '@inertiajs/inertia-vue3'
|
||||
import FormField from '@/Components/FormField.vue';
|
||||
import FormControl from '@/Components/FormControl.vue';
|
||||
</script> -->
|
||||
|
||||
<template>
|
||||
<LayoutGuest>
|
||||
|
||||
<Head title="Login" />
|
||||
|
||||
<!-- <SectionFullScreen v-slot="{ cardClass }" :bg="'greenBlue'"> -->
|
||||
|
@ -40,8 +10,9 @@ import FormControl from '@/Components/FormControl.vue';
|
|||
<img src="/logo.svg" class="h-10 mr-4" alt="Windster Logo" />
|
||||
<!-- <span class="self-center text-2xl font-bold whitespace-nowrap">Tethys</span> -->
|
||||
</a>
|
||||
<!-- Card -->
|
||||
<CardBox :class="cardClass" form @submit.prevent="submit">
|
||||
|
||||
<CardBox v-if="isTwoFactorAuthNeeded == false" :class="cardClass" form
|
||||
@submit.prevent="submit">
|
||||
<FormValidationErrors v-bind:errors="errors" />
|
||||
|
||||
<NotificationBarInCard v-if="status" color="info">
|
||||
|
@ -49,18 +20,13 @@ import FormControl from '@/Components/FormControl.vue';
|
|||
</NotificationBarInCard>
|
||||
|
||||
<FormField label="Email" label-for="email" help="Please enter your email">
|
||||
<FormControl v-model="form.email" :icon="mdiAccount" id="email" autocomplete="email" type="email" required />
|
||||
<FormControl v-model="form.email" :icon="mdiAccount" id="email" autocomplete="email" type="email"
|
||||
required />
|
||||
</FormField>
|
||||
|
||||
<FormField label="Password" label-for="password" help="Please enter your password">
|
||||
<FormControl
|
||||
v-model="form.password"
|
||||
:icon="mdiAsterisk"
|
||||
type="password"
|
||||
id="password"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
/>
|
||||
<FormControl v-model="form.password" :icon="mdiAsterisk" type="password" id="password"
|
||||
autocomplete="current-password" required />
|
||||
</FormField>
|
||||
|
||||
<FormCheckRadioGroup v-model="form.remember" name="remember" :options="{ remember: 'Remember' }" />
|
||||
|
@ -79,36 +45,62 @@ import FormControl from '@/Components/FormControl.vue';
|
|||
<BaseDivider />
|
||||
|
||||
<!-- buttons -->
|
||||
<BaseLevel>
|
||||
<BaseButtons>
|
||||
<!-- <BaseButton type="submit" color="info" label="Login" :class="{ 'opacity-25': form.processing }"
|
||||
v-bind:disabled="form.processing" /> -->
|
||||
<button
|
||||
type="submit"
|
||||
v-bind:disabled="form.processing"
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
class="text-white bg-cyan-600 hover:bg-cyan-700 focus:ring-4 focus:ring-cyan-200 font-medium rounded-lg text-base px-5 py-3 w-full sm:w-auto text-center"
|
||||
>
|
||||
Login to your account
|
||||
</button>
|
||||
<!-- <BaseButton v-if="canResetPassword" :route-name="route('password.request')" color="info" outline
|
||||
|
||||
<BaseButtons>
|
||||
<BaseButton type="submit" color="info" label=" Login to your account" :class="{ 'opacity-25': form.processing }"
|
||||
v-bind:disabled="form.processing" />
|
||||
<!-- <button type="submit" v-bind:disabled="form.processing" :class="{ 'opacity-25': form.processing }"
|
||||
class="text-white bg-cyan-600 hover:bg-cyan-700 focus:ring-4 focus:ring-cyan-200 font-medium rounded-lg text-base px-5 py-3 w-full sm:w-auto text-center">
|
||||
Login to your account
|
||||
</button> -->
|
||||
<!-- <BaseButton v-if="canResetPassword" :route-name="route('password.request')" color="info" outline
|
||||
label="Remind" /> -->
|
||||
</BaseButtons>
|
||||
<!-- <Link :href="stardust.route('app.register.show')"> Register </Link> -->
|
||||
</BaseLevel>
|
||||
<div class="text-sm font-medium text-gray-500">
|
||||
</BaseButtons>
|
||||
<!-- <Link :href="stardust.route('app.register.show')"> Register </Link> -->
|
||||
|
||||
<!-- <div class="text-sm font-medium text-gray-500">
|
||||
Not registered? <a href="" class="text-teal-500 hover:underline">Create account</a>
|
||||
</div>
|
||||
</div> -->
|
||||
</CardBox>
|
||||
|
||||
|
||||
<CardBox v-else-if="isTwoFactorAuthNeeded" :icon="mdiTwoFactorAuthentication" :class="cardClass" form
|
||||
@submit.prevent="submitFa2Form">
|
||||
<FormField label="2FA Code" label-for="code" help="Please enter 2factor code">
|
||||
<FormControl v-model="fa2Form.code" :icon="mdiAccount" id="code" type="tel" required />
|
||||
</FormField>
|
||||
|
||||
<div v-if="flash && flash.message" class="flex flex-col mt-6 animate-fade-in">
|
||||
<div class="bg-yellow-500 border-l-4 border-orange-400 text-white p-4" role="alert">
|
||||
<p class="font-bold">Be Warned</p>
|
||||
<p>{{ flash.message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<BaseDivider />
|
||||
|
||||
|
||||
<BaseButtons>
|
||||
<!-- <button type="submit" :icon="mdiContentSaveCheck" :disabled="fa2Form.processing"
|
||||
:class="{ 'opacity-25': fa2Form.processing }"
|
||||
class="text-white bg-cyan-600 hover:bg-cyan-700 focus:ring-4 focus:ring-cyan-200 font-medium rounded-lg text-base px-5 py-3 w-full sm:w-auto text-center">
|
||||
Verify
|
||||
</button> -->
|
||||
<BaseButton type="submit" :icon="mdiContentSaveCheck" color="info" label=" Login to your account" :class="{ 'opacity-25': fa2Form.processing }"
|
||||
v-bind:disabled="fa2Form.processing" />
|
||||
</BaseButtons>
|
||||
<!-- <Link :href="stardust.route('app.register.show')"> Register </Link> -->
|
||||
|
||||
</CardBox>
|
||||
|
||||
</SectionFullScreen>
|
||||
</LayoutGuest>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useForm, Head } from '@inertiajs/vue3';
|
||||
import { Ref } from 'vue';
|
||||
import { Ref, ref } from 'vue';
|
||||
// import { Head, Link, useForm } from '@inertiajs/inertia-vue3';
|
||||
import { mdiAccount, mdiAsterisk } from '@mdi/js';
|
||||
import { mdiAccount, mdiAsterisk, mdiTwoFactorAuthentication, mdiContentSaveCheck, mdiLock } from '@mdi/js';
|
||||
import LayoutGuest from '@/Layouts/LayoutGuest.vue';
|
||||
import SectionFullScreen from '@/Components/SectionFullScreen.vue';
|
||||
import CardBox from '@/Components/CardBox.vue';
|
||||
|
@ -116,22 +108,55 @@ import FormCheckRadioGroup from '@/Components/FormCheckRadioGroup.vue';
|
|||
import FormField from '@/Components/FormField.vue';
|
||||
import FormControl from '@/Components/FormControl.vue';
|
||||
import BaseDivider from '@/Components/BaseDivider.vue';
|
||||
// import BaseButton from '@/Components/BaseButton.vue';
|
||||
import BaseButton from '@/Components/BaseButton.vue';
|
||||
import BaseButtons from '@/Components/BaseButtons.vue';
|
||||
import FormValidationErrors from '@/Components/FormValidationErrors.vue';
|
||||
import NotificationBarInCard from '@/Components/NotificationBarInCard.vue';
|
||||
import BaseLevel from '@/Components/BaseLevel.vue';
|
||||
|
||||
|
||||
import { stardust } from '@eidellev/adonis-stardust/client';
|
||||
// import NotificationBar from '@/Components/NotificationBar.vue';
|
||||
import { computed } from 'vue';
|
||||
import { computed, watch } from 'vue';
|
||||
import { usePage } from '@inertiajs/vue3';
|
||||
// import axios from 'axios';
|
||||
// import { LoginState } from '@/Dataset';
|
||||
|
||||
// interface IErrorMessage {
|
||||
// [key: string]: Array<string>;
|
||||
// }
|
||||
const flash: Ref<any> = computed(() => {
|
||||
return usePage().props.flash;
|
||||
let test = usePage().props;
|
||||
return test.flash;
|
||||
});
|
||||
|
||||
const user_id: Ref<any> = computed(() => {
|
||||
let test = usePage().props;
|
||||
return test.user_id;
|
||||
});
|
||||
|
||||
const isTwoFactorAuthNeeded = ref(false);
|
||||
|
||||
// const user_id: ComputedRef<number> = computed(() => {
|
||||
// return usePage().props.flash.user_id as number;
|
||||
// });
|
||||
|
||||
|
||||
// const isTwoFactorAuthNeeded = computed(() => {
|
||||
// if (flash.new_user_id) {
|
||||
// return true;
|
||||
// } else {
|
||||
// return false;
|
||||
// }
|
||||
// });
|
||||
|
||||
|
||||
|
||||
watch(user_id, () => {
|
||||
if (user_id.value) {
|
||||
isTwoFactorAuthNeeded.value = true;
|
||||
} else {
|
||||
isTwoFactorAuthNeeded.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
defineProps({
|
||||
|
@ -146,11 +171,57 @@ defineProps({
|
|||
},
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
// const { version } = usePage();
|
||||
|
||||
const form = useForm(() => ({
|
||||
email: '',
|
||||
password: '',
|
||||
remember: [],
|
||||
});
|
||||
}));
|
||||
|
||||
// const login = async (data): Promise<any | null> => {
|
||||
// try {
|
||||
// let resp = await axios.post(stardust.route('login.store'), data, {
|
||||
// headers: {
|
||||
// 'X-Inertia': true,
|
||||
// 'X-Inertia-Partial-Component': 'Users',
|
||||
// 'X-Inertia-Partial-Data': 'users',
|
||||
// 'X-Inertia-Version': version,
|
||||
// }
|
||||
// });
|
||||
|
||||
// // Check if the response contains a redirect status
|
||||
// if (resp.data.component && resp.data.url) {
|
||||
// // Use Inertia.js to visit the specified component
|
||||
// router.visit(resp.data.url, resp.data.props);
|
||||
// } else {
|
||||
// // If it's not a redirect, reset the password field in the form
|
||||
// form.reset('password');
|
||||
// return resp.data;
|
||||
// }
|
||||
|
||||
// } catch (error) {
|
||||
// // Handle errors if any
|
||||
// console.error('Error during login:', error);
|
||||
// throw error;
|
||||
// }
|
||||
// };
|
||||
|
||||
// const submit = async () => {
|
||||
// // Transform the formData object
|
||||
// const formData = form.data();
|
||||
// const transformedData = {
|
||||
// ...formData,
|
||||
// remember: formData.remember && formData.remember.length ? 'on' : '',
|
||||
// };
|
||||
// const resp = await login(transformedData);
|
||||
// if (resp) {
|
||||
// if (resp.state == LoginState.STATE_VALIDATED) {
|
||||
// isTwoFactorAuthNeeded.value = true
|
||||
// }
|
||||
// fa2Form.login_id = resp.new_user_id;
|
||||
// }
|
||||
// };
|
||||
|
||||
const submit = async () => {
|
||||
await form
|
||||
|
@ -164,4 +235,26 @@ const submit = async () => {
|
|||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const fa2Form = useForm(() => ({
|
||||
code: '',
|
||||
remember: [],
|
||||
login_id: ''
|
||||
}));
|
||||
const submitFa2Form = async () => {
|
||||
await fa2Form
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
remember: fa2Form.remember && fa2Form.remember.length ? 'on' : '',
|
||||
login_id: user_id.value
|
||||
}))
|
||||
.post(stardust.route('login.twoFactorChallenge'), {
|
||||
onFinish: () => {
|
||||
fa2Form.reset('code');
|
||||
fa2Form.reset('login_id');
|
||||
// form.reset('password');
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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');
|
||||
|
|
Loading…
Reference in New Issue
Block a user