Add Logto auth to your Next.js application using Server Actions

Integrates Logto auth to your Next.js application using Server Actions.
Sijie
SijieDeveloper
October 01, 20236 min read
Add Logto auth to your Next.js application using Server Actions

Server Actions presents a refreshed approach to create robust web applications without the need for traditional REST APIs. We've previously discussed this in our article.

Today, we're thrilled to announce the official support for Server Actions in our Next.js SDK, despite it being an experimental feature.

For a quick overview, check out this sample and follow along as we detail how to integrate Logto with Server Actions in this guide.

Prerequisites

To get started, make sure you have the following:

  • A running Logto instance or access to a Logto Cloud account.
  • A Next.js project with Server Actions feature enabled.

Configure Logto

If you are self-hosting Logto, refer to the Logto "Get started" documentation to set up your Logto instance.

Open Logto Console by entering the URL https://cloud.logto.io/ if you are using Logto Cloud, or the endpoint you have set up for self-hosting.

Next, navigate to the "Applications" tab and click on "Create application".

Applications tab

In the modal that appears, choose "Next.js (App Router)" and provide an application name, such as "Next.js App". Then click on "Create application”.

Create application

You will be directed to a tutorial page in Logto. Click on "Finish and done" to proceed to the Application details page.

Applications details

In the "Redirect URIs" section, enter the following value:

[your-nextjs-base-url]/callback

For example, if you are hosting Next.js on http://localhost:3000, the value should be:

http://localhost:3000/callback
Save changes

Click on the "Save Changes" button at the bottom. Once successful, keep this page open as it will be useful for the Next.js configuration.

Set up Next.js application

Ensure you have a project with the latest version of Next.js. If you don't have one yet, you could follow the offical installation guide to create one.

At the time of writing this guide, the feature is experimental and requires activation in the next.config.js

module.exports = {
  experimental: {
    serverActions: true,
  },
};

Define the Logto library

Start by install the @logto/next module using npm as follows:

npm i @logto/next

You can alse use yarn or pnpm.

Then let's create some functions as "server actions”, create the new file libraries/logto.ts:

'use server';

import LogtoClient from '@logto/next/server-actions';
import { cookies } from 'next/headers';

const config = {
  appId: process.env.APP_ID ?? '<app-id>', // You can find this in the details page.
  appSecret: process.env.APP_SECRET ?? '<app-secret>', // You can find this in the details page.
  endpoint: process.env.ENDPOINT ?? 'http://localhost:3001', // You can find this in the details page.
  baseUrl: process.env.BASE_URL ?? 'http://localhost:3000',
  cookieSecret: process.env.COOKIE_SECRET ?? 'complex_password_at_least_32_characters_long',
  cookieSecure: process.env.NODE_ENV === 'production',
};

const logtoClient = new LogtoClient(config);

const cookieName = `logto:${config.appId}`;

const setCookies = (value?: string) => {
  if (value === undefined) {
    return;
  }

  cookies().set(cookieName, value, {
    maxAge: 14 * 3600 * 24,
    secure: config.cookieSecure,
  });
};

const getCookie = () => {
  return cookies().get(cookieName)?.value ?? '';
};

export const signIn = async () => {
  const { url, newCookie } = await logtoClient.handleSignIn(
    getCookie(),
    `${config.baseUrl}/callback`
  );

  setCookies(newCookie);

  return url;
};

export const handleSignIn = async (searchParams: Record<string, string>) => {
  // Convert searchParams object into a query string.
  const search = new URLSearchParams(searchParams).toString();

  const newCookie = await logtoClient.handleSignInCallback(
    getCookie(),
    `${config.baseUrl}/callback?${search}`
  );

  setCookies(newCookie);
};

export const signOut = async () => {
  const url = await logtoClient.handleSignOut(getCookie(), `${config.baseUrl}/callback`);

  setCookies('');

  return url;
};

export const getLogtoContext = async () => {
  return await logtoClient.getLogtoContext(getCookie());
};

In this file, we export four functions for authentication purposes. Note the first line, "use server" indicates that the code in the file can only run on the server side. We use "next/headers" to manage cookie-based sessions.

The above functions we exported can be called directly from the client side React component. That is the main advantage of using Server Actions. Let's go to the next chapter to see how to use these functions.

Implement sign in and sign out buttons

With the authentication functions in place, let's construct the page. We'll create two client components to initiate the sign in and out actions.

Sign in

/app/sign-in.tsx:

'use client';

import { useRouter } from 'next/navigation';
import { signIn } from '../libraries/logto';

const SignIn = () => {
  const router = useRouter();

  const handleClick = async () => {
    const redirectUrl = await signIn();

    router.push(redirectUrl);
  };

  return <button onClick={handleClick}>Sign In</button>;
};

export default SignIn;

Here we import the signIn function that was just defined in the previous chapter. Although the code is executed on the server side, this function can still be directly invoked by the <button> component when a user clicks the sign-in button. By doing this, we eliminate the need to write any REST API to handle the sign-in process. In fact, Next.js handles the "POST" request dispatcher details for us. Upon receiving the redirectUrl, we can call router.push to redirect to the Logto sign-in page.

Sign out

/app/sign-out.tsx:

'use client';

import { useRouter } from 'next/navigation';
import { signOut } from '../libraries/logto';

const SignOut = () => {
  const router = useRouter();

  const handleClick = async () => {
    const redirectUrl = await signOut();

    router.push(redirectUrl);
  };

  return <button onClick={handleClick}>Sign Out</button>;
};

export default SignOut;

The sign-out process is similar to the sign-in process.

Prepare a callback page

As a standard OIDC identity provider, Logto redirects users to a callback URL after authentication. We must, therefore, prepare a callback page to handle the sign-in result.

/app/callback/page.tsx

'use client';

import { useRouter } from 'next/navigation';
import { handleSignIn } from '../../libraries/logto';
import { useEffect } from 'react';

type Props = {
  searchParams: Record<string, string>;
};

export default function Callback({ searchParams }: Props) {
  const router = useRouter();

  useEffect(() => {
    handleSignIn(searchParams).then(() => {
      router.push('/');
    });
  }, [router, searchParams]);

  return <div>Signing in...</div>;
}

Here we use a client component with useEffect, that makes it easy to display a “loading” page for better user experience.

Display user context and secure page

Now, let's make a minimal home page to exhibit the utility of the Logto SDK. If needed, protect any resource from unknown users by checking the isAuthenticated value and redirecting to the sign-in page or showing error messages.

app/page.tsx

import { getLogtoContext } from '../libraries/logto';
import styles from './page.module.css';
import SignIn from './sign-in';
import SignOut from './sign-out';

export default async function Home() {
  const { isAuthenticated, claims } = await getLogtoContext();
  return (
    <main className={styles.main}>
      <h1>Hello Logto.</h1>
      <div>{isAuthenticated ? <SignOut /> : <SignIn />}</div>
      {claims && (
        <div>
          <h2>Claims:</h2>
          <table>
            <thead>
              <tr>
                <th>Name</th>
                <th>Value</th>
              </tr>
            </thead>
            <tbody>
              {Object.entries(claims).map(([key, value]) => (
                <tr key={key}>
                  <td>{key}</td>
                  <td>{value}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
    </main>
  );
}

As you can observe, this is a server component that obviates the need for useEffect and managing complex state changes.

Conclusion

Server actions offer a streamlined and straightforward way to implement authentication compared to traditional Next.js applications that rely on REST APIs.

The entire code sample can be found in this repository: https://github.com/logto-io/js/tree/master/packages/next-server-actions-sample

Why not give Logto Cloud a try and experience the ease in action?