Giter Club home page Giter Club logo

nuxt-auth-utils's Introduction

Nuxt Auth Utils

npm version npm downloads License Nuxt

Minimalist Authentication module for Nuxt exposing Vue composables and server utils.

Features

Requirements

This module only works with SSR (server-side rendering) enabled as it uses server API routes. You cannot use this module with nuxt generate.

Quick Setup

  1. Add nuxt-auth-utils in your Nuxt project
npx nuxi@latest module add auth-utils
  1. Add a NUXT_SESSION_PASSWORD env variable with at least 32 characters in the .env.
# .env
NUXT_SESSION_PASSWORD=password-with-at-least-32-characters

Nuxt Auth Utils generates one for you when running Nuxt in development the first time if no NUXT_SESSION_PASSWORD is set.

  1. That's it! You can now add authentication to your Nuxt app ✨

Vue Composables

Nuxt Auth Utils automatically adds some plugins to fetch the current user session to let you access it from your Vue components.

User Session

<script setup>
const { loggedIn, user, session, clear } = useUserSession()
</script>

<template>
  <div v-if="loggedIn">
    <h1>Welcome {{ user.login }}!</h1>
    <p>Logged in since {{ session.loggedInAt }}</p>
    <button @click="clear">Logout</button>
  </div>
  <div v-else>
    <h1>Not logged in</h1>
    <a href="/auth/github">Login with GitHub</a>
  </div>
</template>

Server Utils

The following helpers are auto-imported in your server/ directory.

Session Management

// Set a user session, note that this data is encrypted in the cookie but can be decrypted with an API call
// Only store the data that allow you to recognize a user, but do not store sensitive data
// Merges new data with existing data using defu()
await setUserSession(event, {
  user: {
    // ... user data
  },
  loggedInAt: new Date()
  // Any extra fields
})

// Replace a user session. Same behaviour as setUserSession, except it does not merge data with existing data
await replaceUserSession(event, data)

// Get the current user session
const session = await getUserSession(event)

// Clear the current user session
await clearUserSession(event)

// Require a user session (send back 401 if no `user` key in session)
const session = await requireUserSession(event)

You can define the type for your user session by creating a type declaration file (for example, auth.d.ts) in your project to augment the UserSession type:

// auth.d.ts
declare module '#auth-utils' {
  interface User {
    // Add your own fields
  }

  interface UserSession {
    // Add your own fields
  }
}

export {}

OAuth Event Handlers

All helpers are exposed from the oauth global variable and can be used in your server routes or API routes.

The pattern is oauth.<provider>EventHandler({ onSuccess, config?, onError? }), example: oauth.githubEventHandler.

The helper returns an event handler that automatically redirects to the provider authorization page and then calls onSuccess or onError depending on the result.

The config can be defined directly from the runtimeConfig in your nuxt.config.ts:

export default defineNuxtConfig({
  runtimeConfig: {
    oauth: {
      <provider>: {
        clientId: '...',
        clientSecret: '...'
      }
    }
  }
})

It can also be set using environment variables:

  • NUXT_OAUTH_<PROVIDER>_CLIENT_ID
  • NUXT_OAUTH_<PROVIDER>_CLIENT_SECRET

Supported OAuth Providers

  • Auth0
  • AWS Cognito
  • Battle.net
  • Discord
  • Facebook
  • GitHub
  • Google
  • Keycloak
  • LinkedIn
  • Microsoft
  • Spotify
  • Twitch

You can add your favorite provider by creating a new file in src/runtime/server/lib/oauth/.

Example

Example: ~/server/routes/auth/github.get.ts

export default oauth.githubEventHandler({
  config: {
    emailRequired: true
  },
  async onSuccess(event, { user, tokens }) {
    await setUserSession(event, {
      user: {
        githubId: user.id
      }
    })
    return sendRedirect(event, '/')
  },
  // Optional, will return a json error and 401 status code by default
  onError(event, error) {
    console.error('GitHub OAuth error:', error)
    return sendRedirect(event, '/')
  },
})

Make sure to set the callback URL in your OAuth app settings as <your-domain>/auth/github.

Extend Session

We leverage hooks to let you extend the session data with your own data or log when the user clears the session.

// server/plugins/session.ts
export default defineNitroPlugin(() => {
  // Called when the session is fetched during SSR for the Vue composable (/api/_auth/session)
  // Or when we call useUserSession().fetch()
  sessionHooks.hook('fetch', async (session, event) => {
    // extend User Session by calling your database
    // or
    // throw createError({ ... }) if session is invalid for example
  })

  // Called when we call useServerSession().clear() or clearUserSession(event)
  sessionHooks.hook('clear', async (session, event) => {
    // Log that user logged out
  })
})

Configuration

We leverage runtimeConfig.session to give the defaults option to h3 useSession.

