Next.js Pages Router SDK
SDKs and APIs
This SDK is for developers already using the Next.js SDK v1.8.25 or earlier document. It is relevant for Next.js version 13+ and uses Server Side Components and App Router.
If you haven’t already got a Kinde account, register for free here (no credit card required). Registering gives you a Kinde domain, which you need to get started, e.g. yourapp.kinde.com.
You can also view the Next.js docs and Next.js starter kit in GitHub.
npm i @kinde-oss/kinde-auth-nextjs@1
yarn add @kinde-oss/kinde-auth-nextjs@1
pnpm add @kinde-oss/kinde-auth-nextjs@1
http://localhost:3000/api/auth/kinde_callback
http://localhost:3000
If you would like to use our Environments feature as part of your development process. You will need to create them first within your Kinde account. In this case you would use the Environment subdomain in the code block above.
Put these variables in your .env.local
file. You can find these variables on your Kinde Settings > Applications > [Your app] > View details page.
Replace the information in the <angle brackets> with your own information. You might also set different URLs depending where your project is running. They need to be the same as the callback URLs you entered in Kinde, above.
KINDE_CLIENT_ID=<your_kinde_client_id>KINDE_CLIENT_SECRET=<your_kinde_client_secret>KINDE_ISSUER_URL=https://<your_kinde_subdomain>.kinde.com
KINDE_SITE_URL=http://localhost:3000KINDE_POST_LOGOUT_REDIRECT_URL=http://localhost:3000KINDE_POST_LOGIN_REDIRECT_URL=http://localhost:3000/dashboard
Create the following file src/app/api/auth/[kindeAuth]/route.js
inside your Next.js project. Inside the file route.js
put this code:
import {handleAuth} from "@kinde-oss/kinde-auth-nextjs/server";
export async function GET(request, {params}) { const endpoint = params.kindeAuth; return await handleAuth(request, endpoint);}
This will handle Kinde Auth endpoints in your Next.js app.
Important! Our SDK relies on this file existing in this location specified above.
The SDK ships with <LoginLink>
and <RegisterLink>
components which can be used to start the auth flow.
import {RegisterLink, LoginLink} from "@kinde-oss/kinde-auth-nextjs/server";
...
<LoginLink>Sign in</LoginLink><RegisterLink>Sign up</RegisterLink>
This is implemented in much the same way as signing up or signing in. A component is provided for you.
import {LogoutLink} from "@kinde-oss/kinde-auth-nextjs/server";
...
<LogoutLink>Log out</LogoutLink>
Register your first user by signing up yourself. You’ll see your newly registered user on the Users page in Kinde.
You can get an authorized user’s profile from any component using the Kinde Next.js getKindeServerSession
helper:
import {getKindeServerSession} from "@kinde-oss/kinde-auth-nextjs/server";
const SayHello = async () => { const {getUser} = getKindeServerSession(); const user = await getUser(); return <p>Hi {user.given_name}!</p>;};
To be on the safe side we also provide isAuthenticated
flag in this helper:
import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server";
const UserProfile = async () => { const { getUser, isAuthenticated } = getKindeServerSession(); const user = await getUser();
return ( { (await isAuthenticated()) ? <div> <h2>{user.given_name}</h2> <p>{user.email}</p> </div> : <p>Please sign in or register!</p> } );};
In Next.js you can opt into using Client Components which give you interactivity benefits and access to the browser APIs. You can read more about them in the Next.js docs.
To get the Kinde session data in your Client Components follow these two steps:
Create an API route in your Next.js project that returns the data from getKindeServerSession
.
import {getKindeServerSession} from "@kinde-oss/kinde-auth-nextjs/server";import {NextResponse} from "next/server";
export async function GET() { const {getUser, isAuthenticated, getPermissions, getOrganization} = getKindeServerSession(); const user = await getUser(); const authenticated = await isAuthenticated(); const permissions = await getPermissions(); const organization = await getOrganization();
return NextResponse.json({user, authenticated, permissions, organization});}
Fetch the data from the API endpoint in your component inside a useEffect
and then save the data to the component state.
// some client component
"use client";
import { useEffect, useState } from "react";
export default function UploadProfilePic() { const [user, setUser] = useState<any>(); const [authStatus, setAuthStatus] = useState(null);
console.log(user);
useEffect(() => { const getKindeSession = async () => { const res = await fetch("/api/kindeSession"); const data = await res.json(); setUser(data.user); setAuthStatus(data.authenticated); };
getKindeSession(); }, []);
It’s likely that your application will have both pages that are publicly available and private ones which should only be available to logged in users.
Inside your src
folder create a middleware.js
file containing the following code:
import {authMiddleware} from "@kinde-oss/kinde-auth-nextjs/server";
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 (favicon file) */ "/((?!api|_next/static|_next/image|favicon.ico).*)" ]};
export default authMiddleware;
Create a page at src/app/dashboard/page.js
export default function Dashboard() { return ( <div> <p>Welcome to the dashboard!</p> </div> );}
Try to access this page when signed in and when signed out. Notice how you’ll be redirected to the home page when not authenticated.
The getKindeServerSession
helper is also available in your API. Create an endpoint in the new App Router pattern at app/api/protected/route.js
and include the following code block:
import {NextResponse} from "next/server";import {getKindeServerSession} from "@kinde-oss/kinde-auth-nextjs/server";
export async function GET() { const {getUser, isAuthenticated} = getKindeServerSession();
if (await !isAuthenticated()) { return new Response("Unauthorized", {status: 401}); } const user = await getUser(); const data = {message: "Hello User", id: user.id};
return NextResponse.json({data});}
This will check if the user is authenticated, and if not, will throw a 401 error.
Once a user has been verified as signed in, your product/application will receive the JWT token with an array of permissions for that user. You will need to configure your product/application to read permissions and unlock the respective functions.
You set permissions in your Kinde account (see help article), the below is an example set of permissions.
"permissions":[ "create:todos", "update:todos", "read:todos", "delete:todos", "create:tasks", "update:tasks", "read:tasks", "delete:tasks"]
We provide helper functions to more easily access permissions:
const {getPermission, getPermissions} = getKindeServerSession();
getPermission("create:todos");// {orgCode: "org_1234", isGranted: true}
getPermissions();// {orgCode: "org_1234", permissions: ["create:todos", "update:todos", "read:todos"]}
A practical example in code might look something like:
{ (await getPermission("create:todos").isGranted) ? <button>Create todo</button> : null;}
An audience
is the intended recipient of an access token - for example the API for your application. The audience
argument can be set against KINDE_AUDIENCE
in your environment variables.
The audience of a token is the intended recipient of the token.
// .env file
KINDE_AUDIENCE = your_audience;
For details on how to connect, see Register an API.
When a user signs in the Access token your product/application receives contains a custom claim called feature_flags
which is an object detailing the feature flags for that user.
You can set feature flags in your Kinde account. Here’s an example.
feature_flags: { theme: { "t": "s", "v": "pink" }, is_dark_mode: { "t": "b", "v": true }, competitions_limit: { "t": "i", "v": 5 }}
In order to minimize the payload in the token we have used single letter keys / values where possible. The single letters represent the following:
t
= type
v
= value
s
= string
b
= boolean
i
= integer
We provide helper functions to more easily access feature flags:
/** * Get a flag from the feature_flags claim of the access_token. * @param {string} code - The name of the flag. * @param {obj} [defaultValue] - A fallback value if the flag isn't found. * @param {'s'|'b'|'i'|undefined} [flagType] - The data type of the flag (integer / boolean / string). * @return {object} Flag details.*/const { getFlag } = getKindeServerSession();
/* Example usage */getFlag('theme');/*{// "code": "theme",// "type": "string",// "value": "pink",// "is_default": false // whether the fallback value had to be used*/}
getFlag('create_competition', {defaultValue: false});/*{ "code": "create_competition", "value": false, "is_default": true // because fallback value had to be used}*/
A practical example in code might look something like:
const {getFlag} = getKindeServerSession();
{ (await getFlag("create_competition").value) ? <button>Create competition</button> : null;}
We also require wrapper functions by type which should leverage getFlag
above.
Booleans:
/** * Get a boolean flag from the feature_flags claim of the access_token. * @param {string} code - The name of the flag. * @param {bool} [defaultValue] - A fallback value if the flag isn't found. * @return {bool} */const {getBooleanFlag} = getKindeServerSession();
/* Example usage */getBooleanFlag("is_dark_mode");// true
getBooleanFlag("is_dark_mode", false);// true
getBooleanFlag("new_feature", false);// false (flag does not exist so falls back to default)
Strings and integers work in the same way as booleans above:
/** * Get a string flag from the feature_flags claim of the access_token. * @param {string} code - The name of the flag. * @param {string} [defaultValue] - A fallback value if the flag isn't found. * @return {string} */const {getStringFlag} = getKindeServerSession();
/** * Get an integer flag from the feature_flags claim of the access_token. * @param {string} code - The name of the flag. * @param {int} [defaultValue] - A fallback value if the flag isn't found. * @return {int} */const {getIntegerFlag} = getKindeServerSession();
A practical example in code might look something like:
const {getBooleanFlag, getStringFlag} = getKindeServerSession();
{ (await getBooleanFlag("create_competition")) ? ( <button className={`theme-${getStringFlag("theme")}`}>Create competition</button> ) : null;}
To have a new organization created within your application, you can use the <CreateOrgLink>
component that ships with the SDK. This will redirect the user to Kinde and create an organization with them as a member.
import {CreateOrgLink} from "@kinde-oss/kinde-auth-nextjs/server";
...
<CreateOrgLink orgName="My org">Create Org</CreateOrgLink>
Every organization in Kinde has a unique code. To sign up a new user into a particular organization you will need to pass through this code in the <RegisterLink />
component.
import {RegisterLink} from "@kinde-oss/kinde-auth-nextjs/server";
...
<RegisterLink orgCode="org_123456">Sign up to org</RegisterLink>
This code should also be passed along with the <LoginLink>
component if you wish for a user to be logged into a specific organization.
<LoginLink orgCode="org_123456">Sign into org</LoginLink>
For more information about using organizations, see Kinde organizations for developers.
You need to enable the application’s access to the Kinde Management API. You can do this in Kinde by going to Settings > APIs > Kinde Management API and then toggling on your Next.js application under the Applications tab.
To use our management API please see @kinde/management-api-js
If you want your project to remember which url your user was intending to visit before they were asked to authenticate, you can pass an additional parameter in the /login
and /register
links.
After the user has completed authentication at your defined callback url they will be redirected to the path you define here. This value does not need to be added to your allowed callback urls in Kinde.
Next.js 13
// Next.js 13{/* eslint-disable-next-line @next/next/no-html-link-for-pages */}<a href="api/auth/login?post_login_redirect_url=/dashboard"> Sign in</a>
<LoginLink postLoginRedirectURL={'/dashboard'}>Login</LoginLink><RegisterLink postLoginRedirectURL={'/dashboard'}>Register</RegisterLink>
Note: the value of post_login_redirect_url
should either be a url on the same origin or a relative path.
Our Kinde Next.js SDK currently requires that these environment variables KINDE_SITE_URL
, KINDE_POST_LOGOUT_REDIRECT_URL
, and KINDE_POST_LOGIN_REDIRECT_URL
are defined, and that the callback URLs and logout redirect URLs are added to your app in Kinde.
To add Vercel’s dynamically generated URLs you can either securely use our API to add them on the fly or you can use wildcard URLs. It should be noted that whilst wildcards are more convenient it is not as secure as explicitly adding the url to the allowlist via API as we outline below.
Add the following to your next.config.js
.
/** @type {import('next').NextConfig} */const nextConfig = { env: { KINDE_SITE_URL: process.env.KINDE_SITE_URL ?? `https://${process.env.VERCEL_URL}`, KINDE_POST_LOGOUT_REDIRECT_URL: process.env.KINDE_POST_LOGOUT_REDIRECT_URL ?? `https://${process.env.VERCEL_URL}`, KINDE_POST_LOGIN_REDIRECT_URL: process.env.KINDE_POST_LOGIN_REDIRECT_URL ?? `https://${process.env.VERCEL_URL}/dashboard` }};
module.exports = nextConfig;
This ensures Vercel uses its generated preview URLs to populate the three Kinde variables.
KINDE_POST_LOGIN_REDIRECT_URL
)next.config.js
file.Create a script that will run each time a new preview is deployed by Vercel, which will add the newly generated URL to Kinde.
Enable the Kinde Management API for your Next.js app.
In your application source code, create a folder at the top level called scripts
.
Within that folder, create a file called add-urls-to-kinde.js
and add the following code:
async function getAuthToken() { try { const response = await fetch(`${process.env.KINDE_ISSUER_URL}/oauth2/token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" }, body: new URLSearchParams({ client_id: process.env.KINDE_CLIENT_ID, client_secret: process.env.KINDE_CLIENT_SECRET, grant_type: "client_credentials", audience: `${process.env.KINDE_ISSUER_URL}/api` }) });
if (!response.ok) { throw new Error(`Failed to get auth token: ${response.statusText}`); }
const data = await response.json(); return data.access_token; } catch (error) { console.error("Error getting auth token:", error); throw error; }}
async function addLogoutUrlToKinde(token) { try { const response = await fetch( `${process.env.KINDE_ISSUER_URL}/api/v1/applications/${process.env.KINDE_CLIENT_ID}/auth_logout_urls`, { method: "POST", headers: { Authorization: `Bearer ${token}`, Accept: "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ urls: [`https://${process.env.VERCEL_URL}`] }) } );
if (!response.ok) { throw new Error(`Failed to add logout URL to Kinde: ${response.statusText}`); }
const responseData = await response.json(); console.log(`SUCCESS: Logout URL added to Kinde: ${process.env.VERCEL_URL}`, responseData); } catch (error) { console.error("Failed to add logout URL to Kinde", error); throw error; }}
async function addCallbackUrlToKinde(token) { try { const response = await fetch( `${process.env.KINDE_ISSUER_URL}/api/v1/applications/${process.env.KINDE_CLIENT_ID}/auth_redirect_urls`, { method: "POST", headers: { Authorization: `Bearer ${token}`, Accept: "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ urls: [`https://${process.env.VERCEL_URL}/api/auth/kinde_callback`] }) } );
if (!response.ok) { throw new Error(`Failed to add callback URL to Kinde: ${response.statusText}`); }
const responseData = await response.json(); console.log( `SUCCESS: Callback URL added to Kinde: ${process.env.VERCEL_URL}/api/auth/kinde_callback`, responseData ); } catch (error) { console.error("Failed to add callback URL to Kinde", error); throw error; }}
(async () => { if (process.env.VERCEL == 1) { try { const authToken = await getAuthToken(); await addCallbackUrlToKinde(authToken); await addLogoutUrlToKinde(authToken); } catch (error) { console.error("Script failed:", error); } }})();
In your package.json
, add a postbuild
script that will run the /scripts/add-urls-to-kinde.js
file after Vercel builds your app.
"scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint", "postbuild": "node ./scripts/add-urls-to-kinde.js"}
Commit these changes. The next deploy will add the newly created preview URLs to your Kinde application.