Skip to content
  • SDKs and APIs
  • Back end SDKs

Next.js App Router SDK

This SDK is for Next.js version 13+ and uses Server Side Components and App Router.

New to Kinde? Get started here

Install for a new project

Link to this section

The easiest way to get started is to use the Next.js starter kit, and watch a demo video.

Install for an existing project

Link to this section
Terminal window
npm i @kinde-oss/kinde-auth-nextjs

Set callback URLs

Link to this section
  1. In Kinde, go to Settings > Applications > [Your app] > View details.
  2. Add your callback URLs in the relevant fields. For example:
    • Allowed callback URLs (also known as redirect URIs) - for example http://localhost:3000/api/auth/kinde_callback
    • Allowed logout redirect URLs - for example http://localhost:3000
  3. Select Save.

Configure environment variables

Link to this section

Put these variables in a .env.local file in the root of your Next.js app. You can find these variables on your Kinde Settings > Applications > [Your app] > View details page.

  • KINDE_CLIENT_ID - Your business’s unique ID on Kinde
  • KINDE_CLIENT_SECRET - Your business’s secret key (do not share)
  • KINDE_ISSUER_URL - your kinde domain
  • KINDE_SITE_URL - where your app is running
  • KINDE_POST_LOGOUT_REDIRECT_URL - where you want users to be redirected to after logging out. Make sure this URL is under your allowed logout redirect URLs.
  • KINDE_POST_LOGIN_REDIRECT_URL - where you want users to be redirected to after authenticating.
  • KINDE_AUDIENCE - optional - a whitespace separated list of audiences to populate the aud claim in the token.

Replace the information in the example with your own information. You might also set different URLs depending where your project is running. They need to match the callback URLs you entered in Kinde, above.

Terminal window
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:3000
KINDE_POST_LOGOUT_REDIRECT_URL=http://localhost:3000
KINDE_POST_LOGIN_REDIRECT_URL=http://localhost:3000/dashboard

Set up Kinde Auth Route Handlers

Link to this section

Create the following file 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 const GET = handleAuth();

This will handle Kinde Auth endpoints in your Next.js app.

Important! Our SDK relies on this file existing in this location specified above.

Set up the Kinde Auth Provider

Link to this section

Wrap your app in the Kinde Auth Provider. This will give you access to the Kinde Auth data in your app and will ensure that the tokens are refreshed when needed.

Create a file AuthProvider.tsx in your app directory.

AuthProvider.tsx
"use client";
import {KindeProvider} from "@kinde-oss/kinde-auth-nextjs";
export const AuthProvider = ({children}) => {
return <KindeProvider>{children}</KindeProvider>;
};

Then wrap your app in the AuthProvider component.

layout.tsx
...
import {AuthProvider} from './AuthProvider';
export const metadata = {
title: 'Kinde Auth',
description: 'Kinde with NextJS App Router'
};
export default async function RootLayout({
children
}: {
children: React.ReactNode;
}) {
return (
<AuthProvider>
<html lang="en">
// Your app code here
</html>
</AuthProvider>
);
}

Authentication

Link to this section

Sign up and sign in

Link to this section

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/components";
...
<LoginLink>Sign in</LoginLink>
<RegisterLink>Sign up</RegisterLink>

Redirecting after authentication

Link to this section

Static redirect

If you want to redirect users to a certain page after logging in, you can set the KINDE_POST_LOGIN_REDIRECT_URL environment variable in your .env.local file.

Dynamic redirect

You can also set a postLoginRedirectURL parameter to tell us where to redirect after authenticating.

import {RegisterLink, LoginLink} from "@kinde-oss/kinde-auth-nextjs/components";
...
<LoginLink postLoginRedirectURL="/dashboard">Sign in</LoginLink>
<RegisterLink postLoginRedirectURL="/welcome">Sign up</RegisterLink>

This appends post_login_redirect_url to the search params when redirecting to Kinde Auth. You can achieve the same result as above, like this:

import { redirect } from "next/navigation";
...
redirect('/api/auth/login?post_login_redirect_url=/dashboard')
...

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/components";
...
<LogoutLink>Log out</LogoutLink>

Kinde Auth data - Server

Link to this section

You can get an authorized user’s Kinde Auth data from any server component using the getKindeServerSession helper.

