Files
sub2api/backend/internal/service/setting_oauth.go
T
李建琦 6d655c9903
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

1016 lines
42 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"strconv"
"strings"
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
"github.com/imroc/req/v3"
)
// CoerceDingTalkCorpPolicyForWrite 是 coerceDeprecatedDingTalkCorpPolicy 的导出版本,
// 用于 admin handler 在写入路径上对客户端直传的入参做防御性 coerce(前端 UI 虽已无 whitelist 选项,
// 但 API 可被直接调用)。
func CoerceDingTalkCorpPolicyForWrite(policy string) string {
return coerceDeprecatedDingTalkCorpPolicy(policy)
}
// coerceDeprecatedDingTalkCorpPolicy 把已废弃的 corp_restriction_policy 值替换成安全的等价值。
// 升级前残留在 DB 中的 "whitelist" 会导致 callback 链路在 default case 静默 fail-closed
// (所有钉钉登录被拒)。这里统一退化为 "none" 让服务保持可用,并 warn 日志提醒 admin 重新保存设置。
func coerceDeprecatedDingTalkCorpPolicy(policy string) string {
if policy == "whitelist" {
slog.Warn("dingtalk: corp_restriction_policy=whitelist is deprecated and unsupported, coercing to none",
"hint", "re-save DingTalk settings in admin UI to clear this warning")
return "none"
}
return policy
}
func normalizeWeChatConnectModeSetting(raw string) string {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "mp":
return "mp"
case "mobile":
return "mobile"
default:
return "open"
}
}
func defaultWeChatConnectScopeForMode(mode string) string {
switch normalizeWeChatConnectModeSetting(mode) {
case "mp":
return "snsapi_userinfo"
case "mobile":
return ""
}
return defaultWeChatConnectScopes
}
func normalizeWeChatConnectScopeSetting(raw, mode string) string {
switch normalizeWeChatConnectModeSetting(mode) {
case "mp":
switch strings.TrimSpace(raw) {
case "snsapi_base":
return "snsapi_base"
case "snsapi_userinfo":
return "snsapi_userinfo"
default:
return defaultWeChatConnectScopeForMode(mode)
}
case "mobile":
return ""
default:
return defaultWeChatConnectScopes
}
}
func parseWeChatConnectCapabilitySettings(settings map[string]string, enabled bool, mode string) (bool, bool, bool) {
mode = normalizeWeChatConnectModeSetting(mode)
rawOpen, hasOpen := settings[SettingKeyWeChatConnectOpenEnabled]
rawMP, hasMP := settings[SettingKeyWeChatConnectMPEnabled]
rawMobile, hasMobile := settings[SettingKeyWeChatConnectMobileEnabled]
openConfigured := hasOpen && strings.TrimSpace(rawOpen) != ""
mpConfigured := hasMP && strings.TrimSpace(rawMP) != ""
mobileConfigured := hasMobile && strings.TrimSpace(rawMobile) != ""
if openConfigured || mpConfigured || mobileConfigured {
openEnabled := strings.TrimSpace(rawOpen) == "true"
mpEnabled := strings.TrimSpace(rawMP) == "true"
mobileEnabled := strings.TrimSpace(rawMobile) == "true"
return openEnabled, mpEnabled, mobileEnabled
}
if !enabled {
return false, false, false
}
if mode == "mp" {
return false, true, false
}
if mode == "mobile" {
return false, false, true
}
return true, false, false
}
func normalizeWeChatConnectStoredMode(openEnabled, mpEnabled, mobileEnabled bool, mode string) string {
mode = normalizeWeChatConnectModeSetting(mode)
switch mode {
case "open":
if openEnabled {
return "open"
}
case "mp":
if mpEnabled {
return "mp"
}
case "mobile":
if mobileEnabled {
return "mobile"
}
}
switch {
case openEnabled:
return "open"
case mpEnabled:
return "mp"
case mobileEnabled:
return "mobile"
default:
return mode
}
}
func mergeWeChatConnectCapabilitySettings(settings map[string]string, base config.WeChatConnectConfig, enabled bool, mode string) (bool, bool, bool) {
mode = normalizeWeChatConnectModeSetting(firstNonEmpty(mode, base.Mode))
rawOpen, hasOpen := settings[SettingKeyWeChatConnectOpenEnabled]
rawMP, hasMP := settings[SettingKeyWeChatConnectMPEnabled]
rawMobile, hasMobile := settings[SettingKeyWeChatConnectMobileEnabled]
openConfigured := hasOpen && strings.TrimSpace(rawOpen) != ""
mpConfigured := hasMP && strings.TrimSpace(rawMP) != ""
mobileConfigured := hasMobile && strings.TrimSpace(rawMobile) != ""
if openConfigured || mpConfigured || mobileConfigured {
openEnabled := strings.TrimSpace(rawOpen) == "true"
mpEnabled := strings.TrimSpace(rawMP) == "true"
mobileEnabled := strings.TrimSpace(rawMobile) == "true"
_, enabledConfigured := settings[SettingKeyWeChatConnectEnabled]
if !enabledConfigured &&
enabled &&
!openEnabled &&
!mpEnabled &&
!mobileEnabled &&
(base.OpenEnabled || base.MPEnabled || base.MobileEnabled) {
return base.OpenEnabled, base.MPEnabled, base.MobileEnabled
}
return openEnabled, mpEnabled, mobileEnabled
}
if !enabled {
return false, false, false
}
if base.OpenEnabled || base.MPEnabled || base.MobileEnabled {
return base.OpenEnabled, base.MPEnabled, base.MobileEnabled
}
return parseWeChatConnectCapabilitySettings(settings, enabled, mode)
}
func (s *SettingService) effectiveWeChatConnectOAuthConfig(settings map[string]string) WeChatConnectOAuthConfig {
base := config.WeChatConnectConfig{}
if s != nil && s.cfg != nil {
base = s.cfg.WeChat
}
enabled := base.Enabled
if raw, ok := settings[SettingKeyWeChatConnectEnabled]; ok {
enabled = strings.TrimSpace(raw) == "true"
}
legacyAppID := strings.TrimSpace(firstNonEmpty(
settings[SettingKeyWeChatConnectAppID],
base.AppID,
base.OpenAppID,
base.MPAppID,
base.MobileAppID,
))
legacyAppSecret := strings.TrimSpace(firstNonEmpty(
settings[SettingKeyWeChatConnectAppSecret],
base.AppSecret,
base.OpenAppSecret,
base.MPAppSecret,
base.MobileAppSecret,
))
openAppID := strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectOpenAppID], base.OpenAppID, legacyAppID))
openAppSecret := strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectOpenAppSecret], base.OpenAppSecret, legacyAppSecret))
mpAppID := strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectMPAppID], base.MPAppID, legacyAppID))
mpAppSecret := strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectMPAppSecret], base.MPAppSecret, legacyAppSecret))
mobileAppID := strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectMobileAppID], base.MobileAppID, legacyAppID))
mobileAppSecret := strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectMobileAppSecret], base.MobileAppSecret, legacyAppSecret))
modeRaw := firstNonEmpty(settings[SettingKeyWeChatConnectMode], base.Mode)
openEnabled, mpEnabled, mobileEnabled := mergeWeChatConnectCapabilitySettings(settings, base, enabled, modeRaw)
mode := normalizeWeChatConnectStoredMode(openEnabled, mpEnabled, mobileEnabled, modeRaw)
return WeChatConnectOAuthConfig{
Enabled: enabled,
LegacyAppID: legacyAppID,
LegacyAppSecret: legacyAppSecret,
OpenAppID: openAppID,
OpenAppSecret: openAppSecret,
MPAppID: mpAppID,
MPAppSecret: mpAppSecret,
MobileAppID: mobileAppID,
MobileAppSecret: mobileAppSecret,
OpenEnabled: openEnabled,
MPEnabled: mpEnabled,
MobileEnabled: mobileEnabled,
Mode: mode,
Scopes: normalizeWeChatConnectScopeSetting(firstNonEmpty(settings[SettingKeyWeChatConnectScopes], base.Scopes), mode),
RedirectURL: strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectRedirectURL], base.RedirectURL)),
FrontendRedirectURL: strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectFrontendRedirectURL], base.FrontendRedirectURL, defaultWeChatConnectFrontend)),
}
}
func DefaultWeChatConnectScopesForMode(mode string) string {
return defaultWeChatConnectScopeForMode(mode)
}
func (s *SettingService) parseWeChatConnectOAuthConfig(settings map[string]string) (WeChatConnectOAuthConfig, error) {
cfg := s.effectiveWeChatConnectOAuthConfig(settings)
if !cfg.Enabled || (!cfg.OpenEnabled && !cfg.MPEnabled) {
return WeChatConnectOAuthConfig{}, infraerrors.NotFound("OAUTH_DISABLED", "wechat oauth is disabled")
}
if cfg.OpenEnabled {
if cfg.AppIDForMode("open") == "" {
return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth pc app id not configured")
}
if cfg.AppSecretForMode("open") == "" {
return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth pc app secret not configured")
}
}
if cfg.MPEnabled {
if cfg.AppIDForMode("mp") == "" {
return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth official account app id not configured")
}
if cfg.AppSecretForMode("mp") == "" {
return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth official account app secret not configured")
}
}
if cfg.MobileEnabled {
if cfg.AppIDForMode("mobile") == "" {
return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth mobile app id not configured")
}
if cfg.AppSecretForMode("mobile") == "" {
return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth mobile app secret not configured")
}
}
if v := strings.TrimSpace(cfg.RedirectURL); v != "" {
if err := config.ValidateAbsoluteHTTPURL(v); err != nil {
return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth redirect url invalid")
}
}
if err := config.ValidateFrontendRedirectURL(cfg.FrontendRedirectURL); err != nil {
return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth frontend redirect url invalid")
}
return cfg, nil
}
func (s *SettingService) weChatOAuthCapabilitiesFromSettings(settings map[string]string) (bool, bool, bool, bool) {
cfg := s.effectiveWeChatConnectOAuthConfig(settings)
if !cfg.Enabled {
return false, false, false, false
}
openReady := cfg.OpenEnabled && cfg.AppIDForMode("open") != "" && cfg.AppSecretForMode("open") != ""
mpReady := cfg.MPEnabled && cfg.AppIDForMode("mp") != "" && cfg.AppSecretForMode("mp") != ""
mobileReady := cfg.MobileEnabled && cfg.AppIDForMode("mobile") != "" && cfg.AppSecretForMode("mobile") != ""
return openReady || mpReady, openReady, mpReady, mobileReady
}
func (s *SettingService) emailOAuthBaseConfig(provider string) config.EmailOAuthProviderConfig {
switch strings.ToLower(strings.TrimSpace(provider)) {
case "github":
cfg := config.EmailOAuthProviderConfig{
AuthorizeURL: defaultGitHubOAuthAuthorize,
TokenURL: defaultGitHubOAuthToken,
UserInfoURL: defaultGitHubOAuthUserInfo,
EmailsURL: defaultGitHubOAuthEmails,
Scopes: defaultGitHubOAuthScopes,
FrontendRedirectURL: defaultGitHubOAuthFrontend,
}
if s != nil && s.cfg != nil {
cfg = mergeEmailOAuthBaseConfig(cfg, s.cfg.GitHubOAuth)
}
return cfg
case "google":
cfg := config.EmailOAuthProviderConfig{
AuthorizeURL: defaultGoogleOAuthAuthorize,
TokenURL: defaultGoogleOAuthToken,
UserInfoURL: defaultGoogleOAuthUserInfo,
Scopes: defaultGoogleOAuthScopes,
FrontendRedirectURL: defaultGoogleOAuthFrontend,
}
if s != nil && s.cfg != nil {
cfg = mergeEmailOAuthBaseConfig(cfg, s.cfg.GoogleOAuth)
}
return cfg
default:
return config.EmailOAuthProviderConfig{}
}
}
func mergeEmailOAuthBaseConfig(base, override config.EmailOAuthProviderConfig) config.EmailOAuthProviderConfig {
base.Enabled = override.Enabled
if strings.TrimSpace(override.ClientID) != "" {
base.ClientID = strings.TrimSpace(override.ClientID)
}
if strings.TrimSpace(override.ClientSecret) != "" {
base.ClientSecret = strings.TrimSpace(override.ClientSecret)
}
if strings.TrimSpace(override.AuthorizeURL) != "" {
base.AuthorizeURL = strings.TrimSpace(override.AuthorizeURL)
}
if strings.TrimSpace(override.TokenURL) != "" {
base.TokenURL = strings.TrimSpace(override.TokenURL)
}
if strings.TrimSpace(override.UserInfoURL) != "" {
base.UserInfoURL = strings.TrimSpace(override.UserInfoURL)
}
if strings.TrimSpace(override.EmailsURL) != "" {
base.EmailsURL = strings.TrimSpace(override.EmailsURL)
}
if strings.TrimSpace(override.Scopes) != "" {
base.Scopes = strings.TrimSpace(override.Scopes)
}
if strings.TrimSpace(override.RedirectURL) != "" {
base.RedirectURL = strings.TrimSpace(override.RedirectURL)
}
if strings.TrimSpace(override.FrontendRedirectURL) != "" {
base.FrontendRedirectURL = strings.TrimSpace(override.FrontendRedirectURL)
}
return base
}
func (s *SettingService) emailOAuthPublicEnabled(settings map[string]string, provider string) bool {
cfg := s.effectiveEmailOAuthConfig(settings, provider)
return cfg.Enabled && strings.TrimSpace(cfg.ClientID) != "" && strings.TrimSpace(cfg.ClientSecret) != ""
}
func (s *SettingService) effectiveEmailOAuthConfig(settings map[string]string, provider string) config.EmailOAuthProviderConfig {
cfg := s.emailOAuthBaseConfig(provider)
switch strings.ToLower(strings.TrimSpace(provider)) {
case "github":
if raw, ok := settings[SettingKeyGitHubOAuthEnabled]; ok {
cfg.Enabled = raw == "true"
}
cfg.ClientID = firstNonEmpty(settings[SettingKeyGitHubOAuthClientID], cfg.ClientID)
cfg.ClientSecret = firstNonEmpty(settings[SettingKeyGitHubOAuthClientSecret], cfg.ClientSecret)
cfg.RedirectURL = firstNonEmpty(settings[SettingKeyGitHubOAuthRedirectURL], cfg.RedirectURL)
cfg.FrontendRedirectURL = firstNonEmpty(settings[SettingKeyGitHubOAuthFrontendRedirectURL], cfg.FrontendRedirectURL, defaultGitHubOAuthFrontend)
case "google":
if raw, ok := settings[SettingKeyGoogleOAuthEnabled]; ok {
cfg.Enabled = raw == "true"
}
cfg.ClientID = firstNonEmpty(settings[SettingKeyGoogleOAuthClientID], cfg.ClientID)
cfg.ClientSecret = firstNonEmpty(settings[SettingKeyGoogleOAuthClientSecret], cfg.ClientSecret)
cfg.RedirectURL = firstNonEmpty(settings[SettingKeyGoogleOAuthRedirectURL], cfg.RedirectURL)
cfg.FrontendRedirectURL = firstNonEmpty(settings[SettingKeyGoogleOAuthFrontendRedirectURL], cfg.FrontendRedirectURL, defaultGoogleOAuthFrontend)
}
return cfg
}
func oidcUsePKCECompatibilityDefault(base config.OIDCConnectConfig) bool {
if base.UsePKCEExplicit {
return base.UsePKCE
}
return true
}
func oidcValidateIDTokenCompatibilityDefault(base config.OIDCConnectConfig) bool {
if base.ValidateIDTokenExplicit {
return base.ValidateIDToken
}
return true
}
func oidcCompatibilityWriteDefault(base config.OIDCConnectConfig, configured bool, raw string, explicit bool, explicitValue bool) bool {
if configured {
return strings.TrimSpace(raw) == "true"
}
if explicit {
return explicitValue
}
return false
}
func (s *SettingService) OIDCSecurityWriteDefaults(ctx context.Context) (bool, bool, error) {
rawSettings, err := s.settingRepo.GetMultiple(ctx, []string{
SettingKeyOIDCConnectUsePKCE,
SettingKeyOIDCConnectValidateIDToken,
})
if err != nil {
return false, false, fmt.Errorf("get oidc security write defaults: %w", err)
}
base := config.OIDCConnectConfig{}
if s != nil && s.cfg != nil {
base = s.cfg.OIDC
}
rawUsePKCE, hasUsePKCE := rawSettings[SettingKeyOIDCConnectUsePKCE]
rawValidateIDToken, hasValidateIDToken := rawSettings[SettingKeyOIDCConnectValidateIDToken]
return oidcCompatibilityWriteDefault(base, hasUsePKCE, rawUsePKCE, base.UsePKCEExplicit, base.UsePKCE),
oidcCompatibilityWriteDefault(base, hasValidateIDToken, rawValidateIDToken, base.ValidateIDTokenExplicit, base.ValidateIDToken),
nil
}
func (s *SettingService) GetEmailOAuthProviderConfig(ctx context.Context, provider string) (config.EmailOAuthProviderConfig, error) {
provider = strings.ToLower(strings.TrimSpace(provider))
if provider != "github" && provider != "google" {
return config.EmailOAuthProviderConfig{}, infraerrors.NotFound("OAUTH_PROVIDER_NOT_FOUND", "oauth provider not found")
}
keys := []string{
SettingKeyGitHubOAuthEnabled,
SettingKeyGitHubOAuthClientID,
SettingKeyGitHubOAuthClientSecret,
SettingKeyGitHubOAuthRedirectURL,
SettingKeyGitHubOAuthFrontendRedirectURL,
SettingKeyGoogleOAuthEnabled,
SettingKeyGoogleOAuthClientID,
SettingKeyGoogleOAuthClientSecret,
SettingKeyGoogleOAuthRedirectURL,
SettingKeyGoogleOAuthFrontendRedirectURL,
}
settings, err := s.settingRepo.GetMultiple(ctx, keys)
if err != nil {
return config.EmailOAuthProviderConfig{}, fmt.Errorf("get email oauth settings: %w", err)
}
cfg := s.effectiveEmailOAuthConfig(settings, provider)
if !cfg.Enabled {
return config.EmailOAuthProviderConfig{}, infraerrors.NotFound("OAUTH_DISABLED", "oauth login is disabled")
}
if strings.TrimSpace(cfg.ClientID) == "" {
return config.EmailOAuthProviderConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth client id not configured")
}
if strings.TrimSpace(cfg.ClientSecret) == "" {
return config.EmailOAuthProviderConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth client secret not configured")
}
for label, rawURL := range map[string]string{
"authorize": cfg.AuthorizeURL,
"token": cfg.TokenURL,
"userinfo": cfg.UserInfoURL,
"redirect": cfg.RedirectURL,
} {
if strings.TrimSpace(rawURL) == "" {
return config.EmailOAuthProviderConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth "+label+" url not configured")
}
if err := config.ValidateAbsoluteHTTPURL(rawURL); err != nil {
return config.EmailOAuthProviderConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth "+label+" url invalid")
}
}
if strings.TrimSpace(cfg.EmailsURL) != "" {
if err := config.ValidateAbsoluteHTTPURL(cfg.EmailsURL); err != nil {
return config.EmailOAuthProviderConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth emails url invalid")
}
}
if err := config.ValidateFrontendRedirectURL(cfg.FrontendRedirectURL); err != nil {
return config.EmailOAuthProviderConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth frontend redirect url invalid")
}
return cfg, nil
}
// GetLinuxDoConnectOAuthConfig 返回用于登录的"最终生效" LinuxDo Connect 配置。
//
// 优先级:
// - 若对应系统设置键存在,则覆盖 config.yaml/env 的值
// - 否则回退到 config.yaml/env 的值
func (s *SettingService) GetLinuxDoConnectOAuthConfig(ctx context.Context) (config.LinuxDoConnectConfig, error) {
if s == nil || s.cfg == nil {
return config.LinuxDoConnectConfig{}, infraerrors.ServiceUnavailable("CONFIG_NOT_READY", "config not loaded")
}
effective := s.cfg.LinuxDo
keys := []string{
SettingKeyLinuxDoConnectEnabled,
SettingKeyLinuxDoConnectClientID,
SettingKeyLinuxDoConnectClientSecret,
SettingKeyLinuxDoConnectRedirectURL,
}
settings, err := s.settingRepo.GetMultiple(ctx, keys)
if err != nil {
return config.LinuxDoConnectConfig{}, fmt.Errorf("get linuxdo connect settings: %w", err)
}
if raw, ok := settings[SettingKeyLinuxDoConnectEnabled]; ok {
effective.Enabled = raw == "true"
}
if v, ok := settings[SettingKeyLinuxDoConnectClientID]; ok && strings.TrimSpace(v) != "" {
effective.ClientID = strings.TrimSpace(v)
}
if v, ok := settings[SettingKeyLinuxDoConnectClientSecret]; ok && strings.TrimSpace(v) != "" {
effective.ClientSecret = strings.TrimSpace(v)
}
if v, ok := settings[SettingKeyLinuxDoConnectRedirectURL]; ok && strings.TrimSpace(v) != "" {
effective.RedirectURL = strings.TrimSpace(v)
}
if !effective.Enabled {
return config.LinuxDoConnectConfig{}, infraerrors.NotFound("OAUTH_DISABLED", "oauth login is disabled")
}
// 基础健壮性校验(避免把用户重定向到一个必然失败或不安全的 OAuth 流程里)。
if strings.TrimSpace(effective.ClientID) == "" {
return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth client id not configured")
}
if strings.TrimSpace(effective.AuthorizeURL) == "" {
return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth authorize url not configured")
}
if strings.TrimSpace(effective.TokenURL) == "" {
return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth token url not configured")
}
if strings.TrimSpace(effective.UserInfoURL) == "" {
return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth userinfo url not configured")
}
if strings.TrimSpace(effective.RedirectURL) == "" {
return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth redirect url not configured")
}
if strings.TrimSpace(effective.FrontendRedirectURL) == "" {
return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth frontend redirect url not configured")
}
if err := config.ValidateAbsoluteHTTPURL(effective.AuthorizeURL); err != nil {
return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth authorize url invalid")
}
if err := config.ValidateAbsoluteHTTPURL(effective.TokenURL); err != nil {
return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth token url invalid")
}
if err := config.ValidateAbsoluteHTTPURL(effective.UserInfoURL); err != nil {
return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth userinfo url invalid")
}
if err := config.ValidateAbsoluteHTTPURL(effective.RedirectURL); err != nil {
return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth redirect url invalid")
}
if err := config.ValidateFrontendRedirectURL(effective.FrontendRedirectURL); err != nil {
return config.LinuxDoConnectConfig{}, 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.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth client secret not configured")
}
case "none":
default:
return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth token_auth_method invalid")
}
return effective, nil
}
// GetDingTalkConnectOAuthConfig 返回用于登录的"最终生效" DingTalk Connect 配置。
//
// 优先级:
// - 若对应系统设置键存在,则覆盖 config.yaml/env 的值
// - 否则回退到 config.yaml/env 的值
func (s *SettingService) GetDingTalkConnectOAuthConfig(ctx context.Context) (config.DingTalkConnectConfig, error) {
if s == nil || s.cfg == nil {
return config.DingTalkConnectConfig{}, infraerrors.ServiceUnavailable("CONFIG_NOT_READY", "config not loaded")
}
effective := s.cfg.DingTalk
keys := []string{
SettingKeyDingTalkConnectEnabled,
SettingKeyDingTalkConnectClientID,
SettingKeyDingTalkConnectClientSecret,
SettingKeyDingTalkConnectRedirectURL,
SettingKeyDingTalkConnectCorpRestrictionPolicy,
SettingKeyDingTalkConnectInternalCorpID,
SettingKeyDingTalkConnectBypassRegistration,
SettingKeyDingTalkConnectSyncCorpEmail,
SettingKeyDingTalkConnectSyncDisplayName,
SettingKeyDingTalkConnectSyncDept,
SettingKeyDingTalkConnectSyncCorpEmailAttrKey,
SettingKeyDingTalkConnectSyncDisplayNameAttrKey,
SettingKeyDingTalkConnectSyncDeptAttrKey,
}
settings, err := s.settingRepo.GetMultiple(ctx, keys)
if err != nil {
return config.DingTalkConnectConfig{}, fmt.Errorf("get dingtalk connect settings: %w", err)
}
if raw, ok := settings[SettingKeyDingTalkConnectEnabled]; ok {
effective.Enabled = raw == "true"
}
if v, ok := settings[SettingKeyDingTalkConnectClientID]; ok && strings.TrimSpace(v) != "" {
effective.ClientID = strings.TrimSpace(v)
}
if v, ok := settings[SettingKeyDingTalkConnectClientSecret]; ok && strings.TrimSpace(v) != "" {
effective.ClientSecret = strings.TrimSpace(v)
}
if v, ok := settings[SettingKeyDingTalkConnectRedirectURL]; ok && strings.TrimSpace(v) != "" {
effective.RedirectURL = strings.TrimSpace(v)
}
if v, ok := settings[SettingKeyDingTalkConnectCorpRestrictionPolicy]; ok && strings.TrimSpace(v) != "" {
effective.CorpRestrictionPolicy = strings.TrimSpace(v)
}
effective.CorpRestrictionPolicy = coerceDeprecatedDingTalkCorpPolicy(effective.CorpRestrictionPolicy)
if v, ok := settings[SettingKeyDingTalkConnectInternalCorpID]; ok && strings.TrimSpace(v) != "" {
effective.InternalCorpID = strings.TrimSpace(v)
}
if v, ok := settings[SettingKeyDingTalkConnectBypassRegistration]; ok && strings.TrimSpace(v) != "" {
effective.BypassRegistration = strings.EqualFold(strings.TrimSpace(v), "true")
}
// bypass_registration 仅在 internal_only 模式下有意义;其它策略下强制 false,
// 以保证 OAuth callback 看到的 effective config 永远是一致状态。
if effective.CorpRestrictionPolicy != "internal_only" {
effective.BypassRegistration = false
}
if v, ok := settings[SettingKeyDingTalkConnectSyncCorpEmail]; ok && strings.TrimSpace(v) != "" {
effective.SyncCorpEmail = strings.EqualFold(strings.TrimSpace(v), "true")
}
if v, ok := settings[SettingKeyDingTalkConnectSyncDisplayName]; ok && strings.TrimSpace(v) != "" {
effective.SyncDisplayName = strings.EqualFold(strings.TrimSpace(v), "true")
}
if v, ok := settings[SettingKeyDingTalkConnectSyncDept]; ok && strings.TrimSpace(v) != "" {
effective.SyncDept = strings.EqualFold(strings.TrimSpace(v), "true")
}
// 身份同步三开关仅在 internal_only 模式下有意义;其它策略强制 false。
if effective.CorpRestrictionPolicy != "internal_only" {
effective.SyncCorpEmail = false
effective.SyncDisplayName = false
effective.SyncDept = false
}
// 身份同步目标 attr keyDB 空 → fallback 默认值)
if v := strings.TrimSpace(settings[SettingKeyDingTalkConnectSyncCorpEmailAttrKey]); v != "" {
effective.SyncCorpEmailAttrKey = v
}
if effective.SyncCorpEmailAttrKey == "" {
effective.SyncCorpEmailAttrKey = "dingtalk_email"
}
if v := strings.TrimSpace(settings[SettingKeyDingTalkConnectSyncDisplayNameAttrKey]); v != "" {
effective.SyncDisplayNameAttrKey = v
}
if effective.SyncDisplayNameAttrKey == "" {
effective.SyncDisplayNameAttrKey = "dingtalk_name"
}
if v := strings.TrimSpace(settings[SettingKeyDingTalkConnectSyncDeptAttrKey]); v != "" {
effective.SyncDeptAttrKey = v
}
if effective.SyncDeptAttrKey == "" {
effective.SyncDeptAttrKey = "dingtalk_department"
}
if !effective.Enabled {
return config.DingTalkConnectConfig{}, infraerrors.NotFound("OAUTH_DISABLED", "dingtalk oauth login is disabled")
}
// 基础健壮性校验(避免把用户重定向到一个必然失败或不安全的 OAuth 流程里)。
if strings.TrimSpace(effective.ClientID) == "" {
return config.DingTalkConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "dingtalk oauth client id not configured")
}
if strings.TrimSpace(effective.AuthorizeURL) == "" {
return config.DingTalkConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "dingtalk oauth authorize url not configured")
}
if strings.TrimSpace(effective.TokenURL) == "" {
return config.DingTalkConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "dingtalk oauth token url not configured")
}
if strings.TrimSpace(effective.UserInfoURL) == "" {
return config.DingTalkConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "dingtalk oauth userinfo url not configured")
}
if strings.TrimSpace(effective.RedirectURL) == "" {
return config.DingTalkConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "dingtalk oauth redirect url not configured")
}
if strings.TrimSpace(effective.FrontendRedirectURL) == "" {
return config.DingTalkConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "dingtalk oauth frontend redirect url not configured")
}
if err := config.ValidateAbsoluteHTTPURL(effective.AuthorizeURL); err != nil {
return config.DingTalkConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "dingtalk oauth authorize url invalid")
}
if err := config.ValidateAbsoluteHTTPURL(effective.TokenURL); err != nil {
return config.DingTalkConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "dingtalk oauth token url invalid")
}
if err := config.ValidateAbsoluteHTTPURL(effective.UserInfoURL); err != nil {
return config.DingTalkConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "dingtalk oauth userinfo url invalid")
}
if err := config.ValidateAbsoluteHTTPURL(effective.RedirectURL); err != nil {
return config.DingTalkConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "dingtalk oauth redirect url invalid")
}
if err := config.ValidateFrontendRedirectURL(effective.FrontendRedirectURL); err != nil {
return config.DingTalkConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "dingtalk oauth frontend redirect url invalid")
}
if strings.TrimSpace(effective.ClientSecret) == "" {
return config.DingTalkConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "dingtalk oauth client secret not configured")
}
// 镜像 admin handler 行为:internal_only policy 隐式要求 AppType=internal
if effective.CorpRestrictionPolicy == "internal_only" {
effective.AppType = "internal"
}
if err := config.ValidateDingTalkConfig(effective); err != nil {
return config.DingTalkConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", err.Error())
}
return effective, nil
}
// GetWeChatConnectOAuthConfig 返回用于登录的最终生效 WeChat Connect 配置。
//
// WeChat Connect 已回归 DB 系统设置模型,不再回退到 config/env。
func (s *SettingService) GetWeChatConnectOAuthConfig(ctx context.Context) (WeChatConnectOAuthConfig, error) {
keys := []string{
SettingKeyWeChatConnectEnabled,
SettingKeyWeChatConnectAppID,
SettingKeyWeChatConnectAppSecret,
SettingKeyWeChatConnectOpenAppID,
SettingKeyWeChatConnectOpenAppSecret,
SettingKeyWeChatConnectMPAppID,
SettingKeyWeChatConnectMPAppSecret,
SettingKeyWeChatConnectMobileAppID,
SettingKeyWeChatConnectMobileAppSecret,
SettingKeyWeChatConnectOpenEnabled,
SettingKeyWeChatConnectMPEnabled,
SettingKeyWeChatConnectMobileEnabled,
SettingKeyWeChatConnectMode,
SettingKeyWeChatConnectScopes,
SettingKeyWeChatConnectRedirectURL,
SettingKeyWeChatConnectFrontendRedirectURL,
}
settings, err := s.settingRepo.GetMultiple(ctx, keys)
if err != nil {
return WeChatConnectOAuthConfig{}, fmt.Errorf("get wechat connect settings: %w", err)
}
return s.parseWeChatConnectOAuthConfig(settings)
}
// 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"
} else {
effective.UsePKCE = oidcUsePKCECompatibilityDefault(effective)
}
if raw, ok := settings[SettingKeyOIDCConnectValidateIDToken]; ok {
effective.ValidateIDToken = raw == "true"
} else {
effective.ValidateIDToken = oidcValidateIDTokenCompatibilityDefault(effective)
}
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":
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
}