Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refresh token rotation not working (Azure AD) #6462

Closed
wouter-deen opened this issue Jan 22, 2023 · 1 comment
Closed

Refresh token rotation not working (Azure AD) #6462

wouter-deen opened this issue Jan 22, 2023 · 1 comment
Labels
question Ask how to do something or how something works

Comments

@wouter-deen
Copy link

Question 馃挰

I recently implemented refresh token rotation into my Next.js webapp using Azure Active Directory (organizations version, not B2C). I used this official resource to do this, and slightly modified the code to my needs and to work with Azure AD instead of Google. However, it does not work.

During debugging, I found out that the function async function refreshAccessToken(token) works, i.e., the Graph API returns a new access token. However, it seems that somehow, this refreshed token is not passed to the async session({session, token}) callback, resulting in token to be undefined. This means that if a user navigates to an authenticated route 1+ hours after logging in, the error below appears in the Node.js console. Instead of passing the refreshed accessToken to the client-side, the user is forwarded to the signin page.

Error code

[next-auth][error][JWT_SESSION_ERROR] 
https://next-auth.js.org/errors#jwt_session_error Cannot read properties of undefined (reading 'accessToken') {
  message: "Cannot read properties of undefined (reading 'accessToken')",
  stack: "TypeError: Cannot read properties of undefined (reading 'accessToken')\n" +
    '    at Object.session (webpack-internal:///(api)/./pages/api/auth/[...nextauth].ts:71:41)\n' +
    '    at Object.session (D:\\Programmeren\\Heatwaves\\heatwaves\\node_modules\\next-auth\\core\\routes\\session.js:56:42)\n' +
    '    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n' +
    '    at async AuthHandler (D:\\Programmeren\\Heatwaves\\heatwaves\\node_modules\\next-auth\\core\\index.js:158:27)\n' +
    '    at async NextAuthHandler (D:\\Programmeren\\Heatwaves\\heatwaves\\node_modules\\next-auth\\next\\index.js:23:19)\n' +
    '    at async D:\\Programmeren\\Heatwaves\\heatwaves\\node_modules\\next-auth\\next\\index.js:59:32\n' +
    '    at async Object.apiResolver (D:\\Programmeren\\Heatwaves\\heatwaves\\node_modules\\next\\dist\\server\\api-utils\\node.js:363:9)\n' +
    '    at async DevServer.runApi (D:\\Programmeren\\Heatwaves\\heatwaves\\node_modules\\next\\dist\\server\\next-server.js:487:9)\n' +
    '    at async Object.fn (D:\\Programmeren\\Heatwaves\\heatwaves\\node_modules\\next\\dist\\server\\next-server.js:749:37)\n' +
    '    at async Router.execute (D:\\Programmeren\\Heatwaves\\heatwaves\\node_modules\\next\\dist\\server\\router.js:253:36)\n' +
    '    at async DevServer.run (D:\\Programmeren\\Heatwaves\\heatwaves\\node_modules\\next\\dist\\server\\base-server.js:384:29)\n' +
    '    at async DevServer.run (D:\\Programmeren\\Heatwaves\\heatwaves\\node_modules\\next\\dist\\server\\dev\\next-dev-server.js:741:20)\n' +
    '    at async DevServer.handleRequest (D:\\Programmeren\\Heatwaves\\heatwaves\\node_modules\\next\\dist\\server\\base-server.js:322:20)',
  name: 'TypeError'
}

How to reproduce 鈽曪笍

[...nextauth] API endpoint

import NextAuth from "next-auth"
import AzureAD from "next-auth/providers/azure-ad";

async function refreshAccessToken(accessToken) {
  try {
    const url = "https://login.microsoftonline.com/02cd5db4-6c31-4cb1-881d-2c79631437e8/oauth2/v2.0/token"
    await fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
      },
      body: `grant_type=refresh_token`
      + `&client_secret=${process.env.AZURE_AD_CLIENT_SECRET}`
      + `&refresh_token=${accessToken.refreshToken}`
      + `&client_id=${process.env.AZURE_AD_CLIENT_ID}`
    }).then(res => res.json())
      .then(res => {
        return {
          ...accessToken,
          accessToken: res.access_token,
          accessTokenExpires: Date.now() + res.expires_in * 1000,
          refreshToken: res.refresh_token ?? accessToken.refreshToken, // Fall back to old refresh token
        }
      })
  } catch (error) {
    console.log(error)

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

export const authOptions = {
  providers: [
    AzureAD({
      clientId: process.env.AZURE_AD_CLIENT_ID,
      clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
      tenantId: process.env.AZURE_AD_TENANT_ID,
      authorization: {
        params: {
          scope: "offline_access openid profile email Application.ReadWrite.All Directory.ReadWrite.All " +
            "Group.ReadWrite.All GroupMember.ReadWrite.All User.Read User.ReadWrite.All"
        }
      }
    }),
  ],
  callbacks: {
    async jwt({token, account, profile}) {
      // Persist the OAuth access_token and or the user id to the token right after signin
      if (account && profile) {
        token.accessToken = account.access_token;
        token.accessTokenExpires = account.expires_at * 1000;
        token.refreshToken = account.refresh_token;

        token.id = profile.oid; // For convenience, the user's OID is called ID.
        token.groups = profile.groups;
        token.username = profile.preferred_username;

        return token;
      }

      if (Date.now() < token.accessTokenExpires) {
        return token;
      }

      return refreshAccessToken(token);
    },
    async session({session, token}) {
      // Send properties to the client, like an access_token and user id from a provider.
      session.accessToken = token.accessToken;
      session.user.id = token.id;
      session.user.groups = token.groups;
      session.user.username = token.username;

      const splittedName = session.user.name.split(" ");
      session.user.firstName = splittedName.length > 0 ? splittedName[0] : null;
      session.user.lastName = splittedName.length > 1 ? splittedName[1] : null;

      return session;
    },
  },
  pages: {
    signIn: '/login',
  }
}

export default NextAuth(authOptions)

Middleware (Next.js)

export {default} from "next-auth/middleware";

// NextAuth config
export const config = {
  // Add protected routes here
  matcher: [
    "/",
    "/admin/:path*",
    "/course/:path*",
  ]
}

Azure AD dashboard

Below, I have an image of my Authentication page of my Azure AD app registration, might there be something wrong with that:
Screenshot 2023-01-22 121240

Contributing 馃檶馃徑

No, I am afraid I cannot help regarding this

@wouter-deen wouter-deen added the question Ask how to do something or how something works label Jan 22, 2023
@balazsorban44
Copy link
Member

your refreshAccessToken never returns since you are using .then and no return on the fetch call. I would recommend against mixing await and Promises in general.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Ask how to do something or how something works
Projects
None yet
Development

No branches or pull requests

2 participants