feat: implement validateRedirect utility for open redirect protection

This commit is contained in:
2025-12-12 13:57:44 +01:00
parent 8867dff780
commit 64d9df5469
3 changed files with 25 additions and 9 deletions

View File

@@ -15,6 +15,7 @@ export interface LoggedInUser extends RecordModel {
const user = ref<LoggedInUser | null>(null); const user = ref<LoggedInUser | null>(null);
const loading = ref<boolean>(false); const loading = ref<boolean>(false);
const error = ref<Error | null>(null); const error = ref<Error | null>(null);
let isInitialized = false;
export const useAuth = () => { export const useAuth = () => {
const pb = usePocketbase(); const pb = usePocketbase();
@@ -22,13 +23,18 @@ export const useAuth = () => {
const userCollection = 'users'; const userCollection = 'users';
const isAuthenticated = computed<boolean>(() => pb.authStore.isValid && !!user.value);
const initAuth = async () => { const initAuth = async () => {
user.value = pb.authStore.record as LoggedInUser; user.value = pb.authStore.record as LoggedInUser;
pb.authStore.onChange((_token, model) => (user.value = model as LoggedInUser)); pb.authStore.onChange((_token, model) => (user.value = model as LoggedInUser));
}; };
if (!isInitialized) {
initAuth();
isInitialized = true;
}
const isAuthenticated = computed<boolean>(() => pb.authStore.isValid && !!user.value);
const authProviders = async (): Promise<AuthProviderInfo[]> => { const authProviders = async (): Promise<AuthProviderInfo[]> => {
const authMethods = await pb.collection(userCollection).listAuthMethods(); const authMethods = await pb.collection(userCollection).listAuthMethods();
return authMethods.oauth2.enabled ? authMethods.oauth2.providers : []; return authMethods.oauth2.enabled ? authMethods.oauth2.providers : [];
@@ -44,7 +50,9 @@ export const useAuth = () => {
throw new Error(`${provider} OAuth is not configured`); throw new Error(`${provider} OAuth is not configured`);
} }
const response = await pb.collection(userCollection).authWithOAuth2({ provider }); const response = await pb.collection(userCollection).authWithOAuth2({ provider });
console.log('Auth response:', response)
user.value = response.record as LoggedInUser; user.value = response.record as LoggedInUser;
console.log('User value', user.value)
} catch (pbError) { } catch (pbError) {
error.value = pbError as Error; error.value = pbError as Error;
} finally { } finally {
@@ -64,9 +72,9 @@ export const useAuth = () => {
}; };
const logout = () => { const logout = () => {
pb.authStore.clear();
user.value = null; user.value = null;
error.value = null; error.value = null;
pb.authStore.clear();
}; };
return { return {
@@ -76,7 +84,6 @@ export const useAuth = () => {
isAuthenticated, isAuthenticated,
login, login,
logout, logout,
initAuth,
refreshAuth, refreshAuth,
handleOAuthCallback, handleOAuthCallback,
authProviders, authProviders,

View File

@@ -24,14 +24,14 @@ definePageMeta({
}); });
const route = useRoute(); const route = useRoute();
const redirectPath = (route.query.redirect as string) || '/dashboard'; const redirectPath = validateRedirect(route.query.redirect, '/dashboard');
const { authProviders, error, isAuthenticated } = useAuth(); const { authProviders, error, isAuthenticated } = useAuth();
const providers = await authProviders(); const providers = await authProviders();
const redirect = (authenticated: boolean) => {
watch(isAuthenticated, (authenticated) => {
if (authenticated) { if (authenticated) {
navigateTo(redirectPath); navigateTo(redirectPath);
} }
}); };
redirect(isAuthenticated.value);
watch(isAuthenticated, redirect);
</script> </script>

View File

@@ -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;
}