You can overwrite the options in your nuxt.config.ts:

export default defineNuxtConfig({
  modules: ['nuxt-auth-utils'],
  runtimeConfig: {
    session: {
      maxAge: 60 * 60 * 24 * 7 // 1 week
    }
  }
})

Our defaults are:

{
  name: 'nuxt-session',
  password: process.env.NUXT_SESSION_PASSWORD || '',
  cookie: {
    sameSite: 'lax'
  }
}

Checkout the SessionConfig for all options.

Development

# Install dependencies
npm install

# Generate type stubs
npm run dev:prepare

# Develop with the playground
npm run dev

# Build the playground
npm run dev:build

# Run ESLint
npm run lint

# Run Vitest
npm run test
npm run test:watch

# Release new version
npm run release

nuxt-auth-utils's People

Contributors

adam-hudak avatar ahmedrangel avatar aksharahegde avatar andreagroferreira avatar arashsheyda avatar atinux avatar azurency avatar berzinsu avatar brendonmatos avatar danielroe avatar dethdkn avatar diizzayy avatar dvh91 avatar gerbuuun avatar harlan-zw avatar jfrelik avatar justserdar avatar kingyue737 avatar leomo-27 avatar maximilianmikus avatar onmax avatar ozancakir avatar samulefevre avatar sifferhans avatar silvio-e avatar timibadass avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

nuxt-auth-utils's Issues

Strange issue, Auth0 always needs to be logged in twice repeatedly to be recognized properly

Problem Description

I'm using this module for Auth widgets, but I'm currently having a problem that the data is not updated in time. Here are a few examples:

  1. It always takes two logins to be released properly
  2. When exiting, a route protection is always triggered to show normal

devDependencies

"devDependencies": {
    "@hypernym/nuxt-anime": "^2.1.1",
    "@nuxt/devtools": "latest",
    "@nuxtjs/color-mode": "^3.3.2",
    "@nuxtjs/tailwindcss": "^6.10.1",
    "nuxt": "^3.9.0",
    "nuxt-auth-utils": "^0.0.13",
    "vue": "^3.3.12",
    "vue-router": "^4.2.5"
  }

Additional questions.

In node .output/server/index.mjs, you can never log in successfully

Usage with email/password - questions

Hey @Atinux, I am using this library and had a few questions if I can use this with a email/password setup.

I am using it as below

import { Argon2id } from "oslo/password";
import { useValidatedBody, z } from "h3-zod";
import { eq } from "drizzle-orm";

const validationSchema = z.object({
  email: z.string().email(),
  password: z.string(),
});

export default defineEventHandler(async (event) => {
  const { email, password } = await useValidatedBody(event, validationSchema);
  const db = useDB();
  const argon2id = new Argon2id();

  // Attempt to find the user by email
  const users = await db
    .select()
    .from(tables.users)
    .where(eq(tables.users.email, email));

  if (users.length === 0) {
    // No user found with the provided email
    throw createError({
      statusCode: 401,
      statusMessage: "Invalid email or password",
    });
  }

  const user = users[0];
  // Verify the password
  const validPassword = await argon2id.verify(user.password, password);

  if (!validPassword) {
    // Password does not match
    throw createError({
      statusCode: 401,
      statusMessage: "Invalid email or password",
    });
  }

  delete user.password;

  // Password matches, set the user session
  await setUserSession(event, { user });

  return user;
});

When I login in the client form and use the navigateTo composable, the session is set but in client it's still empty so my middle always redirects it back to the index page

<template>
  <div class="flex min-h-screen flex-1">
    <div
      class="flex flex-1 flex-col justify-center px-4 py-12 sm:px-6 lg:flex-none lg:px-20 xl:px-24"
    >
      <div class="mx-auto w-full max-w-sm lg:w-96">
        <div>
          <img class="h-10 w-auto" src="/logo.png" alt="Fullstack" />
          <h2
            class="mt-8 text-2xl font-bold leading-9 tracking-tight text-gray-900 font-display"
          >
            Sign in to your account
          </h2>
          <p class="mt-2 text-sm leading-6 text-gray-500">
            Not a member?
            {{ " " }}
            <a
              href="#"
              class="font-semibold text-primary-600 hover:text-primary-500"
              >Start a 14 day free trial</a
            >
          </p>
        </div>

        <div class="mt-10">
          <div>
            <UForm
              :schema="schema"
              :state="state"
              class="space-y-6"
              @submit="onSubmit"
            >
              <UFormGroup label="Email" name="email" size="xl">
                <UInput v-model="state.email" />
              </UFormGroup>
              <div class="relative">
                <NuxtLink
                  href="#"
                  class="text-gray-500 text-xs hover:text-gray-400 leading-6 absolute right-0 -top-1"
                  >Forgot password?</NuxtLink
                >
                <UFormGroup label="Password" name="password" size="xl">
                  <UInput v-model="state.password" type="password" />
                </UFormGroup>
              </div>

              <UButton
                type="submit"
                size="xl"
                label="Submit"
                block
                :loading="loading"
                :disabled="loading"
                color="black"
              />
            </UForm>
          </div>

          <div class="mt-10">
            <UDivider label="Or continue with" />

            <div class="mt-6 grid grid-cols-2 gap-4">
              <UButton
                to="/api/auth/google"
                external
                size="lg"
                color="white"
                block
              >
                <Icon class="h-5 w-5" name="i-logos-google-icon" />
                <span>Google</span>
              </UButton>
              <UButton
                to="/api/auth/github"
                external
                label="Github"
                icon="i-simple-icons-github"
                size="lg"
                color="white"
                block
              />
            </div>
          </div>
        </div>
      </div>
    </div>
    <div class="relative hidden w-0 flex-1 lg:block">
      <img
        class="absolute inset-0 h-full w-full object-cover"
        src="https://images.unsplash.com/photo-1496917756835-20cb06e75b4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1908&q=80"
        alt=""
      />
    </div>
  </div>
