Improve Feishu permission checks and OpenVPN auth result page

This commit is contained in:
opsx committed 2026-10-22 21:13:27 +08:00
1 parent 0231c376c6
commit 373a0904be
9 files changed
+104 -40

No files matched your search

+5 -1
View File
@@ -162,6 +162,7 @@ func (p *Provider) Status(c *gin.Context) {
"tenant_key": cfg.TenantKey, "client_id": cfg.ClientID, "client_secret_configured": cfg.ClientSecret != "",
"issuer": base, "feishu_callback": callback, "vpn_callback": vpnCallback,
"tls_ready": tlsError == nil, "gateway_ready": issuerRoute,
"user_scope_verified": cfg.UserScopeVerifiedAt != nil,
})
}
@@ -218,7 +219,7 @@ func (p *Provider) Configure(c *gin.Context) {
return
}
}
if cfg.AppID != req.AppID && req.Enabled && !autoTenant {
if (cfg.AppID != req.AppID || req.AppSecret != "") && req.Enabled && !autoTenant {
creds, err := resolveTenantCredentials(tenantCredentials{req.AppID, req.AppSecret}, cfg.AppID, cfg.AppSecret)
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
@@ -245,6 +246,9 @@ func (p *Provider) Configure(c *gin.Context) {
return
}
}
if cfg.AppID != req.AppID || req.AppSecret != "" || cfg.TenantKey != req.TenantKey {
cfg.UserScopeVerifiedAt = nil
}
if req.Enabled && cfg.AppSecret == "" {
c.JSON(400, gin.H{"error": "飞书 App Secret 未配置"})
return
@@ -222,6 +222,9 @@ func (p *Provider) FeishuCallback(c *gin.Context) {
oidcError(c, 503)
return
}
// A successful user-info lookup confirms this application's browser scope.
// Do not store user identifiers; reset this evidence when credentials change.
_ = store.DB.Model(&model.IdentityProvider{}).Where("id = ? AND app_id = ? AND tenant_key = ?", cfg.ID, cfg.AppID, cfg.TenantKey).Update("user_scope_verified_at", time.Now()).Error
u, _ := url.Parse(state.Redirect)
q := u.Query()
q.Set("code", code)
+9 -8
View File
@@ -6,14 +6,15 @@ import "time"
// IdentityProvider is the opt-in, single-tenant Feishu OIDC issuer configuration.
// Credentials are encrypted with the platform key and never serialized in API responses.
type IdentityProvider struct {
ID uint `gorm:"primaryKey" json:"-"`
Enabled bool `json:"enabled"`
AppID string `gorm:"size:128" json:"app_id"`
AppSecret string `gorm:"type:text" json:"-"`
TenantKey string `gorm:"size:128" json:"tenant_key"`
ClientID string `gorm:"size:128" json:"client_id"`
ClientSecret string `gorm:"type:text" json:"-"`
UpdatedAt time.Time `json:"updated_at"`
ID uint `gorm:"primaryKey" json:"-"`
Enabled bool `json:"enabled"`
AppID string `gorm:"size:128" json:"app_id"`
AppSecret string `gorm:"type:text" json:"-"`
TenantKey string `gorm:"size:128" json:"tenant_key"`
ClientID string `gorm:"size:128" json:"client_id"`
ClientSecret string `gorm:"type:text" json:"-"`
UserScopeVerifiedAt *time.Time `json:"-"` // Set only after a successful Feishu user-info callback.
UpdatedAt time.Time `json:"updated_at"`
}
// IdentityGrant stores only a hash of each browser state, authorization code,
@@ -0,0 +1,17 @@
package openvpn
// This page is intentionally outcome-neutral: the auth component can render its
// end template for different outcomes. Only the VPN client knows connection state.
const authResultPage = `<!doctype html>
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="color-scheme" content="light"><title>返回 VPN 客户端 · OPSX VPN</title>
<style>
*{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px;background:radial-gradient(circle at 50% 0%,#e8f2ff 0%,#f5f8fc 48%,#f3f6fa 100%);color:#1c334e;font:16px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
.card{width:min(100%,440px);padding:44px 36px 38px;background:#fff;border:1px solid #e3eaf3;border-radius:22px;box-shadow:0 22px 65px rgba(29,68,113,.09);text-align:center}
.brand{color:#3975bb;letter-spacing:.16em;font-size:12px;font-weight:800}.mark{display:grid;place-items:center;width:72px;height:72px;margin:28px auto 22px;background:#ebf4ff;border-radius:22px;color:#3479c8}
h1{font-size:25px;line-height:1.3;margin:0 0 12px}p{margin:0;color:#62758a}.tip{margin-top:30px;padding:14px;background:#f4f8fd;border-radius:12px;font-size:14px;color:#526a84}
@media(max-width:480px){.card{padding:36px 24px}}
</style></head><body><main class="card"><div class="brand">OPSX VPN</div>
<div class="mark" aria-hidden="true"><svg width="38" height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M19 3 33 9v9c0 9-5.8 14.6-14 17C10.8 32.6 5 27 5 18V9L19 3Z" stroke="currentColor" stroke-width="2.5"/><path d="m12.5 19 4.5 4.5 8.5-9" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
<h1>请返回 VPN 客户端</h1><p>浏览器认证步骤已结束。连接状态请以客户端显示为准。</p>
<div class="tip">此页面可以关闭</div></main></body></html>`
+9 -1
View File
@@ -62,10 +62,11 @@ func yamlString(s string) string {
func oidcConfig(t *model.Tunnel, cfg map[string]interface{}) string {
dir := service.InstanceDir(t)
return fmt.Sprintf("http:\n baseurl: %s\n listen: %s\n secret: %s\noauth2:\n issuer: %s\n client:\n id: %s\n secret: %s\nopenvpn:\n addr: %s\n password: %s\n auth-token-user: true\n",
return fmt.Sprintf("http:\n baseurl: %s\n listen: %s\n secret: %s\n template: %s\noauth2:\n issuer: %s\n client:\n id: %s\n secret: %s\nopenvpn:\n addr: %s\n password: %s\n auth-token-user: true\n",
yamlString(service.Str(cfg, "oidc_base_url")),
yamlString(fmt.Sprintf("127.0.0.1:%d", int(service.Num(cfg, "oidc_http_port", 19090)))),
yamlString(service.Str(cfg, "oidc_http_secret")),
yamlString(filepath.Join(dir, "auth-result.html")),
yamlString(service.Str(cfg, "oidc_issuer")),
yamlString(service.Str(cfg, "oidc_client_id")),
yamlString(service.Str(cfg, "oidc_client_secret")),
@@ -140,6 +141,13 @@ func prepareOIDC(t *model.Tunnel, cfg map[string]interface{}) error {
if err := os.Chmod(passwordPath, 0o600); err != nil {
return err
}
pagePath, err := service.WriteConfig(t, "auth-result.html", authResultPage)
if err != nil {
return err
}
if err := os.Chmod(pagePath, 0o600); err != nil {
return err
}
configPath, err := service.WriteConfig(t, "openvpn-auth-oauth2.yaml", oidcConfig(t, cfg))
if err != nil {
return err