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
31 changes: 29 additions & 2 deletions docs/tips-guides/oauth.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
## Social Login Setup (Google & GitHub, English)
## Social Login Setup (Google, GitHub, Microsoft, Okta)

### Get your Google credentials

Expand Down Expand Up @@ -52,6 +52,27 @@ To use Microsoft as a social provider, you need to get your Microsoft credential
MICROSOFT_TENANT_ID=your_tenant_id # Optional
```

### Get your Okta credentials

To use Okta as a social provider, create an OIDC app integration in the Okta Admin Console.

- In Okta Admin, go to **Applications > Applications** and click **Create App Integration**.
- Choose **Sign-in method: OIDC - OpenID Connect** and **Application type: Web Application**.
- In **Sign-in redirect URIs**, set:
- For local development: `http://localhost:3000/api/auth/callback/okta`
- For production: `https://your-domain.com/api/auth/callback/okta`
- After creation, copy:
- Your **Okta domain/issuer** (e.g. `https://dev-XXXX.okta.com/oauth2/default`). Use this as `OKTA_ISSUER`.
- **Client ID** and **Client Secret**.
- Add your credentials to your `.env` file:

```text
OKTA_CLIENT_ID=your_okta_client_id
OKTA_CLIENT_SECRET=your_okta_client_secret
# Full issuer URL, e.g. https://dev-XXXX.okta.com/oauth2/default
OKTA_ISSUER=https://your-okta-domain/oauth2/default
```

## Environment Variable Check

Make sure your `.env` file contains the following variables:
Expand All @@ -74,6 +95,12 @@ MICROSOFT_TENANT_ID=your_microsoft_tenant_id
MICROSOFT_FORCE_ACCOUNT_SELECTION=1


# Okta
OKTA_CLIENT_ID=your_okta_client_id
OKTA_CLIENT_SECRET=your_okta_client_secret
# Full issuer URL (e.g. https://dev-XXXX.okta.com/oauth2/default)
OKTA_ISSUER=https://your-okta-domain/oauth2/default