</template>

<script setup>
definePageMeta({ layout: "dashboard", colorMode: "light" });
import { z } from "zod";
const { loggedIn } = useUserSession();
const loading = ref(false);
const toast = useToast();
const schema = z.object({
  email: z.string().email(),
  password: z
    .string()
    .min(8, "Password must be at least 8 characters long")
    .regex(/[A-Z]/, "Password must include at least one uppercase letter")
    .regex(/[0-9]/, "Password must include at least one number")
    .regex(/[\W_]/, "Password must include at least one symbol"),
});

const state = reactive({
  email: undefined,
  password: undefined,
});

onMounted(async () => {
  await nextTick();
  if (loggedIn.value) navigateTo("/dashboard");
});

async function onSubmit(event) {
  try {
    loading.value = true;
    await $fetch("/api/auth/login", {
      method: "POST",
      body: state,
    });
    return navigateTo("/dashboard");
  } catch (error) {
    toast.add({
      title: "Error",
      description: error.statusMessage,
      color: "rose",
    });
    loading.value = false;
  }
}
</script>

Can you please guide me to use this in the right way?

The API (server routes) for getting and deleting sessions should be configurable

The API paths for getting and deleting sessions are hard-coded:

  • [get] api/_auth/session
  • [delete] api/_auth/session

The configurable APIs allow users to specify how they want to get and delete sessions.

Currently, in the module definition, information about the session and cookie, such as the name, password, and type of cookie, is read from the runtimeConfig.session. In the same way, the main path of the session API (for the get and delete methods) should be configurable, for example from module options.

email/username + password auth?

How can we extend this to support username+password auth?
I understand it may involve some kind of db configuration to store user data

Types not correctly imported

It seems like the types generated in .nuxt/types/nitro-imports.d.ts imports from the wrong files.
The utilities (setUserSession, getUserSession etc.) are being imported from the .mjs files as seen in the screenshot below.

CleanShot 2023-11-07 at 13 52 19

This causes everything to be typed as any, as in this githubEventHandler example:

CleanShot 2023-11-07 at 13 55 07

I guess this might have something to do with the way the utils are imported here?

CleanShot 2023-11-07 at 13 56 22

Microsoft OAuth Question about Access Token

@jfrelik ; thanks for the Microsoft provider. Works great to authenticate against MS Graph.

How do you use it to authenticate against your own API Application? I am stuck, as when I add the access_as_user scope in the microsoftEventHandler config section.

scope: ['api://{my_application}/access_as_user','openid','offline_access', 'profile','User.Read',], // USE AN ARRAY FOR YOUR SCOPES
this will allow me to call my own API, as only access_as_user will be included in scp of the access_token.

scope: ['openid','offline_access', 'profile','User.Read','api://{my_application}/access_as_user',], // USE AN ARRAY FOR YOUR SCOPES
this will log-me in correctly and allow me to call the grap, as all but the access_as_user scp is in the access token.

Do I need two access tokens? One to access the Graph, and one for my API?

Use HTTPS for redirect URIs

For example, when I use Google as my OAuth provider and deploy my app to the server, I get this error message when I try to log in with Google:

image

While I could just use the HTTP URI in Google Cloud Console, this is very unsafe and I think nuxt-auth-utils should use HTTPS if available.

(discord oauth2) Session state not persisting across page refreshes with Nuxt 3 and useUserSession only when deployed on Vercel

Hello !

The project's working in local development, it's only messing up when deployed on Vercel :
https://talent-hub.fr

You can even try it on in the website.

The redirect_uri is set, client_id / secret as well, everything is working, you can connect with Discord, but after the redirection, nothing seems persisted :
const {loggedIn, user, session, clear, logout} = useUserSession()

The loggedIn is not set and the navbar is like for an authenticated user.

However, there's a page secured by an auth middleware :
pages/Profile.vue :

<template>
</template>
<script lang="ts" setup>
definePageMeta({
    middleware: 'auth'
})
</script>

With the middleware :

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

    if (!loggedIn.value) {
        return navigateTo('/')
    }
})

When navigating, you can access it, and when you refresh, you could see that's the navbar is working as expected.

But once you comeback to the home page, and refresh, everything disappear, and when coming back again on profile page, everythink works.

It might be silly as I'm using nuxt-auth-utils for the first time.

GitHub: https://github.com/TalentHubProject/monolith

(It's the talent-hub_website nuxtjs3 project)

Regards;

[Google] Not possible to provide extra config for authorization

When authorizing with google, these params are hardcoded

response_type: "code",
client_id: config.clientId,
redirect_uri: redirectUrl,
scope: config.scope.join(" ")

However, several other options can be provided here. See https://developers.google.com/identity/protocols/oauth2/web-server#httprest_3

What is currently blocking me is access_type that allows you to specify offline, which is the only way to get a refresh_token, which is the only way to keep the user logged in for more than 1 hour (when they need to access Google APIs).

Maybe an option like useRuntimeConfig(event).oauth.google.authorizationParams

config: {
    redirectUrl: '/auth/google',
    scope: [
      'https://www.googleapis.com/auth/userinfo.email',
    ],
    authorizationParams: {
      prompt: 'consent',
      access_type: 'offline',
    },
  },

Why only server side

Thanks a lot Atinux for the great work!

This module is very clean and easy to understand!

My question is what are the reasons for which it is only server-side, by looking at the endpoints it seems that code could easily be in pages or composables, so, why the limitation?

[Question]: When is the session server side available? Initial authorized api request possible?

I call my endpoint /api/countries which is doing a request to an external API which needs info from the session. That works fine.

But when I call my api route on the initial loading of the app, for example in app.js in a callOnce, it seems that the session is not available yet.

// This seems not to work

await callOnce(async () => {
  const result = await $fetch('/api/countries')
})

Am I doing something wrong or is this correct behavior?

The idea was to fetch data and filling the store on app load. How can this be achieved with using a token from the authentication?

[Question] Expose/access the tokens

Hi
First, thanks for the great work

Currently, I'm working on a POC to migrate our Vue 3 SSR app to Nuxt 3 and I'm trying a few libs to replace the express-openid-connect that we are using to manage the authentication.

The question is that on each request to our APIs I need to send the token and also check if the token is still valid.
From what I see from the example, this is only done during the authentication process.

Is it possible to access/expose the tokens that are returned at onSuccess from

const tokens: any = await ofetch(
tokenURL as string,
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: {
grant_type: 'authorization_code',
client_id: config.clientId,
client_secret: config.clientSecret,
redirect_uri: parsePath(redirectUrl).pathname,
code,
}
}
).catch(error => {
return { error }
})
if (tokens.error) {
const error = createError({
statusCode: 401,
message: `Auth0 login failed: ${tokens.error?.data?.error_description || 'Unknown error'}`,
data: tokens
})
if (!onError) throw error
return onError(event, error)
}
const tokenType = tokens.token_type
const accessToken = tokens.access_token
const user: any = await ofetch(`https://${config.domain}/userinfo`, {
headers: {
Authorization: `${tokenType} ${accessToken}`
}
})
return onSuccess(event, {
tokens,
user
})

Thanks

PS: Maybe it would be great to have a "Discussions" page for this kind of questions

Is session refresh implemented?

I saw the "offline_access" scope being used for the OAuth0 provider but no reference to refresh tokens in the codebase. Are refresh tokens implemented/utilized? Or is the session from the OAuth2 provider only used once and afterwards everything is delegated to h3?

Nuxt UI Pro license required for building the playground

when running npm run dev:build I get the following error:

ERROR  You must provide the NUXT_UI_PRO_LICENSE environment variable.                                                                                   3:12:14 PM
 Purchase Nuxt UI Pro at https://ui.nuxt.com/pro/purchase to build your app for production.

I have set:
NODE_ENV="development"
but it does not help

I don't want to deploy anything to production so I should not need the license, correct? Can I set some ENV variable to make Nuxt UI Pro recognize this?

Is Laravel Sanctum as a Provider on the roadmap?

Was curious if Laravel Sanctum is on the roadmap to becoming a provider. I currently use Nuxt Auth for my Nuxt2 Applications and really enjoyed how simple that was to get Laravel Sanctum working with. Have recently started several new Nuxt3 projects with a Laravel backend.

Any info would be great, Thanks!

bug: Session Fails to Set When Exceeding Data Size Limit

Description:

While implementing session management with the nuxt-auth-utils library, I encountered a very hard to debug issue where the session fails to set/update with a slightly large amount of data.

How did I discover this?

One of the user entered a large amount of data to my users table field called "bio", the next time they logged in this data was getting included in the session, I have now removed this added a limit to this field. This kept breaking auth flow because the session was never getting set, and always returned a 401 when this specific user tried logging in, this can be handled in the setUserSession method

Steps to Reproduce:

  1. Initialize a session for a user with a minimal amount of data.
  2. The next time they login, set the session with additional data, gradually increasing the total size.
  3. Observe the behavior of the session update functionality as the total session data size approaches and exceeds 4KB (max cookie size)

Suggested Remedies:

  • Throw an error stating the cookie is getting too large, instead of continuing, made this quite hard to figure out the issue

Environment:

nuxt-auth-utils Version: ^0.0.22
Nuxt Version: 3.11.2
Browser/Node.js Version: Chrome 123.0.6312.107
Platform: Macos Sonoma

Create OAuth provider in userland code

I'm curious about the possibility of writing an OAuth provider in userland code. I'm currently interacting with one that doesn't seem to justify inclusion in this library due to its limited usage within a small region. Can anyone provide insights on how this can be achieved? Thank you!

Discord oauth error

Using the discrod provider I get this error in the terminal:

 error: FetchError: [POST] "https://discord.com/api/oauth2/token": 400 Bad Request

I think this could be a problem:

Correct generated URL (from discord/developers )is:

https://discord.com/api/oauth2/authorize?client_id=CLIENTID&response_type=code&redirect_uri=https%3A%2F%2F199.254.176.236%3A3000%2Fapi%2Fauth%2Fdiscord&scope=identify

But from discord oauth is generated:

https://discord.com/oauth2/authorize?response_type=code&client_id=CLIENTID&redirect_uri=http://199.254.176.236:3000/api/auth/discord&scope=identify

Only expose public data part of session

(context: question by @harlan-zw in nuxt discord regarding reliablilty of sessions and weather he should use storage to keep private session data)

H3 sessions are encrypted and only readable by server-side. This can guarantee two things:

  • Only server can mutate session so it's data is reliable
  • Only server can read/decrypt session so it's data is private

Auth-utils, exposes an edpoint (session.get) that server-side decrypts the session for user. It takes away the second benefit of session encoding which can guarantee data remains secret and private.

While there must be good benefits of this, it is something IMO insecure to do by default and developers might wrongly put sensitive data based on encryption guarantee that will be exposed again.

I would highly recommend (as a breaking change) to only expose data.public part of the session.

`setUserSession()` is merging data

The naming of the function setUserSession suggest you can set data to what you provide as an argument. In fact new data is always merged with old session data.

/**
 * Set a user session
 * @param event
 * @param data User session data, please only store public information since it can be decoded with API calls
 */
export async function setUserSession (event: H3Event, data: UserSession) {
  const session = await _useSession(event)

  await session.update(defu(data, session.data))

  return session.data
}

defu(data, session.data) merges data deeply. If you want to actually delete data, you cannot do it. In addition to this session.update will perform an object.assign call (shallow merge).

I think we should have different functions for setting and updating(merging) session data.

Facebook Login

Hey,

are there plans to integrate facebook as a provider?

Configure session expiration?

The cookie created with setUserSession is configured to expire with the browser session.
How do I persist the cookie for a longer time?

Add option for additional query parameters

Would be great if the config would have a key like authorization_params where we can add any optional parameter the provider expects.

In my example I would like to add ?access_type=offline to the authorizationURL for Google (So that it returns also the refresh token). Having one general authorization_params object would avoid having to add different parameters for each provider manually.

Ideally I would have something like:

export default oauth.googleEventHandler({
	config:{
		authorization_params: {  // Something like this would work perfectly
		    access_type:"offline"
		},
		scope: [
			'https://www.googleapis.com/auth/userinfo.email',
			'https://www.googleapis.com/auth/tasks.readonly' // This is great!
		]
	},
	async onSuccess(event, { user, tokens }) {
		console.log(tokens.refresh_token) // Save this in DB
		await setUserSession(event, { user, loggedInAt: Date.now() })
	  	return sendRedirect(event, '/')
	}
})

From my understanding this is currently impossible, right?

Max key length?

Hey,

I'm trying to store the auth token (jwt) in the session (to pass it in the header for fetch)

This works

await setUserSession(event, {
    user,
    loggedInAt: Date.now(),
    thing:	'isathing',
});

This doesn't work

await setUserSession(event, {
    user,
    loggedInAt: Date.now(),
    thing: 'nwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2osnwnn10eyu9mfeqnf1j1ry4gdagcyv2os',
});

There's no explicit error but my guess is that it's related to the length
Either there's a bug or i'm not supposed to store the auth token here?

