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;
}
}
});
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`
}
}
})
],