|
| 1 | +# API Endpoints and Authentication |
| 2 | + |
| 3 | +This guide covers how to add new API endpoints and properly integrate them with the auth/permissions system. |
| 4 | + |
| 5 | +## Architecture overview |
| 6 | + |
| 7 | +Authentication and authorization flow through three layers: |
| 8 | + |
| 9 | +1. **Global auth middleware** (`core/http/auth/middleware.go` → `auth.Middleware`) — applied to every request in `core/http/app.go`. Handles session cookies, Bearer tokens, API keys, and legacy API keys. Populates `auth_user` and `auth_role` in the Echo context. |
| 10 | +2. **Feature middleware** (`auth.RequireFeature`) — per-feature access control applied to route groups or individual routes. Checks if the authenticated user has the specific feature enabled. |
| 11 | +3. **Admin middleware** (`auth.RequireAdmin`) — restricts endpoints to admin users only. |
| 12 | + |
| 13 | +When auth is disabled (no auth DB, no legacy API keys), all middleware becomes pass-through (`auth.NoopMiddleware`). |
| 14 | + |
| 15 | +## Adding a new API endpoint |
| 16 | + |
| 17 | +### Step 1: Create the handler |
| 18 | + |
| 19 | +Write the endpoint handler in the appropriate package under `core/http/endpoints/`. Follow existing patterns: |
| 20 | + |
| 21 | +```go |
| 22 | +// core/http/endpoints/localai/my_feature.go |
| 23 | +func MyFeatureEndpoint(app *application.Application) echo.HandlerFunc { |
| 24 | + return func(c echo.Context) error { |
| 25 | + // Use auth.GetUser(c) to get the authenticated user (may be nil if auth is disabled) |
| 26 | + user := auth.GetUser(c) |
| 27 | + |
| 28 | + // Your logic here |
| 29 | + return c.JSON(http.StatusOK, result) |
| 30 | + } |
| 31 | +} |
| 32 | +``` |
| 33 | + |
| 34 | +### Step 2: Register routes |
| 35 | + |
| 36 | +Add routes in the appropriate file under `core/http/routes/`. The file you use depends on the endpoint category: |
| 37 | + |
| 38 | +| File | Category | |
| 39 | +|------|----------| |
| 40 | +| `routes/openai.go` | OpenAI-compatible API endpoints (`/v1/...`) | |
| 41 | +| `routes/localai.go` | LocalAI-specific endpoints (`/api/...`, `/models/...`, `/backends/...`) | |
| 42 | +| `routes/agents.go` | Agent pool endpoints (`/api/agents/...`) | |
| 43 | +| `routes/auth.go` | Auth endpoints (`/api/auth/...`) | |
| 44 | +| `routes/ui_api.go` | UI backend API endpoints | |
| 45 | + |
| 46 | +### Step 3: Apply the right middleware |
| 47 | + |
| 48 | +Choose the appropriate protection level: |
| 49 | + |
| 50 | +#### No auth required (public) |
| 51 | +Exempt paths bypass auth entirely. Add to `isExemptPath()` in `middleware.go` or use the `/api/auth/` prefix (always exempt). Use sparingly — most endpoints should require auth. |
| 52 | + |
| 53 | +#### Standard auth (any authenticated user) |
| 54 | +The global middleware already handles this. API paths (`/api/`, `/v1/`, etc.) automatically require authentication when auth is enabled. You don't need to add any extra middleware. |
| 55 | + |
| 56 | +```go |
| 57 | +router.GET("/v1/my-endpoint", myHandler) // auth enforced by global middleware |
| 58 | +``` |
| 59 | + |
| 60 | +#### Admin only |
| 61 | +Pass `adminMiddleware` to the route. This is set up in `app.go` and passed to `Register*Routes` functions: |
| 62 | + |
| 63 | +```go |
| 64 | +// In the Register function signature, accept the middleware: |
| 65 | +func RegisterMyRoutes(router *echo.Echo, app *application.Application, adminMiddleware echo.MiddlewareFunc) { |
| 66 | + router.POST("/models/apply", myHandler, adminMiddleware) |
| 67 | +} |
| 68 | +``` |
| 69 | + |
| 70 | +#### Feature-gated |
| 71 | +For endpoints that should be toggleable per-user, use feature middleware. There are two approaches: |
| 72 | + |
| 73 | +**Approach A: Route-level middleware** (preferred for groups of related endpoints) |
| 74 | + |
| 75 | +```go |
| 76 | +// In app.go, create the feature middleware: |
| 77 | +myFeatureMw := auth.RequireFeature(application.AuthDB(), auth.FeatureMyFeature) |
| 78 | + |
| 79 | +// Pass it to the route registration function: |
| 80 | +routes.RegisterMyRoutes(e, app, myFeatureMw) |
| 81 | + |
| 82 | +// In the routes file, apply to a group: |
| 83 | +g := e.Group("/api/my-feature", myFeatureMw) |
| 84 | +g.GET("", listHandler) |
| 85 | +g.POST("", createHandler) |
| 86 | +``` |
| 87 | + |
| 88 | +**Approach B: RouteFeatureRegistry** (preferred for individual OpenAI-compatible endpoints) |
| 89 | + |
| 90 | +Add an entry to `RouteFeatureRegistry` in `core/http/auth/features.go`. The `RequireRouteFeature` global middleware will automatically enforce it: |
| 91 | + |
| 92 | +```go |
| 93 | +var RouteFeatureRegistry = []RouteFeature{ |
| 94 | + // ... existing entries ... |
| 95 | + {"POST", "/v1/my-endpoint", FeatureMyFeature}, |
| 96 | +} |
| 97 | +``` |
| 98 | + |
| 99 | +## Adding a new feature |
| 100 | + |
| 101 | +When you need a new toggleable feature (not just a new endpoint under an existing feature): |
| 102 | + |
| 103 | +### 1. Define the feature constant |
| 104 | + |
| 105 | +Add to `core/http/auth/permissions.go`: |
| 106 | + |
| 107 | +```go |
| 108 | +const ( |
| 109 | + // Add to the appropriate group: |
| 110 | + // Agent features (default OFF for new users) |
| 111 | + FeatureMyFeature = "my_feature" |
| 112 | + |
| 113 | + // OR API features (default ON for new users) |
| 114 | + FeatureMyFeature = "my_feature" |
| 115 | +) |
| 116 | +``` |
| 117 | + |
| 118 | +Then add it to the appropriate slice: |
| 119 | + |
| 120 | +```go |
| 121 | +// Default OFF — user must be explicitly granted access: |
| 122 | +var AgentFeatures = []string{..., FeatureMyFeature} |
| 123 | + |
| 124 | +// Default ON — user has access unless explicitly revoked: |
| 125 | +var APIFeatures = []string{..., FeatureMyFeature} |
| 126 | +``` |
| 127 | + |
| 128 | +### 2. Add feature metadata |
| 129 | + |
| 130 | +In `core/http/auth/features.go`, add to the appropriate `FeatureMetas` function so the admin UI can display it: |
| 131 | + |
| 132 | +```go |
| 133 | +func AgentFeatureMetas() []FeatureMeta { |
| 134 | + return []FeatureMeta{ |
| 135 | + // ... existing ... |
| 136 | + {FeatureMyFeature, "My Feature", false}, // false = default OFF |
| 137 | + } |
| 138 | +} |
| 139 | +``` |
| 140 | + |
| 141 | +### 3. Wire up the middleware |
| 142 | + |
| 143 | +In `core/http/app.go`: |
| 144 | + |
| 145 | +```go |
| 146 | +myFeatureMw := auth.RequireFeature(application.AuthDB(), auth.FeatureMyFeature) |
| 147 | +``` |
| 148 | + |
| 149 | +Then pass it to the route registration function. |
| 150 | + |
| 151 | +### 4. Register route-feature mappings (if applicable) |
| 152 | + |
| 153 | +If your feature gates standard API endpoints (like `/v1/...`), add entries to `RouteFeatureRegistry` in `features.go` instead of using per-route middleware. |
| 154 | + |
| 155 | +## Accessing the authenticated user in handlers |
| 156 | + |
| 157 | +```go |
| 158 | +import "github.com/mudler/LocalAI/core/http/auth" |
| 159 | + |
| 160 | +func MyHandler(c echo.Context) error { |
| 161 | + // Get the user (nil when auth is disabled or unauthenticated) |
| 162 | + user := auth.GetUser(c) |
| 163 | + if user == nil { |
| 164 | + // Handle unauthenticated — or let middleware handle it |
| 165 | + } |
| 166 | + |
| 167 | + // Check role |
| 168 | + if user.Role == auth.RoleAdmin { |
| 169 | + // admin-specific logic |
| 170 | + } |
| 171 | + |
| 172 | + // Check feature access programmatically (when you need conditional behavior, not full blocking) |
| 173 | + if auth.HasFeatureAccess(db, user, auth.FeatureMyFeature) { |
| 174 | + // feature-specific logic |
| 175 | + } |
| 176 | + |
| 177 | + // Check model access |
| 178 | + if !auth.IsModelAllowed(db, user, modelName) { |
| 179 | + return c.JSON(http.StatusForbidden, ...) |
| 180 | + } |
| 181 | +} |
| 182 | +``` |
| 183 | + |
| 184 | +## Middleware composition patterns |
| 185 | + |
| 186 | +Middleware can be composed at different levels. Here are the patterns used in the codebase: |
| 187 | + |
| 188 | +### Group-level middleware (agents pattern) |
| 189 | +```go |
| 190 | +// All routes in the group share the middleware |
| 191 | +g := e.Group("/api/agents", poolReadyMw, agentsMw) |
| 192 | +g.GET("", listHandler) |
| 193 | +g.POST("", createHandler) |
| 194 | +``` |
| 195 | + |
| 196 | +### Per-route middleware (localai pattern) |
| 197 | +```go |
| 198 | +// Individual routes get middleware as extra arguments |
| 199 | +router.POST("/models/apply", applyHandler, adminMiddleware) |
| 200 | +router.GET("/metrics", metricsHandler, adminMiddleware) |
| 201 | +``` |
| 202 | + |
| 203 | +### Middleware slice (openai pattern) |
| 204 | +```go |
| 205 | +// Build a middleware chain for a handler |
| 206 | +chatMiddleware := []echo.MiddlewareFunc{ |
| 207 | + usageMiddleware, |
| 208 | + traceMiddleware, |
| 209 | + modelFilterMiddleware, |
| 210 | +} |
| 211 | +app.POST("/v1/chat/completions", chatHandler, chatMiddleware...) |
| 212 | +``` |
| 213 | + |
| 214 | +## Error response format |
| 215 | + |
| 216 | +Always use `schema.ErrorResponse` for auth/permission errors to stay consistent with the OpenAI-compatible API: |
| 217 | + |
| 218 | +```go |
| 219 | +return c.JSON(http.StatusForbidden, schema.ErrorResponse{ |
| 220 | + Error: &schema.APIError{ |
| 221 | + Message: "feature not enabled for your account", |
| 222 | + Code: http.StatusForbidden, |
| 223 | + Type: "authorization_error", |
| 224 | + }, |
| 225 | +}) |
| 226 | +``` |
| 227 | + |
| 228 | +Use these HTTP status codes: |
| 229 | +- `401 Unauthorized` — no valid credentials provided |
| 230 | +- `403 Forbidden` — authenticated but lacking permission |
| 231 | +- `429 Too Many Requests` — rate limited (auth endpoints) |
| 232 | + |
| 233 | +## Usage tracking |
| 234 | + |
| 235 | +If your endpoint should be tracked for usage (token counts, request counts), add the `usageMiddleware` to its middleware chain. See `core/http/middleware/usage.go` and how it's applied in `routes/openai.go`. |
| 236 | + |
| 237 | +## Path protection rules |
| 238 | + |
| 239 | +The global auth middleware classifies paths as API paths or non-API paths: |
| 240 | + |
| 241 | +- **API paths** (always require auth when auth is enabled): `/api/`, `/v1/`, `/models/`, `/backends/`, `/backend/`, `/tts`, `/vad`, `/video`, `/stores/`, `/system`, `/ws/`, `/metrics` |
| 242 | +- **Exempt paths** (never require auth): `/api/auth/` prefix, anything in `appConfig.PathWithoutAuth` |
| 243 | +- **Non-API paths** (UI, static assets): pass through without auth — the React UI handles login redirects client-side |
| 244 | + |
| 245 | +If you add endpoints under a new top-level path prefix, add it to `isAPIPath()` in `middleware.go` to ensure it requires authentication. |
| 246 | + |
| 247 | +## Checklist |
| 248 | + |
| 249 | +When adding a new endpoint: |
| 250 | + |
| 251 | +- [ ] Handler in `core/http/endpoints/` |
| 252 | +- [ ] Route registered in appropriate `core/http/routes/` file |
| 253 | +- [ ] Auth level chosen: public / standard / admin / feature-gated |
| 254 | +- [ ] If feature-gated: constant in `permissions.go`, metadata in `features.go`, middleware in `app.go` |
| 255 | +- [ ] If new path prefix: added to `isAPIPath()` in `middleware.go` |
| 256 | +- [ ] If OpenAI-compatible: entry in `RouteFeatureRegistry` |
| 257 | +- [ ] If token-counting: `usageMiddleware` added to middleware chain |
| 258 | +- [ ] Error responses use `schema.ErrorResponse` format |
| 259 | +- [ ] Tests cover both authenticated and unauthenticated access |
0 commit comments