DEV Community

Next.js Authentication - JWT Refresh Token Rotation with NextAuth.js

Mateusz Baranowski on March 04, 2022

Recently I was implementing authentication in a Next.js app. After weighing in a few options, I’ve settled on NextAuth.js, as it's tailor-made for ...
Collapse
 
htissink profile image
Henrick Tissink

Absolutely excellent article Mateusz! :D what a lifesaver! ...just a small heads up :) not sure if intentional - the hook you create is called useAuth but after that, you mention const isAuthenticated = useWithAuth(true) - maybe a small typo?

Collapse
 
mabaranowski profile image
Mateusz Baranowski

Yeh, a typo, it should be useAuth. Fixed it, thank you Henrick! :)

Collapse
 
htissink profile image
Henrick Tissink

useAuth(true) - could you explain how you pass in the boolean param?

Thread Thread
 
mabaranowski profile image
Mateusz Baranowski • Edited

I originally used it to decide if I want to be redirected or not:

export default function useAuth(shouldRedirect) {
  ...
  signOut({ callbackUrl: '/login', redirect: shouldRedirect });
  ...
}
Enter fullscreen mode Exit fullscreen mode

When the user is on the unprotected page, and for whatever reason his token expires, I want to silently log him out, without redirecting him to the login page.

I'll update the example code with this redirect flag :)

Thread Thread
 
htissink profile image
Henrick Tissink

That functionality sounds great :) thanks for updating!

Collapse
 
bodich profile image
Bogdan • Edited

Unfortunately, token rotation does not work at all (next-auth bug). New token is just lost each time and the old firstly acquired token is being re-used each time. Here is my report to next-auth, but many devs are experiencing that.
github.com/nextauthjs/next-auth/is...
See the first issue comment with the link to another similar topic.

Collapse
 
leomunizq profile image
Leonardo Muniz

I have the same problem, do you have any idea or have you found a solution for this? even if maybe with another library.
thanks

Collapse
 
bodich profile image
Bogdan • Edited

I've solved it 100% successfully
Don't have time to format in this buggy editor. Code formatting does not work well

In next-auth.d.ts

declare module "next-auth" {
interface KeycloakTokenSet {
access_token: string
refresh_token: string
id_token: string
expires_in: number
refresh_expires_in: number
}

enum UserRole {
    user,
    admin,
}
Enter fullscreen mode Exit fullscreen mode

}

declare module "next-auth/jwt" {
interface JWT extends JWT {
access_token: string
refresh_token: string
id_token: string
expires_at: number
provider: string
userRole: UserRole
error?: "RefreshAccessTokenError"
}
}

declare module "next-auth/providers" {
interface OAuthConfig extends OAuthConfig {
tokenUrl: string
}
}

declare module "next-auth/providers/keycloak" {
export interface KeycloakProfileToken extends KeycloakProfile {
realm_access: {roles: [string]}
}
}

Then in [...nextauth].ts

export const authOptions: NextAuthOptions = {
secret: process.env.NEXTAUTH_SECRET,
// next-auth.js.org/configuration/pro...
providers: [
keycloak,
// zitadel,
],
theme: {
colorScheme: "light",
},
callbacks: {
async jwt({ token, account, user }) {
if (account && user) {
if(!account.access_token) throw Error('Auth Provider missing access token');
if(!account.refresh_token) throw Error('Auth Provider missing refresh token');
if(!account.id_token) throw Error('Auth Provider missing ID token');

            console.log(`\n>>> Signed in with account: ${account.expires_at}`)

            // Save the access token and refresh token in the JWT on the initial login
            const newToken: JWT = {
                ...token,
                access_token: account.access_token,
                refresh_token: account.refresh_token,
                id_token: account.id_token,
                expires_at: Math.floor(account.expires_at ?? 0),
                provider: account.provider,
            }
            return newToken
        }

        if (Date.now() < token.expires_at * 1000) {
            // If the access token has not expired yet, return it
            console.log(`Token is valid: ${token.expires_at}`)
            try {
                const tokenData: KeycloakProfileToken = parseJwt(token.access_token)
                console.log(tokenData.realm_access.roles.includes('admin'))
            } catch(e) {console.log(e)}
            return token
        }

        const provider = [zitadel, keycloak].find(p => p.id == token.provider)!

        // If the access token has expired, try to refresh it
        console.log(`\n>>> Old token expired: ${token.expires_at}`)
        const newToken = await refreshAccessToken(token, provider)
        console.log(`New token acquired: ${newToken.expires_at}`)
        return newToken
    },
    async session({ session, token }) {
        console.log(`Executing session() with token ${token.expires_at}`)
        return session
    },
},
events: {
    signOut: async({ session, token }) => {
        const provider = [zitadel, keycloak].find(p => p.id == token.provider)!
        doFinalSignoutHandshake(token, provider)
    },
},
jwt: {
    maxAge: 1 * 60 // 5 minutes : 300, temporary 1 minute
},
session: {
    maxAge: 30 * 24 * 60 * 60 // 30 days : 2592000
}
Enter fullscreen mode Exit fullscreen mode

}

Thread Thread
 
pizofreude profile image
Pizofreude

Awesome solution, thanks Bogdan!

Thread Thread
 
bodich profile image
Bogdan

You are welcome!

Collapse
 
tasmiarahmantanjin profile image
Tasmia Rahman

Hi, Thanks for your tutorial this is really good and explains a lot. Currently i am trying add refreshToken to my company project, In my case we want to set i a bit differently. For example the requirement is I need to send accessToken and refreshToken from backend Normally with res.body then Frontend need to set it in Header and then I need to get the refreshToken in auth/refresh-token endpoint with req.headers. I am a bit confused how to achieve this. Do you think it's possible? Any help will be highly appreciated. thanks a lot in advance!

Collapse
 
mabaranowski profile image
Mateusz Baranowski

On the server, I use express-jwt package, which takes care of reading the authorization header. On the client, you can set the auth header, with accessToken taken from useSession:

axios.get(API_URL + 'endpoint', {
                headers: {
                    'Authorization': `Bearer ${session.accessToken}`
                }
            })
Enter fullscreen mode Exit fullscreen mode

Do You want to refresh the token from the client code?

Collapse
 
tasmiarahmantanjin profile image
Tasmia Rahman

Thanks a lot Mateusz for the reply! I was able to achieve what i wanted to, front nextauth.js I called the refresh-token end point. Now, I am trying logOut on refreshToken error, and refreshTokeExpire. Because of the initial project setup I am still trying out the best way to logOut user on refreshTokenExpire. I noticed you used useAuth hook, I am struggling to use in correct place on my code! As this is my very first task on nextauth therefor it´s seems a bit hard to me!

Thread Thread
 
mabaranowski profile image
Mateusz Baranowski

You can call the useAuth hook directly from the page (pages folder).

export default function Page() {
    const isAuthenticated = useAuth(true);

    return (
        <>
            {isAuthenticated ?
                <YourComponent />
                : null}
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

In the example above, we use isAuthenticated to decide if we should render the page. If you do not need this functionality, calling useAuth(true) should be sufficient. This hook will log out the user when his token expires while being on that page.

Thread Thread
 
tasmiarahmantanjin profile image
Tasmia Rahman

Thanks Mateusz! I got it, but my boss wants me to call auto logout inside nextauth.js.
`
events: {
session: async ({ session }) => {
// if RefreshAccessTokenError then logout
if (session?.error === 'RefreshAccessTokenError') {
signOut()
}

  // if refreshTokenExpiresIn then signOut
  if (
    session?.refreshTokenExpiresIn &&
    Date.now() > new Date(session.refreshTokenExpiresIn).getTime()
  ) {
    console.log('I am logging out')
    signOut()
  }
}
Enter fullscreen mode Exit fullscreen mode

}
`

I kanda figure one way out which is using session event like below. He don't want to call hooks on pages. However with event I am getting a error message also like error - unhandledRejection: ReferenceError: window is not defined .That's why it's a bit complicated in my case!

Thread Thread
 
mabaranowski profile image
Mateusz Baranowski

You are getting "window is not defined" because you are trying to call a signOut function (which requires a browser window) in a session callback inside [...nextauth].js.

[...nextauth].js lives in pages/api/auth, and pages/api in Next.js are the server functions. You can look up the documentation on signOut.

If you want to logout a user from the session callback, you should probably use POST /api/auth/signout. Call it as you would a regular endpoint. This is used by signOut() internally.

I'm not sure if it's gonna work, but it's worth exploring. Let me know how you did :)

Collapse
 
sleepwalky profile image
Alexander Kleshchukevich

Thank you for writing this article! It's so much better than the example in next-auth's docs. Lifesaver, indeed!

Collapse
 
marcelx8 profile image
Marcelx8

Thanks for sharing your experience and guidance on Next-Auth. It's my first time implementing authentication myself and with so many authentication libs out there, this seemed to be the one that has so much given functionality.

After my 3 day search and testing, I just couldn't get enough documentation or examples explaining the jwt and session callbacks and its uses together, especially with the CredentialsProvider.

You have provided reusable pieces of code as well as explaining them. Appreciated sincerely.

Collapse
 
navidmadannezhad profile image
Navid Madannezhad

Hi, thanks for this valuable article. But anyone here facing Invalid hook call error because of using useSession in useAuth.js? useSession is a hook and can be only used in functional components, while useAuth is not a functional component.

Collapse
 
mfreemanxtivia profile image
Mike Freeman

Maybe I am the only one out to lunch here, but I am struggling to see how the updated _app.js does not result in an endless loop

i.e. if the RefreshTokenHandler hook computes timeRemaining to be > 0, it essentially updates the state for the page (through the setInterval function passed in props). This would trigger another render of the page, which causes RefreshTokenHandler to run, it computes a different value for timeRemaining, which causes it to update the outer state, triggering another refresh and on and on and on

It feels like i must be missing something basic here

Collapse
 
jon_rivera_9152520f0c7293 profile image
Jon Rivera • Edited

hey Mateusz!

thanks so much for this thorough tutorial. Currently, my auth flow returns me a code on the url, that I can POST and get a accessToken, accessTokenExpiry and refreshToken. Using the CredentialsProvider, I get that my credentials are wrong.

Is is required to provide credentials (username and password) when using CredentialsProvider ?

Collapse
 
asifsaho profile image
Asif Nawaz

anyone able to implement this? for my case if refetchInterval getting a value from state it is in infinite loop. Next auth is keep calling session.

I am using next auth 4.10 with Next 13

Collapse
 
isaacpro01 profile image
ssemugenyi isaac

Hi, Thanks for the article, I have tried to follow the article, working with nextjs, typescript and graphql, i am faced with a challenge when upon login and the application trying to route to the home page. The application logs out and clears that session. Any pointers on how I can solve this.

Collapse
 
ricardasjak profile image
Ricardas Jaksebaga

How would you refresh token, if user doesn't initiate page navigation, and web app (e.g. react query) refreshes data in the background - it calls nextjs pages api handler? After certain amount of idle time, such attempt to refresh page data can end up with an error because of internal token (e.g. azure ad access token) expiration.

Collapse
 
luissilv4 profile image
Luís Silva

Great content Mateusz!

I'm new into NextJS, I've been able to login users against my Django API through your code :)
I have a question I believe it's pretty easy for you.
I need to get the user details, which are available trough another endpoint, on which part of the code should I do this? Is it inside of one of the callbacks?

Thanks and congrats for the great content!

Collapse
 
khiukv profile image
kh

Thanks for the awesome tutorial! I'm currently working on my first next .js project and it's not always clear to me what should go where. Can I see the full application code somewhere? I probably have something misconfigured. I'm working with a local fake server and clicking on signIn redirects me to the server page instead of the application page. I would be very happy to help!

Collapse
 
andriyfm profile image
Andri Firmansyah

can you explain where to place the const isAuthenticated = useAuth(true) are?
by the way thanks for the great article

Collapse
 
jlex_ profile image
Jean

Hello Mateusz, great article ! Sorry if my question is stupid, but I don't see where you check if the accessToken is valid or not.
How can we tell nextAuth to verify the accessToken instead of just the JWT ?
Thanks !

Collapse
 
developerbishwas profile image
Bishwas Bhandari

Thanks a lot for sharing, useful. I do have some confusions on it, bbut I guess I'll figure it out.

Collapse
 
juancamiloqhz profile image
Juan Camilo QHz

Really nice explanation thx

Collapse
 
napster profile image
Marouane Etaraz

Thanks lot brother ! that what i'm looking for. 😍😍

Collapse
 
rodriguesvinicius profile image
Vinicius Alves

accessTokenExpiry is in milliseconds ?

Collapse
 
shobanamg profile image
shobanamg • Edited

Hi Mateusz, Great article. I am using auth0 as provider. The way the interval is handled, doesn't it keep extending the NextAuth session life time. When will the session get invalidated

Collapse
 
aapdomingues profile image
aapdomingues • Edited

Hello Mateusz! Thank you very much for sharing!!

Your article helps me a lot!!

Collapse
 
tonymarques profile image
Tony Marques

Hello do you have a auth/login file please ?