diff --git a/auth4genai/.vale.ini b/auth4genai/.vale.ini new file mode 100644 index 0000000..13ba512 --- /dev/null +++ b/auth4genai/.vale.ini @@ -0,0 +1,10 @@ +MinAlertLevel = error + +[formats] +mdx = md + +[*.mdx] +BasedOnStyles = Vale +Vale.Terms = NO +Vale.Spelling = NO +Vale.Repetition = NO diff --git a/auth4genai/get-started/call-others-apis-on-users-behalf.mdx b/auth4genai/get-started/call-others-apis-on-users-behalf.mdx index 06c976c..60b92e8 100644 --- a/auth4genai/get-started/call-others-apis-on-users-behalf.mdx +++ b/auth4genai/get-started/call-others-apis-on-users-behalf.mdx @@ -5,6 +5,7 @@ mode: "wide" --- import VercelCallOthersApi from "/snippets/get-started/vercel-ai-next-js/call-others-api.mdx"; +import ReactSpaVercelCallOthersApi from "/snippets/get-started/react-spa-vercel-ai/call-others-api.mdx"; import LangChainNextjsCallOthersApi from "/snippets/get-started/langchain-next-js/call-others-api.mdx"; import FastApiCallOthersApi from "/snippets/get-started/langchain-fastapi-py/call-others-api.mdx"; @@ -36,6 +37,12 @@ By the end of this quickstart, you should have an AI application integrated with > + + + ## Next steps diff --git a/auth4genai/snippets/get-started/langchain-fastapi-py/call-others-api.mdx b/auth4genai/snippets/get-started/langchain-fastapi-py/call-others-api.mdx index b29eb1b..88e3f72 100644 --- a/auth4genai/snippets/get-started/langchain-fastapi-py/call-others-api.mdx +++ b/auth4genai/snippets/get-started/langchain-fastapi-py/call-others-api.mdx @@ -1,8 +1,11 @@ import { Prerequisites } from "/snippets/get-started/prerequisites/call-others-api.jsx"; ### Prepare the FastAPI app diff --git a/auth4genai/snippets/get-started/prerequisites/call-others-api.jsx b/auth4genai/snippets/get-started/prerequisites/call-others-api.jsx index 5fcd72a..feeeaf8 100644 --- a/auth4genai/snippets/get-started/prerequisites/call-others-api.jsx +++ b/auth4genai/snippets/get-started/prerequisites/call-others-api.jsx @@ -1,76 +1,236 @@ +/** + * Prerequisites for call-others-api quickstarts. + * @param {Object} props - The props object + * + * @param {Object|undefined} [props.createAuth0ApplicationStep] - Configuration for Auth0 application creation step + * @param {string} [props.createAuth0ApplicationStep.applicationType] - Type of Auth0 application (e.g., "Regular Web") + * @param {string} [props.createAuth0ApplicationStep.callbackUrl] - Allowed callback URL for the application + * @param {string} [props.createAuth0ApplicationStep.logoutUrl] - Allowed logout URL for the application + * + * @param {string|undefined} [props.createAuth0ApplicationStep.allowedWebOrigins] - Allowed web origins for the application + * + * @param {Object|undefined} [props.refreshTokenGrantStep] - Configuration for refresh token grant step + * @param {string} [props.refreshTokenGrantStep.applicationName] - Name of the application for refresh token grant + * + * @param {Object|undefined} [props.createAuth0ApiStep] - Configuration for Auth0 API creation step + * + * @param {Object|undefined} [props.createResourceServerClientStep] - Configuration for resource server client creation step + * + * @param {Object|undefined} [props.tokenExchangeGrantStep] - Configuration for token exchange grant step + * @param {string} [props.tokenExchangeGrantStep.applicationName] - Name of the application for token exchange grant + * + * @returns {JSX.Element} A React component containing prerequisite steps + */ + export const Prerequisites = ({ - callbackUrl = "http://localhost:3000/auth/callback", - logoutUrl = "http://localhost:3000", + createAuth0ApplicationStep = { + applicationType: "Regular Web", + callbackUrl: "http://localhost:3000/auth/callback", + logoutUrl: "http://localhost:3000", + allowedWebOrigins: undefined, + }, + refreshTokenGrantStep = undefined, + createAuth0ApiStep = undefined, + createResourceServerClientStep = undefined, + tokenExchangeGrantStep = undefined, }) => { + // Build steps array dynamically based on conditions + const steps = []; + + // Always include these steps + steps.push( + + To continue with this quickstart, you need an{" "} + + Auth0 account + {" "} + and a Developer Tenant. + + ); + + if (createAuth0ApplicationStep) { + steps.push( + + + Create and configure an Auth0 Application + {" "} + with the following properties: +
    +
  • + Type: {createAuth0ApplicationStep.applicationType} +
  • +
  • + Allowed Callback URLs: {createAuth0ApplicationStep.callbackUrl} +
  • +
  • + Allowed Logout URLs: {createAuth0ApplicationStep.logoutUrl} +
  • + {createAuth0ApplicationStep.allowedWebOrigins && ( +
  • + Allowed Web Origins: {createAuth0ApplicationStep.allowedWebOrigins} +
  • + )} +
