SDK - Handling Access and Refresh Tokens from Server-Side

I’m trying to figure out the best practice for dealing with authentication with the Directus SDK.

I’ll get to the question before getting into the long boring details. The docs for the NextJS authentication tutorial say this:

You may have noticed in ./lib/directus.ts that the Directus client is initialized with the parameter cookie as a string. This serves well for server-side rendering applications, such as what this tutorial covers.

But the tutorial doesn’t get into the specifics of handling refresh tokens. The way it’s written means that every 15 minutes the access token is invalidated (based on the access token TTL) or if the user completely closes their browser since it’s a session cookie. I’d prefer it not to be a session based cookie but I’m guessing I just need to specify a maxAge when setting the cookie.

But the example never gets into the specifics of how to handle the refresh token. It only says this:

Temporary tokens will expire after some time, which can be rectified using the refresh functionality offered by the Directus SDK:

When using the Directus client in cookie mode I only get the access token in the login(...) response. I don’t get a refresh token. So if I attempt to do a refresh() call I get the error:

Invalid payload. The refresh token is required in either the payload or cookie.

I don’t have a refresh token. I assumed that this would be handled by the sdk client but I guess that’s not the case. Am I missing something about the authentication flow? On this page of the documentation it states:

You do not need to provide the refresh_token, but you must specify the mode.

Am I missing something in the documentation that I should be reading?

Thanks!


Now to the boring implementation details for anyone interested.

I have a Svelte 5 + SvelteKit project and I want to make my API calls via the SDK from the server-side logic (like +page.server.ts or +layout.server.ts). This question would most likely apply to any server-side scenario.

The Svelte example in the documentation is specific to client-side so it doesn’t help in my situation. The NextJS example does make calls from the server-side so I thought I would go by those tutorial steps but translated to a Svelte 5 app.

Here are some code specifics:

$lib/auth/directus.ts - Directus Client

export const client = createDirectus(env.DIRECTUS_URL)
  .with(rest())
  .with(authentication('cookie', { credentials: 'include' }));

NOTE ABOUT THIS - should I be using a “singleton” like pattern like this from the server-side or creating a new client per SDK/API request? If no user-specific data (like tokens) is stored on the server-side I’m assuming this is fine.

routes/(auth)/login/+page.server.ts - Server side logic for the login

const response = await client.login({ email: form.data.email, password: form.data.password });

if (response.access_token) {
  event.cookies.set('directus_session_token', response.access_token, {
    sameSite: 'strict',
    path: '/',
    secure: true,
  });
} else {
  throw new Error('could not find access_token in login response');
}

Checking if the user is authenticated - using the readMe() function

client.setToken(token);
const user = await client.request(readMe());

Trying to attempt a refresh - this doesn’t use the previously exported client:

const client = createDirectus(env.DIRECTUS_URL).with(authentication()).with(rest());
const response = await client.request(refresh({ mode: 'cookie' }));

if (!response.access_token) {
  return false;
}

event.cookies.set('directus_session_token', response.access_token, {
  sameSite: 'strict',
  path: '/',
  secure: true,
});

Generally speaking cookie handling on the serverside can be a little finicky, as the tooling around cookies is quite inconsistent. The cookie / session auth modes are optimized for a browser environment first and foremost.

When it comes to refreshing the session, there’s effectively three ways of doing it:

  • Keep track of the expiration date and manually refresh right before you hit this time
  • Look out for the token expired error and refresh once you hit auth-expiration related failing requests
  • Have the server auto-refresh through the session auth method

Option 3 is by far the easiest, but also relies on cookies getting and setting being properly supported, like it is in the browser. I’ve personally not dabbled in NextJS or SvelteKit and their authentication handling too much, so am afraid I won’t be able to answer in too much detail for those setups specifically.

@Milk in the setup shared above, you might want to try using the session auth mode instead of cookie auth mode, so it’s all handled through just the cookie back and forth :thinking:

For now I guess I will just increase the ACCESS_TOKEN_TTL and strip out any refresh functionality. This isn’t the ideal secure solution I would prefer but I can’t stop development. I’ll look a bit more through the docs or maybe look and see how the web UI handles refresh tokens.

Hello @Chromag

The Directus SDK is designed mainly for client-side use where it can handle cookies automatically. If you’re using it on the server side (like with SvelteKit or Remix), you need to manage tokens manually, store the access and refresh tokens (e.g. in Redis), check if the access token is expired, and refresh it when needed. Instead of using setToken, which can lead to shared state issues in server environments, use withToken(accessToken) to safely create a per-request client instance with the correct token.

Hi @ahmad_quvor

Interesting. The documentation provides both Astro and NextJS tutorials of authentication. Both of these tutorials handle the authentication SDK calls via server-side API endpoints.

