Skip to content

Commit

Permalink
docs(tutorials): refresh token rotation (nextauthjs#1310)
Browse files Browse the repository at this point in the history
* 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
lawrencecchen and balazsorban44 authored Feb 21, 2021
1 parent 782482b commit 6b5a215
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 2 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*

yarn.lock

# Dependencies
node_modules

Expand All @@ -37,4 +39,4 @@ www/providers.json
/_work

# Prisma migrations
/prisma/migrations
/prisma/migrations
5 changes: 4 additions & 1 deletion www/docs/tutorials.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
139 changes: 139 additions & 0 deletions www/docs/tutorials/refresh-token-rotation.md
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 (...)
}
```

0 comments on commit 6b5a215

Please sign in to comment.