MethodDescription
isAuthenticatedCheck if the user is authenticated
getUserGet the current user’s details
getOrganizationGet the current user’s organization
getUserOrganizationsGet all the organizations the current user belongs to
getPermissionCheck if the current user has a permission
getPermissionsGet the current user’s permissions
getFlagGet a feature flag
getBooleanFlagGet a boolean feature flag
getIntegerFlagGet an integer feature flag
getStringFlagGet a string feature flag
refreshTokensRefresh tokens to get up-to-date Kinde data
getAccessTokenGet the decoded access token
getAccessTokenRawGet the access token
getIdTokenGet the decoded ID token
getIdTokenRawGet the ID token
getClaimGet a claim from either token

isAuthenticated

Link to this section

Check if the user is authenticated.

import {getKindeServerSession} from "@kinde-oss/kinde-auth-nextjs/server";
const {isAuthenticated} = getKindeServerSession();
const isUserAuthenticated = await isAuthenticated();
true;

Get the logged in user’s details.

import {getKindeServerSession} from "@kinde-oss/kinde-auth-nextjs/server";
const {getUser} = getKindeServerSession();
const user = await getUser();
console.log(user);
{
"id": "kp_123",
"email": "example@email.com",
"family_name": "Example",
"given_name": "User",
"picture": null,
"username": "ExampleUsername",
"phone_number": "1234567890",
"properties": {
"usr_city": "",
"usr_industry": "",
"usr_job_title": "",
"usr_middle_name": "",
"usr_postcode": "",
"usr_salutation": "",
"usr_state_region": "",
"usr_street_address": "",
"usr_street_address_2": ""
}
}

getOrganization

Link to this section

Get the current user’s organization

import {getKindeServerSession} from "@kinde-oss/kinde-auth-nextjs/server";
const {getOrganization} = getKindeServerSession();
const org = await getOrganization();
console.log(org);
{
"orgCode": "org_123",
"orgName": "Deafault Org",
"properties": {
"org_city": "",
"org_country": "",
"org_industry": "",
"org_postcode": "",
"org_state_region": "",
"org_street_address": "",
"org_street_address_2": ""
}
}

getUserOrganizations

Link to this section

Get all the organizations the current user belongs to

import {getKindeServerSession} from "@kinde-oss/kinde-auth-nextjs/server";
const {getUserOrganizations} = getKindeServerSession();
const userOrgs = await getUserOrganizations();
console.log(userOrgs);
{
"orgCodes": ["org_123", "org_456"],
"orgs": [
{
"code": "org_123",
"name": "Deafault Org"
},
{
"code": "org_456",
"name": "Another Org"
}
]
}

Check if the current user has a permission.

ParameterTypeDescription
codestringThe permission code to check
import {getKindeServerSession} from "@kinde-oss/kinde-auth-nextjs/server";
const {getPermission} = getKindeServerSession();
const canEatTacos = await getPermission("eat:tacos");
console.log(canEatTacos);
{
"isGranted": true,
"orgCode": "org_123"
}

getPermissions

Link to this section

Get the current user’s permissions.

import {getKindeServerSession} from "@kinde-oss/kinde-auth-nextjs/server";
const {getPermissions} = getKindeServerSession();
const permissions = await getPermissions();
console.log(permissions);
{
"permissions": ["eat:tacos", "read:books"],
"orgCode": "org_123"
}

Get a feature flag

ParameterTypeDescription
codestringThe flag code to check
defaultValueboolean | string | numberThe default value to return if the flag is not set
typeenum (b | s | i)The type of the flag
import {getKindeServerSession} from "@kinde-oss/kinde-auth-nextjs/server";
const {getFlag} = getKindeServerSession();
const billingFlag = await getFlag("billing", false, "b");
console.log(billingFlag);
{
"code": "billing",
"type": "boolean",
"value": true,
"defaultValue": false,
"is_default": false
}

getBooleanFlag

Link to this section

Get a boolean feature flag

ParameterTypeDescription
codestringThe flag code to check
defaultValuebooleanThe default value to return if the flag is not set
import {getKindeServerSession} from "@kinde-oss/kinde-auth-nextjs/server";
const {getBooleanFlag} = getKindeServerSession();
const billingFlag = await getBooleanFlag("billing", false);
console.log(billingFlag);
true;

getIntegerFlag

Link to this section

Get a boolean feature flag

ParameterTypeDescription
codestringThe flag code to check
defaultValuebooleanThe default value to return if the flag is not set
import {getKindeServerSession} from "@kinde-oss/kinde-auth-nextjs/server";
const {getIntegerFlag} = getKindeServerSession();
const billingVersion = await getIntegerFlag("billingVersion", 0);
console.log(billingVersion);
2

Get a string feature flag

ParameterTypeDescription
codestringThe flag code to check
defaultValuestringThe default value to return if the flag is not set
import {getKindeServerSession} from "@kinde-oss/kinde-auth-nextjs/server";
const {getStringFlag} = getKindeServerSession();
const theme = await getStringFlag("theme", "system");
console.log(theme);
"light"

Refresh tokens to get up-to-date Kinde data

import {getKindeServerSession} from "@kinde-oss/kinde-auth-nextjs/server";
import {someUpdateFunction} from "@/app/actions";
const {refreshTokens} = getKindeServerSession();
await someUpdateFunction({
param_1: "value_1",
param_2: "value_2"
});
await refreshTokens();

getAccessToken

Link to this section

Get the decoded access token

import {getKindeServerSession} from "@kinde-oss/kinde-auth-nextjs/server";
const {getAccessToken} = getKindeServerSession();
const accessToken = await getAccessToken();
console.log(accessToken);
{
"aud": ["your-api"],
"azp": 1234567890,
"email": "example@email.com",
"exp": 1234567890,
"feature_flags": {
"isonboardingcomplete": {
"t": "b",
"v": false
}
},
"iat": 1234567890,
"iss": "https://your-kinde-subdomain.kinde.com",
"jti": "7802e2d2-asdf-431e-bc72-5ed95asdf475d",
"org_code": "org_123",
"org_name": "Default Org",
"organization_properties": {
"kp_org_city": {}
},
"permissions": ["create:template"],
"roles": [
{
"id": "018ee9aa-f92b-83fc-1d40-1234567890",
"key": "admin",
"name": "Admin"
}
],
"scp": ["openid", "profile", "email", "offline"],
"sub": "kp_6123456789009876",
"user_properties": {
"kp_usr_city": {}
}
}

getAccessTokenRaw

Link to this section

Get the access token

import {getKindeServerSession} from "@kinde-oss/kinde-auth-nextjs/server";
const {getAccessTokenRaw} = getKindeServerSession();
const accessToken = await getAccessTokenRaw();
console.log(accessToken);
eyJhxxx.eyJhdxxx.A4djjxxx

Get the decoded ID token

import {getKindeServerSession} from "@kinde-oss/kinde-auth-nextjs/server";
const {getIdToken} = getKindeServerSession();
const idToken = await getIdToken();
console.log(idToken);
{
"at_hash": "ZY6jx1SGjzgkHGJ_2Jabcd",
"aud": ["feb8e697b967466eacb96d26c5ca0e12"],
"auth_time": 1234567890,
"azp": "feb8e099xxx",
"email": "example@email.com",
"email_verified": true,
"exp": 123456789,
"ext_provider": {
"claims": {
"connection_id": "bcc486xxx",
"email": "example@email.com",
"family_name": "User",
"given_name": "Example",
"is_confirmed": true,
"picture": "https://lh3.googleusercontent.com/a/ACgxxx",
"profile": {
"email": "example@email.com",
"family_name": "User",
"given_name": "Example",
"id": "1234567890",
"name": "Example user",
"picture": "https://lh3.googleusercontent.com/a/ACgxxx",
"verified_email": true
}
},
"connection_id": "bccxxx",
"name": "Google"
},
"family_name": "User",
"given_name": "Example",
"iat": 1234567890,
"iss": "https://your-kinde-subdomain.kinde.com",
"jti": "e7e18303-0ea5-402d-932c-xxx",
"name": "Example user",
"org_codes": ["org_123"],
"organizations": [
{
"id": "org_123",
"name": "Default Organization"
}
],
"picture": "https://lh3.googleusercontent.com/a/ACgxxx",
"rat": 1234567890,
"sub": "kp_1234567890",
"updated_at": 1234567890,
"user_properties": {
"kp_usr_city": {}
}
}

Get the ID token

import {getKindeServerSession} from "@kinde-oss/kinde-auth-nextjs/server";
const {getIdTokenRaw} = getKindeServerSession();
const idToken = await getIdTokenRaw();
console.log(idToken);
eyJhxxx.eyJhdxxx.A4djjxxx

Get a claim from either token

ParameterTypeDescription
claimstringThe claim key
typeenum (access_token | id_token)The token to get the claim from
import {getKindeServerSession} from "@kinde-oss/kinde-auth-nextjs/server";
const {getClaim} = getKindeServerSession();
const username = await getClaim("preferred_username", "id_token");
console.log(idToken);
"exampleUsername"

Kinde Auth data - Client

Link to this section

You can get an authorized user’s Kinde Auth data from any client component using the useKindeBrowser helper.

