+ This endpoint uses on-behalf-of user authorization. Add these scopes to your
+ app via the Databricks UI or databricks.yml:{' '}
+ {oboRequiredScopes.join(', ')}.
+ Note: UC function scopes are not yet supported.
+ Click to learn more.
+
+
+
+
+ )}
);
diff --git a/e2e-chatbot-app-next/client/src/contexts/AppConfigContext.tsx b/e2e-chatbot-app-next/client/src/contexts/AppConfigContext.tsx
index caee9956..0a8cb3a8 100644
--- a/e2e-chatbot-app-next/client/src/contexts/AppConfigContext.tsx
+++ b/e2e-chatbot-app-next/client/src/contexts/AppConfigContext.tsx
@@ -7,6 +7,10 @@ interface ConfigResponse {
chatHistory: boolean;
feedback: boolean;
};
+ obo?: {
+ enabled: boolean;
+ requiredScopes: string[];
+ };
}
interface AppConfigContextType {
@@ -15,6 +19,8 @@ interface AppConfigContextType {
error: Error | undefined;
chatHistoryEnabled: boolean;
feedbackEnabled: boolean;
+ oboEnabled: boolean;
+ oboRequiredScopes: string[];
}
const AppConfigContext = createContext(
@@ -40,6 +46,8 @@ export function AppConfigProvider({ children }: { children: ReactNode }) {
// Default to true until loaded to avoid breaking existing behavior
chatHistoryEnabled: data?.features.chatHistory ?? true,
feedbackEnabled: data?.features.feedback ?? false,
+ oboEnabled: data?.obo?.enabled ?? false,
+ oboRequiredScopes: data?.obo?.requiredScopes ?? [],
};
return (
diff --git a/e2e-chatbot-app-next/packages/ai-sdk-providers/src/providers-server.ts b/e2e-chatbot-app-next/packages/ai-sdk-providers/src/providers-server.ts
index ef1d0d12..8f2bf994 100644
--- a/e2e-chatbot-app-next/packages/ai-sdk-providers/src/providers-server.ts
+++ b/e2e-chatbot-app-next/packages/ai-sdk-providers/src/providers-server.ts
@@ -73,10 +73,10 @@ const LOG_SSE_EVENTS = process.env.LOG_SSE_EVENTS === 'true';
const API_PROXY = process.env.API_PROXY;
-// Cache for endpoint details to check task type
+// Cache for endpoint details to check task type and OBO scopes
const endpointDetailsCache = new Map<
string,
- { task: string | undefined; timestamp: number }
+ { task: string | undefined; userApiScopes: string[]; timestamp: number }
>();
const ENDPOINT_DETAILS_CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
@@ -271,7 +271,17 @@ async function getOrCreateDatabricksProvider(): Promise {
return provider;
}
-// Get the task type of the serving endpoint
+// Response type for serving endpoint details
+interface EndpointDetailsResponse {
+ task: string | undefined;
+ auth_policy?: {
+ user_auth_policy: {
+ api_scopes: string[];
+ };
+ };
+}
+
+// Get the task type and OBO scopes of the serving endpoint
const getEndpointDetails = async (servingEndpoint: string) => {
const cached = endpointDetailsCache.get(servingEndpoint);
if (
@@ -294,15 +304,41 @@ const getEndpointDetails = async (servingEndpoint: string) => {
headers,
},
);
- const data = (await response.json()) as { task: string | undefined };
+ const data = (await response.json()) as EndpointDetailsResponse;
+ const userApiScopes = data.auth_policy?.user_auth_policy?.api_scopes ?? [];
+
+ if (userApiScopes.length > 0) {
+ console.warn(
+ `⚠ OBO detected on endpoint "${servingEndpoint}". Required user authorization scopes: ${JSON.stringify(userApiScopes)}\n` +
+ ` → Add these scopes to your app via the Databricks UI or in databricks.yml under resources.apps..user_authorization.scopes\n` +
+ ` → See: https://docs.databricks.com/aws/en/dev-tools/databricks-apps/auth`,
+ );
+ }
+
const returnValue = {
task: data.task as string | undefined,
+ userApiScopes,
timestamp: Date.now(),
};
endpointDetailsCache.set(servingEndpoint, returnValue);
return returnValue;
};
+/**
+ * Returns the OBO scopes for the configured serving endpoint, or empty array.
+ * Fetches endpoint details if not yet cached.
+ */
+export async function getEndpointOboScopes(): Promise {
+ const servingEndpoint = process.env.DATABRICKS_SERVING_ENDPOINT;
+ if (!servingEndpoint) return [];
+ try {
+ const details = await getEndpointDetails(servingEndpoint);
+ return details.userApiScopes;
+ } catch {
+ return [];
+ }
+}
+
// Create a smart provider wrapper that handles OAuth initialization
interface SmartProvider {
languageModel(id: string): Promise;
diff --git a/e2e-chatbot-app-next/server/src/routes/config.ts b/e2e-chatbot-app-next/server/src/routes/config.ts
index 0413eb93..1a62f5eb 100644
--- a/e2e-chatbot-app-next/server/src/routes/config.ts
+++ b/e2e-chatbot-app-next/server/src/routes/config.ts
@@ -5,18 +5,24 @@ import {
type Router as RouterType,
} from 'express';
import { isDatabaseAvailable } from '@chat-template/db';
+import { getEndpointOboScopes } from '@chat-template/ai-sdk-providers';
export const configRouter: RouterType = Router();
/**
* GET /api/config - Get application configuration
- * Returns feature flags based on environment configuration
+ * Returns feature flags and OBO status based on environment configuration
*/
-configRouter.get('/', (_req: Request, res: Response) => {
+configRouter.get('/', async (_req: Request, res: Response) => {
+ const oboScopes = await getEndpointOboScopes();
res.json({
features: {
chatHistory: isDatabaseAvailable(),
feedback: !!process.env.MLFLOW_EXPERIMENT_ID,
},
+ obo: {
+ enabled: oboScopes.length > 0,
+ requiredScopes: oboScopes,
+ },
});
});
diff --git a/e2e-chatbot-app-next/tests/api-mocking/api-mock-handlers.ts b/e2e-chatbot-app-next/tests/api-mocking/api-mock-handlers.ts
index 6c52d625..8d714aa6 100644
--- a/e2e-chatbot-app-next/tests/api-mocking/api-mock-handlers.ts
+++ b/e2e-chatbot-app-next/tests/api-mocking/api-mock-handlers.ts
@@ -280,10 +280,16 @@ export const handlers = [
// Mock fetching endpoint details
// Returns agent/v1/responses to enable context injection testing
+ // Includes auth_policy to simulate an OBO-enabled endpoint
http.get(/\/api\/2\.0\/serving-endpoints\/[^/]+$/, () => {
return HttpResponse.json({
name: 'test-endpoint',
task: 'agent/v1/responses',
+ auth_policy: {
+ user_auth_policy: {
+ api_scopes: ['serving.serving-endpoints'],
+ },
+ },
});
}),