Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions e2e-chatbot-app-next/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,28 @@ req.session = {
};
```

### On-Behalf-Of (OBO) Authentication

When deployed in **Databricks Apps**, the platform injects an `x-forwarded-access-token` header containing the user's OAuth token. This enables On-Behalf-Of authentication where API calls execute with the user's identity and permissions.

**How it works:**
1. Databricks Apps injects `x-forwarded-access-token` header with user's token
2. Chat routes extract this token from request headers
3. Token is passed to AI SDK via `streamText({ headers: {...} })`
4. Provider fetch wrapper uses OBO token instead of service principal

**Authentication priority** (highest to lowest):
1. OBO token (`x-forwarded-access-token` header) - when in Databricks Apps
2. PAT token (`DATABRICKS_TOKEN` env var)
3. OAuth service principal (`DATABRICKS_CLIENT_ID` + `DATABRICKS_CLIENT_SECRET`)
4. CLI auth (`databricks auth token`)

**Benefits of OBO:**
- API calls use user's Unity Catalog permissions
- MLflow traces show actual user identity
- Custom API_PROXY backends receive user context
- Row-level security and column masks are enforced

### Auth Middleware Usage

- `authMiddleware` - Extracts session (doesn't reject)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,17 @@ async function getOrCreateDatabricksProvider(): Promise<CachedProvider> {
baseURL: `${hostname}/serving-endpoints`,
formatUrl: ({ baseUrl, path }) => API_PROXY ?? `${baseUrl}${path}`,
fetch: async (...[input, init]: Parameters<typeof fetch>) => {
// Always get fresh token for each request (will use cache if valid)
const currentToken = await getProviderToken();
const headers = new Headers(init?.headers);
headers.set('Authorization', `Bearer ${currentToken}`);

// Check if OBO (On-Behalf-Of) token was passed via headers from route handler.
// When running in Databricks Apps, the x-forwarded-access-token header contains
// the user's token for OBO authentication. If Authorization is already set,
// it means OBO headers were passed - use them instead of service principal.
if (!headers.has('Authorization')) {
// No OBO token provided, use service principal/PAT/CLI fallback
const currentToken = await getProviderToken();
headers.set('Authorization', `Bearer ${currentToken}`);
}

return databricksFetch(input, {
...init,
Expand Down
40 changes: 38 additions & 2 deletions e2e-chatbot-app-next/server/src/routes/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,14 @@ chatRouter.post('/', requireAuth, async (req: Request, res: Response) => {

if (!chat) {
if (isDatabaseAvailable()) {
const title = await generateTitleFromUserMessage({ message });
// Extract OBO token for title generation (same token used for streaming below)
const oboTokenForTitle = req.headers[
'x-forwarded-access-token'
] as string | undefined;
const title = await generateTitleFromUserMessage({
message,
oboToken: oboTokenForTitle,
});

await saveChat({
id,
Expand Down Expand Up @@ -147,9 +154,25 @@ chatRouter.post('/', requireAuth, async (req: Request, res: Response) => {
const streamId = generateUUID();

const model = await myProvider.languageModel(selectedChatModel);

// Extract OBO (On-Behalf-Of) token if present.
// Databricks Apps injects x-forwarded-access-token header for user authorization.
// When present, use it for downstream API calls to execute with user's identity/permissions.
const oboToken = req.headers['x-forwarded-access-token'] as string | undefined;

// Build auth headers: send both Authorization and x-forwarded-access-token
// for maximum compatibility with Databricks APIs and custom API_PROXY backends.
const oboHeaders = oboToken
? {
Authorization: `Bearer ${oboToken}`,
'x-forwarded-access-token': oboToken,
}
: undefined;

const result = streamText({
model,
messages: convertToModelMessages(uiMessages),
headers: oboHeaders,
onFinish: ({ usage }) => {
finalUsage = usage;
},
Expand Down Expand Up @@ -352,7 +375,8 @@ chatRouter.get(
chatRouter.post('/title', requireAuth, async (req: Request, res: Response) => {
try {
const { message } = req.body;
const title = await generateTitleFromUserMessage({ message });
const oboToken = req.headers['x-forwarded-access-token'] as string | undefined;
const title = await generateTitleFromUserMessage({ message, oboToken });
res.json({ title });
} catch (error) {
console.error('Error generating title:', error);
Expand Down Expand Up @@ -387,12 +411,24 @@ chatRouter.patch(
// Helper function to generate title from user message
async function generateTitleFromUserMessage({
message,
oboToken,
}: {
message: ChatMessage;
oboToken?: string;
}) {
const model = await myProvider.languageModel('title-model');

// Build OBO headers if token is present (same pattern as streaming endpoint)
const oboHeaders = oboToken
? {
Authorization: `Bearer ${oboToken}`,
'x-forwarded-access-token': oboToken,
}
: undefined;

const { text: title } = await generateText({
model,
headers: oboHeaders,
system: `\n
- you will generate a short title based on the first message a user begins a conversation with
- ensure it is not more than 80 characters long
Expand Down