Directus + Provider authjs
communityauthjsdirectus
madsh93This 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:
ts
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: