sidebase / nuxt-auth

Authentication built for Nuxt 3! Easily add authentication via OAuth providers, credentials or Email Magic URLs!
https://auth.sidebase.io
MIT License
1.31k stars 164 forks source link

Local provider: Authorization not being added to requests #436

Closed thommydz closed 1 year ago

thommydz commented 1 year ago

Environment

Reproduction

Setup in nuxt.conifg.ts:

auth: {
        globalAppMiddleware: true,
        baseURL:process.env.API_URL,
        sessionDataType: {
            id: 'string | number',
            authorisation: {
                token: 'string',
                type: 'string'
            }
        },
        provider: {
            type: 'local',
            endpoints: {
                signIn: { path: '/login', method: 'post' },
                signOut: { path: '/logout', method: 'post' },
                signUp: { path: '/register', method: 'post' },
                getSession: { path: '/user', method: 'get' }
            },
            pages: {
                login: '/login'
            },
            token: {
                signInResponseTokenPointer: '/authorisation/token',
                type: 'Bearer',
                headerName: "Authorization",
                maxAgeInSeconds: 60 * 15
            }
        }
},

I am talking to a Laravel 9 API with php-open-source-saver/jwt-auth for auth.

My API response after login looks like this:

{
"status":"success",
    "user":{
        "id":1,
        "username":"thommy",
        "email":"xx@xx",
        "confirmation_code":"xxx",
        "confirmed":0,
        "locale":"nl"
     },
     "authorisation": {
        "token":"xxx",
        "type":"bearer"
     }
}

My API response for the getSession endpoint is the same as the response above.

Describe the bug

I have auth setup and it's working. I can login a user with the local provider, which is pointed to my API that works with JWT. The token gets stored in a cookie and in my application I have everything related to auth working as expected.

However, one thing that does not work, is the Authorization header being added to my API calls.

When I use the "signIn" endpoint and I successfully login automatically the getSession endpoint gets called. In that call, I see the Authorization as expected.

Unfortunately, that's the only call where this header gets added. I would expect the header to be added to all my calls if a user is authenticated.

Is this expected behavior? Or is something going wrong?

Of course I would be able te create some middleware to add the header manually, but I would expect the module to do this automatically.

Additional context

No response

Logs

No response

romulomanzano commented 1 year ago

Having the same issue here. Seems like this should be a basic functionality. Upgrading from nuxt2 to 3, and finding the gaps between this and https://auth.nuxtjs.org/ challenging.

thommydz commented 1 year ago

I ended up building auth myself. For this use case SideBase is just not ready for production yet.

I now store the auth token in a cookie, check auth through a middleware, have a composable that adds the auth header to my requests and I remove the cookie when a user logs out.

vishnug1995 commented 1 year ago

I ended up building auth myself. For this use case SideBase is just not ready for production yet.

I now store the auth token in a cookie, check auth through a middleware, have a composable that adds the auth header to my requests and I remove the cookie when a user logs out.

Can you give me the code snippet

thommydz commented 1 year ago

The code related to auth is implemented in multiple places throughout my application, but most important ones:

Login function (expects API to return user and Auth token)

    async login(userdata) {
      this.loading = true;
      const { data, error } = await useCustomFetch<object>('/login',  {
        method: 'post',
        body: userdata,
      })
      this.loading = false;
      if (error && error.value) {
        this.account.errors.request = error.value.statusMessage;
      } else {
        this.auth.loggedIn = true; 
        this.auth.user = data.value.user;
        this.auth.token = data.value.authorisation.token;
        const authcookie = useCookie('auth:token', { maxAge: 24 * 60 * 60 });
        authcookie.value = data.value.authorisation.token;
      }
    },

getUser endpoint (expects API to return user and Auth token)

    async getUser(userdata) {
      this.loading = true;
      const { data, error } = await useCustomFetch <object>('/user',  {
        method: 'get'
      })
      this.loading = false;
      if (error && error.value) {
        const authcookie = useCookie('auth:token');
        authcookie.value = null;
        self.auth.loggedIn = false;
        self.auth.user = false;
        self.auth.token = false;
      } else {
        this.auth.loggedIn = true; 
        this.auth.user = data.value.user;
        this.auth.token = data.value.authorisation.token;
      }
    },

Middleware that checks for Auth cookie:

import { storeToRefs } from 'pinia';
import { useAccountStore } from '~/stores/accountStore';
export default defineNuxtRouteMiddleware(async () => {
    const accountStore = useAccountStore();
    const { auth } = storeToRefs(accountStore);
    const authcookie = useCookie('auth:token');
    if(authcookie.value && !auth.value.loggedIn) {
        await accountStore.getUser();
    }
})

Middleware that redirects if user is not loggedIn

export default defineNuxtRouteMiddleware(async ({ to, from, next }) => {
  const router = useRouter();
  const authcookie = useCookie('auth:token');

  if (!authcookie.value) {
    await router.replace('/login');
  }
});

Composable for custom Fetch to add headers to my request:

