Remodel reset token handling due to browsers sometimes refusing to set the cookie when opening the link (from another site).
This commit is contained in:
@@ -20,12 +20,22 @@ import ResetView from '@/components/ResetView.vue'
|
||||
const store = useAuthStore()
|
||||
|
||||
onMounted(async () => {
|
||||
// Was an error message passed in the URL?
|
||||
// Was an error message passed in the URL hash?
|
||||
const message = location.hash.substring(1)
|
||||
if (message) {
|
||||
store.showMessage(decodeURIComponent(message), 'error')
|
||||
history.replaceState(null, '', location.pathname)
|
||||
}
|
||||
// Capture reset token from query parameter and then remove it
|
||||
const params = new URLSearchParams(location.search)
|
||||
const reset = params.get('reset')
|
||||
if (reset) {
|
||||
store.resetToken = reset
|
||||
// Remove query param to avoid lingering in history / clipboard
|
||||
const targetPath = '/auth/'
|
||||
const currentPath = location.pathname.endsWith('/') ? location.pathname : location.pathname + '/'
|
||||
history.replaceState(null, '', currentPath.startsWith('/auth') ? '/auth/' : targetPath)
|
||||
}
|
||||
try {
|
||||
await store.loadUserInfo()
|
||||
} catch (error) {
|
||||
|
||||
@@ -27,12 +27,14 @@ async function register() {
|
||||
authStore.showMessage('Starting registration...', 'info')
|
||||
|
||||
try {
|
||||
const result = await passkey.register()
|
||||
console.log("Result", result)
|
||||
await authStore.setSessionCookie(result.session_token)
|
||||
|
||||
authStore.showMessage('Passkey registered successfully!', 'success', 2000)
|
||||
authStore.loadUserInfo().then(authStore.selectView)
|
||||
const result = await passkey.register(authStore.resetToken)
|
||||
console.log("Result", result)
|
||||
await authStore.setSessionCookie(result.session_token)
|
||||
// resetToken cleared by setSessionCookie; ensure again
|
||||
authStore.resetToken = null
|
||||
authStore.showMessage('Passkey registered successfully!', 'success', 2000)
|
||||
await authStore.loadUserInfo()
|
||||
authStore.selectView()
|
||||
} catch (error) {
|
||||
authStore.showMessage(`Registration failed: ${error.message}`, 'error')
|
||||
} finally {
|
||||
|
||||
@@ -6,6 +6,7 @@ export const useAuthStore = defineStore('auth', {
|
||||
// Auth State
|
||||
userInfo: null, // Contains the full user info response: {user, credentials, aaguid_info, session_type, authenticated}
|
||||
isLoading: false,
|
||||
resetToken: null, // transient reset token (never stored in cookie)
|
||||
|
||||
// UI State
|
||||
currentView: 'login', // 'login', 'profile', 'device-link', 'reset'
|
||||
@@ -37,6 +38,9 @@ export const useAuthStore = defineStore('auth', {
|
||||
if (result.detail) {
|
||||
throw new Error(result.detail)
|
||||
}
|
||||
// On successful session establishment, discard any reset token to avoid
|
||||
// sending stale Authorization headers on subsequent API calls.
|
||||
this.resetToken = null
|
||||
return result
|
||||
},
|
||||
async register() {
|
||||
@@ -69,9 +73,25 @@ export const useAuthStore = defineStore('auth', {
|
||||
else this.currentView = 'reset'
|
||||
},
|
||||
async loadUserInfo() {
|
||||
const response = await fetch('/auth/user-info', {method: 'POST'})
|
||||
const result = await response.json()
|
||||
if (result.detail) throw new Error(`Server: ${result.detail}`)
|
||||
const headers = {}
|
||||
// Reset tokens are only passed via query param now, not Authorization header
|
||||
const url = this.resetToken ? `/auth/user-info?reset=${encodeURIComponent(this.resetToken)}` : '/auth/user-info'
|
||||
const response = await fetch(url, { method: 'POST', headers })
|
||||
let result = null
|
||||
try {
|
||||
result = await response.json()
|
||||
} catch (_) {
|
||||
// ignore JSON parse errors (unlikely)
|
||||
}
|
||||
if (response.status === 401 && result?.detail) {
|
||||
this.showMessage(result.detail, 'error', 5000)
|
||||
throw new Error(result.detail)
|
||||
}
|
||||
if (result?.detail) {
|
||||
// Other error style
|
||||
this.showMessage(result.detail, 'error', 5000)
|
||||
throw new Error(result.detail)
|
||||
}
|
||||
this.userInfo = result
|
||||
console.log('User info loaded:', result)
|
||||
},
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { startRegistration, startAuthentication } from '@simplewebauthn/browser'
|
||||
import aWebSocket from '@/utils/awaitable-websocket'
|
||||
|
||||
export async function register() {
|
||||
const ws = await aWebSocket("/auth/ws/register")
|
||||
export async function register(resetToken = null) {
|
||||
const url = resetToken ? `/auth/ws/register?reset=${encodeURIComponent(resetToken)}` : "/auth/ws/register"
|
||||
const ws = await aWebSocket(url)
|
||||
try {
|
||||
const optionsJSON = await ws.receive_json()
|
||||
const registrationResponse = await startRegistration({ optionsJSON })
|
||||
|
||||
@@ -27,7 +27,9 @@ export default defineConfig(({ command, mode }) => ({
|
||||
// and static assets so that HMR works. Bypass tells http-proxy to skip
|
||||
// proxying when we return a (possibly rewritten) local path.
|
||||
bypass(req) {
|
||||
const url = req.url || ''
|
||||
const rawUrl = req.url || ''
|
||||
// Strip query/hash to match path-only for SPA entrypoints with query params (e.g. ?reset=token)
|
||||
const url = rawUrl.split('?')[0].split('#')[0]
|
||||
// Bypass only root SPA entrypoints + static assets so Vite serves them for HMR.
|
||||
// Admin API endpoints (e.g., /auth/admin/orgs) must still hit backend.
|
||||
if (url === '/auth/' || url === '/auth') return '/'
|
||||
|
||||
Reference in New Issue
Block a user