Skip to content

Commit

Permalink
Merge pull request #254 from kinde-oss/fix/refresh-token-app-router
Browse files Browse the repository at this point in the history
Fix/refresh token app router
  • Loading branch information
DanielRivers authored Dec 20, 2024
2 parents 14fe7fa + cd91971 commit 2bc539c
Show file tree
Hide file tree
Showing 26 changed files with 368 additions and 131 deletions.
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({ });
}
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;
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"
]
}
169 changes: 146 additions & 23 deletions src/authMiddleware/authMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
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";
import { copyCookiesToRequest } from "../utils/copyCookiesToRequest";

const handleMiddleware = async (req, options, onSuccess) => {
const { pathname } = req.nextUrl;
Expand All @@ -21,36 +28,118 @@ const handleMiddleware = async (req, options, onSuccess) => {
? `${loginPage}?post_login_redirect_url=${pathname}`
: loginPage;

if (loginPage == pathname || publicPaths.some((p) => pathname.startsWith(p)))
return;
const isPublicPath = loginPage == pathname || publicPaths.some((p) => pathname.startsWith(p));

const { value: kindeToken } = req.cookies.get("access_token");
// getAccessToken will validate the token
let kindeAccessToken = await getAccessToken(req);
// getIdToken will validate the token
let kindeIdToken = await getIdToken(req);

if (!kindeToken) {
const response = NextResponse.redirect(

// if no access token, redirect to login
if ((!kindeAccessToken || !kindeIdToken) && !isPublicPath) {
if(config.isDebugMode) {
console.log('authMiddleware: no access or id token, redirecting to login')
}
return NextResponse.redirect(
new URL(loginRedirectUrl, options?.redirectURLBase || config.redirectURL),
);
return response;
}

const accessTokenValue = jwtDecoder<KindeAccessToken>(
req.cookies.get("access_token")?.value,
);
const idTokenValue = jwtDecoder<KindeIdToken>(
req.cookies.get("id_token")?.value,
);
const session = await sessionManager(req)
let refreshResponse: OAuth2CodeExchangeResponse | null = null
const resp = NextResponse.next();

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

try {
refreshResponse = await kindeClient.refreshTokens(session);
kindeAccessToken = refreshResponse.access_token
kindeIdToken = refreshResponse.id_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 splitAccessTokenCookies = getSplitCookies("access_token", refreshResponse.access_token)
splitAccessTokenCookies.forEach((cookie) => {
resp.cookies.set(cookie.name, cookie.value, cookie.options);
})

const splitIdTokenCookies = getSplitCookies("id_token", refreshResponse.id_token)
splitIdTokenCookies.forEach((cookie) => {
resp.cookies.set(cookie.name, cookie.value, cookie.options);
})

// copy the cookies from the response to the request
// in Next versions prior to 14.2.8, the cookies function
// reads the Set-Cookie header from the *request* object, not the *response* object
// in order to get the new cookies to the request, we need to copy them over
copyCookiesToRequest(req, resp)

if(config.isDebugMode) {
console.log('authMiddleware: tokens refreshed')
}
} catch(error) {
// token is expired and refresh failed, redirect to login
if(config.isDebugMode) {
console.error('authMiddleware: token refresh failed, redirecting to login')
}

if(!isPublicPath) {
return NextResponse.redirect(
new URL(loginRedirectUrl, options?.redirectURLBase || config.redirectURL),
);
}
}
}

// we don't bail out earlier than here because we want to refresh the tokens
// if they are expired, even if the path is public
if(isPublicPath) {
return resp;
}

let accessTokenValue: KindeAccessToken | null = null
let idTokenValue: KindeIdToken | null = null

try {
accessTokenValue = jwtDecoder<KindeAccessToken>(
kindeAccessToken,
);
} catch(error) {
if(config.isDebugMode) {
console.error('authMiddleware: access token decode failed, redirecting to login')
}
return NextResponse.redirect(
new URL(loginRedirectUrl, options?.redirectURLBase || config.redirectURL),
);
}

// check token is valid
const isTokenValid = await validateToken({
token: kindeToken,
});
try {
idTokenValue = jwtDecoder<KindeIdToken>(
kindeIdToken,
);
} catch(error) {
if(config.isDebugMode) {
console.error('authMiddleware: id token decode failed, redirecting to login')
}
return NextResponse.redirect(
new URL(loginRedirectUrl, options?.redirectURLBase || config.redirectURL),
);
}

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

if (isTokenValid && customValidationValid && onSuccess) {
return await onSuccess({
if (customValidationValid && onSuccess) {
if(config.isDebugMode) {
console.log('authMiddleware: invoking onSuccess callback')
}
const callbackResult = await onSuccess({
token: accessTokenValue,
user: {
family_name: idTokenValue.family_name,
Expand All @@ -60,10 +149,44 @@ const handleMiddleware = async (req, options, onSuccess) => {
picture: idTokenValue.picture,
},
});

// If a user returned a response from their onSuccess callback, copy our refreshed tokens to it
if (callbackResult instanceof NextResponse) {
if(config.isDebugMode) {
console.log('authMiddleware: onSuccess callback returned a response, copying our cookies to it')
}
// Copy our cookies to their response
resp.cookies.getAll().forEach(cookie => {
callbackResult.cookies.set(cookie.name, cookie.value, {
...cookie
});
});

// Copy any headers we set (if any) to their response
resp.headers.forEach((value, key) => {
callbackResult.headers.set(key, value);
});

return callbackResult;
}

// If they didn't return a response, return our response with the refreshed tokens
if(config.isDebugMode) {
console.log('authMiddleware: onSuccess callback did not return a response, returning our response')
}

return resp;
}

if (customValidationValid) {
if(config.isDebugMode) {
console.log('authMiddleware: customValidationValid is true, returning response')
}
return resp;
}

if (isTokenValid && customValidationValid) {
return NextResponse.next();
if(config.isDebugMode) {
console.log('authMiddleware: default behaviour, redirecting to login')
}

return NextResponse.redirect(
Expand Down Expand Up @@ -97,4 +220,4 @@ export function withAuth(...args) {
const options = args[0];
// @ts-ignore
return async (...args) => await handleMiddleware(args[0], options);
}
}
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);
};
Loading

0 comments on commit 2bc539c

Please sign in to comment.