import type { UseFetchOptions } from 'nuxt/app'
import { defu } from 'defu'

export function useCustomFetch<T> (url: string, options: UseFetchOptions<T> = {}) {
  const config = useRuntimeConfig()
  const api_url = config.public['api_url'];
  const credentials = config.public['credentials'];
  const origin = config.public['origin'];
  const authcookie = useCookie('auth:token');
  const defaults: UseFetchOptions<T> = {
    baseURL: api_url ?? 'https://api.nuxtjs.dev',
    // cache request
    key: url,

    // set user token if user is authenticated (authcookie is set)
    headers: {
      Authorization: authcookie.value ? `Bearer ${authcookie.value}` : '',
      credentials: credentials,
      origin:origin,
      Accept : 'application/json'
    },
    method: options.method || 'GET',

    onResponse (_ctx) {
      // _ctx.response._data = new myBusinessResponse(_ctx.response._data)
    },

    onResponseError (_ctx) {
      throw createError({ statusCode: _ctx.response.status, statusMessage: _ctx.response._data.message })
    }
  }
  // for nice deep defaults, please use unjs/defu
  const params = defu(options, defaults)

  return useFetch(url, params)
}
vishnug1995 commented 1 year ago

The code related to auth is implemented in multiple places throughout my application, but most important ones:

Login function (expects API to return user and Auth token)

    async login(userdata) {
      this.loading = true;
      const { data, error } = await useCustomFetch<object>('/login',  {
        method: 'post',
        body: userdata,
      })
      this.loading = false;
      if (error && error.value) {
        this.account.errors.request = error.value.statusMessage;
      } else {
        this.auth.loggedIn = true; 
        this.auth.user = data.value.user;
        this.auth.token = data.value.authorisation.token;
        const authcookie = useCookie('auth:token', { maxAge: 24 * 60 * 60 });
        authcookie.value = data.value.authorisation.token;
      }
    },

getUser endpoint (expects API to return user and Auth token)

    async getUser(userdata) {
      this.loading = true;
      const { data, error } = await useCustomFetch <object>('/user',  {
        method: 'get'
      })
      this.loading = false;
      if (error && error.value) {
        const authcookie = useCookie('auth:token');
        authcookie.value = null;
        self.auth.loggedIn = false;
        self.auth.user = false;
        self.auth.token = false;
      } else {
        this.auth.loggedIn = true; 
        this.auth.user = data.value.user;
        this.auth.token = data.value.authorisation.token;
      }
    },

Middleware that checks for Auth cookie:

import { storeToRefs } from 'pinia';
import { useAccountStore } from '~/stores/accountStore';
export default defineNuxtRouteMiddleware(async () => {
  const accountStore = useAccountStore();
  const { auth } = storeToRefs(accountStore);
  const authcookie = useCookie('auth:token');
  if(authcookie.value && !auth.value.loggedIn) {
      await accountStore.getUser();
  }
})

Middleware that redirects if user is not loggedIn

export default defineNuxtRouteMiddleware(async ({ to, from, next }) => {
  const router = useRouter();
  const authcookie = useCookie('auth:token');

  if (!authcookie.value) {
    await router.replace('/login');
  }
});

Composable for custom Fetch to add headers to my request:

import type { UseFetchOptions } from 'nuxt/app'
import { defu } from 'defu'

export function useSyseroFetch<T> (url: string, options: UseFetchOptions<T> = {}) {
  const config = useRuntimeConfig()
  const api_url = config.public['api_url'];
  const credentials = config.public['credentials'];
  const origin = config.public['origin'];
  const authcookie = useCookie('auth:token');
  const defaults: UseFetchOptions<T> = {
    baseURL: api_url ?? 'https://api.nuxtjs.dev',
    // cache request
    key: url,

    // set user token if user is authenticated (authcookie is set)
    headers: {
      Authorization: authcookie.value ? `Bearer ${authcookie.value}` : '',
      credentials: credentials,
      origin:origin,
      Accept : 'application/json'
    },
    method: options.method || 'GET',

    onResponse (_ctx) {
      // _ctx.response._data = new myBusinessResponse(_ctx.response._data)
    },

    onResponseError (_ctx) {
      throw createError({ statusCode: _ctx.response.status, statusMessage: _ctx.response._data.message })
    }
  }
  // for nice deep defaults, please use unjs/defu
  const params = defu(options, defaults)

  return useFetch(url, params)
}

Thank you ...

Do you have a refresh token functionality in your app?

thommydz commented 1 year ago

Not yet. I am planning on adding that. My API already has is so it's just syncing the the valid time for the tokens and creating the endpoint to refresh when it's needed.

I can let you know as soon as I added it.

andreasvirkus commented 1 year ago

@thommydz did you add it?

thommydz commented 1 year ago

@thommydz did you add it?

I am actually working on it right now. Will reply with the code snippets here as soon as possible.

thommydz commented 1 year ago