+ To learn more about Auth0 applications, read{" "} + + Applications + + . +
+ ); + } + // Conditionally add steps + if (refreshTokenGrantStep) { + steps.push( + + Enable the Refresh Token Grant for your{" "} + {refreshTokenGrantStep.applicationName}. Go to{" "} + + Applications > [Your Application] > Settings > Advanced > + Grant Types + {" "} + and enable the Refresh Token grant type. + + ); + } + + if (createAuth0ApiStep) { + steps.push( + +
    +
  • In your Auth0 Dashboard, go to Applications > APIs
  • +
  • Create a new API with an identifier (audience)
  • +
  • Once API is created, go to the APIs Settings > Access Settings and enable Allow Offline Access
  • +
  • Note down the API identifier for your environment variables
  • +
+ To learn more about Auth0 APIs, read{" "} + + APIs + + . +
+ ); + } + + if (createResourceServerClientStep) { + steps.push( + + This is a special client that allows your API server to perform token + exchanges using{" "} + + access tokens + {" "} + instead of{" "} + + refresh tokens + + . This client enables Token Vault to exchange an access token for an + external API access token (e.g., Google Calendar API). +
+
+ Create this client programmatically via the Auth0 Management API: + + + {`curl -L 'https://{tenant}.auth0.com/api/v2/clients' \\ +-H 'Content-Type: application/json' \\ +-H 'Accept: application/json' \\ +-H 'Authorization: Bearer {MANAGEMENT_API_TOKEN}' \\ +-d '{ + "name": "Calendar API Resource Server Client", + "app_type": "resource_server", + "grant_types": ["urn:auth0:params:oauth:grant-type:token-exchange:federated-connection-access-token"], + "resource_server_identifier": "YOUR_API_IDENTIFIER" +}'`} + + +
    +
  • + Note that your MANAGEMENT_API_TOKEN above must have the{" "} + create:clients scope in order to create a new client. + One way you can create a new token with this access is by doing the + following: +
      +
    • + Navigate to Applications > APIs > Auth0 Management API > API Explorer + tab in your tenant +
    • +
    • Hit the Create & Authorize Test Application button
    • +
    • + Copy the jwt access token shown and provide it as the{" "} + MANAGEMENT_API_TOKEN +
    • +
    +
  • +
  • + Note down the client_id and client_secret{" "} + returned from the curl response for your environment variables after running curl + successfully. +
  • +
+
+ ); + } + + if (tokenExchangeGrantStep) { + steps.push( + + Enable the Token Exchange Grant for your{" "} + {tokenExchangeGrantStep.applicationName}. Go to{" "} + + Applications > [Your Application] > Settings > Advanced > + Grant Types + {" "} + and enable the Token Exchange grant type. + + ); + } + + // Always include these final steps + steps.push( + + Set up a Google developer account that allows for third-party API calls + following the{" "} + + Google Sign-in and Authorization + {" "} + instructions. + + ); + + steps.push( + + Set up an{" "} + + OpenAI account and API key + + . + + ); + return ( <> Prerequisites Before getting started, make sure you have completed the following steps: - - - To continue with this quickstart, you need an{" "} - - Auth0 account - {" "} - and a Developer Tenant. - - - - Create and configure an Auth0 Application - {" "} - with the following properties: -
    -
  • - Type: Regular Web -
  • -
  • - Allowed Callback URLs: {callbackUrl} -
  • -
  • - Allowed Logout URLs: {logoutUrl} -
  • -
