Handling JWT token refresh #1053
-
Hey, I'm working on a simple Spotify webapp and have been able to get nextauth mostly configured but I'm at a loss for how to manage the token refresh process. I've seen a few solutions in other discussions and I understand that I make my own call to Spotify for the refresh token. The problem I've had with that is that all of the solutions I've tried fall apart when trying to determine when the current token expires. I know that Spotify returns the parameter 'expires_in' when it returns accessToken and refreshToken, but I don't know how to access that in any of the nextauth callbacks. I'm able to grab and set both the accessToken and refreshToken, but I've noticed that the 'accessTokenExpires' property is always null, and it appears to be intentional? I'm also aware that the jwt token has both 'iat' and 'exp' properties but those seem to be for the NextAuth session, and not Spotify's access token. Lastly, I've seen solutions where people decode the token with jwt_decode and are able to determine when it expires from that, but it always gives me a InvalidTokenError, stating "Invalid token specified: Cannot read property 'replace' of undefined" I know this is probably vague, this is my first time messing around with OAuth flows and I'm just looking for a good solution to refresh my access token when it expires. If needed I can show my code or link to solutions I've tried. I'd appreciate any help on this, thanks! |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 2 replies
-
We use IdentityServer 4 at work, and accessTokenExpires is null for us as well. The way I get around this is I just set it to a future date that is before the token will expire. Then you can check that in the jwt callback, and fetch a new access token if you see that the expire value is getting closer. |
Beta Was this translation helpful? Give feedback.
-
For anyone else who may be having the same problem, I was able to get it working by manually setting an initial token expiration date, then grabbing the real expiration date later when I refreshed it. I pulled the general structure from #871 and just modified it a little bit. I hope this helps someone else later on! import NextAuth from 'next-auth'
import Providers from 'next-auth/providers'
// time in seconds, spotify default is 1 hour or 3600s
const expires = 60 * 60
const options = {
site: process.env.NEXTAUTH_URL,
providers: [
Providers.Spotify({
scope: 'playlist-read-private',
clientId: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
state: process.env.STATE
})
],
// https://github.com/nextauthjs/next-auth/discussions/871
callbacks: {
async jwt(prevToken, _, account, profile) {
// Signing in
if (account && profile) {
return {
accessToken: account.accessToken,
accessTokenExpires: addSeconds(new Date(), (expires - 10)), // set initial expire time
refreshToken: account.refreshToken,
user: profile,
}
}
// Subsequent use of JWT, the user has been logged in before
if (new Date().toISOString() < prevToken.accessTokenExpires) {
return prevToken
}
// access token has expired, try to update it
return refreshAccessToken(prevToken)
},
async session(_, token) {
return token
},
}
}
async function refreshAccessToken(token) {
console.log('refreshing access token')
try {
const url = `https://accounts.spotify.com/api/token`
const data = {
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
grant_type: "refresh_token",
refresh_token: token.refreshToken
}
// https://github.com/github/fetch/issues/263
const searchParams = Object.keys(data).map((key) => {
return encodeURIComponent(key) + '=' + encodeURIComponent(data[key])
}).join('&')
const response = await fetch(url, {
body: searchParams,
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
method: "POST",
})
const refreshToken = await response.json()
if (!response.ok) {
throw refreshToken
}
// Give a 10 sec buffer
const accessTokenExpires = addSeconds(
new Date(),
refreshToken.expires_in - 10
).toISOString()
console.log('accessTokenExpires: ', new Date(accessTokenExpires).toLocaleTimeString('en-US'))
return {
...token,
accessToken: refreshToken.access_token,
accessTokenExpires: accessTokenExpires,
refreshToken: token.refreshToken,
}
} catch (error) {
console.log('error refreshing: ', error)
return {
...token,
error: "RefreshAccessTokenError",
}
}
}
const addSeconds = (date, seconds) => {
date.setSeconds(date.getSeconds() + seconds)
return date
}
export default (req, res) => NextAuth(req, res, options) |
Beta Was this translation helpful? Give feedback.
We use IdentityServer 4 at work, and accessTokenExpires is null for us as well. The way I get around this is I just set it to a future date that is before the token will expire. Then you can check that in the jwt callback, and fetch a new access token if you see that the expire value is getting closer.