Thanks for the help (and thanks for building this module!)

Does this module work in an SPA setting?

I've already seen #9 but wanted to confirm because the requirements highlight:

You cannot use this module with nuxt generate.

Does this mean we can use this module in a Nuxt project where the landing page runs with SSR but the logged in pages are an SPA? Or does it mean the whole auth flow must happen within an SSR context?

I've tried to migrate to this module in my SaaS starter template but it seems it cannot find the user session if I redirect to an SPA page after log in, although if I refresh the page, it returns the session fine.

Utils does not work with Azure SWA

We are trying to use this with Azure Static Web App, but the redirect url is wrong.

From what i can see it gets the redirectUrl from getRequestUrl(event).href, this does not work with SWA since the hostname refers to an internal url for the function app.

You can see an example output here:
https://ambitious-dune-01c6e3503-preview.westeurope.4.azurestaticapps.net/api/info

Not sure what the best approach for this would be. X-Forward-For header contains only IP Adresse (i assume this is internal infrastructure at Microsoft). SWA does have a special header called 'x-ms-original-url' that contains the original url, it seems very specific for SWA and would only apply to apps running in Azure SWA.

One possible solution would be to allow a configured redirectUrl (but its not the best solution IMO, in case we have multiple domains pointing to the same instance), another would be to create a custom handler for apps running in SWA.

Any suggestions on the best way to handle this?

Prefer Single Exports Over `oauth`

Currently, users will set up their endpoints by accessing one of the oauth.*.

export const oauth = {
  githubEventHandler,
  spotifyEventHandler,
  googleEventHandler,
  twitchEventHandler,
  auth0EventHandler,
  microsoftEventHandler,
  discordEventHandler,
  battledotnetEventHandler,
  keycloakEventHandler,
  linkedinEventHandler,
  cognitoEventHandler
};

This works and the auto-completion is nice but I think it can be improved:

  • treeshaking afaik won't work as all providers are within the same const, meaning we will bundle all providers (which will grow)
  • oauth seems likely for collisions in global import namespace

Exporting each provider as its own function should fix tree shaking and namespace problems, i.e authGoogleEventHandler where autocompletion should be good.

Add more scopes on the google oath

Im try to use the youtube service and i try to add the some more scopes like youtube.readonly, etc... When I did this I have an invalid_scope error. Do you have any solution or something to advice?

