Skip to main content
  • https://mintlify-assets.b-cdn.net/auth0/nextjs-svgrepo-com.svg Next.js

Prerequisites

Before using this example, make sure you:

1. Configure Auth0 AI

First, you must install the SDK:
npm install @auth0/ai-genkit
Then, you need to initialize Auth0 AI and set up the connection to request access tokens with the required Google Calendar scopes.
./src/lib/auth0-ai.ts
import { Auth0AI } from "@auth0/ai-genkit";
import { auth0 } from "@/lib/auth0";

// importing GenKit instance
import { ai } from "./genkit";

const auth0AI = new Auth0AI({
  genkit: ai,
});

export const withGoogleCalendar = auth0AI.withTokenForConnection({
  connection: "google-oauth2",
  scopes: ["https://www.googleapis.com/auth/calendar.freebusy"],
  refreshToken: async () => {
    const session = await auth0.getSession();
    const refreshToken = session?.tokenSet.refreshToken as string;
    return refreshToken;
  },
});
Here, the property auth0 is an instance of @auth0/nextjs-auth0 to handle the application auth flows.
You can check different authentication options for Next.js with Auth0 at the official documentation.

2. Integrate your tool with Google Calendar

Wrap your tool using the Auth0 AI SDK to obtain an access token for the Google Calendar API.
./src/lib/tools/checkUsersCalendar.ts
import { addHours } from "date-fns";
import { z } from "zod";
import { getAccessTokenForConnection } from "@auth0/ai-genkit";
import { FederatedConnectionError } from "@auth0/ai/interrupts";
import { withGoogleCalendar } from "@/lib/auth0-ai";

// importing GenKit instance
import { ai } from "../genkit";

export const checkUsersCalendar = ai.defineTool(
  ...withGoogleCalendar(
    {
      description:
        "Check user availability on a given date time on their calendar",
      inputSchema: z.object({
        date: z.coerce
          .date()
          .describe("Date to check availability for in UTC time always."),
      }),
      name: "checkUsersCalendar",
    },
    async ({ date }) => {
      // Get the access token from Auth0 AI
      const accessToken = getAccessTokenForConnection();

      // Google SDK
      try {
        const calendar = google.calendar("v3");
        const auth = new google.auth.OAuth2();

        auth.setCredentials({
          access_token: .accessToken,
        });

        const response = await calendar.freebusy.query({
          auth,
          requestBody: {
            timeMin: formatISO(date),
            timeMax: addHours(date, 1).toISOString(),
            timeZone: "UTC",
            items: [{ id: "primary" }],
          },
        });

        return {
          available: response.data?.calendars?.primary?.busy?.length === 0,
        };
      } catch (error) {
        if (error instanceof GaxiosError) {
          if (error.status === 401) {
            throw new FederatedConnectionError(
              `Authorization required to access the Federated Connection`
            );
          }
        }

        throw error;
      }
    }
  )
);

3. Handle authentication redirects

Interrupts are a way for the system to pause execution and prompt the user to take an action—such as authenticating or granting API access—before resuming the interaction. This ensures that any required access is granted dynamically and securely during the chat experience. In this context, Auth0-AI SDK manages authentication redirects in the GenKit SDK via these interrupts.

Server Side

On the server-side code of your Next.js App, you need to set up the tool invocation and handle the interruption messaging via the errorSerializer. The setAIContext function is used to set the async-context for the Auth0 AI SDK.
./src/app/api/chat/route.ts
import { ToolRequestPart } from "genkit";
import path from "path";
import { ai } from "@/lib/genkit";
import { checkUsersCalendar } from "@/lib/tools/check-user-calendar";
import { resumeAuth0Interrupts } from "@auth0/ai-genkit";
import { auth0 } from "@/lib/auth0";

export async function POST(
  request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const auth0Session = await auth0.getSession();
  const { id } = await params;
  const {
    message,
    interruptedToolRequest,
    timezone,
  }: {
    message?: string;
    interruptedToolRequest?: ToolRequestPart;
    timezone: { region: string; offset: number };
  } = await request.json();

  let session = await ai.loadSession(id);

  if (!session) {
    session = ai.createSession({
      sessionId: id,
    });
  }

  const tools = [checkUsersCalendar];

  const chat = session.chat({
    tools: tools,
    system: `You are a helpful assistant.
    The user's timezone is ${timezone.region} with an offset of ${timezone.offset} minutes.
    User's details: ${JSON.stringify(auth0Session?.user, null, 2)}.
    You can use the tools provided to help the user.
    You can also ask the user for more information if needed.
    Chat started at ${new Date().toISOString()}
    `,
  });

  const r = await chat.send({
    prompt: message,
    resume: resumeAuth0Interrupts(tools, interruptedToolRequest),
  });

  return Response.json({ messages: r.messages, interrupts: r.interrupts });
}

export async function GET(
  request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;

  const session = await ai.loadSession(id);

  if (!session) {
    return new Response("Session not found", {
      status: 404,
    });
  }

  const json = session.toJSON();

  if (!json?.threads?.main) {
    return new Response("Session not found", {
      status: 404,
    });
  }

  return Response.json(json.threads.main);
}

