Skip to content

Instantly share code, notes, and snippets.

@bugproof
Created September 6, 2024 02:13
Show Gist options
  • Select an option

  • Save bugproof/dbef4972bc3d8c57b638490e316f42fa to your computer and use it in GitHub Desktop.

Select an option

Save bugproof/dbef4972bc3d8c57b638490e316f42fa to your computer and use it in GitHub Desktop.
Kinde Auth + Paraglide-Next
import {NextRequest, NextResponse} from 'next/server';
import jwt_decode, {JwtPayload} from 'jwt-decode';
import {isTokenValid} from '@kinde-oss/kinde-auth-nextjs';
import {removeTrailingSlash} from "next/dist/shared/lib/router/utils/remove-trailing-slash";
const KINDE_SITE_URL = removeTrailingSlash(process.env.KINDE_SITE_URL as string);
interface Options {
isReturnToCurrentPage?: boolean;
loginPage?: string;
publicPaths?: string[];
redirectURLBase?: string;
isAuthorized?: (params: { req: NextRequest; token: any }) => boolean;
lang?: string;
}
interface User {
family_name: string;
given_name: string;
email: string;
id: string;
picture: string;
}
interface OnSuccessParams {
token: any;
user: User;
}
type OnSuccessFunction = (params: OnSuccessParams) => Promise<NextResponse | void>;
const handleMiddleware = async (
req: NextRequest,
options?: Options,
onSuccess?: OnSuccessFunction
): Promise<NextResponse | void> => {
const { pathname } = req.nextUrl;
const isReturnToCurrentPage = options?.isReturnToCurrentPage;
const loginPage = options?.loginPage || '/api/auth/login';
let publicPaths = ['/_next', '/favicon.ico'];
if (options?.publicPaths !== undefined) {
if (Array.isArray(options?.publicPaths)) {
publicPaths = options.publicPaths;
}
}
let loginRedirectUrl = isReturnToCurrentPage
? `${loginPage}?post_login_redirect_url=${pathname}`
: loginPage;
if (options?.lang) {
loginRedirectUrl += `${loginRedirectUrl.includes('?') ? '&' : '?'}lang=${options.lang}`;
}
if (loginPage === pathname || publicPaths.some((p) => pathname.startsWith(p)))
return;
const kindeToken = req.cookies.get('access_token');
if (!kindeToken) {
return NextResponse.redirect(
new URL(loginRedirectUrl, options?.redirectURLBase || KINDE_SITE_URL)
);
}
const accessTokenValue = jwt_decode<JwtPayload>(req.cookies.get('access_token')!.value);
const idTokenValue = jwt_decode<JwtPayload & User>(req.cookies.get('id_token')?.value ?? '');
const isAuthorized = options?.isAuthorized
? options.isAuthorized({ req, token: accessTokenValue })
: isTokenValid(kindeToken.value);
if (isAuthorized && onSuccess) {
return await onSuccess({
token: accessTokenValue,
user: {
family_name: idTokenValue.family_name,
given_name: idTokenValue.given_name,
email: idTokenValue.email,
id: idTokenValue.sub as string,
picture: idTokenValue.picture
}
});
}
if (isAuthorized) {
return;
}
return NextResponse.redirect(
new URL(loginRedirectUrl, options?.redirectURLBase || KINDE_SITE_URL)
);
};
interface KindeAuth {
user: User;
token: string;
}
type MiddlewareFunction = (
req: NextRequest & { kindeAuth?: KindeAuth },
...args: any[]
) => Promise<NextResponse | void>;
export function withCustomAuth(
reqOrOptionsOrMiddleware?: NextRequest | Options | MiddlewareFunction,
options?: Options
): MiddlewareFunction | Promise<NextResponse | void> {
// Case 1: Called with NextRequest (and possibly options)
if (reqOrOptionsOrMiddleware instanceof NextRequest) {
return handleMiddleware(reqOrOptionsOrMiddleware, options);
}
// Case 2: Called with options or middleware function
if (typeof reqOrOptionsOrMiddleware === 'function') {
// Case 2a: Called with middleware function
return async (req: NextRequest & { kindeAuth?: KindeAuth }, ...args: any[]) =>
await handleMiddleware(req, options, async ({ token, user }) => {
req.kindeAuth = { token, user };
return await reqOrOptionsOrMiddleware(req, ...args);
});
}
// Case 2b: Called with options or no arguments
const middlewareOptions = reqOrOptionsOrMiddleware as Options | undefined;
return async (req: NextRequest) => await handleMiddleware(req, middlewareOptions);
}
import {NextRequest, NextResponse} from 'next/server';
import { middleware as i18nMiddleware } from "@/lib/i18n";
import {withCustomAuth} from "@/customAuthMiddleware";
const protectedPaths = ["/dashboard", "/checkout"];
export default async function middleware(request: NextRequest) {
// Detect language
const lang = i18nMiddleware.detectLanguage(request);
// Function to check if the path is protected
const isProtectedPath = (path: string) => {
return protectedPaths.some(protectedPath => path.startsWith(protectedPath));
};
// Get path without language prefix (if it exists)
const pathWithoutLang = request.nextUrl.pathname.replace(/^\/[a-z]{2}\//, '/');
if (isProtectedPath(pathWithoutLang)) {
// For protected routes, apply Kinde Auth
const kindeLang = lang === "pt" ? "pt-PT" : lang;
const authResult = await withCustomAuth(request, {
isReturnToCurrentPage: true,
lang: kindeLang,
loginPage: pathWithoutLang === '/checkout' ? '/api/auth/register' : '/api/auth/login'
// Add any other options you need for withAuth here
});
if (authResult) {
// If authResult is not undefined, it means we need to redirect (e.g., to login)
return authResult;
}
}
// If we reach here, either the route is not protected or the user is authenticated
// Return the i18n response (or the default response if i18n didn't modify anything)
return i18nMiddleware.getResponse(request, lang) || NextResponse.next();
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico, sitemap.xml, robots.txt (metadata files)
*/
'/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)'
]
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment