From 64d9df5469a971c0d707b5d5b9ddd3a1d46ed3b3 Mon Sep 17 00:00:00 2001 From: Lucien Cartier-Tilet Date: Fri, 12 Dec 2025 13:57:44 +0100 Subject: [PATCH] feat: implement validateRedirect utility for open redirect protection --- app/composables/useAuth.ts | 15 +++++++++++---- app/pages/login.vue | 10 +++++----- app/utils/validateRedirect.ts | 9 +++++++++ 3 files changed, 25 insertions(+), 9 deletions(-) create mode 100644 app/utils/validateRedirect.ts diff --git a/app/composables/useAuth.ts b/app/composables/useAuth.ts index aaa68d1..a5d6f28 100644 --- a/app/composables/useAuth.ts +++ b/app/composables/useAuth.ts @@ -15,6 +15,7 @@ export interface LoggedInUser extends RecordModel { const user = ref(null); const loading = ref(false); const error = ref(null); +let isInitialized = false; export const useAuth = () => { const pb = usePocketbase(); @@ -22,13 +23,18 @@ export const useAuth = () => { const userCollection = 'users'; - const isAuthenticated = computed(() => pb.authStore.isValid && !!user.value); - const initAuth = async () => { user.value = pb.authStore.record as LoggedInUser; pb.authStore.onChange((_token, model) => (user.value = model as LoggedInUser)); }; + if (!isInitialized) { + initAuth(); + isInitialized = true; + } + + const isAuthenticated = computed(() => pb.authStore.isValid && !!user.value); + const authProviders = async (): Promise => { const authMethods = await pb.collection(userCollection).listAuthMethods(); return authMethods.oauth2.enabled ? authMethods.oauth2.providers : []; @@ -44,7 +50,9 @@ export const useAuth = () => { throw new Error(`${provider} OAuth is not configured`); } const response = await pb.collection(userCollection).authWithOAuth2({ provider }); + console.log('Auth response:', response) user.value = response.record as LoggedInUser; + console.log('User value', user.value) } catch (pbError) { error.value = pbError as Error; } finally { @@ -64,9 +72,9 @@ export const useAuth = () => { }; const logout = () => { - pb.authStore.clear(); user.value = null; error.value = null; + pb.authStore.clear(); }; return { @@ -76,7 +84,6 @@ export const useAuth = () => { isAuthenticated, login, logout, - initAuth, refreshAuth, handleOAuthCallback, authProviders, diff --git a/app/pages/login.vue b/app/pages/login.vue index ef49081..4751bde 100644 --- a/app/pages/login.vue +++ b/app/pages/login.vue @@ -24,14 +24,14 @@ definePageMeta({ }); const route = useRoute(); -const redirectPath = (route.query.redirect as string) || '/dashboard'; +const redirectPath = validateRedirect(route.query.redirect, '/dashboard'); const { authProviders, error, isAuthenticated } = useAuth(); - const providers = await authProviders(); - -watch(isAuthenticated, (authenticated) => { +const redirect = (authenticated: boolean) => { if (authenticated) { navigateTo(redirectPath); } -}); +}; +redirect(isAuthenticated.value); +watch(isAuthenticated, redirect); diff --git a/app/utils/validateRedirect.ts b/app/utils/validateRedirect.ts new file mode 100644 index 0000000..10b6479 --- /dev/null +++ b/app/utils/validateRedirect.ts @@ -0,0 +1,9 @@ +export const validateRedirect = (redirect: string | unknown, fallback = '/dashboard'): string => { + if (typeof redirect !== 'string') { + return fallback; + } + if (redirect.startsWith('/') && !redirect.startsWith('//')) { + return redirect; + } + return fallback; +}