Some requested scopes were invalid. {valid=[https://www.googleapis.com/auth/userinfo.email, https://www.googleapis.com/auth/userinfo.profile], invalid=[youtube.readonly]} 

Better typing of UserSession

My problem

It has been bugging me for a while that the types of this module do not work at all.

Lets say this is your auth.d.ts:

declare module '#auth-utils' {
  interface UserSession {
    user: {
      id: number
      name: string
      email: string
    }
    loggedInAt: number
  }
}

On the client-side, all properties of the useUserSession composable are of type any (only in the playground it works)

Scherm­afbeelding 2024-01-16 om 14 00 54

On the server-side, UserSession is defined except for the user object.
Its type will always be {} | undefined regardless of what is specified in the auth.d.ts (see issue #31 )
And when checking for a session using requireUserSession the returned user object is still possibly undefined even though we just checked and required it to not be.

I have been tinkering and came up with the following:

Instead of looking for a specific property in the session data (user in this case), we check if there are any properties at all.

export async function requireUserSession(event: H3Event) {
  const userSession = await getUserSession(event)

  if (Object.keys(userSession).length === 0) {
    throw createError({
      statusCode: 401,
      message: 'Unauthorized'
    })
  }

  return userSession
}

This way you can define anything in the UserSession interface and the type works (this uses the playground's auth.d.ts)

Scherm­afbeelding 2024-01-16 om 14 22 30

On the client-side I made the following changes:

  • loggedIn is now computed based on wether there exist properties on the session object
  • There is no longer a user getter as this property is no longer defined but now the typing is at least correct.
const useSessionState = () => useState<UserSession>('nuxt-session', () => ({}))

export const useUserSession = () => {
  const sessionState = useSessionState()
  return {
    loggedIn: computed(() => Object.keys(sessionState.value).length > 0),
    session: sessionState,
    fetch,
    clear
  }
}

I would like to hear feedback from you guys

Mixed use of ofetch and $fetch

While reading the codebase (to potentially implement #89, most likely based on the existing Keycloak provider) I noticed that some places use ofetch and others use $fetch. Is there a specific reason for this? Otherwise it might be wise to unify them and use one of the two consistently throughout the codebase.

Mocking providers for E2E Testing

I'm looking to create E2E test scenarios that include the login of an application. Currently, I'm using Playwright, but it cannot mock server-side calls, unfortunately.

I'm working on a workaround by implementing an oauth2 mock server that accepts the contracts as Google, but, there is any other suggestion to make it work, or do you see any improvement that can be implemented to ease that kind of usage? Seems that approach is kind hacky.

Support Dynamic Config

Sometimes we won't know ahead of time the exact config to use for an oauth request, it would be nice if we could provide config as a function that takes the event property.

It's related to this issue: #48.

Bit edge case but I think it's a fairly simple change that provides much more flexibility.

config(event) {
  // do logic   
},

Discord Always Fails

Hello, I was giving discord a try:

{
    label: 'Discord',
    icon: 'i-simple-icons-discord',
    color: 'indigo',
    click: () => {
      loading.value = true
      window.location.href = '/api/auth/discord'
    }
  }

Inside of ~/server/api/auth/discord.get.ts I have

export default oauth.discordEventHandler({
config: {
  emailRequired: true,
  profileRequired: true,
},
async onSuccess(event, { user, tokens }) {
  console.log('Discord OAuth success:', user)
  await setUserSession(event, {
    user
  })
  return sendRedirect(event, '/dashboard')
},
// Optional, will return a json error and 401 status code by default
onError(event, error) {
  console.error('Discord OAuth error:', event, error)
  return sendRedirect(event, '/')
},
})

I am able to load the discord authorization screen, but when it performs the callback, I get an error:

Discord OAuth error: H3Event {
  __is_event__: true,
  node:
   { req:
      IncomingMessage {
        _events: [Object],
        _readableState: [ReadableState],
        _maxListeners: undefined,
        socket: [Socket],
        httpVersionMajor: 1,
        httpVersionMinor: 1,
        httpVersion: '1.1',
        complete: true,
        rawHeaders: [Array],
        rawTrailers: [],
        joinDuplicateHeaders: null,
        aborted: false,
        upgrade: false,
        url: '/api/auth/discord?code=04yK3Z0BVKV2B83O3UVZooaTZa2G0f',
        method: 'GET',
        statusCode: null,
        statusMessage: null,
        client: [Socket],
        _consuming: false,
        _dumped: false,
        originalUrl: '/api/auth/discord?code=04yK3Z0BVKV2B83O3UVZooaTZa2G0f',
        [Symbol(shapeMode)]: true,
        [Symbol(kCapture)]: false,
        [Symbol(kHeaders)]: [Object],
        [Symbol(kHeadersCount)]: 36,
        [Symbol(kTrailers)]: null,
        [Symbol(kTrailersCount)]: 0 },
     res:
      ServerResponse {
        _events: [Object: null prototype],
        _eventsCount: 1,
        _maxListeners: undefined,
        outputData: [],
        outputSize: 0,
        writable: true,
        destroyed: false,
        _last: false,
        chunkedEncoding: false,
        shouldKeepAlive: false,
        maxRequestsOnConnectionReached: false,
        _defaultKeepAlive: true,
        useChunkedEncodingByDefault: true,
        sendDate: true,
        _removedConnection: false,
        _removedContLen: false,
        _removedTE: false,
        strictContentLength: false,
        _contentLength: null,
        _hasBody: true,
        _trailer: '',
        finished: false,
        _headerSent: false,
        _closed: false,
        _header: null,
        _keepAliveTimeout: 5000,
        _onPendingData: [Function: bound updateOutgoingData],
        req: [IncomingMessage],
        _sent100: false,
        _expect_continue: false,
        _maxRequestsPerSocket: 0,
        [Symbol(shapeMode)]: false,
        [Symbol(kCapture)]: false,
        [Symbol(kBytesWritten)]: 0,
        [Symbol(kNeedDrain)]: false,
        [Symbol(corked)]: 0,
        [Symbol(kChunkedBuffer)]: [],
        [Symbol(kChunkedLength)]: 0,
        [Symbol(kSocket)]: [Socket],
        [Symbol(kOutHeaders)]: null,
        [Symbol(errored)]: null,
        [Symbol(kHighWaterMark)]: 16384,
        [Symbol(kRejectNonStandardBodyWrites)]: false,
        [Symbol(kUniqueHeaders)]: null } },
  web: undefined,
  context:
   { _nitro: { routeRules: {} },
     nitro: { errors: [], runtimeConfig: [Object] },
     matchedRoute: { path: '/api/auth/discord', handlers: [Object] },
     params: {} },
  _method: 'GET',
  _path: '/api/auth/discord?code=04yK3Z0BVKV2B83O3UVZooaTZa2G0f',
  _headers: undefined,
  _requestBody: undefined,
  _handled: false,
  fetch: [Function (anonymous)],
  '$fetch': [Function (anonymous)],
  waitUntil: [Function (anonymous)],
  captureError: [Function (anonymous)] } Discord login failed: Unknown error

Any ideas?

How to block/suspend users?

Not a problem, but do you have any suggestions for blocking users by invalidating all tokens and preventing future sign-ins in a clean way?

Support for OIDC providers which expose `.well-known/openid-configuration`

I was sad to see that the closest thing currently available is the Keycloak provider however that mandates usage of a realm. Many OIDC providers providers, especially FOSS ones which you can self-host provide a standardized well-known/openid-configuration endpoint where all further endpoints and supported values are exposed. It would be great to support this and would eliminate the need for many specialized providers. Ideally, there is a general manualOIDC provider where one can manually set the authorization/token/userinfo/revokation etc endpoint and one wellKnownOIDC provider which simply takes a single URL, fetches the values and delegates the rest to the manualOIDC provider.

[question] Working in a SPA

Congratulations on the release of this awesome module. I have been waiting for it since I tried h3's sessions.

Question: How to make this work in a SPA (ssr: false in nuxt.config.ts)? Thank you.

204 No Content on Cloudflare Pages

Hey folks, thanks for the awesome library. My github oauth login is working on localhost:3000 just fine.

But when I deploy to CF Pages, nothing seems to happen when I use window.location.href = '/api/auth/github

Screen Shot 2024-04-13 at 12 53 23 PM

Cloudflare seems to be returning 204 no content, it's like I'm never hitting this route. I can see the github token has never been accessed. Any ideas? Seems like infrastructure configuration, but I can't figure it out.

I am not using nuxt generate, and my server API calls are working fine: https://minitinkertown.com/api/hello returns JSON

Cannot use clear in middleware

I have a global middleware that looks something like this.

const { loggedIn, user, clear } = useUserSession()

// If user is logged in but permissions are not loaded, then force the user to re-login
if (loggedIn?.value && !user?.value?.permissions) {
  await clear()
  return navigateTo(`/login`)
}

Calling the clear method during SSR results in the following error.

Feb 26 14:27:09 xxx.com node[152599]: Error clearing user session Error: [nuxt] instance unavailable
Feb 26 14:27:09 xxx.com node[152599]: at useNuxtApp (file:///xxx/.output/server/chunks/app/server.mjs:1503:13)
Feb 26 14:27:09 xxx.com node[152599]: at useState (file:///xxx/.output/server/chunks/app/server.mjs:2104:35)
Feb 26 14:27:09 xxx.com node[152599]: at useSessionState (file:///xxx/.output/server/chunks/app/server.mjs:2140:31)
Feb 26 14:27:09 xxx.com node[152599]: at clear (file:///xxx/.output/server/chunks/app/server.mjs:2160:3)
Feb 26 14:27:09 xxx.com node[152599]: at async file:///xxx/.output/server/chunks/app/server.mjs:2191:59
Feb 26 14:27:09 xxx.com node[152599]: at async Object.callAsync (file:///xxx/.output/server/chunks/app/server.mjs:90:16)
Feb 26 14:27:09 xxx.com node[152599]: at async file:///xxx/.output/server/chunks/app/server.mjs:2336:26

Navigation to provider route SSR issue

Hello Nuxters/Vuetists 🔥💚,

I was playing for hours to solve my issue.
If I use useFetch('/api/auth/github', {redirect: 'follow'}), I get error:

(redirected from 'http://localhost:3000/api/auth/github') from origin 'http://localhost:3000' has been blocked by CORS policy: No 'vi-tr.js:1 
        
        
       GET https://github.com/login/oauth/authorize?client_id=*****&redirect_uri=http://localhost:3000/api/auth/github&scope=user:email net::ERR_FAILED 302 (Found)
login:1 Access to fetch at 'https://github.com/login/oauth/authorize?client_id=*****&redirect_uri=http://localhost:3000/api/auth/github&scope=user:email' (redirected from 'http://localhost:3000/api/auth/github') from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled..

Then if I use navigateTo('/api/auth/github'):

image

And when I refresh the page with url http://localhost:3000/api/auth/github, then it redirects to Github login page.

So my only options are:

  1. navigateTo('/api/auth/github', {open: {target: '_self'}}):
  2. window.location.href = '/api/auth/github'

Wanna read your thoughts. 👋

Impossible to login using Safari with localhost

I tried to use nuxt-auth-utils with Safari on macOS 14.4.1, but I've never been able to get it to work.

The cookie nuxt-session is never received on Safari, but it's working on Arc.

To reproduce:

  • I use the playground from this repository or nuxt-todos-edge
  • Put variables in the .env, I tried with Github and Auth0 providers: same results
  • Use Safari, log in on the provider page like Github or Auth0
  • The redirection works correctly to http://localhost:3000/auth/auth0 for example
  • The code in auth0.get.ts is executed without error. If I log user from the onSuccess event handler, I get the information like expected.
  • But client side, I never get the nuxt-session cookie

But I can get https://todos.nuxt.dev/ works on Safari.

Is there anything I'm missing?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.