|
|
|
@@ -16,6 +16,7 @@ import (
|
|
|
|
|
|
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
|
|
|
|
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
|
|
|
|
"github.com/imroc/req/v3"
|
|
|
|
|
"golang.org/x/sync/singleflight"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
@@ -164,6 +165,8 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
|
|
|
|
|
SettingKeyCustomEndpoints,
|
|
|
|
|
SettingKeyLinuxDoConnectEnabled,
|
|
|
|
|
SettingKeyBackendModeEnabled,
|
|
|
|
|
SettingKeyOIDCConnectEnabled,
|
|
|
|
|
SettingKeyOIDCConnectProviderName,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
settings, err := s.settingRepo.GetMultiple(ctx, keys)
|
|
|
|
@@ -177,6 +180,19 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
|
|
|
|
|
} else {
|
|
|
|
|
linuxDoEnabled = s.cfg != nil && s.cfg.LinuxDo.Enabled
|
|
|
|
|
}
|
|
|
|
|
oidcEnabled := false
|
|
|
|
|
if raw, ok := settings[SettingKeyOIDCConnectEnabled]; ok {
|
|
|
|
|
oidcEnabled = raw == "true"
|
|
|
|
|
} else {
|
|
|
|
|
oidcEnabled = s.cfg != nil && s.cfg.OIDC.Enabled
|
|
|
|
|
}
|
|
|
|
|
oidcProviderName := strings.TrimSpace(settings[SettingKeyOIDCConnectProviderName])
|
|
|
|
|
if oidcProviderName == "" && s.cfg != nil {
|
|
|
|
|
oidcProviderName = strings.TrimSpace(s.cfg.OIDC.ProviderName)
|
|
|
|
|
}
|
|
|
|
|
if oidcProviderName == "" {
|
|
|
|
|
oidcProviderName = "OIDC"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Password reset requires email verification to be enabled
|
|
|
|
|
emailVerifyEnabled := settings[SettingKeyEmailVerifyEnabled] == "true"
|
|
|
|
@@ -209,6 +225,8 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
|
|
|
|
|
CustomEndpoints: settings[SettingKeyCustomEndpoints],
|
|
|
|
|
LinuxDoOAuthEnabled: linuxDoEnabled,
|
|
|
|
|
BackendModeEnabled: settings[SettingKeyBackendModeEnabled] == "true",
|
|
|
|
|
OIDCOAuthEnabled: oidcEnabled,
|
|
|
|
|
OIDCOAuthProviderName: oidcProviderName,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -256,6 +274,8 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
|
|
|
|
|
CustomEndpoints json.RawMessage `json:"custom_endpoints"`
|
|
|
|
|
LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"`
|
|
|
|
|
BackendModeEnabled bool `json:"backend_mode_enabled"`
|
|
|
|
|
OIDCOAuthEnabled bool `json:"oidc_oauth_enabled"`
|
|
|
|
|
OIDCOAuthProviderName string `json:"oidc_oauth_provider_name"`
|
|
|
|
|
Version string `json:"version,omitempty"`
|
|
|
|
|
}{
|
|
|
|
|
RegistrationEnabled: settings.RegistrationEnabled,
|
|
|
|
@@ -281,6 +301,8 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
|
|
|
|
|
CustomEndpoints: safeRawJSONArray(settings.CustomEndpoints),
|
|
|
|
|
LinuxDoOAuthEnabled: settings.LinuxDoOAuthEnabled,
|
|
|
|
|
BackendModeEnabled: settings.BackendModeEnabled,
|
|
|
|
|
OIDCOAuthEnabled: settings.OIDCOAuthEnabled,
|
|
|
|
|
OIDCOAuthProviderName: settings.OIDCOAuthProviderName,
|
|
|
|
|
Version: s.version,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
@@ -460,6 +482,32 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet
|
|
|
|
|
updates[SettingKeyLinuxDoConnectClientSecret] = settings.LinuxDoConnectClientSecret
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generic OIDC OAuth 登录
|
|
|
|
|
updates[SettingKeyOIDCConnectEnabled] = strconv.FormatBool(settings.OIDCConnectEnabled)
|
|
|
|
|
updates[SettingKeyOIDCConnectProviderName] = settings.OIDCConnectProviderName
|
|
|
|
|
updates[SettingKeyOIDCConnectClientID] = settings.OIDCConnectClientID
|
|
|
|
|
updates[SettingKeyOIDCConnectIssuerURL] = settings.OIDCConnectIssuerURL
|
|
|
|
|
updates[SettingKeyOIDCConnectDiscoveryURL] = settings.OIDCConnectDiscoveryURL
|
|
|
|
|
updates[SettingKeyOIDCConnectAuthorizeURL] = settings.OIDCConnectAuthorizeURL
|
|
|
|
|
updates[SettingKeyOIDCConnectTokenURL] = settings.OIDCConnectTokenURL
|
|
|
|
|
updates[SettingKeyOIDCConnectUserInfoURL] = settings.OIDCConnectUserInfoURL
|
|
|
|
|
updates[SettingKeyOIDCConnectJWKSURL] = settings.OIDCConnectJWKSURL
|
|
|
|
|
updates[SettingKeyOIDCConnectScopes] = settings.OIDCConnectScopes
|
|
|
|
|
updates[SettingKeyOIDCConnectRedirectURL] = settings.OIDCConnectRedirectURL
|
|
|
|
|
updates[SettingKeyOIDCConnectFrontendRedirectURL] = settings.OIDCConnectFrontendRedirectURL
|
|
|
|
|
updates[SettingKeyOIDCConnectTokenAuthMethod] = settings.OIDCConnectTokenAuthMethod
|
|
|
|
|
updates[SettingKeyOIDCConnectUsePKCE] = strconv.FormatBool(settings.OIDCConnectUsePKCE)
|
|
|
|
|
updates[SettingKeyOIDCConnectValidateIDToken] = strconv.FormatBool(settings.OIDCConnectValidateIDToken)
|
|
|
|
|
updates[SettingKeyOIDCConnectAllowedSigningAlgs] = settings.OIDCConnectAllowedSigningAlgs
|
|
|
|
|
updates[SettingKeyOIDCConnectClockSkewSeconds] = strconv.Itoa(settings.OIDCConnectClockSkewSeconds)
|
|
|
|
|
updates[SettingKeyOIDCConnectRequireEmailVerified] = strconv.FormatBool(settings.OIDCConnectRequireEmailVerified)
|
|
|
|
|
updates[SettingKeyOIDCConnectUserInfoEmailPath] = settings.OIDCConnectUserInfoEmailPath
|
|
|
|
|
updates[SettingKeyOIDCConnectUserInfoIDPath] = settings.OIDCConnectUserInfoIDPath
|
|
|
|
|
updates[SettingKeyOIDCConnectUserInfoUsernamePath] = settings.OIDCConnectUserInfoUsernamePath
|
|
|
|
|
if settings.OIDCConnectClientSecret != "" {
|
|
|
|
|
updates[SettingKeyOIDCConnectClientSecret] = settings.OIDCConnectClientSecret
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// OEM设置
|
|
|
|
|
updates[SettingKeySiteName] = settings.SiteName
|
|
|
|
|
updates[SettingKeySiteLogo] = settings.SiteLogo
|
|
|
|
@@ -826,6 +874,8 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
|
|
|
|
|
SettingKeyPurchaseSubscriptionURL: "",
|
|
|
|
|
SettingKeyCustomMenuItems: "[]",
|
|
|
|
|
SettingKeyCustomEndpoints: "[]",
|
|
|
|
|
SettingKeyOIDCConnectEnabled: "false",
|
|
|
|
|
SettingKeyOIDCConnectProviderName: "OIDC",
|
|
|
|
|
SettingKeyDefaultConcurrency: strconv.Itoa(s.cfg.Default.UserConcurrency),
|
|
|
|
|
SettingKeyDefaultBalance: strconv.FormatFloat(s.cfg.Default.UserBalance, 'f', 8, 64),
|
|
|
|
|
SettingKeyDefaultSubscriptions: "[]",
|
|
|
|
@@ -951,6 +1001,138 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
|
|
|
|
|
}
|
|
|
|
|
result.LinuxDoConnectClientSecretConfigured = result.LinuxDoConnectClientSecret != ""
|
|
|
|
|
|
|
|
|
|
// Generic OIDC 设置:
|
|
|
|
|
// - 兼容 config.yaml/env
|
|
|
|
|
// - 支持后台系统设置覆盖并持久化(存储于 DB)
|
|
|
|
|
oidcBase := config.OIDCConnectConfig{}
|
|
|
|
|
if s.cfg != nil {
|
|
|
|
|
oidcBase = s.cfg.OIDC
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if raw, ok := settings[SettingKeyOIDCConnectEnabled]; ok {
|
|
|
|
|
result.OIDCConnectEnabled = raw == "true"
|
|
|
|
|
} else {
|
|
|
|
|
result.OIDCConnectEnabled = oidcBase.Enabled
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectProviderName]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
result.OIDCConnectProviderName = strings.TrimSpace(v)
|
|
|
|
|
} else {
|
|
|
|
|
result.OIDCConnectProviderName = strings.TrimSpace(oidcBase.ProviderName)
|
|
|
|
|
}
|
|
|
|
|
if result.OIDCConnectProviderName == "" {
|
|
|
|
|
result.OIDCConnectProviderName = "OIDC"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectClientID]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
result.OIDCConnectClientID = strings.TrimSpace(v)
|
|
|
|
|
} else {
|
|
|
|
|
result.OIDCConnectClientID = strings.TrimSpace(oidcBase.ClientID)
|
|
|
|
|
}
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectIssuerURL]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
result.OIDCConnectIssuerURL = strings.TrimSpace(v)
|
|
|
|
|
} else {
|
|
|
|
|
result.OIDCConnectIssuerURL = strings.TrimSpace(oidcBase.IssuerURL)
|
|
|
|
|
}
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectDiscoveryURL]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
result.OIDCConnectDiscoveryURL = strings.TrimSpace(v)
|
|
|
|
|
} else {
|
|
|
|
|
result.OIDCConnectDiscoveryURL = strings.TrimSpace(oidcBase.DiscoveryURL)
|
|
|
|
|
}
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectAuthorizeURL]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
result.OIDCConnectAuthorizeURL = strings.TrimSpace(v)
|
|
|
|
|
} else {
|
|
|
|
|
result.OIDCConnectAuthorizeURL = strings.TrimSpace(oidcBase.AuthorizeURL)
|
|
|
|
|
}
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectTokenURL]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
result.OIDCConnectTokenURL = strings.TrimSpace(v)
|
|
|
|
|
} else {
|
|
|
|
|
result.OIDCConnectTokenURL = strings.TrimSpace(oidcBase.TokenURL)
|
|
|
|
|
}
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectUserInfoURL]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
result.OIDCConnectUserInfoURL = strings.TrimSpace(v)
|
|
|
|
|
} else {
|
|
|
|
|
result.OIDCConnectUserInfoURL = strings.TrimSpace(oidcBase.UserInfoURL)
|
|
|
|
|
}
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectJWKSURL]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
result.OIDCConnectJWKSURL = strings.TrimSpace(v)
|
|
|
|
|
} else {
|
|
|
|
|
result.OIDCConnectJWKSURL = strings.TrimSpace(oidcBase.JWKSURL)
|
|
|
|
|
}
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectScopes]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
result.OIDCConnectScopes = strings.TrimSpace(v)
|
|
|
|
|
} else {
|
|
|
|
|
result.OIDCConnectScopes = strings.TrimSpace(oidcBase.Scopes)
|
|
|
|
|
}
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectRedirectURL]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
result.OIDCConnectRedirectURL = strings.TrimSpace(v)
|
|
|
|
|
} else {
|
|
|
|
|
result.OIDCConnectRedirectURL = strings.TrimSpace(oidcBase.RedirectURL)
|
|
|
|
|
}
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectFrontendRedirectURL]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
result.OIDCConnectFrontendRedirectURL = strings.TrimSpace(v)
|
|
|
|
|
} else {
|
|
|
|
|
result.OIDCConnectFrontendRedirectURL = strings.TrimSpace(oidcBase.FrontendRedirectURL)
|
|
|
|
|
}
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectTokenAuthMethod]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
result.OIDCConnectTokenAuthMethod = strings.ToLower(strings.TrimSpace(v))
|
|
|
|
|
} else {
|
|
|
|
|
result.OIDCConnectTokenAuthMethod = strings.ToLower(strings.TrimSpace(oidcBase.TokenAuthMethod))
|
|
|
|
|
}
|
|
|
|
|
if raw, ok := settings[SettingKeyOIDCConnectUsePKCE]; ok {
|
|
|
|
|
result.OIDCConnectUsePKCE = raw == "true"
|
|
|
|
|
} else {
|
|
|
|
|
result.OIDCConnectUsePKCE = oidcBase.UsePKCE
|
|
|
|
|
}
|
|
|
|
|
if raw, ok := settings[SettingKeyOIDCConnectValidateIDToken]; ok {
|
|
|
|
|
result.OIDCConnectValidateIDToken = raw == "true"
|
|
|
|
|
} else {
|
|
|
|
|
result.OIDCConnectValidateIDToken = oidcBase.ValidateIDToken
|
|
|
|
|
}
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectAllowedSigningAlgs]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
result.OIDCConnectAllowedSigningAlgs = strings.TrimSpace(v)
|
|
|
|
|
} else {
|
|
|
|
|
result.OIDCConnectAllowedSigningAlgs = strings.TrimSpace(oidcBase.AllowedSigningAlgs)
|
|
|
|
|
}
|
|
|
|
|
clockSkewSet := false
|
|
|
|
|
if raw, ok := settings[SettingKeyOIDCConnectClockSkewSeconds]; ok && strings.TrimSpace(raw) != "" {
|
|
|
|
|
if parsed, err := strconv.Atoi(strings.TrimSpace(raw)); err == nil {
|
|
|
|
|
result.OIDCConnectClockSkewSeconds = parsed
|
|
|
|
|
clockSkewSet = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !clockSkewSet {
|
|
|
|
|
result.OIDCConnectClockSkewSeconds = oidcBase.ClockSkewSeconds
|
|
|
|
|
}
|
|
|
|
|
if !clockSkewSet && result.OIDCConnectClockSkewSeconds == 0 {
|
|
|
|
|
result.OIDCConnectClockSkewSeconds = 120
|
|
|
|
|
}
|
|
|
|
|
if raw, ok := settings[SettingKeyOIDCConnectRequireEmailVerified]; ok {
|
|
|
|
|
result.OIDCConnectRequireEmailVerified = raw == "true"
|
|
|
|
|
} else {
|
|
|
|
|
result.OIDCConnectRequireEmailVerified = oidcBase.RequireEmailVerified
|
|
|
|
|
}
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectUserInfoEmailPath]; ok {
|
|
|
|
|
result.OIDCConnectUserInfoEmailPath = strings.TrimSpace(v)
|
|
|
|
|
} else {
|
|
|
|
|
result.OIDCConnectUserInfoEmailPath = strings.TrimSpace(oidcBase.UserInfoEmailPath)
|
|
|
|
|
}
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectUserInfoIDPath]; ok {
|
|
|
|
|
result.OIDCConnectUserInfoIDPath = strings.TrimSpace(v)
|
|
|
|
|
} else {
|
|
|
|
|
result.OIDCConnectUserInfoIDPath = strings.TrimSpace(oidcBase.UserInfoIDPath)
|
|
|
|
|
}
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectUserInfoUsernamePath]; ok {
|
|
|
|
|
result.OIDCConnectUserInfoUsernamePath = strings.TrimSpace(v)
|
|
|
|
|
} else {
|
|
|
|
|
result.OIDCConnectUserInfoUsernamePath = strings.TrimSpace(oidcBase.UserInfoUsernamePath)
|
|
|
|
|
}
|
|
|
|
|
result.OIDCConnectClientSecret = strings.TrimSpace(settings[SettingKeyOIDCConnectClientSecret])
|
|
|
|
|
if result.OIDCConnectClientSecret == "" {
|
|
|
|
|
result.OIDCConnectClientSecret = strings.TrimSpace(oidcBase.ClientSecret)
|
|
|
|
|
}
|
|
|
|
|
result.OIDCConnectClientSecretConfigured = result.OIDCConnectClientSecret != ""
|
|
|
|
|
|
|
|
|
|
// Model fallback settings
|
|
|
|
|
result.EnableModelFallback = settings[SettingKeyEnableModelFallback] == "true"
|
|
|
|
|
result.FallbackModelAnthropic = s.getStringOrDefault(settings, SettingKeyFallbackModelAnthropic, "claude-3-5-sonnet-20241022")
|
|
|
|
@@ -1323,6 +1505,282 @@ func (s *SettingService) SetOverloadCooldownSettings(ctx context.Context, settin
|
|
|
|
|
return s.settingRepo.Set(ctx, SettingKeyOverloadCooldownSettings, string(data))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetOIDCConnectOAuthConfig 返回用于登录的“最终生效” OIDC 配置。
|
|
|
|
|
//
|
|
|
|
|
// 优先级:
|
|
|
|
|
// - 若对应系统设置键存在,则覆盖 config.yaml/env 的值
|
|
|
|
|
// - 否则回退到 config.yaml/env 的值
|
|
|
|
|
func (s *SettingService) GetOIDCConnectOAuthConfig(ctx context.Context) (config.OIDCConnectConfig, error) {
|
|
|
|
|
if s == nil || s.cfg == nil {
|
|
|
|
|
return config.OIDCConnectConfig{}, infraerrors.ServiceUnavailable("CONFIG_NOT_READY", "config not loaded")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
effective := s.cfg.OIDC
|
|
|
|
|
|
|
|
|
|
keys := []string{
|
|
|
|
|
SettingKeyOIDCConnectEnabled,
|
|
|
|
|
SettingKeyOIDCConnectProviderName,
|
|
|
|
|
SettingKeyOIDCConnectClientID,
|
|
|
|
|
SettingKeyOIDCConnectClientSecret,
|
|
|
|
|
SettingKeyOIDCConnectIssuerURL,
|
|
|
|
|
SettingKeyOIDCConnectDiscoveryURL,
|
|
|
|
|
SettingKeyOIDCConnectAuthorizeURL,
|
|
|
|
|
SettingKeyOIDCConnectTokenURL,
|
|
|
|
|
SettingKeyOIDCConnectUserInfoURL,
|
|
|
|
|
SettingKeyOIDCConnectJWKSURL,
|
|
|
|
|
SettingKeyOIDCConnectScopes,
|
|
|
|
|
SettingKeyOIDCConnectRedirectURL,
|
|
|
|
|
SettingKeyOIDCConnectFrontendRedirectURL,
|
|
|
|
|
SettingKeyOIDCConnectTokenAuthMethod,
|
|
|
|
|
SettingKeyOIDCConnectUsePKCE,
|
|
|
|
|
SettingKeyOIDCConnectValidateIDToken,
|
|
|
|
|
SettingKeyOIDCConnectAllowedSigningAlgs,
|
|
|
|
|
SettingKeyOIDCConnectClockSkewSeconds,
|
|
|
|
|
SettingKeyOIDCConnectRequireEmailVerified,
|
|
|
|
|
SettingKeyOIDCConnectUserInfoEmailPath,
|
|
|
|
|
SettingKeyOIDCConnectUserInfoIDPath,
|
|
|
|
|
SettingKeyOIDCConnectUserInfoUsernamePath,
|
|
|
|
|
}
|
|
|
|
|
settings, err := s.settingRepo.GetMultiple(ctx, keys)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return config.OIDCConnectConfig{}, fmt.Errorf("get oidc connect settings: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if raw, ok := settings[SettingKeyOIDCConnectEnabled]; ok {
|
|
|
|
|
effective.Enabled = raw == "true"
|
|
|
|
|
}
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectProviderName]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
effective.ProviderName = strings.TrimSpace(v)
|
|
|
|
|
}
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectClientID]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
effective.ClientID = strings.TrimSpace(v)
|
|
|
|
|
}
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectClientSecret]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
effective.ClientSecret = strings.TrimSpace(v)
|
|
|
|
|
}
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectIssuerURL]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
effective.IssuerURL = strings.TrimSpace(v)
|
|
|
|
|
}
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectDiscoveryURL]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
effective.DiscoveryURL = strings.TrimSpace(v)
|
|
|
|
|
}
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectAuthorizeURL]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
effective.AuthorizeURL = strings.TrimSpace(v)
|
|
|
|
|
}
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectTokenURL]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
effective.TokenURL = strings.TrimSpace(v)
|
|
|
|
|
}
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectUserInfoURL]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
effective.UserInfoURL = strings.TrimSpace(v)
|
|
|
|
|
}
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectJWKSURL]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
effective.JWKSURL = strings.TrimSpace(v)
|
|
|
|
|
}
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectScopes]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
effective.Scopes = strings.TrimSpace(v)
|
|
|
|
|
}
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectRedirectURL]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
effective.RedirectURL = strings.TrimSpace(v)
|
|
|
|
|
}
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectFrontendRedirectURL]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
effective.FrontendRedirectURL = strings.TrimSpace(v)
|
|
|
|
|
}
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectTokenAuthMethod]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
effective.TokenAuthMethod = strings.ToLower(strings.TrimSpace(v))
|
|
|
|
|
}
|
|
|
|
|
if raw, ok := settings[SettingKeyOIDCConnectUsePKCE]; ok {
|
|
|
|
|
effective.UsePKCE = raw == "true"
|
|
|
|
|
}
|
|
|
|
|
if raw, ok := settings[SettingKeyOIDCConnectValidateIDToken]; ok {
|
|
|
|
|
effective.ValidateIDToken = raw == "true"
|
|
|
|
|
}
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectAllowedSigningAlgs]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
effective.AllowedSigningAlgs = strings.TrimSpace(v)
|
|
|
|
|
}
|
|
|
|
|
if raw, ok := settings[SettingKeyOIDCConnectClockSkewSeconds]; ok && strings.TrimSpace(raw) != "" {
|
|
|
|
|
if parsed, parseErr := strconv.Atoi(strings.TrimSpace(raw)); parseErr == nil {
|
|
|
|
|
effective.ClockSkewSeconds = parsed
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if raw, ok := settings[SettingKeyOIDCConnectRequireEmailVerified]; ok {
|
|
|
|
|
effective.RequireEmailVerified = raw == "true"
|
|
|
|
|
}
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectUserInfoEmailPath]; ok {
|
|
|
|
|
effective.UserInfoEmailPath = strings.TrimSpace(v)
|
|
|
|
|
}
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectUserInfoIDPath]; ok {
|
|
|
|
|
effective.UserInfoIDPath = strings.TrimSpace(v)
|
|
|
|
|
}
|
|
|
|
|
if v, ok := settings[SettingKeyOIDCConnectUserInfoUsernamePath]; ok {
|
|
|
|
|
effective.UserInfoUsernamePath = strings.TrimSpace(v)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !effective.Enabled {
|
|
|
|
|
return config.OIDCConnectConfig{}, infraerrors.NotFound("OAUTH_DISABLED", "oauth login is disabled")
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(effective.ProviderName) == "" {
|
|
|
|
|
effective.ProviderName = "OIDC"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(effective.ClientID) == "" {
|
|
|
|
|
return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth client id not configured")
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(effective.IssuerURL) == "" {
|
|
|
|
|
return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth issuer url not configured")
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(effective.RedirectURL) == "" {
|
|
|
|
|
return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth redirect url not configured")
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(effective.FrontendRedirectURL) == "" {
|
|
|
|
|
return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth frontend redirect url not configured")
|
|
|
|
|
}
|
|
|
|
|
if !scopesContainOpenID(effective.Scopes) {
|
|
|
|
|
return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth scopes must contain openid")
|
|
|
|
|
}
|
|
|
|
|
if effective.ClockSkewSeconds < 0 || effective.ClockSkewSeconds > 600 {
|
|
|
|
|
return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth clock skew must be between 0 and 600")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := config.ValidateAbsoluteHTTPURL(effective.IssuerURL); err != nil {
|
|
|
|
|
return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth issuer url invalid")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
discoveryURL := strings.TrimSpace(effective.DiscoveryURL)
|
|
|
|
|
if discoveryURL == "" {
|
|
|
|
|
discoveryURL = oidcDefaultDiscoveryURL(effective.IssuerURL)
|
|
|
|
|
effective.DiscoveryURL = discoveryURL
|
|
|
|
|
}
|
|
|
|
|
if discoveryURL != "" {
|
|
|
|
|
if err := config.ValidateAbsoluteHTTPURL(discoveryURL); err != nil {
|
|
|
|
|
return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth discovery url invalid")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
needsDiscovery := strings.TrimSpace(effective.AuthorizeURL) == "" ||
|
|
|
|
|
strings.TrimSpace(effective.TokenURL) == "" ||
|
|
|
|
|
(effective.ValidateIDToken && strings.TrimSpace(effective.JWKSURL) == "")
|
|
|
|
|
if needsDiscovery && discoveryURL != "" {
|
|
|
|
|
metadata, resolveErr := oidcResolveProviderMetadata(ctx, discoveryURL)
|
|
|
|
|
if resolveErr != nil {
|
|
|
|
|
return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth discovery resolve failed").WithCause(resolveErr)
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(effective.AuthorizeURL) == "" {
|
|
|
|
|
effective.AuthorizeURL = strings.TrimSpace(metadata.AuthorizationEndpoint)
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(effective.TokenURL) == "" {
|
|
|
|
|
effective.TokenURL = strings.TrimSpace(metadata.TokenEndpoint)
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(effective.UserInfoURL) == "" {
|
|
|
|
|
effective.UserInfoURL = strings.TrimSpace(metadata.UserInfoEndpoint)
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(effective.JWKSURL) == "" {
|
|
|
|
|
effective.JWKSURL = strings.TrimSpace(metadata.JWKSURI)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if strings.TrimSpace(effective.AuthorizeURL) == "" {
|
|
|
|
|
return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth authorize url not configured")
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(effective.TokenURL) == "" {
|
|
|
|
|
return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth token url not configured")
|
|
|
|
|
}
|
|
|
|
|
if err := config.ValidateAbsoluteHTTPURL(effective.AuthorizeURL); err != nil {
|
|
|
|
|
return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth authorize url invalid")
|
|
|
|
|
}
|
|
|
|
|
if err := config.ValidateAbsoluteHTTPURL(effective.TokenURL); err != nil {
|
|
|
|
|
return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth token url invalid")
|
|
|
|
|
}
|
|
|
|
|
if v := strings.TrimSpace(effective.UserInfoURL); v != "" {
|
|
|
|
|
if err := config.ValidateAbsoluteHTTPURL(v); err != nil {
|
|
|
|
|
return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth userinfo url invalid")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if effective.ValidateIDToken {
|
|
|
|
|
if strings.TrimSpace(effective.JWKSURL) == "" {
|
|
|
|
|
return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth jwks url not configured")
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(effective.AllowedSigningAlgs) == "" {
|
|
|
|
|
return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth signing algs not configured")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if v := strings.TrimSpace(effective.JWKSURL); v != "" {
|
|
|
|
|
if err := config.ValidateAbsoluteHTTPURL(v); err != nil {
|
|
|
|
|
return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth jwks url invalid")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if err := config.ValidateAbsoluteHTTPURL(effective.RedirectURL); err != nil {
|
|
|
|
|
return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth redirect url invalid")
|
|
|
|
|
}
|
|
|
|
|
if err := config.ValidateFrontendRedirectURL(effective.FrontendRedirectURL); err != nil {
|
|
|
|
|
return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth frontend redirect url invalid")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
method := strings.ToLower(strings.TrimSpace(effective.TokenAuthMethod))
|
|
|
|
|
switch method {
|
|
|
|
|
case "", "client_secret_post", "client_secret_basic":
|
|
|
|
|
if strings.TrimSpace(effective.ClientSecret) == "" {
|
|
|
|
|
return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth client secret not configured")
|
|
|
|
|
}
|
|
|
|
|
case "none":
|
|
|
|
|
if !effective.UsePKCE {
|
|
|
|
|
return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth pkce must be enabled when token_auth_method=none")
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth token_auth_method invalid")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return effective, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func scopesContainOpenID(scopes string) bool {
|
|
|
|
|
for _, scope := range strings.Fields(strings.ToLower(strings.TrimSpace(scopes))) {
|
|
|
|
|
if scope == "openid" {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type oidcProviderMetadata struct {
|
|
|
|
|
AuthorizationEndpoint string `json:"authorization_endpoint"`
|
|
|
|
|
TokenEndpoint string `json:"token_endpoint"`
|
|
|
|
|
UserInfoEndpoint string `json:"userinfo_endpoint"`
|
|
|
|
|
JWKSURI string `json:"jwks_uri"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func oidcDefaultDiscoveryURL(issuerURL string) string {
|
|
|
|
|
issuerURL = strings.TrimSpace(issuerURL)
|
|
|
|
|
if issuerURL == "" {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
return strings.TrimRight(issuerURL, "/") + "/.well-known/openid-configuration"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func oidcResolveProviderMetadata(ctx context.Context, discoveryURL string) (*oidcProviderMetadata, error) {
|
|
|
|
|
discoveryURL = strings.TrimSpace(discoveryURL)
|
|
|
|
|
if discoveryURL == "" {
|
|
|
|
|
return nil, fmt.Errorf("discovery url is empty")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resp, err := req.C().
|
|
|
|
|
SetTimeout(15*time.Second).
|
|
|
|
|
R().
|
|
|
|
|
SetContext(ctx).
|
|
|
|
|
SetHeader("Accept", "application/json").
|
|
|
|
|
Get(discoveryURL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("request discovery document: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if !resp.IsSuccessState() {
|
|
|
|
|
return nil, fmt.Errorf("discovery request failed: status=%d", resp.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
metadata := &oidcProviderMetadata{}
|
|
|
|
|
if err := json.Unmarshal(resp.Bytes(), metadata); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("parse discovery document: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return metadata, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetStreamTimeoutSettings 获取流超时处理配置
|
|
|
|
|
func (s *SettingService) GetStreamTimeoutSettings(ctx context.Context) (*StreamTimeoutSettings, error) {
|
|
|
|
|
value, err := s.settingRepo.GetValue(ctx, SettingKeyStreamTimeoutSettings)
|
|
|
|
|