Variable / MethodDescription
isAuthenticatedGet a feature flag
user / getUserThe current in user’s details
organization / getOrganizationThe current user’s organization
userOrganizations / getUserOrganizationsAll the organizations the current user belongs to
getPermissionCheck if the current user has a permission
permissions / getPermissionsThe current user’s permissions
getFlagGet the access token
getBooleanFlagGet the access token
getIntegerFlagGet the access token
getStringFlagGet the access token
refreshDataRefresh tokens to get up-to-date Kinde data
accessToken / getAccessTokenCheck if the user is authenticated
accessTokenRaw / getAccessTokenRawGet the current user’s details
idToken / getIdTokenGet all the organizations the current user belongs to
idTokenRaw / getIdTokenRawCheck if the current user has a permission
isLoadingIs Kinde data loading
errorError message if there is an error

Tip: Use isLoading to ensure the data is up to date. You can return a loading spinner or something similar if you want.

isAuthenticated

Link to this section

Check if the user is authenticated.

"use client";
import {useKindeBrowserClient} from "@kinde-oss/kinde-auth-nextjs";
const {isAuthenticated} = useKindeBrowserClient();
console.log(isAuthenticated);
true;

user / getUser

Link to this section

Get the logged in user’s details.

"use client";
import {useKindeBrowserClient} from "@kinde-oss/kinde-auth-nextjs";
const {user, getUser} = useKindeBrowserClient();
const alsoUser = getUser();
console.log(user);
{
"id": "kp_123",
"email": "example@email.com",
"family_name": "Example",
"given_name": "User",
"picture": null,
"username": "ExampleUsername",
"phone_number": "1234567890",
"properties": {
"usr_city": "",
"usr_industry": "",
"usr_job_title": "",
"usr_middle_name": "",
"usr_postcode": "",
"usr_salutation": "",
"usr_state_region": "",
"usr_street_address": "",
"usr_street_address_2": ""
}
}

organization / getOrganization

Link to this section

Get the current user’s organization

"use client";
import {useKindeBrowserClient} from "@kinde-oss/kinde-auth-nextjs";
const {organization, getOrganization} = useKindeBrowserClient();
const org = getOrganization();
console.log(organization, org);
{
"orgCode": "org_123",
"orgName": "Deafault Org",
"properties": {
"org_city": "",
"org_country": "",
"org_industry": "",
"org_postcode": "",
"org_state_region": "",
"org_street_address": "",
"org_street_address_2": ""
}
}

userOrganizations / getUserOrganizations

Link to this section

Get all the organizations the current user belongs to

"use client";
import {useKindeBrowserClient} from "@kinde-oss/kinde-auth-nextjs";
const {userOrganizations, getUserOrganizations} = useKindeBrowserClient();
const userOrgs = getUserOrganizations();
console.log(userOrganizations, userOrgs);
{
"orgCodes": ["org_123", "org_456"],
"orgs": [
{
"code": "org_123",
"name": "Deafault Org"
},
{
"code": "org_456",
"name": "Another Org"
}
]
}

Check if the current user has a permission.

ParameterTypeDescription
codestringThe permission code to check
"use client";
import {useKindeBrowserClient} from "@kinde-oss/kinde-auth-nextjs";
const {getPermission} = useKindeBrowserClient();
const canEatTacos = getPermission("eat:tacos");
console.log(canEatTacos);
{
"isGranted": true,
"orgCode": "org_123"
}

permissions / getPermissions

Link to this section

Get the current user’s permissions.

"use client";
import {useKindeBrowserClient} from "@kinde-oss/kinde-auth-nextjs";
const {permissions, getPermissions} = useKindeBrowserClient();
const perms = getPermissions();
console.log(permissions, permis);
{
"permissions": ["eat:tacos", "read:books"],
"orgCode": "org_123"
}

Get a feature flag

ParameterTypeDescription
codestringThe flag code to check
defaultValueboolean | string | numberThe default value to return if the flag is not set
typeenum (b | s | i)The type of the flag
"use client";
import {useKindeBrowserClient} from "@kinde-oss/kinde-auth-nextjs";
const {getFlag} = useKindeBrowserClient();
const billingFlag = getFlag("billing", false, "b");
console.log(billingFlag);
{
"code": "billing",
"type": "boolean",
"value": true,
"defaultValue": false,
"is_default": false
}

getBooleanFlag

Link to this section

Get a boolean feature flag

