diff --git a/.gitignore b/.gitignore index 5e8c4c93b5..3a52776c99 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,8 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +yarn.lock + # Dependencies node_modules @@ -37,4 +39,4 @@ www/providers.json /_work # Prisma migrations -/prisma/migrations \ No newline at end of file +/prisma/migrations diff --git a/www/docs/tutorials.md b/www/docs/tutorials.md index be0733eb0b..2c765c45a7 100644 --- a/www/docs/tutorials.md +++ b/www/docs/tutorials.md @@ -9,6 +9,10 @@ _These tutorials are contributed by the community and hosted on this site._ _New submissions and edits are welcome!_ +### [Refresh Token Rotation](tutorials/refresh-token-rotation) + +How to implement refresh token rotation. + ### [Securing pages and API routes](tutorials/securing-pages-and-api-routes) How to restrict access to pages and API routes. @@ -67,7 +71,6 @@ This example shows how to implement a fullstack app in TypeScript with Next.js u This `dev.to` tutorial walks one through adding NextAuth.js to an existing project. Including setting up the OAuth client id and secret, adding the API routes for authentication, protecting pages and api routes behind that authentication, etc. - ### [Adding Sign in With Apple Next JS](https://thesiddd.com/blog/apple-auth) This tutorial walks step by step on how to get Sign In with Apple working (both locally and on a deployed website) using NextAuth.js. diff --git a/www/docs/tutorials/refresh-token-rotation.md b/www/docs/tutorials/refresh-token-rotation.md new file mode 100644 index 0000000000..b3f36762cf --- /dev/null +++ b/www/docs/tutorials/refresh-token-rotation.md @@ -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 (...) +} +```