Telerik blogs

Get one dev’s tips for the minimalist approach to authentication in Nuxt 3.

After spending a significant amount of my time working with Nuxt 3 this summer, I had to focus on designing a few authentication systems. I’ve explored various approaches, discovering that some are far simpler and more efficient than others.

In this article, I’ll share with you my most streamlined approach to managing authentication in Nuxt 3. 👩‍💻

What Solutions Are Available?

With the transition from Nuxt 2 to Nuxt 3, many developers might notice that familiar packages like @nuxtjs/auth are no longer viable. Fortunately for us, the Nuxt team is actively developing a new official module for Nuxt 3. 🏋️‍♀️

In the meantime, there are three main solutions currently available:

  1. Built-in Nuxt 3 utilities: Nuxt 3’s native utilities offer a straightforward, integrated approach to session management and authentication. This is the method I’ll focus on in this article.

  2. nuxt-auth-utils: A minimalistic module maintained by Sebastien Chopin, nuxt-auth-utils provides essential tools for session management and OAuth integration. It’s lightweight yet powerful for many projects.

  3. @sidebase/nuxt-auth: If you’re looking for a more feature-rich solution, this package is a strong contender. It supports OAuth providers, email-based authentication with Magic URLs and more. Leveraging the well-established Auth.js/NextAuth.js ecosystem, it’s ideal for more complex projects.

Key Differences Between nuxt-auth-utils and @sidebase/nuxt-auth

While both nuxt-auth-utils and @sidebase/nuxt-auth are excellent options, they cater to different needs. nuxt-auth-utils is a more streamlined solution, focusing on core utilities for session and OAuth management. On the other hand, @sidebase/nuxt-auth offers a broader range of features, including support for multiple authentication providers and enhanced session management capabilities, making it a robust choice for complex applications. 🦹‍♀️

I haven’t had the chance to use any of these packages in production yet so I won’t cover them in this article. I thought they were worth mentioning, though, as they might be more suitable for your project’s requirements if the solution I’m about to present doesn’t meet your needs. 🤗

Leveraging Built-in Nuxt 3 Utilities

This approach is one I frequently use in various applications due to its simplicity and direct integration with Nuxt 3. This solution assumes that your backend handles authentication using tokens, such as JWT, and that these tokens are accessible via an API.

To implement this, you’ll need three components: a simple composable function (or a Pinia store), middleware to handle SSR and an Axios plugin to attach the token to the request headers. While you can use fetch or any other HTTP client, I prefer Axios due to familiarity. 😅

Step 1: Create a Composable Function

Let’s start by creating a composable function to manage authentication:

// composable/useAuth.ts
export function useAuth() {
  // Custom composable to interact with your API
  const api = useApi()

  // Define reactive states for user and token
  const user = ref<null | User>(null)
  const opaqueToken = useState<null | string>('opaqueToken')

  // Function to handle login via email
  const loginWithEmail = async (params: {
    email: string
    password: string
  }) => {
    try {
      const { data: { opaqueToken, user } } = await api.auth.loginWithEmail({
        email: params.email,
        password: params.password,
      })

      opaqueToken.value = opaqueToken
      user.value = user

      return { user }
    } catch (error) {
      return Promise.reject(error)
    }
  }

  // Function to handle login via Google by exchanging the code for an opaque token
  const loginWithGoogle = async (params: {
    code: string
  }) => {
    try {
      const { data: { opaqueToken, user } } = await api.auth.loginWithGoogle({
        code: params.code,
      })

      opaqueToken.value = opaqueToken
      user.value = user

      return { user }
    }
    catch (error) {
      return Promise.reject(error)
    }
  }

  // Function to handle logout
  const logout = async () => {
    try {
      await api.auth.logout()

      opaqueToken.value = null
      user.value = null
    } catch (error) {
      return Promise.reject(error)
    }
  }

  // Watch for changes in the opaqueToken and sync with a cookie
  watch(opaqueToken, (newVal) => {
    const cookie = useCookie('opaqueToken')

    if (cookie.value !== newVal)
      cookie.value = newVal
  })

  return {
    opaqueToken,
    user,
    loginWithEmail,
    loginWithGoogle,
    logout,
  }
}

By invoking the loginWithEmail function anywhere in your application, you’ll have access to both the user data and the opaque token.

Step 2: Create a Middleware for SSR

Now let’s create a global middleware to share the opaque token between the server and the client. As you can see, we are using the useCookie composable as a proxy to share the opaque token between the server and the client. useState should be initialized on the server during SSR so it’s available immediately when the client hydrates.

// middleware/opaqueToken.global.ts

// Define a global middleware that will run for every route in the application
export default defineNuxtRouteMiddleware(() => {
  // Create or access a reactive state named 'opaqueToken'
  // This state will be available throughout the application
  const state = useState('opaqueToken')

  // Retrieve the value of the 'opaqueToken' cookie and assign it to the 'opaqueToken' state
  // This ensures that the state is always in sync with the cookie value
  // This synchronization is crucial for maintaining a consistent user session across server and client.
  state.value = useCookie('opaqueToken').value
})

This way, the token is always available and synchronized between SSR and CSR, maintaining a consistent user session.

Step 3: Set Up an Axios Plugin

Finally, let’s set up an Axios plugin to automatically include the opaque token in the authorization headers for each request:

// plugins/axios.ts
export default defineNuxtPlugin(() => {
  // Retrieve the 'opaqueToken' state
  const opaqueToken = useCookie('opaqueToken').value

  // Set the 'Authorization' header with the 'opaqueToken' value
  axios.interceptors.request.use((config) => {
    if (opaqueToken.value) {
      config.headers.Authorization = `Bearer ${opaqueToken.value}`
    }

    return config
  })
})

With this plugin, the opaque token is included in the authorization headers for each request, allowing the backend to authenticate the user.

Conclusion

And that’s all you need to implement a basic, yet effective, authentication system using built-in Nuxt 3 utilities! 🧚‍♀️

Sometimes, the simplest solutions are the most powerful. After countless hours of trial and error, this approach has proven to be the most reliable and easy to implement. Whether you’re building a small project or a more complex application, this method provides a solid foundation for secure and efficient authentication. 🤓

If you found this article helpful, feel free to reach out to me on Twitter @RifkiNada. For more articles and insights, check out my work at www.nadarifki.com. 😉

Thank you for reading! 🙏


author-photo_nada-rifki
About the Author

Nada Rifki

Nada is a JavaScript developer who likes to play with UI components to create interfaces with great UX. She specializes in Vue/Nuxt, and loves sharing anything and everything that could help her fellow frontend web developers. Nada also dabbles in digital marketing, dance and Chinese. You can reach her on Twitter @RifkiNada or visit her website, nadarifki.com.

Related Posts

Comments

Comments are disabled in preview mode.