Skip to content

Commit 67f8066

Browse files
feat(auth): implementa OAuth 2.0 per MCP con supporto Bearer token e login GitHub
Aggiunge authorization server OAuth 2.0 completo con discovery endpoint, dynamic client registration e PKCE per integrazione MCP con Claude Code, unifica callback OAuth per browser e MCP, migliora frontend con status authentication cliccabile
1 parent 05c0450 commit 67f8066

26 files changed

+1779
-617
lines changed

.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
# GitHub OAuth (required for authentication)
55
# Create an OAuth App at: https://github.com/settings/developers
6-
# Set callback URL to: http://localhost:8080/auth/callback
6+
# Set callback URL to: http://localhost:8080/oauth/github-callback
77
GITHUB_CLIENT_ID=your_github_client_id
88
GITHUB_CLIENT_SECRET=your_github_client_secret
99

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,7 @@ markdown_urls.txt
4444
# Docker/Container
4545
.env
4646
users.yaml
47+
docker-compose.yml
4748
!docker/users.yaml.example
49+
!docker-compose.yml.example
4850
!.env.example

CHANGELOG.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Changelog
2+
3+
Tutte le modifiche rilevanti al progetto sono documentate in questo file.
4+
5+
Il formato segue [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6+
e il progetto aderisce al [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [1.0.0] - 2025-12-02
9+
10+
### Aggiunto
11+
- OAuth 2.0 Authorization Server per MCP (RFC 8414, RFC 7591)
12+
- Supporto Bearer token per autenticazione API e MCP
13+
- Dynamic Client Registration per client MCP
14+
- PKCE support (S256 e plain) per OAuth flow
15+
- Frontend: status light "Authenticated" cliccabile con modal login GitHub
16+
- Endpoint discovery `.well-known/oauth-authorization-server`
17+
- Docker Compose example con configurazione OAuth
18+
- Favicon SVG per frontend
19+
20+
### Modificato
21+
- AuthMiddleware supporta sia session cookie che Bearer token
22+
- Callback OAuth unificato per browser e MCP (`/oauth/github-callback`)
23+
- Migliorata gestione errori nel middleware (JSONResponse invece di HTTPException)
24+
- Aggiornato .gitignore per escludere docker-compose.yml e file sensibili
25+
26+
### Sicurezza
27+
- Tutte le credenziali lette da variabili d'ambiente
28+
- Session token firmati con itsdangerous
29+
- CSRF protection con state parameter OAuth
30+
- Whitelist utenti autorizzati via YAML

Dockerfile

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
3939
# Install Ollama
4040
RUN curl -fsSL https://ollama.ai/install.sh | sh
4141

42-
# Install Qdrant (standalone binary)
43-
RUN curl -L https://github.com/qdrant/qdrant/releases/download/v1.12.4/qdrant-x86_64-unknown-linux-musl.tar.gz | \
42+
# Install Qdrant (standalone binary - architecture aware)
43+
RUN ARCH=$(uname -m) && \
44+
if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then \
45+
QDRANT_ARCH="aarch64-unknown-linux-musl"; \
46+
else \
47+
QDRANT_ARCH="x86_64-unknown-linux-musl"; \
48+
fi && \
49+
curl -L "https://github.com/qdrant/qdrant/releases/download/v1.12.4/qdrant-${QDRANT_ARCH}.tar.gz" | \
4450
tar xz -C /usr/local/bin
4551

4652
# Copy virtual environment from builder

Dockerfile.tika

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,22 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
3737
openjdk-17-jre-headless \
3838
&& rm -rf /var/lib/apt/lists/*
3939

40-
# Set Java environment
41-
ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
40+
# Set Java environment (create symlink to avoid architecture-specific path)
41+
RUN ln -s /usr/lib/jvm/java-17-openjdk-* /usr/lib/jvm/java-17
42+
ENV JAVA_HOME=/usr/lib/jvm/java-17
43+
ENV PATH="${JAVA_HOME}/bin:${PATH}"
4244

4345
# Install Ollama
4446
RUN curl -fsSL https://ollama.ai/install.sh | sh
4547

46-
# Install Qdrant (standalone binary)
47-
RUN curl -L https://github.com/qdrant/qdrant/releases/download/v1.12.4/qdrant-x86_64-unknown-linux-musl.tar.gz | \
48+
# Install Qdrant (standalone binary - architecture aware)
49+
RUN ARCH=$(uname -m) && \
50+
if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then \
51+
QDRANT_ARCH="aarch64-unknown-linux-musl"; \
52+
else \
53+
QDRANT_ARCH="x86_64-unknown-linux-musl"; \
54+
fi && \
55+
curl -L "https://github.com/qdrant/qdrant/releases/download/v1.12.4/qdrant-${QDRANT_ARCH}.tar.gz" | \
4856
tar xz -C /usr/local/bin
4957

5058
# Copy virtual environment from builder

api/auth.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ async def login(request: Request):
140140
state = secrets.token_urlsafe(32)
141141

142142
# Store state in cookie (short-lived)
143-
redirect_uri = f"{BASE_URL}/auth/callback"
143+
redirect_uri = f"{BASE_URL}/oauth/github-callback"
144144
auth_url = (
145145
f"{GITHUB_AUTH_URL}?"
146146
f"client_id={GITHUB_CLIENT_ID}&"

api/main.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from starlette.responses import Response
1717

1818
from api.routes import collections, upload, search, system, mcp
19-
from api import auth
19+
from api import auth, oauth
2020
from api.middleware.auth_middleware import AuthMiddleware
2121

2222
# Metrics
@@ -80,6 +80,7 @@ async def metrics_middleware(request: Request, call_next):
8080
app.add_middleware(AuthMiddleware)
8181

8282
# Include routers
83+
app.include_router(oauth.router, tags=["OAuth"]) # OAuth at root for .well-known paths
8384
app.include_router(auth.router, prefix="/auth", tags=["Authentication"])
8485
app.include_router(collections.router, prefix="/api/collections", tags=["Collections"])
8586
app.include_router(upload.router, prefix="/api", tags=["Upload"])
@@ -157,6 +158,25 @@ async def metrics():
157158
app.mount("/static", StaticFiles(directory=str(FRONTEND_DIR / "static")), name="static")
158159

159160

161+
# Serve favicon
162+
@app.get("/favicon.ico", tags=["Frontend"], include_in_schema=False)
163+
@app.get("/favicon.svg", tags=["Frontend"], include_in_schema=False)
164+
async def serve_favicon():
165+
"""Serve favicon."""
166+
favicon_path = FRONTEND_DIR / "static" / "favicon.svg"
167+
if favicon_path.exists():
168+
return FileResponse(str(favicon_path), media_type="image/svg+xml")
169+
return Response(status_code=204)
170+
171+
172+
# Serve apple touch icons (return 204 No Content to suppress 404)
173+
@app.get("/apple-touch-icon.png", tags=["Frontend"], include_in_schema=False)
174+
@app.get("/apple-touch-icon-precomposed.png", tags=["Frontend"], include_in_schema=False)
175+
async def serve_apple_icon():
176+
"""Serve apple touch icon (or empty response)."""
177+
return Response(status_code=204)
178+
179+
160180
# Serve frontend SPA (catch-all for client-side routing)
161181
@app.get("/", tags=["Frontend"])
162182
@app.get("/dashboard", tags=["Frontend"])

api/middleware/auth_middleware.py

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
Authentication middleware for FastAPI.
33
44
Protects routes based on authentication status and configuration.
5+
Supports both session cookies and Bearer tokens for OAuth 2.0.
56
"""
67

78
from fastapi import Request, HTTPException
8-
from fastapi.responses import RedirectResponse
9+
from fastapi.responses import RedirectResponse, JSONResponse
910
from starlette.middleware.base import BaseHTTPMiddleware
1011

1112
from api.auth import is_auth_enabled, get_current_user
13+
from api.oauth import validate_bearer_token
1214

1315

1416
# Paths that don't require authentication
@@ -23,13 +25,16 @@
2325
"/api/redoc",
2426
"/api/openapi.json",
2527
"/static",
26-
"/favicon.ico"
28+
"/favicon.ico",
29+
"/register", # OAuth dynamic client registration
2730
}
2831

2932
# Path prefixes that don't require authentication
3033
PUBLIC_PREFIXES = (
3134
"/static/",
3235
"/auth/",
36+
"/.well-known/", # OAuth discovery endpoints
37+
"/oauth/", # OAuth endpoints (authorize, token, register)
3338
)
3439

3540

@@ -73,21 +78,32 @@ async def dispatch(self, request: Request, call_next):
7378
if not is_auth_enabled():
7479
return await call_next(request)
7580

76-
# Check authentication
81+
# Check for Bearer token first (for MCP/API clients)
82+
auth_header = request.headers.get("Authorization", "")
83+
if auth_header.startswith("Bearer "):
84+
token = auth_header[7:] # Remove "Bearer " prefix
85+
token_data = validate_bearer_token(token)
86+
if token_data:
87+
# Token is valid, continue
88+
request.state.user = token_data
89+
return await call_next(request)
90+
91+
# Check session cookie (for browser users)
7792
user = get_current_user(request)
7893

7994
if not user:
80-
# API routes return 401
95+
# API routes return 401 JSON response
8196
if path.startswith("/api/") or path.startswith("/mcp/"):
82-
raise HTTPException(
97+
return JSONResponse(
8398
status_code=401,
84-
detail="Authentication required"
99+
content={"detail": "Authentication required"}
85100
)
86101

87102
# Browser routes redirect to login
88103
return RedirectResponse(url="/auth/login", status_code=302)
89104

90105
# User is authenticated, continue
106+
request.state.user = user
91107
return await call_next(request)
92108

93109

@@ -96,6 +112,7 @@ def require_auth(request: Request) -> dict:
96112
Dependency to require authentication.
97113
98114
Use as a FastAPI dependency on routes that need auth.
115+
Supports both session cookies and Bearer tokens.
99116
100117
Args:
101118
request: FastAPI request
@@ -109,11 +126,24 @@ def require_auth(request: Request) -> dict:
109126
if not is_auth_enabled():
110127
return {"username": "anonymous", "authenticated": False}
111128

129+
# Check for Bearer token first
130+
auth_header = request.headers.get("Authorization", "")
131+
if auth_header.startswith("Bearer "):
132+
token = auth_header[7:]
133+
token_data = validate_bearer_token(token)
134+
if token_data:
135+
return {
136+
"username": token_data.get("username"),
137+
"authenticated": True,
138+
"auth_type": "bearer"
139+
}
140+
141+
# Check session cookie
112142
user = get_current_user(request)
113143
if not user:
114144
raise HTTPException(
115145
status_code=401,
116146
detail="Authentication required"
117147
)
118148

119-
return user
149+
return {**user, "auth_type": "session"}

0 commit comments

Comments
 (0)