Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/refresh token app router #254

Merged
merged 36 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
19217fd
fix: WIP
DanielRivers Dec 13, 2024
5b64eb8
test commit
Yoshify Dec 16, 2024
17e25ff
refactor: move validateToken into jwt/validation.ts to colocate with …
Yoshify Dec 16, 2024
0248395
refactor: move cookie consts out of sessionManager into constants.ts
Yoshify Dec 16, 2024
a47061c
feat(auto-refresh): move refresh logic out of getAccessToken and getI…
Yoshify Dec 16, 2024
d28a034
feat(auto-refresh): util function for generating splitCookie objects
Yoshify Dec 16, 2024
921caa3
chore: update types on GLOBAL_COOKIE_OPTIONS
Yoshify Dec 16, 2024
48d3bfc
docs: commentary
Yoshify Dec 16, 2024
a4f2d05
refactor: move splitString to it's own util file
Yoshify Dec 16, 2024
01a5bee
chore: format
Yoshify Dec 16, 2024
6891b8b
chore: format
Yoshify Dec 16, 2024
f8d516d
feat(auto-refresh): rework middleware to be the source-of-truth for r…
Yoshify Dec 16, 2024
76e57be
chore: fix commentary wording
Yoshify Dec 16, 2024
8a5a13c
feat(auto-refresh): add a token expiry check to isAuthenticatedFactory
Yoshify Dec 16, 2024
a6b3be2
chore: tidy console logs
Yoshify Dec 16, 2024
c2d48ab
fix: await getIdToken before passing it to jwtDecoder. Fixes #252
Yoshify Dec 16, 2024
bc42c10
feat(auto-refresh): optimize refresh flow, re-use previous refresh re…
Yoshify Dec 16, 2024
317e477
chore: wrap console.logs in isDebugMode check, remove unnecessary log…
Yoshify Dec 16, 2024
f5b4ca3
chore: naming nitpicks
Yoshify Dec 17, 2024
1c28f9a
fix: update kindeAccessToken and kindeIdToken when refreshed
Yoshify Dec 17, 2024
7b229f5
fix: decodedToken can be null, check it's not null before checking ex…
Yoshify Dec 17, 2024
370a9fd
refactor: break expired token redirection for RSCs into own utility f…
Yoshify Dec 18, 2024
e8ced3b
refactor: use new redirectOnExpiredToken function
Yoshify Dec 18, 2024
2f5eb9c
refactor: make debug logs warn instead of error
Yoshify Dec 18, 2024
9a80b3c
chore: commentary changes
Yoshify Dec 18, 2024
7ce9b83
chore: more debug logs
Yoshify Dec 18, 2024
508a8de
feat(auto-refresh): copy cookies to the request object as well
Yoshify Dec 18, 2024
199ead9
feat(auto-refresh): export the standard middleware regex pattern
Yoshify Dec 18, 2024
d29222c
chore: formatting
Yoshify Dec 18, 2024
7e8816f
fix: wrap token decoding in middleware in try/catch blocks
Yoshify Dec 18, 2024
95bacf0
chore: debug logs
Yoshify Dec 18, 2024
a7ee5bb
feat(auto-refresh): refresh on public paths
Yoshify Dec 18, 2024
dd12000
chore: fix exports
Yoshify Dec 18, 2024
45d7211
feat(auto-refresh): override callback result headers/cookies if the r…
Yoshify Dec 18, 2024
f662f4a
Merge branch 'main' into fix/refresh-token-app-router
Yoshify Dec 18, 2024
cd91971
fix: refresh both tokens at the same time
Yoshify Dec 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,28 @@ All notable changes to this project will be documented in this file. Dates are d

Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).

#### [v2.5.0-6](https://github.com/kinde-oss/kinde-auth-nextjs/compare/v2.4.6...v2.5.0-6)

