Skip to content
Merged
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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,16 @@ Authentication, registration, conversation settings, model option policies, file

When SSRF protection is enabled in production, administrator-saved model, MCP, Embedding, OIDC/OAuth2, and custom Turnstile endpoints are authorized locally by exact origin (`scheme + host + port`) and do not require entries in the global allowlist. Model, MCP, and Embedding redirects retain standard compatibility: public cross-origin targets are allowed, while private cross-origin targets must match `SSRF_ALLOWED_HOSTS` or `SSRF_ALLOWED_CIDRS`; OIDC/OAuth2 and Turnstile keep their stricter identity boundary. Generated media is downloaded, validated, and stored by the backend: a private artifact URL inherits trust only when it has the same origin as the selected model endpoint; public cross-origin artifact URLs remain subject to the strict public-network policy, and private cross-origin artifact URLs are blocked. The global allowlist also remains available for deployment-level integrations that cannot be tied to an administrator-saved endpoint, such as selected GeoIP or extraction deployments. Link-local, multicast, unspecified, and known metadata targets always remain blocked. Invalid allowlist entries stop backend startup, and global allowlist changes require a restart.

### OAuth callbacks for Web, App, and Desktop (multi-platform clients not yet released)

Set `PUBLIC_API_BASE_URL` to the externally reachable API origin before enabling the provider auth bridge. For every OIDC/OAuth2 provider, register the server callback shown in the admin provider dialog:

```text
<PUBLIC_API_BASE_URL>/api/v1/auth/providers/<provider-slug>/callback
```

Web, App, and Desktop clients then reuse that instance callback automatically. The external provider authorization code and client secret remain on the self-hosted server; public clients receive only a short-lived, one-time DEEIX grant bound to their PKCE verifier. Keep the legacy Web callback shown by the admin dialog registered when account identity binding or older Web clients are still in use.

## Feature Guides

- [User Guide](https://deeix.com/docs/deeix-chat/new-chat)
Expand Down
10 changes: 10 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,16 @@ observability:

启用 Turnstile 需要同时启用 `auth:email_registration_enabled`,并配置 Site Key 与 Secret Key。开启邮箱验证码注册时,前端在 `/api/v1/auth/register/email/start` 提交 `turnstileToken`;关闭邮箱验证码但允许邮箱注册时,前端在 `/api/v1/auth/register/email/complete` 提交 `turnstileToken`。

## OAuth 公共客户端授权桥(多端暂未发布)

Web、App 和桌面端统一通过当前实例完成第三方 OAuth 回调。部署必须提供外部可访问的 `PUBLIC_API_BASE_URL`,身份源回调格式为:

```text
<PUBLIC_API_BASE_URL>/api/v1/auth/providers/<provider-slug>/callback
```

`POST /auth/providers/:slug/authorize` 创建短时事务并使用服务端独立 PKCE 访问上游;`GET /auth/providers/:slug/callback` 在服务端兑换上游授权码;`POST /auth/providers/:slug/exchange` 使用公共客户端 PKCE verifier 原子兑换一次性 DEEIX grant。事务与 grant 使用现有 Redis/内存缓存后端,外部 provider code、Client Secret 和 Token 均不会进入公共客户端。旧 `/start` 与 `POST /callback` 流程继续保留,用于账号身份绑定与旧版 Web 客户端兼容。

生产环境安全校验:

- `APP_ENV` 支持 `dev`/`development` 和 `prod`/`production`,其他值会启动失败。
Expand Down
217 changes: 217 additions & 0 deletions backend/docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -7267,6 +7267,106 @@ const docTemplate = `{
}
}
},
"/auth/providers/{slug}/authorize": {
"post": {
"description": "为 Web、App 或桌面公共客户端创建 PKCE 保护的 OAuth 授权事务;外部身份源仅回调当前 DEEIX 实例",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "创建第三方登录授权桥事务",
"parameters": [
{
"type": "string",
"description": "身份源 slug",
"name": "slug",
"in": "path",
"required": true
},
{
"description": "授权桥参数",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/ProviderAuthBridgeStartRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/ProviderAuthBridgeStartResponseDoc"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/AuthErrorDoc"
}
}
}
}
},
"/auth/providers/{slug}/exchange": {
"post": {
"description": "使用客户端 PKCE verifier 原子兑换服务端回调签发的一次性授权码,并进入统一 2FA/会话流程",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "兑换第三方登录一次性授权码",
"parameters": [
{
"type": "string",
"description": "身份源 slug",
"name": "slug",
"in": "path",
"required": true
},
{
"description": "授权码兑换参数",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/ProviderAuthBridgeExchangeRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/LoginResponseDoc"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/AuthErrorDoc"
}
},
"409": {
"description": "Conflict",
"schema": {
"$ref": "#/definitions/AuthErrorDoc"
}
}
}
}
},
"/auth/refresh": {
"post": {
"description": "使用 HttpOnly refresh cookie 轮换并签发新的 access token",
Expand Down Expand Up @@ -16305,6 +16405,7 @@ const docTemplate = `{
"emailRegistrationEnabled",
"emailVerificationEnabled",
"passwordResetEnabled",
"providerAuthBridge",
"providers",
"turnstileRegistrationEnabled",
"turnstileSiteKey",
Expand All @@ -16323,6 +16424,9 @@ const docTemplate = `{
"passwordResetEnabled": {
"type": "boolean"
},
"providerAuthBridge": {
"$ref": "#/definitions/ProviderAuthBridgeResponse"
},
"providers": {
"type": "array",
"items": {
Expand Down Expand Up @@ -18898,6 +19002,119 @@ const docTemplate = `{
}
}
},
"ProviderAuthBridgeExchangeRequest": {
"type": "object",
"required": [
"clientID",
"codeVerifier",
"grant"
],
"properties": {
"clientID": {
"type": "string",
"maxLength": 128
},
"codeVerifier": {
"type": "string",
"maxLength": 128,
"minLength": 43
},
"grant": {
"type": "string",
"maxLength": 128,
"minLength": 43
}
}
},
"ProviderAuthBridgeResponse": {
"type": "object",
"required": [
"callbackBaseURL",
"enabled",
"protocolVersion"
],
"properties": {
"callbackBaseURL": {
"type": "string"
},
"enabled": {
"type": "boolean"
},
"protocolVersion": {
"type": "integer"
}
}
},
"ProviderAuthBridgeStartRequest": {
"type": "object",
"required": [
"clientID",
"clientState",
"codeChallenge",
"redirectURI"
],
"properties": {
"clientID": {
"type": "string",
"maxLength": 128
},
"clientState": {
"type": "string",
"maxLength": 128,
"minLength": 43
},
"codeChallenge": {
"type": "string",
"maxLength": 128,
"minLength": 43
},
"intent": {
"type": "string",
"enum": [
"login",
"register"
]
},
"next": {
"type": "string",
"maxLength": 2048
},
"redirectURI": {
"type": "string",
"maxLength": 2048
}
}
},
"ProviderAuthBridgeStartResponse": {
"type": "object",
"required": [
"authorizationURL",
"expiresAt"
],
"properties": {
"authorizationURL": {
"type": "string"
},
"expiresAt": {
"type": "string"
}
}
},
"ProviderAuthBridgeStartResponseDoc": {
"type": "object",
"required": [
"data",
"errorMsg"
],
"properties": {
"data": {
"$ref": "#/definitions/ProviderAuthBridgeStartResponse"
},
"errorMsg": {
"type": "string"
}
}
},
"PublicModelListResponseDoc": {
"type": "object",
"required": [
Expand Down
Loading
Loading