ParameterTypeDescription
codestringThe flag code to check
defaultValuebooleanThe default value to return if the flag is not set
"use client";
import {useKindeBrowserClient} from "@kinde-oss/kinde-auth-nextjs";
const {getBooleanFlag} = useKindeBrowserClient();
const billingFlag = getBooleanFlag("billing", false);
console.log(billingFlag);
true;

getIntegerFlag

Link to this section

Get a boolean feature flag

ParameterTypeDescription
codestringThe flag code to check
defaultValuebooleanThe default value to return if the flag is not set
"use client";
import {useKindeBrowserClient} from "@kinde-oss/kinde-auth-nextjs";
const {getIntegerFlag} = useKindeBrowserClient();
const billingVersion = getIntegerFlag("billingVersion", 0);
console.log(billingVersion);
2

Get a string feature flag

ParameterTypeDescription
codestringThe flag code to check
defaultValuestringThe default value to return if the flag is not set
"use client";
import {useKindeBrowserClient} from "@kinde-oss/kinde-auth-nextjs";
const {getStringFlag} = useKindeBrowserClient();
const theme = getStringFlag("theme", "system");
console.log(theme);
"light"

Refresh tokens to get up-to-date Kinde data

"use client";
import {useKindeBrowserClient} from "@kinde-oss/kinde-auth-nextjs";
import {someUpdateFunction} from "@/app/actions";
const {refreshData} = useKindeBrowserClient();
await someUpdateFunction({
param_1: "value_1",
param_2: "value_2"
});
await refreshData();

accessToken / getAccessToken

Link to this section

Get the decoded access token

"use client";
import {useKindeBrowserClient} from "@kinde-oss/kinde-auth-nextjs";
const {accessToken, getAccessToken} = useKindeBrowserClient();
const aTok = getAccessToken();
console.log(accessToken, aTok);
{
"aud": ["your-api"],
"azp": 1234567890,
"email": "example@email.com",
"exp": 1234567890,
"feature_flags": {
"isonboardingcomplete": {
"t": "b",
"v": false
}
},
"iat": 1234567890,
"iss": "https://your-kinde-subdomain.kinde.com",
"jti": "7802e2d2-asdf-431e-bc72-5ed95asdf475d",
"org_code": "org_123",
"org_name": "Default Org",
"organization_properties": {
"kp_org_city": {}
},
"permissions": ["create:template"],
"roles": [
{
"id": "018ee9aa-f92b-83fc-1d40-1234567890",
"key": "admin",
"name": "Admin"
}
],
"scp": ["openid", "profile", "email", "offline"],
"sub": "kp_6123456789009876",
"user_properties": {
"kp_usr_city": {}
}
}

accessTokenRaw / getAccessTokenRaw

Link to this section

Get the access token

import {useKindeBrowserClient} from "@kinde-oss/kinde-auth-nextjs";
const {accessTokenRaw, getAccessTokenRaw} = useKindeBrowserClient();
const aTokRaw = getAccessTokenRaw();
console.log(accessTokenRaw, aTokRaw);
eyJhxxx.eyJhdxxx.A4djjxxx

idToken / getIdToken

Link to this section

Get the decoded ID token

import {useKindeBrowserClient} from "@kinde-oss/kinde-auth-nextjs";
const {idToken, getIdToken} = useKindeBrowserClient();
const idTok = getIdToken();
console.log(idToken, idTok);
{
"at_hash": "ZY6jx1SGjzgkHGJ_2Jabcd",
"aud": ["feb8e697b967466eacb96d26c5ca0e12"],
"auth_time": 1234567890,
"azp": "feb8e099xxx",
"email": "example@email.com",
"email_verified": true,
"exp": 123456789,
"ext_provider": {
"claims": {
"connection_id": "bcc486xxx",
"email": "example@email.com",
"family_name": "User",
"given_name": "Example",
"is_confirmed": true,
"picture": "https://lh3.googleusercontent.com/a/ACgxxx",
"profile": {
"email": "example@email.com",
"family_name": "User",
"given_name": "Example",
"id": "1234567890",
"name": "Example user",
"picture": "https://lh3.googleusercontent.com/a/ACgxxx",
"verified_email": true
}
},
"connection_id": "bccxxx",
"name": "Google"
},
"family_name": "User",
"given_name": "Example",
"iat": 1234567890,
"iss": "https://your-kinde-subdomain.kinde.com",
"jti": "e7e18303-0ea5-402d-932c-xxx",
"name": "Example user",
"org_codes": ["org_123"],
"organizations": [
{
"id": "org_123",
"name": "Default Organization"
}
],
"picture": "https://lh3.googleusercontent.com/a/ACgxxx",
"rat": 1234567890,
"sub": "kp_1234567890",
"updated_at": 1234567890,
"user_properties": {
"kp_usr_city": {}
}
}

