atinux / nuxt-auth-utils

Add Authentication to Nuxt applications with secured & sealed cookies sessions.
MIT License
974 stars 91 forks source link

Middleware can interfere with `api/_auth/session` endpoint #165

Closed rudokemper closed 2 months ago

rudokemper commented 2 months ago

I am having an issue that others seem to have encountered before (cf. this Discord thread or perhaps this issue), but have not been able to figure out a solution:

Sessions are not being set after successfully authenticating, and loggedIn remains false across the application. This is regardless of the auth provider (I have tried auth0 and Github and am experiencing the same issue).

On the client side, the flow is as follows: Upon accessing the login API route, I am redirected to the auth provider, able to authenticate, and then redirected back after successfully logging in. The API route then does show that a session being created successfully, through getUserSession, but there is no session stored elsewhere in cookies or local storage, and loggedIn stays false, so it is as if I never logged in.

Per the documentation, this is my API route:

import { H3Event } from 'h3';

interface Auth0User {
  email: string;
}

export default oauthAuth0EventHandler({
  config: {
    emailRequired: true,
  },

  async onSuccess(event: H3Event, { user }: { user: Auth0User }) {
    await setUserSession(event, { 
      user: {
        auth0: user.email,
      },
      loggedInAt: Date.now(),
    });

    // Here, I am logging the successful establishment of a user session.
   // This will log something like OAuth success on API side: { user: { auth0: 'email@email.com' }, loggedInAt: 1725721978213 }
    const session = await getUserSession(event)
    console.log("OAuth success on API side:", session);

    return sendRedirect(event, '/')
  },
  onError(event: H3Event, error: string) {
    console.error("OAuth error:", error);
    return sendRedirect(event, "/login");
  },
});

I have tried to follow the example in atidone as closely as possible, with similar redirect middleware, but I seem to be missing something, and I am not sure why; it could be because I am not using @nuxthub/core or Nuxt 4, but I haven't tested that.

Here is my login page:

<template>
  <div class="flex flex-col items-center justify-center h-screen">
    <p class="italic">{{ $t("authMessage") }}.</p>
    <button
      class="px-4 py-2 mt-4 mb-4 bg-blue-500 text-white rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-opacity-50"
      @click="loginWithAuth0"
    >
      {{ $t("loginButton") }}
    </button>
    <p v-if="errorMessage" class="text-red-500 text-xs italic">
      {{ errorMessage }}
    </p>
  </div>
</template>

<script setup>
import { ref, onMounted } from "vue";
import { useI18n } from "vue-i18n";

// Define composables
const errorMessage = ref("");
const { t } = useI18n();
const router = useRouter();
const localePath = useLocalePath();
const { loggedIn } = useUserSession()

const redirectPath = ref(localePath("/"));

// On mount
onMounted(() => {
  const redirect = router.currentRoute.value.query.redirect;
  redirectPath.value = redirect
    ? decodeURIComponent(redirect)
    : localePath("/map");

  const hashParams = new URLSearchParams(window.location.hash.substring(1));
  const error = hashParams.get("error");
  const errorDescription = hashParams.get("error_description");

  if (error === "access_denied") {
    errorMessage.value = decodeURIComponent(errorDescription);
  }

  if (loggedIn.value) {
    router.push(redirectPath.value);
  }
});

const loginWithAuth0 = () => {
  window.location.href = "/auth/auth0";
};
</script>

Upon authentication, I am supposed to be returned back to /, but my middleware automatically redirects me here since loggedIn is false across the app:

export default defineNuxtRouteMiddleware((to) => {
  const { loggedIn } = useUserSession();

  if (!loggedIn.value && to.path !== "/login") {
    return navigateTo("/login");
  }
});

Lastly, just to clarify, I am using SSR, so that's not the problem. There is nothing else about my Nuxt use case that would seem to interfere with the auth flow. I am using nuxt 3.12.2 and nuxt-auth-utils 0.3.6.

Any ideas?

Since others seem to have had this issue, I would love to contribute to this project by documenting a solution for when this issue is encountered.

rudokemper commented 2 months ago

The issue was that I had middleware applied to the api/ directory, which was interfering with the route used by Nuxt Auth Utils api/_auth/session. Filing a PR to expose the usage of this route in the readme, which would have saved me (and possibly others) some time.

Barbapapazes commented 2 months ago

Hey 👋,

What was doing the middleware that was interfering with your route? I think that the usage of global middleware (within Nitro) should be clarified because their are using a lot of trouble.

rudokemper commented 2 months ago

Hi, yes that makes sense. I really didn't expect my middleware to be impacting the functionality of this module.

My middleware is forbidding access to API routes unless an API key is included in the header - I use this to protect my data API routes. And you can see how I solved my problem by adding a bypass for the auth-nuxt-utils route.

import { API_KEY } from "../../config";

export default defineEventHandler((event: H3Event) => {
  // Only apply middleware to API routes
  if (!event.node.req.url?.startsWith("/api/")) {
    return;
  }

  // Bypass middleware for auth-nuxt-utils
  if (
    event.node.req.url?.startsWith("/api/_auth/")
  ) {
    return;
  }

  const apiKey = event.node.req.headers["x-api-key"];
  if (apiKey !== API_KEY) {
    event.node.res.statusCode = 403;
    event.node.res.end("Forbidden");
    return;
  }
});
Barbapapazes commented 2 months ago

Yeah, I totally understand the issue.

You can create a utils from this middleware and apply it only to want event handler by leveraging the object syntax or as a simple function like the requireUserSession utils.

Barbapapazes commented 2 months ago

And instead of using

 if (apiKey !== API_KEY) {
    event.node.res.statusCode = 403;
    event.node.res.end("Forbidden");
    return;
  }

You could simply throw an error using createError utility.

 if (apiKey !== API_KEY) {
    throw createError({
        status: 403,
        message: 'Forbidden'
    })
  }
rudokemper commented 2 months ago

:+1: thanks for these helpful tips!