- To learn more about Auth0 applications, read{" "} - - Applications - - . -
- - Enable the Token Exchange Grant for your Auth0 Application. Go to{" "} - - Applications > [Your Application] > Settings > Advanced - > Grant Types - {" "} - and enable the Token Exchange grant type. - - - - Set up a Google developer account that allows for third-party API - calls following the{" "} - Google Sign-in and Authorization{" "} - instructions. - - - - Set up an{" "} - - OpenAI account and API key - - . - -
+ {steps} ); }; diff --git a/auth4genai/snippets/get-started/react-spa-vercel-ai/call-others-api.mdx b/auth4genai/snippets/get-started/react-spa-vercel-ai/call-others-api.mdx new file mode 100644 index 0000000..99a9664 --- /dev/null +++ b/auth4genai/snippets/get-started/react-spa-vercel-ai/call-others-api.mdx @@ -0,0 +1,557 @@ +import { Prerequisites } from "/snippets/get-started/prerequisites/call-others-api.jsx"; + + + +### Prepare React SPA + Hono API + +**Recommended**: To use this example, clone the [Auth0 AI JS](https://github.com/auth0-lab/auth0-ai-js.git) repository: + +```bash wrap lines +git clone https://github.com/auth0-lab/auth0-ai-js.git +cd auth0-ai-js/examples/calling-apis/spa-with-backend-api/react-hono-ai-sdk +``` + +### Install dependencies + +In the root directory of your project, note some of the following dependencies: + +**Client dependencies:** + +- `@auth0/auth0-spa-js`: Auth0 SPA SDK for client-side authentication +- `@auth0/ai-vercel`: [Auth0 AI SDK for Vercel AI](https://github.com/auth0-lab/auth0-ai-js/tree/main/packages/ai-vercel) built for GenAI applications +- `ai`: Core [Vercel AI SDK](https://sdk.vercel.ai/docs) module + +**Server dependencies:** + +- `@hono/node-server`: Node.js server adapter for Hono +- `hono`: Lightweight web framework +- `ai`: Core [Vercel AI SDK](https://sdk.vercel.ai/docs) module +- `@ai-sdk/openai`: [OpenAI](https://sdk.vercel.ai/providers/ai-sdk-providers/openai) provider +- `googleapis`: Node.js client for Google APIs +- `jose`: JavaScript Object Signing and Encryption library for JWT verification + +```bash wrap lines +# Install all client & server dependencies from the root directory of the project. +npm install +``` + +### Update the environment files + +#### Client (.env) + +```bash .env wrap lines +VITE_AUTH0_DOMAIN=your-auth0-domain +VITE_AUTH0_CLIENT_ID=your-spa-client-id +VITE_AUTH0_AUDIENCE=your-api-identifier +VITE_API_URL=http://localhost:3001 +``` + +#### Server (.env) + +```bash .env wrap lines +AUTH0_DOMAIN=your-auth0-domain +AUTH0_AUDIENCE=your-api-identifier +AUTH0_CLIENT_ID=your-resource-server-client-id +AUTH0_CLIENT_SECRET=your-resource-server-client-secret +OPENAI_API_KEY=your-openai-api-key +PORT=3001 +``` + +### Configure the SPA for step-up authorization + +Unlike the Next.js example which uses refresh tokens, this React SPA approach uses **access tokens** for token exchange with Token Vault. The SPA handles step-up authorization using Auth0 SPA SDK's `loginWithPopup()` method to display the consent screen, and allow the user to grant additional permissions. + +Create `client/src/components/FederatedConnectionPopup.tsx`: + +```tsx client/src/components/FederatedConnectionPopup.tsx wrap lines +import { getAuth0Client } from "../lib/auth0"; +import { Button } from "./ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"; + +import type { Auth0InterruptionUI } from "@auth0/ai-vercel/react"; +interface FederatedConnectionPopupProps { + interrupt: Auth0InterruptionUI; +} + +export function FederatedConnectionPopup({ + interrupt, +}: FederatedConnectionPopupProps) { + const [isLoading, setIsLoading] = useState(false); + + const { connection, requiredScopes, resume } = interrupt; + + // Use Auth0 SPA SDK to request additional connection/scopes + const startFederatedLogin = useCallback(async () => { + try { + setIsLoading(true); + + // Filter out empty scopes + const validScopes = requiredScopes.filter( + (scope: string) => scope && scope.trim() !== "" + ); + + const auth0Client = getAuth0Client(); + + // Use getTokenWithPopup for step-up authorization to request additional scopes + await auth0Client.getTokenWithPopup({ + authorizationParams: { + prompt: "consent", // Required for Google Calendar scopes + connection: connection, // e.g., "google-oauth2" + connection_scope: validScopes.join(" "), // Google-specific scopes + access_type: "offline", + }, + }); + + // The Auth0 client should automatically use the new token, but we should trigger + // a refresh to ensure the latest token is cached. + await auth0Client.getTokenSilently(); + + setIsLoading(false); + + // Resume the interrupted tool after successful authorization + if (typeof resume === "function") { + resume(); + } + } catch (error) { + setIsLoading(false); + + if (typeof resume === "function") { + resume(); + } + } + }, [connection, requiredScopes, resume]); + + if (isLoading) { + return ( + + +
+
+

+ Connecting to {connection.replace("-", " ")}... +

+
+
+
+ ); + } + + return ( + + + + Authorization Required + + + +

+ To access your {connection.replace("-", " ")} data, you need to authorize this application. +

+

+ Required permissions:{" "} + {requiredScopes + .filter((scope: string) => scope && scope.trim() !== "") + .join(", ")} +

+ +
+
+ ); +} +``` + +#### Create tools with integrated Token Vault support for retrieving third-party access tokens + +Create `server/src/lib/tools/listUserCalendars.ts`: + +```ts server/src/lib/tools/listUserCalendars.ts wrap lines +import { tool } from "ai"; +import { google } from "googleapis"; +import { z } from "zod"; + +import { getAccessTokenForConnection } from "@auth0/ai-vercel"; + +import type { ToolWrapper } from "@auth0/ai-vercel"; + +/** + * Tool: listUserCalendars + * Lists all calendars the user has access to. + * Uses the enhanced @auth0/ai SDK for token exchange with Token Vault. + */ +export const createListUserCalendarsTool = ( + googleCalendarWrapper: ToolWrapper +) => + googleCalendarWrapper( + tool({ + description: "List all calendars the user has access to", + parameters: z.object({}), + execute: async () => { + // Get the access token from Token Vault using the enhanced SDK + const token = getAccessTokenForConnection(); + + const calendar = google.calendar("v3"); + const auth = new google.auth.OAuth2(); + auth.setCredentials({ access_token: token }); + + const res = await calendar.calendarList.list({ auth }); + + const calendars = + res.data.items?.map((cal) => ({ + id: cal.id, + name: cal.summary, + accessRole: cal.accessRole, + })) ?? []; + + return calendars; + }, + }) + ); +``` + +#### Configure the API server with Google connection wrapper for calendar tools + +Create `server/src/lib/auth0.ts`: + +```ts server/src/lib/auth0.ts wrap lines +import { SUBJECT_TOKEN_TYPES } from "@auth0/ai"; +import { Auth0AI } from "@auth0/ai-vercel"; + +import type { Context } from "hono"; + +import type { ToolWrapper } from "@auth0/ai-vercel"; +// Create an Auth0AI instance configured with reserver client credentials +const auth0AI = new Auth0AI({ + auth0: { + domain: process.env.AUTH0_DOMAIN!, + clientId: process.env.RESOURCE_SERVER_CLIENT_ID!, // Resource server client ID for token exchange + clientSecret: process.env.RESOURCE_SERVER_CLIENT_SECRET!, // Resource server client secret + }, +}); + +// Enhanced token exchange with Token Vault, setup with access token support +// This demonstrates the new API pattern where access tokens can be used directly +export const createGoogleCalendarTool = (c: Context): ToolWrapper => { + const accessToken = c.get("auth")?.token; + if (!accessToken) { + throw new Error("Access token not available in auth context"); + } + return auth0AI.withTokenForConnection({ + accessToken: async () => accessToken, + subjectTokenType: SUBJECT_TOKEN_TYPES.SUBJECT_TYPE_ACCESS_TOKEN, + connection: process.env.GOOGLE_CONNECTION_NAME || "google-oauth2", + scopes: [ + "https://www.googleapis.com/auth/calendar.calendarlist.readonly", // Read-only access to calendar list + "https://www.googleapis.com/auth/calendar.events.readonly", // Read-only access to events + ], + }); +}; +``` + +### Create Hono API Chat API server with interrupt handling + +Create `server/src/index.ts`: + +```ts server/src/index.ts wrap lines +import { + createDataStreamResponse, + generateId, + streamText, + ToolExecutionError, +} from "ai"; +import { Hono } from "hono"; +import { cors } from "hono/cors"; +import { decodeJwt } from "jose"; + +import { openai } from "@ai-sdk/openai"; +import { setAIContext } from "@auth0/ai-vercel"; +import { InterruptionPrefix, withInterruptions } from "@auth0/ai-vercel/interrupts"; +import { Auth0Interrupt } from "@auth0/ai/interrupts"; +import { serve } from "@hono/node-server"; + +import { listUserCalendars } from "./lib/tools/listUserCalendars"; +import { jwtAuthMiddleware } from "./middleware/auth"; + +import type { ApiResponse } from "shared/dist"; + +export const app = new Hono().post("/chat", jwtAuthMiddleware(), async (c) => { + // auth middleware adds the auth context to the request + const auth = c.get("auth"); + + const { messages: requestMessages } = await c.req.json(); + + // Generate a thread ID for this conversation + const threadID = generateId(); + + // Set AI context for the tools to access + setAIContext({ threadID }); + + // Create the Google Calendar wrapper with auth context + const googleCalendarWrapper = createGoogleCalendarTool(c); + + // Create tools with the auth context + const listUserCalendars = createListUserCalendarsTool(googleCalendarWrapper); + + // Use the messages from the request directly + const tools = { listUserCalendars }; + + // note: you can see more examples of Hono API consumption with AI SDK here: + // https://ai-sdk.dev/cookbook/api-servers/hono?utm_source=chatgpt.com#hono + + return createDataStreamResponse({ + execute: withInterruptions( + async (dataStream) => { + const result = streamText({ + model: openai("gpt-4o-mini"), + system: + "You are a helpful calendar assistant! You can help users with their calendar events and schedules. Keep your responses concise and helpful.", + messages: requestMessages, + maxSteps: 5, + tools, + }); + + result.mergeIntoDataStream(dataStream, { + sendReasoning: true, + }); + }, + { messages: requestMessages, tools } + ), + onError: (error: any) => { + // Handle Auth0 AI interrupts + if ( + error.cause instanceof Auth0Interrupt || + error.cause instanceof FederatedConnectionInterrupt + ) { + const serializableError = { + ...error.cause.toJSON(), + toolCall: { + id: error.toolCallId, + args: error.toolArgs, + name: error.toolName, + }, + }; + + return `${InterruptionPrefix}${JSON.stringify(serializableError)}`; + } + + return "Oops! An error occurred."; + }, + }); + }); + +// Start the server for Node.js +const port = Number(process.env.PORT) || 3000; + +console.log(`🚀 Server starting on port ${port}`); +serve({ fetch: app.fetch, port }); + +console.log(`✅ Server running on http://localhost:${port}`); +``` + +### Implement interrupt handling in React + +Update your chat component to handle step-up auth interrupts: + +```tsx client/src/components/Chat.tsx wrap lines highlight={3,5,12,18-33,49-65, 124-126} +import { Loader2, Send, Trash2 } from "lucide-react"; +import { useChat } from "@ai-sdk/react"; +import { useInterruptions } from "@auth0/ai-vercel/react"; +import { useAuth0 } from "../hooks/useAuth0"; +import { FederatedConnectionPopup } from "./FederatedConnectionPopup"; +import { Button } from "./ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"; +import { Input } from "./ui/input"; + +import type { Message } from "ai"; + +const InterruptionPrefix = "AUTH0_AI_INTERRUPTION:"; +const SERVER_URL = import.meta.env.VITE_SERVER_URL; + +export function Chat() { + const { getToken } = useAuth0(); + + const chatHelpers = useInterruptions((errorHandler) => + useChat({ + api: `${SERVER_URL}/chat`, + fetch: async (url: string | URL | Request, init?: RequestInit) => { + const token = await getToken(); + return fetch(url, { + ...init, + headers: { + "Content-Type": "application/json", + ...init?.headers, + Authorization: `Bearer ${token}`, + }, + }); + }, + }) + ); + + const { + messages, + input, + handleInputChange, + handleSubmit, + isLoading, + error, + setMessages, + toolInterrupt, + } = chatHelpers; + + // Filter out interrupted tool calls from messages + let displayMessages = messages; + + if (toolInterrupt) { + displayMessages = messages.map((message) => ({ + ...message, + parts: message.parts?.map((part) => + part.type === "tool-invocation" && + part.toolInvocation.toolCallId === toolInterrupt.toolCall?.id + ? { + ...part, + toolInvocation: { + ...part.toolInvocation, + state: "call", + }, + } + : part + ), + })); + } + + const clearMessages = () => { + setMessages([]); + }; + + return ( + + + + Calendar Assistant + + {messages.length > 0 && ( + + )} + + + {/* Messages */} +
+ {displayMessages.length === 0 ? ( +
+

Ask me about your calendar events!

+

+ Try: "What meetings do I have today?" or "Show me my upcoming + events" +

+
+ ) : ( + displayMessages.map((message) => ( + + )) + )} + {isLoading && ( +
+
+ + + Thinking... + +
+
+ )} +
+ + {/* Error message - hide if it's an Auth0 interrupt (we show the popup instead) */} + {error && !error.message.startsWith(InterruptionPrefix) && ( +
+ Error: {error.message} +
+ )} + + {/* Step-Up Auth Interrupt Handling */} + {toolInterrupt && ( + + )} + + {/* Input form */} +
+ + +
+
+
+ ); +} + +function MessageBubble({ message }: { message: Message }) { + const isUser = message.role === "user"; + + return ( +
+
+

{message.content}

+
+
+ ); +} +``` + +### Key differences from Next.js approach + +This React SPA implementation differs from the Next.js example in a few important ways: + +1. **Token Vault Access Token Exchange**: Instead of using refresh tokens, this approach exchanges the SPA's access token for a third-party access token +2. **Client-Side Authorization**: Client Login and Step-up authorization is all handled client-side using `@auth0/auth0-spa-js` +3. **Resource Server Client**: Requires a special Resource Server Client configured for token exchange with Token Vault +4. **Interrupt Handling**: Tool access errors and Step-up authorization are handled via interrupts in the React client with popup-based re-authorization + +### Test your application + +1. Start the client & server using Turbo: `npm run dev` +2. Navigate to `http://localhost:5173` +3. Log in with Google and ask your AI agent about your calendar + +The application will automatically prompt for additional calendar permissions when needed using Auth0's step-up authorization flow. + +That's it! You've successfully integrated federated connections with access tokens into your React SPA + Vercel AI application. + +Explore [the example app on GitHub](https://github.com/auth0-lab/auth0-ai-js/tree/main/examples/calling-apis/spa-with-backend-api/react-hono-ai-sdk).