idTokenRaw / getIdTokenRaw

Link to this section

Get the ID token

import {useKindeBrowserClient} from "@kinde-oss/kinde-auth-nextjs";
const {idTokenRaw, getIdTokenRaw} = useKindeBrowserClient();
const idTokRaw = getIdTokenRaw();
console.log(idTokenRaw, idTokRaw);
eyJhxxx.eyJhdxxx.A4djjxxx

Is Kinde data loading

import {useKindeBrowserClient} from "@kinde-oss/kinde-auth-nextjs";
const {user, isLoading} = useKindeBrowserClient();
if (isLoading) return <div>Loading...</div>;
return <div>Hello {user.given_name}</div>;
true

Error message if there is an error

import {useKindeBrowserClient} from "@kinde-oss/kinde-auth-nextjs";
const {user, isLoading, error} = useKindeBrowserClient();
if (isLoading) return <div>Loading...</div>;
if (error) return <div>There was an error</div>;
return <div>Hello {user.given_name}</div>;
true

Protecting routes

Link to this section

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. There are multiple ways you can protect pages with Kinde Auth.

Protect routes with Kinde Auth data

Link to this section

On the page you want to protect, you can check if the user is authenticated and then handle it right then and there by grabbing the Kinde Auth data.

  • In Server Components you can get the Kinde Auth data by using the getKindeServerSession helper
  • In Client Components you can get the Kinde Auth Data using the useKindeBrowserClient helper
// app/protected/page.tsx - Server Component
import { LoginLink } from "@kinde-oss/kinde-auth-nextjs/components";
import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server";
export default async function Protected() {
const { isAuthenticated } = getKindeServerSession();
return (await isAuthenticated()) ? (
<div>
This page is protected - but you can view it because you are authenticated
</div>
) : (
<div>
This page is protected, please <LoginLink>Login</LoginLink> to view it
</div>
);
}
// app/protected/page.tsx - Client component
"use client";
import { useKindeBrowserClient } from "@kinde-oss/kinde-auth-nextjs";
import { LoginLink } from "@kinde-oss/kinde-auth-nextjs/components";
export default function Admin() {
const { isAuthenticated, isLoading } = useKindeBrowserClient();
if (isLoading) return <div>Loading...</div>;
return isAuthenticated ? (
<div>Admin content</div>
) : (
<div>
You have to <LoginLink>Login</LoginLink> to see this page
</div>
);
}

In the example above, we show different content based on whether or not the user is authenticated. If you want to automatically send the user to the sign in screen, you can do something like the following:

// app/protected/page.tsx - Server Component
import {getKindeServerSession} from "@kinde-oss/kinde-auth-nextjs/server";
import {redirect} from "next/navigation";
export default async function Protected() {
const {isAuthenticated} = getKindeServerSession();
if (!(await isAuthenticated())) {
redirect("/api/auth/login");
}
return <div>Protected content</div>;
}
// app/protected/page.tsx - Client Component
// As of right now, this can't be done in Client Components because of how Next.js handles
// navigation in client components with prefetching and caching.
// But you can still achieve an automatic redirect with middleware

If you want the user to be redirected back to that route after signing in, you can set post_login_redirect_url in the search params of the redirect.

if (!(await isAuthenticated())) {
redirect("/api/auth/login?post_login_redirect_url=/protected");
}

Protect routes using middleware

Link to this section

You can also protect routes with Next.js middleware.

Default page protection

We provide a withAuth helper that will protect routes covered by the matcher. If the user is not authenticated then they are redirected to login and once they have logged in they will be redirected back to the protected page which they should now have access to.

import {withAuth} from "@kinde-oss/kinde-auth-nextjs/middleware";
export default function middleware(req) {
return withAuth(req);
}
export const config = {
matcher: ["/admin"]
};

Page protection with callback function after authorization

You can use the withAuth helper as shown below with a middleware callback function which has access to the req.kindeAuth object that exposes the token and user data.

import {withAuth} from "@kinde-oss/kinde-auth-nextjs/middleware";
export default withAuth(async function middleware(req) {
console.log("look at me", req.kindeAuth);
});
export const config = {
matcher: ["/admin"]
};

Middleware options

There are options that can be passed into the middleware function to configure its functionality.

  • isReturnToCurrentPage - redirect the user back to the page they were trying to access
  • loginPage - define the path of the login page (where the users are redirected to when not authenticated)
  • publicPaths - define the public paths
  • isAuthorized - define the criteria for authorization