```

## Additional Configuration Options
Expand Down Expand Up @@ -107,4 +134,4 @@ BETTER_AUTH_URL=https://yourdomain.com

## Done

You can now sign in to better-chatbot using your Google, GitHub or Microsoft account. Restart the application to apply the changes.
You can now sign in to better-chatbot using your Google, GitHub, Microsoft, or Okta account. Restart the application to apply the changes.
4 changes: 4 additions & 0 deletions src/app/(chat)/mcp/modify/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ export default async function Page({
initialConfig={mcpClient.config}
name={mcpClient.name}
id={mcpClient.id}
initialUserSessionAuth={mcpClient.userSessionAuth}
initialRequiresAuth={mcpClient.requiresAuth}
initialAuthProvider={mcpClient.authProvider}
initialAuthConfig={mcpClient.authConfig}
/>
) : (
<Alert variant="destructive">MCP client not found</Alert>
Expand Down
8 changes: 7 additions & 1 deletion src/app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ import { customModelProvider, isToolCallUnsupportedModel } from "lib/ai/models";

import { mcpClientsManager } from "lib/ai/mcp/mcp-manager";

import { agentRepository, chatRepository } from "lib/db/repository";
import {
agentRepository,
chatRepository,
userSessionAuthorizationRepository,
} from "lib/db/repository";
import globalLogger from "logger";
import {
buildMcpServerCustomizationsSystemPrompt,
Expand Down Expand Up @@ -246,6 +250,8 @@ export async function POST(request: Request) {
part,
{ ...MCP_TOOLS, ...WORKFLOW_TOOLS, ...APP_DEFAULT_TOOLS },
request.signal,
session.user.id,
userSessionAuthorizationRepository,
);
part.output = output;

Expand Down
14 changes: 14 additions & 0 deletions src/app/api/chat/shared.chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ export function manualToolExecuteByLastMessage(
part: ToolUIPart,
tools: Record<string, VercelAIMcpTool | VercelAIWorkflowTool | Tool>,
abortSignal?: AbortSignal,
userId?: string,
userOAuthRepository?: import(
"app-types/mcp",
).UserSessionAuthorizationRepository,
) {
const { input } = part;

Expand All @@ -137,6 +141,16 @@ export function manualToolExecuteByLastMessage(
messages: [],
});
} else if (VercelAIMcpToolTag.isMaybe(tool)) {
// Use user-authenticated tool call if user context is available
if (userId && userOAuthRepository) {
return mcpClientsManager.toolCallWithUserAuth(
tool._mcpServerId,
tool._originToolName,
input,
userId,
userOAuthRepository,
);
}
return mcpClientsManager.toolCall(
tool._mcpServerId,
tool._originToolName,
Expand Down
114 changes: 114 additions & 0 deletions src/app/api/mcp/user-oauth/authorize/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { NextRequest, NextResponse } from "next/server";
import { getSession } from "auth/server";
import { mcpRepository, mcpUserOAuthRepository } from "lib/db/repository";
import { generateUUID } from "lib/utils";
import globalLogger from "logger";
import { colorize } from "consola/utils";

const logger = globalLogger.withDefaults({
message: colorize("bgBlue", `MCP User OAuth Authorize: `),
});

/**
* Initiates OAuth flow for a user to authenticate with an MCP server
* This creates a per-user OAuth session and returns the authorization URL
*/
export async function POST(request: NextRequest) {
try {
const session = await getSession();
if (!session?.user?.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const { mcpServerId } = await request.json();
if (!mcpServerId) {
return NextResponse.json(
{ error: "MCP server ID is required" },
{ status: 400 },
);
}

// Get the MCP server configuration
const server = await mcpRepository.selectById(mcpServerId);
if (!server) {
return NextResponse.json(
{ error: "MCP server not found" },
{ status: 404 },
);
}

// Check if server requires authentication
if (!server.requiresAuth || server.authProvider === "none") {
return NextResponse.json(
{ error: "This MCP server does not require authentication" },
{ status: 400 },
);
}

// Generate OAuth state and code verifier for PKCE
const state = generateUUID();
const codeVerifier = generateUUID() + generateUUID(); // Longer for PKCE

// Create or update the user's OAuth session
await mcpUserOAuthRepository.upsertSession(session.user.id, mcpServerId, {
state,
codeVerifier,
});

// Build the authorization URL based on the auth provider
let authorizationUrl: string;

if (server.authProvider === "okta" && server.authConfig?.issuer) {
const params = new URLSearchParams({
client_id: server.authConfig.clientId || "",
response_type: "code",
scope: server.authConfig.scopes?.join(" ") || "openid profile email",
redirect_uri: `${process.env.NEXT_PUBLIC_BASE_URL || process.env.BETTER_AUTH_URL}/api/mcp/user-oauth/callback`,
state,
code_challenge: await generateCodeChallenge(codeVerifier),
code_challenge_method: "S256",
});

authorizationUrl = `${server.authConfig.issuer}/v1/authorize?${params.toString()}`;
} else {
// Generic OAuth2 - would need to be configured per server
return NextResponse.json(
{ error: "OAuth2 provider not fully configured" },
{ status: 400 },
);
}

logger.info(
`User ${session.user.id} initiating OAuth for MCP server ${server.name}`,
);

return NextResponse.json({
authorizationUrl,
state,
});
} catch (error: any) {
logger.error("Failed to initiate OAuth flow", error);
return NextResponse.json(
{ error: error.message || "Failed to initiate OAuth flow" },
{ status: 500 },
);
}
}

/**
* Generate PKCE code challenge from code verifier
*/
async function generateCodeChallenge(codeVerifier: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const digest = await crypto.subtle.digest("SHA-256", data);
return base64UrlEncode(new Uint8Array(digest));
}

function base64UrlEncode(buffer: Uint8Array): string {
let binary = "";
for (let i = 0; i < buffer.length; i++) {
binary += String.fromCharCode(buffer[i]);
}
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}
Loading