Closed thommydz closed 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.
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.
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
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)
}
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?
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.
@thommydz did you add it?
@thommydz did you add it?
I am actually working on it right now. Will reply with the code snippets here as soon as possible.
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:
const
to be able to use my account functions from piniaskipRefresh = false
to the useCustomFetch. This is to prevent an infinite loop. The refresh endpoint will also go through this custom fetch so if it always does this when the result is 401, it will do this infinite if we don't add this check.onResponseError
function. If the response is 401 and it did not already try to refresh, it calls accountStore.refresh. 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;
}
},
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,
},
});
I'm using Sidebase, and im new in this module
@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?
oh, i know whats problem, the problem is node version. i used 18.18.0, but its working on 20.1.0
Closing this, as it seems like you needed a cookie based solution, which you implemented yourself! 🤗
Environment
Darwin
v20.1.0
3.4.3
2.3.3
npm@9.6.4
vite
devtools
,imports
,nitro
,experimental
,devServerHandlers
,modules
,auth
,pinia
,css
,runtimeConfig
,build
@sidebase/nuxt-auth@0.6.0-beta.2
,@pinia/nuxt@0.4.9
,nuxt-swiper@0.1.9
,@nuxt/image-edge@1.0.0-28050764.aaee7ac
-
Reproduction
Setup in nuxt.conifg.ts:
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:
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