Directus + Provider authjs
This section gives an example of how the NuxtAuthHandler
can be configured to use Directus JWTs for authentication via the CredentialsProvider
provider and how to implement a token refresh for the Directus JWT.
The below is a code-example that needs to be adapted to your specific configuration:
import CredentialsProvider from "next-auth/providers/credentials";import { NuxtAuthHandler } from "#auth";/** * Takes a token, and returns a new token with updated * `accessToken` and `accessTokenExpires`. If an error occurs, * returns the old token and an error property */async function refreshAccessToken(refreshToken: { accessToken: string; accessTokenExpires: string; refreshToken: string;}) { try { console.warn("trying to post to refresh token"); const refreshedTokens = await $fetch<{ data: { access_token: string; expires: number; refresh_token: string; }; } | null>("https://domain.directus.app/auth/refresh", { method: "POST", headers: { "Content-Type": "application/json", }, body: { refresh_token: refreshToken.refreshToken, mode: "json", }, }); if (!refreshedTokens || !refreshedTokens.data) { console.warn("No refreshed tokens"); throw refreshedTokens; } console.warn("Refreshed tokens successfully"); return { ...refreshToken, accessToken: refreshedTokens.data.access_token, accessTokenExpires: Date.now() + refreshedTokens.data.expires, refreshToken: refreshedTokens.data.refresh_token, }; } catch (error) { console.warn("Error refreshing token", error); return { ...refreshToken, error: "RefreshAccessTokenError", }; }}export default NuxtAuthHandler({ // secret needed to run nuxt-auth in production mode (used to encrypt data) secret: process.env.NUXT_SECRET, providers: [ // @ts-expect-error You need to use .default here for it to work during SSR. May be fixed via Vite at some point CredentialsProvider.default({ // The name to display on the sign in form (e.g. 'Sign in with...') name: "Credentials", // The credentials is used to generate a suitable form on the sign in page. // You can specify whatever fields you are expecting to be submitted. // e.g. domain, username, password, 2FA token, etc. // You can pass any HTML attribute to the <input> tag through the object. credentials: { email: { label: "Email", type: "text" }, password: { label: "Password", type: "password" }, }, async authorize(credentials: any) { // You need to provide your own logic here that takes the credentials // submitted and returns either a object representing a user or value // that is false/null if the credentials are invalid. // NOTE: THE BELOW LOGIC IS NOT SAFE OR PROPER FOR AUTHENTICATION! try { const payload = { email: credentials.email, password: credentials.password, }; const userTokens = await $fetch<{ data: { access_token: string; expires: number; refresh_token: string }; } | null>("https://domain.directus.app/auth/login", { method: "POST", body: payload, headers: { "Content-Type": "application/json", "Accept-Language": "en-US", }, }); const userDetails = await $fetch<{ data: { id: string; email: string; first_name: string; last_name: string; role: string; phone?: string; cvr?: string; company_name?: string; }; } | null>("https://domain.directus.app/users/me", { method: "GET", headers: { "Content-Type": "application/json", "Accept-Language": "en-US", Authorization: `Bearer ${userTokens?.data?.access_token}`, }, }); if (!userTokens || !userTokens.data || !userDetails || !userDetails.data) { throw createError({ statusCode: 500, statusMessage: "Next auth failed", }); } const user = { id: userDetails.data.id, email: userDetails.data.email, firstName: userDetails.data.first_name, lastName: userDetails.data.last_name, role: userDetails.data.role, phone: userDetails.data.phone, cvr: userDetails.data.cvr, companyName: userDetails.data.company_name, accessToken: userTokens.data.access_token, accessTokenExpires: Date.now() + userTokens.data.expires, refreshToken: userTokens.data.refresh_token, }; const allowedRoles = [ "53ed3a6a-b236-49aa-be72-f26e6e4857a0", "d9b59a92-e85d-43e2-8062-7a1242a8fce6", ]; // Only allow admins and sales if (!allowedRoles.includes(user.role)) { throw createError({ statusCode: 403, statusMessage: "Not allowed", }); } return user; } catch (error) { console.warn("Error logging in", error); return null; } }, }), ], session: { strategy: "jwt", }, callbacks: { async jwt({ token, user, account }) { if (account && user) { console.warn("JWT callback", { token, user, account }); return { ...token, ...user, }; } // Handle token refresh before it expires of 15 minutes if (token.accessTokenExpires && Date.now() > token.accessTokenExpires) { console.warn("Token is expired. Getting a new"); return refreshAccessToken(token); } return token; }, async session({ session, token }) { session.user = { ...session.user, ...token, }; return session; }, },});
This was contributes by @madsh93 from Github here: