Authentication flow change - Remove requirement for user to enter username - Use Passkey discoverability instead.

This commit is contained in:
Cameron Redmore 2025-04-26 08:32:54 +01:00
parent 0d277e3035
commit 7564937faa
8 changed files with 120 additions and 75 deletions

View file

@ -16,5 +16,8 @@
"editor.formatOnSave": true, "editor.formatOnSave": true,
"files.eol": "\n", "files.eol": "\n",
"files.trimTrailingWhitespace": true, "files.trimTrailingWhitespace": true,
"editor.trimAutoWhitespace": true "editor.trimAutoWhitespace": true,
"[scss]": {
"editor.defaultFormatter": "vscode.css-language-features"
}
} }

View file

@ -50,7 +50,10 @@ export default defineConfig((/* ctx */) =>
// publicPath: '/', // publicPath: '/',
// analyze: true, // analyze: true,
// env: {}, env: {
API_URL: process.env.API_URL || '/api',
PRODUCT_NAME: process.env.PRODUCT_NAME || 'StylePoint',
},
// rawDefine: {} // rawDefine: {}
// ignorePublicFolder: true, // ignorePublicFolder: true,
// minify: false, // minify: false,

View file

@ -60,7 +60,7 @@ router.post('/generate-registration-options', async(req, res) =>
//Check if the registrationToken matches the setting //Check if the registrationToken matches the setting
const registrationTokenSetting = await getSetting('REGISTRATION_TOKEN'); const registrationTokenSetting = await getSetting('REGISTRATION_TOKEN');
if (registrationTokenSetting !== registrationToken) if (registrationTokenSetting !== registrationToken && !req.session.loggedInUserId)
{ {
return res.status(403).json({ error: 'Invalid registration token' }); return res.status(403).json({ error: 'Invalid registration token' });
} }
@ -200,9 +200,6 @@ router.post('/verify-registration', async(req, res) =>
challengeStore.delete(userId); challengeStore.delete(userId);
delete req.session.userId; delete req.session.userId;
// Log the user in by setting the final session userId
req.session.loggedInUserId = user.id;
res.json({ verified: true }); res.json({ verified: true });
} }
else else
@ -243,27 +240,29 @@ router.post('/generate-authentication-options', async(req, res) =>
user = await getUserById(req.session.loggedInUserId); user = await getUserById(req.session.loggedInUserId);
} }
if (!user) // if (!user)
{ // {
return res.status(404).json({ error: 'User not found' }); // return res.status(404).json({ error: 'User not found' });
} // }
const userAuthenticators = await getUserAuthenticators(user.id); // const userAuthenticators = await getUserAuthenticators(user.id);
const options = await generateAuthenticationOptions({ const options = await generateAuthenticationOptions({
rpID, rpID,
// Require users to use a previously-registered authenticator // Require users to use a previously-registered authenticator
allowCredentials: userAuthenticators.map(auth => ({ // allowCredentials: userAuthenticators.map(auth => ({
id: auth.credentialID, // id: auth.credentialID,
type: 'public-key', // type: 'public-key',
transports: auth.transports ? auth.transports.split(',') : undefined, // transports: auth.transports ? auth.transports.split(',') : undefined,
})), // })),
allowCredentials: [],
userVerification: 'preferred', userVerification: 'preferred',
}); });
// Store the challenge associated with the user ID for verification //Store challenge associated with random ID
challengeStore.set(user.id, options.challenge); const challengeId = crypto.randomUUID();
req.session.challengeUserId = user.id; // Store user ID associated with this challenge challengeStore.set(challengeId, options.challenge);
req.session.challengeUserId = challengeId; // Store user ID associated with this challenge
res.json(options); res.json(options);
} }
@ -294,11 +293,11 @@ router.post('/verify-authentication', async(req, res) =>
try try
{ {
const user = await getUserById(challengeUserId); // const user = await getUserById(challengeUserId);
if (!user) // if (!user)
{ // {
return res.status(404).json({ error: 'User associated with challenge not found' }); // return res.status(404).json({ error: 'User associated with challenge not found' });
} // }
const authenticator = await getAuthenticatorByCredentialID(authenticationResponse.id); const authenticator = await getAuthenticatorByCredentialID(authenticationResponse.id);
@ -307,6 +306,12 @@ router.post('/verify-authentication', async(req, res) =>
return res.status(404).json({ error: 'Authenticator not found' }); return res.status(404).json({ error: 'Authenticator not found' });
} }
const user = await getUserById(authenticator.userId);
if (!user)
{
return res.status(404).json({ error: 'User not found' });
}
// Ensure the authenticator belongs to the user attempting to log in // Ensure the authenticator belongs to the user attempting to log in
if (authenticator.userId !== user.id) if (authenticator.userId !== user.id)
{ {

View file

@ -2,6 +2,23 @@
@import url('https://fonts.googleapis.com/css2?family=Exo+2:ital,wght@0,100..900;1,100..900&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Exo+2:ital,wght@0,100..900;1,100..900&display=swap');
html, body { html,
body {
font-family: 'Exo 2', sans-serif; font-family: 'Exo 2', sans-serif;
}
.bg-theme {
background: linear-gradient(to top left,
#fdc730,
#ea2963,
#bd288a 50%,
#6e43ac,
#4763bf,
#16a3e8) !important;
}
.bg-blurred {
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(10px);
} }

View file

@ -18,7 +18,7 @@
</q-item-section> </q-item-section>
<q-item-section v-if="leftDrawerOpen"> <q-item-section v-if="leftDrawerOpen">
<q-item-label class="text-h4 absolute-center"> <q-item-label class="text-h4 absolute-center">
StylePoint {{ productName }}
</q-item-label> </q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
@ -195,7 +195,7 @@
<q-fab <q-fab
v-model="fabOpen" v-model="fabOpen"
icon="chat" icon="chat"
color="accent" class="bg-theme"
direction="up" direction="up"
@click="toggleChat" @click="toggleChat"
/> />
@ -265,6 +265,8 @@ import { usePreferencesStore } from 'stores/preferences'; // Import the preferen
import ChatInterface from 'components/ChatInterface.vue'; // Adjust path as needed import ChatInterface from 'components/ChatInterface.vue'; // Adjust path as needed
import routes from '../router/routes'; // Import routes import routes from '../router/routes'; // Import routes
const productName = process.env.PRODUCT_NAME;
const $q = useQuasar(); const $q = useQuasar();
const leftDrawerOpen = ref(false); const leftDrawerOpen = ref(false);
const router = useRouter(); const router = useRouter();

View file

@ -1,14 +1,14 @@
<template> <template>
<q-page class="landing-page column items-center q-pa-md wallpaper"> <q-page class="landing-page column items-center q-pa-md bg-theme">
<div class="hero text-center q-pa-xl full-width"> <div class="hero text-center q-pa-xl full-width">
<q-img <img
src="/stylepoint.png" src="/stylepoint.png"
alt="StylePoint Logo" alt="StylePoint Logo"
class="logo q-mb-md" class="logo q-mb-md"
style="max-width: 300px; width: 100%;" style="max-width: 300px; width: 100%;"
/> >
<h1 class="text-h3 text-weight-bold text-yellow q-mb-sm"> <h1 class="text-h3 text-weight-bold text-yellow q-mb-sm">
Welcome to StylePoint Welcome to {{ productName }}
</h1> </h1>
<p class="text-h6 text-white q-mb-lg"> <p class="text-h6 text-white q-mb-lg">
An all-in-one tool designed for StyleTech Developers. An all-in-one tool designed for StyleTech Developers.
@ -47,6 +47,8 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from 'vue';
const productName = process.env.PRODUCT_NAME;
const features = ref([ const features = ref([
'Automated Daily Reports', 'Automated Daily Reports',
'Deep Mantis Integration', 'Deep Mantis Integration',
@ -65,18 +67,6 @@ const features = ref([
filter: drop-shadow(0 0 25px rgba(0, 0, 0, 0.5)); filter: drop-shadow(0 0 25px rgba(0, 0, 0, 0.5));
} }
.wallpaper {
background: linear-gradient(
to top left,
#fdc730,
#ea2963,
#bd288a 50%,
#6e43ac,
#4763bf,
#16a3e8
);
}
.features { .features {
background-color: rgba(0, 0, 0, 0.25); background-color: rgba(0, 0, 0, 0.25);
border-radius: 10px; border-radius: 10px;

View file

@ -1,29 +1,38 @@
<template> <template>
<q-page class="flex flex-center"> <q-page class="flex flex-center bg-theme">
<q-card style="width: 400px; max-width: 90vw;"> <img
src="/stylepoint.png"
alt="StylePoint Logo"
class="logo q-mb-md absolute"
style="max-width: 300px; width: 100%; top: 75px;"
>
<q-card
style="width: 400px; max-width: 90vw;"
dark
class="bg-blurred"
flat
bordered
>
<q-card-section> <q-card-section>
<div class="text-h6"> <div
Login class="
text-h4
text-center"
>
<small>Login</small>
</div> </div>
</q-card-section> </q-card-section>
<q-card-section> <q-separator />
<q-input
v-model="username" <q-card-section class="row justify-center">
label="Username"
outlined
dense
class="q-mb-md"
@keyup.enter="handleLogin"
:hint="errorMessage ? errorMessage : ''"
:rules="[val => !!val || 'Username is required']"
/>
<q-btn <q-btn
label="Login with Passkey" icon="key"
color="primary" round
class="full-width"
@click="handleLogin" @click="handleLogin"
:loading="loading" :loading="loading"
size="xl"
class="bg-theme shadow-5"
/> />
<div <div
v-if="errorMessage" v-if="errorMessage"
@ -35,7 +44,8 @@
<q-card-actions align="center"> <q-card-actions align="center">
<q-btn <q-btn
flat outline
color="secondary"
label="Don't have an account? Register" label="Don't have an account? Register"
to="/register" to="/register"
/> />
@ -51,7 +61,8 @@ import { startAuthentication } from '@simplewebauthn/browser';
import axios from 'boot/axios'; import axios from 'boot/axios';
import { useAuthStore } from 'stores/auth'; // Import the auth store import { useAuthStore } from 'stores/auth'; // Import the auth store
const username = ref(''); const productName = process.env.PRODUCT_NAME;
const loading = ref(false); const loading = ref(false);
const errorMessage = ref(''); const errorMessage = ref('');
const router = useRouter(); const router = useRouter();
@ -65,9 +76,7 @@ async function handleLogin()
try try
{ {
// 1. Get options from server // 1. Get options from server
const optionsRes = await axios.post('/api/auth/generate-authentication-options', { const optionsRes = await axios.post('/api/auth/generate-authentication-options');
username: username.value || undefined, // Send username if provided
});
const options = optionsRes.data; const options = optionsRes.data;
// 2. Start authentication ceremony in browser // 2. Start authentication ceremony in browser
@ -125,4 +134,4 @@ async function handleLogin()
loading.value = false; loading.value = false;
} }
} }
</script> </script>

View file

@ -1,13 +1,27 @@
<template> <template>
<q-page class="flex flex-center"> <q-page class="flex flex-center bg-theme">
<q-card style="width: 400px; max-width: 90vw;"> <img
src="/stylepoint.png"
alt="StylePoint Logo"
class="logo q-mb-md absolute"
style="max-width: 300px; width: 100%; top: 75px;"
>
<q-card
style="width: 400px; max-width: 90vw;"
dark
class="bg-blurred"
flat
bordered
>
<q-card-section> <q-card-section>
<!-- Update title --> <!-- Update title -->
<div class="text-h6"> <div class="text-h4 text-center">
Register Passkey Register Account
</div> </div>
</q-card-section> </q-card-section>
<q-separator />
<q-card-section> <q-card-section>
<q-input <q-input
v-model="username" v-model="username"
@ -45,12 +59,12 @@
@keyup.enter="handleRegister" @keyup.enter="handleRegister"
/> />
<q-btn <q-btn
label="Register Passkey" label="Register Account"
color="primary" color="primary"
class="full-width" class="full-width"
@click="handleRegister" @click="handleRegister"
:loading="loading" :loading="loading"
:disable="loading || !username || !email || !fullName" :disable="loading || !username || !email || !fullName || (showTokenInput && !registrationToken)"
/> />
<div <div
v-if="successMessage" v-if="successMessage"
@ -66,14 +80,16 @@
</div> </div>
</q-card-section> </q-card-section>
<q-card-actions align="center"> <q-card-section>
<!-- Always show login link --> <!-- Always show login link -->
<q-btn <q-btn
color="secondary"
flat flat
label="Already have an account? Login" label="Already have an account? Login"
to="/login" to="/login"
class="full-width"
/> />
</q-card-actions> </q-card-section>
</q-card> </q-card>
</q-page> </q-page>
</template> </template>