import {withAuth} from "@kinde-oss/kinde-auth-nextjs/middleware";
export default withAuth(
async function middleware(req) {
console.log("look at me", req.kindeAuth);
},
{
isReturnToCurrentPage: true,
loginPage: "/login",
isAuthorized: ({token}) => {
// The user will be considered authorized if they have the permission 'eat:chips'
return token.permissions.includes("eat:chips");
}
}
);
export const config = {
matcher: ["/admin"]
};

Refreshing Kinde data

Link to this section

Kinde data can be updated via the UI or with the Management API. To have these updates be reflected in your app, you will need to get the most up-to-date Kinde data and then refresh the tokens in your session.

To get the most up-to-date Kinde data in your session, use the refreshTokens helper function.

const {refreshTokens} = getKindeServerSession();
const handleRefresh = async () => {
await refreshTokens();
}
...
<button onClick={handleRefresh}>Get up to date data</button>

Kinde Management API

Link to this section

To use our management API please see @kinde/management-api-js

Server Component example:

import { Roles, Users } from "@kinde/management-api-js";
export default async function Dashboard() {
const { roles } = await Roles.getRoles();
const { users } = await Users.getUsers();
return (
<div className="container">
<div className="card start-hero">
<p className="text-body-2 start-hero-intro">Woohoo!</p>
<p className="text-display-2">
Your authentication is all sorted.
<br />
Build the important stuff.
</p>
</div>
<section className="next-steps-section">
<h2 className="text-heading-1">Next steps for you</h2>
</section>
<pre>{JSON.stringify(users, null, 2)}</pre>
</div>
);
}

Route Handler example:

import {NextResponse} from "next/server";
import {Users} from "@kinde/management-api-js";
export async function GET() {
const {users} = await Users.getUsers();
return NextResponse.json({users});
}

Create organizations

Link to this section

To create an organization from your app, you can use the CreateOrgLink component.

import {CreateOrgLink} from "@kinde-oss/kinde-auth-nextjs/components";
<CreateOrgLink orgName="Hurlstone">Create org</CreateOrgLink>;

Sign in to organizations

Link to this section

You can have your users sign in to a specific organization by setting the orgCode param in the LoginLink and RegisterLink components.

import {LoginLink, RegisterLink} from "@kinde-oss/kinde-auth-nextjs/components";
<LoginLink orgCode="org_7392cf35a1e">Login</LoginLink>
<RegisterLink orgCode="org_7392cf35a1e">Register</RegisterLink>

If the orgCode is not specified and the user belongs to multiple organizations, they will be prompted to choose which organization to sign in to during the login or register flow.

UTM tags can be used with the LoginLink and RegisterLink components to track auth traffic from its origin. You can then track the tags on the Analytics dashboard from within the Kinde app.

import {LoginLink} from "@kinde-oss/kinde-auth-nextjs/components";
<LoginLink
authUrlParams={{
utm_source: "reddit",
utm_medium: "social",
utm_campaign: "redjune23",
utm_term: "save90",
utm_content: "desktop"
}}
>
Login
</LoginLink>;

Internationalization

Link to this section

You can set the language you wish your users to see when they hit the login flow by including the lang attribute as a part of the authUrlParams when using the LoginLink and RegisterLink components.

import {LoginLink} from "@kinde-oss/kinde-auth-nextjs/components";
<LoginLink
authUrlParams={{
lang: "en-AU"
}}
>
Login
</LoginLink>;

An audience is the intended recipient of an access token - for example the API for your application. The audience argument can be passed to the Kinde client to request an audience be added to the provided token.

.env.local
...
KINDE_AUDIENCE=<your-api>

You can request multiple audiences by providing a white space separated list

.env.local
...
KINDE_AUDIENCE=<your-api-1> <your-api-2>

For details on how to connect, see Register an API.

Working with subdomains

Link to this section

In the case you have a custom domain and you would like to start the authentication flow from a URL like auth.mysite.com and you want to redirect to a URL like app.mysite.com , all you have to do is set the KINDE_COOKIE_DOMAIN to match the domain.

.env
...
KINDE_COOKIE_DOMAIN=.mysite.com

If the URL you want to start the authentication flow from and the URL you want to redirect to don’t share the same domain, then this will not work.

Working with preview URLs