Client Side

On this example we utilize the EnsureAPIAccessPopup component to show a popup that allows the user to authenticate with Google Calendar and grant access with the requested scopes. You’ll first need to install the @auth0/ai-components package:
npx @auth0/ai-components add FederatedConnections
Then, you can integrate the authentication popup in your chat component, using the interruptions helper from the SDK:
./src/components/chat.tsx
"use client";
import { useQueryState } from "nuqs";
import { FormEventHandler, useEffect, useRef, useState } from "react";
import { FederatedConnectionInterrupt } from "@auth0/ai/interrupts";
import { EnsureAPIAccessPopup } from "@/components/auth0-ai/FederatedConnections/popup";
import Markdown from "react-markdown";

const useFocus = () => {
  const htmlElRef = useRef<HTMLInputElement>(null);
  const setFocus = () => {
    if (!htmlElRef.current) {
      return;
    }
    htmlElRef.current.focus();
  };
  return [htmlElRef, setFocus] as const;
};

export default function Chat() {
  const [threadId, setThreadId] = useQueryState("threadId");
  const [input, setInput] = useState("");
  const [isLoading, setIsLoading] = useState(false);
  const [messages, setMessages] = useState<
    {
      role: "user" | "model";
      content: [{ text?: string; metadata?: { interrupt?: any } }];
    }[]
  >([]);

  useEffect(() => {
    if (!threadId) {
      setThreadId(self.crypto.randomUUID());
    }
  }, [threadId, setThreadId]);

  useEffect(() => {
    if (!threadId) {
      return;
    }

    setIsLoading(true);

    (async () => {
      const messagesResponse = await fetch(`/api/chat/${threadId}`, {
        method: "GET",
        credentials: "include",
      });
      if (!messagesResponse.ok) {
        setMessages([]);
      } else {
        setMessages(await messagesResponse.json());
      }
      setIsLoading(false);
    })();
  }, [threadId]);

  const [inputRef, setInputFocus] = useFocus();
  useEffect(() => {
    if (isLoading) {
      return;
    }
    setInputFocus();
  }, [isLoading, setInputFocus]);

  const submit = async ({
    message,
    interruptedToolRequest,
  }: {
    message?: string;
    interruptedToolRequest?: any;
  }) => {
    setIsLoading(true);
    const timezone = {
      region: Intl.DateTimeFormat().resolvedOptions().timeZone,
      offset: new Date().getTimezoneOffset(),
    };
    const response = await fetch(`/api/chat/${threadId}`, {
      method: "POST",
      credentials: "include",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ message, interruptedToolRequest, timezone }),
    });
    if (!response.ok) {
      console.error("Error sending message");
    } else {
      const { messages: messagesResponse } = await response.json();
      setMessages(messagesResponse);
    }
    setIsLoading(false);
  };

  // //When the user submits a message, add it to the list of messages and resume the conversation.
  const handleSubmit: FormEventHandler<HTMLFormElement> = async (e) => {
    e.preventDefault();
    setMessages((messages) => [
      ...messages,
      { role: "user", content: [{ text: input }] },
    ]);
    submit({ message: input });
    setInput("");
  };

  return (
    <div>
      {messages
        .filter(
          (m) =>
            ["model", "user", "tool"].includes(m.role) &&
            m.content?.length > 0 &&
            (m.content[0].text || m.content[0].metadata?.interrupt)
        )
        .map((message, index) => (
          <div key={index}>
            <Markdown>
              {(message.role === "user" ? "User: " : "AI: ") +
                (message.content[0].text || "")}
            </Markdown>
            {!isLoading &&
            message.content[0].metadata?.interrupt &&
            FederatedConnectionInterrupt.isInterrupt(
              message.content[0].metadata?.interrupt
            )
              ? (() => {
                  const interrupt: any = message.content[0].metadata?.interrupt;
                  return (
                    <div>
                      <EnsureAPIAccessPopup
                        onFinish={() => submit({ interruptedToolRequest: message.content[0] })}
                        interrupt={interrupt}
                        connectWidget={{
                          title: `Requested by: "${interrupt.toolCall.toolName}"`,
                          description: "Description...",
                          action: { label: "Check" },
                        }}
                      />
                    </div>
                  );
                })()
              : null}
          </div>
        ))}

      <form onSubmit={handleSubmit}>
        <input value={input} ref={inputRef} placeholder="Say something..." readOnly={isLoading} disabled={isLoading} onChange={(e) => setInput(e.target.value)} />
      </form>
    </div>
  );
}

Account Linking

If you're integrating with Google, but users in your app or agent can sign in using other methods (e.g., a username and password or another social provider), you'll need to link these identities into a single user account. Auth0 refers to this process as Account Linking.Account Linking logic and handling will vary depending on your app or agent. You can find an example of how to implement it in a Next.js chatbot app here. If you have questions or are looking for best practices, join our Discord and ask in the #auth0-for-gen-ai channel.