Ok so the refresh functionality. First it's important to say that my Nuxt3 frontend talks to a Laravel API. That API has JWT installed and set up.

In my API I have these endpoints for auth:

Route::post('/register', [AuthController::class, 'register']);
Route::post('/login', [AuthController::class, 'login']);
Route::post('/refresh', [AuthController::class, 'refresh']);
Route::post('/logout', [AuthController::class, 'logout']);

Also I have endpoints that are wrapped inside Route::middleware(['auth:api']). Those are endpoints that should only be accessible if a user is loggedIn.

For testing purposes, in my API I have set these to variables in my .env file:

JWT_TTL=1
JWT_REFRESH_TTL=6000

Now on my Nuxt3 frontend I expect this:

To get this all working in Nuxt 3 I've updated my composable for custom Fetch:

import type { UseFetchOptions } from 'nuxt/app'
import { useAccountStore } from '~/stores/accountStore';
import { defu } from 'defu'

export function useCustomFetch<T> (url: string, options: UseFetchOptions<T> = {}, skipRefresh = false) {
  const config = useRuntimeConfig()
  const api_url = config.public['api_url'];
  const credentials = config.public['credentials'];
  const origin = config.public['origin'];
  const authcookie = useCookie('auth:token');
  const accountStore = useAccountStore();
  const defaults: UseFetchOptions<T> = {
    baseURL: api_url ?? 'https://api.nuxtjs.dev',
    // cache request
    key: url,

    // set user token if user is authenticated (authcookie is set)
    headers: {
      Authorization: authcookie.value ? `Bearer ${authcookie.value}` : '',
      credentials: credentials,
      origin:origin,
      Accept : 'application/json'
    },
    method: options.method || 'GET',

    onResponse (_ctx) {
      // _ctx.response._data = new myBusinessResponse(_ctx.response._data)
    },

    async onResponseError(_ctx) {
      if (_ctx.response.status === 401 && !skipRefresh) {
        const refreshSuccess = await accountStore.refresh();
        if (refreshSuccess) {
          const updatedAuthCookie = useCookie('auth:token');
          defaults.headers.Authorization = updatedAuthCookie.value ? `Bearer ${updatedAuthCookie.value}` : '';

          const updatedParams = defu(options, defaults);
          return useFetch(url, updatedParams);
        } else {
          await accountStore.logout();
          throw createError({ statusCode: 401, statusMessage: "Unauthorized after refresh attempt" });
        }
      } else {
        throw createError({ statusCode: _ctx.response.status, statusMessage: _ctx.response._data.message });
      }
    }

  }
  // for nice deep defaults, please use unjs/defu
  const params = defu(options, defaults)

  return useFetch(url, params)
}

Most important changes:

Then the accountStore. That now has the refresh function:

    async refresh() {
      const { data, error } = await useSyseroFetch<object>('/refresh',  {
        method: 'post'
      }, true)
      if (error && error.value) {
        this.account.errors.request = error.value.statusMessage;
        return false;
      } else {
        this.auth.loggedIn = true; 
        this.auth.user = data.value.user;
        this.auth.token = data.value.authorisation.token;
        const authcookie = useCookie('auth:token', { maxAge: 24 * 60 * 60 });
        authcookie.value = data.value.authorisation.token;
        return true;
      }
    },
wildy13 commented 1 year ago

gusys, can i askk you something? when i success login to dashboard. and i refresh it, it redirect to login again again and again.

export default defineNuxtConfig({

app: {

  head: {
    titleTemplate: "Company Profile",
    meta: [
      { charset: "utf-8" },
      { name: "viewport", content: "width=device-width, initial-scale=1" },
      { name: "description", content: "Meta description" },
    ],
  },
},

devtools: { enabled: true },
modules: ["@nuxt/ui", "@sidebase/nuxt-auth"],  

runtimeConfig: {
  public: {
    apiUrl: '',
  },
},

auth: {
  origin: process.env.ORIGIN,
  baseURL: process.env.BACKEND_URL,
  provider: {
    type: 'local',
    sessionDataType: {
      id: 'string',
      username: 'string',
    },
    pages:{
      login: "/auth/login/"
    },
    endpoints: {
      signIn: {
        path: "/api/auth/login",
        method: "post"
      },
      signOut: {
        path: "/logout",  
        method: "post"
      },
      getSession: {
        path: "/api/auth/session",
        method: "get"
      }
    },

    token: {
      maxAgeInSeconds: 8 * 60 * 60,
    },
  },

  globalAppMiddleware: true,
},
});

My Repo

I'm using Sidebase, and im new in this module

wildy13 commented 1 year ago

@thommydz did you add it?

I am actually working on it right now. Will reply with the code snippets here as soon as possible.

guys, can you help my problem?

wildy13 commented 1 year ago

oh, i know whats problem, the problem is node version. i used 18.18.0, but its working on 20.1.0

zoey-kaiser commented 1 year ago

Closing this, as it seems like you needed a cookie based solution, which you implemented yourself! 🤗