Search code examples
javascriptoauth-2.0azure-active-directorysveltekitnext-auth

Missing Azure AD refresh token in Next-Auth / Auth.js


With this configuration for Auth.js with Sveltekit and Azure AD, I only receive an access_token and an id_token, but no refresh_token to persist the session longer. The refresh_token is needed for the auth code flow as described in Microsoft docs : https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow

import { SvelteKitAuth } from "@auth/sveltekit";
import AzureADProvider from "@auth/core/providers/azure-ad";
import { env as privateEnv } from "$env/dynamic/private";
import { env as publicEnv } from "$env/dynamic/public";
import type { HandleFetch } from "@sveltejs/kit";

async function refreshAccessToken(accessToken) {
  console.log("refreshAccessToken");
  try {
    const url = `https://login.microsoftonline.com/${privateEnv.AZURE_TENANT_ID}/oauth2/v2.0/token`;
    const req = await fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded"
      },
      body: `grant_type=refresh_token`
        + `&client_secret=${privateEnv.AZURE_CLIENT_SECRET}`
        + `&refresh_token=${accessToken.refreshToken}`
        + `&client_id=${privateEnv.AZURE_CLIENT_ID}`
    });
    const res = await req.json();
    console.log(res);
    return {
      ...accessToken,
      accessToken: res.access_token,
      accessTokenExpires: Date.now() + res.expires_in * 1000,
      refreshToken: res.refresh_token ?? accessToken.refreshToken
    };

  } catch (error) {
    console.log(error);

    return {
      ...accessToken,
      error: "RefreshAccessTokenError"
    };
  }
}

export const handle = SvelteKitAuth({
  providers: [
    //@ts-expect-error issue https://github.com/nextauthjs/next-auth/issues/6174
    AzureADProvider({
      clientId: privateEnv.AZURE_CLIENT_ID,
      clientSecret: privateEnv.AZURE_CLIENT_SECRET,
      tenantId: privateEnv.AZURE_TENANT_ID,
      authorization: {
        params: {
          scope: `openid profile email api://${privateEnv.AZURE_CLIENT_ID}/Test`
        }
      }
    })
  ],
  callbacks: {
    async jwt({ token, account }) {
      console.log("jwt callback");
      console.log(token);
      // Persist the OAuth access_token to the token right after signin
      if (account) {
        console.log("account");
        console.log(account);
        token.accessToken = account.access_token;
        token.accessTokenExpires = Date.now() + account.expires_in * 1000;
      }
      console.log(Date.now() + " " + token.accessTokenExpires);

      if (Date.now() < token.accessTokenExpires) {
        return token;
      }
      console.log("token expired, refreshing...");
      return refreshAccessToken(token);
    },
    async session({ session, token, user }) {
      // Send properties to the client, like an access_token from a provider.
      console.log(token);
      session.accessToken = token.accessToken;
      console.log("session callback");
      console.log(session);
      return session;
    }
  }
});

Solution

  • Finally the issue was that the scope offline_access was missing in provider configuration. It was needed to receive a refresh_token in response.

    Described here in docs : https://learn.microsoft.com/en-us/azure/active-directory/develop/scopes-oidc#offline_access

      providers: [
        //@ts-expect-error issue https://github.com/nextauthjs/next-auth/issues/6174
        AzureADProvider({
          clientId: privateEnv.AZURE_CLIENT_ID,
          clientSecret: privateEnv.AZURE_CLIENT_SECRET,
          tenantId: privateEnv.AZURE_TENANT_ID,
          authorization: {
            params: {
              scope: `openid profile email offline_access api://${privateEnv.AZURE_CLIENT_ID}/Test`
            }
          }
        })
      ],