Link to this section

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.

  • Make sure the above values match your application (e.g. “/dashboard” for KINDE_POST_LOGIN_REDIRECT_URL)
  • Also make sure variables are not set for the preview environment in your Vercel project. If they are, they will be overridden by the new variables in the next.config.js file.

Add callback URLs and logout redirect URLs to Kinde dynamically

Link to this section

Create a script that will run each time a new preview is deployed by Vercel, which will add the newly generated URL to Kinde.

You need to create a machine to machine (M2M) application to connect to the Kinde Management API.

  1. Create a Machine to machine (M2M) app.

    1. In Kinde, go to Settings > Applications and click on Add application.
    2. Give your application a name and select Machine to machine (M2M).
    3. Select Save.
    4. On the next screen, take note of the Client ID and Client secret values and add them to your .env.local file as KINDE_M2M_CLIENT_ID and KINDE_M2M_CLIENT_SECRET.
    5. On the same screen, click on APIs on the left menu.
    6. Authorize your M2M application to access the Kinde Management API by selecting the three dots (...) and clicking Authorize application.
    7. Once the application is authorized, select the three dots (...) again and this time select Manage scopes.
    8. Since we will be adding callback and redirect URLs dynamically via the Kinde Management API, you will need to toggle the switch for create:application_redirect_uris and create:application_logout_uris.
    9. Select Save.
  2. In your application source code, create a folder at the top level called scripts.

  3. 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_M2M_CLIENT_ID,
    client_secret: process.env.KINDE_M2M_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);
    }
    }
    })();
  4. 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"
    }
  5. Commit these changes. The next deploy will add the newly created preview URLs to your Kinde application.

To check your configuration, the SDK exposes an endpoint with your settings.

Note: The client secret will indicate only if the secret is set or not set correctly.

/api/auth/health

{
"apiPath": "/api/auth",
"redirectURL": "http://localhost:3000/api/auth/kinde_callback",
"postLoginRedirectURL": "http://localhost:3000/dashboard",
"issuerURL": "https://<your_kinde_subdomain>.kinde.com",
"clientID": "<your_kinde_client_id>",
"clientSecret": "Set correctly",
"postLogoutRedirectURL": "http://localhost:3000",
"logoutRedirectURL": "http://localhost:3000"
}

State not found error

Link to this section
  1. Confirm that the domain you start the auth flow from is different from the domain you are redirected to after the auth flow is complete. If this is not the case, see the explanation.
  2. Dynamically set the KINDE_SITE_URL and KINDE_POST_LOGIN_REDIRECT_URL when working with vercel preview domains. If you are using Vercel, you can set the KINDE_SITE_URL and KINDE_POST_LOGIN_REDIRECT_URL dynamically.
next.config.js
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;

The State not found error in production is usually a result of a mismatch between a few variables.

  • KINDE_SITE_URL and/or KINDE_POST_LOGIN_REDIRECT_URL
  • The domain you are on e.g. your-app-projects.vercel.app
  • Callback URL set on the Kinde dashboard

If you set KINDE_SITE_URL=https:// your-app-projects.vercel.app and KINDE_POST_LOGIN_REDIRECT_URL=https:// your-app-projects.vercel.app/dashboard. And you also set your Callback URL to be your-app-\*.vercel.app/api/auth/kinde_callback. You should be able to click login and complete the auth flow.

However if you start the auth flow from a Vercel preview domain your-app-PREVIEW-projects.vercel.app and complete the auth flow, you will be redirected to your-app-projects.vercel.app/api/auth/kinde_callback which is NOT the same as the domain you started the auth flow on.

The error happens because when you start the auth flow, a state cookie is set which needs to be checked against when you return back to your app. In this case, you are NOT being redirect to the app you started the flow on, but rather another domain where the app is running which does not have the state cookie. Since there is a state cookie mismatch, the auth flow is aborted for security reasons.

The reason why you are redirected to the wrong domain because is likely because your KINDE_POST_LOGIN_REDIRECT_URL environment variable is static and is set for all your deployments/domains.

You should set the KINDE_POST_LOGIN_REDIRECT_URL dynamically based on the domain you initiating the auth flow from.

In debug mode you will see more logs in your console that may help with debugging.

.env
KINDE_DEBUG_MODE = true;

Migration guide

Link to this section

Changes when moving from the previous version.

handleAuth - is now imported from “@kinde-oss/kinde-auth-nextjs/server”

import {handleAuth} from "@kinde-oss/kinde-auth-nextjs/server";
export const GET = handleAuth();

getKindeServerSession - functions returned from getKindeServerSession now return promises

const {getUser} = getKindeServerSession();
const user = await getUser();