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,
"files.eol": "\n",
"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: '/',
// analyze: true,
// env: {},
env: {
API_URL: process.env.API_URL || '/api',
PRODUCT_NAME: process.env.PRODUCT_NAME || 'StylePoint',
},
// rawDefine: {}
// ignorePublicFolder: true,
// minify: false,

View file

@ -60,7 +60,7 @@ router.post('/generate-registration-options', async(req, res) =>
//Check if the registrationToken matches the setting
const registrationTokenSetting = await getSetting('REGISTRATION_TOKEN');
if (registrationTokenSetting !== registrationToken)
if (registrationTokenSetting !== registrationToken && !req.session.loggedInUserId)
{
return res.status(403).json({ error: 'Invalid registration token' });
}
@ -200,9 +200,6 @@ router.post('/verify-registration', async(req, res) =>
challengeStore.delete(userId);
delete req.session.userId;
// Log the user in by setting the final session userId
req.session.loggedInUserId = user.id;
res.json({ verified: true });
}
else
@ -243,27 +240,29 @@ router.post('/generate-authentication-options', async(req, res) =>
user = await getUserById(req.session.loggedInUserId);
}
if (!user)
{
return res.status(404).json({ error: 'User not found' });
}
// if (!user)
// {
// 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({
rpID,
// Require users to use a previously-registered authenticator
allowCredentials: userAuthenticators.map(auth => ({
id: auth.credentialID,
type: 'public-key',
transports: auth.transports ? auth.transports.split(',') : undefined,
})),
// allowCredentials: userAuthenticators.map(auth => ({
// id: auth.credentialID,
// type: 'public-key',
// transports: auth.transports ? auth.transports.split(',') : undefined,
// })),
allowCredentials: [],
userVerification: 'preferred',
});
// Store the challenge associated with the user ID for verification
challengeStore.set(user.id, options.challenge);
req.session.challengeUserId = user.id; // Store user ID associated with this challenge
//Store challenge associated with random ID
const challengeId = crypto.randomUUID();
challengeStore.set(challengeId, options.challenge);
req.session.challengeUserId = challengeId; // Store user ID associated with this challenge
res.json(options);
}
@ -294,11 +293,11 @@ router.post('/verify-authentication', async(req, res) =>
try
{
const user = await getUserById(challengeUserId);
if (!user)
{
return res.status(404).json({ error: 'User associated with challenge not found' });
}
// const user = await getUserById(challengeUserId);
// if (!user)
// {
// return res.status(404).json({ error: 'User associated with challenge not found' });
// }
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' });
}
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
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');
html, body {
html,
body {
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 v-if="leftDrawerOpen">
<q-item-label class="text-h4 absolute-center">
StylePoint
{{ productName }}
</q-item-label>
</q-item-section>
</q-item>
@ -195,7 +195,7 @@
<q-fab
v-model="fabOpen"
icon="chat"
color="accent"
class="bg-theme"
direction="up"
@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 routes from '../router/routes'; // Import routes
const productName = process.env.PRODUCT_NAME;
const $q = useQuasar();
const leftDrawerOpen = ref(false);
const router = useRouter();

View file

@ -1,14 +1,14 @@
<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">
<q-img
<img
src="/stylepoint.png"
alt="StylePoint Logo"
class="logo q-mb-md"
style="max-width: 300px; width: 100%;"
/>
>
<h1 class="text-h3 text-weight-bold text-yellow q-mb-sm">
Welcome to StylePoint
Welcome to {{ productName }}
</h1>
<p class="text-h6 text-white q-mb-lg">
An all-in-one tool designed for StyleTech Developers.
@ -47,6 +47,8 @@
<script setup>
import { ref } from 'vue';
const productName = process.env.PRODUCT_NAME;
const features = ref([
'Automated Daily Reports',
'Deep Mantis Integration',
@ -65,18 +67,6 @@ const features = ref([
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 {
background-color: rgba(0, 0, 0, 0.25);
border-radius: 10px;

View file

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

View file

@ -1,13 +1,27 @@
<template>
<q-page class="flex flex-center">
<q-card style="width: 400px; max-width: 90vw;">
<q-page class="flex flex-center bg-theme">
<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>
<!-- Update title -->
<div class="text-h6">
Register Passkey
<div class="text-h4 text-center">
Register Account
</div>
</q-card-section>
<q-separator />
<q-card-section>
<q-input
v-model="username"
@ -45,12 +59,12 @@
@keyup.enter="handleRegister"
/>
<q-btn
label="Register Passkey"
label="Register Account"
color="primary"
class="full-width"
@click="handleRegister"
:loading="loading"
:disable="loading || !username || !email || !fullName"
:disable="loading || !username || !email || !fullName || (showTokenInput && !registrationToken)"
/>
<div
v-if="successMessage"
@ -66,14 +80,16 @@
</div>
</q-card-section>
<q-card-actions align="center">
<q-card-section>
<!-- Always show login link -->
<q-btn
color="secondary"
flat
label="Already have an account? Login"
to="/login"
class="full-width"
/>
</q-card-actions>
</q-card-section>
</q-card>
</q-page>
</template>