- chore: pretter run [`#250`](https://github.com/kinde-oss/kinde-auth-nextjs/pull/250)
- test: migrate to vitest [`#249`](https://github.com/kinde-oss/kinde-auth-nextjs/pull/249)
- fix: API handler warning [`#248`](https://github.com/kinde-oss/kinde-auth-nextjs/pull/248)
- Fix/refresh token [`#247`](https://github.com/kinde-oss/kinde-auth-nextjs/pull/247)
- Fix: Token validation strength [`#243`](https://github.com/kinde-oss/kinde-auth-nextjs/pull/243)
- [Snyk] Upgrade @babel/preset-env from 7.25.9 to 7.26.0 [`#245`](https://github.com/kinde-oss/kinde-auth-nextjs/pull/245)
- Peter/split cookie [`#229`](https://github.com/kinde-oss/kinde-auth-nextjs/pull/229)
- [Snyk] Upgrade @babel/preset-env from 7.25.8 to 7.25.9 [`#241`](https://github.com/kinde-oss/kinde-auth-nextjs/pull/241)
- [Snyk] Upgrade @babel/preset-env from 7.25.7 to 7.25.8 [`#235`](https://github.com/kinde-oss/kinde-auth-nextjs/pull/235)
- chore: PR updated [`afd7428`](https://github.com/kinde-oss/kinde-auth-nextjs/commit/afd7428afc04983b04a6953075ce35e3986bb83d)
- chore: update lock [`ed40c31`](https://github.com/kinde-oss/kinde-auth-nextjs/commit/ed40c31ffd5b5fb2e09f859fbede2fdef5ff050a)
- fix: upgrade @babel/preset-env from 7.25.8 to 7.25.9 [`d067227`](https://github.com/kinde-oss/kinde-auth-nextjs/commit/d067227e7fce621186c734e2183bce60c610a551)

#### [v2.4.6](https://github.com/kinde-oss/kinde-auth-nextjs/compare/v2.4.5...v2.4.6)

> 7 November 2024

- fix: remove noisy error log [`#239`](https://github.com/kinde-oss/kinde-auth-nextjs/pull/239)
- [Snyk] Upgrade cookie from 1.0.0 to 1.0.1 [`#236`](https://github.com/kinde-oss/kinde-auth-nextjs/pull/236)
- chore: release v2.4.6 [`1ea683f`](https://github.com/kinde-oss/kinde-auth-nextjs/commit/1ea683fb869368e08445f8afe506a81993b2010f)
- fix: upgrade cookie from 1.0.0 to 1.0.1 [`b387e60`](https://github.com/kinde-oss/kinde-auth-nextjs/commit/b387e6061473ae1e8c6d13561b72f07648ef4776)

#### [v2.4.5](https://github.com/kinde-oss/kinde-auth-nextjs/compare/v2.4.4...v2.4.5)
Expand Down
13 changes: 7 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@kinde-oss/kinde-auth-nextjs",
"version": "2.4.6",
"version": "2.5.0-6",
"description": "Kinde Auth SDK for NextJS",
"main": "dist/cjs/index.js",
"module": "dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion playground/next-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
6 changes: 6 additions & 0 deletions playground/src/app/AuthProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"use client";
import { KindeProvider } from "@kinde-oss/kinde-auth-nextjs";

export const AuthProvider = ({ children }) => {
return <KindeProvider>{children}</KindeProvider>;
};
8 changes: 4 additions & 4 deletions playground/src/app/api/protected/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ export async function GET() {
return new Response("Unauthorized", { status: 401 });
}

const user = await getUser();
const data = { message: "Hello User", id: user?.given_name };

return NextResponse.json({ data });
// const user = await getUser();
// const data = { message: "Hello User", id: user?.given_name };
console.log('I AM BEING CALLED');
return NextResponse.json({ });
Yoshify marked this conversation as resolved.
Show resolved Hide resolved
}
3 changes: 2 additions & 1 deletion playground/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ export default async function RootLayout({
children: React.ReactNode;
}) {
const { isAuthenticated, getUser } = getKindeServerSession();
const user = await getUser();
const user = {} // await getUser();
// const isAuthenticated = () => true;
Yoshify marked this conversation as resolved.
Show resolved Hide resolved
return (
<html lang="en">
<body>
Expand Down
20 changes: 16 additions & 4 deletions playground/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
Expand All @@ -16,8 +20,16 @@
{
"name": "next"
}
]
],
"target": "ES2017"
},
"include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
"include": [
"next-env.d.ts",
".next/types/**/*.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
}
118 changes: 102 additions & 16 deletions src/authMiddleware/authMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { NextResponse } from "next/server";
import { config } from "../config/index";
import { type KindeAccessToken, KindeIdToken } from "../../types";
import { KindeAccessToken, KindeIdToken } from "../../types";
import { jwtDecoder } from "@kinde/jwt-decoder";
import { validateToken } from "../utils/validateToken";
import { isTokenExpired } from "../utils/jwt/validation";
import { getAccessToken } from "../utils/getAccessToken";
import { kindeClient } from "../session/kindeServerClient";
import { sessionManager } from "../session/sessionManager";
import { getSplitCookies } from "../utils/cookies/getSplitSerializedCookies";
import { getIdToken } from "../utils/getIdToken";
import { OAuth2CodeExchangeResponse } from "@kinde-oss/kinde-typescript-sdk";

const handleMiddleware = async (req, options, onSuccess) => {
const { pathname } = req.nextUrl;
Expand All @@ -24,32 +30,112 @@ const handleMiddleware = async (req, options, onSuccess) => {
if (loginPage == pathname || publicPaths.some((p) => pathname.startsWith(p)))
return;

const { value: kindeToken } = req.cookies.get("access_token");
const resp = NextResponse.next();

if (!kindeToken) {
const response = NextResponse.redirect(
// getAccessToken will validate the token
let kindeAccessToken = await getAccessToken(req);

// if no access token, redirect to login
if (!kindeAccessToken) {
return NextResponse.redirect(
new URL(loginRedirectUrl, options?.redirectURLBase || config.redirectURL),
);
return response;
}

const session = await sessionManager(req)
let refreshResponse: OAuth2CodeExchangeResponse | null = null

// if accessToken is expired, refresh it
if(isTokenExpired(kindeAccessToken)) {
if(config.isDebugMode) {
console.log('authMiddleware: access token expired, refreshing')
}

try {
refreshResponse = await kindeClient.refreshTokens(session);
await session.setSessionItem("access_token", refreshResponse.access_token)
kindeAccessToken = refreshResponse.access_token

// if we want layouts/pages to get immediate access to the new token,
// we need to set the cookie on the response here
const splitCookies = getSplitCookies("access_token", refreshResponse.access_token)
splitCookies.forEach((cookie) => {
resp.cookies.set(cookie.name, cookie.value, cookie.options);
})

if(config.isDebugMode) {
console.log('authMiddleware: access token refreshed')
}
} catch(error) {
// token is expired and refresh failed, redirect to login
if(config.isDebugMode) {
console.error('authMiddleware: access token refresh failed, redirecting to login')
}
return NextResponse.redirect(
new URL(loginRedirectUrl, options?.redirectURLBase || config.redirectURL),
);
}
}

// getIdToken will validate the token
let kindeIdToken = await getIdToken(req);

// if no id token, redirect to login
if(!kindeIdToken) {
return NextResponse.redirect(
new URL(loginRedirectUrl, options?.redirectURLBase || config.redirectURL),
);
}

// if idToken is expired, refresh it
if(isTokenExpired(kindeIdToken)) {
if(config.isDebugMode) {
console.log('authMiddleware: id token expired, refreshing')
}

try {
// if we have a refresh response from an access token refresh, we'll use the id_token from that
if(!refreshResponse) {
refreshResponse = await kindeClient.refreshTokens(session);
}

await session.setSessionItem("id_token", refreshResponse.id_token)
kindeIdToken = refreshResponse.id_token

// as above, if we want layouts/pages to get immediate access to the new token,
// we need to set the cookie on the response here
const splitCookies = getSplitCookies("id_token", refreshResponse.id_token)
splitCookies.forEach((cookie) => {
resp.cookies.set(cookie.name, cookie.value, cookie.options);
})

if(config.isDebugMode) {
console.log('authMiddleware: id token refreshed')
}
} catch(error) {
// token is expired and refresh failed, redirect to login
if(config.isDebugMode) {
console.error('authMiddleware: id token refresh failed, redirecting to login')
}
return NextResponse.redirect(
new URL(loginRedirectUrl, options?.redirectURLBase || config.redirectURL),
);
}
}

const accessTokenValue = jwtDecoder<KindeAccessToken>(
req.cookies.get("access_token")?.value,
kindeAccessToken,
);

const idTokenValue = jwtDecoder<KindeIdToken>(
req.cookies.get("id_token")?.value,
kindeIdToken,
Yoshify marked this conversation as resolved.
Show resolved Hide resolved
);

// check token is valid
const isTokenValid = await validateToken({
token: kindeToken,
});

const customValidationValid = options?.isAuthorized
? options.isAuthorized({ req, token: accessTokenValue })
: true;

if (isTokenValid && customValidationValid && onSuccess) {
if (customValidationValid && onSuccess) {
return await onSuccess({
token: accessTokenValue,
user: {
Expand All @@ -62,8 +148,8 @@ const handleMiddleware = async (req, options, onSuccess) => {
});
}

if (isTokenValid && customValidationValid) {
return NextResponse.next();
if (customValidationValid) {
return resp;
}

return NextResponse.redirect(
Expand Down
8 changes: 4 additions & 4 deletions src/handlers/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,9 @@ const appRouterHandler = async (req, res, options) => {
if (route) {
const routerClient = new AppRouterClient(req, res, options);
await routerClient.createStore();
return void (await route(routerClient));
return (await route(routerClient));
} else {
return void new Response("This page could not be found.", { status: 404 });
return new Response("This page could not be found.", { status: 404 });
}
};

Expand All @@ -122,6 +122,6 @@ const pagesRouterHandler = async (req, res, clientOptions) => {
const route = getRoute(endpoint);
return route
? // @ts-ignore
void (await route(new PagesRouterClient(req, res, clientOptions)))
: void res.status(404).end();
(await route(new PagesRouterClient(req, res, clientOptions)))
: res.status(404).end();
};
8 changes: 4 additions & 4 deletions src/handlers/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,16 @@ export const callback = async (routerClient: RouterClient) => {
);
}

return void routerClient.json({ error: error.message }, { status: 500 });
return routerClient.json({ error: error.message }, { status: 500 });
}
if (postLoginRedirectURL) {
if (postLoginRedirectURL.startsWith("http")) {
return void routerClient.redirect(postLoginRedirectURL);
return routerClient.redirect(postLoginRedirectURL);
}
return void routerClient.redirect(
return routerClient.redirect(
`${routerClient.clientConfig.siteUrl}${postLoginRedirectURL}`,
);
}

return void routerClient.redirect(routerClient.clientConfig.siteUrl);
return routerClient.redirect(routerClient.clientConfig.siteUrl);
};
2 changes: 1 addition & 1 deletion src/handlers/createOrg.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ export const createOrg = async (routerClient) => {
options,
);

return void routerClient.redirect(authUrl.toString());
return routerClient.redirect(authUrl.toString());
};
2 changes: 1 addition & 1 deletion src/handlers/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ export const login = async (routerClient) => {
);
}

return void routerClient.redirect(authUrl.toString());
return routerClient.redirect(authUrl.toString());
};
2 changes: 1 addition & 1 deletion src/handlers/logout.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@ export const logout = async (routerClient) => {
authUrl.searchParams.set("redirect", postLogoutRedirectURL);
}

return void routerClient.redirect(authUrl.toString());
return routerClient.redirect(authUrl.toString());
};
1 change: 1 addition & 0 deletions src/handlers/protect.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const protectPage =
const { isAuthenticated, getPermission, getPermissions, getRoles } =
kinde();
try {
console.log('protectPage', config)
const isSignedIn = await isAuthenticated();

if (!isSignedIn) {
Expand Down
2 changes: 1 addition & 1 deletion src/handlers/register.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ export const register = async (routerClient) => {
);
}

return void routerClient.redirect(authUrl.toString());
return routerClient.redirect(authUrl.toString());
};
2 changes: 2 additions & 0 deletions src/session/getAccessToken.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { config } from "../config/index";
import { jwtDecoder } from "@kinde/jwt-decoder";
import { getAccessToken } from "../utils/getAccessToken";
import { redirectOnExpiredToken } from "../utils/redirectOnExpiredToken";

/**
* @callback getAccessToken
Expand All @@ -17,6 +18,7 @@ import { getAccessToken } from "../utils/getAccessToken";
export const getAccessTokenFactory = (req, res) => async () => {
try {
const accessToken = await getAccessToken(req, res);
redirectOnExpiredToken(accessToken);
return jwtDecoder(accessToken);
} catch (err) {
if (config.isDebugMode) {
Expand Down
Loading
Loading