diff --git a/.env.example b/.env.example
index bc49c38..6182d8f 100644
--- a/.env.example
+++ b/.env.example
@@ -39,6 +39,11 @@ SEARXNG_URL=http://localhost:8888
OLLAMA_URL=http://localhost:11434
OLLAMA_MODEL=llama3.1:8b
+# ---------------------------------------------------------------------------
+# LibreChat (external chat via SSO) [OPTIONAL - default: http://localhost:3080]
+# ---------------------------------------------------------------------------
+LIBRECHAT_URL=http://localhost:3080
+
# ---------------------------------------------------------------------------
# LLM Providers (comma-separated list) [OPTIONAL]
# ---------------------------------------------------------------------------
diff --git a/assets/i18n/de.json b/assets/i18n/de.json
index 4ca034c..515c528 100644
--- a/assets/i18n/de.json
+++ b/assets/i18n/de.json
@@ -38,8 +38,6 @@
"dashboard": "Dashboard",
"providers": "Provider",
"chat": "Chat",
- "tools": "Werkzeuge",
- "knowledge_base": "Wissensdatenbank",
"developer": "Entwickler",
"organization": "Organisation",
"switch_light": "Zum hellen Modus wechseln",
@@ -72,28 +70,6 @@
"trending": "Im Trend",
"recent_searches": "Letzte Suchen"
},
- "chat": {
- "new_chat": "Neuer Chat",
- "general": "Allgemein",
- "conversations": "Unterhaltungen",
- "news_chats": "Nachrichten-Chats",
- "all_chats": "Alle Chats",
- "no_conversations": "Noch keine Unterhaltungen",
- "type_message": "Nachricht eingeben...",
- "model_label": "Modell:",
- "no_models": "Keine Modelle verfuegbar",
- "send_to_start": "Senden Sie eine Nachricht, um die Unterhaltung zu starten.",
- "you": "Sie",
- "assistant": "Assistent",
- "thinking": "Denkt nach...",
- "copy_response": "Letzte Antwort kopieren",
- "copy_conversation": "Unterhaltung kopieren",
- "edit_last": "Letzte Nachricht bearbeiten",
- "just_now": "gerade eben",
- "minutes_ago": "vor {n} Min.",
- "hours_ago": "vor {n} Std.",
- "days_ago": "vor {n} T."
- },
"providers": {
"title": "Provider",
"subtitle": "Konfigurieren Sie Ihre LLM- und Embedding-Backends",
@@ -107,37 +83,6 @@
"active_config": "Aktive Konfiguration",
"embedding": "Embedding"
},
- "tools": {
- "title": "Werkzeuge",
- "subtitle": "MCP-Server und Werkzeugintegrationen verwalten",
- "calculator": "Taschenrechner",
- "calculator_desc": "Mathematische Berechnungen und Einheitenumrechnung",
- "tavily": "Tavily-Suche",
- "tavily_desc": "KI-optimierte Websuche-API fuer Echtzeitinformationen",
- "searxng": "SearXNG",
- "searxng_desc": "Datenschutzfreundliche Metasuchmaschine",
- "file_reader": "Dateileser",
- "file_reader_desc": "Lokale Dateien in verschiedenen Formaten lesen und analysieren",
- "code_executor": "Code-Ausfuehrer",
- "code_executor_desc": "Isolierte Codeausfuehrung fuer Python und JavaScript",
- "web_scraper": "Web-Scraper",
- "web_scraper_desc": "Strukturierte Daten aus Webseiten extrahieren",
- "email_sender": "E-Mail-Versand",
- "email_sender_desc": "E-Mails ueber konfigurierten SMTP-Server versenden",
- "git_ops": "Git-Operationen",
- "git_ops_desc": "Mit Git-Repositories fuer Versionskontrolle interagieren"
- },
- "knowledge": {
- "title": "Wissensdatenbank",
- "subtitle": "Dokumente fuer RAG-Abfragen verwalten",
- "search_placeholder": "Dateien suchen...",
- "name": "Name",
- "type": "Typ",
- "size": "Groesse",
- "chunks": "Abschnitte",
- "uploaded": "Hochgeladen",
- "actions": "Aktionen"
- },
"developer": {
"agents_title": "Agent Builder",
"agents_desc": "Erstellen und verwalten Sie KI-Agenten mit LangGraph. Erstellen Sie mehrstufige Schlussfolgerungspipelines, werkzeugnutzende Agenten und autonome Workflows.",
diff --git a/assets/i18n/en.json b/assets/i18n/en.json
index 662b0b7..774f1fa 100644
--- a/assets/i18n/en.json
+++ b/assets/i18n/en.json
@@ -38,8 +38,6 @@
"dashboard": "Dashboard",
"providers": "Providers",
"chat": "Chat",
- "tools": "Tools",
- "knowledge_base": "Knowledge Base",
"developer": "Developer",
"organization": "Organization",
"switch_light": "Switch to light mode",
@@ -72,28 +70,6 @@
"trending": "Trending",
"recent_searches": "Recent Searches"
},
- "chat": {
- "new_chat": "New Chat",
- "general": "General",
- "conversations": "Conversations",
- "news_chats": "News Chats",
- "all_chats": "All Chats",
- "no_conversations": "No conversations yet",
- "type_message": "Type a message...",
- "model_label": "Model:",
- "no_models": "No models available",
- "send_to_start": "Send a message to start the conversation.",
- "you": "You",
- "assistant": "Assistant",
- "thinking": "Thinking...",
- "copy_response": "Copy last response",
- "copy_conversation": "Copy conversation",
- "edit_last": "Edit last message",
- "just_now": "just now",
- "minutes_ago": "{n}m ago",
- "hours_ago": "{n}h ago",
- "days_ago": "{n}d ago"
- },
"providers": {
"title": "Providers",
"subtitle": "Configure your LLM and embedding backends",
@@ -107,37 +83,6 @@
"active_config": "Active Configuration",
"embedding": "Embedding"
},
- "tools": {
- "title": "Tools",
- "subtitle": "Manage MCP servers and tool integrations",
- "calculator": "Calculator",
- "calculator_desc": "Mathematical computation and unit conversion",
- "tavily": "Tavily Search",
- "tavily_desc": "AI-optimized web search API for real-time information",
- "searxng": "SearXNG",
- "searxng_desc": "Privacy-respecting metasearch engine",
- "file_reader": "File Reader",
- "file_reader_desc": "Read and parse local files in various formats",
- "code_executor": "Code Executor",
- "code_executor_desc": "Sandboxed code execution for Python and JavaScript",
- "web_scraper": "Web Scraper",
- "web_scraper_desc": "Extract structured data from web pages",
- "email_sender": "Email Sender",
- "email_sender_desc": "Send emails via configured SMTP server",
- "git_ops": "Git Operations",
- "git_ops_desc": "Interact with Git repositories for version control"
- },
- "knowledge": {
- "title": "Knowledge Base",
- "subtitle": "Manage documents for RAG retrieval",
- "search_placeholder": "Search files...",
- "name": "Name",
- "type": "Type",
- "size": "Size",
- "chunks": "Chunks",
- "uploaded": "Uploaded",
- "actions": "Actions"
- },
"developer": {
"agents_title": "Agent Builder",
"agents_desc": "Build and manage AI agents with LangGraph. Create multi-step reasoning pipelines, tool-using agents, and autonomous workflows.",
diff --git a/assets/i18n/es.json b/assets/i18n/es.json
index eef7960..6a0a4b1 100644
--- a/assets/i18n/es.json
+++ b/assets/i18n/es.json
@@ -38,8 +38,6 @@
"dashboard": "Panel de control",
"providers": "Proveedores",
"chat": "Chat",
- "tools": "Herramientas",
- "knowledge_base": "Base de conocimiento",
"developer": "Desarrollador",
"organization": "Organizacion",
"switch_light": "Cambiar a modo claro",
@@ -72,28 +70,6 @@
"trending": "Tendencias",
"recent_searches": "Busquedas recientes"
},
- "chat": {
- "new_chat": "Nuevo chat",
- "general": "General",
- "conversations": "Conversaciones",
- "news_chats": "Chats de noticias",
- "all_chats": "Todos los chats",
- "no_conversations": "Aun no hay conversaciones",
- "type_message": "Escriba un mensaje...",
- "model_label": "Modelo:",
- "no_models": "No hay modelos disponibles",
- "send_to_start": "Envie un mensaje para iniciar la conversacion.",
- "you": "Usted",
- "assistant": "Asistente",
- "thinking": "Pensando...",
- "copy_response": "Copiar ultima respuesta",
- "copy_conversation": "Copiar conversacion",
- "edit_last": "Editar ultimo mensaje",
- "just_now": "justo ahora",
- "minutes_ago": "hace {n}m",
- "hours_ago": "hace {n}h",
- "days_ago": "hace {n}d"
- },
"providers": {
"title": "Proveedores",
"subtitle": "Configure sus backends de LLM y embeddings",
@@ -107,37 +83,6 @@
"active_config": "Configuracion activa",
"embedding": "Embedding"
},
- "tools": {
- "title": "Herramientas",
- "subtitle": "Gestione servidores MCP e integraciones de herramientas",
- "calculator": "Calculadora",
- "calculator_desc": "Calculo matematico y conversion de unidades",
- "tavily": "Tavily Search",
- "tavily_desc": "API de busqueda web optimizada con IA para informacion en tiempo real",
- "searxng": "SearXNG",
- "searxng_desc": "Motor de metabusqueda que respeta la privacidad",
- "file_reader": "Lector de archivos",
- "file_reader_desc": "Leer y analizar archivos locales en varios formatos",
- "code_executor": "Ejecutor de codigo",
- "code_executor_desc": "Ejecucion de codigo en entorno aislado para Python y JavaScript",
- "web_scraper": "Web Scraper",
- "web_scraper_desc": "Extraer datos estructurados de paginas web",
- "email_sender": "Envio de correo",
- "email_sender_desc": "Enviar correos electronicos a traves del servidor SMTP configurado",
- "git_ops": "Operaciones Git",
- "git_ops_desc": "Interactuar con repositorios Git para control de versiones"
- },
- "knowledge": {
- "title": "Base de conocimiento",
- "subtitle": "Gestione documentos para recuperacion RAG",
- "search_placeholder": "Buscar archivos...",
- "name": "Nombre",
- "type": "Tipo",
- "size": "Tamano",
- "chunks": "Fragmentos",
- "uploaded": "Subido",
- "actions": "Acciones"
- },
"developer": {
"agents_title": "Constructor de agentes",
"agents_desc": "Construya y gestione agentes de IA con LangGraph. Cree pipelines de razonamiento de varios pasos, agentes que utilizan herramientas y flujos de trabajo autonomos.",
diff --git a/assets/i18n/fr.json b/assets/i18n/fr.json
index 113e6f6..9ab76f1 100644
--- a/assets/i18n/fr.json
+++ b/assets/i18n/fr.json
@@ -38,8 +38,6 @@
"dashboard": "Tableau de bord",
"providers": "Fournisseurs",
"chat": "Chat",
- "tools": "Outils",
- "knowledge_base": "Base de connaissances",
"developer": "Developpeur",
"organization": "Organisation",
"switch_light": "Passer en mode clair",
@@ -72,28 +70,6 @@
"trending": "Tendances",
"recent_searches": "Recherches recentes"
},
- "chat": {
- "new_chat": "Nouvelle conversation",
- "general": "General",
- "conversations": "Conversations",
- "news_chats": "Conversations actualites",
- "all_chats": "Toutes les conversations",
- "no_conversations": "Aucune conversation pour le moment",
- "type_message": "Saisissez un message...",
- "model_label": "Modele :",
- "no_models": "Aucun modele disponible",
- "send_to_start": "Envoyez un message pour demarrer la conversation.",
- "you": "Vous",
- "assistant": "Assistant",
- "thinking": "Reflexion en cours...",
- "copy_response": "Copier la derniere reponse",
- "copy_conversation": "Copier la conversation",
- "edit_last": "Modifier le dernier message",
- "just_now": "a l'instant",
- "minutes_ago": "il y a {n} min",
- "hours_ago": "il y a {n} h",
- "days_ago": "il y a {n} j"
- },
"providers": {
"title": "Fournisseurs",
"subtitle": "Configurez vos backends LLM et d'embeddings",
@@ -107,37 +83,6 @@
"active_config": "Configuration active",
"embedding": "Embedding"
},
- "tools": {
- "title": "Outils",
- "subtitle": "Gerez les serveurs MCP et les integrations d'outils",
- "calculator": "Calculatrice",
- "calculator_desc": "Calcul mathematique et conversion d'unites",
- "tavily": "Recherche Tavily",
- "tavily_desc": "API de recherche web optimisee par IA pour des informations en temps reel",
- "searxng": "SearXNG",
- "searxng_desc": "Metamoteur de recherche respectueux de la vie privee",
- "file_reader": "Lecteur de fichiers",
- "file_reader_desc": "Lire et analyser des fichiers locaux dans divers formats",
- "code_executor": "Executeur de code",
- "code_executor_desc": "Execution de code en bac a sable pour Python et JavaScript",
- "web_scraper": "Extracteur web",
- "web_scraper_desc": "Extraire des donnees structurees a partir de pages web",
- "email_sender": "Envoi d'e-mails",
- "email_sender_desc": "Envoyer des e-mails via le serveur SMTP configure",
- "git_ops": "Operations Git",
- "git_ops_desc": "Interagir avec les depots Git pour le controle de version"
- },
- "knowledge": {
- "title": "Base de connaissances",
- "subtitle": "Gerez les documents pour la recuperation RAG",
- "search_placeholder": "Rechercher des fichiers...",
- "name": "Nom",
- "type": "Type",
- "size": "Taille",
- "chunks": "Segments",
- "uploaded": "Importe",
- "actions": "Actions"
- },
"developer": {
"agents_title": "Constructeur d'agents",
"agents_desc": "Construisez et gerez des agents IA avec LangGraph. Creez des pipelines de raisonnement multi-etapes, des agents utilisant des outils et des flux de travail autonomes.",
diff --git a/assets/i18n/pt.json b/assets/i18n/pt.json
index 85ee33e..1d4e7d4 100644
--- a/assets/i18n/pt.json
+++ b/assets/i18n/pt.json
@@ -38,8 +38,6 @@
"dashboard": "Painel",
"providers": "Fornecedores",
"chat": "Chat",
- "tools": "Ferramentas",
- "knowledge_base": "Base de Conhecimento",
"developer": "Programador",
"organization": "Organizacao",
"switch_light": "Mudar para modo claro",
@@ -72,28 +70,6 @@
"trending": "Em destaque",
"recent_searches": "Pesquisas recentes"
},
- "chat": {
- "new_chat": "Nova conversa",
- "general": "Geral",
- "conversations": "Conversas",
- "news_chats": "Conversas de noticias",
- "all_chats": "Todas as conversas",
- "no_conversations": "Ainda sem conversas",
- "type_message": "Escreva uma mensagem...",
- "model_label": "Modelo:",
- "no_models": "Nenhum modelo disponivel",
- "send_to_start": "Envie uma mensagem para iniciar a conversa.",
- "you": "Voce",
- "assistant": "Assistente",
- "thinking": "A pensar...",
- "copy_response": "Copiar ultima resposta",
- "copy_conversation": "Copiar conversa",
- "edit_last": "Editar ultima mensagem",
- "just_now": "agora mesmo",
- "minutes_ago": "ha {n}m",
- "hours_ago": "ha {n}h",
- "days_ago": "ha {n}d"
- },
"providers": {
"title": "Fornecedores",
"subtitle": "Configure os seus backends de LLM e embeddings",
@@ -107,37 +83,6 @@
"active_config": "Configuracao Ativa",
"embedding": "Embedding"
},
- "tools": {
- "title": "Ferramentas",
- "subtitle": "Gerir servidores MCP e integracoes de ferramentas",
- "calculator": "Calculadora",
- "calculator_desc": "Calculo matematico e conversao de unidades",
- "tavily": "Pesquisa Tavily",
- "tavily_desc": "API de pesquisa web otimizada por IA para informacao em tempo real",
- "searxng": "SearXNG",
- "searxng_desc": "Motor de metapesquisa que respeita a privacidade",
- "file_reader": "Leitor de Ficheiros",
- "file_reader_desc": "Ler e analisar ficheiros locais em varios formatos",
- "code_executor": "Executor de Codigo",
- "code_executor_desc": "Execucao de codigo em sandbox para Python e JavaScript",
- "web_scraper": "Web Scraper",
- "web_scraper_desc": "Extrair dados estruturados de paginas web",
- "email_sender": "Envio de Email",
- "email_sender_desc": "Enviar emails atraves do servidor SMTP configurado",
- "git_ops": "Operacoes Git",
- "git_ops_desc": "Interagir com repositorios Git para controlo de versoes"
- },
- "knowledge": {
- "title": "Base de Conhecimento",
- "subtitle": "Gerir documentos para recuperacao RAG",
- "search_placeholder": "Pesquisar ficheiros...",
- "name": "Nome",
- "type": "Tipo",
- "size": "Tamanho",
- "chunks": "Fragmentos",
- "uploaded": "Carregado",
- "actions": "Acoes"
- },
"developer": {
"agents_title": "Construtor de Agentes",
"agents_desc": "Construa e gira agentes de IA com LangGraph. Crie pipelines de raciocinio multi-etapa, agentes com ferramentas e fluxos de trabalho autonomos.",
diff --git a/docker-compose.yml b/docker-compose.yml
index 7194306..5503a04 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,13 +1,12 @@
-version: '3.8'
-
services:
keycloak:
image: quay.io/keycloak/keycloak:26.0
container_name: certifai-keycloak
environment:
- KEYCLOAK_ADMIN: admin
- KEYCLOAK_ADMIN_PASSWORD: admin
+ KC_BOOTSTRAP_ADMIN_USERNAME: admin
+ KC_BOOTSTRAP_ADMIN_PASSWORD: admin
KC_DB: dev-mem
+ KC_HEALTH_ENABLED: "true"
ports:
- "8080:8080"
command:
@@ -17,10 +16,11 @@ services:
- ./keycloak/realm-export.json:/opt/keycloak/data/import/realm-export.json:ro
- ./keycloak/themes/certifai:/opt/keycloak/themes/certifai:ro
healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:8080/health/ready"]
+ test: ["CMD-SHELL", "exec 3<>/dev/tcp/localhost/8080 && echo -e 'GET /realms/master HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n' >&3 && head -1 <&3 | grep -q '200 OK'"]
interval: 10s
timeout: 5s
- retries: 5
+ retries: 10
+ start_period: 30s
mongo:
image: mongo:latest
@@ -40,4 +40,59 @@ services:
environment:
- SEARXNG_BASE_URL=http://localhost:8888
volumes:
- - ./searxng:/etc/searxng:rw
\ No newline at end of file
+ - ./searxng:/etc/searxng:rw
+
+ librechat:
+ image: ghcr.io/danny-avila/librechat:latest
+ container_name: certifai-librechat
+ restart: unless-stopped
+ # Use host networking so localhost:8080 (Keycloak) is reachable for
+ # OIDC discovery, and the browser redirect URLs match the issuer.
+ network_mode: host
+ depends_on:
+ keycloak:
+ condition: service_healthy
+ mongo:
+ condition: service_started
+ environment:
+ # MongoDB (use localhost since we're on host network)
+ MONGO_URI: mongodb://root:example@localhost:27017/librechat?authSource=admin
+ DOMAIN_CLIENT: http://localhost:3080
+ DOMAIN_SERVER: http://localhost:3080
+ # Allow HTTP for local dev OIDC (Keycloak on localhost without TLS)
+ NODE_TLS_REJECT_UNAUTHORIZED: "0"
+ NODE_ENV: development
+ # Keycloak OIDC SSO
+ OPENID_ISSUER: http://localhost:8080/realms/certifai
+ OPENID_CLIENT_ID: certifai-librechat
+ OPENID_CLIENT_SECRET: certifai-librechat-secret
+ OPENID_SESSION_SECRET: "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6"
+ OPENID_CALLBACK_URL: /oauth/openid/callback
+ OPENID_SCOPE: openid profile email
+ OPENID_BUTTON_LABEL: Login with CERTifAI
+ OPENID_AUTH_EXTRA_PARAMS: prompt=none
+ # Disable local auth (SSO only)
+ ALLOW_EMAIL_LOGIN: "false"
+ ALLOW_REGISTRATION: "false"
+ ALLOW_SOCIAL_LOGIN: "true"
+ ALLOW_SOCIAL_REGISTRATION: "true"
+ # JWT / encryption secrets (required by LibreChat)
+ CREDS_KEY: "97e95d72cdda06774a264f9fb7768097a6815dc1e930898d2e39c9a3a253b157"
+ CREDS_IV: "2ea456ab25279089b0ff9e7aca1df6e6"
+ JWT_SECRET: "767b962176666eab56e180e6f2d3fe95145dc6b978e37d4eb8d1da5421c5fb26"
+ JWT_REFRESH_SECRET: "51a43a1fca4b7b501b37e226a638645d962066e0686b82248921f3160e96501e"
+ # App settings
+ APP_TITLE: CERTifAI Chat
+ CUSTOM_FOOTER: CERTifAI - Sovereign GenAI Infrastructure
+ HOST: 0.0.0.0
+ PORT: "3080"
+ NO_INDEX: "true"
+ volumes:
+ - ./librechat/librechat.yaml:/app/librechat.yaml:ro
+ - ./librechat/logo.svg:/app/client/public/assets/logo.svg:ro
+ # Patch: allow HTTP issuer for local dev (openid-client v6 enforces HTTPS)
+ - ./librechat/openidStrategy.js:/app/api/strategies/openidStrategy.js:ro
+ - librechat-data:/app/data
+
+volumes:
+ librechat-data:
\ No newline at end of file
diff --git a/keycloak/realm-export.json b/keycloak/realm-export.json
index 7e3aa42..eb945ee 100644
--- a/keycloak/realm-export.json
+++ b/keycloak/realm-export.json
@@ -78,6 +78,39 @@
"optionalClientScopes": [
"offline_access"
]
+ },
+ {
+ "clientId": "certifai-librechat",
+ "name": "CERTifAI Chat",
+ "description": "LibreChat OIDC client for CERTifAI",
+ "enabled": true,
+ "publicClient": false,
+ "directAccessGrantsEnabled": false,
+ "standardFlowEnabled": true,
+ "implicitFlowEnabled": false,
+ "serviceAccountsEnabled": false,
+ "protocol": "openid-connect",
+ "secret": "certifai-librechat-secret",
+ "rootUrl": "http://localhost:3080",
+ "baseUrl": "http://localhost:3080",
+ "redirectUris": [
+ "http://localhost:3080/*"
+ ],
+ "webOrigins": [
+ "http://localhost:3080",
+ "http://localhost:8000"
+ ],
+ "attributes": {
+ "post.logout.redirect.uris": "http://localhost:3080"
+ },
+ "defaultClientScopes": [
+ "openid",
+ "profile",
+ "email"
+ ],
+ "optionalClientScopes": [
+ "offline_access"
+ ]
}
],
"clientScopes": [
diff --git a/librechat/librechat.yaml b/librechat/librechat.yaml
new file mode 100644
index 0000000..7ba5233
--- /dev/null
+++ b/librechat/librechat.yaml
@@ -0,0 +1,40 @@
+# CERTifAI LibreChat Configuration
+# Ollama backend for self-hosted LLM inference.
+version: 1.2.8
+
+cache: true
+
+registration:
+ socialLogins:
+ - openid
+
+interface:
+ privacyPolicy:
+ externalUrl: https://dash-dev.meghsakha.com/privacy
+ termsOfService:
+ externalUrl: https://dash-dev.meghsakha.com/impressum
+ endpointsMenu: true
+ modelSelect: true
+ parameters: true
+
+endpoints:
+ custom:
+ - name: "Ollama"
+ apiKey: "ollama"
+ baseURL: "https://mac-mini-von-benjamin-2:11434/v1/"
+ models:
+ default:
+ - "llama3.1:8b"
+ - "qwen3:30b-a3b"
+ fetch: true
+ titleConvo: true
+ titleModel: "current_model"
+ summarize: false
+ summaryModel: "current_model"
+ forcePrompt: false
+ modelDisplayLabel: "CERTifAI Ollama"
+ dropParams:
+ - stop
+ - user
+ - frequency_penalty
+ - presence_penalty
diff --git a/librechat/logo.svg b/librechat/logo.svg
new file mode 100644
index 0000000..ac16408
--- /dev/null
+++ b/librechat/logo.svg
@@ -0,0 +1,25 @@
+
diff --git a/librechat/openidStrategy.js b/librechat/openidStrategy.js
new file mode 100644
index 0000000..b2c5575
--- /dev/null
+++ b/librechat/openidStrategy.js
@@ -0,0 +1,743 @@
+const undici = require('undici');
+const { get } = require('lodash');
+const fetch = require('node-fetch');
+const passport = require('passport');
+const client = require('openid-client');
+const jwtDecode = require('jsonwebtoken/decode');
+const { HttpsProxyAgent } = require('https-proxy-agent');
+const { hashToken, logger } = require('@librechat/data-schemas');
+const { Strategy: OpenIDStrategy } = require('openid-client/passport');
+const { CacheKeys, ErrorTypes, SystemRoles } = require('librechat-data-provider');
+const {
+ isEnabled,
+ logHeaders,
+ safeStringify,
+ findOpenIDUser,
+ getBalanceConfig,
+ isEmailDomainAllowed,
+} = require('@librechat/api');
+const { getStrategyFunctions } = require('~/server/services/Files/strategies');
+const { findUser, createUser, updateUser } = require('~/models');
+const { getAppConfig } = require('~/server/services/Config');
+const getLogStores = require('~/cache/getLogStores');
+
+/**
+ * @typedef {import('openid-client').ClientMetadata} ClientMetadata
+ * @typedef {import('openid-client').Configuration} Configuration
+ **/
+
+/**
+ * @param {string} url
+ * @param {client.CustomFetchOptions} options
+ */
+async function customFetch(url, options) {
+ const urlStr = url.toString();
+ logger.debug(`[openidStrategy] Request to: ${urlStr}`);
+ const debugOpenId = isEnabled(process.env.DEBUG_OPENID_REQUESTS);
+ if (debugOpenId) {
+ logger.debug(`[openidStrategy] Request method: ${options.method || 'GET'}`);
+ logger.debug(`[openidStrategy] Request headers: ${logHeaders(options.headers)}`);
+ if (options.body) {
+ let bodyForLogging = '';
+ if (options.body instanceof URLSearchParams) {
+ bodyForLogging = options.body.toString();
+ } else if (typeof options.body === 'string') {
+ bodyForLogging = options.body;
+ } else {
+ bodyForLogging = safeStringify(options.body);
+ }
+ logger.debug(`[openidStrategy] Request body: ${bodyForLogging}`);
+ }
+ }
+
+ try {
+ /** @type {undici.RequestInit} */
+ let fetchOptions = options;
+ if (process.env.PROXY) {
+ logger.info(`[openidStrategy] proxy agent configured: ${process.env.PROXY}`);
+ fetchOptions = {
+ ...options,
+ dispatcher: new undici.ProxyAgent(process.env.PROXY),
+ };
+ }
+
+ const response = await undici.fetch(url, fetchOptions);
+
+ if (debugOpenId) {
+ logger.debug(`[openidStrategy] Response status: ${response.status} ${response.statusText}`);
+ logger.debug(`[openidStrategy] Response headers: ${logHeaders(response.headers)}`);
+ }
+
+ if (response.status === 200 && response.headers.has('www-authenticate')) {
+ const wwwAuth = response.headers.get('www-authenticate');
+ logger.warn(`[openidStrategy] Non-standard WWW-Authenticate header found in successful response (200 OK): ${wwwAuth}.
+This violates RFC 7235 and may cause issues with strict OAuth clients. Removing header for compatibility.`);
+
+ /** Cloned response without the WWW-Authenticate header */
+ const responseBody = await response.arrayBuffer();
+ const newHeaders = new Headers();
+ for (const [key, value] of response.headers.entries()) {
+ if (key.toLowerCase() !== 'www-authenticate') {
+ newHeaders.append(key, value);
+ }
+ }
+
+ return new Response(responseBody, {
+ status: response.status,
+ statusText: response.statusText,
+ headers: newHeaders,
+ });
+ }
+
+ return response;
+ } catch (error) {
+ logger.error(`[openidStrategy] Fetch error: ${error.message}`);
+ throw error;
+ }
+}
+
+/** @typedef {Configuration | null} */
+let openidConfig = null;
+
+/**
+ * Custom OpenID Strategy
+ *
+ * Note: Originally overrode currentUrl() to work around Express 4's req.host not including port.
+ * With Express 5, req.host now includes the port by default, but we continue to use DOMAIN_SERVER
+ * for consistency and explicit configuration control.
+ * More info: https://github.com/panva/openid-client/pull/713
+ */
+class CustomOpenIDStrategy extends OpenIDStrategy {
+ currentUrl(req) {
+ const hostAndProtocol = process.env.DOMAIN_SERVER;
+ return new URL(`${hostAndProtocol}${req.originalUrl ?? req.url}`);
+ }
+
+ authorizationRequestParams(req, options) {
+ const params = super.authorizationRequestParams(req, options);
+ if (options?.state && !params.has('state')) {
+ params.set('state', options.state);
+ }
+
+ if (process.env.OPENID_AUDIENCE) {
+ params.set('audience', process.env.OPENID_AUDIENCE);
+ logger.debug(
+ `[openidStrategy] Adding audience to authorization request: ${process.env.OPENID_AUDIENCE}`,
+ );
+ }
+
+ // Parse OPENID_AUTH_EXTRA_PARAMS (format: "key=value" or "key1=value1,key2=value2")
+ if (process.env.OPENID_AUTH_EXTRA_PARAMS) {
+ const extraParts = process.env.OPENID_AUTH_EXTRA_PARAMS.split(',');
+ for (const part of extraParts) {
+ const [key, ...rest] = part.trim().split('=');
+ if (key && rest.length > 0) {
+ params.set(key.trim(), rest.join('=').trim());
+ logger.debug(`[openidStrategy] Adding extra auth param: ${key.trim()}=${rest.join('=').trim()}`);
+ }
+ }
+ }
+
+ /** Generate nonce for federated providers that require it */
+ const shouldGenerateNonce = isEnabled(process.env.OPENID_GENERATE_NONCE);
+ if (shouldGenerateNonce && !params.has('nonce') && this._sessionKey) {
+ const crypto = require('crypto');
+ const nonce = crypto.randomBytes(16).toString('hex');
+ params.set('nonce', nonce);
+ logger.debug('[openidStrategy] Generated nonce for federated provider:', nonce);
+ }
+
+ return params;
+ }
+}
+
+/**
+ * Exchange the access token for a new access token using the on-behalf-of flow if required.
+ * @param {Configuration} config
+ * @param {string} accessToken access token to be exchanged if necessary
+ * @param {string} sub - The subject identifier of the user. usually found as "sub" in the claims of the token
+ * @param {boolean} fromCache - Indicates whether to use cached tokens.
+ * @returns {Promise} The new access token if exchanged, otherwise the original access token.
+ */
+const exchangeAccessTokenIfNeeded = async (config, accessToken, sub, fromCache = false) => {
+ const tokensCache = getLogStores(CacheKeys.OPENID_EXCHANGED_TOKENS);
+ const onBehalfFlowRequired = isEnabled(process.env.OPENID_ON_BEHALF_FLOW_FOR_USERINFO_REQUIRED);
+ if (onBehalfFlowRequired) {
+ if (fromCache) {
+ const cachedToken = await tokensCache.get(sub);
+ if (cachedToken) {
+ return cachedToken.access_token;
+ }
+ }
+ const grantResponse = await client.genericGrantRequest(
+ config,
+ 'urn:ietf:params:oauth:grant-type:jwt-bearer',
+ {
+ scope: process.env.OPENID_ON_BEHALF_FLOW_USERINFO_SCOPE || 'user.read',
+ assertion: accessToken,
+ requested_token_use: 'on_behalf_of',
+ },
+ );
+ await tokensCache.set(
+ sub,
+ {
+ access_token: grantResponse.access_token,
+ },
+ grantResponse.expires_in * 1000,
+ );
+ return grantResponse.access_token;
+ }
+ return accessToken;
+};
+
+/**
+ * get user info from openid provider
+ * @param {Configuration} config
+ * @param {string} accessToken access token
+ * @param {string} sub - The subject identifier of the user. usually found as "sub" in the claims of the token
+ * @returns {Promise