Skip to main content
Version: 1.x

Next.js: Integrate @logto/next with Next.js 13 App Router

note

This tutorial assumes you have created an Application of type "Traditional Web" in Admin Console. If you are not ready, read this before continuing.

Add Logto SDK as a dependencyโ€‹

npm i @logto/next

Init LogtoClientโ€‹

note

In the following code snippets, we assume your app is running on http://localhost:3000.

tip

You can find and copy "App Secret" from application details page in Admin Console:

App Secret

Import and initialize LogtoClient:

// libraries/logto.ts
import LogtoClient from '@logto/next/edge';

export const logtoClient = new LogtoClient({
appId: '<your-application-id>',
appSecret: '<your-app-secret-copied-from-console>',
endpoint: '<your-logto-endpoint>', // E.g. http://localhost:3001
baseUrl: '<your-nextjs-app-base-url>', // E.g. http://localhost:3000
cookieSecret: 'complex_password_at_least_32_characters_long',
cookieSecure: process.env.NODE_ENV === 'production',
});

Sign inโ€‹

The sign-in flow can be simplified as:

Web sign-in flow

Configure sign-in redirect URIโ€‹

Let's switch to the Application details page of Admin Console in this section. Add a Redirect URI http://localhost:3000/api/logto/sign-in-callback and click "Save Changes".

Redirect URI in Admin Console

Redirect URI is an OAuth 2.0 concept which implies the location should redirect after authentication.

Prepare API routesโ€‹

Prepare API routes to connect with Logto.

Go back to your IDE/editor, use the following code to implement the API routes first:

// app/api/logto/sign-in/route.ts
import { type NextRequest } from 'next/server';

import { logtoClient } from '../../../../libraries/logto';

export const runtime = 'edge';

export async function GET(request: NextRequest) {
return logtoClient.handleSignIn()(request);
}
// app/api/logto/sign-in-callback/route.ts
import { type NextRequest } from 'next/server';

import { logtoClient } from '../../../../libraries/logto';

export const runtime = 'edge';

export async function GET(request: NextRequest) {
return logtoClient.handleSignInCallback()(request);
}
// app/api/logto/sign-out/route.ts
import { type NextRequest } from 'next/server';

import { logtoClient } from '../../../../libraries/logto';

export const runtime = 'edge';

export async function GET(request: NextRequest) {
return logtoClient.handleSignOut()(request);
}
// app/api/logto/user/route.ts
import { type NextRequest } from 'next/server';

import { logtoClient } from '../../../../libraries/logto';

export const runtime = 'edge';

export async function GET(request: NextRequest) {
return logtoClient.handleUser()(request);
}

We created 4 routes:

  1. /api/logto/sign-in: Sign in with Logto.
  2. /api/logto/sign-in-callback: Handle sign-in callback.
  3. /api/logto/sign-out: Sign out with Logto.
  4. /api/logto/user: Check if user is authenticated with Logto. If yes, return user info.

Implement sign-in buttonโ€‹

We're almost there! In the last step, we will create a sign-in button:

import { useRouter } from 'next/router';

<button onClick={() => window.location.assign('/api/logto/sign-in')}>Sign In</button>;

Now you will be navigated to Logto sign-in page when you click the button.

Get user profileโ€‹

We'll use "async component" to get user profile, check the Data Fetching doc to learn more.

Create getUser helperโ€‹

// app/api/logto/user/get-user.ts
import { type LogtoContext } from '@logto/next';
import { cookies } from 'next/headers';

// `server-only` guarantees any modules that import code in file
// will never run on the client. Even though this particular api
// doesn't currently use sensitive environment variables, it's
// good practise to add `server-only` preemptively.
// eslint-disable-next-line import/no-unassigned-import
import 'server-only';
import { config } from '../../../../libraries/config';

export async function getUser() {
const response = await fetch(`${config.baseUrl}/api/logto/user`, {
cache: 'no-store',
headers: {
cookie: cookies().toString(),
},
});

if (!response.ok) {
throw new Error('Something went wrong!');
}

// eslint-disable-next-line no-restricted-syntax
const user = (await response.json()) as LogtoContext;

return user;
}

Create an async component to fetchโ€‹

import { getUser } from './api/logto/user/get-user';

const Page = async () => {
const user = await getUser();

console.log(user); // You'll get user profile here.

return (
<div>
<header>
<h1>Hello Logto.</h1>
</header>
</div>
);
};

export default Page;

Protect API resourcesโ€‹

Call logtoClient.getLogtoContext to get user authentication state.

// pages/api/protected-resource.ts
import { type NextRequest } from 'next/server';

import { logtoClient } from '../../../../libraries/logto-edge';

export const runtime = 'edge';

export async function GET(request: NextRequest) {
const { isAuthenticated, scopes } = await logtoClient.getLogtoContext(request);

if (!isAuthenticated) {
return new Response(JSON.stringify({ message: 'Unauthorized' }), { status: 401 });
}

return new Response(
JSON.stringify({
data: 'this_is_protected_resource',
})
);
}

Sign outโ€‹

Calling /api/logto/sign-out will clear all the Logto data in memory and cookies if they exist.

After signing out, it'll be great to redirect your user back to your website. Let's add http://localhost:3000 as one of the Post Sign-out URIs in Admin Console (shows under Redirect URIs).

Implement a sign-out buttonโ€‹

<button onClick={() => window.location.assign('/api/logto/sign-out')}>Sign Out</button>

Fetch user informationโ€‹

Logto SDK helps you fetch the user information from the OIDC UserInfo Endpoint.

You can get the user information by calling logtoClient.handleUser({ fetchUserInfo: true }) after signing in.

The user information response will vary based on the scopes used in the LogtoConfig while initializing the LogtoClient; and the following table lists the relations between user information and scopes:

Field NameTypeRequired ScopeNotes
substringopenidThe openid scope is added by default.
namestringprofileThe profile scope is added by default.
usernamestringprofileThe profile scope is added by default.
picturestringprofileThe profile scope is added by default.
emailstringemail
email_verifiedbooleanemail
phone_numberstringphone
phone_number_verifiedbooleanphone
custom_dataobjectcustom_data
identitiesobjectidentities

Further readingsโ€‹