The Astro tutorial has this example for logging in. Create the file api/auth/login.ts:

export const POST: APIRoute = async ({ request, cookies, redirect }) => { ... }

The NextJS tutorial has this example for logging in. Create the file app/api/auth/login/route.ts:

export async function POST(request: NextRequest) { ... }

Both tutorials say that when authenticating via server-side you should use the authentication('cookie') method.

Both tutorials show setting the cookie manually like this:

const response = await client.login({ email, password });
if (response.access_token) {
  cookies.set('directus_session_token', response.access_token, { sameSite: 'strict', path: '/', secure: true });
}

If I follow the practice used in the tutorial using authentication('cookie') then I never get a refresh_token in the response from the login() call. I can’t manage it manually.

To do this I would need to switch to authentication('json') mode which the tutorials specifically say should be used for client-side authentication:

Astro Tutorial

The Directus SDK supports two authentication modes: cookie and json . The json mode is useful for client-side applications and the cookie mode for Server-side rendering (SSR) applications like Astro.

NextJS Tutorial

You may have noticed in ./lib/directus.ts that the Directus client is initialized with the parameter cookie as a string. This serves well for server-side rendering applications, such as what this tutorial covers.

If I should be handling all the cookies manually via some other method (custom storage with json mode, maybe?), is there an example or tutorial in the docs for doing this via the server-side, and if so, why provide tutorials saying I should be using cookie mode for server-side?

Good call! Yeah I was concerned about this when using the Directus SDK client via server-side. The NextJS tutorial has you using setToken prior to calling readMe():

client.setToken(token)
const user = await client.request(readMe());

However, the Astro tutorial omits the setToken() call. I’m guessing it’s not required and the SDK client will automatically read the access token stored in the cookie. I tested it locally without setToken and it works as expected.

Thanks for your assistance!

EDIT:

After more thought I think I understand why I don’t get a refresh token and why refresh doesn’t work via cookie mode.

The more I think about it the more I think that a refresh token just isn’t needed on the server-side with HttpOnly cookies like this. I think the SDK handles reading the token from the cookie and sending that to the Directus API endpoints. So the code in the NextJS tutorial using setToken(...) isn’t required.

Since this is all done via a secure HttpOnly cookie and from the server-side, the access token isn’t accessible from the client so there is no issue (or very little) with cross-site-scripting attacks in this scenario.

Hello everyone,

i know this thread is “solved” but i had the same issue of @Chromag when i tried to refresh the cookie with the same tech stack / auth logic via server. I managed to make it work in another way but i would like to go deeper on the “why” server side refresh isn’t working as expected. Maybe this can also be useful to someone else! :slight_smile:

That’s how i’m handling the authentication with “cookie mode” by:

  • Setting the ACCESS_TOKEN_TTL with a high value (like 3 months)
  • Storing the directus_session_cookie manually when a user login
  • Checking if a user is authenticated using hooks (hook.server.ts)
  • Making the user re-authenticate when the cookie expires / gets removed

For that part everything is working fine, but as it was pointed out by the author, if i try to refresh the cookie server side i always get the error Invalid payload. The refresh token is required in either the payload or cookie.

It’s clear to me that refresh_token isn’t necessary for cookie mode. What it’s not clear is why the refresh always fails even if i explicitly set the directus_session_token cookie in the request headers.

For sure the refresh isn’t automatically handled, even if we set .authentication(‘cookie’, {autoRefresh: true} in the SDK config because, at least for me, everything about auth is on the server side.

A little bit code context for hooks.server.ts.

Directus sdk setup

const client= createDirectus<CustomSchema>(PUBLIC_APIURL,options).with(authentication('cookie', {credentials: true}).with(rest());

isAuth function example

function isAuthenticated(){
const loggedUser = await client.request(readMe());
return { authenticated: !!loggedUser?.id, loggedUser }
}

Refresh attempts via one of these methods

await client.refresh(); // OR
await client.request(refresh('cookie'))

Relevant part setting headers manually
In sveltekit server hooks is possible to manipulate the fetch request before it is made. So there’s 3 ways i tried

export const handleFetch: HandleFetch = async ({ request, fetch, event }) => {  

if(request.url === PUBLIC_APIURL/auth/refresh){

const token = event.cookies.get('directus_session_token');

// Method 1
request.headers.set('cookie', `directus_session_token=${token}`);

// Method 2
request.headers.set('authorization', `Bearer ${token}`);
}

// Method 3
 request = new Request(
      request.url + `?access_token=${token}`
      request
    );

return fetch(request)
}

Final thoughts

I don’t know if it’s something i missed or something that is obvious for people who’s more skilled than me but it would be cool to understand what’s the best way to deal with this.

Thanks in advance to everyone that will answer! :slight_smile: