forked from nextauthjs/next-auth
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs(tutorials): refresh token rotation (nextauthjs#1310)
* docs(tutorials): refresh token rotation * use simple initialization * be optimistic Co-authored-by: Balázs Orbán <[email protected]> * add yarn.lock to .gitignore Co-authored-by: Balázs Orbán <[email protected]>
- Loading branch information
1 parent
782482b
commit 6b5a215
Showing
3 changed files
with
146 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
--- | ||
id: refresh-token-rotation | ||
title: Refresh Token Rotation | ||
--- | ||
|
||
While NextAuth.js doesn't automatically handle access token rotation for OAuth providers yet, this functionality can be implemented using [callbacks](https://next-auth.js.org/configuration/callbacks). | ||
|
||
## Source Code | ||
|
||
_A working example can be accessed [here](https://github.com/lawrencecchen/next-auth-refresh-tokens)._ | ||
|
||
## Implementation | ||
|
||
### Server Side | ||
|
||
Using a [JWT callback](https://next-auth.js.org/configuration/callbacks#jwt-callback) and a [session callback](https://next-auth.js.org/configuration/callbacks#session-callback), we can persist OAuth tokens and refresh them when they expire. | ||
|
||
Below is a sample implementation using Google's Identity Provider. Please note that the OAuth 2.0 request in the `refreshAccessToken()` function will vary between different providers, but the core logic should remain similar. | ||
|
||
```js title="pages/auth/[...nextauth.js]" | ||
import NextAuth from "next-auth"; | ||
import Providers from "next-auth/providers"; | ||
|
||
const GOOGLE_AUTHORIZATION_URL = | ||
"https://accounts.google.com/o/oauth2/v2/auth?" + | ||
new URLSearchParams({ | ||
prompt: "consent", | ||
access_type: "offline", | ||
response_type: "code", | ||
}); | ||
|
||
/** | ||
* Takes a token, and returns a new token with updated | ||
* `accessToken` and `accessTokenExpires`. If an error occurs, | ||
* returns the old token and an error property | ||
*/ | ||
async function refreshAccessToken(token) { | ||
try { | ||
const url = | ||
"https://oauth2.googleapis.com/token?" + | ||
new URLSearchParams({ | ||
client_id: process.env.GOOGLE_CLIENT_ID, | ||
client_secret: process.env.GOOGLE_CLIENT_SECRET, | ||
grant_type: "refresh_token", | ||
refresh_token: token.refreshToken, | ||
}); | ||
|
||
const response = await fetch(url, { | ||
headers: { | ||
"Content-Type": "application/x-www-form-urlencoded", | ||
}, | ||
method: "POST", | ||
}); | ||
|
||
const refreshedTokens = await response.json(); | ||
|
||
if (!response.ok) { | ||
throw refreshedTokens; | ||
} | ||
|
||
return { | ||
...token, | ||
accessToken: refreshedTokens.access_token, | ||
accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000, | ||
refreshToken: refreshedTokens.refresh_token ?? token.refreshToken, // Fall back to old refresh token | ||
}; | ||
} catch (error) { | ||
console.log(error); | ||
|
||
return { | ||
...token, | ||
error: "RefreshAccessTokenError", | ||
}; | ||
} | ||
} | ||
|
||
export default NextAuth({ | ||
providers: [ | ||
Providers.Google({ | ||
clientId: process.env.GOOGLE_CLIENT_ID, | ||
clientSecret: process.env.GOOGLE_CLIENT_SECRET, | ||
authorizationUrl: GOOGLE_AUTHORIZATION_URL, | ||
}), | ||
], | ||
callbacks: { | ||
async jwt(token, user, account) { | ||
// Initial sign in | ||
if (account && user) { | ||
return { | ||
accessToken: account.accessToken, | ||
accessTokenExpires: Date.now() + account.expires_in * 1000, | ||
refreshToken: account.refresh_token, | ||
user, | ||
}; | ||
} | ||
|
||
// Return previous token if the access token has not expired yet | ||
if (Date.now() < token.accessTokenExpires) { | ||
return token; | ||
} | ||
|
||
// Access token has expired, try to update it | ||
return refreshAccessToken(token); | ||
}, | ||
async session(session, token) { | ||
if (token) { | ||
session.user = token.user; | ||
session.accessToken = token.accessToken; | ||
session.error = token.error; | ||
} | ||
|
||
return session; | ||
}, | ||
}, | ||
}); | ||
``` | ||
### Client Side | ||
The `RefreshAccessTokenError` error that is caught in the `refreshAccessToken()` method is passed all the way to the client. This means that you can direct the user to the sign in flow if we cannot refresh their token. | ||
We can handle this functionality as a side effect: | ||
```js title="pages/auth/[...nextauth.js]" | ||
import { signIn, useSession } from "next-auth/client"; | ||
import { useEffect } from "react"; | ||
|
||
const HomePage() { | ||
const [session] = useSession(); | ||
|
||
useEffect(() => { | ||
if (session?.error === "RefreshAccessTokenError") { | ||
signIn(); // Force sign in to hopefully resolve error | ||
} | ||
}, [session]); | ||
|
||
return (...) | ||
} | ||
``` |